Skip to content

Commit 6babfc7

Browse files
JS-1608 Port TS6 rule FP fixes from #6626 (#6838)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 8c307bc commit 6babfc7

15 files changed

Lines changed: 249 additions & 29 deletions

File tree

its/ruling/src/test/expected/TypeScript/javascript-S3403.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
"TypeScript:lib/tsc.js": [
33
935,
44
4077,
5-
6973,
65
36581,
76
36592,
87
38926,

its/ruling/src/test/expected/ace/javascript-S1154.json

Lines changed: 0 additions & 5 deletions
This file was deleted.

its/ruling/src/test/expected/es5-shim/javascript-S1154.json

Lines changed: 0 additions & 6 deletions
This file was deleted.

its/ruling/src/test/expected/paper.js/javascript-S1154.json

Lines changed: 0 additions & 5 deletions
This file was deleted.

its/ruling/src/test/expected/react-cloud-music/javascript-S3403.json

Lines changed: 0 additions & 5 deletions
This file was deleted.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"ruling": "tsx --tsconfig packages/tsconfig.test.json --test packages/ruling/projects/*.ruling.test.ts",
1313
"sync-rspec": "tsx tools/sync-rspec.ts",
1414
"sync-rspec-all": "npm run sync-rspec -- --language javascript && npm run sync-rspec -- --language css && npm run deploy-rule-data",
15-
"ruling-sync": "rsync -avh packages/ruling/actual/ its/ruling/src/test/expected/ --delete",
15+
"ruling-sync": "node tools/ruling-sync.js",
1616
"bridge:compile:ts5": "tsc -b packages && tsx tools/generate-proto.ts --copy",
1717
"bridge:compile": "tsgo -p packages/tsconfig.app.json && tsgo -p packages/tsconfig.test.json && tsx tools/generate-proto.ts --copy",
1818
"bridge:test": "tsx --tsconfig packages/tsconfig.test.json --test --test-concurrency=4 --test-reporter=spec --test-reporter-destination stdout \"packages/**/src/*/rules/*[!node_modules]/**/*.test.ts\" \"packages/**/tests/**/*.test.ts\"",

packages/analysis/src/jsts/rules/S1154/rule.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,34 @@ export const rule: Rule.RuleModule = {
5050
return variable;
5151
}
5252

53+
function isFunctionType(arg: estree.Expression | estree.SpreadElement | undefined) {
54+
if (!arg || arg.type === 'SpreadElement') {
55+
return false;
56+
}
57+
return getTypeFromTreeNode(arg, services).getCallSignatures().length > 0;
58+
}
59+
60+
function isCallbackBasedReplacement(
61+
property: estree.MemberExpression['property'],
62+
args: Array<estree.Expression | estree.SpreadElement>,
63+
) {
64+
if (property.type !== 'Identifier' || !['replace', 'replaceAll'].includes(property.name)) {
65+
return false;
66+
}
67+
const replacementArg = args[1];
68+
return isFunctionLike(replacementArg) || isFunctionType(replacementArg);
69+
}
70+
5371
return {
5472
'ExpressionStatement > CallExpression[callee.type="MemberExpression"]': (
5573
node: estree.Node,
5674
) => {
57-
const { object, property } = (node as estree.CallExpression)
58-
.callee as estree.MemberExpression;
75+
const callExpression = node as estree.CallExpression;
76+
const { object, property } = callExpression.callee as estree.MemberExpression;
5977
if (isString(object) && property.type === 'Identifier') {
78+
if (isCallbackBasedReplacement(property, callExpression.arguments)) {
79+
return;
80+
}
6081
context.report({
6182
messageId: 'uselessStringOp',
6283
data: {
@@ -69,3 +90,7 @@ export const rule: Rule.RuleModule = {
6990
};
7091
},
7192
};
93+
94+
function isFunctionLike(arg: estree.Expression | estree.SpreadElement | undefined) {
95+
return arg?.type === 'FunctionExpression' || arg?.type === 'ArrowFunctionExpression';
96+
}

packages/analysis/src/jsts/rules/S1154/unit.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,17 @@ describe('S1154', () => {
5353
{
5454
code: `'hello'['whatever']();`,
5555
},
56+
{
57+
code: `
58+
'hello'.replace(/l/g, () => 'x');
59+
`,
60+
},
61+
{
62+
code: `
63+
const replacer = (value: string) => value.toUpperCase();
64+
'hello'.replace(/l/g, replacer);
65+
`,
66+
},
5667
],
5768
invalid: [
5869
{
@@ -99,6 +110,10 @@ describe('S1154', () => {
99110
code: `'hello'.substr(1, 2).toUpperCase();`,
100111
errors: 1,
101112
},
113+
{
114+
code: `'hello'.replace(/l/g, 'x');`,
115+
errors: 1,
116+
},
102117
],
103118
});
104119
});

packages/analysis/src/jsts/rules/S3403/rule.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { generateMeta } from '../helpers/generate-meta.js';
2323
import { getTypeFromTreeNode } from '../helpers/type.js';
2424
import { isRequiredParserServices } from '../helpers/parser-services.js';
2525
import { report, toSecondaryLocation } from '../helpers/location.js';
26+
import { isNullLiteral, isUndefined } from '../helpers/ast.js';
2627
import * as meta from './generated-meta.js';
2728

2829
export const rule: Rule.RuleModule = {
@@ -53,7 +54,11 @@ export const rule: Rule.RuleModule = {
5354
return {
5455
BinaryExpression: (node: estree.Node) => {
5556
const { left, operator, right } = node as estree.BinaryExpression;
56-
if (['===', '!=='].includes(operator) && !isComparableTo(left, right)) {
57+
if (
58+
['===', '!=='].includes(operator) &&
59+
!isDefensiveIndexedNullishCheck(left, right) &&
60+
!isComparableTo(left, right)
61+
) {
5762
const [actual, expected, outcome] =
5863
operator === '===' ? ['===', '==', 'false'] : ['!==', '!=', 'true'];
5964
const operatorToken = context.sourceCode
@@ -79,6 +84,32 @@ export const rule: Rule.RuleModule = {
7984
},
8085
};
8186

87+
function isDefensiveIndexedNullishCheck(left: estree.Node, right: estree.Node) {
88+
return (
89+
(isNullishLiteral(left) && isIndexedAccess(right)) ||
90+
(isNullishLiteral(right) && isIndexedAccess(left))
91+
);
92+
}
93+
94+
function isNullishLiteral(node: estree.Node) {
95+
return (
96+
isNullLiteral(node) ||
97+
isUndefined(node) ||
98+
(node.type === 'UnaryExpression' && node.operator === 'void')
99+
);
100+
}
101+
102+
function isIndexedAccess(node: estree.Node) {
103+
if (node.type === 'MemberExpression') {
104+
return node.computed;
105+
}
106+
return (
107+
node.type === 'ChainExpression' &&
108+
node.expression.type === 'MemberExpression' &&
109+
node.expression.computed
110+
);
111+
}
112+
82113
/**
83114
* Checks if a type is indeterminate (its actual value cannot be determined at compile time).
84115
* - Unknown: can be compared with anything

packages/analysis/src/jsts/rules/S3403/unit.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
RuleTester,
2121
} from '../../../../tests/jsts/tools/testers/rule-tester.js';
2222
import { describe, it } from 'node:test';
23+
import path from 'node:path';
2324

2425
describe('S3403', () => {
2526
it('S3403', () => {
@@ -38,6 +39,10 @@ describe('S3403', () => {
3839
);
3940

4041
const ruleTesterTs = new RuleTester();
42+
const strictProject = path.join(
43+
import.meta.dirname,
44+
'../../../../tests/jsts/tools/testers/fixtures/tsconfig.strict.json',
45+
);
4146
ruleTesterTs.run(
4247
`Strict equality operators should not be used with dissimilar types [ts]`,
4348
rule,
@@ -154,6 +159,21 @@ describe('S3403', () => {
154159
let str = 'str', obj = {};
155160
str !== obj;`,
156161
},
162+
{
163+
// Defensive check on indexed access can be intentional in strict TS configurations.
164+
code: `
165+
const items = ['a'];
166+
if (items[0] === undefined) {
167+
doSomething();
168+
}
169+
function doSomething() {}
170+
`,
171+
languageOptions: {
172+
parserOptions: {
173+
project: strictProject,
174+
},
175+
},
176+
},
157177
{
158178
code: `
159179
const foo = Symbol('foo');
@@ -262,6 +282,21 @@ describe('S3403', () => {
262282
},
263283
],
264284
},
285+
{
286+
code: `
287+
const s: string = 'a';
288+
if (s === undefined) {
289+
doSomething();
290+
}
291+
function doSomething() {}
292+
`,
293+
languageOptions: {
294+
parserOptions: {
295+
project: strictProject,
296+
},
297+
},
298+
errors: 1,
299+
},
265300
],
266301
},
267302
);

0 commit comments

Comments
 (0)