Skip to content

Commit f74dc48

Browse files
authored
feat: add support for BinaryExpression node (#2411)
1 parent 41b841c commit f74dc48

File tree

6 files changed

+209
-0
lines changed

6 files changed

+209
-0
lines changed

factory/parser.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { AnyTypeNodeParser } from "../src/NodeParser/AnyTypeNodeParser.js";
1212
import { ArrayLiteralExpressionNodeParser } from "../src/NodeParser/ArrayLiteralExpressionNodeParser.js";
1313
import { ArrayNodeParser } from "../src/NodeParser/ArrayNodeParser.js";
1414
import { AsExpressionNodeParser } from "../src/NodeParser/AsExpressionNodeParser.js";
15+
import { BinaryExpressionNodeParser } from "../src/NodeParser/BinaryExpressionNodeParser.js";
1516
import { BooleanLiteralNodeParser } from "../src/NodeParser/BooleanLiteralNodeParser.js";
1617
import { BooleanTypeNodeParser } from "../src/NodeParser/BooleanTypeNodeParser.js";
1718
import { CallExpressionParser } from "../src/NodeParser/CallExpressionParser.js";
@@ -118,6 +119,7 @@ export function createParser(program: ts.Program, config: CompletedConfig, augme
118119
.addNodeParser(new NeverTypeNodeParser())
119120
.addNodeParser(new ObjectTypeNodeParser())
120121
.addNodeParser(new AsExpressionNodeParser(chainNodeParser))
122+
.addNodeParser(new BinaryExpressionNodeParser(chainNodeParser))
121123
.addNodeParser(new SatisfiesNodeParser(chainNodeParser))
122124
.addNodeParser(withJsDoc(new ParameterParser(chainNodeParser)))
123125
.addNodeParser(new StringLiteralNodeParser())

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"release": "npm run build && auto shipit",
5151
"run": "tsx ts-json-schema-generator.ts",
5252
"test": "jest test/ --verbose",
53+
"test:debug": "node --inspect-brk node_modules/.bin/jest test/ --verbose --runInBand",
5354
"test:coverage": "npm run jest -- test/ --collectCoverage=true",
5455
"test:fast": "cross-env FAST_TEST=1 jest test/ --verbose",
5556
"test:update": "cross-env UPDATE_SCHEMA=true npm run test:fast",
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import ts from "typescript";
2+
import type { Context, NodeParser } from "../NodeParser.js";
3+
import type { SubNodeParser } from "../SubNodeParser.js";
4+
import { AnyType } from "../Type/AnyType.js";
5+
import type { BaseType } from "../Type/BaseType.js";
6+
import { BooleanType } from "../Type/BooleanType.js";
7+
import { LiteralType } from "../Type/LiteralType.js";
8+
import { NumberType } from "../Type/NumberType.js";
9+
import { StringType } from "../Type/StringType.js";
10+
import { UnionType } from "../Type/UnionType.js";
11+
import { AliasType } from "../Type/AliasType.js";
12+
13+
export class BinaryExpressionNodeParser implements SubNodeParser {
14+
public constructor(protected childNodeParser: NodeParser) {}
15+
16+
public supportsNode(node: ts.Node): boolean {
17+
return node.kind === ts.SyntaxKind.BinaryExpression;
18+
}
19+
20+
public createType(node: ts.BinaryExpression, context: Context): BaseType {
21+
const leftType = this.childNodeParser.createType(node.left, context);
22+
const rightType = this.childNodeParser.createType(node.right, context);
23+
24+
if (leftType instanceof AnyType || rightType instanceof AnyType) {
25+
return new AnyType();
26+
}
27+
28+
if (this.isStringLike(leftType) || this.isStringLike(rightType)) {
29+
return new StringType();
30+
}
31+
32+
if (this.isDefinitelyNumberLike(leftType) && this.isDefinitelyNumberLike(rightType)) {
33+
return new NumberType();
34+
}
35+
36+
if (this.isBooleanLike(leftType) && this.isBooleanLike(rightType)) {
37+
return new BooleanType();
38+
}
39+
40+
// Anything else (objects, any, unknown, weird unions, etc.) return
41+
// 'string' because at runtime + will usually go through ToPrimitive and
42+
// end up in the "string concatenation" branch when non-numeric stuff is
43+
// involved.
44+
return new StringType();
45+
}
46+
47+
private isStringLike(type: BaseType): boolean {
48+
if (type instanceof AliasType) {
49+
return this.isStringLike(type.getType());
50+
}
51+
52+
if (type instanceof StringType) {
53+
return true;
54+
}
55+
56+
if (type instanceof LiteralType && type.isString()) {
57+
return true;
58+
}
59+
60+
// Any union member being string-like is enough.
61+
if (type instanceof UnionType) {
62+
return type.getTypes().some((t) => this.isStringLike(t));
63+
}
64+
65+
return false;
66+
}
67+
68+
private isBooleanLike(type: BaseType): boolean {
69+
if (type instanceof BooleanType) {
70+
return true;
71+
}
72+
73+
if (type instanceof LiteralType && typeof type.getValue() === "boolean") {
74+
return true;
75+
}
76+
77+
return false;
78+
}
79+
80+
private isDefinitelyNumberLike(type: BaseType): boolean {
81+
if (type instanceof AliasType) {
82+
return this.isDefinitelyNumberLike(type.getType());
83+
}
84+
85+
if (type instanceof NumberType) {
86+
return true;
87+
}
88+
89+
if (type instanceof LiteralType && typeof type.getValue() === "number") {
90+
return true;
91+
}
92+
93+
if (type instanceof UnionType) {
94+
return type.getTypes().every((t) => this.isDefinitelyNumberLike(t));
95+
}
96+
97+
return false;
98+
}
99+
}

test/valid-data-type.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ describe("valid-data-type", () => {
99
it("type-aliases-object", assertValidSchema("type-aliases-object", "MyAlias"));
1010
it("type-aliases-mixed", assertValidSchema("type-aliases-mixed", "MyObject"));
1111
it("type-aliases-union", assertValidSchema("type-aliases-union", "MyUnion"));
12+
it("binary-expression", assertValidSchema("binary-expression", "MyObject"));
1213
it("type-aliases-anonymous", assertValidSchema("type-aliases-anonymous", "MyObject"));
1314
it("type-aliases-local-namespace", assertValidSchema("type-aliases-local-namespace", "MyObject"));
1415
it("type-aliases-recursive-anonymous", assertValidSchema("type-aliases-recursive-anonymous", "MyAlias"));
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
type StringUnion = "a" | "b";
2+
type NumberUnion = 10 | 20;
3+
type MixedUnion = "c" | 30;
4+
5+
function getAny(): any {
6+
return "test" as any;
7+
}
8+
9+
function getBoolean(): boolean {
10+
return Math.random() > 0.5;
11+
}
12+
13+
function getStringUnion(): StringUnion {
14+
return Math.random() > 0.5 ? "a" : "b";
15+
}
16+
17+
function getNumberUnion(): NumberUnion {
18+
return Math.random() > 0.5 ? 10 : 20;
19+
}
20+
21+
function getMixedUnion(): MixedUnion {
22+
return Math.random() > 0.5 ? "c" : 30;
23+
}
24+
25+
function getUnknown(): unknown {
26+
return "unknown value";
27+
}
28+
29+
function getStringType(): string {
30+
return Math.random() > 0.5 ? "hello" : "world";
31+
}
32+
33+
const anyString: any = getAny();
34+
35+
const aStringUnion: StringUnion = getStringUnion();
36+
const bStringUnion: StringUnion = getStringUnion();
37+
38+
const tenNumberUnion: NumberUnion = getNumberUnion();
39+
const twentyNumberUnion: NumberUnion = getNumberUnion();
40+
41+
const thirtyMixedUnion: MixedUnion = getMixedUnion();
42+
43+
const a: boolean = getBoolean();
44+
const b: boolean = getBoolean();
45+
46+
const unknownValue: unknown = getUnknown();
47+
48+
const foo = {
49+
numbers: 60 * 5,
50+
stringLiterals: "a" + "b",
51+
stringTypes: getStringType() + getStringType(),
52+
booleanTypes: a || b,
53+
booleanLiterals: true || false,
54+
any: 1 + anyString,
55+
threeNumbers: 60 * 5 + 1,
56+
mixedStringAndNumbers: 60 * 5 + " minutes",
57+
bigintType: BigInt(123),
58+
59+
unknowns: unknownValue && unknownValue,
60+
61+
stringUnion: aStringUnion + bStringUnion,
62+
numberUnion: tenNumberUnion + twentyNumberUnion,
63+
mixedUnion: thirtyMixedUnion + " is a number",
64+
} as const;
65+
66+
export type MyObject = typeof foo;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"$ref": "#/definitions/MyObject",
3+
"$schema": "http://json-schema.org/draft-07/schema#",
4+
"definitions": {
5+
"MyObject": {
6+
"additionalProperties": false,
7+
"properties": {
8+
"numbers": { "type": "number" },
9+
"threeNumbers": { "type": "number" },
10+
"stringLiterals": { "type": "string" },
11+
"stringTypes": { "type": "string" },
12+
"mixedStringAndNumbers": { "type": "string" },
13+
"booleanTypes": { "type": "boolean" },
14+
"booleanLiterals": { "type": "boolean" },
15+
"bigintType": { "type": "number" },
16+
"numberUnion": { "type": "number" },
17+
"stringUnion": { "type": "string" },
18+
"unknowns": { "type": "string" },
19+
"mixedUnion": { "type": "string" },
20+
"any": {}
21+
},
22+
"required": [
23+
"numbers",
24+
"stringLiterals",
25+
"stringTypes",
26+
"booleanTypes",
27+
"booleanLiterals",
28+
"any",
29+
"threeNumbers",
30+
"mixedStringAndNumbers",
31+
"bigintType",
32+
"unknowns",
33+
"stringUnion",
34+
"numberUnion",
35+
"mixedUnion"
36+
],
37+
"type": "object"
38+
}
39+
}
40+
}

0 commit comments

Comments
 (0)