Skip to content

Commit

Permalink
[utils] [new] parse: support flat config
Browse files Browse the repository at this point in the history
  • Loading branch information
DMartens authored and ljharb committed Feb 10, 2023
1 parent 3a22c3b commit dc596a2
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 11 deletions.
59 changes: 59 additions & 0 deletions tests/src/core/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,63 @@ describe('parse(content, { settings, ecmaFeatures })', function () {
expect(parse.bind(null, path, content, { settings: { 'import/parsers': { [parseStubParserPath]: [ '.js' ] } }, parserPath: null, parserOptions })).not.to.throw(Error);
expect(parseSpy.callCount, 'custom parser to be called once').to.equal(1);
});

it('throws on invalid languageOptions', function () {
expect(parse.bind(null, path, content, { settings: {}, parserPath: null, languageOptions: null })).to.throw(Error);
});

it('throws on non-object languageOptions.parser', function () {
expect(parse.bind(null, path, content, { settings: {}, parserPath: null, languageOptions: { parser: 'espree' } })).to.throw(Error);
});

it('throws on null languageOptions.parser', function () {
expect(parse.bind(null, path, content, { settings: {}, parserPath: null, languageOptions: { parser: null } })).to.throw(Error);
});

it('throws on empty languageOptions.parser', function () {
expect(parse.bind(null, path, content, { settings: {}, parserPath: null, languageOptions: { parser: {} } })).to.throw(Error);
});

it('throws on non-function languageOptions.parser.parse', function () {
expect(parse.bind(null, path, content, { settings: {}, parserPath: null, languageOptions: { parser: { parse: 'espree' } } })).to.throw(Error);
});

it('throws on non-function languageOptions.parser.parse', function () {
expect(parse.bind(null, path, content, { settings: {}, parserPath: null, languageOptions: { parser: { parseForESLint: 'espree' } } })).to.throw(Error);
});

it('requires only one of the parse methods', function () {
expect(parse.bind(null, path, content, { settings: {}, parserPath: null, languageOptions: { parser: { parseForESLint: () => ({ ast: {} }) } } })).not.to.throw(Error);
});

it('uses parse from languageOptions.parser', function () {
const parseSpy = sinon.spy();
expect(parse.bind(null, path, content, { settings: {}, languageOptions: { parser: { parse: parseSpy } } })).not.to.throw(Error);
expect(parseSpy.callCount, 'passed parser to be called once').to.equal(1);
});

it('uses parseForESLint from languageOptions.parser', function () {
const parseSpy = sinon.spy(() => ({ ast: {} }));
expect(parse.bind(null, path, content, { settings: {}, languageOptions: { parser: { parseForESLint: parseSpy } } })).not.to.throw(Error);
expect(parseSpy.callCount, 'passed parser to be called once').to.equal(1);
});

it('prefers parsers specified in the settings over languageOptions.parser', () => {
const parseSpy = sinon.spy();
parseStubParser.parse = parseSpy;
expect(parse.bind(null, path, content, { settings: { 'import/parsers': { [parseStubParserPath]: [ '.js' ] } }, parserPath: null, languageOptions: { parser: { parse() {} } } })).not.to.throw(Error);
expect(parseSpy.callCount, 'custom parser to be called once').to.equal(1);
});

it('ignores parser options from language options set to null', () => {
const parseSpy = sinon.spy();
parseStubParser.parse = parseSpy;
expect(parse.bind(null, path, content, { settings: {}, parserPath: 'espree', languageOptions: { parserOptions: null }, parserOptions: { sourceType: 'module', ecmaVersion: 2015, ecmaFeatures: { jsx: true } } })).not.to.throw(Error);
});

it('prefers languageOptions.parserOptions over parserOptions', () => {
const parseSpy = sinon.spy();
parseStubParser.parse = parseSpy;
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);
});
});
22 changes: 22 additions & 0 deletions tests/src/rules/no-unused-modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import fs from 'fs';
import eslintPkg from 'eslint/package.json';
import semver from 'semver';

let FlatRuleTester;
try {
({ FlatRuleTester } = require('eslint/use-at-your-own-risk'));
} catch (e) { /**/ }

// TODO: figure out why these tests fail in eslint 4 and 5
const isESLint4TODO = semver.satisfies(eslintPkg.version, '^4 || ^5');

Expand Down Expand Up @@ -1371,3 +1376,20 @@ describe('parser ignores prefixes like BOM and hashbang', () => {
invalid: [],
});
});

describe('supports flat eslint', { skip: !FlatRuleTester }, () => {
const flatRuleTester = new FlatRuleTester();
flatRuleTester.run('no-unused-modules', rule, {
valid: [{
options: unusedExportsOptions,
code: 'import { o2 } from "./file-o";export default () => 12',
filename: testFilePath('./no-unused-modules/file-a.js'),
}],
invalid: [{
options: unusedExportsOptions,
code: 'export default () => 13',
filename: testFilePath('./no-unused-modules/file-f.js'),
errors: [error(`exported declaration 'default' not used within other modules`)],
}],
});
});
38 changes: 27 additions & 11 deletions utils/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ function keysFromParser(parserPath, parserInstance, parsedResult) {
if (parsedResult && parsedResult.visitorKeys) {
return parsedResult.visitorKeys;
}
if (/.*espree.*/.test(parserPath)) {
if (typeof parserPath === 'string' && /.*espree.*/.test(parserPath)) {
return parserInstance.VisitorKeys;
}
if (/.*babel-eslint.*/.test(parserPath)) {
if (typeof parserPath === 'string' && /.*babel-eslint.*/.test(parserPath)) {
return getBabelEslintVisitorKeys(parserPath);
}
return null;
Expand All @@ -51,13 +51,13 @@ function transformHashbang(text) {
}

exports.default = function parse(path, content, context) {

if (context == null) throw new Error('need context to parse properly');

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

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

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

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

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

const ast = parser.parse(content, parserOptions);
return makeParseReturn(ast, keysFromParser(parserPath, parser, undefined));
return makeParseReturn(ast, keysFromParser(parserOrPath, parser, undefined));
};

function getParser(path, context) {
const parserPath = getParserPath(path, context);
if (parserPath) {
return parserPath;
}
const isFlat = context.languageOptions
&& context.languageOptions.parser
&& typeof context.languageOptions.parser !== 'string'
&& (
typeof context.languageOptions.parser.parse === 'function'
|| typeof context.languageOptions.parser.parseForESLint === 'function'
);

return isFlat ? context.languageOptions.parser : null;
}

function getParserPath(path, context) {
const parsers = context.settings['import/parsers'];
if (parsers != null) {
Expand Down

0 comments on commit dc596a2

Please sign in to comment.