Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support for sourceless nodes. #1386

Merged
merged 24 commits into from
Oct 17, 2022
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
acda4a4
feat: updates
arthurfiorette Aug 28, 2022
3c539c3
Merge branch 'next' of github.com:vega/ts-json-schema-generator into …
arthurfiorette Aug 28, 2022
58fb069
chore: better comments
arthurfiorette Aug 28, 2022
c4180f9
test: added tests for `useDefinitions`
arthurfiorette Aug 29, 2022
9108d66
fix: added useDefinition param
arthurfiorette Aug 29, 2022
72c92f2
fix: more tests
arthurfiorette Aug 30, 2022
965cc8a
test: sourceless nodes and promise wrapping tests
arthurfiorette Aug 30, 2022
a1a3d8f
fix: correct definition removal when needed
arthurfiorette Aug 30, 2022
9e022fa
refactor: protected `createSubContext` & better typeSymbol detection
arthurfiorette Aug 30, 2022
f04cd5b
chore: mention PR in TODO comment
arthurfiorette Aug 30, 2022
1f6b11b
Merge branch 'next' of github.com:vega/ts-json-schema-generator into …
arthurfiorette Sep 9, 2022
27333ea
refactor: more precise error description
arthurfiorette Sep 9, 2022
4a5f575
refactor: use protected properties
arthurfiorette Sep 9, 2022
5ea3e11
Update src/NodeParser/TypeReferenceNodeParser.ts
arthurfiorette Sep 9, 2022
3010834
chore: removed optional `#/references/`
arthurfiorette Sep 9, 2022
25d1fe3
fix: removed DEFINITION import
arthurfiorette Sep 9, 2022
caf3fe5
fix: Im dumb :)
arthurfiorette Sep 10, 2022
bc61919
Merge branch 'next' of github.com:vega/ts-json-schema-generator into …
arthurfiorette Oct 2, 2022
fa175fe
Update test/sourceless-nodes/index.test.ts
arthurfiorette Oct 16, 2022
9529fef
Update test/sourceless-nodes/index.test.ts
arthurfiorette Oct 16, 2022
f12f953
refactor: removed unused !
arthurfiorette Oct 16, 2022
4108cf0
Merge branch 'next' into vega-pr
arthurfiorette Oct 16, 2022
4163f8f
style: formatted code
arthurfiorette Oct 16, 2022
b428d2d
fix: getPropertyName handles sourceless nodes
arthurfiorette Oct 16, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/NodeParser/AnnotatedNodeParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ export class AnnotatedNodeParser implements SubNodeParser {
const baseType = this.childNodeParser.createType(node, context, reference);

// Don't return annotations for lib types such as Exclude.
if (node.getSourceFile().fileName.match(/[/\\]typescript[/\\]lib[/\\]lib\.[^/\\]+\.d\.ts$/i)) {
// Sourceless nodes may not have a fileName, just ignore them.
if (node.getSourceFile()?.fileName.match(/[/\\]typescript[/\\]lib[/\\]lib\.[^/\\]+\.d\.ts$/i)) {
let specialCase = false;

// Special case for Exclude<T, U>: use the annotation of T.
Expand Down
7 changes: 5 additions & 2 deletions src/NodeParser/TypeLiteralNodeParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,12 @@ export class TypeLiteralNodeParser implements SubNodeParser {
.filter(ts.isPropertySignature)
.filter((propertyNode) => !isNodeHidden(propertyNode))
.map((propertyNode) => {
const propertySymbol: ts.Symbol = (propertyNode as any).symbol;
const type = this.childNodeParser.createType(propertyNode.type!, context);
const objectProperty = new ObjectProperty(propertySymbol.getName(), type, !propertyNode.questionToken);

// The following avoids errors when propertySymbol is undefined
const name = (propertyNode as any).symbol?.getName() || (propertyNode.name as any).escapedText;

const objectProperty = new ObjectProperty(name, type, !propertyNode.questionToken);

return objectProperty;
})
Expand Down
63 changes: 42 additions & 21 deletions src/NodeParser/TypeReferenceNodeParser.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import ts from "typescript";

import { Context, NodeParser } from "../NodeParser";
import { SubNodeParser } from "../SubNodeParser";
import type { SubNodeParser } from "../SubNodeParser";
import { AnnotatedType } from "../Type/AnnotatedType";
import { AnyType } from "../Type/AnyType";
import { ArrayType } from "../Type/ArrayType";
import { BaseType } from "../Type/BaseType";
import { NeverType } from "../Type/NeverType";
import type { BaseType } from "../Type/BaseType";
import { StringType } from "../Type/StringType";

const invalidTypes: { [index: number]: boolean } = {
const invalidTypes: Record<number, boolean> = {
[ts.SyntaxKind.ModuleDeclaration]: true,
[ts.SyntaxKind.VariableDeclaration]: true,
};
Expand All @@ -20,41 +21,61 @@ export class TypeReferenceNodeParser implements SubNodeParser {
}

public createType(node: ts.TypeReferenceNode, context: Context): BaseType {
const typeSymbol = this.typeChecker.getSymbolAtLocation(node.typeName)!;
const typeSymbol =
this.typeChecker.getSymbolAtLocation(node.typeName) ??
// When the node doesn't have a valid source file, its position is -1, so we can't
// search for a symbol based on its location. In that case, the ts.factory defines a symbol
// property on the node itself.
(node.typeName as unknown as ts.Type).symbol;

// Wraps promise type to avoid resolving to a empty Object type.
if (typeSymbol.name === "Promise") {
return this.childNodeParser.createType(node.typeArguments![0]!, this.createSubContext(node, context));
arthurfiorette marked this conversation as resolved.
Show resolved Hide resolved
}

if (typeSymbol.flags & ts.SymbolFlags.Alias) {
const aliasedSymbol = this.typeChecker.getAliasedSymbol(typeSymbol);

return this.childNodeParser.createType(
aliasedSymbol.declarations!.filter((n: ts.Declaration) => !invalidTypes[n.kind])[0],
aliasedSymbol.declarations!.filter((n: ts.Declaration) => !invalidTypes[n.kind])[0]!,
arthurfiorette marked this conversation as resolved.
Show resolved Hide resolved

this.createSubContext(node, context)
);
} else if (typeSymbol.flags & ts.SymbolFlags.TypeParameter) {
}

if (typeSymbol.flags & ts.SymbolFlags.TypeParameter) {
return context.getArgument(typeSymbol.name);
} else if (typeSymbol.name === "Array" || typeSymbol.name === "ReadonlyArray") {
}

if (typeSymbol.name === "Array" || typeSymbol.name === "ReadonlyArray") {
const type = this.createSubContext(node, context).getArguments()[0];
if (type === undefined || type instanceof NeverType) {
return new NeverType();
}
return new ArrayType(type);
} else if (typeSymbol.name === "Date") {

return type === undefined ? new AnyType() : new ArrayType(type);
}

if (typeSymbol.name === "Date") {
return new AnnotatedType(new StringType(), { format: "date-time" }, false);
} else if (typeSymbol.name === "RegExp") {
}

if (typeSymbol.name === "RegExp") {
return new AnnotatedType(new StringType(), { format: "regex" }, false);
} else {
return this.childNodeParser.createType(
typeSymbol.declarations!.filter((n: ts.Declaration) => !invalidTypes[n.kind])[0],
this.createSubContext(node, context)
);
}

return this.childNodeParser.createType(
typeSymbol.declarations!.filter((n: ts.Declaration) => !invalidTypes[n.kind])[0]!,
this.createSubContext(node, context)
);
}

protected createSubContext(node: ts.TypeReferenceNode, parentContext: Context): Context {
const subContext = new Context(node);

if (node.typeArguments?.length) {
for (const typeArg of node.typeArguments) {
const type = this.childNodeParser.createType(typeArg, parentContext);
subContext.pushArgument(type);
subContext.pushArgument(this.childNodeParser.createType(typeArg, parentContext));
}
}

return subContext;
}
}
6 changes: 5 additions & 1 deletion src/TypeFormatter/DefinitionTypeFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@ export class DefinitionTypeFormatter implements SubTypeFormatter {
public supportsType(type: DefinitionType): boolean {
return type instanceof DefinitionType;
}

public getDefinition(type: DefinitionType): Definition {
const ref = type.getName();
return { $ref: `#/definitions/${this.encodeRefs ? encodeURIComponent(ref) : ref}` };
return {
$ref: `#/references/${this.encodeRefs ? encodeURIComponent(ref) : ref}`,
};
}

public getChildren(type: DefinitionType): BaseType[] {
return uniqueArray([type, ...this.childTypeFormatter.getChildren(type.getType())]);
}
Expand Down
4 changes: 3 additions & 1 deletion src/TypeFormatter/ReferenceTypeFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ export class ReferenceTypeFormatter implements SubTypeFormatter {
}
public getDefinition(type: ReferenceType): Definition {
const ref = type.getName();
return { $ref: `#/definitions/${this.encodeRefs ? encodeURIComponent(ref) : ref}` };
return {
$ref: `#/definitions/${this.encodeRefs ? encodeURIComponent(ref) : ref}`,
};
}
public getChildren(type: ReferenceType): BaseType[] {
const referredType = type.getType();
Expand Down
12 changes: 8 additions & 4 deletions src/Utils/nodeKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,19 @@ export function hash(a: unknown): string | number {

export function getKey(node: Node, context: Context): string {
const ids: (number | string)[] = [];

while (node) {
const file = node
.getSourceFile()
.fileName.substr(process.cwd().length + 1)
.replace(/\//g, "_");
const source = node.getSourceFile();
const file = !source
? // TODO: Use better filename for unknown files (See #1386)
"unresolved"
: source.fileName.substring(process.cwd().length + 1).replace(/\//g, "_");

ids.push(hash(file), node.pos, node.end);

node = node.parent;
}

const id = ids.join("-");

const argumentIds = context.getArguments().map((arg) => arg?.getId());
Expand Down
2 changes: 1 addition & 1 deletion test/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import addFormats from "ajv-formats";
import { readFileSync } from "fs";
import { resolve } from "path";
import ts from "typescript";
import { BaseType, Context, DefinitionType, ReferenceType, SubNodeParser } from "../index";
import { createFormatter, FormatterAugmentor } from "../factory/formatter";
import { createParser, ParserAugmentor } from "../factory/parser";
import { createProgram } from "../factory/program";
import { BaseType, Context, DefinitionType, ReferenceType, SubNodeParser } from "../index";
import { Config, DEFAULT_CONFIG } from "../src/Config";
import { Definition } from "../src/Schema/Definition";
import { SchemaGenerator } from "../src/SchemaGenerator";
Expand Down
53 changes: 53 additions & 0 deletions test/sourceless-nodes/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import path from "path";
import ts from "typescript";
import { createParser } from "../../factory";
import { Context } from "../../src/NodeParser";
import { LiteralType } from "../../src/Type/LiteralType";
import { NumberType } from "../../src/Type/NumberType";
import { ObjectType } from "../../src/Type/ObjectType";

const SOURCE = path.resolve(__dirname, "./source.ts");

describe("sourceless-nodes", () => {
it("tests creating json schemas with ts.Nodes without valid source files", () => {
const program = ts.createProgram([SOURCE], {});
const parser = createParser(program, {});

// Finds the typescript function declaration node
arthurfiorette marked this conversation as resolved.
Show resolved Hide resolved
const source = program.getSourceFile(SOURCE);
const fn = source!.statements[0] as ts.FunctionDeclaration;

// Creates a sourceless type by inferring the function return type.
const inferredReturnType = getReturnType(fn, program.getTypeChecker());

// Checks that the inferred return type does not have any real source file.
expect(inferredReturnType.getSourceFile()).toBeUndefined();

// Generates the json schema of this inferred return type
const baseType = parser.createType(inferredReturnType, new Context(inferredReturnType));

const objectType = (baseType as any).type as ObjectType;
expect(objectType).toBeDefined();
expect(objectType).toBeInstanceOf(ObjectType);

const [propA, propB] = objectType.getProperties();

expect(propA.getName()).toBe("a");
expect(propA.getType()).toBeInstanceOf(NumberType);

expect(propB.getName()).toBe("b");
expect(propB.getType()).toBeInstanceOf(LiteralType);
});
});

// github.com/arthurfiorette/kita/blob/main/packages/generator/src/util/type-resolver.ts
arthurfiorette marked this conversation as resolved.
Show resolved Hide resolved
function getReturnType(node: ts.SignatureDeclaration, typeChecker: ts.TypeChecker) {
if (node.type) {
return node.type;
}

const signature = typeChecker.getSignatureFromDeclaration(node);
const implicitType = typeChecker.getReturnTypeOfSignature(signature!);

return typeChecker.typeToTypeNode(implicitType, undefined, ts.NodeBuilderFlags.NoTruncation) as ts.TypeNode;
}
4 changes: 4 additions & 0 deletions test/sourceless-nodes/source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Async function to test promise unwrapping.
export async function withInferredReturnType() {
return { a: 1, b: `constant` as const };
}