Skip to content

Commit ef980d4

Browse files
ombeneljharb
authored andcommitted
[Fix] importType: properly resolve @/*-aliased imports as internal
1 parent e156316 commit ef980d4

File tree

8 files changed

+154
-41
lines changed

8 files changed

+154
-41
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
99
### Fixed
1010
- `importType`: avoid crashing on a non-string' ([#2305], thanks [@ljharb])
1111
- [`first`]: prevent crash when parsing angular templates ([#2210], thanks [@ljharb])
12+
- `importType`: properly resolve `@/*`-aliased imports as internal ([#2334], thanks [@ombene])
1213

1314
### Changed
1415
- [`no-default-import`]: report on the token "default" instead of the entire node ([#2299], thanks [@pmcelhaney])
@@ -950,6 +951,7 @@ for info on changes for earlier releases.
950951

951952
[`memo-parser`]: ./memo-parser/README.md
952953

954+
[#2334]: https://github.com/import-js/eslint-plugin-import/pull/2334
953955
[#2305]: https://github.com/import-js/eslint-plugin-import/pull/2305
954956
[#2299]: https://github.com/import-js/eslint-plugin-import/pull/2299
955957
[#2297]: https://github.com/import-js/eslint-plugin-import/pull/2297
@@ -1571,6 +1573,7 @@ for info on changes for earlier releases.
15711573
[@noelebrun]: https://github.com/noelebrun
15721574
[@ntdb]: https://github.com/ntdb
15731575
[@nwalters512]: https://github.com/nwalters512
1576+
[@ombene]: https://github.com/ombene
15741577
[@ota-meshi]: https://github.com/ota-meshi
15751578
[@panrafal]: https://github.com/panrafal
15761579
[@paztis]: https://github.com/paztis

src/core/importType.js

+50-28
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ function baseModule(name) {
1313
return pkg;
1414
}
1515

16+
function isInternalRegexMatch(name, settings) {
17+
const internalScope = (settings && settings['import/internal-regex']);
18+
return internalScope && new RegExp(internalScope).test(name);
19+
}
20+
1621
export function isAbsolute(name) {
1722
return typeof name === 'string' && nodeIsAbsolute(name);
1823
}
@@ -25,33 +30,18 @@ export function isBuiltIn(name, settings, path) {
2530
return isCoreModule(base) || extras.indexOf(base) > -1;
2631
}
2732

28-
export function isExternalModule(name, settings, path, context) {
29-
if (arguments.length < 4) {
30-
throw new TypeError('isExternalModule: name, settings, path, and context are all required');
33+
export function isExternalModule(name, path, context) {
34+
if (arguments.length < 3) {
35+
throw new TypeError('isExternalModule: name, path, and context are all required');
3136
}
32-
return (isModule(name) || isScoped(name)) && isExternalPath(name, settings, path, getContextPackagePath(context));
33-
}
34-
35-
export function isExternalModuleMain(name, settings, path, context) {
36-
return isModuleMain(name) && isExternalPath(name, settings, path, getContextPackagePath(context));
37+
return (isModule(name) || isScoped(name)) && typeTest(name, context, path) === 'external';
3738
}
3839

39-
function isExternalPath(name, settings, path, packagePath) {
40-
const internalScope = (settings && settings['import/internal-regex']);
41-
if (internalScope && new RegExp(internalScope).test(name)) {
42-
return false;
43-
}
44-
45-
if (!path || relative(packagePath, path).startsWith('..')) {
46-
return true;
40+
export function isExternalModuleMain(name, path, context) {
41+
if (arguments.length < 3) {
42+
throw new TypeError('isExternalModule: name, path, and context are all required');
4743
}
48-
49-
const folders = (settings && settings['import/external-module-folders']) || ['node_modules'];
50-
return folders.some((folder) => {
51-
const folderPath = nodeResolve(packagePath, folder);
52-
const relativePath = relative(folderPath, path);
53-
return !relativePath.startsWith('..');
54-
});
44+
return isModuleMain(name) && typeTest(name, context, path) === 'external';
5545
}
5646

5747
const moduleRegExp = /^\w/;
@@ -87,17 +77,49 @@ function isRelativeToSibling(name) {
8777
return /^\.[\\/]/.test(name);
8878
}
8979

90-
function typeTest(name, context, path) {
80+
function isExternalPath(path, context) {
81+
if (!path) {
82+
return false;
83+
}
84+
85+
const { settings } = context;
86+
const packagePath = getContextPackagePath(context);
87+
88+
if (relative(packagePath, path).startsWith('..')) {
89+
return true;
90+
}
91+
92+
const folders = (settings && settings['import/external-module-folders']) || ['node_modules'];
93+
return folders.some((folder) => {
94+
const folderPath = nodeResolve(packagePath, folder);
95+
const relativePath = relative(folderPath, path);
96+
return !relativePath.startsWith('..');
97+
});
98+
}
99+
100+
function isInternalPath(path, context) {
101+
if (!path) {
102+
return false;
103+
}
104+
const packagePath = getContextPackagePath(context);
105+
return !relative(packagePath, path).startsWith('../');
106+
}
107+
108+
function isExternalLookingName(name) {
109+
return isModule(name) || isScoped(name);
110+
}
111+
112+
function typeTest(name, context, path ) {
91113
const { settings } = context;
114+
if (isInternalRegexMatch(name, settings)) { return 'internal'; }
92115
if (isAbsolute(name, settings, path)) { return 'absolute'; }
93116
if (isBuiltIn(name, settings, path)) { return 'builtin'; }
94-
if (isModule(name, settings, path) || isScoped(name, settings, path)) {
95-
const packagePath = getContextPackagePath(context);
96-
return isExternalPath(name, settings, path, packagePath) ? 'external' : 'internal';
97-
}
98117
if (isRelativeToParent(name, settings, path)) { return 'parent'; }
99118
if (isIndex(name, settings, path)) { return 'index'; }
100119
if (isRelativeToSibling(name, settings, path)) { return 'sibling'; }
120+
if (isExternalPath(path, context)) { return 'external'; }
121+
if (isInternalPath(path, context)) { return 'internal'; }
122+
if (isExternalLookingName(name)) { return 'external'; }
101123
return 'unknown';
102124
}
103125

src/rules/extensions.js

-1
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,6 @@ module.exports = {
159159
// determine if this is a module
160160
const isPackage = isExternalModule(
161161
importPath,
162-
context.settings,
163162
resolve(importPath, context),
164163
context,
165164
) || isScoped(importPath);

src/rules/no-cycle.js

-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ module.exports = {
4444
const maxDepth = typeof options.maxDepth === 'number' ? options.maxDepth : Infinity;
4545
const ignoreModule = (name) => options.ignoreExternal && isExternalModule(
4646
name,
47-
context.settings,
4847
resolve(name, context),
4948
context,
5049
);

tests/src/core/importType.js

+22-10
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,17 @@ describe('importType(name)', function () {
7575
expect(importType('constants', pathContext)).to.equal('internal');
7676
});
7777

78+
it("should return 'internal' for aliased internal modules that are found, even if they are not discernible as scoped", function () {
79+
// `@` for internal modules is a common alias and is different from scoped names.
80+
// Scoped names are prepended with `@` (e.g. `@scoped/some-file.js`) whereas `@`
81+
// as an alias by itelf is the full root name (e.g. `@/some-file.js`).
82+
const alias = { '@': path.join(pathToTestFiles, 'internal-modules') };
83+
const webpackConfig = { resolve: { alias } };
84+
const pathContext = testContext({ 'import/resolver': { webpack: { config: webpackConfig } } });
85+
expect(importType('@/api/service', pathContext)).to.equal('internal');
86+
expect(importType('@/does-not-exist', pathContext)).to.equal('unknown');
87+
});
88+
7889
it("should return 'parent' for internal modules that go through the parent", function () {
7990
expect(importType('../foo', context)).to.equal('parent');
8091
expect(importType('../../foo', context)).to.equal('parent');
@@ -100,6 +111,7 @@ describe('importType(name)', function () {
100111
it("should return 'unknown' for any unhandled cases", function () {
101112
expect(importType(' /malformed', context)).to.equal('unknown');
102113
expect(importType(' foo', context)).to.equal('unknown');
114+
expect(importType('-/no-such-path', context)).to.equal('unknown');
103115
});
104116

105117
it("should return 'builtin' for additional core modules", function () {
@@ -233,20 +245,20 @@ describe('importType(name)', function () {
233245

234246
it('`isExternalModule` works with windows directory separator', function () {
235247
const context = testContext();
236-
expect(isExternalModule('foo', {}, 'E:\\path\\to\\node_modules\\foo', context)).to.equal(true);
237-
expect(isExternalModule('@foo/bar', {}, 'E:\\path\\to\\node_modules\\@foo\\bar', context)).to.equal(true);
238-
expect(isExternalModule('foo', {
239-
'import/external-module-folders': ['E:\\path\\to\\node_modules'],
240-
}, 'E:\\path\\to\\node_modules\\foo', context)).to.equal(true);
248+
expect(isExternalModule('foo', 'E:\\path\\to\\node_modules\\foo', context)).to.equal(true);
249+
expect(isExternalModule('@foo/bar', 'E:\\path\\to\\node_modules\\@foo\\bar', context)).to.equal(true);
250+
expect(isExternalModule('foo', 'E:\\path\\to\\node_modules\\foo', testContext({
251+
settings: { 'import/external-module-folders': ['E:\\path\\to\\node_modules'] },
252+
}))).to.equal(true);
241253
});
242254

243255
it('`isExternalModule` works with unix directory separator', function () {
244256
const context = testContext();
245-
expect(isExternalModule('foo', {}, '/path/to/node_modules/foo', context)).to.equal(true);
246-
expect(isExternalModule('@foo/bar', {}, '/path/to/node_modules/@foo/bar', context)).to.equal(true);
247-
expect(isExternalModule('foo', {
248-
'import/external-module-folders': ['/path/to/node_modules'],
249-
}, '/path/to/node_modules/foo', context)).to.equal(true);
257+
expect(isExternalModule('foo', '/path/to/node_modules/foo', context)).to.equal(true);
258+
expect(isExternalModule('@foo/bar', '/path/to/node_modules/@foo/bar', context)).to.equal(true);
259+
expect(isExternalModule('foo', '/path/to/node_modules/foo', testContext({
260+
settings: { 'import/external-module-folders': ['/path/to/node_modules'] },
261+
}))).to.equal(true);
250262
});
251263

252264
it('correctly identifies scoped modules with `isScoped`', () => {

tests/src/rules/no-extraneous-dependencies.js

+17
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,23 @@ ruleTester.run('no-extraneous-dependencies', rule, {
164164
`,
165165
settings: { 'import/resolver': 'webpack' },
166166
}),
167+
168+
test({
169+
code: 'import "@custom-internal-alias/api/service";',
170+
settings: {
171+
'import/resolver': {
172+
webpack: {
173+
config: {
174+
resolve: {
175+
alias: {
176+
'@custom-internal-alias': testFilePath('internal-modules'),
177+
},
178+
},
179+
},
180+
},
181+
},
182+
},
183+
}),
167184
],
168185
invalid: [
169186
test({

tests/src/rules/no-internal-modules.js

+24
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,30 @@ ruleTester.run('no-internal-modules', rule, {
304304
column: 8,
305305
} ],
306306
}),
307+
test({
308+
code: 'import "@/api/service";',
309+
options: [ {
310+
forbid: [ '**/api/*' ],
311+
} ],
312+
errors: [ {
313+
message: 'Reaching to "@/api/service" is not allowed.',
314+
line: 1,
315+
column: 8,
316+
} ],
317+
settings: {
318+
'import/resolver': {
319+
webpack: {
320+
config: {
321+
resolve: {
322+
alias: {
323+
'@': testFilePath('internal-modules'),
324+
},
325+
},
326+
},
327+
},
328+
},
329+
},
330+
}),
307331
// exports
308332
test({
309333
code: 'export * from "./plugin2/index.js";\nexport * from "./plugin2/app/index"',

tests/src/rules/order.js

+38-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { test, getTSParsers, getNonDefaultParsers } from '../utils';
1+
import { test, getTSParsers, getNonDefaultParsers, testFilePath } from '../utils';
22

33
import { RuleTester } from 'eslint';
44
import eslintPkg from 'eslint/package.json';
@@ -847,6 +847,43 @@ ruleTester.run('order', rule, {
847847
],
848848
}),
849849
]),
850+
// Using `@/*` to alias internal modules
851+
test({
852+
code: `
853+
import fs from 'fs';
854+
855+
import express from 'express';
856+
857+
import service from '@/api/service';
858+
859+
import fooParent from '../foo';
860+
861+
import fooSibling from './foo';
862+
863+
import index from './';
864+
865+
import internalDoesNotExistSoIsUnknown from '@/does-not-exist';
866+
`,
867+
options: [
868+
{
869+
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'unknown'],
870+
'newlines-between': 'always',
871+
},
872+
],
873+
settings: {
874+
'import/resolver': {
875+
webpack: {
876+
config: {
877+
resolve: {
878+
alias: {
879+
'@': testFilePath('internal-modules'),
880+
},
881+
},
882+
},
883+
},
884+
},
885+
},
886+
}),
850887
],
851888
invalid: [
852889
// builtin before external module (require)

0 commit comments

Comments
 (0)