Skip to content

Commit 802ce7d

Browse files
JEROMEHljharb
authored andcommitted
[Fix] extensions/no-cycle/no-extraneous-dependencies: Correct module real path resolution
add real support of isAbsolute (windows + unix support) importType refactoring: use the real resolved package path to check if external of internal, and not the name only like before: in case of monorepo, external modules are not under node_modules due to symlink but still out of the module. correct tests node_modules dependencies to really provide teh package.json like in real usage correct no-extraneous-dependencies rule: get the real name from the resolved package.json. If not aliased imports (alias/react for example) will not be correctly interpreted change path import add real support of isAbsolute (windows + unix support) correct no-extraneous-dependencies rule: get the real name from the resolved package.json. If not aliased imports (alias/react for example) will not be correctly interpreted even externals like "a/file.js" must not use extension. only module names like 'module.js' and '@scope/module.js' are allowed correct bad external definition: must be the folder path under the root of the module. Here the module root is test folder, not cycles folder
1 parent e22fc53 commit 802ce7d

File tree

31 files changed

+140
-67
lines changed

31 files changed

+140
-67
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel
1616
- [`no-cycle`]: fix perf regression ([#1944], thanks [@Blasz])
1717
- [`first`]: fix handling of `import = require` ([#1963], thanks [@MatthiasKunnen])
1818
- [`no-cycle`]/[`extensions`]: fix isExternalModule usage ([#1696], thanks [@paztis])
19+
- [`extensions`]/[`no-cycle`]/[`no-extraneous-dependencies`]: Correct module real path resolution ([#1696], thanks [@paztis])
1920

2021
### Changed
2122
- [Generic Import Callback] Make callback for all imports once in rules ([#1237], thanks [@ljqx])

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104
"doctrine": "1.5.0",
105105
"eslint-import-resolver-node": "^0.3.4",
106106
"eslint-module-utils": "^2.6.0",
107+
"find-up": "^2.0.0",
107108
"has": "^1.0.3",
108109
"is-core-module": "^1.0.2",
109110
"minimatch": "^3.0.4",

src/core/importType.js

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import { isAbsolute as nodeIsAbsolute, relative, resolve as nodeResolve } from 'path';
12
import isCoreModule from 'is-core-module';
23

34
import resolve from 'eslint-module-utils/resolve';
5+
import { getContextPackagePath } from './packagePath';
46

57
function baseModule(name) {
68
if (isScoped(name)) {
@@ -12,7 +14,7 @@ function baseModule(name) {
1214
}
1315

1416
export function isAbsolute(name) {
15-
return name && name.startsWith('/');
17+
return nodeIsAbsolute(name);
1618
}
1719

1820
// path is defined only when a resolver resolves to a non-standard path
@@ -23,35 +25,43 @@ export function isBuiltIn(name, settings, path) {
2325
return isCoreModule(base) || extras.indexOf(base) > -1;
2426
}
2527

26-
function isExternalPath(path, name, settings) {
27-
const folders = (settings && settings['import/external-module-folders']) || ['node_modules'];
28-
return !path || folders.some(folder => isSubpath(folder, path));
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');
31+
}
32+
return isModule(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));
2937
}
3038

31-
function isSubpath(subpath, path) {
32-
const normPath = path.replace(/\\/g, '/');
33-
const normSubpath = subpath.replace(/\\/g, '/').replace(/\/$/, '');
34-
if (normSubpath.length === 0) {
39+
function isExternalPath(name, settings, path, packagePath) {
40+
const internalScope = (settings && settings['import/internal-regex']);
41+
if (internalScope && new RegExp(internalScope).test(name)) {
3542
return false;
3643
}
37-
const left = normPath.indexOf(normSubpath);
38-
const right = left + normSubpath.length;
39-
return left !== -1 &&
40-
(left === 0 || normSubpath[0] !== '/' && normPath[left - 1] === '/') &&
41-
(right >= normPath.length || normPath[right] === '/');
42-
}
4344

44-
const externalModuleRegExp = /^(?:\w|@)/;
45-
export function isExternalModule(name, settings, path) {
46-
if (arguments.length < 3) {
47-
throw new TypeError('isExternalModule: name, settings, and path are all required');
45+
if (!path || relative(packagePath, path).startsWith('..')) {
46+
return true;
4847
}
49-
return externalModuleRegExp.test(name) && isExternalPath(path, name, settings);
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+
});
55+
}
56+
57+
const moduleRegExp = /^\w/;
58+
function isModule(name) {
59+
return name && moduleRegExp.test(name);
5060
}
5161

52-
const externalModuleMainRegExp = /^[\w]((?!\/).)*$/;
53-
export function isExternalModuleMain(name, settings, path) {
54-
return externalModuleMainRegExp.test(name) && isExternalPath(path, name, settings);
62+
const moduleMainRegExp = /^[\w]((?!\/).)*$/;
63+
function isModuleMain(name) {
64+
return name && moduleMainRegExp.test(name);
5565
}
5666

5767
const scopedRegExp = /^@[^/]*\/?[^/]+/;
@@ -64,12 +74,6 @@ export function isScopedMain(name) {
6474
return name && scopedMainRegExp.test(name);
6575
}
6676

67-
function isInternalModule(name, settings, path) {
68-
const internalScope = (settings && settings['import/internal-regex']);
69-
const matchesScopedOrExternalRegExp = scopedRegExp.test(name) || externalModuleRegExp.test(name);
70-
return (matchesScopedOrExternalRegExp && (internalScope && new RegExp(internalScope).test(name) || !isExternalPath(path, name, settings)));
71-
}
72-
7377
function isRelativeToParent(name) {
7478
return/^\.\.$|^\.\.[\\/]/.test(name);
7579
}
@@ -83,12 +87,14 @@ function isRelativeToSibling(name) {
8387
return /^\.[\\/]/.test(name);
8488
}
8589

86-
function typeTest(name, settings, path) {
90+
function typeTest(name, context, path) {
91+
const { settings } = context;
8792
if (isAbsolute(name, settings, path)) { return 'absolute'; }
8893
if (isBuiltIn(name, settings, path)) { return 'builtin'; }
89-
if (isInternalModule(name, settings, path)) { return 'internal'; }
90-
if (isExternalModule(name, settings, path)) { return 'external'; }
91-
if (isScoped(name, settings, path)) { return 'external'; }
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+
}
9298
if (isRelativeToParent(name, settings, path)) { return 'parent'; }
9399
if (isIndex(name, settings, path)) { return 'index'; }
94100
if (isRelativeToSibling(name, settings, path)) { return 'sibling'; }
@@ -100,5 +106,5 @@ export function isScopedModule(name) {
100106
}
101107

102108
export default function resolveImportType(name, context) {
103-
return typeTest(name, context.settings, resolve(name, context));
109+
return typeTest(name, context, resolve(name, context));
104110
}

src/core/packagePath.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { dirname } from 'path';
2+
import findUp from 'find-up';
3+
import readPkgUp from 'read-pkg-up';
4+
5+
6+
export function getContextPackagePath(context) {
7+
return getFilePackagePath(context.getFilename());
8+
}
9+
10+
export function getFilePackagePath(filePath) {
11+
const fp = findUp.sync('package.json', { cwd: filePath });
12+
return dirname(fp);
13+
}
14+
15+
export function getFilePackageName(filePath) {
16+
const { pkg } = readPkgUp.sync({ cwd: filePath, normalize: false });
17+
return pkg && pkg.name;
18+
}

src/rules/extensions.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,8 @@ module.exports = {
130130
function isExternalRootModule(file) {
131131
const slashCount = file.split('/').length - 1;
132132

133+
if (slashCount === 0) return true;
133134
if (isScopedModule(file) && slashCount <= 1) return true;
134-
if (isExternalModule(file, context, resolve(file, context)) && !slashCount) return true;
135135
return false;
136136
}
137137

@@ -160,7 +160,8 @@ module.exports = {
160160
const isPackage = isExternalModule(
161161
importPath,
162162
context.settings,
163-
resolve(importPath, context)
163+
resolve(importPath, context),
164+
context
164165
) || isScoped(importPath);
165166

166167
if (!extension || !importPath.endsWith(`.${extension}`)) {

src/rules/no-cycle.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ module.exports = {
4545
const ignoreModule = (name) => options.ignoreExternal && isExternalModule(
4646
name,
4747
context.settings,
48-
resolve(name, context)
48+
resolve(name, context),
49+
context
4950
);
5051

5152
function checkSourceValue(sourceNode, importer) {

src/rules/no-extraneous-dependencies.js

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import minimatch from 'minimatch';
55
import resolve from 'eslint-module-utils/resolve';
66
import moduleVisitor from 'eslint-module-utils/moduleVisitor';
77
import importType from '../core/importType';
8+
import { getFilePackageName } from '../core/packagePath';
89
import docsUrl from '../docsUrl';
910

1011
const depFieldCache = new Map();
@@ -116,6 +117,15 @@ function optDepErrorMessage(packageName) {
116117
`not optionalDependencies.`;
117118
}
118119

120+
function getModuleOriginalName(name) {
121+
const [first, second] = name.split('/');
122+
return first.startsWith('@') ? `${first}/${second}` : first;
123+
}
124+
125+
function getModuleRealName(resolved) {
126+
return getFilePackageName(resolved);
127+
}
128+
119129
function reportIfMissing(context, deps, depsOptions, node, name) {
120130
// Do not report when importing types
121131
if (node.importKind === 'type' || (node.parent && node.parent.importKind === 'type')) {
@@ -129,10 +139,11 @@ function reportIfMissing(context, deps, depsOptions, node, name) {
129139
const resolved = resolve(name, context);
130140
if (!resolved) { return; }
131141

132-
const splitName = name.split('/');
133-
const packageName = splitName[0][0] === '@'
134-
? splitName.slice(0, 2).join('/')
135-
: splitName[0];
142+
// get the real name from the resolved package.json
143+
// if not aliased imports (alias/react for example) will not be correctly interpreted
144+
// fallback on original name in case no package.json found
145+
const packageName = getModuleRealName(resolved) || getModuleOriginalName(name);
146+
136147
const isInDeps = deps.dependencies[packageName] !== undefined;
137148
const isInDevDeps = deps.devDependencies[packageName] !== undefined;
138149
const isInOptDeps = deps.optionalDependencies[packageName] !== undefined;

tests/files/node_modules/@generated/bar/package.json

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/files/node_modules/@generated/foo/package.json

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/files/node_modules/@org/not-a-dependency/package.json

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)