@nestia/sdk
Version:
Nestia SDK and Swagger generator
172 lines (159 loc) • 4.83 kB
text/typescript
import ts from "typescript";
import { IReflectType } from "../structures/IReflectType";
import { IReflectTypeImport } from "../structures/IReflectTypeImport";
import { MapUtil } from "../utils/MapUtil";
export namespace ImportAnalyzer {
export interface IOutput {
imports: IReflectTypeImport[];
type: IReflectType | null;
}
export const analyze = (
checker: ts.TypeChecker,
generics: WeakMap<ts.Type, ts.Type>,
type: ts.Type,
): IOutput => {
const imports: Map<string, Set<string>> = new Map();
try {
type = escape(checker, type);
return {
type: explore({
checker,
generics,
imports,
type,
}),
imports: [...imports].map(([file, instances]) => ({
file,
instances: Array.from(instances),
})),
};
} catch {
return {
imports: [],
type: null,
};
}
};
export const unique = (
imports: IReflectTypeImport[],
): IReflectTypeImport[] => {
const map: Map<string, Set<string>> = new Map();
imports.forEach(({ file, instances }) => {
const set: Set<string> = MapUtil.take(map, file, () => new Set());
instances.forEach((instance) => set.add(instance));
});
return [...map].map(([file, instances]) => ({
file,
instances: Array.from(instances),
}));
};
/* ---------------------------------------------------------
TYPE
--------------------------------------------------------- */
const escape = (checker: ts.TypeChecker, type: ts.Type): ts.Type => {
if (type.symbol && getNameOfSymbol(type.symbol) === "Promise") {
const generic: readonly ts.Type[] = checker.getTypeArguments(
type as ts.TypeReference,
);
if (generic.length !== 1)
throw new Error(
"Error on ImportAnalyzer.analyze(): invalid promise type.",
);
type = generic[0];
}
return type;
};
const getNameOfSymbol = (symbol: ts.Symbol): string =>
exploreName(
symbol.escapedName.toString(),
symbol.getDeclarations()?.[0]?.parent,
);
/* ---------------------------------------------------------
ESCAPED TEXT WITH IMPORT STATEMENTS
--------------------------------------------------------- */
const explore = (props: {
checker: ts.TypeChecker;
generics: WeakMap<ts.Type, ts.Type>;
imports: Map<string, Set<string>>;
type: ts.Type;
}): IReflectType => {
//----
// CONDITIONAL BRANCHES
//----
// DECOMPOSE GENERIC ARGUMENT
let type: ts.Type = props.type;
while (props.generics.has(type) === true) type = props.generics.get(type)!;
// PRIMITIVE
const symbol: ts.Symbol | undefined = type.aliasSymbol ?? type.symbol;
// UNION OR INTERSECT
if (type.aliasSymbol === undefined && type.isUnionOrIntersection()) {
const joiner: string = type.isIntersection() ? " & " : " | ";
return {
name: type.types
.map((child) =>
explore({
...props,
type: child,
}),
)
.map(getEscapedText)
.join(joiner),
};
}
// NO SYMBOL
else if (symbol === undefined)
return {
name: props.checker.typeToString(
type,
undefined,
ts.TypeFormatFlags.NoTruncation,
),
};
//----
// SPECIALIZATION
//----
const name: string = getNameOfSymbol(symbol);
const sourceFile: ts.SourceFile | undefined =
symbol.declarations?.[0]?.getSourceFile();
if (sourceFile === undefined) return { name };
else if (sourceFile.fileName.indexOf("typescript/lib") === -1) {
const set: Set<string> = MapUtil.take(
props.imports,
sourceFile.fileName,
() => new Set(),
);
set.add(name.split(".")[0]);
}
// CHECK GENERIC
const generic: readonly ts.Type[] = type.aliasSymbol
? type.aliasTypeArguments ?? []
: props.checker.getTypeArguments(type as ts.TypeReference);
return generic.length
? name === "Promise"
? explore({
...props,
type: generic[0],
})
: {
name,
typeArguments: generic.map((child) =>
explore({
...props,
type: child,
}),
),
}
: { name };
};
const exploreName = (name: string, decl?: ts.Node): string =>
decl && ts.isModuleBlock(decl)
? exploreName(
`${decl.parent.name.getFullText().trim()}.${name}`,
decl.parent.parent,
)
: name;
const getEscapedText = (type: IReflectType): string =>
type.typeArguments
? `${type.name}<${type.typeArguments.map(getEscapedText).join(", ")}>`
: type.name;
}