Skip to content

Commit 540a4d4

Browse files
authored
prefer-includes: Fix compatibility with Vue SFC (#2704)
1 parent 1601e9c commit 540a4d4

File tree

7 files changed

+98
-39
lines changed

7 files changed

+98
-39
lines changed

rules/no-instanceof-builtins.js

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
import {checkVueTemplate} from './utils/rule.js';
2-
import {getParenthesizedRange} from './utils/parentheses.js';
1+
import {
2+
checkVueTemplate,
3+
getParenthesizedRange,
4+
getTokenStore,
5+
} from './utils/index.js';
36
import {replaceNodeOrTokenAndSpacesBefore, fixSpaceAroundKeyword} from './fix/index.js';
47
import builtinErrors from './shared/builtin-errors.js';
58
import typedArray from './shared/typed-array.js';
@@ -53,9 +56,11 @@ const strictStrategyConstructors = [
5356
'FinalizationRegistry',
5457
];
5558

56-
const replaceWithFunctionCall = (node, sourceCode, functionName) => function * (fixer) {
57-
const {tokenStore, instanceofToken} = getInstanceOfToken(sourceCode, node);
59+
const replaceWithFunctionCall = (node, context, functionName) => function * (fixer) {
5860
const {left, right} = node;
61+
const tokenStore = getTokenStore(context, node);
62+
const instanceofToken = tokenStore.getTokenAfter(left, isInstanceofToken);
63+
const {sourceCode} = context;
5964

6065
yield * fixSpaceAroundKeyword(fixer, node, sourceCode);
6166

@@ -67,9 +72,11 @@ const replaceWithFunctionCall = (node, sourceCode, functionName) => function * (
6772
yield * replaceNodeOrTokenAndSpacesBefore(right, '', fixer, sourceCode, tokenStore);
6873
};
6974

70-
const replaceWithTypeOfExpression = (node, sourceCode) => function * (fixer) {
71-
const {tokenStore, instanceofToken} = getInstanceOfToken(sourceCode, node);
75+
const replaceWithTypeOfExpression = (node, context) => function * (fixer) {
7276
const {left, right} = node;
77+
const tokenStore = getTokenStore(context, node);
78+
const instanceofToken = tokenStore.getTokenAfter(left, isInstanceofToken);
79+
const {sourceCode} = context;
7380

7481
// Check if the node is in a Vue template expression
7582
const vueExpressionContainer = sourceCode.getAncestors(node).findLast(ancestor => ancestor.type === 'VExpressionContainer');
@@ -89,19 +96,6 @@ const replaceWithTypeOfExpression = (node, sourceCode) => function * (fixer) {
8996
yield fixer.replaceTextRange(rightRange, safeQuote + sourceCode.getText(right).toLowerCase() + safeQuote);
9097
};
9198

92-
const getInstanceOfToken = (sourceCode, node) => {
93-
const {left} = node;
94-
95-
let tokenStore = sourceCode;
96-
let instanceofToken = tokenStore.getTokenAfter(left, isInstanceofToken);
97-
if (!instanceofToken && sourceCode.parserServices.getTemplateBodyTokenStore) {
98-
tokenStore = sourceCode.parserServices.getTemplateBodyTokenStore();
99-
instanceofToken = tokenStore.getTokenAfter(left, isInstanceofToken);
100-
}
101-
102-
return {tokenStore, instanceofToken};
103-
};
104-
10599
/** @param {import('eslint').Rule.RuleContext} context */
106100
const create = context => {
107101
const {
@@ -117,8 +111,6 @@ const create = context => {
117111
: include,
118112
);
119113

120-
const {sourceCode} = context;
121-
122114
return {
123115
/** @param {import('estree').BinaryExpression} node */
124116
BinaryExpression(node) {
@@ -141,12 +133,12 @@ const create = context => {
141133
|| (constructorName === 'Error' && useErrorIsError)
142134
) {
143135
const functionName = constructorName === 'Array' ? 'Array.isArray' : 'Error.isError';
144-
problem.fix = replaceWithFunctionCall(node, sourceCode, functionName);
136+
problem.fix = replaceWithFunctionCall(node, context, functionName);
145137
return problem;
146138
}
147139

148140
if (constructorName === 'Function') {
149-
problem.fix = replaceWithTypeOfExpression(node, sourceCode);
141+
problem.fix = replaceWithTypeOfExpression(node, context);
150142
return problem;
151143
}
152144

@@ -155,7 +147,7 @@ const create = context => {
155147
{
156148
messageId: MESSAGE_ID_SWITCH_TO_TYPE_OF,
157149
data: {type: constructorName.toLowerCase()},
158-
fix: replaceWithTypeOfExpression(node, sourceCode),
150+
fix: replaceWithTypeOfExpression(node, context),
159151
},
160152
];
161153
return problem;

rules/prefer-includes.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
import {checkVueTemplate} from './utils/rule.js';
2-
import isMethodNamed from './utils/is-method-named.js';
1+
import {
2+
checkVueTemplate,
3+
isMethodNamed,
4+
getTokenStore,
5+
} from './utils/index.js';
36
import simpleArraySearchRule from './shared/simple-array-search-rule.js';
47
import {isLiteral, isNegativeOne} from './ast/index.js';
58

@@ -15,7 +18,7 @@ const isNegativeResult = node => ['===', '==', '<'].includes(node.operator);
1518

1619
const getProblem = (context, node, target, argumentsNodes) => {
1720
const {sourceCode} = context;
18-
const tokenStore = sourceCode.parserServices.getTemplateBodyTokenStore?.() ?? sourceCode;
21+
const tokenStore = getTokenStore(context, target);
1922

2023
const memberExpressionNode = target.parent;
2124
const dotToken = tokenStore.getTokenBefore(memberExpressionNode.property);

rules/utils/get-token-store.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// When parsing with `vue-eslint-parser`, we need to use `getTemplateBodyTokenStore()` to get a token inside a `<template>`, and `sourceCode` to get a token inside a `<script>`.
2+
// https://github.com/sindresorhus/eslint-plugin-unicorn/pull/2704/files#r2209196626
3+
4+
/**
5+
@param {import('eslint').Rule.RuleContext} context
6+
@param {import('estree').Node} node
7+
@returns {import('eslint').SourceCode}
8+
*/
9+
function getTokenStore(context, node) {
10+
const {sourceCode} = context;
11+
12+
if (
13+
sourceCode.parserServices.getTemplateBodyTokenStore
14+
&& sourceCode.ast.templateBody
15+
&& sourceCode.getRange(sourceCode.ast.templateBody)[0] <= sourceCode.getRange(node)[0]
16+
&& sourceCode.getRange(node)[1] <= sourceCode.getRange(sourceCode.ast.templateBody)[1]
17+
) {
18+
return sourceCode.parserServices.getTemplateBodyTokenStore();
19+
}
20+
21+
return sourceCode;
22+
}
23+
24+
export default getTokenStore;

rules/utils/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export {default as getCallExpressionTokens} from './get-call-expression-tokens.j
2929
export {default as getIndentString} from './get-indent-string.js';
3030
export {default as getReferences} from './get-references.js';
3131
export {default as getScopes} from './get-scopes.js';
32+
export {default as getTokenStore} from './get-token-store.js';
3233
export {default as getVariableIdentifiers} from './get-variable-identifiers.js';
3334
export {default as hasOptionalChainElement} from './has-optional-chain-element.js';
3435
export {default as isFunctionSelfUsedInside} from './is-function-self-used-inside.js';
@@ -45,6 +46,7 @@ export {default as isShorthandImportLocal} from './is-shorthand-import-local.js'
4546
export {default as isShorthandPropertyValue} from './is-shorthand-property-value.js';
4647
export {default as isValueNotUsable} from './is-value-not-usable.js';
4748
export {default as needsSemicolon} from './needs-semicolon.js';
49+
export {checkVueTemplate} from './rule.js';
4850
export {default as shouldAddParenthesesToAwaitExpressionArgument} from './should-add-parentheses-to-await-expression-argument.js';
4951
export {default as shouldAddParenthesesToCallExpressionCallee} from './should-add-parentheses-to-call-expression-callee.js';
5052
export {default as shouldAddParenthesesToConditionalExpressionChild} from './should-add-parentheses-to-conditional-expression-child.js';

test/prefer-includes.js

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {outdent} from 'outdent';
12
import {getTester, parsers} from './utils/test.js';
23
import tests from './shared/simple-array-search-rule-tests.js';
34

@@ -23,18 +24,28 @@ test.snapshot({
2324
'f(0) < 0',
2425
],
2526
invalid: [
26-
'\'foobar\'.indexOf(\'foo\') !== -1',
27-
'str.indexOf(\'foo\') != -1',
28-
'str.indexOf(\'foo\') > -1',
29-
'str.indexOf(\'foo\') == -1',
30-
'\'foobar\'.indexOf(\'foo\') >= 0',
31-
'[1,2,3].indexOf(4) !== -1',
32-
'str.indexOf(\'foo\') < 0',
33-
'\'\'.indexOf(\'foo\') < 0',
34-
'(a || b).indexOf(\'foo\') === -1',
35-
'foo.indexOf(bar, 0) !== -1',
36-
'foo.indexOf(bar, 1) !== -1',
37-
].flatMap(code => [code, code.replace('.indexOf', '.lastIndexOf'), {code: `<template><div v-if="${code}"></div></template>`, languageOptions: {parser: parsers.vue}}]),
27+
...[
28+
'\'foobar\'.indexOf(\'foo\') !== -1',
29+
'str.indexOf(\'foo\') != -1',
30+
'str.indexOf(\'foo\') > -1',
31+
'str.indexOf(\'foo\') == -1',
32+
'\'foobar\'.indexOf(\'foo\') >= 0',
33+
'[1,2,3].indexOf(4) !== -1',
34+
'str.indexOf(\'foo\') < 0',
35+
'\'\'.indexOf(\'foo\') < 0',
36+
'(a || b).indexOf(\'foo\') === -1',
37+
'foo.indexOf(bar, 0) !== -1',
38+
'foo.indexOf(bar, 1) !== -1',
39+
].flatMap(code => [code, code.replace('.indexOf', '.lastIndexOf'), {code: `<template><div v-if="${code}"></div></template>`, languageOptions: {parser: parsers.vue}}]),
40+
{
41+
code: outdent`
42+
<script setup lang="ts">
43+
console.log([].indexOf(1) != -1);
44+
</script>
45+
`,
46+
languageOptions: {parser: parsers.vue},
47+
},
48+
],
3849
});
3950

4051
const {snapshot, typescript} = tests({

test/snapshots/prefer-includes.js.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,33 @@ Generated by [AVA](https://avajs.dev).
697697
| ^^^^^^^ Use \`.includes()\`, rather than \`.indexOf()\`, when checking for existence.␊
698698
`
699699

700+
## invalid(34): <script setup lang="ts"> console.log([].indexOf(1) != -1); </script>
701+
702+
> Input
703+
704+
`␊
705+
1 | <script setup lang="ts">␊
706+
2 | console.log([].indexOf(1) != -1);␊
707+
3 | </script>␊
708+
`
709+
710+
> Output
711+
712+
`␊
713+
1 | <script setup lang="ts">␊
714+
2 | console.log([].includes(1));␊
715+
3 | </script>␊
716+
`
717+
718+
> Error 1/1
719+
720+
`␊
721+
1 | <script setup lang="ts">␊
722+
> 2 | console.log([].indexOf(1) != -1);␊
723+
| ^^^^^^^ Use \`.includes()\`, rather than \`.indexOf()\`, when checking for existence.␊
724+
3 | </script>␊
725+
`
726+
700727
## invalid(1): values.some(x => x === "foo")
701728

702729
> Input
111 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)