diff --git a/.README/rules/imports-as-dependencies.md b/.README/rules/imports-as-dependencies.md
new file mode 100644
index 000000000..96850fa24
--- /dev/null
+++ b/.README/rules/imports-as-dependencies.md
@@ -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||
+
+
diff --git a/docs/rules/imports-as-dependencies.md b/docs/rules/imports-as-dependencies.md
new file mode 100644
index 000000000..d59f92286
--- /dev/null
+++ b/docs/rules/imports-as-dependencies.md
@@ -0,0 +1,16 @@
+
+
+### 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||
+
+
diff --git a/src/index.js b/src/index.js
index 83d420796..1895b03c5 100644
--- a/src/index.js
+++ b/src/index.js
@@ -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';
@@ -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,
@@ -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',
diff --git a/src/rules/importsAsDependencies.js b/src/rules/importsAsDependencies.js
new file mode 100644
index 000000000..975210830
--- /dev/null
+++ b/src/rules/importsAsDependencies.js
@@ -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}
+ */
+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',
+ },
+});
diff --git a/test/rules/assertions/checkExamples.js b/test/rules/assertions/checkExamples.js
index 1c319a9a0..efc829d9c 100644
--- a/test/rules/assertions/checkExamples.js
+++ b/test/rules/assertions/checkExamples.js
@@ -1,6 +1,3 @@
-// Change `process.cwd()` when testing `checkEslintrc: true`
-process.chdir('test/rules/data');
-
export default {
invalid: [
{
diff --git a/test/rules/assertions/importsAsDependencies.js b/test/rules/assertions/importsAsDependencies.js
new file mode 100644
index 000000000..321a6c96b
--- /dev/null
+++ b/test/rules/assertions/importsAsDependencies.js
@@ -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(}
+ */
+ `,
+ },
+ ],
+};
diff --git a/test/rules/index.js b/test/rules/index.js
index b181d7dc5..07475543b 100644
--- a/test/rules/index.js
+++ b/test/rules/index.js
@@ -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'));
@@ -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) {
diff --git a/test/rules/ruleNames.json b/test/rules/ruleNames.json
index 7cff81eab..24038af51 100644
--- a/test/rules/ruleNames.json
+++ b/test/rules/ruleNames.json
@@ -12,6 +12,7 @@
"check-values",
"empty-tags",
"implements-on-classes",
+ "imports-as-dependencies",
"informative-docs",
"match-description",
"match-name",
diff --git a/tsconfig.json b/tsconfig.json
index 98579a525..49dffe62a 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -10,7 +10,7 @@
"declarationMap": true,
"allowSyntheticDefaultImports": true,
"strict": true,
- "target": "es6",
+ "target": "es2017",
"outDir": "dist"
},
"include": [