@nestia/sdk
Version:
Nestia SDK and Swagger generator
247 lines (229 loc) • 7.12 kB
text/typescript
import ts from "typescript";
import { IReflectImport } from "../structures/IReflectImport";
import { IReflectType } from "../structures/IReflectType";
import { ImportAnalyzer } from "./ImportAnalyzer";
export namespace DtoAnalyzer {
export interface INodeProps {
checker: ts.TypeChecker;
imports: IReflectImport[];
typeNode: ts.TypeNode;
}
export interface ITypeProps {
checker: ts.TypeChecker;
imports: IReflectImport[];
type: ts.Type;
}
export interface IOutput {
imports: IReflectImport[];
type: IReflectType;
}
export const analyzeNode = (props: INodeProps): IOutput | null => {
try {
const container: IReflectImport[] = [];
const type: IReflectType = exploreNode(
{
checker: props.checker,
imports: props.imports,
container,
},
props.typeNode,
);
return {
type,
imports: ImportAnalyzer.merge(container),
};
} catch {
return null;
}
};
export const analyzeType = (props: ITypeProps): IOutput | null => {
try {
const container: IReflectImport[] = [];
const type: IReflectType = exploreType(
{
checker: props.checker,
imports: props.imports,
container,
},
props.type,
);
return {
type,
imports: ImportAnalyzer.merge(container),
};
} catch {
return null;
}
};
type IContext = Omit<INodeProps, "typeNode"> & {
container: IReflectImport[];
};
const exploreNode = (ctx: IContext, typeNode: ts.TypeNode): IReflectType => {
// Analyze symbol, and take special cares
if (ts.isIntersectionTypeNode(typeNode))
return {
name: typeNode.types
.map((child) => exploreNode(ctx, child))
.map(getEscapedText)
.join(" & "),
};
else if (ts.isUnionTypeNode(typeNode))
return {
name: typeNode.types
.map((child) => exploreNode(ctx, child))
.map(getEscapedText)
.join(" | "),
};
else if (ts.isParenthesizedTypeNode(typeNode))
return {
name: `(${exploreNode(ctx, typeNode.type).name})`,
};
else if (ts.isTypeOperatorNode(typeNode)) {
const prefix: string | null =
typeNode.operator === ts.SyntaxKind.KeyOfKeyword
? "keyof"
: typeNode.operator === ts.SyntaxKind.UniqueKeyword
? "unique"
: typeNode.operator === ts.SyntaxKind.ReadonlyKeyword
? "readonly"
: null;
if (prefix === null)
return exploreType(ctx, ctx.checker.getTypeFromTypeNode(typeNode));
return {
name: `${prefix} ${exploreNode(ctx, typeNode.type).name}`,
};
} else if (ts.isTypePredicateNode(typeNode) || ts.isTypeQueryNode(typeNode))
return exploreType(ctx, ctx.checker.getTypeFromTypeNode(typeNode));
else if (ts.isTypeReferenceNode(typeNode) === false)
return {
name: typeNode.getText(),
};
// Find matched import statement
const name: string = typeNode.typeName.getText();
const prefix: string = name.split(".")[0];
let matched: boolean = false;
const insert = (imp: IReflectImport): void => {
matched ||= true;
ctx.container.push(imp);
};
for (const imp of ctx.imports)
if (prefix === imp.default)
insert({
file: imp.file,
asterisk: null,
default: prefix,
elements: [],
});
else if (prefix === imp.asterisk)
insert({
file: imp.file,
asterisk: prefix,
default: null,
elements: [],
});
else if (imp.elements.includes(prefix))
insert({
file: imp.file,
asterisk: null,
default: null,
elements: [prefix],
});
if (prefix !== "Promise" && matched === false)
return exploreType(ctx, ctx.checker.getTypeFromTypeNode(typeNode));
// Finalize with generic arguments
if (!!typeNode.typeArguments?.length) {
const top: ts.TypeNode = typeNode.typeArguments[0];
if (name === "Promise") return exploreNode(ctx, top);
return {
name,
typeArguments: typeNode.typeArguments.map((child) =>
exploreNode(ctx, child),
),
};
}
return { name };
};
const exploreType = (ctx: IContext, type: ts.Type): IReflectType => {
// Analyze symbol, and take special cares
const symbol: ts.Symbol | undefined = type.aliasSymbol ?? type.symbol;
if (type.aliasSymbol === undefined && type.isUnionOrIntersection()) {
const joiner: string = type.isIntersection() ? " & " : " | ";
return {
name: type.types
.map((child) => exploreType(ctx, child))
.map(getEscapedText)
.join(joiner),
};
} else if (symbol === undefined)
return {
name: ctx.checker.typeToString(
type,
undefined,
ts.TypeFormatFlags.NoTruncation,
),
};
// Find matched import statement
const name: string = getNameOfSymbol(symbol);
const prefix: string = name.split(".")[0];
let matched: boolean = false;
const insert = (imp: IReflectImport): void => {
matched ||= true;
ctx.container.push(imp);
};
for (const imp of ctx.imports)
if (imp.elements.includes(prefix))
insert({
file: imp.file,
asterisk: null,
default: null,
elements: [prefix],
});
if (prefix !== "Promise" && matched === false)
emplaceSymbol(ctx, symbol, prefix);
// Finalize with generic arguments
const generic: readonly ts.Type[] = type.aliasSymbol
? (type.aliasTypeArguments ?? [])
: ctx.checker.getTypeArguments(type as ts.TypeReference);
return generic.length
? name === "Promise"
? exploreType(ctx, generic[0])
: {
name,
typeArguments: generic.map((child) => exploreType(ctx, child)),
}
: { name };
};
const emplaceSymbol = (
ctx: IContext,
symbol: ts.Symbol,
prefix: string,
): void => {
// GET SOURCE FILE
const sourceFile: ts.SourceFile | undefined =
symbol.declarations?.[0]?.getSourceFile();
if (sourceFile === undefined) return;
if (sourceFile.fileName.indexOf("/typescript/lib") === -1)
ctx.container.push({
file: sourceFile.fileName,
asterisk: null,
default: null,
elements: [prefix],
});
};
}
const getEscapedText = (type: IReflectType): string =>
type.typeArguments
? `${type.name}<${type.typeArguments.map(getEscapedText).join(", ")}>`
: type.name;
const getNameOfSymbol = (symbol: ts.Symbol): string =>
exploreName(
symbol.escapedName.toString(),
symbol.getDeclarations()?.[0]?.parent,
);
const exploreName = (name: string, decl?: ts.Node): string =>
decl && ts.isModuleBlock(decl)
? exploreName(
`${decl.parent.name.getFullText().trim()}.${name}`,
decl.parent.parent,
)
: name;