Skip to content

Commit cb99f6e

Browse files
authored
feat(parser): support SpreadElement in array literals (#2269)
* add IdentifierNodeParser to resolve const-bound identifiers * add SpreadElementNodeParser to translate `...expr` to RestType * register both parsers in factory chain * add tests: array-literal-spread, array-rest-only
1 parent 55dfdf5 commit cb99f6e

File tree

8 files changed

+112
-0
lines changed

8 files changed

+112
-0
lines changed

factory/parser.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ import type { SubNodeParser } from "../src/SubNodeParser.js";
5959
import { TopRefNodeParser } from "../src/TopRefNodeParser.js";
6060
import { SatisfiesNodeParser } from "../src/NodeParser/SatisfiesNodeParser.js";
6161
import { PromiseNodeParser } from "../src/NodeParser/PromiseNodeParser.js";
62+
import { SpreadElementNodeParser } from "../src/NodeParser/SpreadElementNodeParser.js";
63+
import { IdentifierNodeParser } from "../src/NodeParser/IdentifierNodeParser.js";
6264

6365
export type ParserAugmentor = (parser: MutableParser) => void;
6466

@@ -138,6 +140,8 @@ export function createParser(program: ts.Program, config: CompletedConfig, augme
138140
.addNodeParser(new NamedTupleMemberNodeParser(chainNodeParser))
139141
.addNodeParser(new OptionalTypeNodeParser(chainNodeParser))
140142
.addNodeParser(new RestTypeNodeParser(chainNodeParser))
143+
.addNodeParser(new IdentifierNodeParser(chainNodeParser, typeChecker))
144+
.addNodeParser(new SpreadElementNodeParser(chainNodeParser))
141145

142146
.addNodeParser(new CallExpressionParser(typeChecker, chainNodeParser))
143147
.addNodeParser(new PropertyAccessExpressionParser(typeChecker, chainNodeParser))
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import ts from "typescript";
2+
import type { Context, NodeParser } from "../NodeParser.js";
3+
import type { SubNodeParser } from "../SubNodeParser.js";
4+
import type { BaseType } from "../Type/BaseType.js";
5+
import { UnknownNodeError } from "../Error/Errors.js";
6+
7+
/**
8+
* Resolves identifiers whose value is a compile-time constant
9+
*/
10+
export class IdentifierNodeParser implements SubNodeParser {
11+
constructor(
12+
private readonly childNodeParser: NodeParser,
13+
private readonly checker: ts.TypeChecker,
14+
) {}
15+
16+
supportsNode(node: ts.Identifier): boolean {
17+
return node.kind === ts.SyntaxKind.Identifier;
18+
}
19+
20+
createType(node: ts.Identifier, context: Context): BaseType {
21+
const symbol = this.checker.getSymbolAtLocation(node);
22+
if (!symbol) {
23+
throw new UnknownNodeError(node);
24+
}
25+
26+
const decl = symbol.valueDeclaration;
27+
if (
28+
decl &&
29+
ts.isVariableDeclaration(decl) &&
30+
decl.initializer &&
31+
ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const
32+
) {
33+
return this.childNodeParser.createType(decl.initializer, context);
34+
}
35+
36+
throw new UnknownNodeError(node);
37+
}
38+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import ts from "typescript";
2+
import type { Context, NodeParser } from "../NodeParser.js";
3+
import type { SubNodeParser } from "../SubNodeParser.js";
4+
import type { ArrayType } from "../Type/ArrayType.js";
5+
import type { InferType } from "../Type/InferType.js";
6+
import type { TupleType } from "../Type/TupleType.js";
7+
import { RestType } from "../Type/RestType.js";
8+
9+
/**
10+
* Handles `...expr` inside an ArrayLiteralExpression.
11+
* Turns it into RestType so TupleTypeFormatter can emit correct JSON-Schema.
12+
*/
13+
export class SpreadElementNodeParser implements SubNodeParser {
14+
constructor(private readonly childNodeParser: NodeParser) {}
15+
16+
supportsNode(node: ts.SpreadElement): boolean {
17+
return node.kind === ts.SyntaxKind.SpreadElement;
18+
}
19+
20+
createType(node: ts.SpreadElement, context: Context) {
21+
const inner = this.childNodeParser.createType(node.expression, context) as ArrayType | InferType | TupleType;
22+
23+
return new RestType(inner);
24+
}
25+
}

test/valid-data-other.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ describe("valid-data-other", () => {
8383
it("array-min-max-items-optional", assertValidSchema("array-min-max-items-optional", "MyType"));
8484
it("array-function-generics", assertValidSchema("array-function-generics", "*"));
8585
it("array-max-items-optional", assertValidSchema("array-max-items-optional", "MyType"));
86+
it("array-literal-spread", assertValidSchema("array-literal-spread", "MyType"));
87+
it("array-rest-only", assertValidSchema("array-rest-only", "MyType"));
8688
it("shorthand-array", assertValidSchema("shorthand-array", "MyType"));
8789
it("function-generic", assertValidSchema("function-generic", "MyType"));
8890

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
const BASE = ["foo", "bar"] as const;
2+
3+
export const ALL = [...BASE, "baz"] as const;
4+
export type MyType = typeof ALL;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"$ref": "#/definitions/MyType",
3+
"$schema": "http://json-schema.org/draft-07/schema#",
4+
"definitions": {
5+
"MyType": {
6+
"items": [
7+
{
8+
"const": "foo",
9+
"type": "string"
10+
},
11+
{
12+
"const": "bar",
13+
"type": "string"
14+
},
15+
{
16+
"const": "baz",
17+
"type": "string"
18+
}
19+
],
20+
"maxItems": 3,
21+
"minItems": 3,
22+
"type": "array"
23+
}
24+
}
25+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type MyType = [...string[]];
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"$ref": "#/definitions/MyType",
3+
"$schema": "http://json-schema.org/draft-07/schema#",
4+
"definitions": {
5+
"MyType": {
6+
"items": {
7+
"type": "string"
8+
},
9+
"minItems": 0,
10+
"type": "array"
11+
}
12+
}
13+
}

0 commit comments

Comments
 (0)