Skip to content

Commit

Permalink
feat(imports-as-dependencies): add new rule to detect missing depen…
Browse files Browse the repository at this point in the history
…dencies for import statements; fixes gajus#896
  • Loading branch information
brettz9 committed May 31, 2023
1 parent 0adfbe6 commit 45b80bb
Show file tree
Hide file tree
Showing 9 changed files with 219 additions and 4 deletions.
14 changes: 14 additions & 0 deletions .README/rules/imports-as-dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
### `imports-as-dependencies`

This rule will report an issue if JSDoc `import()` statements point to a package
which is not listed in `dependencies` or `devDependencies`.

|||
|---|---|
|Context|everywhere|
|Tags|``|
|Recommended|false|
|Settings||
|Options||

<!-- assertions importsAsDependencies -->
16 changes: 16 additions & 0 deletions docs/rules/imports-as-dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<a name="user-content-imports-as-dependencies"></a>
<a name="imports-as-dependencies"></a>
### <code>imports-as-dependencies</code>

This rule will report an issue if JSDoc `import()` statements point to a package
which is not listed in `dependencies` or `devDependencies`.

|||
|---|---|
|Context|everywhere|
|Tags|``|
|Recommended|false|
|Settings||
|Options||

<!-- assertions importsAsDependencies -->
3 changes: 3 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import checkTypes from './rules/checkTypes';
import checkValues from './rules/checkValues';
import emptyTags from './rules/emptyTags';
import implementsOnClasses from './rules/implementsOnClasses';
import importsAsDependencies from './rules/importsAsDependencies';
import informativeDocs from './rules/informativeDocs';
import matchDescription from './rules/matchDescription';
import matchName from './rules/matchName';
Expand Down Expand Up @@ -70,6 +71,7 @@ const index = {
'check-values': checkValues,
'empty-tags': emptyTags,
'implements-on-classes': implementsOnClasses,
'imports-as-dependencies': importsAsDependencies,
'informative-docs': informativeDocs,
'match-description': matchDescription,
'match-name': matchName,
Expand Down Expand Up @@ -135,6 +137,7 @@ const createRecommendedRuleset = (warnOrError) => {
'jsdoc/check-values': warnOrError,
'jsdoc/empty-tags': warnOrError,
'jsdoc/implements-on-classes': warnOrError,
'jsdoc/imports-as-dependencies': 'off',
'jsdoc/informative-docs': 'off',
'jsdoc/match-description': 'off',
'jsdoc/match-name': 'off',
Expand Down
82 changes: 82 additions & 0 deletions src/rules/importsAsDependencies.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import iterateJsdoc from '../iterateJsdoc';
import {
parse,
traverse,
tryParse,
} from '@es-joy/jsdoccomment';
import {
readFileSync,
} from 'fs';
import {
join,
} from 'path';

/**
* @type {Set<string>}
*/
let deps;
try {
const pkg = JSON.parse(
// @ts-expect-error It's ok
readFileSync(join(process.cwd(), './package.json')),
);
deps = new Set([
...(pkg.dependencies ?
Object.keys(pkg.dependencies) :
// istanbul ignore next
[]),
...(pkg.devDependencies ?
Object.keys(pkg.devDependencies) :
// istanbul ignore next
[]),
]);
} catch (error) {
/* eslint-disable no-console -- Inform user */
// istanbul ignore next
console.log(error);
/* eslint-enable no-console -- Inform user */
}

export default iterateJsdoc(({
jsdoc,
settings,
utils,
}) => {
// istanbul ignore if
if (!deps) {
return;
}

const {
mode,
} = settings;

for (const tag of jsdoc.tags) {
let typeAst;
try {
typeAst = mode === 'permissive' ? tryParse(tag.type) : parse(tag.type, mode);
} catch {
continue;
}

traverse(typeAst, (nde) => {
if (nde.type === 'JsdocTypeImport' && !deps.has(nde.element.value.replace(
/(@[^/]+\/[^/]+|[^/]+).*$/u, '$1',
))) {
utils.reportJSDoc(
'import points to package which is not found in dependencies',
tag,
);
}
});
}
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description: 'Reports if JSDoc `import()` statements point to a package which is not listed in `dependencies` or `devDependencies`',
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/imports-as-dependencies.md#repos-sticky-header',
},
type: 'suggestion',
},
});
3 changes: 0 additions & 3 deletions test/rules/assertions/checkExamples.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
// Change `process.cwd()` when testing `checkEslintrc: true`
process.chdir('test/rules/data');

export default {
invalid: [
{
Expand Down
91 changes: 91 additions & 0 deletions test/rules/assertions/importsAsDependencies.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
export default {
invalid: [
{
code: `
/**
* @type {null|import('sth').SomeApi}
*/
`,
errors: [
{
line: 3,
message: 'import points to package which is not found in dependencies',
},
],
},
{
code: `
/**
* @type {null|import('sth').SomeApi}
*/
`,
errors: [
{
line: 3,
message: 'import points to package which is not found in dependencies',
},
],
settings: {
jsdoc: {
mode: 'permissive',
},
},
},
{
code: `
/**
* @type {null|import('missingpackage/subpackage').SomeApi}
*/
`,
errors: [
{
line: 3,
message: 'import points to package which is not found in dependencies',
},
],
},
{
code: `
/**
* @type {null|import('@sth/pkg').SomeApi}
*/
`,
errors: [
{
line: 3,
message: 'import points to package which is not found in dependencies',
},
],
},
],
valid: [
{
code: `
/**
* @type {null|import('eslint').ESLint}
*/
`,
},
{
code: `
/**
* @type {null|import('eslint/use-at-your-own-risk').ESLint}
*/
`,
},
{
code: `
/**
* @type {null|import('@es-joy/jsdoccomment').InlineTag}
*/
`,
},
{
code: `
/**
* @type {null|import(}
*/
`,
},
],
};
11 changes: 11 additions & 0 deletions test/rules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const {
FlatRuleTester,
} = pkg;

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

Expand Down Expand Up @@ -148,7 +149,17 @@ const main = async () => {
}
}

const cwd = process.cwd();
if (ruleName === 'check-examples') {
// Change `process.cwd()` when testing `checkEslintrc: true`
process.chdir('test/rules/data');
}

ruleTester.run(ruleName, rule, assertions);

if (ruleName === 'check-examples') {
process.chdir(cwd);
}
}

if (!process.env.npm_config_rule) {
Expand Down
1 change: 1 addition & 0 deletions test/rules/ruleNames.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"check-values",
"empty-tags",
"implements-on-classes",
"imports-as-dependencies",
"informative-docs",
"match-description",
"match-name",
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"declarationMap": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"target": "es6",
"target": "es2017",
"outDir": "dist"
},
"include": [
Expand Down

0 comments on commit 45b80bb

Please sign in to comment.