Skip to content

Commit 585ed30

Browse files
committed
Add disallowStringConstructor option to lint rule
This commit adds a new option to the react-internal/safe-string-coercion lint rule that controls whether `String(value)` should be disallowed. ESLint is now configured set up to disallow `String(value)` in perf-sensitive production code files, and to allow it elsewhere. Although this option is enabled for prod code, error-handling code should use `String()` for safety when building error messages and call stacks. There were 5 lines where `String()` was used for prod error handling. Tthis commit disables the lint rule for these lines.
1 parent 59ff83e commit 585ed30

File tree

6 files changed

+83
-4
lines changed

6 files changed

+83
-4
lines changed

.eslintrc.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,10 @@ module.exports = {
108108
// CUSTOM RULES
109109
// the second argument of warning/invariant should be a literal string
110110
'react-internal/no-primitive-constructors': ERROR,
111-
'react-internal/safe-string-coercion': ERROR,
111+
'react-internal/safe-string-coercion': [
112+
ERROR,
113+
{disallowStringConstructor: true},
114+
],
112115
'react-internal/no-to-warn-dev-within-to-throw': ERROR,
113116
'react-internal/invariant-args': ERROR,
114117
'react-internal/warning-args': ERROR,
@@ -169,10 +172,17 @@ module.exports = {
169172
'packages/*/npm/**/*.js',
170173
'packages/dom-event-testing-library/**/*.js',
171174
'packages/react-devtools*/**/*.js',
175+
'dangerfile.js',
176+
'fixtures',
177+
'packages/react-dom/src/test-utils/*.js',
172178
],
173179
rules: {
174180
'react-internal/no-production-logging': OFF,
175181
'react-internal/warning-args': OFF,
182+
'react-internal/safe-string-coercion': [
183+
ERROR,
184+
{disallowStringConstructor: false},
185+
],
176186

177187
// Disable accessibility checks
178188
'jsx-a11y/aria-role': OFF,

packages/react-server/src/ReactFlightServer.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ function describeValueForErrorMessage(value: ReactModel): string {
296296
case 'function':
297297
return 'function';
298298
default:
299-
// eslint-disable-next-line
299+
// eslint-disable-next-line react-internal/safe-string-coercion
300300
return String(value);
301301
}
302302
}
@@ -616,7 +616,9 @@ function emitErrorChunk(request: Request, id: number, error: mixed): void {
616616
let stack = '';
617617
try {
618618
if (error instanceof Error) {
619+
// eslint-disable-next-line react-internal/safe-string-coercion
619620
message = String(error.message);
621+
// eslint-disable-next-line react-internal/safe-string-coercion
620622
stack = String(error.stack);
621623
} else {
622624
message = 'Error: ' + (error: any);

packages/react/src/ReactChildren.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ function mapIntoArray(
202202
);
203203
}
204204
} else if (type === 'object') {
205+
// eslint-disable-next-line react-internal/safe-string-coercion
205206
const childrenString = String((children: any));
206207
invariant(
207208
false,

packages/shared/consoleWithStackDev.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ function printWarning(level, format, args) {
4747
args = args.concat([stack]);
4848
}
4949

50+
// eslint-disable-next-line react-internal/safe-string-coercion
5051
const argsWithFormat = args.map(item => String(item));
5152
// Careful: RN currently depends on this prefix
5253
argsWithFormat.unshift('Warning: ' + format);

scripts/eslint-rules/__tests__/safe-string-coercion-test.internal.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,14 @@ const {RuleTester} = require('eslint');
1414
const ruleTester = new RuleTester();
1515

1616
ruleTester.run('eslint-rules/safe-string-coercion', rule, {
17-
valid: ['String(obj)', "'a' + obj"],
17+
valid: [
18+
{
19+
code: 'String(obj)',
20+
options: [{disallowStringConstructor: false}],
21+
},
22+
'String(obj)',
23+
"'a' + obj",
24+
],
1825
invalid: [
1926
{
2027
code: "'' + obj",
@@ -40,5 +47,18 @@ ruleTester.run('eslint-rules/safe-string-coercion', rule, {
4047
},
4148
],
4249
},
50+
{
51+
code: 'String(obj)',
52+
options: [{disallowStringConstructor: true}],
53+
errors: [
54+
{
55+
message:
56+
"For perf-sensitive coercion, avoid `String(value)`. Instead, use `'' + value`." +
57+
' Precede it with a DEV check from shared/CheckStringCoercion' +
58+
' unless Symbol and Temporal.* values are impossible.' +
59+
' For non-prod code and prod error handling, use `String(value)` and disable this rule.',
60+
},
61+
],
62+
},
4363
],
4464
});

scripts/eslint-rules/safe-string-coercion.js

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,41 @@ function isEmptyLiteral(node) {
1717
);
1818
}
1919

20+
// Symbols and Temporal.* objects will throw when using `'' + value`, but that
21+
// pattern can be faster than `String(value)` because JS engines can optimize
22+
// `+` better in some cases. Therefore, in perf-sensitive production codepaths
23+
// we require using `'' + value` for string coercion. The only exception is prod
24+
// error handling code, because it's bad to crash while assembling an error
25+
// message or call stack! Also, error-handling code isn't usually perf-critical.
26+
//
27+
// Non-production codepaths (tests, devtools extension, build tools, etc.)
28+
// should use `String(value)` because it will never crash and the (small) perf
29+
// difference doesn't matter enough for non-prod use cases.
30+
//
31+
// This rule assists enforcing these guidelines:
32+
// * `'' + value` is flagged with a message to remind developers to add a DEV
33+
// check from shared/CheckStringCoercion.js to make sure that the user gets a
34+
// clear error message in DEV is the coercion will throw. These checks are not
35+
// needed if throwing is not possible, e.g. if the value is already known to
36+
// be a string or number.
37+
// * `String(value)` is flagged only if the `disallowStringConstructor` option
38+
// is set. Set this option for prod code files, and don't set it for non-prod
39+
// files.
40+
2041
module.exports = {
2142
meta: {
22-
schema: [],
43+
schema: [
44+
{
45+
type: 'object',
46+
properties: {
47+
disallowStringConstructor: {
48+
type: 'boolean',
49+
default: false,
50+
},
51+
},
52+
additionalProperties: false,
53+
},
54+
],
2355
},
2456
create(context) {
2557
return {
@@ -38,6 +70,19 @@ module.exports = {
3870
return;
3971
}
4072
},
73+
CallExpression: function(node) {
74+
const disallowStringConstructor =
75+
context.options[0] && context.options[0].disallowStringConstructor;
76+
if (disallowStringConstructor && node.callee.name === 'String') {
77+
context.report(
78+
node,
79+
"For perf-sensitive coercion, avoid `String(value)`. Instead, use `'' + value`." +
80+
' Precede it with a DEV check from shared/CheckStringCoercion' +
81+
' unless Symbol and Temporal.* values are impossible.' +
82+
' For non-prod code and prod error handling, use `String(value)` and disable this rule.'
83+
);
84+
}
85+
},
4186
};
4287
},
4388
};

0 commit comments

Comments
 (0)