Skip to content

Commit 00883a8

Browse files
authored
prefer-set-has: Use snapshot to test (#2035)
1 parent 7ed738a commit 00883a8

File tree

7 files changed

+1485
-510
lines changed

7 files changed

+1485
-510
lines changed

package.json

+3-8
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@
109109
]
110110
},
111111
"xo": {
112+
"extends": [
113+
"plugin:internal-rules/all"
114+
],
112115
"ignores": [
113116
".cache-eslint-remote-tester",
114117
"eslint-remote-tester-results",
@@ -160,14 +163,6 @@
160163
"eslint-plugin/require-meta-has-suggestions": "off",
161164
"eslint-plugin/require-meta-schema": "off"
162165
}
163-
},
164-
{
165-
"files": [
166-
"rules/**/*.js"
167-
],
168-
"extends": [
169-
"plugin:internal-rules/all"
170-
]
171166
}
172167
]
173168
},

rules/no-useless-spread.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -148,11 +148,11 @@ function * unwrapSingleArraySpread(fixer, arrayExpression, sourceCode) {
148148
] = sourceCode.getLastTokens(arrayExpression, 2);
149149

150150
// `[...value]`
151-
// ^
151+
// ^
152152
yield fixer.remove(closingBracketToken);
153153

154154
// `[...value,]`
155-
// ^
155+
// ^
156156
if (isCommaToken(commaToken)) {
157157
yield fixer.remove(commaToken);
158158
}
+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
'use strict';
2+
const assert = require('node:assert');
3+
const {
4+
isCommaToken,
5+
} = require('@eslint-community/eslint-utils');
6+
const {methodCallSelector} = require('../../rules/selectors/index.js');
7+
8+
const MESSAGE_ID_DISALLOWED_PROPERTY = 'disallow-property';
9+
const MESSAGE_ID_NO_SINGLE_CODE_OBJECT = 'use-string';
10+
const MESSAGE_ID_REMOVE_FIX_MARK_COMMENT = 'remove-fix-mark';
11+
const messages = {
12+
[MESSAGE_ID_DISALLOWED_PROPERTY]: '"{{name}}" not allowed.{{autoFixEnableTip}}',
13+
[MESSAGE_ID_NO_SINGLE_CODE_OBJECT]: 'Use string instead of object with "code".',
14+
[MESSAGE_ID_REMOVE_FIX_MARK_COMMENT]: 'This comment should be removed.',
15+
};
16+
17+
// Top-level `test.snapshot({invalid: []})`
18+
const selector = [
19+
'Program > ExpressionStatement.body > .expression',
20+
// `test.snapshot()`
21+
methodCallSelector({
22+
argumentsLength: 1,
23+
object: 'test',
24+
method: 'snapshot',
25+
}),
26+
' > ObjectExpression.arguments:first-child',
27+
/*
28+
```
29+
test.snapshot({
30+
invalid: [], <- Property
31+
})
32+
```
33+
*/
34+
' > Property.properties',
35+
'[computed!=true]',
36+
'[method!=true]',
37+
'[shorthand!=true]',
38+
'[kind="init"]',
39+
'[key.type="Identifier"]',
40+
'[key.name="invalid"]',
41+
42+
' > ArrayExpression.value',
43+
' > ObjectExpression.elements',
44+
' > Property.properties[computed!=true][key.type="Identifier"]',
45+
].join('');
46+
47+
function * removeObjectProperty(node, fixer, sourceCode) {
48+
yield fixer.remove(node);
49+
const nextToken = sourceCode.getTokenAfter(node);
50+
if (isCommaToken(nextToken)) {
51+
yield fixer.remove(nextToken);
52+
}
53+
}
54+
55+
// The fix deletes lots of code, disabled auto-fix by default, unless `/* fix */ test.snapshot()` pattern is used.
56+
function hasFixMarkComment(propertyNode, sourceCode) {
57+
const snapshotTestCall = propertyNode.parent.parent.parent.parent.parent;
58+
assert.ok(snapshotTestCall.type === 'CallExpression');
59+
const comment = sourceCode.getTokenBefore(snapshotTestCall, {includeComments: true});
60+
61+
if (
62+
(comment?.type === 'Block' || comment?.type === 'Line')
63+
&& comment.value.trim().toLowerCase() === 'fix'
64+
&& (
65+
comment.loc.start.line === snapshotTestCall.loc.start.line
66+
|| comment.loc.start.line === snapshotTestCall.loc.start.line - 1
67+
)
68+
) {
69+
return true;
70+
}
71+
}
72+
73+
module.exports = {
74+
create(context) {
75+
const sourceCode = context.getSourceCode();
76+
77+
return {
78+
[selector](propertyNode) {
79+
const {key} = propertyNode;
80+
81+
switch (key.name) {
82+
case 'errors':
83+
case 'output': {
84+
const canFix = sourceCode.getCommentsInside(propertyNode).length === 0;
85+
const hasFixMark = hasFixMarkComment(propertyNode, sourceCode);
86+
87+
context.report({
88+
node: key,
89+
messageId: MESSAGE_ID_DISALLOWED_PROPERTY,
90+
data: {
91+
name: key.name,
92+
autoFixEnableTip: !hasFixMark && canFix
93+
? ' Put /* fix */ before `test.snapshot()` to enable auto-fix.'
94+
: '',
95+
},
96+
fix: hasFixMark && canFix
97+
? fixer => removeObjectProperty(propertyNode, fixer, sourceCode)
98+
: undefined
99+
,
100+
});
101+
break;
102+
}
103+
104+
case 'code': {
105+
const testCase = propertyNode.parent;
106+
if (testCase.properties.length === 1) {
107+
const commentsCount = sourceCode.getCommentsInside(testCase).length
108+
- sourceCode.getCommentsInside(propertyNode).length;
109+
context.report({
110+
node: testCase,
111+
messageId: MESSAGE_ID_NO_SINGLE_CODE_OBJECT,
112+
fix: commentsCount === 0
113+
? fixer => fixer.replaceText(testCase, sourceCode.getText(propertyNode.value))
114+
: undefined,
115+
});
116+
}
117+
118+
break;
119+
}
120+
121+
// No default
122+
}
123+
},
124+
};
125+
},
126+
meta: {
127+
fixable: 'code',
128+
messages,
129+
},
130+
};

scripts/internal-rules/index.js

+33-4
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,47 @@
11
'use strict';
22

3+
const path = require('node:path');
4+
35
const pluginName = 'internal-rules';
6+
const TEST_DIRECTORIES = [
7+
path.join(__dirname, '../../test'),
8+
];
9+
const RULES_DIRECTORIES = [
10+
path.join(__dirname, '../../rules'),
11+
];
412

513
const rules = [
6-
'prefer-negative-boolean-attribute',
7-
'prefer-disallow-over-forbid',
14+
{id: 'fix-snapshot-test', directories: TEST_DIRECTORIES},
15+
{id: 'prefer-disallow-over-forbid', directories: RULES_DIRECTORIES},
16+
{id: 'prefer-negative-boolean-attribute', directories: RULES_DIRECTORIES},
817
];
918

19+
const isFileInsideDirectory = (filename, directory) => filename.startsWith(directory + path.sep);
20+
1021
module.exports = {
11-
rules: Object.fromEntries(rules.map(id => [id, require(`./${id}.js`)])),
22+
rules: Object.fromEntries(
23+
rules.map(({id, directories}) => {
24+
const rule = require(`./${id}.js`);
25+
return [
26+
id,
27+
{
28+
...rule,
29+
create(context) {
30+
const filename = context.getPhysicalFilename();
31+
if (directories.every(directory => !isFileInsideDirectory(filename, directory))) {
32+
return {};
33+
}
34+
35+
return rule.create(context);
36+
},
37+
},
38+
];
39+
}),
40+
),
1241
configs: {
1342
all: {
1443
plugins: [pluginName],
15-
rules: Object.fromEntries(rules.map(id => [`${pluginName}/${id}`, 'error'])),
44+
rules: Object.fromEntries(rules.map(({id}) => [`${pluginName}/${id}`, 'error'])),
1645
},
1746
},
1847
};

0 commit comments

Comments
 (0)