Skip to content

Commit

Permalink
[New] no extraneous type-only dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
jablko committed Sep 21, 2021
1 parent 4f0f560 commit eedd9f1
Show file tree
Hide file tree
Showing 6 changed files with 46 additions and 16 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
"cross-env": "^4.0.0",
"eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0",
"eslint-import-resolver-node": "file:./resolvers/node",
"eslint-import-resolver-typescript": "^1.0.2 || ^1.1.1",
"eslint-import-resolver-typescript": "^1.0.2 || ^2.5.0",
"eslint-import-resolver-webpack": "file:./resolvers/webpack",
"eslint-import-test-order-redirect": "file:./tests/files/order-redirect",
"eslint-module-utils": "file:./utils",
Expand Down
41 changes: 32 additions & 9 deletions src/rules/no-extraneous-dependencies.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,28 @@ function optDepErrorMessage(packageName) {
`not optionalDependencies.`;
}

function resolveTypeOnly(modulePath, context) {
const sourceFile = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename();
const configResolvers = context.settings['import/resolver'];
if (!configResolvers) {
return;
}

const resolvers = resolve.resolverReducer(configResolvers, new Map());
for (const [name, config] of resolvers) {
switch (name) {
case 'typescript':
case 'eslint-import-resolver-typescript': {
const resolver = resolve.requireResolver(name, sourceFile);
const resolved = resolver.resolve(modulePath, sourceFile, config);
if (resolved.found) {
return resolved.path;
}
}
}
}
}

function getModuleOriginalName(name) {
const [first, second] = name.split('/');
return first.startsWith('@') ? `${first}/${second}` : first;
Expand Down Expand Up @@ -160,23 +182,24 @@ function checkDependencyDeclaration(deps, packageName, declarationStatus) {

function reportIfMissing(context, deps, depsOptions, node, name) {
// Do not report when importing types
if (
node.importKind === 'type' ||
(node.parent && node.parent.importKind === 'type') ||
node.importKind === 'typeof'
) {
if (node.importKind === 'typeof') {
return;
}

if (importType(name, context) !== 'external') {
return;
}

const resolved = resolve(name, context);
const isTypeOnly = node.importKind === 'type';
const resolved = (isTypeOnly ? resolveTypeOnly : resolve)(name, context);
if (!resolved) { return; }

const importPackageName = getModuleOriginalName(name);
let declarationStatus = checkDependencyDeclaration(deps, importPackageName);
// If this is type-only, only check realPackageName, because if you
// e.g. import type { JSONSchema7Type } from 'json-schema',
// realPackageName could be @types/json-schema but importPackageName
// will be json-schema and that package could be untyped.
const importPackageName = !isTypeOnly && getModuleOriginalName(name);
let declarationStatus = !isTypeOnly && checkDependencyDeclaration(deps, importPackageName);

if (
declarationStatus.isInDeps ||
Expand All @@ -196,7 +219,7 @@ function reportIfMissing(context, deps, depsOptions, node, name) {

if (
declarationStatus.isInDeps ||
(depsOptions.allowDevDeps && declarationStatus.isInDevDeps) ||
((depsOptions.allowDevDeps || isTypeOnly) && declarationStatus.isInDevDeps) ||
(depsOptions.allowPeerDeps && declarationStatus.isInPeerDeps) ||
(depsOptions.allowOptDeps && declarationStatus.isInOptDeps) ||
(depsOptions.allowBundledDeps && declarationStatus.isInBundledDeps)
Expand Down
Empty file.
3 changes: 3 additions & 0 deletions tests/files/node_modules/@types/a/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 8 additions & 6 deletions tests/src/rules/no-extraneous-dependencies.js
Original file line number Diff line number Diff line change
Expand Up @@ -392,12 +392,7 @@ describe('TypeScript', () => {
};

ruleTester.run('no-extraneous-dependencies', rule, {
valid: [
test(Object.assign({
code: 'import type T from "a";',
options: [{ packageDir: packageDirWithTypescriptDevDependencies, devDependencies: false }],
}, parserConfig)),
],
valid: [],
invalid: [
test(Object.assign({
code: 'import T from "a";',
Expand All @@ -406,6 +401,13 @@ describe('TypeScript', () => {
message: "'a' should be listed in the project's dependencies, not devDependencies.",
}],
}, parserConfig)),
test(Object.assign({
code: 'import type T from "a";',
errors: [{
message: "'@types/a' should be listed in the project's dependencies. Run 'npm i -S @types/a' to add it",
}],
options: [{ packageDir: packageDirWithTypescriptDevDependencies, devDependencies: false }],
}, parserConfig)),
],
});
});
Expand Down
2 changes: 2 additions & 0 deletions utils/resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,4 +235,6 @@ function resolve(p, context) {
}
}
resolve.relative = relative;
resolve.resolverReducer = resolverReducer;
resolve.requireResolver = requireResolver;
exports.default = resolve;

0 comments on commit eedd9f1

Please sign in to comment.