Skip to content

Commit 0f2ed8c

Browse files
committed
[Fix] importType: handle symlinked external modules whose realpath is outside node_modules
When resolve v2 realpaths symlinked packages, the resolved path may fall outside `node_modules`, causing `isExternalPath` to misclassify them as internal. Add `isInExternalModuleFolder` to walk up the directory tree and check if the base module exists in any external module folder.
1 parent 47f732e commit 0f2ed8c

2 files changed

Lines changed: 38 additions & 0 deletions

File tree

src/core/importType.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { isAbsolute as nodeIsAbsolute, relative, resolve as nodeResolve } from 'path';
2+
import { statSync } from 'fs';
23
import isCoreModule from 'is-core-module';
34

45
import resolve from 'eslint-module-utils/resolve';
@@ -108,6 +109,31 @@ function isExternalLookingName(name) {
108109
return isModule(name) || isScoped(name);
109110
}
110111

112+
function isInExternalModuleFolder(name, context) {
113+
const packagePath = getContextPackagePath(context);
114+
const { settings } = context;
115+
const folders = settings && settings['import/external-module-folders'] || ['node_modules'];
116+
const base = baseModule(name);
117+
118+
return folders.some((folder) => {
119+
if (nodeIsAbsolute(folder)) {
120+
try { statSync(nodeResolve(folder, base)); return true; } catch (e) { return false; }
121+
}
122+
// Walk up directories checking each external module folder
123+
let dir = packagePath;
124+
while (true) { // eslint-disable-line no-constant-condition
125+
try {
126+
statSync(nodeResolve(dir, folder, base));
127+
return true;
128+
} catch (e) { /* continue */ }
129+
const parent = nodeResolve(dir, '..');
130+
if (parent === dir) { break; }
131+
dir = parent;
132+
}
133+
return false;
134+
});
135+
}
136+
111137
function typeTest(name, context, path) {
112138
const { settings } = context;
113139
if (isInternalRegexMatch(name, settings)) { return 'internal'; }
@@ -117,6 +143,9 @@ function typeTest(name, context, path) {
117143
if (isIndex(name, settings, path)) { return 'index'; }
118144
if (isRelativeToSibling(name, settings, path)) { return 'sibling'; }
119145
if (isExternalPath(path, context)) { return 'external'; }
146+
// Symlinked external modules may realpath outside node_modules.
147+
// Check if the base module exists in any external module folder.
148+
if (path && isExternalLookingName(name) && isInExternalModuleFolder(name, context)) { return 'external'; }
120149
if (isInternalPath(path, context)) { return 'internal'; }
121150
if (isExternalLookingName(name)) { return 'external'; }
122151
return 'unknown';

tests/src/core/importType.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ describe('importType(name)', function () {
4848
expect(importType('@eslint/import-test-order-redirect-scoped/module', context)).to.equal('external');
4949
});
5050

51+
it("should return 'external' for symlinked modules in node_modules even if realpath is outside", function () {
52+
// eslint-import-test-order-redirect is a symlink in node_modules/ pointing
53+
// to tests/files/order-redirect/. When resolve follows symlinks (the default),
54+
// the resolved path is under tests/files/, not node_modules/. The module should
55+
// still be classified as external because it exists in node_modules/.
56+
expect(importType('eslint-import-test-order-redirect', context)).to.equal('external');
57+
expect(importType('@eslint/import-test-order-redirect-scoped', context)).to.equal('external');
58+
});
59+
5160
it("should return 'internal' for non-builtins resolved outside of node_modules", function () {
5261
const pathContext = testContext({ 'import/resolver': { node: { paths: [pathToTestFiles] } } });
5362
expect(importType('importType', pathContext)).to.equal('internal');

0 commit comments

Comments
 (0)