Repository: import-js/eslint-plugin-import
Pull request #3061 (2024-09-15 15:21:49)

[utils] [fix] `parse`: espree parser isn't working with flat config
This change addresses an issue with using the espree parser with flat config.  `keysFromParser` is responsible for finding the `visitorKeys` from a combination of the parser instance, parserPath, and the results of a call to `parseForESLint`.  When using flat config, the `parserPath` will not be on the context object, and when using the espree parser there are no results from `parseForESLint`.  The espree parser _does_ provide visitor keys as a  prop on the parser itself.  However, this logic was only returning it when it found that "espree" was somewhere in the `parserPath` (which doesn't exist on flat config).

I changed it so that it first does the check for the old-school babel parser using parser path, and then just checks the parser instance for the presence of `VisitorKeys`, rather than using parserPath at all. The reason for the re-order is that if the old babel-eslint parser, for some reason has `VisitorKeys`, having the new condition first, would change the behavior.


Patch:
diff --git a/.nycrc b/.nycrc
index 5d75e215..c5396cb1 100644
--- a/.nycrc
+++ b/.nycrc
@@ -14,6 +14,7 @@
 		"resolvers/*/test",
 		"scripts",
 		"memo-parser",
-		"lib"
+		"lib",
+		"examples"
 	]
 }
diff --git a/examples/flat/eslint.config.mjs b/examples/flat/eslint.config.mjs
index 370514a6..14326526 100644
--- a/examples/flat/eslint.config.mjs
+++ b/examples/flat/eslint.config.mjs
@@ -20,6 +20,7 @@ export default [
       'import/no-dynamic-require': 'warn',
       'import/no-nodejs-modules': 'warn',
       'import/no-unused-modules': ['warn', { unusedExports: true }],
+      'import/no-cycle': 'warn',
     },
   },
 ];
diff --git a/examples/flat/src/depth-zero.js b/examples/flat/src/depth-zero.js
new file mode 100644
index 00000000..8cfde997
--- /dev/null
+++ b/examples/flat/src/depth-zero.js
@@ -0,0 +1,3 @@
+import { foo } from "./es6/depth-one-dynamic";
+
+foo();
diff --git a/examples/flat/src/es6/depth-one-dynamic.js b/examples/flat/src/es6/depth-one-dynamic.js
new file mode 100644
index 00000000..ca129fd6
--- /dev/null
+++ b/examples/flat/src/es6/depth-one-dynamic.js
@@ -0,0 +1,3 @@
+export function foo() {}
+
+export const bar = () => import("../depth-zero").then(({foo}) => foo);
diff --git a/examples/legacy/.eslintrc.cjs b/examples/legacy/.eslintrc.cjs
index e3cec097..90e065c9 100644
--- a/examples/legacy/.eslintrc.cjs
+++ b/examples/legacy/.eslintrc.cjs
@@ -20,5 +20,6 @@ module.exports = {
     'import/no-dynamic-require': 'warn',
     'import/no-nodejs-modules': 'warn',
     'import/no-unused-modules': ['warn', { unusedExports: true }],
+    'import/no-cycle': 'warn',
   },
 };
diff --git a/examples/legacy/src/depth-zero.js b/examples/legacy/src/depth-zero.js
new file mode 100644
index 00000000..8cfde997
--- /dev/null
+++ b/examples/legacy/src/depth-zero.js
@@ -0,0 +1,3 @@
+import { foo } from "./es6/depth-one-dynamic";
+
+foo();
diff --git a/examples/legacy/src/es6/depth-one-dynamic.js b/examples/legacy/src/es6/depth-one-dynamic.js
new file mode 100644
index 00000000..cda7091c
--- /dev/null
+++ b/examples/legacy/src/es6/depth-one-dynamic.js
@@ -0,0 +1,3 @@
+export function foo() {}
+
+export const bar = () => import("../depth-zero").then(({ foo }) => foo);
diff --git a/utils/CHANGELOG.md b/utils/CHANGELOG.md
index ca733eee..704a282d 100644
--- a/utils/CHANGELOG.md
+++ b/utils/CHANGELOG.md
@@ -7,6 +7,8 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
 
 ### Fixed
 - `parse`: remove unneeded extra backticks ([#3057], thanks [@G-Rath])
+- `parse`: espree parser isn't working with flat config ([#3061], thanks [@michaelfaith])
+- `parse`: add `ecmaVersion` and `sourceType` to `parserOptions` ([#3061], thanks [@michaelfaith])
 
 ## v2.11.0 - 2024-09-05
 
@@ -168,6 +170,7 @@ Yanked due to critical issue with cache key resulting from #839.
 ### Fixed
 - `unambiguous.test()` regex is now properly in multiline mode
 
+[#3061]: https://github.com/import-js/eslint-plugin-import/pull/3061
 [#3057]: https://github.com/import-js/eslint-plugin-import/pull/3057
 [#3049]: https://github.com/import-js/eslint-plugin-import/pull/3049
 [#3039]: https://github.com/import-js/eslint-plugin-import/pull/3039
diff --git a/utils/parse.js b/utils/parse.js
index 21a443ec..03022ac4 100644
--- a/utils/parse.js
+++ b/utils/parse.js
@@ -29,12 +29,18 @@ function keysFromParser(parserPath, parserInstance, parsedResult) {
   if (parsedResult && parsedResult.visitorKeys) {
     return parsedResult.visitorKeys;
   }
-  if (typeof parserPath === 'string' && (/.*espree.*/).test(parserPath)) {
-    return parserInstance.VisitorKeys;
-  }
+  // The old babel parser doesn't have a `parseForESLint` eslint function, so we don't end
+  // up with a `parsedResult` here.  It also doesn't expose the visitor keys on the parser itself,
+  // so we have to try and infer the visitor-keys module from the parserPath.
+  // This is NOT supported in flat config!
   if (typeof parserPath === 'string' && (/.*babel-eslint.*/).test(parserPath)) {
     return getBabelEslintVisitorKeys(parserPath);
   }
+  // The espree parser doesn't have the `parseForESLint` function, so we don't end up with a
+  // `parsedResult` here, but it does expose the visitor keys on the parser instance that we can use.
+  if (parserInstance && parserInstance.VisitorKeys) {
+    return parserInstance.VisitorKeys;
+  }
   return null;
 }
 
@@ -107,7 +113,8 @@ exports.default = function parse(path, content, context) {
   if (context == null) { throw new Error('need context to parse properly'); }
 
   // ESLint in "flat" mode only sets context.languageOptions.parserOptions
-  let parserOptions = context.languageOptions && context.languageOptions.parserOptions || context.parserOptions;
+  const languageOptions = context.languageOptions;
+  let parserOptions = languageOptions && languageOptions.parserOptions || context.parserOptions;
   const parserOrPath = getParser(path, context);
 
   if (!parserOrPath) { throw new Error('parserPath or languageOptions.parser is required!'); }
@@ -138,6 +145,17 @@ exports.default = function parse(path, content, context) {
   delete parserOptions.project;
   delete parserOptions.projects;
 
+  // If this is a flat config, we need to add ecmaVersion and sourceType (if present) from languageOptions
+  if (languageOptions && languageOptions.ecmaVersion) {
+    parserOptions.ecmaVersion = languageOptions.ecmaVersion;
+  }
+  if (languageOptions && languageOptions.sourceType) {
+    // @ts-expect-error languageOptions is from the flatConfig Linter type in 8.57 while parserOptions is not.
+    // Non-flat config parserOptions.sourceType doesn't have "commonjs" in the type.  Once upgraded to v9 types,
+    // they'll be the same and this expect-error should be removed.
+    parserOptions.sourceType = languageOptions.sourceType;
+  }
+
   // require the parser relative to the main module (i.e., ESLint)
   const parser = typeof parserOrPath === 'string' ? moduleRequire(parserOrPath) : parserOrPath;
 


Test patch:
diff --git a/tests/src/core/parse.js b/tests/src/core/parse.js
index 275b9398..b2132689 100644
--- a/tests/src/core/parse.js
+++ b/tests/src/core/parse.js
@@ -138,4 +138,18 @@ describe('parse(content, { settings, ecmaFeatures })', function () {
     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);
   });
+
+  it('passes ecmaVersion and sourceType from languageOptions to parser', () => {
+    const parseSpy = sinon.spy();
+    const languageOptions = { ecmaVersion: 'latest', sourceType: 'module', parserOptions: { ecmaFeatures: { jsx: true } } };
+    parseStubParser.parse = parseSpy;
+    parse(path, content, { settings: {}, parserPath: parseStubParserPath, languageOptions });
+    expect(parseSpy.args[0][1], 'custom parser to clone the parserOptions object').to.not.equal(languageOptions);
+    expect(parseSpy.args[0][1], 'custom parser to get ecmaFeatures in parserOptions which is a clone of ecmaFeatures passed in')
+      .to.have.property('ecmaFeatures')
+      .that.is.eql(languageOptions.parserOptions.ecmaFeatures)
+      .and.is.not.equal(languageOptions.parserOptions.ecmaFeatures);
+    expect(parseSpy.args[0][1], 'custom parser to get ecmaVersion in parserOptions from languageOptions').to.have.property('ecmaVersion', languageOptions.ecmaVersion);
+    expect(parseSpy.args[0][1], 'custom parser to get sourceType in parserOptions from languageOptions').to.have.property('sourceType', languageOptions.sourceType);
+  });
 });