Repository: import-js/eslint-plugin-import
Pull request #2160 (2021-07-21 14:02:19)

Use getPhysicalFilename() instead of getFilename() if available (ESLint 7.28+)
Starting with 7.28.0, ESLint has a new feature [distinguishing the physical filename from the virtual filename](https://eslint.org/blog/2021/06/eslint-v7.28.0-released#contextgetphysicalfilename).

> ## context.getPhysicalFilename()
> Rules can now use the new method getPhysicalFilename() on the context object to get the full path of the file on disk without any code block information.
>
> The difference between getPhysicalFilename and getFilename is observable when ESLint is used with processors:
>
> ```js
> context.getPhysicalFilename() // "/project/example.md"        - original file
> context.getFilename()         // "/project/example.md/0_0.js" - virtual filename assigned to a code block
> ```

When there's a difference, this plugin needs the real, physical filename. (See 
https://github.com/eslint/eslint/issues/11989) 

I ran into the problem when I noticed `no-unresolved` was yielding false positives inside of code blocks in Storybook / Markdown files and helped push the necessary change through in ESLint. 

To maintain backwards compatibility, this change uses `context.getPhysicalFilename()` if available and otherwise falls back to `getFilename()`. 







Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 29452930..e8173955 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel
 - [`no-duplicates`]: ensure autofix avoids excessive newlines ([#2028], thanks [@ertrzyiks])
 - [`extensions`]: avoid crashing on partially typed import/export statements ([#2118], thanks [@ljharb])
 - [`no-extraneous-dependencies`]: add ESM intermediate package.json support] ([#2121], thanks [@paztis])
+ - Use `context.getPhysicalFilename()` when available (ESLint 7.28+) ([#2160], thanks [@pmcelhaney])
 
 ### Changed
 - [Docs] `extensions`: removed incorrect cases ([#2138], thanks [@wenfangdu])
@@ -809,6 +810,7 @@ for info on changes for earlier releases.
 
 [`memo-parser`]: ./memo-parser/README.md
 
+[#2160]: https://github.com/benmosher/eslint-plugin-import/pull/2160
 [#2158]: https://github.com/benmosher/eslint-plugin-import/pull/2158
 [#2138]: https://github.com/benmosher/eslint-plugin-import/pull/2138
 [#2121]: https://github.com/benmosher/eslint-plugin-import/pull/2121
@@ -1374,6 +1376,7 @@ for info on changes for earlier releases.
 [@paztis]: https://github.com/paztis
 [@pcorpet]: https://github.com/pcorpet
 [@Pessimistress]: https://github.com/Pessimistress
+[@pmcelhaney]: https://github.com/pmcelhaney
 [@preco21]: https://github.com/preco21
 [@pzhine]: https://github.com/pzhine
 [@ramasilveyra]: https://github.com/ramasilveyra
diff --git a/src/core/packagePath.js b/src/core/packagePath.js
index 315ec091..a8c3c676 100644
--- a/src/core/packagePath.js
+++ b/src/core/packagePath.js
@@ -4,7 +4,7 @@ import readPkgUp from 'read-pkg-up';
 
 
 export function getContextPackagePath(context) {
-  return getFilePackagePath(context.getFilename());
+  return getFilePackagePath(context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename());
 }
 
 export function getFilePackagePath(filePath) {
diff --git a/src/rules/named.js b/src/rules/named.js
index c4ce5ea9..7dee06ce 100644
--- a/src/rules/named.js
+++ b/src/rules/named.js
@@ -43,7 +43,7 @@ module.exports = {
         if (!deepLookup.found) {
           if (deepLookup.path.length > 1) {
             const deepPath = deepLookup.path
-              .map(i => path.relative(path.dirname(context.getFilename()), i.path))
+              .map(i => path.relative(path.dirname(context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename()), i.path))
               .join(' -> ');
 
             context.report(im[key],
diff --git a/src/rules/newline-after-import.js b/src/rules/newline-after-import.js
index f9a81784..cf5dce83 100644
--- a/src/rules/newline-after-import.js
+++ b/src/rules/newline-after-import.js
@@ -48,7 +48,7 @@ function isExportDefaultClass(node) {
 }
 
 function isExportNameClass(node) {
-  
+
   return node.type === 'ExportNamedDeclaration' && node.declaration && node.declaration.type === 'ClassDeclaration';
 }
 
@@ -124,7 +124,7 @@ after ${type} statement not followed by another ${type}.`,
       const { parent } = node;
       const nodePosition = parent.body.indexOf(node);
       const nextNode = parent.body[nodePosition + 1];
-        
+
       // skip "export import"s
       if (node.type === 'TSImportEqualsDeclaration' && node.isExport) {
         return;
@@ -144,7 +144,7 @@ after ${type} statement not followed by another ${type}.`,
         }
       },
       'Program:exit': function () {
-        log('exit processing for', context.getFilename());
+        log('exit processing for', context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename());
         const scopeBody = getScopeBody(context.getScope());
         log('got scope:', scopeBody);
 
diff --git a/src/rules/no-cycle.js b/src/rules/no-cycle.js
index 9d9a28cd..ec4cfeae 100644
--- a/src/rules/no-cycle.js
+++ b/src/rules/no-cycle.js
@@ -37,7 +37,7 @@ module.exports = {
   },
 
   create: function (context) {
-    const myPath = context.getFilename();
+    const myPath = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename();
     if (myPath === '<text>') return {}; // can't cycle-check a non-file
 
     const options = context.options[0] || {};
diff --git a/src/rules/no-extraneous-dependencies.js b/src/rules/no-extraneous-dependencies.js
index 7e46ac34..9403931f 100644
--- a/src/rules/no-extraneous-dependencies.js
+++ b/src/rules/no-extraneous-dependencies.js
@@ -69,7 +69,7 @@ function getDependencies(context, packageDir) {
       Object.assign(
         packageContent,
         extractDepFields(
-          readPkgUp.sync({ cwd: context.getFilename(), normalize: false }).pkg
+          readPkgUp.sync({ cwd: context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename(), normalize: false }).pkg
         )
       );
     }
@@ -254,7 +254,7 @@ module.exports = {
 
   create: function (context) {
     const options = context.options[0] || {};
-    const filename = context.getFilename();
+    const filename = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename();
     const deps = getDependencies(context, options.packageDir) || extractDepFields({});
 
     const depsOptions = {
diff --git a/src/rules/no-import-module-exports.js b/src/rules/no-import-module-exports.js
index 8ce5f4c9..50ba212c 100644
--- a/src/rules/no-import-module-exports.js
+++ b/src/rules/no-import-module-exports.js
@@ -3,7 +3,7 @@ import path from 'path';
 import pkgUp from 'pkg-up';
 
 function getEntryPoint(context) {
-  const pkgPath = pkgUp.sync(context.getFilename());
+  const pkgPath = pkgUp.sync(context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename());
   try {
     return require.resolve(path.dirname(pkgPath));
   } catch (error) {
@@ -39,7 +39,7 @@ module.exports = {
     let alreadyReported = false;
 
     function report(node) {
-      const fileName = context.getFilename();
+      const fileName = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename();
       const isEntryPoint = entryPoint === fileName;
       const isIdentifier = node.object.type === 'Identifier';
       const hasKeywords = (/^(module|exports)$/).test(node.object.name);
diff --git a/src/rules/no-relative-packages.js b/src/rules/no-relative-packages.js
index a654c083..90c1ecc7 100644
--- a/src/rules/no-relative-packages.js
+++ b/src/rules/no-relative-packages.js
@@ -21,7 +21,7 @@ function checkImportForRelativePackage(context, importPath, node) {
   }
 
   const resolvedImport = resolve(importPath, context);
-  const resolvedContext = context.getFilename();
+  const resolvedContext = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename();
 
   if (!resolvedImport || !resolvedContext) {
     return;
diff --git a/src/rules/no-relative-parent-imports.js b/src/rules/no-relative-parent-imports.js
index 9826da82..8e369627 100644
--- a/src/rules/no-relative-parent-imports.js
+++ b/src/rules/no-relative-parent-imports.js
@@ -15,7 +15,7 @@ module.exports = {
   },
 
   create: function noRelativePackages(context) {
-    const myPath = context.getFilename();
+    const myPath = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename();
     if (myPath === '<text>') return {}; // can't check a non-file
 
     function checkSourceValue(sourceNode) {
diff --git a/src/rules/no-restricted-paths.js b/src/rules/no-restricted-paths.js
index 6409ff57..058aa43e 100644
--- a/src/rules/no-restricted-paths.js
+++ b/src/rules/no-restricted-paths.js
@@ -52,7 +52,7 @@ module.exports = {
     const options = context.options[0] || {};
     const restrictedPaths = options.zones || [];
     const basePath = options.basePath || process.cwd();
-    const currentFilename = context.getFilename();
+    const currentFilename = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename();
     const matchingZones = restrictedPaths.filter((zone) => {
       const targetPath = path.resolve(basePath, zone.target);
 
diff --git a/src/rules/no-self-import.js b/src/rules/no-self-import.js
index a10be567..58c393b6 100644
--- a/src/rules/no-self-import.js
+++ b/src/rules/no-self-import.js
@@ -8,7 +8,7 @@ import moduleVisitor from 'eslint-module-utils/moduleVisitor';
 import docsUrl from '../docsUrl';
 
 function isImportingSelf(context, node, requireName) {
-  const filePath = context.getFilename();
+  const filePath = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename();
 
   // If the input is from stdin, this test can't fail
   if (filePath !== '<text>' && filePath === resolve(requireName, context)) {
diff --git a/src/rules/no-unassigned-import.js b/src/rules/no-unassigned-import.js
index ed292e91..37be903e 100644
--- a/src/rules/no-unassigned-import.js
+++ b/src/rules/no-unassigned-import.js
@@ -32,7 +32,7 @@ function testIsAllow(globs, filename, source) {
 
 function create(context) {
   const options = context.options[0] || {};
-  const filename = context.getFilename();
+  const filename = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename();
   const isAllow = source => testIsAllow(options.allow, filename, source);
 
   return {
diff --git a/src/rules/no-unused-modules.js b/src/rules/no-unused-modules.js
index 99b564ed..205259ee 100644
--- a/src/rules/no-unused-modules.js
+++ b/src/rules/no-unused-modules.js
@@ -463,7 +463,7 @@ module.exports = {
       doPreparation(src, ignoreExports, context);
     }
 
-    const file = context.getFilename();
+    const file = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename();
 
     const checkExportPresence = node => {
       if (!missingExports) {
diff --git a/src/rules/no-useless-path-segments.js b/src/rules/no-useless-path-segments.js
index b22aa947..a0586080 100644
--- a/src/rules/no-useless-path-segments.js
+++ b/src/rules/no-useless-path-segments.js
@@ -58,7 +58,7 @@ module.exports = {
   },
 
   create(context) {
-    const currentDir = path.dirname(context.getFilename());
+    const currentDir = path.dirname(context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename());
     const options = context.options[0];
 
     function checkSourceValue(source) {
diff --git a/utils/resolve.js b/utils/resolve.js
index ab9f13d5..f488ea79 100644
--- a/utils/resolve.js
+++ b/utils/resolve.js
@@ -217,7 +217,7 @@ const erroredContexts = new Set();
  */
 function resolve(p, context) {
   try {
-    return relative(p, context.getFilename(), context.settings);
+    return relative(p, context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename(), context.settings);
   } catch (err) {
     if (!erroredContexts.has(context)) {
       // The `err.stack` string starts with `err.name` followed by colon and `err.message`.


Test patch:
diff --git a/tests/src/core/getExports.js b/tests/src/core/getExports.js
index 5a9bdadb..74c8d9ee 100644
--- a/tests/src/core/getExports.js
+++ b/tests/src/core/getExports.js
@@ -11,11 +11,18 @@ import { getFilename } from '../utils';
 import * as unambiguous from 'eslint-module-utils/unambiguous';
 
 describe('ExportMap', function () {
-  const fakeContext = {
-    getFilename: getFilename,
-    settings: {},
-    parserPath: 'babel-eslint',
-  };
+  const fakeContext = Object.assign(
+    semver.satisfies(eslintPkg.version, '>= 7.28') ? {
+      getFilename: function () { throw new Error('Should call getPhysicalFilename() instead of getFilename()'); },
+      getPhysicalFilename: getFilename,
+    } : {
+      getFilename,
+    },
+    {
+      settings: {},
+      parserPath: 'babel-eslint',
+    },
+  );
 
   it('handles ExportAllDeclaration', function () {
     let imports;
diff --git a/tests/src/core/resolve.js b/tests/src/core/resolve.js
index b8deaa6d..ccfe5f6c 100644
--- a/tests/src/core/resolve.js
+++ b/tests/src/core/resolve.js
@@ -1,4 +1,6 @@
 import { expect } from 'chai';
+import eslintPkg from 'eslint/package.json';
+import semver from 'semver';
 
 import resolve, { CASE_SENSITIVE_FS, fileExistsWithCaseSync } from 'eslint-module-utils/resolve';
 import ModuleCache from 'eslint-module-utils/ModuleCache';
@@ -162,6 +164,158 @@ describe('resolve', function () {
     expect(testContextReports[0].loc).to.eql({ line: 1, column: 0 });
   });
 
+  // context.getPhysicalFilename() is available in ESLint 7.28+
+  (semver.satisfies(eslintPkg.version, '>= 7.28') ? describe : describe.skip)('getPhysicalFilename()', () => {
+    function unexpectedCallToGetFilename() {
+      throw new Error('Expected to call to getPhysicalFilename() instead of getFilename()');
+    }
+
+    it('resolves via a custom resolver with interface version 1', function () {
+      const testContext = utils.testContext({ 'import/resolver': './foo-bar-resolver-v1' });
+
+      expect(resolve( '../files/foo'
+        , Object.assign({}, testContext, { getFilename: unexpectedCallToGetFilename, getPhysicalFilename: function () { return utils.getFilename('foo.js'); } }),
+      )).to.equal(utils.testFilePath('./bar.jsx'));
+
+      expect(resolve( '../files/exception'
+        , Object.assign({}, testContext, { getFilename: unexpectedCallToGetFilename, getPhysicalFilename: function () { return utils.getFilename('exception.js'); } }),
+      )).to.equal(undefined);
+
+      expect(resolve( '../files/not-found'
+        , Object.assign({}, testContext, { getFilename: unexpectedCallToGetFilename, getPhysicalFilename: function () { return utils.getFilename('not-found.js'); } }),
+      )).to.equal(undefined);
+    });
+
+    it('resolves via a custom resolver with interface version 1 assumed if not specified', function () {
+      const testContext = utils.testContext({ 'import/resolver': './foo-bar-resolver-no-version' });
+
+      expect(resolve( '../files/foo'
+        , Object.assign({}, testContext, { getFilename: unexpectedCallToGetFilename, getPhysicalFilename: function () { return utils.getFilename('foo.js'); } }),
+      )).to.equal(utils.testFilePath('./bar.jsx'));
+
+      expect(resolve( '../files/exception'
+        , Object.assign({}, testContext, { getFilename: unexpectedCallToGetFilename, getPhysicalFilename: function () { return utils.getFilename('exception.js'); } }),
+      )).to.equal(undefined);
+
+      expect(resolve( '../files/not-found'
+        , Object.assign({}, testContext, { getFilename: unexpectedCallToGetFilename, getPhysicalFilename: function () { return utils.getFilename('not-found.js'); } }),
+      )).to.equal(undefined);
+    });
+
+    it('resolves via a custom resolver with interface version 2', function () {
+      const testContext = utils.testContext({ 'import/resolver': './foo-bar-resolver-v2' });
+      const testContextReports = [];
+      testContext.report = function (reportInfo) {
+        testContextReports.push(reportInfo);
+      };
+
+      expect(resolve( '../files/foo'
+        , Object.assign({}, testContext, { getFilename: unexpectedCallToGetFilename, getPhysicalFilename: function () { return utils.getFilename('foo.js'); } }),
+      )).to.equal(utils.testFilePath('./bar.jsx'));
+
+      testContextReports.length = 0;
+      expect(resolve( '../files/exception'
+        , Object.assign({}, testContext, { getFilename: unexpectedCallToGetFilename, getPhysicalFilename: function () { return utils.getFilename('exception.js'); } }),
+      )).to.equal(undefined);
+      expect(testContextReports[0]).to.be.an('object');
+      expect(replaceErrorStackForTest(testContextReports[0].message)).to.equal('Resolve error: foo-bar-resolver-v2 resolve test exception\n<stack-was-here>');
+      expect(testContextReports[0].loc).to.eql({ line: 1, column: 0 });
+
+      testContextReports.length = 0;
+      expect(resolve( '../files/not-found'
+        , Object.assign({}, testContext, { getFilename: unexpectedCallToGetFilename, getPhysicalFilename: function () { return utils.getFilename('not-found.js'); } }),
+      )).to.equal(undefined);
+      expect(testContextReports.length).to.equal(0);
+    });
+
+    it('respects import/resolver as array of strings', function () {
+      const testContext = utils.testContext({ 'import/resolver': [ './foo-bar-resolver-v2', './foo-bar-resolver-v1' ] });
+
+      expect(resolve( '../files/foo'
+        , Object.assign({}, testContext, { getFilename: unexpectedCallToGetFilename, getPhysicalFilename: function () { return utils.getFilename('foo.js'); } }),
+      )).to.equal(utils.testFilePath('./bar.jsx'));
+    });
+
+    it('respects import/resolver as object', function () {
+      const testContext = utils.testContext({ 'import/resolver': { './foo-bar-resolver-v2': {} } });
+
+      expect(resolve( '../files/foo'
+        , Object.assign({}, testContext, { getFilename: unexpectedCallToGetFilename, getPhysicalFilename: function () { return utils.getFilename('foo.js'); } }),
+      )).to.equal(utils.testFilePath('./bar.jsx'));
+    });
+
+    it('respects import/resolver as array of objects', function () {
+      const testContext = utils.testContext({ 'import/resolver': [ { './foo-bar-resolver-v2': {} }, { './foo-bar-resolver-v1': {} } ] });
+
+      expect(resolve( '../files/foo'
+        , Object.assign({}, testContext, { getFilename: unexpectedCallToGetFilename, getPhysicalFilename: function () { return utils.getFilename('foo.js'); } }),
+      )).to.equal(utils.testFilePath('./bar.jsx'));
+    });
+
+    it('finds resolvers from the source files rather than eslint-module-utils', function () {
+      const testContext = utils.testContext({ 'import/resolver': { 'foo': {} } });
+
+      expect(resolve( '../files/foo'
+        , Object.assign({}, testContext, { getFilename: unexpectedCallToGetFilename, getPhysicalFilename: function () { return utils.getFilename('foo.js'); } }),
+      )).to.equal(utils.testFilePath('./bar.jsx'));
+    });
+
+    it('reports invalid import/resolver config', function () {
+      const testContext = utils.testContext({ 'import/resolver': 123.456 });
+      const testContextReports = [];
+      testContext.report = function (reportInfo) {
+        testContextReports.push(reportInfo);
+      };
+
+      testContextReports.length = 0;
+      expect(resolve( '../files/foo'
+        , Object.assign({}, testContext, { getFilename: unexpectedCallToGetFilename, getPhysicalFilename: function () { return utils.getFilename('foo.js'); } }),
+      )).to.equal(undefined);
+      expect(testContextReports[0]).to.be.an('object');
+      expect(testContextReports[0].message).to.equal('Resolve error: invalid resolver config');
+      expect(testContextReports[0].loc).to.eql({ line: 1, column: 0 });
+    });
+
+    it('reports loaded resolver with invalid interface', function () {
+      const resolverName = './foo-bar-resolver-invalid';
+      const testContext = utils.testContext({ 'import/resolver': resolverName });
+      const testContextReports = [];
+      testContext.report = function (reportInfo) {
+        testContextReports.push(reportInfo);
+      };
+      testContextReports.length = 0;
+      expect(resolve( '../files/foo'
+        , Object.assign({}, testContext, { getFilename: unexpectedCallToGetFilename, getPhysicalFilename: function () { return utils.getFilename('foo.js'); } }),
+      )).to.equal(undefined);
+      expect(testContextReports[0]).to.be.an('object');
+      expect(testContextReports[0].message).to.equal(`Resolve error: ${resolverName} with invalid interface loaded as resolver`);
+      expect(testContextReports[0].loc).to.eql({ line: 1, column: 0 });
+    });
+
+    it('respects import/resolve extensions', function () {
+      const testContext = utils.testContext({ 'import/resolve': { 'extensions': ['.jsx'] } });
+
+      expect(resolve( './jsx/MyCoolComponent'
+        , testContext,
+      )).to.equal(utils.testFilePath('./jsx/MyCoolComponent.jsx'));
+    });
+
+    it('reports load exception in a user resolver', function () {
+      const testContext = utils.testContext({ 'import/resolver': './load-error-resolver' });
+      const testContextReports = [];
+      testContext.report = function (reportInfo) {
+        testContextReports.push(reportInfo);
+      };
+
+      expect(resolve( '../files/exception'
+        , Object.assign({}, testContext, { getFilename: unexpectedCallToGetFilename, getPhysicalFilename: function () { return utils.getFilename('exception.js'); } }),
+      )).to.equal(undefined);
+      expect(testContextReports[0]).to.be.an('object');
+      expect(replaceErrorStackForTest(testContextReports[0].message)).to.equal('Resolve error: SyntaxError: TEST SYNTAX ERROR\n<stack-was-here>');
+      expect(testContextReports[0].loc).to.eql({ line: 1, column: 0 });
+    });
+  });
+
   const caseDescribe = (!CASE_SENSITIVE_FS ? describe : describe.skip);
   caseDescribe('case sensitivity', function () {
     let file;