Skip to content

Commit 45b80bb

Browse files
committed
feat(imports-as-dependencies): add new rule to detect missing dependencies for import statements; fixes gajus#896
1 parent 0adfbe6 commit 45b80bb

File tree

9 files changed

+219
-4
lines changed

9 files changed

+219
-4
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
### `imports-as-dependencies`
2+
3+
This rule will report an issue if JSDoc `import()` statements point to a package
4+
which is not listed in `dependencies` or `devDependencies`.
5+
6+
|||
7+
|---|---|
8+
|Context|everywhere|
9+
|Tags|``|
10+
|Recommended|false|
11+
|Settings||
12+
|Options||
13+
14+
<!-- assertions importsAsDependencies -->

docs/rules/imports-as-dependencies.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<a name="user-content-imports-as-dependencies"></a>
2+
<a name="imports-as-dependencies"></a>
3+
### <code>imports-as-dependencies</code>
4+
5+
This rule will report an issue if JSDoc `import()` statements point to a package
6+
which is not listed in `dependencies` or `devDependencies`.
7+
8+
|||
9+
|---|---|
10+
|Context|everywhere|
11+
|Tags|``|
12+
|Recommended|false|
13+
|Settings||
14+
|Options||
15+
16+
<!-- assertions importsAsDependencies -->

src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import checkTypes from './rules/checkTypes';
1111
import checkValues from './rules/checkValues';
1212
import emptyTags from './rules/emptyTags';
1313
import implementsOnClasses from './rules/implementsOnClasses';
14+
import importsAsDependencies from './rules/importsAsDependencies';
1415
import informativeDocs from './rules/informativeDocs';
1516
import matchDescription from './rules/matchDescription';
1617
import matchName from './rules/matchName';
@@ -70,6 +71,7 @@ const index = {
7071
'check-values': checkValues,
7172
'empty-tags': emptyTags,
7273
'implements-on-classes': implementsOnClasses,
74+
'imports-as-dependencies': importsAsDependencies,
7375
'informative-docs': informativeDocs,
7476
'match-description': matchDescription,
7577
'match-name': matchName,
@@ -135,6 +137,7 @@ const createRecommendedRuleset = (warnOrError) => {
135137
'jsdoc/check-values': warnOrError,
136138
'jsdoc/empty-tags': warnOrError,
137139
'jsdoc/implements-on-classes': warnOrError,
140+
'jsdoc/imports-as-dependencies': 'off',
138141
'jsdoc/informative-docs': 'off',
139142
'jsdoc/match-description': 'off',
140143
'jsdoc/match-name': 'off',

src/rules/importsAsDependencies.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import iterateJsdoc from '../iterateJsdoc';
2+
import {
3+
parse,
4+
traverse,
5+
tryParse,
6+
} from '@es-joy/jsdoccomment';
7+
import {
8+
readFileSync,
9+
} from 'fs';
10+
import {
11+
join,
12+
} from 'path';
13+
14+
/**
15+
* @type {Set<string>}
16+
*/
17+
let deps;
18+
try {
19+
const pkg = JSON.parse(
20+
// @ts-expect-error It's ok
21+
readFileSync(join(process.cwd(), './package.json')),
22+
);
23+
deps = new Set([
24+
...(pkg.dependencies ?
25+
Object.keys(pkg.dependencies) :
26+
// istanbul ignore next
27+
[]),
28+
...(pkg.devDependencies ?
29+
Object.keys(pkg.devDependencies) :
30+
// istanbul ignore next
31+
[]),
32+
]);
33+
} catch (error) {
34+
/* eslint-disable no-console -- Inform user */
35+
// istanbul ignore next
36+
console.log(error);
37+
/* eslint-enable no-console -- Inform user */
38+
}
39+
40+
export default iterateJsdoc(({
41+
jsdoc,
42+
settings,
43+
utils,
44+
}) => {
45+
// istanbul ignore if
46+
if (!deps) {
47+
return;
48+
}
49+
50+
const {
51+
mode,
52+
} = settings;
53+
54+
for (const tag of jsdoc.tags) {
55+
let typeAst;
56+
try {
57+
typeAst = mode === 'permissive' ? tryParse(tag.type) : parse(tag.type, mode);
58+
} catch {
59+
continue;
60+
}
61+
62+
traverse(typeAst, (nde) => {
63+
if (nde.type === 'JsdocTypeImport' && !deps.has(nde.element.value.replace(
64+
/(@[^/]+\/[^/]+|[^/]+).*$/u, '$1',
65+
))) {
66+
utils.reportJSDoc(
67+
'import points to package which is not found in dependencies',
68+
tag,
69+
);
70+
}
71+
});
72+
}
73+
}, {
74+
iterateAllJsdocs: true,
75+
meta: {
76+
docs: {
77+
description: 'Reports if JSDoc `import()` statements point to a package which is not listed in `dependencies` or `devDependencies`',
78+
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/imports-as-dependencies.md#repos-sticky-header',
79+
},
80+
type: 'suggestion',
81+
},
82+
});

test/rules/assertions/checkExamples.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
// Change `process.cwd()` when testing `checkEslintrc: true`
2-
process.chdir('test/rules/data');
3-
41
export default {
52
invalid: [
63
{
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
export default {
2+
invalid: [
3+
{
4+
code: `
5+
/**
6+
* @type {null|import('sth').SomeApi}
7+
*/
8+
`,
9+
errors: [
10+
{
11+
line: 3,
12+
message: 'import points to package which is not found in dependencies',
13+
},
14+
],
15+
},
16+
{
17+
code: `
18+
/**
19+
* @type {null|import('sth').SomeApi}
20+
*/
21+
`,
22+
errors: [
23+
{
24+
line: 3,
25+
message: 'import points to package which is not found in dependencies',
26+
},
27+
],
28+
settings: {
29+
jsdoc: {
30+
mode: 'permissive',
31+
},
32+
},
33+
},
34+
{
35+
code: `
36+
/**
37+
* @type {null|import('missingpackage/subpackage').SomeApi}
38+
*/
39+
`,
40+
errors: [
41+
{
42+
line: 3,
43+
message: 'import points to package which is not found in dependencies',
44+
},
45+
],
46+
},
47+
{
48+
code: `
49+
/**
50+
* @type {null|import('@sth/pkg').SomeApi}
51+
*/
52+
`,
53+
errors: [
54+
{
55+
line: 3,
56+
message: 'import points to package which is not found in dependencies',
57+
},
58+
],
59+
},
60+
],
61+
valid: [
62+
{
63+
code: `
64+
/**
65+
* @type {null|import('eslint').ESLint}
66+
*/
67+
`,
68+
},
69+
{
70+
code: `
71+
/**
72+
* @type {null|import('eslint/use-at-your-own-risk').ESLint}
73+
*/
74+
`,
75+
},
76+
{
77+
code: `
78+
/**
79+
* @type {null|import('@es-joy/jsdoccomment').InlineTag}
80+
*/
81+
`,
82+
},
83+
{
84+
code: `
85+
/**
86+
* @type {null|import(}
87+
*/
88+
`,
89+
},
90+
],
91+
};

test/rules/index.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const {
2525
FlatRuleTester,
2626
} = pkg;
2727

28+
// eslint-disable-next-line complexity -- Temporary
2829
const main = async () => {
2930
const ruleNames = JSON.parse(readFileSync(join(__dirname, './ruleNames.json'), 'utf8'));
3031

@@ -148,7 +149,17 @@ const main = async () => {
148149
}
149150
}
150151

152+
const cwd = process.cwd();
153+
if (ruleName === 'check-examples') {
154+
// Change `process.cwd()` when testing `checkEslintrc: true`
155+
process.chdir('test/rules/data');
156+
}
157+
151158
ruleTester.run(ruleName, rule, assertions);
159+
160+
if (ruleName === 'check-examples') {
161+
process.chdir(cwd);
162+
}
152163
}
153164

154165
if (!process.env.npm_config_rule) {

test/rules/ruleNames.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"check-values",
1313
"empty-tags",
1414
"implements-on-classes",
15+
"imports-as-dependencies",
1516
"informative-docs",
1617
"match-description",
1718
"match-name",

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"declarationMap": true,
1111
"allowSyntheticDefaultImports": true,
1212
"strict": true,
13-
"target": "es6",
13+
"target": "es2017",
1414
"outDir": "dist"
1515
},
1616
"include": [

0 commit comments

Comments
 (0)