From dc596a29bb3fe705396acc64f38d891b2ce52669 Mon Sep 17 00:00:00 2001 From: Daniel Martens Date: Fri, 10 Feb 2023 20:57:33 +0100 Subject: [PATCH] [utils] [new] `parse`: support flat config --- tests/src/core/parse.js | 59 ++++++++++++++++++++++++++++ tests/src/rules/no-unused-modules.js | 22 +++++++++++ utils/parse.js | 38 ++++++++++++------ 3 files changed, 108 insertions(+), 11 deletions(-) diff --git a/tests/src/core/parse.js b/tests/src/core/parse.js index 407070aa2f..4ab8370ed2 100644 --- a/tests/src/core/parse.js +++ b/tests/src/core/parse.js @@ -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); + }); }); diff --git a/tests/src/rules/no-unused-modules.js b/tests/src/rules/no-unused-modules.js index de169c65da..87714b599b 100644 --- a/tests/src/rules/no-unused-modules.js +++ b/tests/src/rules/no-unused-modules.js @@ -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'); @@ -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`)], + }], + }); +}); diff --git a/utils/parse.js b/utils/parse.js index ac728ec5b2..dd0746aaa7 100644 --- a/utils/parse.js +++ b/utils/parse.js @@ -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; @@ -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); @@ -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 @@ -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); @@ -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) {