From 0d494774a6a9451f960b3f063b9b8674842cf4fd Mon Sep 17 00:00:00 2001 From: Sander Verweij Date: Sun, 1 Dec 2024 20:26:05 +0100 Subject: [PATCH] feat(extract): adds recognition of jsdoc 'bracket' imports (#969) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description - adds recognition for the 'older' bracket style imports in jsdoc (so things like `/** @type {import('thing')} */`, `/** @typedef {import('thing')} Thing */`, `/** @param {import('thing')} pParameter blablabla */`, ...) - updates documentation to reflect that it isn't a 'future feature' anymore - 🏕️ drive-by: updates jsdoc type references in dependency-cruiser's own sources TODO - [x] Add support for more exotic ways to import (e.g. `/** @type {Partial} */`, `/** @type{boolean|import('bla')|import('ble')} */`, `/** {Set} */`, ...) - [x] Verify with [the list of jsdoc tag types typescript supports](https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html) there's no other corner cases that can bite us. - [x] so... we currently seem to occasionally resolve these type only imports to the real implementation. Is that an issue? > [!NOTE] > an iteration of this pull request has been published [as a beta on npmjs](https://www.npmjs.com/package/dependency-cruiser/v/16.7.0-beta-2) ## Motivation and Context - necessary to be more complete than just `@import` tags (see #964 and #965) - needs to be introduced at the same time as those to prevent a breaking change ## How Has This Been Tested? - [x] green ci - [x] additional unit tests ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] Documentation only change - [ ] Refactor (non-breaking change which fixes an issue without changing functionality) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) --- .dependency-cruiser.mjs | 3 +- ...dependency-cruiser-show-metrics-config.mjs | 2 +- configs/.dependency-cruiser-unlimited.mjs | 2 +- doc/faq.md | 18 +- doc/options-reference.md | 8 +- doc/rules-reference.md | 56 ++-- package.json | 2 +- src/extract/tsc/extract-typescript-deps.mjs | 91 +++++- test/cache/cache.spec.mjs | 8 +- test/cache/content-strategy.spec.mjs | 10 +- .../tsc/jsdoc-bracket-imports.spec.mjs | 261 ++++++++++++++++++ test/extract/tsc/jsdoc-import-tags.spec.mjs | 7 - tools/schema/.dependency-cruiser.mjs | 2 +- types/.dependency-cruiser.mjs | 2 +- 14 files changed, 403 insertions(+), 69 deletions(-) create mode 100644 test/extract/tsc/jsdoc-bracket-imports.spec.mjs diff --git a/.dependency-cruiser.mjs b/.dependency-cruiser.mjs index f7ebe0f90..9244b7d34 100644 --- a/.dependency-cruiser.mjs +++ b/.dependency-cruiser.mjs @@ -1,9 +1,10 @@ +/* eslint-disable max-lines */ import { fileURLToPath } from "node:url"; const defaultStrictRules = fileURLToPath( new URL("configs/recommended-strict.cjs", import.meta.url), ); -/** @type {import('./').IConfiguration} */ +/** @type {import('./types/configuration.mjs').IConfiguration} */ export default { extends: defaultStrictRules, forbidden: [ diff --git a/configs/.dependency-cruiser-show-metrics-config.mjs b/configs/.dependency-cruiser-show-metrics-config.mjs index 090b0dafe..1bdcbf6c9 100644 --- a/configs/.dependency-cruiser-show-metrics-config.mjs +++ b/configs/.dependency-cruiser-show-metrics-config.mjs @@ -1,5 +1,5 @@ import baseConfig from "../.dependency-cruiser.mjs"; -/** @type {import('../').IConfiguration} */ +/** @type {import('../types/configuration.mjs').IConfiguration} */ export default { ...baseConfig, forbidden: [ diff --git a/configs/.dependency-cruiser-unlimited.mjs b/configs/.dependency-cruiser-unlimited.mjs index d532f879b..ab0106687 100644 --- a/configs/.dependency-cruiser-unlimited.mjs +++ b/configs/.dependency-cruiser-unlimited.mjs @@ -1,5 +1,5 @@ import baseConfig from "../.dependency-cruiser.mjs"; -/** @type {import('../').IConfiguration} */ +/** @type {import('../types/configuration.mjs').IConfiguration} */ export default { ...baseConfig, options: { diff --git a/doc/faq.md b/doc/faq.md index 2f8e04d1e..9c5782a97 100644 --- a/doc/faq.md +++ b/doc/faq.md @@ -576,10 +576,24 @@ _teamcity_ reporters (he _dot_ and _ddot_ reporters already did before). **A**: From version 5.4.0 or higher you can add an _exoticRequireStrings_ key in your configuration with the wrapper(s) and/ or re-definitions of require: -```json -"exoticRequireStrings": ["window.require", "need", "tryRequire"] +```javascript +exoticRequireStrings: ["window.require", "need", "tryRequire"]; ``` +### Q: I'm using jsdoc/ tsdoc comments to declare dependencies - how do I make sure dependency-cruiser picks those up? + +**A** From version 16.7.0 you can add this to your configuration: + +```javascript +//... +detectJSDocImports: true; // implies `parser: tsc` +// ... +``` + +As only the `tsc` TypeScript parser supports this, it will need `typescript` +to be installed (dependency-cruiser will automatically use it). For more +information see [detectJSDocImports in the options reference](./options-reference#detectjsdocimports-detect-dependencies-in-jsdoc-comments) + ### Q: Can I get code completion for .dependency-cruiser.js? **A**: Yes. diff --git a/doc/options-reference.md b/doc/options-reference.md index 5cb4a2864..b61582771 100644 --- a/doc/options-reference.md +++ b/doc/options-reference.md @@ -791,12 +791,10 @@ you can provide the parameters like so: If you have dependencies in JSDoc comments that you want to take into account you can set this option to `true`. This will make dependency-cruiser look at -TypeScript 5.5+ [`@import` tags](https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/#the-jsdoc-import-tag). +TypeScript 5.5+ [`@import` tags](https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/#the-jsdoc-import-tag) as well as to bracket style imports (e.g. `/** @type {import('./thing').SomeType} */`) +that have been part of TypeScript jsdoc/ tsdoc specification for a long time. -In the near future it will also look to bracket style imports (e.g. `/** @type {import('./thing').SomeType} */`) -in all JSDoc tags they can occur in (e.g. `@param`, `@returns`, `@type`, `@typedef` etc). - -As currently on the TypeScript compiler (`tsc`) can detect these imports, switching +As currently only the TypeScript compiler (`tsc`) can detect these imports, switching on this option implies dependency-cruiser will set `options.parser` to `tsc` so it uses the TypeScript compiler to parse not only TypeScript but also JavaScript. diff --git a/doc/rules-reference.md b/doc/rules-reference.md index cfeb4775d..8263cb843 100644 --- a/doc/rules-reference.md +++ b/doc/rules-reference.md @@ -984,34 +984,34 @@ the dependency was declared. One or more of these can occur at the same time. E. dependency which resolves to a base url in a tsconfig.json you'll see `import`, `aliased` as well as `aliased-tsconfig` and `aliased-tsconfig-base-url`. -| dependency type | meaning the module was imported ... | example | -| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | -| aliased | via an alias of some sort (e.g. tsconfig paths, subpath imports, npm workspace or webpack aliases) | "~/hello.ts" | -| aliased-subpath-import | via a [subpath import](https://nodejs.org/api/packages.html#subpath-imports) | "#thing/hello.mjs" | -| aliased-tsconfig | via a typescript compilerOptions.paths or compilerOptions.baseUrl setting in tsconfig. | "@thing/hello" | -| aliased-tsconfig-base-url | via a typescript [compilerOptions.baseUrl setting in tsconfig](https://www.typescriptlang.org/tsconfig#baseUrl) | "libs/utensils/src/hello.js" | -| aliased-tsconfig-paths | via a typescript [compilerOptions.paths setting in tsconfig](https://www.typescriptlang.org/tsconfig#paths) | "@thing/hello" | -| aliased-webpack | via a [webpack resolve alias](https://webpack.js.org/configuration/resolve/#resolvealias) | "Utilities" | -| aliased-workspace | via a [workspace](https://docs.npmjs.com/cli/v10/configuring-npm/package-json#workspaces) | "local-workspace-package" | -| amd-define | with an AMD `define` wrapper (popularized by requirejs) | `define(["./thing"], function(thing){ /* do stuff */ })` | -| amd-exotic-require | with a require statement within an AMD module (but with the first parameter baring an insensible non-standard name) | `define(function(want, exports, module){ var one = want('./thing')})` | -| amd-require | with a require statement within an AMD module | `define(function(require, exports, module){ var one = require('./thing')})` | -| dynamic-import | with a dynamic import statement | `const { thing } = await import("./things")` | -| exotic-require | with a statement that isn't 'require' see [exoticallyRequired](#exoticallyrequired-exoticrequire-and-exoticrequirenot) | `const { thing } = want("./thing")` | -| export | implicitly via a module export | `export { thing } from "./things"` | -| import | with a 'regular' ES import | `import { thing } from "./things` | -| import-equals | with an 'import equals' statement | `import fs = require("fs")` | -| jsdoc | in jsdoc. See `jsdoc-bracket-import` and `jsdoc-import-tag`. Needs [detectJSDocImports](options-reference.md#detectjsdocimports-detect-dependencies-in-jsdoc-comments) set to `true` | `/** @type {import('./things').thing} */`, `/** @import { thing } from "things" */` | -| jsdoc-bracket-import | in a jsdoc comment with a 'bracket' style import. Always `type-only`, also has the `jsdoc` dependency type. FUTURE FEATURE | `/** @type {import('./things').thing} */` | -| jsdoc-import-tag | in a jsdoc comment with an @import tag. Always `type-only`, also has the `jsdoc` dependency type. | `/** @import { thing } from "things" */` | -| pre-compilation-only | but the dependency will disappear at runtime. See [preCompilationOnly](#preCompilationOnly) | `import { thing } from "./things"` // and continue to not use `thing` | -| require | with a commonjs 'require' statement | `const memoize = require("lodash/memoize")` | -| triple-slash-amd-dependency | with a triple slash directive, specifically declaring an AMD dependency | `/// ` | -| triple-slash-directive | with a triple slash directive (oldskool TypeScript) | | -| triple-slash-file-reference | with a triple slash directive, specifically importing another module | `/// ` | -| triple-slash-type-reference | with a triple slash directive, specifically importing types | `/// ` | -| type-import | as part of a type declaration | `const lAThing: import('./things').IThing = {}` | -| type-only | as 'type only' - only available for TypeScript sources, only for tsPreCompilationDeps !== false. | `import type { IThing } from "./things"` | +| dependency type | meaning the module was imported ... | example | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | +| aliased | via an alias of some sort (e.g. tsconfig paths, subpath imports, npm workspace or webpack aliases) | "~/hello.ts" | +| aliased-subpath-import | via a [subpath import](https://nodejs.org/api/packages.html#subpath-imports) | "#thing/hello.mjs" | +| aliased-tsconfig | via a typescript compilerOptions.paths or compilerOptions.baseUrl setting in tsconfig. | "@thing/hello" | +| aliased-tsconfig-base-url | via a typescript [compilerOptions.baseUrl setting in tsconfig](https://www.typescriptlang.org/tsconfig#baseUrl) | "libs/utensils/src/hello.js" | +| aliased-tsconfig-paths | via a typescript [compilerOptions.paths setting in tsconfig](https://www.typescriptlang.org/tsconfig#paths) | "@thing/hello" | +| aliased-webpack | via a [webpack resolve alias](https://webpack.js.org/configuration/resolve/#resolvealias) | "Utilities" | +| aliased-workspace | via a [workspace](https://docs.npmjs.com/cli/v10/configuring-npm/package-json#workspaces) | "local-workspace-package" | +| amd-define | with an AMD `define` wrapper (popularized by requirejs) | `define(["./thing"], function(thing){ /* do stuff */ })` | +| amd-exotic-require | with a require statement within an AMD module (but with the first parameter baring an insensible non-standard name) | `define(function(want, exports, module){ var one = want('./thing')})` | +| amd-require | with a require statement within an AMD module | `define(function(require, exports, module){ var one = require('./thing')})` | +| dynamic-import | with a dynamic import statement | `const { thing } = await import("./things")` | +| exotic-require | with a statement that isn't 'require' see [exoticallyRequired](#exoticallyrequired-exoticrequire-and-exoticrequirenot) | `const { thing } = want("./thing")` | +| export | implicitly via a module export | `export { thing } from "./things"` | +| import | with a 'regular' ES import | `import { thing } from "./things` | +| import-equals | with an 'import equals' statement | `import fs = require("fs")` | +| jsdoc | in jsdoc. See `jsdoc-bracket-import` & `jsdoc-import-tag`. Needs [detectJSDocImports](options-reference.md#detectjsdocimports-detect-dependencies-in-jsdoc-comments) set to `true` | `/** @type {import('./things').thing} */`, `/** @import { thing } from "things" */` | +| jsdoc-bracket-import | in jsdoc with a 'bracket' style import. Always `type-only`, also has the `jsdoc` dependency type. | `/** @type {import('./things').thing} */` | +| jsdoc-import-tag | in jsdoc with an @import tag. Always `type-only`, also has the `jsdoc` dependency type. | `/** @import { thing } from "things" */` | +| pre-compilation-only | but the dependency will disappear at runtime. See [preCompilationOnly](#preCompilationOnly) | `import { thing } from "./things"` // and continue to not use `thing` | +| require | with a commonjs 'require' statement | `const memoize = require("lodash/memoize")` | +| triple-slash-amd-dependency | with a triple slash directive, specifically declaring an AMD dependency | `/// ` | +| triple-slash-directive | with a triple slash directive (oldskool TypeScript) | | +| triple-slash-file-reference | with a triple slash directive, specifically importing another module | `/// ` | +| triple-slash-type-reference | with a triple slash directive, specifically importing types | `/// ` | +| type-import | as part of a type declaration | `const lAThing: import('./things').IThing = {}` | +| type-only | as 'type only' - only available for TypeScript sources, only for tsPreCompilationDeps !== false. | `import type { IThing } from "./things"` | ### `dynamic` diff --git a/package.json b/package.json index 714a6679c..9d4d6dee9 100644 --- a/package.json +++ b/package.json @@ -303,4 +303,4 @@ "vue-template-compiler": ">=2.0.0 <3.0.0", "@vue/compiler-sfc": ">=3.0.0 <4.0.0" } -} \ No newline at end of file +} diff --git a/src/extract/tsc/extract-typescript-deps.mjs b/src/extract/tsc/extract-typescript-deps.mjs index 3a142258c..e9b19868a 100644 --- a/src/extract/tsc/extract-typescript-deps.mjs +++ b/src/extract/tsc/extract-typescript-deps.mjs @@ -1,3 +1,4 @@ +/* eslint-disable security/detect-object-injection */ /* eslint-disable unicorn/prevent-abbreviations */ /* eslint-disable max-lines */ /* eslint-disable no-inline-comments */ @@ -263,7 +264,7 @@ function extractJSDocImportTags(pJSDocTags) { .filter( (pTag) => pTag.tagName.escapedText === "import" && - pTag.moduleSpecifier?.kind && + pTag.moduleSpecifier && typescript.SyntaxKind[pTag.moduleSpecifier.kind] === "StringLiteral" && pTag.moduleSpecifier.text, ) @@ -275,10 +276,82 @@ function extractJSDocImportTags(pJSDocTags) { })); } +function isJSDocImport(pTypeNode) { + // import('./hello.mjs') within jsdoc + return ( + typescript.SyntaxKind[pTypeNode?.kind] === "LastTypeNode" && + typescript.SyntaxKind[pTypeNode.argument?.kind] === "LiteralType" && + typescript.SyntaxKind[pTypeNode.argument?.literal?.kind] === + "StringLiteral" && + pTypeNode.argument.literal.text + ); +} + +function keyInJSDocIsIgnorable(pKey) { + return [ + "parent", + "pos", + "end", + "flags", + "emitNode", + "modifierFlagsCache", + "transformFlags", + "id", + "flowNode", + "symbol", + "original", + ].includes(pKey); +} + +export function walkJSDoc(pObject, pCollection = new Set()) { + if (isJSDocImport(pObject)) { + pCollection.add(pObject.argument.literal.text); + } else if (Array.isArray(pObject)) { + pObject.forEach((pValue) => walkJSDoc(pValue, pCollection)); + } else if (typeof pObject === "object") { + for (const lKey in pObject) { + if (!keyInJSDocIsIgnorable(lKey) && pObject[lKey]) { + walkJSDoc(pObject[lKey], pCollection); + } + } + } +} + +export function getJSDocImports(pTagNode) { + const lCollection = new Set(); + walkJSDoc(pTagNode, lCollection); + return Array.from(lCollection); +} + +function extractJSDocBracketImports(pJSDocTags) { + // https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html + return pJSDocTags + .filter( + (pTag) => + pTag.tagName.escapedText !== "import" && + typescript.SyntaxKind[pTag.typeExpression?.kind] === "FirstJSDocNode", + ) + .flatMap((pTag) => getJSDocImports(pTag)) + .map((pImportName) => ({ + module: pImportName, + moduleSystem: "es6", + exoticallyRequired: false, + dependencyTypes: ["type-only", "import", "jsdoc", "jsdoc-bracket-import"], + })); +} + function extractJSDocImports(pJSDocNodes) { - return pJSDocNodes - .filter((pJSDocLine) => pJSDocLine.tags) - .flatMap((pJSDocLine) => extractJSDocImportTags(pJSDocLine.tags)); + // https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/#the-jsdoc-import-tag + const lJSDocNodesWithTags = pJSDocNodes.filter( + (pJSDocLine) => pJSDocLine.tags, + ); + const lJSDocImportTags = lJSDocNodesWithTags.flatMap((pJSDocLine) => + extractJSDocImportTags(pJSDocLine.tags), + ); + const lJSDocBracketImports = lJSDocNodesWithTags.flatMap((pJSDocLine) => + extractJSDocBracketImports(pJSDocLine.tags), + ); + return lJSDocImportTags.concat(lJSDocBracketImports); } /** @@ -338,14 +411,8 @@ function walk(pResult, pExoticRequireStrings, pDetectJSDocImports) { }); } - // /** @import thing from './module' */ - // /** @import {thing} from './module' */ - // /** @import * as thing from './module' */ - // see https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/#the-jsdoc-import-tag - // - // TODO: all the kinds of tags that can have import statements as type declarations - // (e.g. @type, @param, @returns, @typedef, @property, @prop, @arg, ...) - // https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html + // /** @import thing from './module' */ etc + // /** @type {import('module').thing}*/ etc if (pDetectJSDocImports && pASTNode.jsDoc) { const lJSDocImports = extractJSDocImports(pASTNode.jsDoc); diff --git a/test/cache/cache.spec.mjs b/test/cache/cache.spec.mjs index 6b30196e8..31fbba01f 100644 --- a/test/cache/cache.spec.mjs +++ b/test/cache/cache.spec.mjs @@ -82,7 +82,7 @@ describe("[I] cache/cache - writeCache", () => { }); it("writes the passed cruise options to the cache folder (which is created when it doesn't exist yet) - content based cached strategy", async () => { - /** @type {import("../..").ICruiseResult} */ + /** @type {import("../../types/cruise-result.mjs").ICruiseResult} */ const lDummyCacheContents = { modules: [], summary: { optionsUsed: { baseDir: "test/cache/__mocks__/cache" } }, @@ -102,7 +102,7 @@ describe("[I] cache/cache - canServeFromCache", () => { OUTPUTS_FOLDER, "serve-from-cache-compatible", ); - /** @type import("../..").ICruiseResult */ + /** @type import("../../types/cruise-result.mjs").ICruiseResult */ const lMinimalCruiseResult = { modules: [], summary: { @@ -117,7 +117,7 @@ describe("[I] cache/cache - canServeFromCache", () => { revisionData: { cacheFormatVersion: 16.2, SHA1: "dummy-sha", changes: [] }, }; - /** @type import("../..").ICruiseResult */ + /** @type import("../../types/cruise-result.mjs").ICruiseResult */ const lNoVersionCruiseResult = { modules: [], summary: { @@ -132,7 +132,7 @@ describe("[I] cache/cache - canServeFromCache", () => { revisionData: { SHA1: "dummy-sha", changes: [] }, }; - /** @type import("../..").ICruiseResult */ + /** @type import("../../types/cruise-result.mjs").ICruiseResult */ const lOldVersionCruiseResult = { modules: [], summary: { diff --git a/test/cache/content-strategy.spec.mjs b/test/cache/content-strategy.spec.mjs index eb79aaa30..3840a367f 100644 --- a/test/cache/content-strategy.spec.mjs +++ b/test/cache/content-strategy.spec.mjs @@ -254,7 +254,7 @@ describe("[U] cache/content-strategy - prepareRevisionDataForSaving", () => { violations: [], }, }; - /** @type {import("../..").IRevisionData} */ + /** @type {import("../../types/cruise-result.mjs").IRevisionData} */ const lEmptyRevisionData = { SHA1: "shwoop", changes: [], @@ -284,7 +284,7 @@ describe("[U] cache/content-strategy - prepareRevisionDataForSaving", () => { }); it("adds checksums to modules in the cruise result", () => { - /** @type {import("../..").ICruiseResult} */ + /** @type {import("../../types/cruise-result.mjs").ICruiseResult} */ const lEmptyCruiseResult = { modules: [ { source: "foo.js" }, @@ -305,7 +305,7 @@ describe("[U] cache/content-strategy - prepareRevisionDataForSaving", () => { violations: [], }, }; - /** @type {import("../..").IRevisionData} */ + /** @type {import("../../types/cruise-result.mjs").IRevisionData} */ const lEmptyRevisionData = { SHA1: "shwoop", changes: [], @@ -343,7 +343,7 @@ describe("[U] cache/content-strategy - prepareRevisionDataForSaving", () => { }); it("removes changes from the revision data that aren't different anymore from the cruise result", () => { - /** @type {import("../..").ICruiseResult} */ + /** @type {import("../../types/cruise-result.mjs").ICruiseResult} */ const lCruiseResult = { modules: [ { source: "foo.js" }, @@ -364,7 +364,7 @@ describe("[U] cache/content-strategy - prepareRevisionDataForSaving", () => { violations: [], }, }; - /** @type {import("../..").IRevisionData} */ + /** @type {import("../../types/cruise-result.mjs").IRevisionData} */ const lRevisionData = { SHA1: "shwoop", changes: [ diff --git a/test/extract/tsc/jsdoc-bracket-imports.spec.mjs b/test/extract/tsc/jsdoc-bracket-imports.spec.mjs new file mode 100644 index 000000000..e880d3431 --- /dev/null +++ b/test/extract/tsc/jsdoc-bracket-imports.spec.mjs @@ -0,0 +1,261 @@ +import { deepEqual } from "node:assert/strict"; +import extractTypescript from "./extract-typescript.utl.mjs"; + +describe("[U] ast-extractors/extract-typescript - jsdoc 'bracket' imports", () => { + it("extracts @type whole module", () => { + deepEqual( + extractTypescript( + "/** @type {import('./hello.mjs')} */ export default {};", + [], + true, + ), + [ + { + module: "./hello.mjs", + moduleSystem: "es6", + dynamic: false, + exoticallyRequired: false, + dependencyTypes: [ + "type-only", + "import", + "jsdoc", + "jsdoc-bracket-import", + ], + }, + ], + ); + }); + + it("extracts @type one type from a module", () => { + deepEqual( + extractTypescript("/** @type {import('./hello.mjs').thing} */", [], true), + [ + { + module: "./hello.mjs", + moduleSystem: "es6", + dynamic: false, + exoticallyRequired: false, + dependencyTypes: [ + "type-only", + "import", + "jsdoc", + "jsdoc-bracket-import", + ], + }, + ], + ); + }); + + it("extracts @typedef whole module", () => { + deepEqual( + extractTypescript( + "/** @typedef {import('./hello.mjs')} Hello */ ", + [], + true, + ), + [ + { + module: "./hello.mjs", + moduleSystem: "es6", + dynamic: false, + exoticallyRequired: false, + dependencyTypes: [ + "type-only", + "import", + "jsdoc", + "jsdoc-bracket-import", + ], + }, + ], + ); + }); + // * @returns {import('./goodby.mjs).wave} A goodbye + it("extracts @param & @returns for a function definitions", () => { + deepEqual( + extractTypescript( + `/** + * This function says hello and goodbye + * + * @param {import('./hello.mjs')} pHello a hello + * @returns {import('./goodbye.mjs').waveyWavey} A goodbye + */ + function findGoodbyeForGreeting(pHello) { + return Goodbyes[pHello]; + }`, + [], + true, + ), + [ + { + module: "./hello.mjs", + moduleSystem: "es6", + dynamic: false, + exoticallyRequired: false, + dependencyTypes: [ + "type-only", + "import", + "jsdoc", + "jsdoc-bracket-import", + ], + }, + { + module: "./goodbye.mjs", + moduleSystem: "es6", + dynamic: false, + exoticallyRequired: false, + dependencyTypes: [ + "type-only", + "import", + "jsdoc", + "jsdoc-bracket-import", + ], + }, + ], + ); + }); + + it("extracts @type whole module even when wrapped in type shenanigans (Partial)", () => { + deepEqual( + extractTypescript( + "/** @type {Partial} */", + [], + true, + ), + [ + { + module: "./hello.mjs", + moduleSystem: "es6", + dynamic: false, + exoticallyRequired: false, + dependencyTypes: [ + "type-only", + "import", + "jsdoc", + "jsdoc-bracket-import", + ], + }, + ], + ); + }); + + it("extracts @type whole module even when wrapped in type shenanigans (union)", () => { + deepEqual( + extractTypescript( + "/** @type {import('./hello.mjs')|import('./goodbye.mjs')} */", + [], + true, + ), + [ + { + module: "./hello.mjs", + moduleSystem: "es6", + dynamic: false, + exoticallyRequired: false, + dependencyTypes: [ + "type-only", + "import", + "jsdoc", + "jsdoc-bracket-import", + ], + }, + { + module: "./goodbye.mjs", + moduleSystem: "es6", + dynamic: false, + exoticallyRequired: false, + dependencyTypes: [ + "type-only", + "import", + "jsdoc", + "jsdoc-bracket-import", + ], + }, + ], + ); + }); + + it("extracts @type whole module even when wrapped in type shenanigans (Map)", () => { + deepEqual( + extractTypescript( + "/** @type {Map} */", + [], + true, + ), + [ + { + module: "./hello.mjs", + moduleSystem: "es6", + dynamic: false, + exoticallyRequired: false, + dependencyTypes: [ + "type-only", + "import", + "jsdoc", + "jsdoc-bracket-import", + ], + }, + ], + ); + }); + + it("extracts @type whole module even when wrapped in type shenanigans (Map & Partial)", () => { + deepEqual( + extractTypescript( + "/** @type {Map>} */", + [], + true, + ), + [ + { + module: "./hello.mjs", + moduleSystem: "es6", + dynamic: false, + exoticallyRequired: false, + dependencyTypes: [ + "type-only", + "import", + "jsdoc", + "jsdoc-bracket-import", + ], + }, + ], + ); + }); + it("extracts @return wrapped in type shenanigans)", () => { + deepEqual( + extractTypescript( + "/** @return {Promise} */", + [], + true, + ), + [ + { + module: "./types.js", + moduleSystem: "es6", + dynamic: false, + exoticallyRequired: false, + dependencyTypes: [ + "type-only", + "import", + "jsdoc", + "jsdoc-bracket-import", + ], + }, + ], + ); + }); + + it("leaves @type things alone that are not imports (but that look a bit like them)", () => { + deepEqual( + extractTypescript( + "/** @type {notAnImport('./hello.mjs').thing} */", + [], + true, + ), + [], + ); + }); + it("leaves @type things alone that is empty", () => { + deepEqual(extractTypescript("/** @type } */", [], true), []); + }); +}); diff --git a/test/extract/tsc/jsdoc-import-tags.spec.mjs b/test/extract/tsc/jsdoc-import-tags.spec.mjs index eb159f053..dbc84525e 100644 --- a/test/extract/tsc/jsdoc-import-tags.spec.mjs +++ b/test/extract/tsc/jsdoc-import-tags.spec.mjs @@ -93,11 +93,4 @@ describe("[U] ast-extractors/extract-typescript - jsdoc @imports", () => { [], ); }); - - it("does not extract imports with dynamic looking imports (@type {import('./ting.mjs')})", () => { - deepEqual( - extractTypescript("/** @type {import('./thing.mjs').thing} */", [], true), - [], - ); - }); }); diff --git a/tools/schema/.dependency-cruiser.mjs b/tools/schema/.dependency-cruiser.mjs index 41d0dd9da..d488883ca 100644 --- a/tools/schema/.dependency-cruiser.mjs +++ b/tools/schema/.dependency-cruiser.mjs @@ -1,6 +1,6 @@ import baseConfig from "../../.dependency-cruiser.mjs"; -/** @type {import('../../').IConfiguration} */ +/** @type {import('../../types/configuration.mjs').IConfiguration} */ export default { ...baseConfig, options: { diff --git a/types/.dependency-cruiser.mjs b/types/.dependency-cruiser.mjs index 1b4ddc734..12f13b06a 100644 --- a/types/.dependency-cruiser.mjs +++ b/types/.dependency-cruiser.mjs @@ -1,6 +1,6 @@ import baseConfig from "../.dependency-cruiser.mjs"; -/** @type {import('../').IConfiguration} */ +/** @type {import('./configuration.mjs').IConfiguration} */ export default { ...baseConfig, options: {