Skip to content

Commit dc596a2

Browse files
DMartensljharb
authored andcommitted
[utils] [new] parse: support flat config
1 parent 3a22c3b commit dc596a2

File tree

3 files changed

+108
-11
lines changed

3 files changed

+108
-11
lines changed

tests/src/core/parse.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,63 @@ describe('parse(content, { settings, ecmaFeatures })', function () {
6969
expect(parse.bind(null, path, content, { settings: { 'import/parsers': { [parseStubParserPath]: [ '.js' ] } }, parserPath: null, parserOptions })).not.to.throw(Error);
7070
expect(parseSpy.callCount, 'custom parser to be called once').to.equal(1);
7171
});
72+
73+
it('throws on invalid languageOptions', function () {
74+
expect(parse.bind(null, path, content, { settings: {}, parserPath: null, languageOptions: null })).to.throw(Error);
75+
});
76+
77+
it('throws on non-object languageOptions.parser', function () {
78+
expect(parse.bind(null, path, content, { settings: {}, parserPath: null, languageOptions: { parser: 'espree' } })).to.throw(Error);
79+
});
80+
81+
it('throws on null languageOptions.parser', function () {
82+
expect(parse.bind(null, path, content, { settings: {}, parserPath: null, languageOptions: { parser: null } })).to.throw(Error);
83+
});
84+
85+
it('throws on empty languageOptions.parser', function () {
86+
expect(parse.bind(null, path, content, { settings: {}, parserPath: null, languageOptions: { parser: {} } })).to.throw(Error);
87+
});
88+
89+
it('throws on non-function languageOptions.parser.parse', function () {
90+
expect(parse.bind(null, path, content, { settings: {}, parserPath: null, languageOptions: { parser: { parse: 'espree' } } })).to.throw(Error);
91+
});
92+
93+
it('throws on non-function languageOptions.parser.parse', function () {
94+
expect(parse.bind(null, path, content, { settings: {}, parserPath: null, languageOptions: { parser: { parseForESLint: 'espree' } } })).to.throw(Error);
95+
});
96+
97+
it('requires only one of the parse methods', function () {
98+
expect(parse.bind(null, path, content, { settings: {}, parserPath: null, languageOptions: { parser: { parseForESLint: () => ({ ast: {} }) } } })).not.to.throw(Error);
99+
});
100+
101+
it('uses parse from languageOptions.parser', function () {
102+
const parseSpy = sinon.spy();
103+
expect(parse.bind(null, path, content, { settings: {}, languageOptions: { parser: { parse: parseSpy } } })).not.to.throw(Error);
104+
expect(parseSpy.callCount, 'passed parser to be called once').to.equal(1);
105+
});
106+
107+
it('uses parseForESLint from languageOptions.parser', function () {
108+
const parseSpy = sinon.spy(() => ({ ast: {} }));
109+
expect(parse.bind(null, path, content, { settings: {}, languageOptions: { parser: { parseForESLint: parseSpy } } })).not.to.throw(Error);
110+
expect(parseSpy.callCount, 'passed parser to be called once').to.equal(1);
111+
});
112+
113+
it('prefers parsers specified in the settings over languageOptions.parser', () => {
114+
const parseSpy = sinon.spy();
115+
parseStubParser.parse = parseSpy;
116+
expect(parse.bind(null, path, content, { settings: { 'import/parsers': { [parseStubParserPath]: [ '.js' ] } }, parserPath: null, languageOptions: { parser: { parse() {} } } })).not.to.throw(Error);
117+
expect(parseSpy.callCount, 'custom parser to be called once').to.equal(1);
118+
});
119+
120+
it('ignores parser options from language options set to null', () => {
121+
const parseSpy = sinon.spy();
122+
parseStubParser.parse = parseSpy;
123+
expect(parse.bind(null, path, content, { settings: {}, parserPath: 'espree', languageOptions: { parserOptions: null }, parserOptions: { sourceType: 'module', ecmaVersion: 2015, ecmaFeatures: { jsx: true } } })).not.to.throw(Error);
124+
});
125+
126+
it('prefers languageOptions.parserOptions over parserOptions', () => {
127+
const parseSpy = sinon.spy();
128+
parseStubParser.parse = parseSpy;
129+
expect(parse.bind(null, path, content, { settings: {}, parserPath: 'espree', languageOptions: { parserOptions: { sourceType: 'module', ecmaVersion: 2015, ecmaFeatures: { jsx: true } } }, parserOptions: { sourceType: 'script' } })).not.to.throw(Error);
130+
});
72131
});

tests/src/rules/no-unused-modules.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ import fs from 'fs';
77
import eslintPkg from 'eslint/package.json';
88
import semver from 'semver';
99

10+
let FlatRuleTester;
11+
try {
12+
({ FlatRuleTester } = require('eslint/use-at-your-own-risk'));
13+
} catch (e) { /**/ }
14+
1015
// TODO: figure out why these tests fail in eslint 4 and 5
1116
const isESLint4TODO = semver.satisfies(eslintPkg.version, '^4 || ^5');
1217

@@ -1371,3 +1376,20 @@ describe('parser ignores prefixes like BOM and hashbang', () => {
13711376
invalid: [],
13721377
});
13731378
});
1379+
1380+
describe('supports flat eslint', { skip: !FlatRuleTester }, () => {
1381+
const flatRuleTester = new FlatRuleTester();
1382+
flatRuleTester.run('no-unused-modules', rule, {
1383+
valid: [{
1384+
options: unusedExportsOptions,
1385+
code: 'import { o2 } from "./file-o";export default () => 12',
1386+
filename: testFilePath('./no-unused-modules/file-a.js'),
1387+
}],
1388+
invalid: [{
1389+
options: unusedExportsOptions,
1390+
code: 'export default () => 13',
1391+
filename: testFilePath('./no-unused-modules/file-f.js'),
1392+
errors: [error(`exported declaration 'default' not used within other modules`)],
1393+
}],
1394+
});
1395+
});

utils/parse.js

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ function keysFromParser(parserPath, parserInstance, parsedResult) {
2323
if (parsedResult && parsedResult.visitorKeys) {
2424
return parsedResult.visitorKeys;
2525
}
26-
if (/.*espree.*/.test(parserPath)) {
26+
if (typeof parserPath === 'string' && /.*espree.*/.test(parserPath)) {
2727
return parserInstance.VisitorKeys;
2828
}
29-
if (/.*babel-eslint.*/.test(parserPath)) {
29+
if (typeof parserPath === 'string' && /.*babel-eslint.*/.test(parserPath)) {
3030
return getBabelEslintVisitorKeys(parserPath);
3131
}
3232
return null;
@@ -51,13 +51,13 @@ function transformHashbang(text) {
5151
}
5252

5353
exports.default = function parse(path, content, context) {
54-
5554
if (context == null) throw new Error('need context to parse properly');
5655

57-
let parserOptions = context.parserOptions;
58-
const parserPath = getParserPath(path, context);
56+
// ESLint in "flat" mode only sets context.languageOptions.parserOptions
57+
let parserOptions = (context.languageOptions && context.languageOptions.parserOptions) || context.parserOptions;
58+
const parserOrPath = getParser(path, context);
5959

60-
if (!parserPath) throw new Error('parserPath is required!');
60+
if (!parserOrPath) throw new Error('parserPath or languageOptions.parser is required!');
6161

6262
// hack: espree blows up with frozen options
6363
parserOptions = Object.assign({}, parserOptions);
@@ -84,7 +84,7 @@ exports.default = function parse(path, content, context) {
8484
delete parserOptions.projects;
8585

8686
// require the parser relative to the main module (i.e., ESLint)
87-
const parser = moduleRequire(parserPath);
87+
const parser = typeof parserOrPath === 'string' ? moduleRequire(parserOrPath) : parserOrPath;
8888

8989
// replicate bom strip and hashbang transform of ESLint
9090
// https://github.com/eslint/eslint/blob/b93af98b3c417225a027cabc964c38e779adb945/lib/linter/linter.js#L779
@@ -95,7 +95,7 @@ exports.default = function parse(path, content, context) {
9595
try {
9696
const parserRaw = parser.parseForESLint(content, parserOptions);
9797
ast = parserRaw.ast;
98-
return makeParseReturn(ast, keysFromParser(parserPath, parser, parserRaw));
98+
return makeParseReturn(ast, keysFromParser(parserOrPath, parser, parserRaw));
9999
} catch (e) {
100100
console.warn();
101101
console.warn('Error while parsing ' + parserOptions.filePath);
@@ -104,18 +104,34 @@ exports.default = function parse(path, content, context) {
104104
if (!ast || typeof ast !== 'object') {
105105
console.warn(
106106
'`parseForESLint` from parser `' +
107-
parserPath +
107+
(typeof parserOrPath === 'string' ? parserOrPath : '`context.languageOptions.parser`') + // Can only be invalid for custom parser per imports/parser
108108
'` is invalid and will just be ignored'
109109
);
110110
} else {
111-
return makeParseReturn(ast, keysFromParser(parserPath, parser, undefined));
111+
return makeParseReturn(ast, keysFromParser(parserOrPath, parser, undefined));
112112
}
113113
}
114114

115115
const ast = parser.parse(content, parserOptions);
116-
return makeParseReturn(ast, keysFromParser(parserPath, parser, undefined));
116+
return makeParseReturn(ast, keysFromParser(parserOrPath, parser, undefined));
117117
};
118118

119+
function getParser(path, context) {
120+
const parserPath = getParserPath(path, context);
121+
if (parserPath) {
122+
return parserPath;
123+
}
124+
const isFlat = context.languageOptions
125+
&& context.languageOptions.parser
126+
&& typeof context.languageOptions.parser !== 'string'
127+
&& (
128+
typeof context.languageOptions.parser.parse === 'function'
129+
|| typeof context.languageOptions.parser.parseForESLint === 'function'
130+
);
131+
132+
return isFlat ? context.languageOptions.parser : null;
133+
}
134+
119135
function getParserPath(path, context) {
120136
const parsers = context.settings['import/parsers'];
121137
if (parsers != null) {

0 commit comments

Comments
 (0)