Skip to content

Commit 1106fc3

Browse files
authored
fix(prefer-spread-syntax): skip non-array concat receivers (#135)
1 parent c8f20d1 commit 1106fc3

2 files changed

Lines changed: 96 additions & 3 deletions

File tree

src/rules/prefer-spread-syntax.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ ruleTester.run('prefer-spread-syntax (untyped)', preferSpreadSyntax as never, {
3939
'Buffer.concat([buf1, buf2]);',
4040
'Buffer.concat(buffers);',
4141

42+
"'a'.concat('b');",
43+
'`hello`.concat(` world`);',
44+
'const stringWithConcat = "StringWithSubstring".substring(1, 5).concat("test");',
45+
'String(value).concat(suffix);',
46+
'JSON.stringify(value).concat("\\n");',
47+
'arr.join(",").concat(suffix);',
48+
4249
// Array.from with mapper function (should keep as-is)
4350
'Array.from(iterable, x => x * 2);',
4451
'Array.from(arr, mapper);',
@@ -146,6 +153,12 @@ ruleTester.run('prefer-spread-syntax (untyped)', preferSpreadSyntax as never, {
146153
errors: [{messageId: 'preferSpreadArray'}]
147154
},
148155

156+
{
157+
code: 'const result = Object.keys(obj).concat(other);',
158+
output: 'const result = [...Object.keys(obj), other];',
159+
errors: [{messageId: 'preferSpreadArray'}]
160+
},
161+
149162
// Array concat chained expression
150163
{
151164
code: 'const result = [1, 2].concat([3, 4]);',

src/rules/prefer-spread-syntax.ts

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,82 @@ function isNullOrUndefined(node: TSESTree.Expression): boolean {
1515
return node.type === 'Identifier' && node.name === 'undefined';
1616
}
1717

18+
const nonArrayReturningMethods = new Set([
19+
'charAt',
20+
'charCodeAt',
21+
'codePointAt',
22+
'endsWith',
23+
'includes',
24+
'indexOf',
25+
'join',
26+
'lastIndexOf',
27+
'localeCompare',
28+
'normalize',
29+
'padEnd',
30+
'padStart',
31+
'repeat',
32+
'replace',
33+
'replaceAll',
34+
'startsWith',
35+
'substring',
36+
'toExponential',
37+
'toFixed',
38+
'toLocaleLowerCase',
39+
'toLocaleString',
40+
'toLocaleUpperCase',
41+
'toLowerCase',
42+
'toPrecision',
43+
'toString',
44+
'toUpperCase',
45+
'trim',
46+
'trimEnd',
47+
'trimStart'
48+
]);
49+
const primitiveCoercionFunctions = new Set([
50+
'BigInt',
51+
'Boolean',
52+
'Number',
53+
'String'
54+
]);
55+
56+
function isKnownNonArrayConcatReceiver(node: TSESTree.Node): boolean {
57+
switch (node.type) {
58+
case 'Literal':
59+
case 'ObjectExpression':
60+
case 'TemplateLiteral':
61+
return true;
62+
}
63+
64+
if (node.type !== 'CallExpression') {
65+
return false;
66+
}
67+
68+
const {callee} = node;
69+
if (
70+
callee.type === 'Identifier' &&
71+
primitiveCoercionFunctions.has(callee.name)
72+
) {
73+
return true;
74+
}
75+
76+
if (
77+
callee.type !== 'MemberExpression' ||
78+
callee.property.type !== 'Identifier'
79+
) {
80+
return false;
81+
}
82+
83+
if (
84+
callee.object.type === 'Identifier' &&
85+
callee.object.name === 'JSON' &&
86+
callee.property.name === 'stringify'
87+
) {
88+
return true;
89+
}
90+
91+
return nonArrayReturningMethods.has(callee.property.name);
92+
}
93+
1894
export const preferSpreadSyntax: TSESLint.RuleModule<MessageIds, []> = {
1995
meta: {
2096
type: 'suggestion',
@@ -59,15 +135,19 @@ export const preferSpreadSyntax: TSESLint.RuleModule<MessageIds, []> = {
59135
node.callee.object.name === 'Buffer'
60136
)
61137
) {
62-
// If type info is available, only flag when the receiver is an array
63-
if (isArrayType(node.callee.object, context) === false) {
138+
const receiver = node.callee.object;
139+
const receiverArrayType = isArrayType(receiver, context);
140+
if (
141+
receiverArrayType === false ||
142+
(receiverArrayType !== true &&
143+
isKnownNonArrayConcatReceiver(receiver))
144+
) {
64145
return;
65146
}
66147

67148
const parts: string[] = [];
68149

69150
// For array literals, inline elements; otherwise spread
70-
const receiver = node.callee.object;
71151
if (receiver.type === 'ArrayExpression') {
72152
for (const el of receiver.elements) {
73153
if (el) {

0 commit comments

Comments
 (0)