From fcb783cde44eb964ebdbb07e834f0580165cee12 Mon Sep 17 00:00:00 2001 From: Thomas V <2619415+tvillaren@users.noreply.github.com> Date: Mon, 24 Jun 2024 15:34:32 +0200 Subject: [PATCH] fix: adding support for imported QualifiedName (#243) * test: adding 2 failing cases * feat: adding type identification in type traversal * fix: first test now passes (external imports) * feat: excluding already referenced types * adding more tests * fix: typo --- src/core/generate.test.ts | 111 +++++++++++++++ src/core/generate.ts | 132 +++++++++++++----- src/utils/traverseTypes.test.ts | 230 ++++++++++++++++++++++++-------- src/utils/traverseTypes.ts | 29 +++- 4 files changed, 403 insertions(+), 99 deletions(-) diff --git a/src/core/generate.test.ts b/src/core/generate.test.ts index db1367e3..0c6a811f 100644 --- a/src/core/generate.test.ts +++ b/src/core/generate.test.ts @@ -929,6 +929,38 @@ describe("generate", () => { `); }); }); + + describe("reference to an enum value via external import Qualified Name X.Y", () => { + const input = "./person"; + + const sourceText = ` + import { PersonEnum } from "${input}" + + export interface Hero { + id: number + hero: PersonEnum.Hero + } + `; + + const { getZodSchemasFile } = generate({ + sourceText, + }); + + it("should generate the zod schemas with right import", () => { + expect(getZodSchemasFile(input)).toMatchInlineSnapshot(` + "// Generated by ts-to-zod + import { z } from "zod"; + + import { PersonEnum } from "${input}"; + + export const heroSchema = z.object({ + id: z.number(), + hero: z.literal(PersonEnum.Hero) + }); + " + `); + }); + }); }); describe("with input/output mappings to manage imports", () => { @@ -1161,5 +1193,84 @@ describe("generate", () => { `); }); }); + + describe("reference to an enum value via import Qualified Name X.Y referenced in mapping", () => { + const input = "./person"; + const output = "./person.zod"; + const inputOutputMappings: InputOutputMapping[] = [{ input, output }]; + + const sourceText = ` + import { PersonEnum } from "${input}" + + export interface Hero { + id: number + hero: PersonEnum.Hero + } + `; + + const { getZodSchemasFile } = generate({ + sourceText, + inputOutputMappings, + }); + + it("should generate the zod schemas with right import", () => { + expect(getZodSchemasFile(input)).toMatchInlineSnapshot(` + "// Generated by ts-to-zod + import { z } from "zod"; + + import { PersonEnum } from "${input}"; + + export const heroSchema = z.object({ + id: z.number(), + hero: z.literal(PersonEnum.Hero) + }); + " + `); + }); + }); + + describe("with useless imports", () => { + const input = "./hero"; + const output = "./hero.zod"; + + const input2 = "./superhero"; + const output2 = "./superhero.zod"; + const inputOutputMappings: InputOutputMapping[] = [ + { input, output }, + { input: input2, output: output2 }, + ]; + + const sourceText = ` + import { Hero, Villain } from "${input}" + import { SuperHero } from "${input2}" + + export interface Person { + id: number + hero: Hero + villain: Villain + } + `; + + const { getZodSchemasFile } = generate({ + sourceText, + inputOutputMappings, + }); + + it("should generate the zod schemas with right import", () => { + expect(getZodSchemasFile(input)).toMatchInlineSnapshot(` + "// Generated by ts-to-zod + import { z } from "zod"; + + import { heroSchema, villainSchema } from "${output}"; + + export const personSchema = z.object({ + id: z.number(), + hero: heroSchema, + villain: villainSchema + }); + " + `); + }); + }); }); }); diff --git a/src/core/generate.ts b/src/core/generate.ts index 154bf15c..3733bfe0 100644 --- a/src/core/generate.ts +++ b/src/core/generate.ts @@ -10,8 +10,9 @@ import { import { getSimplifiedJsDocTags } from "../utils/getSimplifiedJsDocTags"; import { resolveModules } from "../utils/resolveModules"; import { - getExtractedTypeNames, + getReferencedTypeNames, isTypeNode, + TypeNameReference, TypeNode, } from "../utils/traverseTypes"; @@ -103,21 +104,33 @@ export function generate({ const nodes: Array = []; // declare a map to store the interface name and its corresponding zod schema - const typeNameMapping = new Map(); + const typeNameMapping = new Map(); - // handling existing import statements - const importNamesAvailable = new Set(); - const importNodes: ts.ImportDeclaration[] = []; - const importNamesUsed: string[] = []; + /** + * Following const are keeping track of all the things import-related + */ + // All import nodes in the source file + const zodImportNodes: ts.ImportDeclaration[] = []; + + // Keep track of all the external import names available in the source file + const externalImportNamesAvailable = new Set(); + + // Keep track of all the imports that have an entry in the config file const importedZodNamesAvailable = new Map(); - const typesNeedToBeExtracted = new Set(); + // Keep track of all referenced types in the source file + const candidateTypesToBeExtracted = new Set(); const typeNameMapBuilder = (node: ts.Node) => { if (isTypeNode(node)) { typeNameMapping.set(node.name.text, node); + return; } + if (ts.isImportDeclaration(node) && node.importClause) { + const identifiers = getImportIdentifiers(node); + identifiers.forEach((i) => typeNameMapping.set(i, node)); + // Check if we're importing from a mapped file const eligibleMapping = inputOutputMappings.find( (io: InputOutputMapping) => @@ -136,16 +149,15 @@ export function generate({ importedZodNamesAvailable.set(i, schemaMethod(i)) ); - const importNode = createImportNode( + const zodImportNode = createImportNode( identifiers.map(schemaMethod), eligibleMapping.output ); - importNodes.push(importNode); // We assume all identifiers will be used so pushing the whole node + zodImportNodes.push(zodImportNode); } // Not a Zod import, handling it as 3rd party import later on else { - const identifiers = getImportIdentifiers(node); - identifiers.forEach((i) => importNamesAvailable.add(i)); + identifiers.forEach((i) => externalImportNamesAvailable.add(i)); } } }; @@ -162,34 +174,66 @@ export function generate({ if (!jsDocTagFilter(tags)) return; if (!nameFilter(node.name.text)) return; - const typeNames = getExtractedTypeNames(node, sourceFile); - typeNames.forEach((typeName) => { - typesNeedToBeExtracted.add(typeName); + const typeNames = getReferencedTypeNames(node, sourceFile); + typeNames.forEach((typeRef) => { + candidateTypesToBeExtracted.add(typeRef); }); } }; ts.forEachChild(sourceFile, visitor); + // All external import names actually used in the source file + const importNamesUsed: string[] = []; + + // All zod imports actually used in the source file const importedZodSchemas = new Set(); - typesNeedToBeExtracted.forEach((typeName) => { - const node = typeNameMapping.get(typeName); + // All original import to keep in the target + const importsToKeep = new Map(); + + /** + * We browse all the extracted type references from the source file + * To check if they reference a node from the file or if they are imported + */ + candidateTypesToBeExtracted.forEach((typeRef) => { + const node = typeNameMapping.get(typeRef.name); + if (node) { - nodes.push(node); - return; + // If we have a reference in the file, we add it to the nodes, no import needed + if (isTypeNode(node)) { + nodes.push(node); + return; + } + + // If the reference is part of a qualified name, we need to import it from the same file + if (typeRef.partOfQualifiedName) { + const identifiers = importsToKeep.get(node); + if (identifiers) { + identifiers.push(typeRef.name); + } else { + importsToKeep.set(node, [typeRef.name]); + } + return; + } } - if (importNamesAvailable.has(typeName)) { - importNamesUsed.push(typeName); + + // If the reference is coming from an external import, we'll need to generate a specific statement + // and keep the external import + if (externalImportNamesAvailable.has(typeRef.name)) { + importNamesUsed.push(typeRef.name); return; } - if (importedZodNamesAvailable.has(typeName)) { - importedZodSchemas.add(importedZodNamesAvailable.get(typeName) as string); + + // If the reference is coming from a mapped import, we'll import the corresponding zod schema + if (importedZodNamesAvailable.has(typeRef.name)) { + importedZodSchemas.add( + importedZodNamesAvailable.get(typeRef.name) as string + ); return; } }); // Generate zod schemas for type nodes - const getDependencyName = (identifierName: string) => { if (importedZodNamesAvailable.has(identifierName)) { return importedZodNamesAvailable.get(identifierName) as string; @@ -237,7 +281,9 @@ export function generate({ string, { typeName: string; value: ts.VariableStatement } >(); - const typeImports: Set = new Set(); + + // Keep track of types which need to be imported from the source file + const sourceTypeImports: Set = new Set(); // Zod schemas with direct or indirect dependencies that are not in `zodSchemas`, won't be generated const zodSchemasWithMissingDependencies = new Set(); @@ -266,14 +312,14 @@ export function generate({ if (notGeneratedDependencies.length === 0) { done = false; if (isCircular) { - typeImports.add(typeName); + sourceTypeImports.add(typeName); statements.set(varName, { value: transformRecursiveSchema("z", statement, typeName), typeName, }); } else { if (requiresImport) { - typeImports.add(typeName); + sourceTypeImports.add(typeName); } statements.set(varName, { value: statement, typeName }); } @@ -300,7 +346,7 @@ export function generate({ !zodSchemasWithMissingDependencies.has(varName) ) .forEach(({ varName, statement, typeName }) => { - typeImports.add(typeName); + sourceTypeImports.add(typeName); statements.set(varName, { value: transformRecursiveSchema("z", statement, typeName), typeName, @@ -332,21 +378,39 @@ ${Array.from(zodSchemasWithMissingDependencies).join("\n")}` const transformedSourceText = printerWithComments.printFile(sourceFile); - const typeImportsValues = Array.from(typeImports.values()); + const zodImportToOutput = zodImportNodes.filter((node) => { + const nodeIdentifiers = getImportIdentifiers(node); + return nodeIdentifiers.some((i) => importedZodSchemas.has(i)); + }); + + const originalImportsToOutput = Array.from(importsToKeep.keys()).map((node) => + createImportNode( + importsToKeep.get(node)!, + (node.moduleSpecifier as ts.StringLiteral).text + ) + ); + + const sourceTypeImportsValues = Array.from(sourceTypeImports.values()); const getZodSchemasFile = ( typesImportPath: string ) => `// Generated by ts-to-zod import { z } from "zod"; ${ - typeImportsValues.length - ? `import { ${typeImportsValues.join(", ")} } from "${typesImportPath}";\n` + sourceTypeImportsValues.length + ? `import { ${sourceTypeImportsValues.join( + ", " + )} } from "${typesImportPath}";\n` : "" } ${ - importNodes.length - ? importNodes.map((node) => print(node)).join("\n") + "\n\n" + zodImportToOutput.length + ? zodImportToOutput.map((node) => print(node)).join("\n") + "\n\n" : "" -}${Array.from(statements.values()) +}${ + originalImportsToOutput.length + ? originalImportsToOutput.map((node) => print(node)).join("\n") + "\n\n" + : "" + }${Array.from(statements.values()) .map((statement) => print(statement.value)) .join("\n\n")} `; @@ -448,7 +512,7 @@ ${Array.from(statements.values()) /** * `true` if zodSchemaFile have some resolvable circular dependencies */ - hasCircularDependencies: typeImportsValues.length > 0, + hasCircularDependencies: sourceTypeImportsValues.length > 0, }; } diff --git a/src/utils/traverseTypes.test.ts b/src/utils/traverseTypes.test.ts index 7b0dca87..40489641 100644 --- a/src/utils/traverseTypes.test.ts +++ b/src/utils/traverseTypes.test.ts @@ -1,5 +1,5 @@ import ts from "typescript"; -import { getExtractedTypeNames } from "./traverseTypes"; +import { getReferencedTypeNames } from "./traverseTypes"; import { findNode } from "./findNode"; describe("traverseTypes", () => { @@ -7,19 +7,19 @@ describe("traverseTypes", () => { it("should find only itself", () => { const testCases = [ ` - export type Superhero = { + export type SuperHero = { id: number, name: string }; `, ` - export interface Superhero = { + export interface SuperHero = { id: number, name: string }; `, ` - export enum Superhero = { + export enum SuperHero = { Superman = "superman", ClarkKent = "clark_kent", }; @@ -28,105 +28,135 @@ describe("traverseTypes", () => { testCases.forEach((source: string) => { const result = extractNames(source); - expect(result).toEqual(["Superhero"]); + expect(result).toEqual([ + { name: "SuperHero", partOfQualifiedName: false }, + ]); }); }); it("should extract type referenced in property", () => { const source = ` - export interface Superhero { + export interface SuperHero { id: number, person: Person, }`; const result = extractNames(source); - expect(result).toEqual(["Superhero", "Person"]); + expect(result).toEqual([ + { name: "SuperHero", partOfQualifiedName: false }, + { name: "Person", partOfQualifiedName: false }, + ]); }); it("should extract type referenced in extend clause", () => { const source = ` - export interface Superhero extends Person { + export interface SuperHero extends Person { id: number, }`; const result = extractNames(source); - expect(result).toEqual(["Superhero", "Person"]); + expect(result).toEqual([ + { name: "SuperHero", partOfQualifiedName: false }, + { name: "Person", partOfQualifiedName: false }, + ]); }); it("should extract type referenced in multiple extend clauses", () => { const source = ` - export interface Superhero extends Person, Person2 { + export interface SuperHero extends Person, Person2 { id: number, }`; const result = extractNames(source); - expect(result).toEqual(["Superhero", "Person", "Person2"]); + expect(result).toEqual([ + { name: "SuperHero", partOfQualifiedName: false }, + { name: "Person", partOfQualifiedName: false }, + { name: "Person2", partOfQualifiedName: false }, + ]); }); it("should extract type referenced in extend clause with Pick helper", () => { const source = ` - export interface Superhero extends Pick { + export interface SuperHero extends Pick { id: number, }`; const result = extractNames(source); - expect(result).toEqual(["Superhero", "Person"]); + expect(result).toEqual([ + { name: "SuperHero", partOfQualifiedName: false }, + { name: "Person", partOfQualifiedName: false }, + ]); }); it("should extract type referenced in extend clause with Omit helper", () => { const source = ` - export interface Superhero extends Omit { + export interface SuperHero extends Omit { id: number, }`; const result = extractNames(source); - expect(result).toEqual(["Superhero", "Person"]); + expect(result).toEqual([ + { name: "SuperHero", partOfQualifiedName: false }, + { name: "Person", partOfQualifiedName: false }, + ]); }); it("should extract type referenced in extend clause with Partial helper", () => { const source = ` - export interface Superhero extends Partial { + export interface SuperHero extends Partial { id: number, }`; const result = extractNames(source); - expect(result).toEqual(["Superhero", "Person"]); + expect(result).toEqual([ + { name: "SuperHero", partOfQualifiedName: false }, + { name: "Person", partOfQualifiedName: false }, + ]); }); it("should extract type referenced in extend clause with Record helper", () => { const source = ` - export interface Superhero extends Record { + export interface SuperHero extends Record { id: number, }`; const result = extractNames(source); - expect(result).toEqual(["Superhero", "Person2"]); + expect(result).toEqual([ + { name: "SuperHero", partOfQualifiedName: false }, + { name: "Person2", partOfQualifiedName: false }, + ]); }); it("should extract type referenced in property as array", () => { const source = ` - export interface Superhero { + export interface SuperHero { id: number, sidekicks: Person[], }`; const result = extractNames(source); - expect(result).toEqual(["Superhero", "Person"]); + expect(result).toEqual([ + { name: "SuperHero", partOfQualifiedName: false }, + { name: "Person", partOfQualifiedName: false }, + ]); }); it("should extract type referenced as array in union property", () => { const source = ` - export interface Superhero { + export interface SuperHero { sidekicks: Person[] | null, }`; const result = extractNames(source); - expect(result).toEqual(["Superhero", "Person"]); + expect(result).toEqual([ + { name: "SuperHero", partOfQualifiedName: false }, + { name: "Person", partOfQualifiedName: false }, + ]); }); it("should extract nested type reference", () => { const source = ` - export interface Superhero { + export interface SuperHero { id: number, person: { type: Person, @@ -134,7 +164,10 @@ describe("traverseTypes", () => { }`; const result = extractNames(source); - expect(result).toEqual(["Superhero", "Person"]); + expect(result).toEqual([ + { name: "SuperHero", partOfQualifiedName: false }, + { name: "Person", partOfQualifiedName: false }, + ]); }); it("should extract union type reference", () => { @@ -145,7 +178,11 @@ describe("traverseTypes", () => { }`; const result = extractNames(source); - expect(result).toEqual(["Person", "SuperHero", "Villain"]); + expect(result).toEqual([ + { name: "Person", partOfQualifiedName: false }, + { name: "SuperHero", partOfQualifiedName: false }, + { name: "Villain", partOfQualifiedName: false }, + ]); }); it("should extract intersection type reference", () => { @@ -156,7 +193,11 @@ describe("traverseTypes", () => { }`; const result = extractNames(source); - expect(result).toEqual(["Person", "SuperHero", "Villain"]); + expect(result).toEqual([ + { name: "Person", partOfQualifiedName: false }, + { name: "SuperHero", partOfQualifiedName: false }, + { name: "Villain", partOfQualifiedName: false }, + ]); }); it("should extract intersection type reference with type literal", () => { @@ -167,7 +208,11 @@ describe("traverseTypes", () => { }`; const result = extractNames(source); - expect(result).toEqual(["Person", "SuperHero", "Villain"]); + expect(result).toEqual([ + { name: "Person", partOfQualifiedName: false }, + { name: "SuperHero", partOfQualifiedName: false }, + { name: "Villain", partOfQualifiedName: false }, + ]); }); it("should extract types between parenthesis", () => { @@ -178,7 +223,10 @@ describe("traverseTypes", () => { }`; const result = extractNames(source); - expect(result).toEqual(["Person", "SuperHero"]); + expect(result).toEqual([ + { name: "Person", partOfQualifiedName: false }, + { name: "SuperHero", partOfQualifiedName: false }, + ]); }); it("should extract union types between parenthesis", () => { @@ -189,7 +237,11 @@ describe("traverseTypes", () => { }`; const result = extractNames(source); - expect(result).toEqual(["Person", "SuperHero", "Villain"]); + expect(result).toEqual([ + { name: "Person", partOfQualifiedName: false }, + { name: "SuperHero", partOfQualifiedName: false }, + { name: "Villain", partOfQualifiedName: false }, + ]); }); it("should extract types from Tuple", () => { @@ -200,7 +252,11 @@ describe("traverseTypes", () => { }`; const result = extractNames(source); - expect(result).toEqual(["Person", "SuperHero", "Villain"]); + expect(result).toEqual([ + { name: "Person", partOfQualifiedName: false }, + { name: "SuperHero", partOfQualifiedName: false }, + { name: "Villain", partOfQualifiedName: false }, + ]); }); it("should extract type from type alias", () => { @@ -208,7 +264,10 @@ describe("traverseTypes", () => { export type Person = SuperHero `; const result = extractNames(source); - expect(result).toEqual(["Person", "SuperHero"]); + expect(result).toEqual([ + { name: "Person", partOfQualifiedName: false }, + { name: "SuperHero", partOfQualifiedName: false }, + ]); }); it("should extract type from type alias with union", () => { @@ -216,7 +275,11 @@ describe("traverseTypes", () => { export type Person = Villain | SuperHero `; const result = extractNames(source); - expect(result).toEqual(["Person", "Villain", "SuperHero"]); + expect(result).toEqual([ + { name: "Person", partOfQualifiedName: false }, + { name: "Villain", partOfQualifiedName: false }, + { name: "SuperHero", partOfQualifiedName: false }, + ]); }); it("should extract type from type alias with array", () => { @@ -224,7 +287,10 @@ describe("traverseTypes", () => { export type Person = Villain[] `; const result = extractNames(source); - expect(result).toEqual(["Person", "Villain"]); + expect(result).toEqual([ + { name: "Person", partOfQualifiedName: false }, + { name: "Villain", partOfQualifiedName: false }, + ]); }); it("should extract type from type alias with Array helper", () => { @@ -232,7 +298,10 @@ describe("traverseTypes", () => { export type Person = Array `; const result = extractNames(source); - expect(result).toEqual(["Person", "Villain"]); + expect(result).toEqual([ + { name: "Person", partOfQualifiedName: false }, + { name: "Villain", partOfQualifiedName: false }, + ]); }); it("should extract type from type alias with Promise helper", () => { @@ -240,7 +309,10 @@ describe("traverseTypes", () => { export type Person = Promise `; const result = extractNames(source); - expect(result).toEqual(["Person", "Villain"]); + expect(result).toEqual([ + { name: "Person", partOfQualifiedName: false }, + { name: "Villain", partOfQualifiedName: false }, + ]); }); it("should extract type from type alias with Required helper", () => { @@ -248,7 +320,10 @@ describe("traverseTypes", () => { export type Person = Required `; const result = extractNames(source); - expect(result).toEqual(["Person", "Villain"]); + expect(result).toEqual([ + { name: "Person", partOfQualifiedName: false }, + { name: "Villain", partOfQualifiedName: false }, + ]); }); it("should extract type from type alias with Partial helper", () => { @@ -256,7 +331,10 @@ describe("traverseTypes", () => { export type Person = Partial `; const result = extractNames(source); - expect(result).toEqual(["Person", "Villain"]); + expect(result).toEqual([ + { name: "Person", partOfQualifiedName: false }, + { name: "Villain", partOfQualifiedName: false }, + ]); }); it("should extract type from type alias with Omit helper", () => { @@ -264,7 +342,10 @@ describe("traverseTypes", () => { export type Person = Omit `; const result = extractNames(source); - expect(result).toEqual(["Person", "Villain"]); + expect(result).toEqual([ + { name: "Person", partOfQualifiedName: false }, + { name: "Villain", partOfQualifiedName: false }, + ]); }); it("should extract type from type alias with Pick helper", () => { @@ -272,7 +353,10 @@ describe("traverseTypes", () => { export type Person = Pick `; const result = extractNames(source); - expect(result).toEqual(["Person", "Villain"]); + expect(result).toEqual([ + { name: "Person", partOfQualifiedName: false }, + { name: "Villain", partOfQualifiedName: false }, + ]); }); it("should extract type from type alias with Record helper", () => { @@ -280,7 +364,10 @@ describe("traverseTypes", () => { export type Person = Record `; const result = extractNames(source); - expect(result).toEqual(["Person", "Villain"]); + expect(result).toEqual([ + { name: "Person", partOfQualifiedName: false }, + { name: "Villain", partOfQualifiedName: false }, + ]); }); it("should extract type from type alias with parenthesis", () => { @@ -288,7 +375,10 @@ describe("traverseTypes", () => { export type Person = (Villain) `; const result = extractNames(source); - expect(result).toEqual(["Person", "Villain"]); + expect(result).toEqual([ + { name: "Person", partOfQualifiedName: false }, + { name: "Villain", partOfQualifiedName: false }, + ]); }); it("should extract type from type alias with parenthesis", () => { @@ -296,7 +386,11 @@ describe("traverseTypes", () => { export type Person = (Villain | Hero)[]`; const result = extractNames(source); - expect(result).toEqual(["Person", "Villain", "Hero"]); + expect(result).toEqual([ + { name: "Person", partOfQualifiedName: false }, + { name: "Villain", partOfQualifiedName: false }, + { name: "Hero", partOfQualifiedName: false }, + ]); }); it("should extract type from type alias with object literal", () => { @@ -304,7 +398,10 @@ describe("traverseTypes", () => { export type Person = { hero: SuperHero } `; const result = extractNames(source); - expect(result).toEqual(["Person", "SuperHero"]); + expect(result).toEqual([ + { name: "Person", partOfQualifiedName: false }, + { name: "SuperHero", partOfQualifiedName: false }, + ]); }); it("should extract type from type alias with union & object literal", () => { @@ -312,7 +409,11 @@ describe("traverseTypes", () => { export type Person = Villain | { hero: SuperHero } `; const result = extractNames(source); - expect(result).toEqual(["Person", "Villain", "SuperHero"]); + expect(result).toEqual([ + { name: "Person", partOfQualifiedName: false }, + { name: "Villain", partOfQualifiedName: false }, + { name: "SuperHero", partOfQualifiedName: false }, + ]); }); it("should extract types from a very weird type definition (testing edge cases)", () => { @@ -324,18 +425,31 @@ describe("traverseTypes", () => { const result = extractNames(source); expect(result).toEqual([ - "Person", - "SuperHero", - "Person2", - "SuperHero2", - "Villain2", - "SuperHero3", - "Villain3", - "A", - "B", - "C", - "D", - "Villain", + { name: "Person", partOfQualifiedName: false }, + { name: "SuperHero", partOfQualifiedName: false }, + { name: "Person2", partOfQualifiedName: false }, + { name: "SuperHero2", partOfQualifiedName: false }, + { name: "Villain2", partOfQualifiedName: false }, + { name: "SuperHero3", partOfQualifiedName: false }, + { name: "Villain3", partOfQualifiedName: false }, + { name: "A", partOfQualifiedName: false }, + { name: "B", partOfQualifiedName: false }, + { name: "C", partOfQualifiedName: false }, + { name: "D", partOfQualifiedName: false }, + { name: "Villain", partOfQualifiedName: false }, + ]); + }); + + it("should extract type when part of QualifiedName", () => { + const source = ` + export type Hero = { + qualified: Person.SuperHero + }`; + + const result = extractNames(source); + expect(result).toEqual([ + { name: "Hero", partOfQualifiedName: false }, + { name: "Person", partOfQualifiedName: true }, ]); }); }); @@ -363,5 +477,5 @@ function extractNames(sourceText: string) { throw new Error("No `type` or `interface` found!"); } - return getExtractedTypeNames(declaration, sourceFile); + return getReferencedTypeNames(declaration, sourceFile); } diff --git a/src/utils/traverseTypes.ts b/src/utils/traverseTypes.ts index 6caacee6..74ff4a5e 100644 --- a/src/utils/traverseTypes.ts +++ b/src/utils/traverseTypes.ts @@ -15,6 +15,11 @@ export type TypeNode = | ts.TypeAliasDeclaration | ts.EnumDeclaration; +export type TypeNameReference = { + name: string; + partOfQualifiedName: boolean; +}; + export function isTypeNode(node: ts.Node): node is TypeNode { return ( ts.isInterfaceDeclaration(node) || @@ -23,14 +28,14 @@ export function isTypeNode(node: ts.Node): node is TypeNode { ); } -export function getExtractedTypeNames( +export function getReferencedTypeNames( node: ts.InterfaceDeclaration | ts.TypeAliasDeclaration | ts.EnumDeclaration, sourceFile: ts.SourceFile -): string[] { - const referenceTypeNames = new Set(); +): TypeNameReference[] { + const referenceTypeNames = new Set(); // Adding the node name - referenceTypeNames.add(node.name.text); + referenceTypeNames.add({ name: node.name.text, partOfQualifiedName: false }); const visitorExtract = (child: ts.Node) => { if (!ts.isPropertySignature(child)) { @@ -65,12 +70,19 @@ export function getExtractedTypeNames( }; const handleTypeReferenceNode = (typeRefNode: ts.TypeReferenceNode) => { + if (ts.isQualifiedName(typeRefNode.typeName)) { + const typeName = typeRefNode.typeName.left.getText(sourceFile); + referenceTypeNames.add({ name: typeName, partOfQualifiedName: true }); + return; + } + const typeName = typeRefNode.typeName.getText(sourceFile); if (typeScriptHelper.indexOf(typeName) > -1 && typeRefNode.typeArguments) { typeRefNode.typeArguments.forEach((t) => handleTypeNode(t)); - } else { - referenceTypeNames.add(typeName); + return; } + + referenceTypeNames.add({ name: typeName, partOfQualifiedName: false }); }; if (ts.isInterfaceDeclaration(node)) { @@ -87,7 +99,10 @@ export function getExtractedTypeNames( } if (typeScriptHelper.indexOf(typeName) === -1) { - referenceTypeNames.add(typeName); + referenceTypeNames.add({ + name: typeName, + partOfQualifiedName: false, + }); } }); });