Skip to content

Commit 13af118

Browse files
committed
Lint rule to detect unminified errors
Adds a lint rule that detects when an Error constructor is used without a corresponding production error code. We already have this for `invariant`, but not for regular errors, i.e. `throw new Error(msg)`. There's also nothing that enforces the use of `invariant` besides convention. There are some packages where we don't care to minify errors. These are packages that run in environments where bundle size is not a concern, like react-pg. I added an override in the ESLint config to ignore these.
1 parent d0b70c9 commit 13af118

File tree

4 files changed

+188
-1
lines changed

4 files changed

+188
-1
lines changed

.eslintrc.js

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,36 @@ module.exports = {
123123
},
124124

125125
overrides: [
126+
{
127+
// By default, anything error message that appears the packages directory
128+
// must have a corresponding error code. The exceptions are defined
129+
// in the next override entry.
130+
files: ['packages/react-dom/**/*.js'],
131+
rules: {
132+
'react-internal/prod-error-codes': ERROR,
133+
},
134+
},
135+
{
136+
// These are files where it's OK to have unminified error messages. These
137+
// are environments where bundle size isn't a concern, like tests
138+
// or Node.
139+
files: [
140+
'packages/react-dom/src/test-utils/**/*.js',
141+
'packages/react-devtools-shared/**/*.js',
142+
'packages/react-noop-renderer/**/*.js',
143+
'packages/react-pg/**/*.js',
144+
'packages/react-fs/**/*.js',
145+
'packages/react-refresh/**/*.js',
146+
'packages/react-server-dom-webpack/**/*.js',
147+
'packages/react-test-renderer/**/*.js',
148+
'packages/react-native-renderer/**/*.js',
149+
'packages/**/__tests__/*.js',
150+
'packages/**/npm/*.js',
151+
],
152+
rules: {
153+
'react-internal/prod-error-codes': OFF,
154+
},
155+
},
126156
{
127157
// We apply these settings to files that we ship through npm.
128158
// They must be ES5.
@@ -185,7 +215,7 @@ module.exports = {
185215
{
186216
files: [
187217
'scripts/eslint-rules/*.js',
188-
'packages/eslint-plugin-react-hooks/src/*.js'
218+
'packages/eslint-plugin-react-hooks/src/*.js',
189219
],
190220
plugins: ['eslint-plugin'],
191221
rules: {
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @emails react-core
8+
*/
9+
10+
'use strict';
11+
12+
const rule = require('../prod-error-codes');
13+
const {RuleTester} = require('eslint');
14+
const ruleTester = new RuleTester({
15+
parserOptions: {
16+
ecmaVersion: 2017,
17+
},
18+
});
19+
20+
ruleTester.run('eslint-rules/prod-error-codes', rule, {
21+
valid: [
22+
'arbitraryFunction(a, b)',
23+
'Error(`Expected ${foo} target to be an array; got ${bar}`)',
24+
"Error('Expected ' + foo + ' target to be an array; got ' + bar)",
25+
'Error(`Expected ${foo} target to ` + `be an array; got ${bar}`)',
26+
],
27+
invalid: [
28+
{
29+
code: "Error('Not in error map')",
30+
errors: [
31+
{
32+
message:
33+
'Error message does not have a corresponding production error ' +
34+
'code. Add the following message to codes.json so it can be stripped from ' +
35+
'the production builds:\n\n' +
36+
'Not in error map',
37+
},
38+
],
39+
},
40+
{
41+
code: "Error('Not in ' + 'error map')",
42+
errors: [
43+
{
44+
message:
45+
'Error message does not have a corresponding production error ' +
46+
'code. Add the following message to codes.json so it can be stripped from ' +
47+
'the production builds:\n\n' +
48+
'Not in error map',
49+
},
50+
],
51+
},
52+
{
53+
code: 'Error(`Not in ` + `error map`)',
54+
errors: [
55+
{
56+
message:
57+
'Error message does not have a corresponding production error ' +
58+
'code. Add the following message to codes.json so it can be stripped from ' +
59+
'the production builds:\n\n' +
60+
'Not in error map',
61+
},
62+
],
63+
},
64+
{
65+
code: "Error(`Not in ${'error'} map`)",
66+
errors: [
67+
{
68+
message:
69+
'Error message does not have a corresponding production error ' +
70+
'code. Add the following message to codes.json so it can be stripped from ' +
71+
'the production builds:\n\n' +
72+
'Not in %s map',
73+
},
74+
],
75+
},
76+
],
77+
});

scripts/eslint-rules/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ module.exports = {
66
'no-to-warn-dev-within-to-throw': require('./no-to-warn-dev-within-to-throw'),
77
'warning-args': require('./warning-args'),
88
'invariant-args': require('./invariant-args'),
9+
'prod-error-codes': require('./prod-error-codes'),
910
'no-production-logging': require('./no-production-logging'),
1011
'no-cross-fork-imports': require('./no-cross-fork-imports'),
1112
'no-cross-fork-types': require('./no-cross-fork-types'),
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @emails react-core
8+
*/
9+
10+
'use strict';
11+
12+
const fs = require('fs');
13+
const path = require('path');
14+
const errorMap = JSON.parse(
15+
fs.readFileSync(path.resolve(__dirname, '../error-codes/codes.json'))
16+
);
17+
const errorMessages = new Set();
18+
Object.keys(errorMap).forEach(key => errorMessages.add(errorMap[key]));
19+
20+
function nodeToErrorTemplate(node) {
21+
if (node.type === 'Literal' && typeof node.value === 'string') {
22+
return node.value;
23+
} else if (node.type === 'BinaryExpression' && node.operator === '+') {
24+
const l = nodeToErrorTemplate(node.left);
25+
const r = nodeToErrorTemplate(node.right);
26+
return l + r;
27+
} else if (node.type === 'TemplateLiteral') {
28+
let elements = [];
29+
for (let i = 0; i < node.quasis.length; i++) {
30+
const elementNode = node.quasis[i];
31+
if (elementNode.type !== 'TemplateElement') {
32+
throw new Error('Unsupported type ' + node.type);
33+
}
34+
elements.push(elementNode.value.raw);
35+
}
36+
return elements.join('%s');
37+
} else {
38+
return '%s';
39+
}
40+
}
41+
42+
module.exports = {
43+
meta: {
44+
schema: [],
45+
},
46+
create(context) {
47+
function ErrorCallExpression(node) {
48+
const errorMessageNode = node.arguments[0];
49+
if (errorMessageNode === undefined) {
50+
return;
51+
}
52+
const errorMessage = nodeToErrorTemplate(errorMessageNode);
53+
if (errorMessages.has(errorMessage)) {
54+
return;
55+
}
56+
context.report({
57+
node,
58+
message:
59+
'Error message does not have a corresponding production error code. Add ' +
60+
'the following message to codes.json so it can be stripped ' +
61+
'from the production builds:\n\n' +
62+
errorMessage,
63+
});
64+
}
65+
66+
return {
67+
NewExpression(node) {
68+
if (node.callee.type === 'Identifier' && node.callee.name === 'Error') {
69+
ErrorCallExpression(node);
70+
}
71+
},
72+
CallExpression(node) {
73+
if (node.callee.type === 'Identifier' && node.callee.name === 'Error') {
74+
ErrorCallExpression(node);
75+
}
76+
},
77+
};
78+
},
79+
};

0 commit comments

Comments
 (0)