From 6c98818d4cfaf47ee8fd3e6cc8cb1b303b8dbbba Mon Sep 17 00:00:00 2001 From: sverweij Date: Sat, 30 Nov 2024 21:38:42 +0100 Subject: [PATCH] feat: also detect imports nested deeper into jsdoc tags --- src/extract/tsc/extract-typescript-deps.mjs | 67 ++++++++++++++++--- .../tsc/jsdoc-bracket-imports.spec.mjs | 46 +++++++++++-- 2 files changed, 98 insertions(+), 15 deletions(-) diff --git a/src/extract/tsc/extract-typescript-deps.mjs b/src/extract/tsc/extract-typescript-deps.mjs index c59000a90..6bafd154c 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 */ @@ -275,24 +276,68 @@ 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 keyIsBoring(pKey) { + return [ + "parent", + "pos", + "end", + "flags", + "emitNode", + "modifierFlagsCache", + "transformFlags", + "id", + "flowNode", + "symbol", + "original", + ].includes(pKey); +} + +/** + * Walks the given object, that can have both arrays and objects as values, and returns a new object with the same structure, but with all the values replaced by the result of the given function. + * @param {Object} obj The object to walk. + */ +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 (!keyIsBoring(lKey) && pObject[lKey]) { + walkJSDoc(pObject[lKey], pCollection); + } + } + } +} + +export function getJSDocImports(pObject) { + const lCollection = new Set(); + walkJSDoc(pObject, 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" && - typescript.SyntaxKind[pTag.typeExpression.type?.kind] === - "LastTypeNode" && - typescript.SyntaxKind[pTag.typeExpression.type.argument?.kind] === - "LiteralType" && - typescript.SyntaxKind[ - pTag.typeExpression.type.argument?.literal?.kind - ] === "StringLiteral" && - pTag.typeExpression.type.argument.literal.text, + typescript.SyntaxKind[pTag.typeExpression?.kind] === "FirstJSDocNode", ) - .map((pTag) => ({ - module: pTag.typeExpression.type.argument.literal.text, + .flatMap((pTag) => getJSDocImports(pTag)) + .map((pImportName) => ({ + module: pImportName, moduleSystem: "es6", exoticallyRequired: false, dependencyTypes: ["type-only", "import", "jsdoc", "jsdoc-bracket-import"], diff --git a/test/extract/tsc/jsdoc-bracket-imports.spec.mjs b/test/extract/tsc/jsdoc-bracket-imports.spec.mjs index 8aff34d2d..a63dfec9d 100644 --- a/test/extract/tsc/jsdoc-bracket-imports.spec.mjs +++ b/test/extract/tsc/jsdoc-bracket-imports.spec.mjs @@ -115,7 +115,7 @@ describe("[U] ast-extractors/extract-typescript - jsdoc 'bracket' imports", () = }); /* eslint mocha/no-skipped-tests: "off" */ - xit("extracts @type whole module even when wrapped in type shenanigans (Partial)", () => { + it("extracts @type whole module even when wrapped in type shenanigans (Partial)", () => { deepEqual( extractTypescript( "/** @type {Partial} */", @@ -138,7 +138,44 @@ describe("[U] ast-extractors/extract-typescript - jsdoc 'bracket' imports", () = ], ); }); - xit("extracts @type whole module even when wrapped in type shenanigans (Map)", () => { + + 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} */", @@ -161,10 +198,11 @@ describe("[U] ast-extractors/extract-typescript - jsdoc 'bracket' imports", () = ], ); }); - xit("extracts @type whole module even when wrapped in type shenanigans (Map & Parital)", () => { + + it("extracts @type whole module even when wrapped in type shenanigans (Map & Partial)", () => { deepEqual( extractTypescript( - "/** @type {string, Partial} */", + "/** @type {Map>} */", [], true, ),