Skip to content

Commit ce4d57f

Browse files
ROSSROSALESljharb
andcommitted
[Tests] migrate helper parsers function from eslint-plugin-react
Co-authored-by: Ross Rosales <[email protected]> Co-authored-by: Jordan Harband <[email protected]>
1 parent 9688ad8 commit ce4d57f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+534
-280
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { version } from 'eslint/package.json';
2+
import semver from 'semver';
3+
4+
const isESLintV8 = semver.major(version) >= 8;
5+
6+
// eslint-disable-next-line global-require, import/no-dynamic-require, import/no-unresolved
7+
const getESLintCoreRule = (ruleId) => (isESLintV8 ? require('eslint/use-at-your-own-risk').builtinRules.get(ruleId) : require(`eslint/lib/rules/${ruleId}`));
8+
9+
export default getESLintCoreRule;

__tests__/__util__/helpers/parsers.js

+186
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import path from 'path';
2+
import semver from 'semver';
3+
import entries from 'object.entries';
4+
import { version } from 'eslint/package.json';
5+
import flatMap from 'array.prototype.flatmap';
6+
7+
let tsParserVersion;
8+
try {
9+
// eslint-disable-next-line import/no-unresolved, global-require
10+
tsParserVersion = require('@typescript-eslint/parser/package.json').version;
11+
} catch (e) { /**/ }
12+
13+
const disableNewTS = semver.satisfies(tsParserVersion, '>= 4.1') // this rule is not useful on v4.1+ of the TS parser
14+
? (x) => ({ ...x, features: [].concat(x.features, 'no-ts-new') })
15+
: (x) => x;
16+
17+
function minEcmaVersion(features, parserOptions) {
18+
const minEcmaVersionForFeatures = {
19+
'class fields': 2022,
20+
'optional chaining': 2020,
21+
'nullish coalescing': 2020,
22+
};
23+
const result = Math.max(
24+
...[].concat(
25+
(parserOptions && parserOptions.ecmaVersion) || [],
26+
flatMap(entries(minEcmaVersionForFeatures), (entry) => {
27+
const f = entry[0];
28+
const y = entry[1];
29+
return features.has(f) ? y : [];
30+
}),
31+
).map((y) => (y > 5 && y < 2015 ? y + 2009 : y)), // normalize editions to years
32+
);
33+
return Number.isFinite(result) ? result : undefined;
34+
}
35+
36+
const NODE_MODULES = '../../node_modules';
37+
38+
const parsers = {
39+
BABEL_ESLINT: path.join(__dirname, NODE_MODULES, 'babel-eslint'),
40+
'@BABEL_ESLINT': path.join(__dirname, NODE_MODULES, '@babel/eslint-parser'),
41+
TYPESCRIPT_ESLINT: path.join(__dirname, NODE_MODULES, 'typescript-eslint-parser'),
42+
'@TYPESCRIPT_ESLINT': path.join(__dirname, NODE_MODULES, '@typescript-eslint/parser'),
43+
disableNewTS,
44+
babelParserOptions: function parserOptions(test, features) {
45+
return {
46+
...test.parserOptions,
47+
requireConfigFile: false,
48+
babelOptions: {
49+
presets: [
50+
'@babel/preset-react',
51+
],
52+
plugins: [
53+
'@babel/plugin-syntax-do-expressions',
54+
'@babel/plugin-syntax-function-bind',
55+
['@babel/plugin-syntax-decorators', { legacy: true }],
56+
],
57+
parserOpts: {
58+
allowSuperOutsideMethod: false,
59+
allowReturnOutsideFunction: false,
60+
},
61+
},
62+
ecmaFeatures: {
63+
64+
...test.parserOptions && test.parserOptions.ecmaFeatures,
65+
jsx: true,
66+
modules: true,
67+
legacyDecorators: features.has('decorators'),
68+
},
69+
};
70+
},
71+
all: function all(tests) {
72+
const t = flatMap(tests, (test) => {
73+
/* eslint no-param-reassign: 0 */
74+
if (typeof test === 'string') {
75+
test = { code: test };
76+
}
77+
if ('parser' in test) {
78+
delete test.features;
79+
return test;
80+
}
81+
const features = new Set([].concat(test.features || []));
82+
delete test.features;
83+
84+
const es = minEcmaVersion(features, test.parserOptions);
85+
86+
function addComment(testObject, parser) {
87+
const extras = [].concat(
88+
`features: [${Array.from(features).join(',')}]`,
89+
`parser: ${parser}`,
90+
testObject.parserOptions ? `parserOptions: ${JSON.stringify(testObject.parserOptions)}` : [],
91+
testObject.options ? `options: ${JSON.stringify(testObject.options)}` : [],
92+
testObject.settings ? `settings: ${JSON.stringify(testObject.settings)}` : [],
93+
);
94+
95+
const extraComment = `\n// ${extras.join(', ')}`;
96+
97+
// Augment expected fix code output with extraComment
98+
const nextCode = { code: testObject.code + extraComment };
99+
const nextOutput = testObject.output && { output: testObject.output + extraComment };
100+
101+
// Augment expected suggestion outputs with extraComment
102+
// `errors` may be a number (expected number of errors) or an array of
103+
// error objects.
104+
const nextErrors = testObject.errors
105+
&& typeof testObject.errors !== 'number'
106+
&& {
107+
errors: testObject.errors.map(
108+
(errorObject) => {
109+
const nextSuggestions = errorObject.suggestions && {
110+
suggestions: errorObject.suggestions.map((suggestion) => ({ ...suggestion, output: suggestion.output + extraComment })),
111+
};
112+
113+
return { ...errorObject, ...nextSuggestions };
114+
},
115+
),
116+
};
117+
118+
return {
119+
120+
...testObject,
121+
...nextCode,
122+
...nextOutput,
123+
...nextErrors,
124+
};
125+
}
126+
127+
const skipBase = (features.has('class fields') && semver.satisfies(version, '< 8'))
128+
|| (es >= 2020 && semver.satisfies(version, '< 6'))
129+
|| features.has('no-default')
130+
|| features.has('bind operator')
131+
|| features.has('do expressions')
132+
|| features.has('decorators')
133+
|| features.has('flow')
134+
|| features.has('ts')
135+
|| features.has('types')
136+
|| (features.has('fragment') && semver.satisfies(version, '< 5'));
137+
138+
const skipBabel = features.has('no-babel');
139+
const skipOldBabel = skipBabel
140+
|| features.has('no-babel-old')
141+
|| features.has('optional chaining')
142+
|| semver.satisfies(version, '>= 8');
143+
const skipNewBabel = skipBabel
144+
|| features.has('no-babel-new')
145+
|| !semver.satisfies(version, '^7.5.0') // require('@babel/eslint-parser/package.json').peerDependencies.eslint
146+
|| features.has('flow')
147+
|| features.has('types')
148+
|| features.has('ts');
149+
const skipTS = semver.satisfies(version, '<= 5') // TODO: make these pass on eslint 5
150+
|| features.has('no-ts')
151+
|| features.has('flow')
152+
|| features.has('jsx namespace')
153+
|| features.has('bind operator')
154+
|| features.has('do expressions');
155+
const tsOld = !skipTS && !features.has('no-ts-old');
156+
const tsNew = !skipTS && !features.has('no-ts-new');
157+
158+
return [].concat(
159+
skipBase ? [] : addComment(
160+
{
161+
...test,
162+
...typeof es === 'number' && {
163+
parserOptions: { ...test.parserOptions, ecmaVersion: es },
164+
},
165+
},
166+
'default',
167+
),
168+
skipOldBabel ? [] : addComment({
169+
...test,
170+
parser: parsers.BABEL_ESLINT,
171+
parserOptions: parsers.babelParserOptions(test, features),
172+
}, 'babel-eslint'),
173+
skipNewBabel ? [] : addComment({
174+
...test,
175+
parser: parsers['@BABEL_ESLINT'],
176+
parserOptions: parsers.babelParserOptions(test, features),
177+
}, '@babel/eslint-parser'),
178+
tsOld ? addComment({ ...test, parser: parsers.TYPESCRIPT_ESLINT }, 'typescript-eslint') : [],
179+
tsNew ? addComment({ ...test, parser: parsers['@TYPESCRIPT_ESLINT'] }, '@typescript-eslint/parser') : [],
180+
);
181+
});
182+
return t;
183+
},
184+
};
185+
186+
export default parsers;

__tests__/src/rules/accessible-emoji-test.js

+5-4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import { RuleTester } from 'eslint';
1111
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
1212
import rule from '../../../src/rules/accessible-emoji';
13+
import parsers from '../../__util__/helpers/parsers';
1314

1415
// -----------------------------------------------------------------------------
1516
// Tests
@@ -23,7 +24,7 @@ const expectedError = {
2324
};
2425

2526
ruleTester.run('accessible-emoji', rule, {
26-
valid: [
27+
valid: parsers.all([].concat(
2728
{ code: '<div />;' },
2829
{ code: '<span />' },
2930
{ code: '<span>No emoji here!</span>' },
@@ -42,8 +43,8 @@ ruleTester.run('accessible-emoji', rule, {
4243
code: '<CustomInput type="hidden">🐼</CustomInput>',
4344
settings: { 'jsx-a11y': { components: { CustomInput: 'input' } } },
4445
},
45-
].map(parserOptionsMapper),
46-
invalid: [
46+
)).map(parserOptionsMapper),
47+
invalid: parsers.all([].concat(
4748
{ code: '<span>🐼</span>', errors: [expectedError] },
4849
{ code: '<span>foo🐼bar</span>', errors: [expectedError] },
4950
{ code: '<span>foo 🐼 bar</span>', errors: [expectedError] },
@@ -52,5 +53,5 @@ ruleTester.run('accessible-emoji', rule, {
5253
{ code: '<Foo>🐼</Foo>', errors: [expectedError] },
5354
{ code: '<span aria-hidden="false">🐼</span>', errors: [expectedError] },
5455
{ code: '<CustomInput type="hidden">🐼</CustomInput>', errors: [expectedError] },
55-
].map(parserOptionsMapper),
56+
)).map(parserOptionsMapper),
5657
});

__tests__/src/rules/alt-text-test.js

+20-9
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import { RuleTester } from 'eslint';
1111
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
12+
import parsers from '../../__util__/helpers/parsers';
1213
import rule from '../../../src/rules/alt-text';
1314

1415
// -----------------------------------------------------------------------------
@@ -28,19 +29,29 @@ Use alt="" for presentational images.`,
2829
type: 'JSXOpeningElement',
2930
});
3031

31-
const ariaLabelValueError = 'The aria-label attribute must have a value. The alt attribute is preferred over aria-label for images.';
32-
const ariaLabelledbyValueError = 'The aria-labelledby attribute must have a value. The alt attribute is preferred over aria-labelledby for images.';
32+
const ariaLabelValueError = {
33+
message: 'The aria-label attribute must have a value. The alt attribute is preferred over aria-label for images.',
34+
};
35+
const ariaLabelledbyValueError = {
36+
message: 'The aria-labelledby attribute must have a value. The alt attribute is preferred over aria-labelledby for images.',
37+
};
3338

3439
const preferAltError = () => ({
3540
message: 'Prefer alt="" over a presentational role. First rule of aria is to not use aria if it can be achieved via native HTML.',
3641
type: 'JSXOpeningElement',
3742
});
3843

39-
const objectError = 'Embedded <object> elements must have alternative text by providing inner text, aria-label or aria-labelledby props.';
44+
const objectError = {
45+
message: 'Embedded <object> elements must have alternative text by providing inner text, aria-label or aria-labelledby props.',
46+
};
4047

41-
const areaError = 'Each area of an image map must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.';
48+
const areaError = {
49+
message: 'Each area of an image map must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.',
50+
};
4251

43-
const inputImageError = '<input> elements with type="image" must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.';
52+
const inputImageError = {
53+
message: '<input> elements with type="image" must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.',
54+
};
4455

4556
const componentsSettings = {
4657
'jsx-a11y': {
@@ -58,7 +69,7 @@ const array = [{
5869
}];
5970

6071
ruleTester.run('alt-text', rule, {
61-
valid: [
72+
valid: parsers.all([].concat(
6273
// DEFAULT ELEMENT 'img' TESTS
6374
{ code: '<img alt="foo" />;' },
6475
{ code: '<img alt={"foo"} />;' },
@@ -166,8 +177,8 @@ ruleTester.run('alt-text', rule, {
166177
{ code: '<InputImage alt="" />', options: array },
167178
{ code: '<InputImage alt="This is descriptive!" />', options: array },
168179
{ code: '<InputImage alt={altText} />', options: array },
169-
].map(parserOptionsMapper),
170-
invalid: [
180+
)).map(parserOptionsMapper),
181+
invalid: parsers.all([].concat(
171182
// DEFAULT ELEMENT 'img' TESTS
172183
{ code: '<img />;', errors: [missingPropError('img')] },
173184
{ code: '<img alt />;', errors: [altValueError('img')] },
@@ -273,5 +284,5 @@ ruleTester.run('alt-text', rule, {
273284
{ code: '<InputImage>Foo</InputImage>', errors: [inputImageError], options: array },
274285
{ code: '<InputImage {...this.props} />', errors: [inputImageError], options: array },
275286
{ code: '<Input type="image" />', errors: [inputImageError], settings: componentsSettings },
276-
].map(parserOptionsMapper),
287+
)).map(parserOptionsMapper),
277288
});

__tests__/src/rules/anchor-ambiguous-text-test.js

+5-4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import { RuleTester } from 'eslint';
1212
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
13+
import parsers from '../../__util__/helpers/parsers';
1314
import rule from '../../../src/rules/anchor-ambiguous-text';
1415

1516
// -----------------------------------------------------------------------------
@@ -34,7 +35,7 @@ const expectedErrorGenerator = (words) => ({
3435
const expectedError = expectedErrorGenerator(DEFAULT_AMBIGUOUS_WORDS);
3536

3637
ruleTester.run('anchor-ambiguous-text', rule, {
37-
valid: [
38+
valid: parsers.all([].concat(
3839
{ code: '<a>documentation</a>;' },
3940
{ code: '<a>${here}</a>;' },
4041
{ code: '<a aria-label="tutorial on using eslint-plugin-jsx-a11y">click here</a>;' },
@@ -69,8 +70,8 @@ ruleTester.run('anchor-ambiguous-text', rule, {
6970
}],
7071
settings: { 'jsx-a11y': { components: { Link: 'a' } } },
7172
},
72-
].map(parserOptionsMapper),
73-
invalid: [
73+
)).map(parserOptionsMapper),
74+
invalid: parsers.all([].concat(
7475
{ code: '<a>here</a>;', errors: [expectedError] },
7576
{ code: '<a>HERE</a>;', errors: [expectedError] },
7677
{ code: '<a>click here</a>;', errors: [expectedError] },
@@ -113,5 +114,5 @@ ruleTester.run('anchor-ambiguous-text', rule, {
113114
words: ['a disallowed word'],
114115
}],
115116
},
116-
].map(parserOptionsMapper),
117+
)).map(parserOptionsMapper),
117118
});

__tests__/src/rules/anchor-has-content-test.js

+5-4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import { RuleTester } from 'eslint';
1111
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
12+
import parsers from '../../__util__/helpers/parsers';
1213
import rule from '../../../src/rules/anchor-has-content';
1314

1415
// -----------------------------------------------------------------------------
@@ -23,7 +24,7 @@ const expectedError = {
2324
};
2425

2526
ruleTester.run('anchor-has-content', rule, {
26-
valid: [
27+
valid: parsers.all([].concat(
2728
{ code: '<div />;' },
2829
{ code: '<a>Foo</a>' },
2930
{ code: '<a><Bar /></a>' },
@@ -39,8 +40,8 @@ ruleTester.run('anchor-has-content', rule, {
3940
{ code: '<a title={title} />' },
4041
{ code: '<a aria-label={ariaLabel} />' },
4142
{ code: '<a title={title} aria-label={ariaLabel} />' },
42-
].map(parserOptionsMapper),
43-
invalid: [
43+
)).map(parserOptionsMapper),
44+
invalid: parsers.all([].concat(
4445
{ code: '<a />', errors: [expectedError] },
4546
{ code: '<a><Bar aria-hidden /></a>', errors: [expectedError] },
4647
{ code: '<a>{undefined}</a>', errors: [expectedError] },
@@ -49,5 +50,5 @@ ruleTester.run('anchor-has-content', rule, {
4950
errors: [expectedError],
5051
settings: { 'jsx-a11y': { components: { Link: 'a' } } },
5152
},
52-
].map(parserOptionsMapper),
53+
)).map(parserOptionsMapper),
5354
});

0 commit comments

Comments
 (0)