Skip to content

Commit

Permalink
fix: TypeError on const spread (#2039)
Browse files Browse the repository at this point in the history
  • Loading branch information
arthurfiorette authored Aug 13, 2024
1 parent 0f88a9c commit 7ad481b
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 16 deletions.
2 changes: 1 addition & 1 deletion factory/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
1 change: 1 addition & 0 deletions src/Error/Errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export class DefinitionError extends BaseError {
});
}
}

export class BuildError extends BaseError {
constructor(diag: Omit<PartialDiagnostic, "code">) {
super({
Expand Down
79 changes: 64 additions & 15 deletions src/NodeParser/ObjectLiteralExpressionNodeParser.ts
Original file line number Diff line number Diff line change
@@ -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,
);
});
}
}
1 change: 1 addition & 0 deletions test/valid-data-type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", "*"));

Expand Down
13 changes: 13 additions & 0 deletions test/valid-data/const-spread/main.ts
Original file line number Diff line number Diff line change
@@ -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];
50 changes: 50 additions & 0 deletions test/valid-data/const-spread/schema.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}

0 comments on commit 7ad481b

Please sign in to comment.