From 339fe9e7d4047f821c59a147b6471eae1e957649 Mon Sep 17 00:00:00 2001 From: Arthur Fiorette Date: Tue, 13 Aug 2024 11:14:25 -0300 Subject: [PATCH] code --- factory/parser.ts | 2 +- src/Error/Errors.ts | 1 + .../ObjectLiteralExpressionNodeParser.ts | 79 +++++++++++++++---- test/valid-data-type.test.ts | 1 + test/valid-data/const-spread/main.ts | 13 +++ test/valid-data/const-spread/schema.json | 50 ++++++++++++ 6 files changed, 130 insertions(+), 16 deletions(-) create mode 100644 test/valid-data/const-spread/main.ts create mode 100644 test/valid-data/const-spread/schema.json diff --git a/factory/parser.ts b/factory/parser.ts index 30611baf4..6b3ca867a 100644 --- a/factory/parser.ts +++ b/factory/parser.ts @@ -114,7 +114,7 @@ export function createParser(program: ts.Program, config: CompletedConfig, augme .addNodeParser(new NumberLiteralNodeParser()) .addNodeParser(new BooleanLiteralNodeParser()) .addNodeParser(new NullLiteralNodeParser()) - .addNodeParser(new ObjectLiteralExpressionNodeParser(chainNodeParser)) + .addNodeParser(new ObjectLiteralExpressionNodeParser(chainNodeParser, typeChecker)) .addNodeParser(new ArrayLiteralExpressionNodeParser(chainNodeParser)) .addNodeParser(new PrefixUnaryExpressionNodeParser(chainNodeParser)) diff --git a/src/Error/Errors.ts b/src/Error/Errors.ts index 2d0413082..2ef3b78cd 100644 --- a/src/Error/Errors.ts +++ b/src/Error/Errors.ts @@ -93,6 +93,7 @@ export class DefinitionError extends BaseError { }); } } + export class BuildError extends BaseError { constructor(diag: Omit) { super({ diff --git a/src/NodeParser/ObjectLiteralExpressionNodeParser.ts b/src/NodeParser/ObjectLiteralExpressionNodeParser.ts index 30dc25889..9e9dbed8f 100644 --- a/src/NodeParser/ObjectLiteralExpressionNodeParser.ts +++ b/src/NodeParser/ObjectLiteralExpressionNodeParser.ts @@ -1,28 +1,77 @@ -import { NodeParser } from "../NodeParser.js"; +import type { NodeParser } from "../NodeParser.js"; import ts from "typescript"; -import { Context } from "../NodeParser.js"; -import { SubNodeParser } from "../SubNodeParser.js"; -import { BaseType } from "../Type/BaseType.js"; +import type { Context } from "../NodeParser.js"; +import type { SubNodeParser } from "../SubNodeParser.js"; +import type { BaseType } from "../Type/BaseType.js"; import { getKey } from "../Utils/nodeKey.js"; import { ObjectProperty, ObjectType } from "../Type/ObjectType.js"; +import { ExpectationFailedError, UnknownNodeError } from "../Error/Errors.js"; +import { IntersectionType } from "../Type/IntersectionType.js"; export class ObjectLiteralExpressionNodeParser implements SubNodeParser { - public constructor(protected childNodeParser: NodeParser) {} + public constructor( + protected childNodeParser: NodeParser, + protected checker: ts.TypeChecker, + ) {} public supportsNode(node: ts.ObjectLiteralExpression): boolean { return node.kind === ts.SyntaxKind.ObjectLiteralExpression; } public createType(node: ts.ObjectLiteralExpression, context: Context): BaseType { - const properties = node.properties.map( - (t) => - new ObjectProperty( - t.name!.getText(), - this.childNodeParser.createType((t as any).initializer, context), - !(t as any).questionToken, - ), - ); - - return new ObjectType(`object-${getKey(node, context)}`, [], properties, false); + const spreadAssignments: ts.SpreadAssignment[] = []; + const properties: ts.ObjectLiteralElementLike[] = []; + + for (const prop of node.properties) { + if (ts.isSpreadAssignment(prop)) { + spreadAssignments.push(prop); + } else { + properties.push(prop); + } + } + + const parsedProperties = this.parseProperties(properties, context); + const object = new ObjectType(`object-${getKey(node, context)}`, [], parsedProperties, false); + + if (!spreadAssignments.length) { + return object; + } + + const types: BaseType[] = [object]; + + for (const spread of spreadAssignments) { + const referenced = this.checker.typeToTypeNode( + this.checker.getTypeAtLocation(spread.expression), + undefined, + ts.NodeBuilderFlags.NoTypeReduction, + ); + + if (!referenced) { + throw new ExpectationFailedError("Could not find reference for spread type", spread); + } + + types.push(this.childNodeParser.createType(referenced, context)); + } + + return new IntersectionType(types); + } + + private parseProperties(properties: ts.ObjectLiteralElementLike[], context: Context): ObjectProperty[] { + return properties.flatMap((t) => { + // parsed previously + if (ts.isSpreadAssignment(t)) { + return []; + } + + if (!t.name || !("initializer" in t)) { + throw new UnknownNodeError(t); + } + + return new ObjectProperty( + t.name.getText(), + this.childNodeParser.createType(t.initializer, context), + !(t as any).questionToken, + ); + }); } } diff --git a/test/valid-data-type.test.ts b/test/valid-data-type.test.ts index 39e626b47..18ae20d21 100644 --- a/test/valid-data-type.test.ts +++ b/test/valid-data-type.test.ts @@ -143,6 +143,7 @@ describe("valid-data-type", () => { it("ignore-export", assertValidSchema("ignore-export", "*")); it("lowercase", assertValidSchema("lowercase", "MyType")); + it("const-spread", assertValidSchema("const-spread", "MyType")); it("promise-extensions", assertValidSchema("promise-extensions", "*")); diff --git a/test/valid-data/const-spread/main.ts b/test/valid-data/const-spread/main.ts new file mode 100644 index 000000000..3c93d3ee1 --- /dev/null +++ b/test/valid-data/const-spread/main.ts @@ -0,0 +1,13 @@ +export const a = { + a: "A", +} as const; + +export const b = { + ...a, + b: "B", +} as const; + +export type A = typeof a; +export type B = typeof b; + +export type MyType = [A, B]; diff --git a/test/valid-data/const-spread/schema.json b/test/valid-data/const-spread/schema.json new file mode 100644 index 000000000..4e73a09db --- /dev/null +++ b/test/valid-data/const-spread/schema.json @@ -0,0 +1,50 @@ +{ + "$ref": "#/definitions/MyType", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "A": { + "additionalProperties": false, + "properties": { + "a": { + "const": "A", + "type": "string" + } + }, + "required": [ + "a" + ], + "type": "object" + }, + "B": { + "additionalProperties": false, + "properties": { + "a": { + "const": "A", + "type": "string" + }, + "b": { + "const": "B", + "type": "string" + } + }, + "required": [ + "a", + "b" + ], + "type": "object" + }, + "MyType": { + "items": [ + { + "$ref": "#/definitions/A" + }, + { + "$ref": "#/definitions/B" + } + ], + "maxItems": 2, + "minItems": 2, + "type": "array" + } + } +}