diff --git a/.dependency-cruiser.mjs b/.dependency-cruiser.mjs index f7ebe0f90..caa96fe77 100644 --- a/.dependency-cruiser.mjs +++ b/.dependency-cruiser.mjs @@ -3,7 +3,7 @@ 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/src/extract/tsc/extract-typescript-deps.mjs b/src/extract/tsc/extract-typescript-deps.mjs index 3a142258c..12c2fd178 100644 --- a/src/extract/tsc/extract-typescript-deps.mjs +++ b/src/extract/tsc/extract-typescript-deps.mjs @@ -275,10 +275,43 @@ function extractJSDocImportTags(pJSDocTags) { })); } +function extractJSDocBracketImports(pJSDocTags) { + return pJSDocTags + .filter( + (pTag) => + pTag.tagName.escapedText !== "import" && + /* c8 ignore start */ + typescript.SyntaxKind[pTag.typeExpression?.kind ?? -1] === + "FirstJSDocNode" && + typescript.SyntaxKind[pTag.typeExpression.type?.kind ?? -1] === + "LastTypeNode" && + typescript.SyntaxKind[pTag.typeExpression.type.argument?.kind ?? -1] === + "LiteralType" && + typescript.SyntaxKind[ + pTag.typeExpression.type.argument?.literal?.kind ?? -1 + ] === "StringLiteral" && + /* c8 ignore stop*/ + pTag.typeExpression.type.argument.literal.text, + ) + .map((pTag) => ({ + module: pTag.typeExpression.type.argument.literal.text, + 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)); + 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); } /** 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..8aff34d2d --- /dev/null +++ b/test/extract/tsc/jsdoc-bracket-imports.spec.mjs @@ -0,0 +1,201 @@ +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", + ], + }, + ], + ); + }); + + /* eslint mocha/no-skipped-tests: "off" */ + xit("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", + ], + }, + ], + ); + }); + xit("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", + ], + }, + ], + ); + }); + xit("extracts @type whole module even when wrapped in type shenanigans (Map & Parital)", () => { + deepEqual( + extractTypescript( + "/** @type {string, Partial} */", + [], + true, + ), + [ + { + module: "./hello.mjs", + 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: {