@formspec/build 0.1.0-alpha.11 → 0.1.0-alpha.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -15
- package/dist/__tests__/chain-dsl-canonicalizer.test.d.ts +2 -0
- package/dist/__tests__/chain-dsl-canonicalizer.test.d.ts.map +1 -0
- package/dist/__tests__/constraint-validator.test.d.ts +2 -0
- package/dist/__tests__/constraint-validator.test.d.ts.map +1 -0
- package/dist/__tests__/extension-api.test.d.ts +2 -0
- package/dist/__tests__/extension-api.test.d.ts.map +1 -0
- package/dist/__tests__/fixtures/example-a-builtins.d.ts +18 -0
- package/dist/__tests__/fixtures/example-a-builtins.d.ts.map +1 -1
- package/dist/__tests__/ir-analyzer.test.d.ts +11 -0
- package/dist/__tests__/ir-analyzer.test.d.ts.map +1 -0
- package/dist/__tests__/ir-jsdoc-constraints.test.d.ts +12 -0
- package/dist/__tests__/ir-jsdoc-constraints.test.d.ts.map +1 -0
- package/dist/__tests__/ir-json-schema-generator.test.d.ts +11 -0
- package/dist/__tests__/ir-json-schema-generator.test.d.ts.map +1 -0
- package/dist/__tests__/ir-ui-schema-generator.test.d.ts +2 -0
- package/dist/__tests__/ir-ui-schema-generator.test.d.ts.map +1 -0
- package/dist/__tests__/jsdoc-constraints.test.d.ts +4 -4
- package/dist/__tests__/parity/fixtures/address/chain-dsl.d.ts +9 -0
- package/dist/__tests__/parity/fixtures/address/chain-dsl.d.ts.map +1 -0
- package/dist/__tests__/parity/fixtures/address/expected-ir.d.ts +9 -0
- package/dist/__tests__/parity/fixtures/address/expected-ir.d.ts.map +1 -0
- package/dist/__tests__/parity/fixtures/address/tsdoc.d.ts +19 -0
- package/dist/__tests__/parity/fixtures/address/tsdoc.d.ts.map +1 -0
- package/dist/__tests__/parity/fixtures/product-config/chain-dsl.d.ts +13 -0
- package/dist/__tests__/parity/fixtures/product-config/chain-dsl.d.ts.map +1 -0
- package/dist/__tests__/parity/fixtures/product-config/expected-ir.d.ts +9 -0
- package/dist/__tests__/parity/fixtures/product-config/expected-ir.d.ts.map +1 -0
- package/dist/__tests__/parity/fixtures/product-config/tsdoc.d.ts +28 -0
- package/dist/__tests__/parity/fixtures/product-config/tsdoc.d.ts.map +1 -0
- package/dist/__tests__/parity/fixtures/user-registration/chain-dsl.d.ts +12 -0
- package/dist/__tests__/parity/fixtures/user-registration/chain-dsl.d.ts.map +1 -0
- package/dist/__tests__/parity/fixtures/user-registration/expected-ir.d.ts +9 -0
- package/dist/__tests__/parity/fixtures/user-registration/expected-ir.d.ts.map +1 -0
- package/dist/__tests__/parity/fixtures/user-registration/tsdoc.d.ts +19 -0
- package/dist/__tests__/parity/fixtures/user-registration/tsdoc.d.ts.map +1 -0
- package/dist/__tests__/parity/parity.test.d.ts +14 -0
- package/dist/__tests__/parity/parity.test.d.ts.map +1 -0
- package/dist/__tests__/parity/utils.d.ts +139 -0
- package/dist/__tests__/parity/utils.d.ts.map +1 -0
- package/dist/analyzer/class-analyzer.d.ts +54 -99
- package/dist/analyzer/class-analyzer.d.ts.map +1 -1
- package/dist/analyzer/jsdoc-constraints.d.ts +78 -30
- package/dist/analyzer/jsdoc-constraints.d.ts.map +1 -1
- package/dist/analyzer/tsdoc-parser.d.ts +61 -0
- package/dist/analyzer/tsdoc-parser.d.ts.map +1 -0
- package/dist/browser.cjs +960 -309
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.d.ts +10 -6
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +958 -308
- package/dist/browser.js.map +1 -1
- package/dist/build.d.ts +65 -150
- package/dist/canonicalize/chain-dsl-canonicalizer.d.ts +18 -0
- package/dist/canonicalize/chain-dsl-canonicalizer.d.ts.map +1 -0
- package/dist/canonicalize/index.d.ts +8 -0
- package/dist/canonicalize/index.d.ts.map +1 -0
- package/dist/canonicalize/tsdoc-canonicalizer.d.ts +34 -0
- package/dist/canonicalize/tsdoc-canonicalizer.d.ts.map +1 -0
- package/dist/cli.cjs +1417 -1656
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1421 -1647
- package/dist/cli.js.map +1 -1
- package/dist/extensions/index.d.ts +8 -0
- package/dist/extensions/index.d.ts.map +1 -0
- package/dist/extensions/registry.d.ts +55 -0
- package/dist/extensions/registry.d.ts.map +1 -0
- package/dist/generators/class-schema.d.ts +23 -38
- package/dist/generators/class-schema.d.ts.map +1 -1
- package/dist/generators/method-schema.d.ts +6 -8
- package/dist/generators/method-schema.d.ts.map +1 -1
- package/dist/index.cjs +1353 -1614
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +6 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1365 -1610
- package/dist/index.js.map +1 -1
- package/dist/internals.cjs +1604 -824
- package/dist/internals.cjs.map +1 -1
- package/dist/internals.d.ts +12 -3
- package/dist/internals.d.ts.map +1 -1
- package/dist/internals.js +1607 -820
- package/dist/internals.js.map +1 -1
- package/dist/json-schema/generator.d.ts +10 -5
- package/dist/json-schema/generator.d.ts.map +1 -1
- package/dist/json-schema/ir-generator.d.ts +84 -0
- package/dist/json-schema/ir-generator.d.ts.map +1 -0
- package/dist/json-schema/schema.d.ts +3 -3
- package/dist/json-schema/types.d.ts +5 -6
- package/dist/json-schema/types.d.ts.map +1 -1
- package/dist/ui-schema/generator.d.ts +5 -15
- package/dist/ui-schema/generator.d.ts.map +1 -1
- package/dist/ui-schema/ir-generator.d.ts +53 -0
- package/dist/ui-schema/ir-generator.d.ts.map +1 -0
- package/dist/validate/constraint-validator.d.ts +66 -0
- package/dist/validate/constraint-validator.d.ts.map +1 -0
- package/dist/validate/index.d.ts +9 -0
- package/dist/validate/index.d.ts.map +1 -0
- package/package.json +5 -4
- package/dist/__tests__/analyzer-edge-cases.test.d.ts +0 -13
- package/dist/__tests__/analyzer-edge-cases.test.d.ts.map +0 -1
- package/dist/__tests__/analyzer.test.d.ts +0 -5
- package/dist/__tests__/analyzer.test.d.ts.map +0 -1
- package/dist/__tests__/codegen.test.d.ts +0 -5
- package/dist/__tests__/codegen.test.d.ts.map +0 -1
- package/dist/__tests__/decorator-pipeline.test.d.ts +0 -11
- package/dist/__tests__/decorator-pipeline.test.d.ts.map +0 -1
- package/dist/__tests__/fixtures/example-b-decorators.d.ts +0 -5
- package/dist/__tests__/fixtures/example-b-decorators.d.ts.map +0 -1
- package/dist/__tests__/fixtures/example-b-extended.d.ts +0 -5
- package/dist/__tests__/fixtures/example-b-extended.d.ts.map +0 -1
- package/dist/__tests__/fixtures/example-c-custom.d.ts +0 -5
- package/dist/__tests__/fixtures/example-c-custom.d.ts.map +0 -1
- package/dist/__tests__/fixtures/example-c-decorators.d.ts +0 -5
- package/dist/__tests__/fixtures/example-c-decorators.d.ts.map +0 -1
- package/dist/__tests__/fixtures/example-d-mixed-decorators.d.ts +0 -6
- package/dist/__tests__/fixtures/example-d-mixed-decorators.d.ts.map +0 -1
- package/dist/__tests__/fixtures/example-e-decorators.d.ts +0 -11
- package/dist/__tests__/fixtures/example-e-decorators.d.ts.map +0 -1
- package/dist/__tests__/fixtures/example-e-no-namespace.d.ts +0 -5
- package/dist/__tests__/fixtures/example-e-no-namespace.d.ts.map +0 -1
- package/dist/__tests__/fixtures/example-jsdoc-constraints.d.ts +0 -16
- package/dist/__tests__/fixtures/example-jsdoc-constraints.d.ts.map +0 -1
- package/dist/__tests__/fixtures/example-nested-class.d.ts +0 -45
- package/dist/__tests__/fixtures/example-nested-class.d.ts.map +0 -1
- package/dist/__tests__/interface-types.test.d.ts +0 -11
- package/dist/__tests__/interface-types.test.d.ts.map +0 -1
- package/dist/analyzer/decorator-extractor.d.ts +0 -78
- package/dist/analyzer/decorator-extractor.d.ts.map +0 -1
- package/dist/analyzer/type-converter.d.ts +0 -75
- package/dist/analyzer/type-converter.d.ts.map +0 -1
- package/dist/codegen/index.d.ts +0 -75
- package/dist/codegen/index.d.ts.map +0 -1
package/dist/internals.js
CHANGED
|
@@ -1,3 +1,350 @@
|
|
|
1
|
+
// src/canonicalize/chain-dsl-canonicalizer.ts
|
|
2
|
+
import { IR_VERSION } from "@formspec/core";
|
|
3
|
+
var CHAIN_DSL_PROVENANCE = {
|
|
4
|
+
surface: "chain-dsl",
|
|
5
|
+
file: "",
|
|
6
|
+
line: 0,
|
|
7
|
+
column: 0
|
|
8
|
+
};
|
|
9
|
+
function isGroup(el) {
|
|
10
|
+
return el._type === "group";
|
|
11
|
+
}
|
|
12
|
+
function isConditional(el) {
|
|
13
|
+
return el._type === "conditional";
|
|
14
|
+
}
|
|
15
|
+
function isField(el) {
|
|
16
|
+
return el._type === "field";
|
|
17
|
+
}
|
|
18
|
+
function canonicalizeChainDSL(form) {
|
|
19
|
+
return {
|
|
20
|
+
kind: "form-ir",
|
|
21
|
+
irVersion: IR_VERSION,
|
|
22
|
+
elements: canonicalizeElements(form.elements),
|
|
23
|
+
typeRegistry: {},
|
|
24
|
+
provenance: CHAIN_DSL_PROVENANCE
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function canonicalizeElements(elements) {
|
|
28
|
+
return elements.map(canonicalizeElement);
|
|
29
|
+
}
|
|
30
|
+
function canonicalizeElement(element) {
|
|
31
|
+
if (isField(element)) {
|
|
32
|
+
return canonicalizeField(element);
|
|
33
|
+
}
|
|
34
|
+
if (isGroup(element)) {
|
|
35
|
+
return canonicalizeGroup(element);
|
|
36
|
+
}
|
|
37
|
+
if (isConditional(element)) {
|
|
38
|
+
return canonicalizeConditional(element);
|
|
39
|
+
}
|
|
40
|
+
const _exhaustive = element;
|
|
41
|
+
throw new Error(`Unknown element type: ${JSON.stringify(_exhaustive)}`);
|
|
42
|
+
}
|
|
43
|
+
function canonicalizeField(field) {
|
|
44
|
+
switch (field._field) {
|
|
45
|
+
case "text":
|
|
46
|
+
return canonicalizeTextField(field);
|
|
47
|
+
case "number":
|
|
48
|
+
return canonicalizeNumberField(field);
|
|
49
|
+
case "boolean":
|
|
50
|
+
return canonicalizeBooleanField(field);
|
|
51
|
+
case "enum":
|
|
52
|
+
return canonicalizeStaticEnumField(field);
|
|
53
|
+
case "dynamic_enum":
|
|
54
|
+
return canonicalizeDynamicEnumField(field);
|
|
55
|
+
case "dynamic_schema":
|
|
56
|
+
return canonicalizeDynamicSchemaField(field);
|
|
57
|
+
case "array":
|
|
58
|
+
return canonicalizeArrayField(field);
|
|
59
|
+
case "object":
|
|
60
|
+
return canonicalizeObjectField(field);
|
|
61
|
+
default: {
|
|
62
|
+
const _exhaustive = field;
|
|
63
|
+
throw new Error(`Unknown field type: ${JSON.stringify(_exhaustive)}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function canonicalizeTextField(field) {
|
|
68
|
+
const type = { kind: "primitive", primitiveKind: "string" };
|
|
69
|
+
return buildFieldNode(
|
|
70
|
+
field.name,
|
|
71
|
+
type,
|
|
72
|
+
field.required,
|
|
73
|
+
buildAnnotations(field.label, field.placeholder)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
function canonicalizeNumberField(field) {
|
|
77
|
+
const type = { kind: "primitive", primitiveKind: "number" };
|
|
78
|
+
const constraints = [];
|
|
79
|
+
if (field.min !== void 0) {
|
|
80
|
+
const c = {
|
|
81
|
+
kind: "constraint",
|
|
82
|
+
constraintKind: "minimum",
|
|
83
|
+
value: field.min,
|
|
84
|
+
provenance: CHAIN_DSL_PROVENANCE
|
|
85
|
+
};
|
|
86
|
+
constraints.push(c);
|
|
87
|
+
}
|
|
88
|
+
if (field.max !== void 0) {
|
|
89
|
+
const c = {
|
|
90
|
+
kind: "constraint",
|
|
91
|
+
constraintKind: "maximum",
|
|
92
|
+
value: field.max,
|
|
93
|
+
provenance: CHAIN_DSL_PROVENANCE
|
|
94
|
+
};
|
|
95
|
+
constraints.push(c);
|
|
96
|
+
}
|
|
97
|
+
return buildFieldNode(
|
|
98
|
+
field.name,
|
|
99
|
+
type,
|
|
100
|
+
field.required,
|
|
101
|
+
buildAnnotations(field.label),
|
|
102
|
+
constraints
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
function canonicalizeBooleanField(field) {
|
|
106
|
+
const type = { kind: "primitive", primitiveKind: "boolean" };
|
|
107
|
+
return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
|
|
108
|
+
}
|
|
109
|
+
function canonicalizeStaticEnumField(field) {
|
|
110
|
+
const members = field.options.map((opt) => {
|
|
111
|
+
if (typeof opt === "string") {
|
|
112
|
+
return { value: opt };
|
|
113
|
+
}
|
|
114
|
+
return { value: opt.id, displayName: opt.label };
|
|
115
|
+
});
|
|
116
|
+
const type = { kind: "enum", members };
|
|
117
|
+
return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
|
|
118
|
+
}
|
|
119
|
+
function canonicalizeDynamicEnumField(field) {
|
|
120
|
+
const type = {
|
|
121
|
+
kind: "dynamic",
|
|
122
|
+
dynamicKind: "enum",
|
|
123
|
+
sourceKey: field.source,
|
|
124
|
+
parameterFields: field.params ? [...field.params] : []
|
|
125
|
+
};
|
|
126
|
+
return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
|
|
127
|
+
}
|
|
128
|
+
function canonicalizeDynamicSchemaField(field) {
|
|
129
|
+
const type = {
|
|
130
|
+
kind: "dynamic",
|
|
131
|
+
dynamicKind: "schema",
|
|
132
|
+
sourceKey: field.schemaSource,
|
|
133
|
+
parameterFields: []
|
|
134
|
+
};
|
|
135
|
+
return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
|
|
136
|
+
}
|
|
137
|
+
function canonicalizeArrayField(field) {
|
|
138
|
+
const itemProperties = buildObjectProperties(field.items);
|
|
139
|
+
const itemsType = {
|
|
140
|
+
kind: "object",
|
|
141
|
+
properties: itemProperties,
|
|
142
|
+
additionalProperties: false
|
|
143
|
+
};
|
|
144
|
+
const type = { kind: "array", items: itemsType };
|
|
145
|
+
const constraints = [];
|
|
146
|
+
if (field.minItems !== void 0) {
|
|
147
|
+
const c = {
|
|
148
|
+
kind: "constraint",
|
|
149
|
+
constraintKind: "minItems",
|
|
150
|
+
value: field.minItems,
|
|
151
|
+
provenance: CHAIN_DSL_PROVENANCE
|
|
152
|
+
};
|
|
153
|
+
constraints.push(c);
|
|
154
|
+
}
|
|
155
|
+
if (field.maxItems !== void 0) {
|
|
156
|
+
const c = {
|
|
157
|
+
kind: "constraint",
|
|
158
|
+
constraintKind: "maxItems",
|
|
159
|
+
value: field.maxItems,
|
|
160
|
+
provenance: CHAIN_DSL_PROVENANCE
|
|
161
|
+
};
|
|
162
|
+
constraints.push(c);
|
|
163
|
+
}
|
|
164
|
+
return buildFieldNode(
|
|
165
|
+
field.name,
|
|
166
|
+
type,
|
|
167
|
+
field.required,
|
|
168
|
+
buildAnnotations(field.label),
|
|
169
|
+
constraints
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
function canonicalizeObjectField(field) {
|
|
173
|
+
const properties = buildObjectProperties(field.properties);
|
|
174
|
+
const type = {
|
|
175
|
+
kind: "object",
|
|
176
|
+
properties,
|
|
177
|
+
additionalProperties: false
|
|
178
|
+
};
|
|
179
|
+
return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
|
|
180
|
+
}
|
|
181
|
+
function canonicalizeGroup(g) {
|
|
182
|
+
return {
|
|
183
|
+
kind: "group",
|
|
184
|
+
label: g.label,
|
|
185
|
+
elements: canonicalizeElements(g.elements),
|
|
186
|
+
provenance: CHAIN_DSL_PROVENANCE
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function canonicalizeConditional(c) {
|
|
190
|
+
return {
|
|
191
|
+
kind: "conditional",
|
|
192
|
+
fieldName: c.field,
|
|
193
|
+
// Conditional values from the chain DSL are JSON-serializable primitives
|
|
194
|
+
// (strings, numbers, booleans) produced by the `is()` predicate helper.
|
|
195
|
+
value: assertJsonValue(c.value),
|
|
196
|
+
elements: canonicalizeElements(c.elements),
|
|
197
|
+
provenance: CHAIN_DSL_PROVENANCE
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function assertJsonValue(v) {
|
|
201
|
+
if (v === null || typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
202
|
+
return v;
|
|
203
|
+
}
|
|
204
|
+
if (Array.isArray(v)) {
|
|
205
|
+
return v.map(assertJsonValue);
|
|
206
|
+
}
|
|
207
|
+
if (typeof v === "object") {
|
|
208
|
+
const result = {};
|
|
209
|
+
for (const [key, val] of Object.entries(v)) {
|
|
210
|
+
result[key] = assertJsonValue(val);
|
|
211
|
+
}
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
throw new TypeError(`Conditional value is not a valid JsonValue: ${typeof v}`);
|
|
215
|
+
}
|
|
216
|
+
function buildFieldNode(name, type, required, annotations, constraints = []) {
|
|
217
|
+
return {
|
|
218
|
+
kind: "field",
|
|
219
|
+
name,
|
|
220
|
+
type,
|
|
221
|
+
required: required === true,
|
|
222
|
+
constraints,
|
|
223
|
+
annotations,
|
|
224
|
+
provenance: CHAIN_DSL_PROVENANCE
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function buildAnnotations(label, placeholder) {
|
|
228
|
+
const annotations = [];
|
|
229
|
+
if (label !== void 0) {
|
|
230
|
+
const a = {
|
|
231
|
+
kind: "annotation",
|
|
232
|
+
annotationKind: "displayName",
|
|
233
|
+
value: label,
|
|
234
|
+
provenance: CHAIN_DSL_PROVENANCE
|
|
235
|
+
};
|
|
236
|
+
annotations.push(a);
|
|
237
|
+
}
|
|
238
|
+
if (placeholder !== void 0) {
|
|
239
|
+
const a = {
|
|
240
|
+
kind: "annotation",
|
|
241
|
+
annotationKind: "placeholder",
|
|
242
|
+
value: placeholder,
|
|
243
|
+
provenance: CHAIN_DSL_PROVENANCE
|
|
244
|
+
};
|
|
245
|
+
annotations.push(a);
|
|
246
|
+
}
|
|
247
|
+
return annotations;
|
|
248
|
+
}
|
|
249
|
+
function buildObjectProperties(elements, insideConditional = false) {
|
|
250
|
+
const properties = [];
|
|
251
|
+
for (const el of elements) {
|
|
252
|
+
if (isField(el)) {
|
|
253
|
+
const fieldNode = canonicalizeField(el);
|
|
254
|
+
properties.push({
|
|
255
|
+
name: fieldNode.name,
|
|
256
|
+
type: fieldNode.type,
|
|
257
|
+
// Fields inside a conditional branch are always optional in the
|
|
258
|
+
// data schema, regardless of their `required` flag — the condition
|
|
259
|
+
// may not be met, so the field may be absent.
|
|
260
|
+
optional: insideConditional || !fieldNode.required,
|
|
261
|
+
constraints: fieldNode.constraints,
|
|
262
|
+
annotations: fieldNode.annotations,
|
|
263
|
+
provenance: CHAIN_DSL_PROVENANCE
|
|
264
|
+
});
|
|
265
|
+
} else if (isGroup(el)) {
|
|
266
|
+
properties.push(...buildObjectProperties(el.elements, insideConditional));
|
|
267
|
+
} else if (isConditional(el)) {
|
|
268
|
+
properties.push(...buildObjectProperties(el.elements, true));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return properties;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/canonicalize/tsdoc-canonicalizer.ts
|
|
275
|
+
import { IR_VERSION as IR_VERSION2 } from "@formspec/core";
|
|
276
|
+
function canonicalizeTSDoc(analysis, source) {
|
|
277
|
+
const file = source?.file ?? "";
|
|
278
|
+
const provenance = {
|
|
279
|
+
surface: "tsdoc",
|
|
280
|
+
file,
|
|
281
|
+
line: 1,
|
|
282
|
+
column: 0
|
|
283
|
+
};
|
|
284
|
+
const elements = assembleElements(analysis.fields, analysis.fieldLayouts, provenance);
|
|
285
|
+
return {
|
|
286
|
+
kind: "form-ir",
|
|
287
|
+
irVersion: IR_VERSION2,
|
|
288
|
+
elements,
|
|
289
|
+
typeRegistry: analysis.typeRegistry,
|
|
290
|
+
provenance
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function assembleElements(fields, layouts, provenance) {
|
|
294
|
+
const elements = [];
|
|
295
|
+
const groupMap = /* @__PURE__ */ new Map();
|
|
296
|
+
const topLevelOrder = [];
|
|
297
|
+
for (let i = 0; i < fields.length; i++) {
|
|
298
|
+
const field = fields[i];
|
|
299
|
+
const layout = layouts[i];
|
|
300
|
+
if (!field || !layout) continue;
|
|
301
|
+
const element = wrapInConditional(field, layout, provenance);
|
|
302
|
+
if (layout.groupLabel !== void 0) {
|
|
303
|
+
const label = layout.groupLabel;
|
|
304
|
+
let groupElements = groupMap.get(label);
|
|
305
|
+
if (!groupElements) {
|
|
306
|
+
groupElements = [];
|
|
307
|
+
groupMap.set(label, groupElements);
|
|
308
|
+
topLevelOrder.push({ type: "group", label });
|
|
309
|
+
}
|
|
310
|
+
groupElements.push(element);
|
|
311
|
+
} else {
|
|
312
|
+
topLevelOrder.push({ type: "element", element });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
for (const entry of topLevelOrder) {
|
|
316
|
+
if (entry.type === "group") {
|
|
317
|
+
const groupElements = groupMap.get(entry.label);
|
|
318
|
+
if (groupElements) {
|
|
319
|
+
const groupNode = {
|
|
320
|
+
kind: "group",
|
|
321
|
+
label: entry.label,
|
|
322
|
+
elements: groupElements,
|
|
323
|
+
provenance
|
|
324
|
+
};
|
|
325
|
+
elements.push(groupNode);
|
|
326
|
+
groupMap.delete(entry.label);
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
elements.push(entry.element);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return elements;
|
|
333
|
+
}
|
|
334
|
+
function wrapInConditional(field, layout, provenance) {
|
|
335
|
+
if (layout.showWhen === void 0) {
|
|
336
|
+
return field;
|
|
337
|
+
}
|
|
338
|
+
const conditional = {
|
|
339
|
+
kind: "conditional",
|
|
340
|
+
fieldName: layout.showWhen.field,
|
|
341
|
+
value: layout.showWhen.value,
|
|
342
|
+
elements: [field],
|
|
343
|
+
provenance
|
|
344
|
+
};
|
|
345
|
+
return conditional;
|
|
346
|
+
}
|
|
347
|
+
|
|
1
348
|
// src/analyzer/program.ts
|
|
2
349
|
import * as ts from "typescript";
|
|
3
350
|
import * as path from "path";
|
|
@@ -73,290 +420,116 @@ function findTypeAliasByName(sourceFile, aliasName) {
|
|
|
73
420
|
// src/analyzer/class-analyzer.ts
|
|
74
421
|
import * as ts4 from "typescript";
|
|
75
422
|
|
|
76
|
-
// src/analyzer/
|
|
423
|
+
// src/analyzer/jsdoc-constraints.ts
|
|
424
|
+
import * as ts3 from "typescript";
|
|
425
|
+
import {
|
|
426
|
+
BUILTIN_CONSTRAINT_DEFINITIONS as BUILTIN_CONSTRAINT_DEFINITIONS2
|
|
427
|
+
} from "@formspec/core";
|
|
428
|
+
|
|
429
|
+
// src/analyzer/tsdoc-parser.ts
|
|
77
430
|
import * as ts2 from "typescript";
|
|
78
|
-
import
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
431
|
+
import {
|
|
432
|
+
TSDocParser,
|
|
433
|
+
TSDocConfiguration,
|
|
434
|
+
TSDocTagDefinition,
|
|
435
|
+
TSDocTagSyntaxKind,
|
|
436
|
+
DocPlainText,
|
|
437
|
+
DocSoftBreak,
|
|
438
|
+
TextRange
|
|
439
|
+
} from "@microsoft/tsdoc";
|
|
440
|
+
import {
|
|
441
|
+
BUILTIN_CONSTRAINT_DEFINITIONS
|
|
442
|
+
} from "@formspec/core";
|
|
443
|
+
var NUMERIC_CONSTRAINT_MAP = {
|
|
444
|
+
Minimum: "minimum",
|
|
445
|
+
Maximum: "maximum",
|
|
446
|
+
ExclusiveMinimum: "exclusiveMinimum",
|
|
447
|
+
ExclusiveMaximum: "exclusiveMaximum"
|
|
448
|
+
};
|
|
449
|
+
var LENGTH_CONSTRAINT_MAP = {
|
|
450
|
+
MinLength: "minLength",
|
|
451
|
+
MaxLength: "maxLength"
|
|
452
|
+
};
|
|
453
|
+
var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["Pattern", "EnumOptions"]);
|
|
454
|
+
function isBuiltinConstraintName(tagName) {
|
|
455
|
+
return tagName in BUILTIN_CONSTRAINT_DEFINITIONS;
|
|
90
456
|
}
|
|
91
|
-
function
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const callee = expr.expression;
|
|
102
|
-
let name = null;
|
|
103
|
-
if (ts2.isIdentifier(callee)) {
|
|
104
|
-
name = callee.text;
|
|
105
|
-
} else if (ts2.isPropertyAccessExpression(callee)) {
|
|
106
|
-
name = callee.name.text;
|
|
107
|
-
}
|
|
108
|
-
if (!name) return null;
|
|
109
|
-
const args = expr.arguments.map(extractArgValue);
|
|
110
|
-
return {
|
|
111
|
-
name,
|
|
112
|
-
args,
|
|
113
|
-
node: decorator
|
|
114
|
-
};
|
|
457
|
+
function createFormSpecTSDocConfig() {
|
|
458
|
+
const config = new TSDocConfiguration();
|
|
459
|
+
for (const tagName of Object.keys(BUILTIN_CONSTRAINT_DEFINITIONS)) {
|
|
460
|
+
config.addTagDefinition(
|
|
461
|
+
new TSDocTagDefinition({
|
|
462
|
+
tagName: "@" + tagName,
|
|
463
|
+
syntaxKind: TSDocTagSyntaxKind.BlockTag,
|
|
464
|
+
allowMultiple: true
|
|
465
|
+
})
|
|
466
|
+
);
|
|
115
467
|
}
|
|
116
|
-
return
|
|
468
|
+
return config;
|
|
117
469
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
if (ts2.isPrefixUnaryExpression(node)) {
|
|
135
|
-
if (node.operator === ts2.SyntaxKind.MinusToken && ts2.isNumericLiteral(node.operand)) {
|
|
136
|
-
return -Number(node.operand.text);
|
|
137
|
-
}
|
|
138
|
-
if (node.operator === ts2.SyntaxKind.PlusToken && ts2.isNumericLiteral(node.operand)) {
|
|
139
|
-
return Number(node.operand.text);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
if (ts2.isArrayLiteralExpression(node)) {
|
|
143
|
-
return node.elements.map((el) => {
|
|
144
|
-
if (ts2.isSpreadElement(el)) {
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
147
|
-
return extractArgValue(el);
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
if (ts2.isObjectLiteralExpression(node)) {
|
|
151
|
-
const obj = {};
|
|
152
|
-
for (const prop of node.properties) {
|
|
153
|
-
if (ts2.isPropertyAssignment(prop)) {
|
|
154
|
-
const key = getPropertyName(prop.name);
|
|
155
|
-
if (key) {
|
|
156
|
-
obj[key] = extractArgValue(prop.initializer);
|
|
157
|
-
}
|
|
158
|
-
} else if (ts2.isShorthandPropertyAssignment(prop)) {
|
|
159
|
-
const key = prop.name.text;
|
|
160
|
-
obj[key] = null;
|
|
470
|
+
var sharedParser;
|
|
471
|
+
function getParser() {
|
|
472
|
+
sharedParser ??= new TSDocParser(createFormSpecTSDocConfig());
|
|
473
|
+
return sharedParser;
|
|
474
|
+
}
|
|
475
|
+
function parseTSDocTags(node, file = "") {
|
|
476
|
+
const constraints = [];
|
|
477
|
+
const annotations = [];
|
|
478
|
+
const sourceFile = node.getSourceFile();
|
|
479
|
+
const sourceText = sourceFile.getFullText();
|
|
480
|
+
const commentRanges = ts2.getLeadingCommentRanges(sourceText, node.getFullStart());
|
|
481
|
+
if (commentRanges) {
|
|
482
|
+
for (const range of commentRanges) {
|
|
483
|
+
if (range.kind !== ts2.SyntaxKind.MultiLineCommentTrivia) {
|
|
484
|
+
continue;
|
|
161
485
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (ts2.isNoSubstitutionTemplateLiteral(node)) {
|
|
166
|
-
return node.text;
|
|
167
|
-
}
|
|
168
|
-
if (ts2.isRegularExpressionLiteral(node)) {
|
|
169
|
-
const regexText = node.text;
|
|
170
|
-
const lastSlash = regexText.lastIndexOf("/");
|
|
171
|
-
if (lastSlash > 0) {
|
|
172
|
-
return regexText.substring(1, lastSlash);
|
|
173
|
-
}
|
|
174
|
-
return regexText;
|
|
175
|
-
}
|
|
176
|
-
if (ts2.isNewExpression(node)) {
|
|
177
|
-
if (ts2.isIdentifier(node.expression) && node.expression.text === "RegExp" && node.arguments && node.arguments.length > 0) {
|
|
178
|
-
const firstArg = node.arguments[0];
|
|
179
|
-
if (firstArg && ts2.isStringLiteral(firstArg)) {
|
|
180
|
-
return firstArg.text;
|
|
486
|
+
const commentText = sourceText.substring(range.pos, range.end);
|
|
487
|
+
if (!commentText.startsWith("/**")) {
|
|
488
|
+
continue;
|
|
181
489
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (ts2.isNumericLiteral(name)) {
|
|
197
|
-
return name.text;
|
|
198
|
-
}
|
|
199
|
-
return null;
|
|
200
|
-
}
|
|
201
|
-
var FORMSPEC_DECORATORS = {
|
|
202
|
-
// Display metadata
|
|
203
|
-
Field: { argTypes: ["object"] },
|
|
204
|
-
// Grouping
|
|
205
|
-
Group: { argTypes: ["string"] },
|
|
206
|
-
// Conditional display
|
|
207
|
-
ShowWhen: { argTypes: ["object"] },
|
|
208
|
-
// Enum options
|
|
209
|
-
EnumOptions: { argTypes: ["array"] },
|
|
210
|
-
// Numeric constraints
|
|
211
|
-
Minimum: { argTypes: ["number"] },
|
|
212
|
-
Maximum: { argTypes: ["number"] },
|
|
213
|
-
ExclusiveMinimum: { argTypes: ["number"] },
|
|
214
|
-
ExclusiveMaximum: { argTypes: ["number"] },
|
|
215
|
-
// String constraints
|
|
216
|
-
MinLength: { argTypes: ["number"] },
|
|
217
|
-
MaxLength: { argTypes: ["number"] },
|
|
218
|
-
Pattern: { argTypes: ["string"] }
|
|
219
|
-
};
|
|
220
|
-
function isFormSpecDecoratorsPath(fileName) {
|
|
221
|
-
const normalized = fileName.replace(/\\/g, "/");
|
|
222
|
-
return normalized.includes("node_modules/@formspec/decorators") || normalized.includes("/packages/decorators/");
|
|
223
|
-
}
|
|
224
|
-
function resolveDecorator(decorator, checker) {
|
|
225
|
-
const expr = decorator.expression;
|
|
226
|
-
let targetNode;
|
|
227
|
-
let name;
|
|
228
|
-
if (ts2.isIdentifier(expr)) {
|
|
229
|
-
targetNode = expr;
|
|
230
|
-
name = expr.text;
|
|
231
|
-
} else if (ts2.isCallExpression(expr)) {
|
|
232
|
-
if (ts2.isIdentifier(expr.expression)) {
|
|
233
|
-
targetNode = expr.expression;
|
|
234
|
-
name = expr.expression.text;
|
|
235
|
-
} else {
|
|
236
|
-
return null;
|
|
237
|
-
}
|
|
238
|
-
} else {
|
|
239
|
-
return null;
|
|
240
|
-
}
|
|
241
|
-
if (name in FORMSPEC_DECORATORS) {
|
|
242
|
-
const symbol = checker.getSymbolAtLocation(targetNode);
|
|
243
|
-
if (symbol) {
|
|
244
|
-
const declarations = symbol.declarations;
|
|
245
|
-
if (declarations && declarations.length > 0) {
|
|
246
|
-
const decl = declarations[0];
|
|
247
|
-
if (decl) {
|
|
248
|
-
const sourceFile = decl.getSourceFile();
|
|
249
|
-
const fileName = sourceFile.fileName;
|
|
250
|
-
if (isFormSpecDecoratorsPath(fileName)) {
|
|
251
|
-
return {
|
|
252
|
-
name,
|
|
253
|
-
isFormSpec: true,
|
|
254
|
-
isMarker: !ts2.isCallExpression(expr)
|
|
255
|
-
};
|
|
256
|
-
}
|
|
490
|
+
const parser = getParser();
|
|
491
|
+
const parserContext = parser.parseRange(
|
|
492
|
+
TextRange.fromStringRange(sourceText, range.pos, range.end)
|
|
493
|
+
);
|
|
494
|
+
const docComment = parserContext.docComment;
|
|
495
|
+
for (const block of docComment.customBlocks) {
|
|
496
|
+
const tagName = block.blockTag.tagName.substring(1);
|
|
497
|
+
if (TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
|
|
498
|
+
const text = extractBlockText(block).trim();
|
|
499
|
+
if (text === "") continue;
|
|
500
|
+
const provenance = provenanceForComment(range, sourceFile, file, tagName);
|
|
501
|
+
const constraintNode = parseConstraintValue(tagName, text, provenance);
|
|
502
|
+
if (constraintNode) {
|
|
503
|
+
constraints.push(constraintNode);
|
|
257
504
|
}
|
|
258
505
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
let extendsBuiltin;
|
|
266
|
-
let extensionName;
|
|
267
|
-
let isMarker = false;
|
|
268
|
-
for (const prop of props) {
|
|
269
|
-
const escapedName = prop.getEscapedName();
|
|
270
|
-
if (escapedName.startsWith("__@") && (escapedName.includes("formspec.extends") || escapedName.includes("FORMSPEC_EXTENDS"))) {
|
|
271
|
-
const propType = checker.getTypeOfSymbol(prop);
|
|
272
|
-
if (propType.isStringLiteral()) {
|
|
273
|
-
extendsBuiltin = propType.value;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
if (escapedName.startsWith("__@") && (escapedName.includes("formspec.extension") || escapedName.includes("FORMSPEC_EXTENSION"))) {
|
|
277
|
-
const propType = checker.getTypeOfSymbol(prop);
|
|
278
|
-
if (propType.isStringLiteral()) {
|
|
279
|
-
extensionName = propType.value;
|
|
506
|
+
if (docComment.deprecatedBlock !== void 0) {
|
|
507
|
+
annotations.push({
|
|
508
|
+
kind: "annotation",
|
|
509
|
+
annotationKind: "deprecated",
|
|
510
|
+
provenance: provenanceForComment(range, sourceFile, file, "deprecated")
|
|
511
|
+
});
|
|
280
512
|
}
|
|
281
513
|
}
|
|
282
|
-
if (escapedName.startsWith("__@") && (escapedName.includes("formspec.marker") || escapedName.includes("FORMSPEC_MARKER"))) {
|
|
283
|
-
isMarker = true;
|
|
284
|
-
}
|
|
285
514
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
name,
|
|
289
|
-
extendsBuiltin,
|
|
290
|
-
isFormSpec: true,
|
|
291
|
-
isMarker: false
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
if (extensionName) {
|
|
295
|
-
return {
|
|
296
|
-
name,
|
|
297
|
-
extensionName,
|
|
298
|
-
isFormSpec: true,
|
|
299
|
-
isMarker
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
if (isMarker) {
|
|
303
|
-
return {
|
|
304
|
-
name,
|
|
305
|
-
isFormSpec: true,
|
|
306
|
-
isMarker: true
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
return null;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// src/analyzer/jsdoc-constraints.ts
|
|
313
|
-
import * as ts3 from "typescript";
|
|
314
|
-
import { CONSTRAINT_TAG_DEFINITIONS } from "@formspec/core";
|
|
315
|
-
function extractJSDocConstraints(node) {
|
|
316
|
-
const results = [];
|
|
317
|
-
const jsDocTags = ts3.getJSDocTags(node);
|
|
318
|
-
for (const tag of jsDocTags) {
|
|
515
|
+
const jsDocTagsAll = ts2.getJSDocTags(node);
|
|
516
|
+
for (const tag of jsDocTagsAll) {
|
|
319
517
|
const tagName = tag.tagName.text;
|
|
320
|
-
if (!(tagName
|
|
321
|
-
continue;
|
|
322
|
-
}
|
|
323
|
-
const constraintName = tagName;
|
|
324
|
-
const expectedType = CONSTRAINT_TAG_DEFINITIONS[constraintName];
|
|
518
|
+
if (!TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
|
|
325
519
|
const commentText = getTagCommentText(tag);
|
|
326
|
-
if (commentText === void 0 || commentText === "")
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const
|
|
330
|
-
if (
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
if (expectedType === "number") {
|
|
334
|
-
const value = Number(trimmed);
|
|
335
|
-
if (Number.isNaN(value)) {
|
|
336
|
-
continue;
|
|
337
|
-
}
|
|
338
|
-
results.push(createSyntheticDecorator(constraintName, value));
|
|
339
|
-
} else if (expectedType === "json") {
|
|
340
|
-
try {
|
|
341
|
-
const parsed = JSON.parse(trimmed);
|
|
342
|
-
if (!Array.isArray(parsed)) {
|
|
343
|
-
continue;
|
|
344
|
-
}
|
|
345
|
-
results.push(createSyntheticDecorator(constraintName, parsed));
|
|
346
|
-
} catch {
|
|
347
|
-
continue;
|
|
348
|
-
}
|
|
349
|
-
} else {
|
|
350
|
-
results.push(createSyntheticDecorator(constraintName, trimmed));
|
|
520
|
+
if (commentText === void 0 || commentText.trim() === "") continue;
|
|
521
|
+
const text = commentText.trim();
|
|
522
|
+
const provenance = provenanceForJSDocTag(tag, file);
|
|
523
|
+
const constraintNode = parseConstraintValue(tagName, text, provenance);
|
|
524
|
+
if (constraintNode) {
|
|
525
|
+
constraints.push(constraintNode);
|
|
351
526
|
}
|
|
352
527
|
}
|
|
353
|
-
return results;
|
|
354
|
-
}
|
|
355
|
-
function extractJSDocFieldMetadata(node) {
|
|
356
|
-
const jsDocTags = ts3.getJSDocTags(node);
|
|
357
528
|
let displayName;
|
|
358
529
|
let description;
|
|
359
|
-
|
|
530
|
+
let displayNameTag;
|
|
531
|
+
let descriptionTag;
|
|
532
|
+
for (const tag of jsDocTagsAll) {
|
|
360
533
|
const tagName = tag.tagName.text;
|
|
361
534
|
const commentText = getTagCommentText(tag);
|
|
362
535
|
if (commentText === void 0 || commentText.trim() === "") {
|
|
@@ -365,18 +538,132 @@ function extractJSDocFieldMetadata(node) {
|
|
|
365
538
|
const trimmed = commentText.trim();
|
|
366
539
|
if (tagName === "Field_displayName") {
|
|
367
540
|
displayName = trimmed;
|
|
541
|
+
displayNameTag = tag;
|
|
368
542
|
} else if (tagName === "Field_description") {
|
|
369
543
|
description = trimmed;
|
|
544
|
+
descriptionTag = tag;
|
|
370
545
|
}
|
|
371
546
|
}
|
|
372
|
-
if (displayName
|
|
547
|
+
if (displayName !== void 0 && displayNameTag) {
|
|
548
|
+
annotations.push({
|
|
549
|
+
kind: "annotation",
|
|
550
|
+
annotationKind: "displayName",
|
|
551
|
+
value: displayName,
|
|
552
|
+
provenance: provenanceForJSDocTag(displayNameTag, file)
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
if (description !== void 0 && descriptionTag) {
|
|
556
|
+
annotations.push({
|
|
557
|
+
kind: "annotation",
|
|
558
|
+
annotationKind: "description",
|
|
559
|
+
value: description,
|
|
560
|
+
provenance: provenanceForJSDocTag(descriptionTag, file)
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
return { constraints, annotations };
|
|
564
|
+
}
|
|
565
|
+
function extractBlockText(block) {
|
|
566
|
+
return extractPlainText(block.content);
|
|
567
|
+
}
|
|
568
|
+
function extractPlainText(node) {
|
|
569
|
+
let result = "";
|
|
570
|
+
if (node instanceof DocPlainText) {
|
|
571
|
+
return node.text;
|
|
572
|
+
}
|
|
573
|
+
if (node instanceof DocSoftBreak) {
|
|
574
|
+
return " ";
|
|
575
|
+
}
|
|
576
|
+
if (typeof node.getChildNodes === "function") {
|
|
577
|
+
for (const child of node.getChildNodes()) {
|
|
578
|
+
result += extractPlainText(child);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return result;
|
|
582
|
+
}
|
|
583
|
+
function parseConstraintValue(tagName, text, provenance) {
|
|
584
|
+
if (!isBuiltinConstraintName(tagName)) {
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
const expectedType = BUILTIN_CONSTRAINT_DEFINITIONS[tagName];
|
|
588
|
+
if (expectedType === "number") {
|
|
589
|
+
const value = Number(text);
|
|
590
|
+
if (Number.isNaN(value)) {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
const numericKind = NUMERIC_CONSTRAINT_MAP[tagName];
|
|
594
|
+
if (numericKind) {
|
|
595
|
+
return {
|
|
596
|
+
kind: "constraint",
|
|
597
|
+
constraintKind: numericKind,
|
|
598
|
+
value,
|
|
599
|
+
provenance
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
const lengthKind = LENGTH_CONSTRAINT_MAP[tagName];
|
|
603
|
+
if (lengthKind) {
|
|
604
|
+
return {
|
|
605
|
+
kind: "constraint",
|
|
606
|
+
constraintKind: lengthKind,
|
|
607
|
+
value,
|
|
608
|
+
provenance
|
|
609
|
+
};
|
|
610
|
+
}
|
|
373
611
|
return null;
|
|
374
612
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
613
|
+
if (expectedType === "json") {
|
|
614
|
+
try {
|
|
615
|
+
const parsed = JSON.parse(text);
|
|
616
|
+
if (!Array.isArray(parsed)) {
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
const members = [];
|
|
620
|
+
for (const item of parsed) {
|
|
621
|
+
if (typeof item === "string" || typeof item === "number") {
|
|
622
|
+
members.push(item);
|
|
623
|
+
} else if (typeof item === "object" && item !== null && "id" in item) {
|
|
624
|
+
const id = item["id"];
|
|
625
|
+
if (typeof id === "string" || typeof id === "number") {
|
|
626
|
+
members.push(id);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return {
|
|
631
|
+
kind: "constraint",
|
|
632
|
+
constraintKind: "allowedMembers",
|
|
633
|
+
members,
|
|
634
|
+
provenance
|
|
635
|
+
};
|
|
636
|
+
} catch {
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return {
|
|
641
|
+
kind: "constraint",
|
|
642
|
+
constraintKind: "pattern",
|
|
643
|
+
pattern: text,
|
|
644
|
+
provenance
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
function provenanceForComment(range, sourceFile, file, tagName) {
|
|
648
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(range.pos);
|
|
649
|
+
return {
|
|
650
|
+
surface: "tsdoc",
|
|
651
|
+
file,
|
|
652
|
+
line: line + 1,
|
|
653
|
+
column: character,
|
|
654
|
+
tagName: "@" + tagName
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
function provenanceForJSDocTag(tag, file) {
|
|
658
|
+
const sourceFile = tag.getSourceFile();
|
|
659
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(tag.getStart());
|
|
660
|
+
return {
|
|
661
|
+
surface: "tsdoc",
|
|
662
|
+
file,
|
|
663
|
+
line: line + 1,
|
|
664
|
+
column: character,
|
|
665
|
+
tagName: "@" + tag.tagName.text
|
|
378
666
|
};
|
|
379
|
-
return createSyntheticDecorator("Field", fieldOpts);
|
|
380
667
|
}
|
|
381
668
|
function getTagCommentText(tag) {
|
|
382
669
|
if (tag.comment === void 0) {
|
|
@@ -385,27 +672,73 @@ function getTagCommentText(tag) {
|
|
|
385
672
|
if (typeof tag.comment === "string") {
|
|
386
673
|
return tag.comment;
|
|
387
674
|
}
|
|
388
|
-
return
|
|
675
|
+
return ts2.getTextOfJSDocComment(tag.comment);
|
|
389
676
|
}
|
|
390
|
-
|
|
677
|
+
|
|
678
|
+
// src/analyzer/jsdoc-constraints.ts
|
|
679
|
+
function extractJSDocConstraintNodes(node, file = "") {
|
|
680
|
+
const result = parseTSDocTags(node, file);
|
|
681
|
+
return [...result.constraints];
|
|
682
|
+
}
|
|
683
|
+
function extractJSDocAnnotationNodes(node, file = "") {
|
|
684
|
+
const result = parseTSDocTags(node, file);
|
|
685
|
+
return [...result.annotations];
|
|
686
|
+
}
|
|
687
|
+
function extractDefaultValueAnnotation(initializer, file = "") {
|
|
688
|
+
if (!initializer) return null;
|
|
689
|
+
let value;
|
|
690
|
+
if (ts3.isStringLiteral(initializer)) {
|
|
691
|
+
value = initializer.text;
|
|
692
|
+
} else if (ts3.isNumericLiteral(initializer)) {
|
|
693
|
+
value = Number(initializer.text);
|
|
694
|
+
} else if (initializer.kind === ts3.SyntaxKind.TrueKeyword) {
|
|
695
|
+
value = true;
|
|
696
|
+
} else if (initializer.kind === ts3.SyntaxKind.FalseKeyword) {
|
|
697
|
+
value = false;
|
|
698
|
+
} else if (initializer.kind === ts3.SyntaxKind.NullKeyword) {
|
|
699
|
+
value = null;
|
|
700
|
+
} else if (ts3.isPrefixUnaryExpression(initializer)) {
|
|
701
|
+
if (initializer.operator === ts3.SyntaxKind.MinusToken && ts3.isNumericLiteral(initializer.operand)) {
|
|
702
|
+
value = -Number(initializer.operand.text);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (value === void 0) return null;
|
|
706
|
+
const sourceFile = initializer.getSourceFile();
|
|
707
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(initializer.getStart());
|
|
391
708
|
return {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
709
|
+
kind: "annotation",
|
|
710
|
+
annotationKind: "defaultValue",
|
|
711
|
+
value,
|
|
712
|
+
provenance: {
|
|
713
|
+
surface: "tsdoc",
|
|
714
|
+
file,
|
|
715
|
+
line: line + 1,
|
|
716
|
+
column: character
|
|
717
|
+
}
|
|
395
718
|
};
|
|
396
719
|
}
|
|
397
720
|
|
|
398
721
|
// src/analyzer/class-analyzer.ts
|
|
399
|
-
function
|
|
722
|
+
function isObjectType(type) {
|
|
723
|
+
return !!(type.flags & ts4.TypeFlags.Object);
|
|
724
|
+
}
|
|
725
|
+
function isTypeReference(type) {
|
|
726
|
+
return !!(type.flags & ts4.TypeFlags.Object) && !!(type.objectFlags & ts4.ObjectFlags.Reference);
|
|
727
|
+
}
|
|
728
|
+
function analyzeClassToIR(classDecl, checker, file = "") {
|
|
400
729
|
const name = classDecl.name?.text ?? "AnonymousClass";
|
|
401
730
|
const fields = [];
|
|
731
|
+
const fieldLayouts = [];
|
|
732
|
+
const typeRegistry = {};
|
|
733
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
402
734
|
const instanceMethods = [];
|
|
403
735
|
const staticMethods = [];
|
|
404
736
|
for (const member of classDecl.members) {
|
|
405
737
|
if (ts4.isPropertyDeclaration(member)) {
|
|
406
|
-
const
|
|
407
|
-
if (
|
|
408
|
-
fields.push(
|
|
738
|
+
const fieldNode = analyzeFieldToIR(member, checker, file, typeRegistry, visiting);
|
|
739
|
+
if (fieldNode) {
|
|
740
|
+
fields.push(fieldNode);
|
|
741
|
+
fieldLayouts.push({});
|
|
409
742
|
}
|
|
410
743
|
} else if (ts4.isMethodDeclaration(member)) {
|
|
411
744
|
const methodInfo = analyzeMethod(member, checker);
|
|
@@ -419,155 +752,25 @@ function analyzeClass(classDecl, checker) {
|
|
|
419
752
|
}
|
|
420
753
|
}
|
|
421
754
|
}
|
|
422
|
-
return {
|
|
423
|
-
name,
|
|
424
|
-
fields,
|
|
425
|
-
instanceMethods,
|
|
426
|
-
staticMethods
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
function analyzeField(prop, checker) {
|
|
430
|
-
if (!ts4.isIdentifier(prop.name)) {
|
|
431
|
-
return null;
|
|
432
|
-
}
|
|
433
|
-
const name = prop.name.text;
|
|
434
|
-
const typeNode = prop.type;
|
|
435
|
-
const type = checker.getTypeAtLocation(prop);
|
|
436
|
-
const optional = prop.questionToken !== void 0;
|
|
437
|
-
const decorators = extractDecorators(prop);
|
|
438
|
-
for (const dec of decorators) {
|
|
439
|
-
if (dec.node) {
|
|
440
|
-
const resolved = resolveDecorator(dec.node, checker);
|
|
441
|
-
if (resolved) {
|
|
442
|
-
dec.resolved = resolved;
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
if (prop.type) {
|
|
447
|
-
const aliasConstraints = extractTypeAliasConstraints(prop.type, checker);
|
|
448
|
-
decorators.push(...aliasConstraints);
|
|
449
|
-
}
|
|
450
|
-
const jsdocConstraints = extractJSDocConstraints(prop);
|
|
451
|
-
decorators.push(...jsdocConstraints);
|
|
452
|
-
const deprecated = hasDeprecatedTag(prop);
|
|
453
|
-
const defaultValue = extractDefaultValue(prop.initializer);
|
|
454
|
-
return {
|
|
455
|
-
name,
|
|
456
|
-
typeNode,
|
|
457
|
-
type,
|
|
458
|
-
optional,
|
|
459
|
-
decorators,
|
|
460
|
-
deprecated,
|
|
461
|
-
defaultValue
|
|
462
|
-
};
|
|
463
|
-
}
|
|
464
|
-
function extractTypeAliasConstraints(typeNode, checker) {
|
|
465
|
-
if (!ts4.isTypeReferenceNode(typeNode)) return [];
|
|
466
|
-
const symbol = checker.getSymbolAtLocation(typeNode.typeName);
|
|
467
|
-
if (!symbol?.declarations) return [];
|
|
468
|
-
const aliasDecl = symbol.declarations.find(ts4.isTypeAliasDeclaration);
|
|
469
|
-
if (!aliasDecl) return [];
|
|
470
|
-
if (ts4.isTypeLiteralNode(aliasDecl.type)) return [];
|
|
471
|
-
return extractJSDocConstraints(aliasDecl);
|
|
472
|
-
}
|
|
473
|
-
function hasDeprecatedTag(node) {
|
|
474
|
-
const jsDocTags = ts4.getJSDocTags(node);
|
|
475
|
-
return jsDocTags.some((tag) => tag.tagName.text === "deprecated");
|
|
476
|
-
}
|
|
477
|
-
function extractDefaultValue(initializer) {
|
|
478
|
-
if (!initializer) return void 0;
|
|
479
|
-
if (ts4.isStringLiteral(initializer)) {
|
|
480
|
-
return initializer.text;
|
|
481
|
-
}
|
|
482
|
-
if (ts4.isNumericLiteral(initializer)) {
|
|
483
|
-
return Number(initializer.text);
|
|
484
|
-
}
|
|
485
|
-
if (initializer.kind === ts4.SyntaxKind.TrueKeyword) {
|
|
486
|
-
return true;
|
|
487
|
-
}
|
|
488
|
-
if (initializer.kind === ts4.SyntaxKind.FalseKeyword) {
|
|
489
|
-
return false;
|
|
490
|
-
}
|
|
491
|
-
if (initializer.kind === ts4.SyntaxKind.NullKeyword) {
|
|
492
|
-
return null;
|
|
493
|
-
}
|
|
494
|
-
if (ts4.isPrefixUnaryExpression(initializer)) {
|
|
495
|
-
if (initializer.operator === ts4.SyntaxKind.MinusToken && ts4.isNumericLiteral(initializer.operand)) {
|
|
496
|
-
return -Number(initializer.operand.text);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
return void 0;
|
|
755
|
+
return { name, fields, fieldLayouts, typeRegistry, instanceMethods, staticMethods };
|
|
500
756
|
}
|
|
501
|
-
function
|
|
502
|
-
if (!ts4.isIdentifier(method.name)) {
|
|
503
|
-
return null;
|
|
504
|
-
}
|
|
505
|
-
const name = method.name.text;
|
|
506
|
-
const parameters = [];
|
|
507
|
-
for (const param of method.parameters) {
|
|
508
|
-
if (ts4.isIdentifier(param.name)) {
|
|
509
|
-
const paramInfo = analyzeParameter(param, checker);
|
|
510
|
-
parameters.push(paramInfo);
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
const returnTypeNode = method.type;
|
|
514
|
-
const signature = checker.getSignatureFromDeclaration(method);
|
|
515
|
-
const returnType = signature ? checker.getReturnTypeOfSignature(signature) : checker.getTypeAtLocation(method);
|
|
516
|
-
return {
|
|
517
|
-
name,
|
|
518
|
-
parameters,
|
|
519
|
-
returnTypeNode,
|
|
520
|
-
returnType
|
|
521
|
-
};
|
|
522
|
-
}
|
|
523
|
-
function analyzeParameter(param, checker) {
|
|
524
|
-
const name = ts4.isIdentifier(param.name) ? param.name.text : "param";
|
|
525
|
-
const typeNode = param.type;
|
|
526
|
-
const type = checker.getTypeAtLocation(param);
|
|
527
|
-
const formSpecExportName = detectFormSpecReference(typeNode);
|
|
528
|
-
const optional = param.questionToken !== void 0 || param.initializer !== void 0;
|
|
529
|
-
return {
|
|
530
|
-
name,
|
|
531
|
-
typeNode,
|
|
532
|
-
type,
|
|
533
|
-
formSpecExportName,
|
|
534
|
-
optional
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
function detectFormSpecReference(typeNode) {
|
|
538
|
-
if (!typeNode) return null;
|
|
539
|
-
if (!ts4.isTypeReferenceNode(typeNode)) return null;
|
|
540
|
-
const typeName = ts4.isIdentifier(typeNode.typeName) ? typeNode.typeName.text : ts4.isQualifiedName(typeNode.typeName) ? typeNode.typeName.right.text : null;
|
|
541
|
-
if (typeName !== "InferSchema" && typeName !== "InferFormSchema") return null;
|
|
542
|
-
const typeArg = typeNode.typeArguments?.[0];
|
|
543
|
-
if (!typeArg || !ts4.isTypeQueryNode(typeArg)) return null;
|
|
544
|
-
if (ts4.isIdentifier(typeArg.exprName)) {
|
|
545
|
-
return typeArg.exprName.text;
|
|
546
|
-
}
|
|
547
|
-
if (ts4.isQualifiedName(typeArg.exprName)) {
|
|
548
|
-
return typeArg.exprName.right.text;
|
|
549
|
-
}
|
|
550
|
-
return null;
|
|
551
|
-
}
|
|
552
|
-
function analyzeInterface(interfaceDecl, checker) {
|
|
757
|
+
function analyzeInterfaceToIR(interfaceDecl, checker, file = "") {
|
|
553
758
|
const name = interfaceDecl.name.text;
|
|
554
759
|
const fields = [];
|
|
760
|
+
const typeRegistry = {};
|
|
761
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
555
762
|
for (const member of interfaceDecl.members) {
|
|
556
763
|
if (ts4.isPropertySignature(member)) {
|
|
557
|
-
const
|
|
558
|
-
if (
|
|
559
|
-
fields.push(
|
|
764
|
+
const fieldNode = analyzeInterfacePropertyToIR(member, checker, file, typeRegistry, visiting);
|
|
765
|
+
if (fieldNode) {
|
|
766
|
+
fields.push(fieldNode);
|
|
560
767
|
}
|
|
561
768
|
}
|
|
562
769
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
fields,
|
|
566
|
-
instanceMethods: [],
|
|
567
|
-
staticMethods: []
|
|
568
|
-
};
|
|
770
|
+
const fieldLayouts = fields.map(() => ({}));
|
|
771
|
+
return { name, fields, fieldLayouts, typeRegistry, instanceMethods: [], staticMethods: [] };
|
|
569
772
|
}
|
|
570
|
-
function
|
|
773
|
+
function analyzeTypeAliasToIR(typeAlias, checker, file = "") {
|
|
571
774
|
if (!ts4.isTypeLiteralNode(typeAlias.type)) {
|
|
572
775
|
const sourceFile = typeAlias.getSourceFile();
|
|
573
776
|
const { line } = sourceFile.getLineAndCharacterOfPosition(typeAlias.getStart());
|
|
@@ -579,11 +782,13 @@ function analyzeTypeAlias(typeAlias, checker) {
|
|
|
579
782
|
}
|
|
580
783
|
const name = typeAlias.name.text;
|
|
581
784
|
const fields = [];
|
|
785
|
+
const typeRegistry = {};
|
|
786
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
582
787
|
for (const member of typeAlias.type.members) {
|
|
583
788
|
if (ts4.isPropertySignature(member)) {
|
|
584
|
-
const
|
|
585
|
-
if (
|
|
586
|
-
fields.push(
|
|
789
|
+
const fieldNode = analyzeInterfacePropertyToIR(member, checker, file, typeRegistry, visiting);
|
|
790
|
+
if (fieldNode) {
|
|
791
|
+
fields.push(fieldNode);
|
|
587
792
|
}
|
|
588
793
|
}
|
|
589
794
|
}
|
|
@@ -592,411 +797,601 @@ function analyzeTypeAlias(typeAlias, checker) {
|
|
|
592
797
|
analysis: {
|
|
593
798
|
name,
|
|
594
799
|
fields,
|
|
800
|
+
fieldLayouts: fields.map(() => ({})),
|
|
801
|
+
typeRegistry,
|
|
595
802
|
instanceMethods: [],
|
|
596
803
|
staticMethods: []
|
|
597
804
|
}
|
|
598
805
|
};
|
|
599
806
|
}
|
|
600
|
-
function
|
|
807
|
+
function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting) {
|
|
601
808
|
if (!ts4.isIdentifier(prop.name)) {
|
|
602
809
|
return null;
|
|
603
810
|
}
|
|
604
811
|
const name = prop.name.text;
|
|
605
|
-
const
|
|
606
|
-
const type = checker.getTypeAtLocation(prop);
|
|
812
|
+
const tsType = checker.getTypeAtLocation(prop);
|
|
607
813
|
const optional = prop.questionToken !== void 0;
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
814
|
+
const provenance = provenanceForNode(prop, file);
|
|
815
|
+
const type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
|
|
816
|
+
const constraints = [];
|
|
817
|
+
if (prop.type) {
|
|
818
|
+
constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
|
|
819
|
+
}
|
|
820
|
+
constraints.push(...extractJSDocConstraintNodes(prop, file));
|
|
821
|
+
const annotations = [];
|
|
822
|
+
annotations.push(...extractJSDocAnnotationNodes(prop, file));
|
|
823
|
+
const defaultAnnotation = extractDefaultValueAnnotation(prop.initializer, file);
|
|
824
|
+
if (defaultAnnotation) {
|
|
825
|
+
annotations.push(defaultAnnotation);
|
|
826
|
+
}
|
|
620
827
|
return {
|
|
828
|
+
kind: "field",
|
|
621
829
|
name,
|
|
622
|
-
typeNode,
|
|
623
830
|
type,
|
|
624
|
-
optional,
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
831
|
+
required: !optional,
|
|
832
|
+
constraints,
|
|
833
|
+
annotations,
|
|
834
|
+
provenance
|
|
628
835
|
};
|
|
629
836
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
837
|
+
function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visiting) {
|
|
838
|
+
if (!ts4.isIdentifier(prop.name)) {
|
|
839
|
+
return null;
|
|
840
|
+
}
|
|
841
|
+
const name = prop.name.text;
|
|
842
|
+
const tsType = checker.getTypeAtLocation(prop);
|
|
843
|
+
const optional = prop.questionToken !== void 0;
|
|
844
|
+
const provenance = provenanceForNode(prop, file);
|
|
845
|
+
const type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
|
|
846
|
+
const constraints = [];
|
|
847
|
+
if (prop.type) {
|
|
848
|
+
constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
|
|
849
|
+
}
|
|
850
|
+
constraints.push(...extractJSDocConstraintNodes(prop, file));
|
|
851
|
+
const annotations = [];
|
|
852
|
+
annotations.push(...extractJSDocAnnotationNodes(prop, file));
|
|
853
|
+
return {
|
|
854
|
+
kind: "field",
|
|
855
|
+
name,
|
|
856
|
+
type,
|
|
857
|
+
required: !optional,
|
|
858
|
+
constraints,
|
|
859
|
+
annotations,
|
|
860
|
+
provenance
|
|
861
|
+
};
|
|
637
862
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
863
|
+
function resolveTypeNode(type, checker, file, typeRegistry, visiting) {
|
|
864
|
+
if (type.flags & ts4.TypeFlags.String) {
|
|
865
|
+
return { kind: "primitive", primitiveKind: "string" };
|
|
866
|
+
}
|
|
867
|
+
if (type.flags & ts4.TypeFlags.Number) {
|
|
868
|
+
return { kind: "primitive", primitiveKind: "number" };
|
|
869
|
+
}
|
|
870
|
+
if (type.flags & ts4.TypeFlags.Boolean) {
|
|
871
|
+
return { kind: "primitive", primitiveKind: "boolean" };
|
|
872
|
+
}
|
|
873
|
+
if (type.flags & ts4.TypeFlags.Null) {
|
|
874
|
+
return { kind: "primitive", primitiveKind: "null" };
|
|
875
|
+
}
|
|
876
|
+
if (type.flags & ts4.TypeFlags.Undefined) {
|
|
877
|
+
return { kind: "primitive", primitiveKind: "null" };
|
|
878
|
+
}
|
|
879
|
+
if (type.isStringLiteral()) {
|
|
880
|
+
return {
|
|
881
|
+
kind: "enum",
|
|
882
|
+
members: [{ value: type.value }]
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
if (type.isNumberLiteral()) {
|
|
886
|
+
return {
|
|
887
|
+
kind: "enum",
|
|
888
|
+
members: [{ value: type.value }]
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
if (type.isUnion()) {
|
|
892
|
+
return resolveUnionType(type, checker, file, typeRegistry, visiting);
|
|
893
|
+
}
|
|
894
|
+
if (checker.isArrayType(type)) {
|
|
895
|
+
return resolveArrayType(type, checker, file, typeRegistry, visiting);
|
|
896
|
+
}
|
|
897
|
+
if (isObjectType(type)) {
|
|
898
|
+
return resolveObjectType(type, checker, file, typeRegistry, visiting);
|
|
899
|
+
}
|
|
900
|
+
return { kind: "primitive", primitiveKind: "string" };
|
|
901
|
+
}
|
|
902
|
+
function resolveUnionType(type, checker, file, typeRegistry, visiting) {
|
|
903
|
+
const allTypes = type.types;
|
|
904
|
+
const nonNullTypes = allTypes.filter(
|
|
905
|
+
(t) => !(t.flags & (ts4.TypeFlags.Null | ts4.TypeFlags.Undefined))
|
|
906
|
+
);
|
|
907
|
+
const hasNull = allTypes.some((t) => t.flags & ts4.TypeFlags.Null);
|
|
908
|
+
const isBooleanUnion2 = nonNullTypes.length === 2 && nonNullTypes.every((t) => t.flags & ts4.TypeFlags.BooleanLiteral);
|
|
909
|
+
if (isBooleanUnion2) {
|
|
910
|
+
const boolNode = { kind: "primitive", primitiveKind: "boolean" };
|
|
911
|
+
if (hasNull) {
|
|
912
|
+
return {
|
|
913
|
+
kind: "union",
|
|
914
|
+
members: [boolNode, { kind: "primitive", primitiveKind: "null" }]
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
return boolNode;
|
|
918
|
+
}
|
|
919
|
+
const allStringLiterals = nonNullTypes.every((t) => t.isStringLiteral());
|
|
920
|
+
if (allStringLiterals && nonNullTypes.length > 0) {
|
|
921
|
+
const stringTypes = nonNullTypes.filter((t) => t.isStringLiteral());
|
|
922
|
+
const enumNode = {
|
|
923
|
+
kind: "enum",
|
|
924
|
+
members: stringTypes.map((t) => ({ value: t.value }))
|
|
925
|
+
};
|
|
926
|
+
if (hasNull) {
|
|
927
|
+
return {
|
|
928
|
+
kind: "union",
|
|
929
|
+
members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
return enumNode;
|
|
933
|
+
}
|
|
934
|
+
const allNumberLiterals = nonNullTypes.every((t) => t.isNumberLiteral());
|
|
935
|
+
if (allNumberLiterals && nonNullTypes.length > 0) {
|
|
936
|
+
const numberTypes = nonNullTypes.filter((t) => t.isNumberLiteral());
|
|
937
|
+
const enumNode = {
|
|
938
|
+
kind: "enum",
|
|
939
|
+
members: numberTypes.map((t) => ({ value: t.value }))
|
|
940
|
+
};
|
|
941
|
+
if (hasNull) {
|
|
942
|
+
return {
|
|
943
|
+
kind: "union",
|
|
944
|
+
members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
return enumNode;
|
|
948
|
+
}
|
|
949
|
+
if (nonNullTypes.length === 1 && nonNullTypes[0]) {
|
|
950
|
+
const inner = resolveTypeNode(nonNullTypes[0], checker, file, typeRegistry, visiting);
|
|
951
|
+
if (hasNull) {
|
|
952
|
+
return {
|
|
953
|
+
kind: "union",
|
|
954
|
+
members: [inner, { kind: "primitive", primitiveKind: "null" }]
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
return inner;
|
|
958
|
+
}
|
|
959
|
+
const members = nonNullTypes.map(
|
|
960
|
+
(t) => resolveTypeNode(t, checker, file, typeRegistry, visiting)
|
|
961
|
+
);
|
|
962
|
+
if (hasNull) {
|
|
963
|
+
members.push({ kind: "primitive", primitiveKind: "null" });
|
|
964
|
+
}
|
|
965
|
+
return { kind: "union", members };
|
|
966
|
+
}
|
|
967
|
+
function resolveArrayType(type, checker, file, typeRegistry, visiting) {
|
|
968
|
+
const typeArgs = isTypeReference(type) ? type.typeArguments : void 0;
|
|
969
|
+
const elementType = typeArgs?.[0];
|
|
970
|
+
const items = elementType ? resolveTypeNode(elementType, checker, file, typeRegistry, visiting) : { kind: "primitive", primitiveKind: "string" };
|
|
971
|
+
return { kind: "array", items };
|
|
972
|
+
}
|
|
973
|
+
function resolveObjectType(type, checker, file, typeRegistry, visiting) {
|
|
974
|
+
if (visiting.has(type)) {
|
|
975
|
+
return { kind: "object", properties: [], additionalProperties: false };
|
|
976
|
+
}
|
|
977
|
+
visiting.add(type);
|
|
978
|
+
const typeName = getNamedTypeName(type);
|
|
979
|
+
if (typeName && typeName in typeRegistry) {
|
|
980
|
+
visiting.delete(type);
|
|
981
|
+
return { kind: "reference", name: typeName, typeArguments: [] };
|
|
982
|
+
}
|
|
983
|
+
const properties = [];
|
|
984
|
+
const fieldInfoMap = getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting);
|
|
985
|
+
for (const prop of type.getProperties()) {
|
|
986
|
+
const declaration = prop.valueDeclaration ?? prop.declarations?.[0];
|
|
987
|
+
if (!declaration) continue;
|
|
988
|
+
const propType = checker.getTypeOfSymbolAtLocation(prop, declaration);
|
|
989
|
+
const optional = !!(prop.flags & ts4.SymbolFlags.Optional);
|
|
990
|
+
const propTypeNode = resolveTypeNode(propType, checker, file, typeRegistry, visiting);
|
|
991
|
+
const fieldNodeInfo = fieldInfoMap?.get(prop.name);
|
|
992
|
+
properties.push({
|
|
993
|
+
name: prop.name,
|
|
994
|
+
type: propTypeNode,
|
|
995
|
+
optional,
|
|
996
|
+
constraints: fieldNodeInfo?.constraints ?? [],
|
|
997
|
+
annotations: fieldNodeInfo?.annotations ?? [],
|
|
998
|
+
provenance: fieldNodeInfo?.provenance ?? provenanceForFile(file)
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
visiting.delete(type);
|
|
1002
|
+
const objectNode = {
|
|
1003
|
+
kind: "object",
|
|
1004
|
+
properties,
|
|
1005
|
+
additionalProperties: false
|
|
1006
|
+
};
|
|
1007
|
+
if (typeName) {
|
|
1008
|
+
typeRegistry[typeName] = {
|
|
1009
|
+
name: typeName,
|
|
1010
|
+
type: objectNode,
|
|
1011
|
+
provenance: provenanceForFile(file)
|
|
1012
|
+
};
|
|
1013
|
+
return { kind: "reference", name: typeName, typeArguments: [] };
|
|
1014
|
+
}
|
|
1015
|
+
return objectNode;
|
|
1016
|
+
}
|
|
1017
|
+
function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting) {
|
|
641
1018
|
const symbols = [type.getSymbol(), type.aliasSymbol].filter(
|
|
642
1019
|
(s) => s?.declarations != null && s.declarations.length > 0
|
|
643
1020
|
);
|
|
644
1021
|
for (const symbol of symbols) {
|
|
645
1022
|
const declarations = symbol.declarations;
|
|
646
1023
|
if (!declarations) continue;
|
|
647
|
-
const classDecl = declarations.find(
|
|
1024
|
+
const classDecl = declarations.find(ts4.isClassDeclaration);
|
|
648
1025
|
if (classDecl) {
|
|
649
1026
|
const map = /* @__PURE__ */ new Map();
|
|
650
1027
|
for (const member of classDecl.members) {
|
|
651
|
-
if (
|
|
652
|
-
const
|
|
653
|
-
if (
|
|
1028
|
+
if (ts4.isPropertyDeclaration(member) && ts4.isIdentifier(member.name)) {
|
|
1029
|
+
const fieldNode = analyzeFieldToIR(member, checker, file, typeRegistry, visiting);
|
|
1030
|
+
if (fieldNode) {
|
|
1031
|
+
map.set(fieldNode.name, {
|
|
1032
|
+
constraints: [...fieldNode.constraints],
|
|
1033
|
+
annotations: [...fieldNode.annotations],
|
|
1034
|
+
provenance: fieldNode.provenance
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
654
1037
|
}
|
|
655
1038
|
}
|
|
656
1039
|
return map;
|
|
657
1040
|
}
|
|
658
|
-
const interfaceDecl = declarations.find(
|
|
1041
|
+
const interfaceDecl = declarations.find(ts4.isInterfaceDeclaration);
|
|
659
1042
|
if (interfaceDecl) {
|
|
660
|
-
return
|
|
1043
|
+
return buildFieldNodeInfoMap(interfaceDecl.members, checker, file, typeRegistry, visiting);
|
|
661
1044
|
}
|
|
662
|
-
const typeAliasDecl = declarations.find(
|
|
663
|
-
if (typeAliasDecl &&
|
|
664
|
-
return
|
|
1045
|
+
const typeAliasDecl = declarations.find(ts4.isTypeAliasDeclaration);
|
|
1046
|
+
if (typeAliasDecl && ts4.isTypeLiteralNode(typeAliasDecl.type)) {
|
|
1047
|
+
return buildFieldNodeInfoMap(
|
|
1048
|
+
typeAliasDecl.type.members,
|
|
1049
|
+
checker,
|
|
1050
|
+
file,
|
|
1051
|
+
typeRegistry,
|
|
1052
|
+
visiting
|
|
1053
|
+
);
|
|
665
1054
|
}
|
|
666
1055
|
}
|
|
667
1056
|
return null;
|
|
668
1057
|
}
|
|
669
|
-
function
|
|
1058
|
+
function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting) {
|
|
670
1059
|
const map = /* @__PURE__ */ new Map();
|
|
671
1060
|
for (const member of members) {
|
|
672
|
-
if (
|
|
673
|
-
const
|
|
674
|
-
if (
|
|
675
|
-
map.set(
|
|
1061
|
+
if (ts4.isPropertySignature(member)) {
|
|
1062
|
+
const fieldNode = analyzeInterfacePropertyToIR(member, checker, file, typeRegistry, visiting);
|
|
1063
|
+
if (fieldNode) {
|
|
1064
|
+
map.set(fieldNode.name, {
|
|
1065
|
+
constraints: [...fieldNode.constraints],
|
|
1066
|
+
annotations: [...fieldNode.annotations],
|
|
1067
|
+
provenance: fieldNode.provenance
|
|
1068
|
+
});
|
|
676
1069
|
}
|
|
677
1070
|
}
|
|
678
1071
|
}
|
|
679
1072
|
return map;
|
|
680
1073
|
}
|
|
681
|
-
function
|
|
682
|
-
|
|
683
|
-
const
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
const fieldInfo = fieldInfoMap?.get(prop.name) ?? void 0;
|
|
690
|
-
result.push({ name: prop.name, type: propType, optional, fieldInfo });
|
|
691
|
-
}
|
|
692
|
-
return result;
|
|
1074
|
+
function extractTypeAliasConstraintNodes(typeNode, checker, file) {
|
|
1075
|
+
if (!ts4.isTypeReferenceNode(typeNode)) return [];
|
|
1076
|
+
const symbol = checker.getSymbolAtLocation(typeNode.typeName);
|
|
1077
|
+
if (!symbol?.declarations) return [];
|
|
1078
|
+
const aliasDecl = symbol.declarations.find(ts4.isTypeAliasDeclaration);
|
|
1079
|
+
if (!aliasDecl) return [];
|
|
1080
|
+
if (ts4.isTypeLiteralNode(aliasDecl.type)) return [];
|
|
1081
|
+
return extractJSDocConstraintNodes(aliasDecl, file);
|
|
693
1082
|
}
|
|
694
|
-
function
|
|
695
|
-
|
|
1083
|
+
function provenanceForNode(node, file) {
|
|
1084
|
+
const sourceFile = node.getSourceFile();
|
|
1085
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
1086
|
+
return {
|
|
1087
|
+
surface: "tsdoc",
|
|
1088
|
+
file,
|
|
1089
|
+
line: line + 1,
|
|
1090
|
+
column: character
|
|
1091
|
+
};
|
|
696
1092
|
}
|
|
697
|
-
function
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
return { jsonSchema: { type: "null" }, formSpecFieldType: "null" };
|
|
709
|
-
}
|
|
710
|
-
if (type.flags & ts5.TypeFlags.Undefined) {
|
|
711
|
-
return { jsonSchema: {}, formSpecFieldType: "undefined" };
|
|
1093
|
+
function provenanceForFile(file) {
|
|
1094
|
+
return { surface: "tsdoc", file, line: 0, column: 0 };
|
|
1095
|
+
}
|
|
1096
|
+
function getNamedTypeName(type) {
|
|
1097
|
+
const symbol = type.getSymbol();
|
|
1098
|
+
if (symbol?.declarations) {
|
|
1099
|
+
const decl = symbol.declarations[0];
|
|
1100
|
+
if (decl && (ts4.isClassDeclaration(decl) || ts4.isInterfaceDeclaration(decl) || ts4.isTypeAliasDeclaration(decl))) {
|
|
1101
|
+
const name = ts4.isClassDeclaration(decl) ? decl.name?.text : decl.name.text;
|
|
1102
|
+
if (name) return name;
|
|
1103
|
+
}
|
|
712
1104
|
}
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
1105
|
+
const aliasSymbol = type.aliasSymbol;
|
|
1106
|
+
if (aliasSymbol?.declarations) {
|
|
1107
|
+
const aliasDecl = aliasSymbol.declarations.find(ts4.isTypeAliasDeclaration);
|
|
1108
|
+
if (aliasDecl) {
|
|
1109
|
+
return aliasDecl.name.text;
|
|
1110
|
+
}
|
|
718
1111
|
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
1112
|
+
return null;
|
|
1113
|
+
}
|
|
1114
|
+
function analyzeMethod(method, checker) {
|
|
1115
|
+
if (!ts4.isIdentifier(method.name)) {
|
|
1116
|
+
return null;
|
|
724
1117
|
}
|
|
725
|
-
|
|
726
|
-
|
|
1118
|
+
const name = method.name.text;
|
|
1119
|
+
const parameters = [];
|
|
1120
|
+
for (const param of method.parameters) {
|
|
1121
|
+
if (ts4.isIdentifier(param.name)) {
|
|
1122
|
+
const paramInfo = analyzeParameter(param, checker);
|
|
1123
|
+
parameters.push(paramInfo);
|
|
1124
|
+
}
|
|
727
1125
|
}
|
|
728
|
-
|
|
729
|
-
|
|
1126
|
+
const returnTypeNode = method.type;
|
|
1127
|
+
const signature = checker.getSignatureFromDeclaration(method);
|
|
1128
|
+
const returnType = signature ? checker.getReturnTypeOfSignature(signature) : checker.getTypeAtLocation(method);
|
|
1129
|
+
return { name, parameters, returnTypeNode, returnType };
|
|
1130
|
+
}
|
|
1131
|
+
function analyzeParameter(param, checker) {
|
|
1132
|
+
const name = ts4.isIdentifier(param.name) ? param.name.text : "param";
|
|
1133
|
+
const typeNode = param.type;
|
|
1134
|
+
const type = checker.getTypeAtLocation(param);
|
|
1135
|
+
const formSpecExportName = detectFormSpecReference(typeNode);
|
|
1136
|
+
const optional = param.questionToken !== void 0 || param.initializer !== void 0;
|
|
1137
|
+
return { name, typeNode, type, formSpecExportName, optional };
|
|
1138
|
+
}
|
|
1139
|
+
function detectFormSpecReference(typeNode) {
|
|
1140
|
+
if (!typeNode) return null;
|
|
1141
|
+
if (!ts4.isTypeReferenceNode(typeNode)) return null;
|
|
1142
|
+
const typeName = ts4.isIdentifier(typeNode.typeName) ? typeNode.typeName.text : ts4.isQualifiedName(typeNode.typeName) ? typeNode.typeName.right.text : null;
|
|
1143
|
+
if (typeName !== "InferSchema" && typeName !== "InferFormSchema") return null;
|
|
1144
|
+
const typeArg = typeNode.typeArguments?.[0];
|
|
1145
|
+
if (!typeArg || !ts4.isTypeQueryNode(typeArg)) return null;
|
|
1146
|
+
if (ts4.isIdentifier(typeArg.exprName)) {
|
|
1147
|
+
return typeArg.exprName.text;
|
|
730
1148
|
}
|
|
731
|
-
if (
|
|
732
|
-
return
|
|
1149
|
+
if (ts4.isQualifiedName(typeArg.exprName)) {
|
|
1150
|
+
return typeArg.exprName.right.text;
|
|
733
1151
|
}
|
|
734
|
-
return
|
|
1152
|
+
return null;
|
|
735
1153
|
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
const
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
jsonSchema: { type: "boolean" },
|
|
746
|
-
formSpecFieldType: "boolean"
|
|
747
|
-
};
|
|
748
|
-
if (hasNull) {
|
|
749
|
-
result.jsonSchema = { oneOf: [{ type: "boolean" }, { type: "null" }] };
|
|
750
|
-
}
|
|
751
|
-
return result;
|
|
1154
|
+
|
|
1155
|
+
// src/json-schema/ir-generator.ts
|
|
1156
|
+
function makeContext() {
|
|
1157
|
+
return { defs: {} };
|
|
1158
|
+
}
|
|
1159
|
+
function generateJsonSchemaFromIR(ir) {
|
|
1160
|
+
const ctx = makeContext();
|
|
1161
|
+
for (const [name, typeDef] of Object.entries(ir.typeRegistry)) {
|
|
1162
|
+
ctx.defs[name] = generateTypeNode(typeDef.type, ctx);
|
|
752
1163
|
}
|
|
753
|
-
const
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
1164
|
+
const properties = {};
|
|
1165
|
+
const required = [];
|
|
1166
|
+
collectFields(ir.elements, properties, required, ctx);
|
|
1167
|
+
const uniqueRequired = [...new Set(required)];
|
|
1168
|
+
const result = {
|
|
1169
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
1170
|
+
type: "object",
|
|
1171
|
+
properties,
|
|
1172
|
+
...uniqueRequired.length > 0 && { required: uniqueRequired }
|
|
1173
|
+
};
|
|
1174
|
+
if (Object.keys(ctx.defs).length > 0) {
|
|
1175
|
+
result.$defs = ctx.defs;
|
|
764
1176
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1177
|
+
return result;
|
|
1178
|
+
}
|
|
1179
|
+
function collectFields(elements, properties, required, ctx) {
|
|
1180
|
+
for (const element of elements) {
|
|
1181
|
+
switch (element.kind) {
|
|
1182
|
+
case "field":
|
|
1183
|
+
properties[element.name] = generateFieldSchema(element, ctx);
|
|
1184
|
+
if (element.required) {
|
|
1185
|
+
required.push(element.name);
|
|
1186
|
+
}
|
|
1187
|
+
break;
|
|
1188
|
+
case "group":
|
|
1189
|
+
collectFields(element.elements, properties, required, ctx);
|
|
1190
|
+
break;
|
|
1191
|
+
case "conditional":
|
|
1192
|
+
collectFields(element.elements, properties, required, ctx);
|
|
1193
|
+
break;
|
|
1194
|
+
default: {
|
|
1195
|
+
const _exhaustive = element;
|
|
1196
|
+
void _exhaustive;
|
|
1197
|
+
}
|
|
774
1198
|
}
|
|
775
|
-
return result;
|
|
776
1199
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
1200
|
+
}
|
|
1201
|
+
function generateFieldSchema(field, ctx) {
|
|
1202
|
+
const schema = generateTypeNode(field.type, ctx);
|
|
1203
|
+
applyConstraints(schema, field.constraints);
|
|
1204
|
+
applyAnnotations(schema, field.annotations);
|
|
1205
|
+
return schema;
|
|
1206
|
+
}
|
|
1207
|
+
function generateTypeNode(type, ctx) {
|
|
1208
|
+
switch (type.kind) {
|
|
1209
|
+
case "primitive":
|
|
1210
|
+
return generatePrimitiveType(type);
|
|
1211
|
+
case "enum":
|
|
1212
|
+
return generateEnumType(type);
|
|
1213
|
+
case "array":
|
|
1214
|
+
return generateArrayType(type, ctx);
|
|
1215
|
+
case "object":
|
|
1216
|
+
return generateObjectType(type, ctx);
|
|
1217
|
+
case "union":
|
|
1218
|
+
return generateUnionType(type, ctx);
|
|
1219
|
+
case "reference":
|
|
1220
|
+
return generateReferenceType(type);
|
|
1221
|
+
case "dynamic":
|
|
1222
|
+
return generateDynamicType(type);
|
|
1223
|
+
case "custom":
|
|
1224
|
+
return generateCustomType(type);
|
|
1225
|
+
default: {
|
|
1226
|
+
const _exhaustive = type;
|
|
1227
|
+
return _exhaustive;
|
|
781
1228
|
}
|
|
782
|
-
return result;
|
|
783
1229
|
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
1230
|
+
}
|
|
1231
|
+
function generatePrimitiveType(type) {
|
|
1232
|
+
return { type: type.primitiveKind };
|
|
1233
|
+
}
|
|
1234
|
+
function generateEnumType(type) {
|
|
1235
|
+
const hasDisplayNames = type.members.some((m) => m.displayName !== void 0);
|
|
1236
|
+
if (hasDisplayNames) {
|
|
1237
|
+
return {
|
|
1238
|
+
oneOf: type.members.map((m) => {
|
|
1239
|
+
const entry = { const: m.value };
|
|
1240
|
+
if (m.displayName !== void 0) {
|
|
1241
|
+
entry.title = m.displayName;
|
|
1242
|
+
}
|
|
1243
|
+
return entry;
|
|
1244
|
+
})
|
|
1245
|
+
};
|
|
787
1246
|
}
|
|
788
|
-
return {
|
|
789
|
-
jsonSchema: { oneOf: schemas },
|
|
790
|
-
formSpecFieldType: "union"
|
|
791
|
-
};
|
|
1247
|
+
return { enum: type.members.map((m) => m.value) };
|
|
792
1248
|
}
|
|
793
|
-
function
|
|
794
|
-
const typeArgs = type.typeArguments;
|
|
795
|
-
const elementType = typeArgs?.[0];
|
|
796
|
-
const itemSchema = elementType ? convertTypeInternal(elementType, checker, visiting).jsonSchema : {};
|
|
1249
|
+
function generateArrayType(type, ctx) {
|
|
797
1250
|
return {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
items: itemSchema
|
|
801
|
-
},
|
|
802
|
-
formSpecFieldType: "array"
|
|
1251
|
+
type: "array",
|
|
1252
|
+
items: generateTypeNode(type.items, ctx)
|
|
803
1253
|
};
|
|
804
1254
|
}
|
|
805
|
-
function
|
|
806
|
-
if (visiting.has(type)) {
|
|
807
|
-
return { jsonSchema: { type: "object" }, formSpecFieldType: "object" };
|
|
808
|
-
}
|
|
809
|
-
visiting.add(type);
|
|
1255
|
+
function generateObjectType(type, ctx) {
|
|
810
1256
|
const properties = {};
|
|
811
1257
|
const required = [];
|
|
812
|
-
for (const
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
required.push(propInfo.name);
|
|
1258
|
+
for (const prop of type.properties) {
|
|
1259
|
+
properties[prop.name] = generatePropertySchema(prop, ctx);
|
|
1260
|
+
if (!prop.optional) {
|
|
1261
|
+
required.push(prop.name);
|
|
817
1262
|
}
|
|
818
1263
|
}
|
|
819
|
-
|
|
1264
|
+
const schema = { type: "object", properties };
|
|
1265
|
+
if (required.length > 0) {
|
|
1266
|
+
schema.required = required;
|
|
1267
|
+
}
|
|
1268
|
+
if (!type.additionalProperties) {
|
|
1269
|
+
schema.additionalProperties = false;
|
|
1270
|
+
}
|
|
1271
|
+
return schema;
|
|
1272
|
+
}
|
|
1273
|
+
function generatePropertySchema(prop, ctx) {
|
|
1274
|
+
const schema = generateTypeNode(prop.type, ctx);
|
|
1275
|
+
applyConstraints(schema, prop.constraints);
|
|
1276
|
+
applyAnnotations(schema, prop.annotations);
|
|
1277
|
+
return schema;
|
|
1278
|
+
}
|
|
1279
|
+
function generateUnionType(type, ctx) {
|
|
1280
|
+
if (isBooleanUnion(type)) {
|
|
1281
|
+
return { type: "boolean" };
|
|
1282
|
+
}
|
|
820
1283
|
return {
|
|
821
|
-
|
|
822
|
-
type: "object",
|
|
823
|
-
properties,
|
|
824
|
-
...required.length > 0 ? { required } : {}
|
|
825
|
-
},
|
|
826
|
-
formSpecFieldType: "object"
|
|
1284
|
+
anyOf: type.members.map((m) => generateTypeNode(m, ctx))
|
|
827
1285
|
};
|
|
828
1286
|
}
|
|
829
|
-
function
|
|
830
|
-
|
|
831
|
-
const
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
if (
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
propInfo.name,
|
|
846
|
-
propInfo.type,
|
|
847
|
-
propInfo.fieldInfo?.decorators ?? [],
|
|
848
|
-
propInfo.optional,
|
|
849
|
-
checker,
|
|
850
|
-
visitedTypes
|
|
851
|
-
)
|
|
852
|
-
);
|
|
853
|
-
}
|
|
854
|
-
visitedTypes.delete(type);
|
|
855
|
-
if (nestedFields.length > 0) {
|
|
856
|
-
field.fields = nestedFields;
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
for (const dec of decorators) {
|
|
861
|
-
applyDecoratorToField(field, dec);
|
|
862
|
-
}
|
|
863
|
-
return field;
|
|
864
|
-
}
|
|
865
|
-
function applyDecoratorToField(field, decorator) {
|
|
866
|
-
const { args } = decorator;
|
|
867
|
-
const resolved = decorator.resolved;
|
|
868
|
-
const effectiveName = resolved?.extendsBuiltin ?? decorator.name;
|
|
869
|
-
switch (effectiveName) {
|
|
870
|
-
case "Field": {
|
|
871
|
-
const opts = args[0];
|
|
872
|
-
if (typeof opts === "object" && opts !== null && !Array.isArray(opts)) {
|
|
873
|
-
if (typeof opts["displayName"] === "string") {
|
|
874
|
-
field.label = opts["displayName"];
|
|
875
|
-
}
|
|
876
|
-
if (typeof opts["description"] === "string") {
|
|
877
|
-
field.description = opts["description"];
|
|
878
|
-
}
|
|
879
|
-
if (typeof opts["placeholder"] === "string") {
|
|
880
|
-
field.placeholder = opts["placeholder"];
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
break;
|
|
1287
|
+
function isBooleanUnion(type) {
|
|
1288
|
+
if (type.members.length !== 2) return false;
|
|
1289
|
+
const kinds = type.members.map((m) => m.kind);
|
|
1290
|
+
return kinds.every((k) => k === "primitive") && type.members.every((m) => m.kind === "primitive" && m.primitiveKind === "boolean");
|
|
1291
|
+
}
|
|
1292
|
+
function generateReferenceType(type) {
|
|
1293
|
+
return { $ref: `#/$defs/${type.name}` };
|
|
1294
|
+
}
|
|
1295
|
+
function generateDynamicType(type) {
|
|
1296
|
+
if (type.dynamicKind === "enum") {
|
|
1297
|
+
const schema = {
|
|
1298
|
+
type: "string",
|
|
1299
|
+
"x-formspec-source": type.sourceKey
|
|
1300
|
+
};
|
|
1301
|
+
if (type.parameterFields.length > 0) {
|
|
1302
|
+
schema["x-formspec-params"] = [...type.parameterFields];
|
|
884
1303
|
}
|
|
885
|
-
|
|
886
|
-
if (typeof args[0] === "number") {
|
|
887
|
-
field.min = args[0];
|
|
888
|
-
}
|
|
889
|
-
break;
|
|
890
|
-
case "Maximum":
|
|
891
|
-
if (typeof args[0] === "number") {
|
|
892
|
-
field.max = args[0];
|
|
893
|
-
}
|
|
894
|
-
break;
|
|
895
|
-
case "MinLength":
|
|
896
|
-
if (typeof args[0] === "number") {
|
|
897
|
-
field.minLength = args[0];
|
|
898
|
-
}
|
|
899
|
-
break;
|
|
900
|
-
case "MaxLength":
|
|
901
|
-
if (typeof args[0] === "number") {
|
|
902
|
-
field.maxLength = args[0];
|
|
903
|
-
}
|
|
904
|
-
break;
|
|
905
|
-
case "Pattern":
|
|
906
|
-
if (typeof args[0] === "string") {
|
|
907
|
-
field.pattern = args[0];
|
|
908
|
-
}
|
|
909
|
-
break;
|
|
910
|
-
case "EnumOptions":
|
|
911
|
-
if (Array.isArray(args[0])) {
|
|
912
|
-
field.options = args[0];
|
|
913
|
-
}
|
|
914
|
-
break;
|
|
915
|
-
case "ShowWhen":
|
|
916
|
-
if (typeof args[0] === "object" && args[0] !== null) {
|
|
917
|
-
field.showWhen = args[0];
|
|
918
|
-
}
|
|
919
|
-
break;
|
|
920
|
-
case "Group":
|
|
921
|
-
if (typeof args[0] === "string") {
|
|
922
|
-
field.group = args[0];
|
|
923
|
-
}
|
|
924
|
-
break;
|
|
1304
|
+
return schema;
|
|
925
1305
|
}
|
|
1306
|
+
return {
|
|
1307
|
+
type: "object",
|
|
1308
|
+
additionalProperties: true,
|
|
1309
|
+
"x-formspec-schemaSource": type.sourceKey
|
|
1310
|
+
};
|
|
926
1311
|
}
|
|
927
|
-
function
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1312
|
+
function generateCustomType(_type) {
|
|
1313
|
+
return { type: "object" };
|
|
1314
|
+
}
|
|
1315
|
+
function applyConstraints(schema, constraints) {
|
|
1316
|
+
for (const constraint of constraints) {
|
|
1317
|
+
switch (constraint.constraintKind) {
|
|
1318
|
+
case "minimum":
|
|
1319
|
+
schema.minimum = constraint.value;
|
|
1320
|
+
break;
|
|
1321
|
+
case "maximum":
|
|
1322
|
+
schema.maximum = constraint.value;
|
|
1323
|
+
break;
|
|
1324
|
+
case "exclusiveMinimum":
|
|
1325
|
+
schema.exclusiveMinimum = constraint.value;
|
|
1326
|
+
break;
|
|
1327
|
+
case "exclusiveMaximum":
|
|
1328
|
+
schema.exclusiveMaximum = constraint.value;
|
|
1329
|
+
break;
|
|
1330
|
+
case "multipleOf": {
|
|
1331
|
+
const { value } = constraint;
|
|
1332
|
+
if (value === 1 && schema.type === "number") {
|
|
1333
|
+
schema.type = "integer";
|
|
1334
|
+
} else {
|
|
1335
|
+
schema.multipleOf = value;
|
|
943
1336
|
}
|
|
944
1337
|
break;
|
|
945
1338
|
}
|
|
946
|
-
case "
|
|
947
|
-
|
|
948
|
-
result.minimum = args[0];
|
|
949
|
-
}
|
|
1339
|
+
case "minLength":
|
|
1340
|
+
schema.minLength = constraint.value;
|
|
950
1341
|
break;
|
|
951
|
-
case "
|
|
952
|
-
|
|
953
|
-
result.maximum = args[0];
|
|
954
|
-
}
|
|
1342
|
+
case "maxLength":
|
|
1343
|
+
schema.maxLength = constraint.value;
|
|
955
1344
|
break;
|
|
956
|
-
case "
|
|
957
|
-
|
|
958
|
-
result.exclusiveMinimum = args[0];
|
|
959
|
-
}
|
|
1345
|
+
case "minItems":
|
|
1346
|
+
schema.minItems = constraint.value;
|
|
960
1347
|
break;
|
|
961
|
-
case "
|
|
962
|
-
|
|
963
|
-
result.exclusiveMaximum = args[0];
|
|
964
|
-
}
|
|
1348
|
+
case "maxItems":
|
|
1349
|
+
schema.maxItems = constraint.value;
|
|
965
1350
|
break;
|
|
966
|
-
case "
|
|
967
|
-
|
|
968
|
-
result.minLength = args[0];
|
|
969
|
-
}
|
|
1351
|
+
case "pattern":
|
|
1352
|
+
schema.pattern = constraint.pattern;
|
|
970
1353
|
break;
|
|
971
|
-
case "
|
|
972
|
-
|
|
973
|
-
result.maxLength = args[0];
|
|
974
|
-
}
|
|
1354
|
+
case "uniqueItems":
|
|
1355
|
+
schema.uniqueItems = constraint.value;
|
|
975
1356
|
break;
|
|
976
|
-
case "
|
|
977
|
-
if (typeof args[0] === "string") {
|
|
978
|
-
result.pattern = args[0];
|
|
979
|
-
}
|
|
1357
|
+
case "allowedMembers":
|
|
980
1358
|
break;
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
} else {
|
|
987
|
-
setSchemaExtension(result, key, args[0] ?? true);
|
|
1359
|
+
case "custom":
|
|
1360
|
+
break;
|
|
1361
|
+
default: {
|
|
1362
|
+
const _exhaustive = constraint;
|
|
1363
|
+
void _exhaustive;
|
|
988
1364
|
}
|
|
989
1365
|
}
|
|
990
1366
|
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1367
|
+
}
|
|
1368
|
+
function applyAnnotations(schema, annotations) {
|
|
1369
|
+
for (const annotation of annotations) {
|
|
1370
|
+
switch (annotation.annotationKind) {
|
|
1371
|
+
case "displayName":
|
|
1372
|
+
schema.title = annotation.value;
|
|
1373
|
+
break;
|
|
1374
|
+
case "description":
|
|
1375
|
+
schema.description = annotation.value;
|
|
1376
|
+
break;
|
|
1377
|
+
case "defaultValue":
|
|
1378
|
+
schema.default = annotation.value;
|
|
1379
|
+
break;
|
|
1380
|
+
case "deprecated":
|
|
1381
|
+
schema.deprecated = true;
|
|
1382
|
+
break;
|
|
1383
|
+
case "placeholder":
|
|
1384
|
+
break;
|
|
1385
|
+
case "formatHint":
|
|
1386
|
+
break;
|
|
1387
|
+
case "custom":
|
|
1388
|
+
break;
|
|
1389
|
+
default: {
|
|
1390
|
+
const _exhaustive = annotation;
|
|
1391
|
+
void _exhaustive;
|
|
1392
|
+
}
|
|
997
1393
|
}
|
|
998
1394
|
}
|
|
999
|
-
return result;
|
|
1000
1395
|
}
|
|
1001
1396
|
|
|
1002
1397
|
// src/ui-schema/schema.ts
|
|
@@ -1107,7 +1502,7 @@ var uiSchema = z.lazy(
|
|
|
1107
1502
|
() => z.union([verticalLayoutSchema, horizontalLayoutSchema, groupLayoutSchema, categorizationSchema])
|
|
1108
1503
|
);
|
|
1109
1504
|
|
|
1110
|
-
// src/ui-schema/generator.ts
|
|
1505
|
+
// src/ui-schema/ir-generator.ts
|
|
1111
1506
|
import { z as z2 } from "zod";
|
|
1112
1507
|
function parseOrThrow(schema, value, label) {
|
|
1113
1508
|
try {
|
|
@@ -1122,101 +1517,488 @@ ${error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n")}`
|
|
|
1122
1517
|
throw error;
|
|
1123
1518
|
}
|
|
1124
1519
|
}
|
|
1125
|
-
function
|
|
1520
|
+
function fieldToScope(fieldName) {
|
|
1521
|
+
return `#/properties/${fieldName}`;
|
|
1522
|
+
}
|
|
1523
|
+
function createShowRule(fieldName, value) {
|
|
1524
|
+
return {
|
|
1525
|
+
effect: "SHOW",
|
|
1526
|
+
condition: {
|
|
1527
|
+
scope: fieldToScope(fieldName),
|
|
1528
|
+
schema: { const: value }
|
|
1529
|
+
}
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
function combineRules(parentRule, childRule) {
|
|
1533
|
+
const parentCondition = parentRule.condition;
|
|
1534
|
+
const childCondition = childRule.condition;
|
|
1535
|
+
return {
|
|
1536
|
+
effect: "SHOW",
|
|
1537
|
+
condition: {
|
|
1538
|
+
scope: "#",
|
|
1539
|
+
schema: {
|
|
1540
|
+
allOf: [
|
|
1541
|
+
{
|
|
1542
|
+
properties: {
|
|
1543
|
+
[parentCondition.scope.replace("#/properties/", "")]: parentCondition.schema
|
|
1544
|
+
}
|
|
1545
|
+
},
|
|
1546
|
+
{
|
|
1547
|
+
properties: {
|
|
1548
|
+
[childCondition.scope.replace("#/properties/", "")]: childCondition.schema
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
]
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
function fieldNodeToControl(field, parentRule) {
|
|
1557
|
+
const displayNameAnnotation = field.annotations.find((a) => a.annotationKind === "displayName");
|
|
1126
1558
|
const control = {
|
|
1127
1559
|
type: "Control",
|
|
1128
|
-
scope:
|
|
1560
|
+
scope: fieldToScope(field.name),
|
|
1561
|
+
...displayNameAnnotation !== void 0 && { label: displayNameAnnotation.value },
|
|
1562
|
+
...parentRule !== void 0 && { rule: parentRule }
|
|
1129
1563
|
};
|
|
1130
|
-
if (field.label !== void 0) {
|
|
1131
|
-
control.label = field.label;
|
|
1132
|
-
}
|
|
1133
|
-
if (field.showWhen !== void 0 && typeof field.showWhen === "object" && "field" in field.showWhen && "value" in field.showWhen) {
|
|
1134
|
-
const sw = field.showWhen;
|
|
1135
|
-
control.rule = {
|
|
1136
|
-
effect: "SHOW",
|
|
1137
|
-
condition: {
|
|
1138
|
-
scope: `#/properties/${sw.field}`,
|
|
1139
|
-
schema: { const: sw.value }
|
|
1140
|
-
}
|
|
1141
|
-
};
|
|
1142
|
-
}
|
|
1143
1564
|
return control;
|
|
1144
1565
|
}
|
|
1145
|
-
function
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1566
|
+
function groupNodeToLayout(group, parentRule) {
|
|
1567
|
+
return {
|
|
1568
|
+
type: "Group",
|
|
1569
|
+
label: group.label,
|
|
1570
|
+
elements: irElementsToUiSchema(group.elements, parentRule),
|
|
1571
|
+
...parentRule !== void 0 && { rule: parentRule }
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
function irElementsToUiSchema(elements, parentRule) {
|
|
1575
|
+
const result = [];
|
|
1576
|
+
for (const element of elements) {
|
|
1577
|
+
switch (element.kind) {
|
|
1578
|
+
case "field": {
|
|
1579
|
+
result.push(fieldNodeToControl(element, parentRule));
|
|
1580
|
+
break;
|
|
1155
1581
|
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1582
|
+
case "group": {
|
|
1583
|
+
result.push(groupNodeToLayout(element, parentRule));
|
|
1584
|
+
break;
|
|
1585
|
+
}
|
|
1586
|
+
case "conditional": {
|
|
1587
|
+
const newRule = createShowRule(element.fieldName, element.value);
|
|
1588
|
+
const combinedRule = parentRule !== void 0 ? combineRules(parentRule, newRule) : newRule;
|
|
1589
|
+
const childElements = irElementsToUiSchema(element.elements, combinedRule);
|
|
1590
|
+
result.push(...childElements);
|
|
1591
|
+
break;
|
|
1592
|
+
}
|
|
1593
|
+
default: {
|
|
1594
|
+
const _exhaustive = element;
|
|
1595
|
+
void _exhaustive;
|
|
1596
|
+
throw new Error("Unhandled IR element kind");
|
|
1169
1597
|
}
|
|
1170
|
-
} else {
|
|
1171
|
-
const groupElements = groupMap.get(key) ?? [];
|
|
1172
|
-
const groupLayout = {
|
|
1173
|
-
type: "Group",
|
|
1174
|
-
label: key,
|
|
1175
|
-
elements: groupElements
|
|
1176
|
-
};
|
|
1177
|
-
elements.push(groupLayout);
|
|
1178
1598
|
}
|
|
1179
1599
|
}
|
|
1600
|
+
return result;
|
|
1601
|
+
}
|
|
1602
|
+
function generateUiSchemaFromIR(ir) {
|
|
1180
1603
|
const result = {
|
|
1181
1604
|
type: "VerticalLayout",
|
|
1182
|
-
elements
|
|
1605
|
+
elements: irElementsToUiSchema(ir.elements)
|
|
1183
1606
|
};
|
|
1184
1607
|
return parseOrThrow(uiSchema, result, "UI Schema");
|
|
1185
1608
|
}
|
|
1186
1609
|
|
|
1187
1610
|
// src/generators/class-schema.ts
|
|
1188
|
-
function generateClassSchemas(analysis,
|
|
1189
|
-
const
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1611
|
+
function generateClassSchemas(analysis, source) {
|
|
1612
|
+
const ir = canonicalizeTSDoc(analysis, source);
|
|
1613
|
+
return {
|
|
1614
|
+
jsonSchema: generateJsonSchemaFromIR(ir),
|
|
1615
|
+
uiSchema: generateUiSchemaFromIR(ir)
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
// src/validate/constraint-validator.ts
|
|
1620
|
+
function makeCode(ctx, category, number) {
|
|
1621
|
+
return `${ctx.vendorPrefix}-${category}-${String(number).padStart(3, "0")}`;
|
|
1622
|
+
}
|
|
1623
|
+
function addContradiction(ctx, message, primary, related) {
|
|
1624
|
+
ctx.diagnostics.push({
|
|
1625
|
+
code: makeCode(ctx, "CONTRADICTION", 1),
|
|
1626
|
+
message,
|
|
1627
|
+
severity: "error",
|
|
1628
|
+
primaryLocation: primary,
|
|
1629
|
+
relatedLocations: [related]
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
function addTypeMismatch(ctx, message, primary) {
|
|
1633
|
+
ctx.diagnostics.push({
|
|
1634
|
+
code: makeCode(ctx, "TYPE_MISMATCH", 1),
|
|
1635
|
+
message,
|
|
1636
|
+
severity: "error",
|
|
1637
|
+
primaryLocation: primary,
|
|
1638
|
+
relatedLocations: []
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
function addUnknownExtension(ctx, message, primary) {
|
|
1642
|
+
ctx.diagnostics.push({
|
|
1643
|
+
code: makeCode(ctx, "UNKNOWN_EXTENSION", 1),
|
|
1644
|
+
message,
|
|
1645
|
+
severity: "warning",
|
|
1646
|
+
primaryLocation: primary,
|
|
1647
|
+
relatedLocations: []
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
function findNumeric(constraints, constraintKind) {
|
|
1651
|
+
return constraints.find(
|
|
1652
|
+
(c) => c.constraintKind === constraintKind
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
function findLength(constraints, constraintKind) {
|
|
1656
|
+
return constraints.find(
|
|
1657
|
+
(c) => c.constraintKind === constraintKind
|
|
1658
|
+
);
|
|
1659
|
+
}
|
|
1660
|
+
function findAllowedMembers(constraints) {
|
|
1661
|
+
return constraints.filter(
|
|
1662
|
+
(c) => c.constraintKind === "allowedMembers"
|
|
1663
|
+
);
|
|
1664
|
+
}
|
|
1665
|
+
function checkNumericContradictions(ctx, fieldName, constraints) {
|
|
1666
|
+
const min = findNumeric(constraints, "minimum");
|
|
1667
|
+
const max = findNumeric(constraints, "maximum");
|
|
1668
|
+
const exMin = findNumeric(constraints, "exclusiveMinimum");
|
|
1669
|
+
const exMax = findNumeric(constraints, "exclusiveMaximum");
|
|
1670
|
+
if (min !== void 0 && max !== void 0 && min.value > max.value) {
|
|
1671
|
+
addContradiction(
|
|
1672
|
+
ctx,
|
|
1673
|
+
`Field "${fieldName}": minimum (${String(min.value)}) is greater than maximum (${String(max.value)})`,
|
|
1674
|
+
min.provenance,
|
|
1675
|
+
max.provenance
|
|
1676
|
+
);
|
|
1677
|
+
}
|
|
1678
|
+
if (exMin !== void 0 && max !== void 0 && exMin.value >= max.value) {
|
|
1679
|
+
addContradiction(
|
|
1680
|
+
ctx,
|
|
1681
|
+
`Field "${fieldName}": exclusiveMinimum (${String(exMin.value)}) is greater than or equal to maximum (${String(max.value)})`,
|
|
1682
|
+
exMin.provenance,
|
|
1683
|
+
max.provenance
|
|
1684
|
+
);
|
|
1685
|
+
}
|
|
1686
|
+
if (min !== void 0 && exMax !== void 0 && min.value >= exMax.value) {
|
|
1687
|
+
addContradiction(
|
|
1688
|
+
ctx,
|
|
1689
|
+
`Field "${fieldName}": minimum (${String(min.value)}) is greater than or equal to exclusiveMaximum (${String(exMax.value)})`,
|
|
1690
|
+
min.provenance,
|
|
1691
|
+
exMax.provenance
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
if (exMin !== void 0 && exMax !== void 0 && exMin.value >= exMax.value) {
|
|
1695
|
+
addContradiction(
|
|
1696
|
+
ctx,
|
|
1697
|
+
`Field "${fieldName}": exclusiveMinimum (${String(exMin.value)}) is greater than or equal to exclusiveMaximum (${String(exMax.value)})`,
|
|
1698
|
+
exMin.provenance,
|
|
1699
|
+
exMax.provenance
|
|
1700
|
+
);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
function checkLengthContradictions(ctx, fieldName, constraints) {
|
|
1704
|
+
const minLen = findLength(constraints, "minLength");
|
|
1705
|
+
const maxLen = findLength(constraints, "maxLength");
|
|
1706
|
+
if (minLen !== void 0 && maxLen !== void 0 && minLen.value > maxLen.value) {
|
|
1707
|
+
addContradiction(
|
|
1708
|
+
ctx,
|
|
1709
|
+
`Field "${fieldName}": minLength (${String(minLen.value)}) is greater than maxLength (${String(maxLen.value)})`,
|
|
1710
|
+
minLen.provenance,
|
|
1711
|
+
maxLen.provenance
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1714
|
+
const minItems = findLength(constraints, "minItems");
|
|
1715
|
+
const maxItems = findLength(constraints, "maxItems");
|
|
1716
|
+
if (minItems !== void 0 && maxItems !== void 0 && minItems.value > maxItems.value) {
|
|
1717
|
+
addContradiction(
|
|
1718
|
+
ctx,
|
|
1719
|
+
`Field "${fieldName}": minItems (${String(minItems.value)}) is greater than maxItems (${String(maxItems.value)})`,
|
|
1720
|
+
minItems.provenance,
|
|
1721
|
+
maxItems.provenance
|
|
1722
|
+
);
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
function checkAllowedMembersContradiction(ctx, fieldName, constraints) {
|
|
1726
|
+
const members = findAllowedMembers(constraints);
|
|
1727
|
+
if (members.length < 2) return;
|
|
1728
|
+
const firstSet = new Set(members[0]?.members ?? []);
|
|
1729
|
+
for (let i = 1; i < members.length; i++) {
|
|
1730
|
+
const current = members[i];
|
|
1731
|
+
if (current === void 0) continue;
|
|
1732
|
+
for (const m of firstSet) {
|
|
1733
|
+
if (!current.members.includes(m)) {
|
|
1734
|
+
firstSet.delete(m);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
if (firstSet.size === 0) {
|
|
1739
|
+
const first = members[0];
|
|
1740
|
+
const second = members[1];
|
|
1741
|
+
if (first !== void 0 && second !== void 0) {
|
|
1742
|
+
addContradiction(
|
|
1743
|
+
ctx,
|
|
1744
|
+
`Field "${fieldName}": allowedMembers constraints have an empty intersection (no valid values remain)`,
|
|
1745
|
+
first.provenance,
|
|
1746
|
+
second.provenance
|
|
1747
|
+
);
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
function typeLabel(type) {
|
|
1752
|
+
switch (type.kind) {
|
|
1753
|
+
case "primitive":
|
|
1754
|
+
return type.primitiveKind;
|
|
1755
|
+
case "enum":
|
|
1756
|
+
return "enum";
|
|
1757
|
+
case "array":
|
|
1758
|
+
return "array";
|
|
1759
|
+
case "object":
|
|
1760
|
+
return "object";
|
|
1761
|
+
case "union":
|
|
1762
|
+
return "union";
|
|
1763
|
+
case "reference":
|
|
1764
|
+
return `reference(${type.name})`;
|
|
1765
|
+
case "dynamic":
|
|
1766
|
+
return `dynamic(${type.dynamicKind})`;
|
|
1767
|
+
case "custom":
|
|
1768
|
+
return `custom(${type.typeId})`;
|
|
1769
|
+
default: {
|
|
1770
|
+
const _exhaustive = type;
|
|
1771
|
+
return String(_exhaustive);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
function checkTypeApplicability(ctx, fieldName, type, constraints) {
|
|
1776
|
+
const isNumber = type.kind === "primitive" && type.primitiveKind === "number";
|
|
1777
|
+
const isString = type.kind === "primitive" && type.primitiveKind === "string";
|
|
1778
|
+
const isArray = type.kind === "array";
|
|
1779
|
+
const isEnum = type.kind === "enum";
|
|
1780
|
+
const label = typeLabel(type);
|
|
1781
|
+
for (const constraint of constraints) {
|
|
1782
|
+
const ck = constraint.constraintKind;
|
|
1783
|
+
switch (ck) {
|
|
1784
|
+
case "minimum":
|
|
1785
|
+
case "maximum":
|
|
1786
|
+
case "exclusiveMinimum":
|
|
1787
|
+
case "exclusiveMaximum":
|
|
1788
|
+
case "multipleOf": {
|
|
1789
|
+
if (!isNumber) {
|
|
1790
|
+
addTypeMismatch(
|
|
1791
|
+
ctx,
|
|
1792
|
+
`Field "${fieldName}": constraint "${ck}" is only valid on number fields, but field type is "${label}"`,
|
|
1793
|
+
constraint.provenance
|
|
1794
|
+
);
|
|
1795
|
+
}
|
|
1796
|
+
break;
|
|
1797
|
+
}
|
|
1798
|
+
case "minLength":
|
|
1799
|
+
case "maxLength":
|
|
1800
|
+
case "pattern": {
|
|
1801
|
+
if (!isString) {
|
|
1802
|
+
addTypeMismatch(
|
|
1803
|
+
ctx,
|
|
1804
|
+
`Field "${fieldName}": constraint "${ck}" is only valid on string fields, but field type is "${label}"`,
|
|
1805
|
+
constraint.provenance
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
break;
|
|
1809
|
+
}
|
|
1810
|
+
case "minItems":
|
|
1811
|
+
case "maxItems":
|
|
1812
|
+
case "uniqueItems": {
|
|
1813
|
+
if (!isArray) {
|
|
1814
|
+
addTypeMismatch(
|
|
1815
|
+
ctx,
|
|
1816
|
+
`Field "${fieldName}": constraint "${ck}" is only valid on array fields, but field type is "${label}"`,
|
|
1817
|
+
constraint.provenance
|
|
1818
|
+
);
|
|
1819
|
+
}
|
|
1820
|
+
break;
|
|
1821
|
+
}
|
|
1822
|
+
case "allowedMembers": {
|
|
1823
|
+
if (!isEnum) {
|
|
1824
|
+
addTypeMismatch(
|
|
1825
|
+
ctx,
|
|
1826
|
+
`Field "${fieldName}": constraint "allowedMembers" is only valid on enum fields, but field type is "${label}"`,
|
|
1827
|
+
constraint.provenance
|
|
1828
|
+
);
|
|
1829
|
+
}
|
|
1830
|
+
break;
|
|
1831
|
+
}
|
|
1832
|
+
case "custom": {
|
|
1833
|
+
checkCustomConstraint(ctx, fieldName, type, constraint);
|
|
1834
|
+
break;
|
|
1835
|
+
}
|
|
1836
|
+
default: {
|
|
1837
|
+
const _exhaustive = constraint;
|
|
1838
|
+
throw new Error(
|
|
1839
|
+
`Unhandled constraint kind: ${_exhaustive.constraintKind}`
|
|
1840
|
+
);
|
|
1841
|
+
}
|
|
1198
1842
|
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
function checkCustomConstraint(ctx, fieldName, type, constraint) {
|
|
1846
|
+
if (ctx.extensionRegistry === void 0) return;
|
|
1847
|
+
const registration = ctx.extensionRegistry.findConstraint(constraint.constraintId);
|
|
1848
|
+
if (registration === void 0) {
|
|
1849
|
+
addUnknownExtension(
|
|
1850
|
+
ctx,
|
|
1851
|
+
`Field "${fieldName}": custom constraint "${constraint.constraintId}" is not registered in the extension registry`,
|
|
1852
|
+
constraint.provenance
|
|
1853
|
+
);
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
if (registration.applicableTypes === null) return;
|
|
1857
|
+
if (!registration.applicableTypes.includes(type.kind)) {
|
|
1858
|
+
addTypeMismatch(
|
|
1859
|
+
ctx,
|
|
1860
|
+
`Field "${fieldName}": custom constraint "${constraint.constraintId}" is not applicable to type "${typeLabel(type)}"`,
|
|
1861
|
+
constraint.provenance
|
|
1205
1862
|
);
|
|
1206
|
-
uiElements.push(formSpecField);
|
|
1207
1863
|
}
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1864
|
+
}
|
|
1865
|
+
function validateFieldNode(ctx, field) {
|
|
1866
|
+
validateConstraints(ctx, field.name, field.type, field.constraints);
|
|
1867
|
+
if (field.type.kind === "object") {
|
|
1868
|
+
for (const prop of field.type.properties) {
|
|
1869
|
+
validateObjectProperty(ctx, field.name, prop);
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
function validateObjectProperty(ctx, parentName, prop) {
|
|
1874
|
+
const qualifiedName = `${parentName}.${prop.name}`;
|
|
1875
|
+
validateConstraints(ctx, qualifiedName, prop.type, prop.constraints);
|
|
1876
|
+
if (prop.type.kind === "object") {
|
|
1877
|
+
for (const nestedProp of prop.type.properties) {
|
|
1878
|
+
validateObjectProperty(ctx, qualifiedName, nestedProp);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
function validateConstraints(ctx, name, type, constraints) {
|
|
1883
|
+
checkNumericContradictions(ctx, name, constraints);
|
|
1884
|
+
checkLengthContradictions(ctx, name, constraints);
|
|
1885
|
+
checkAllowedMembersContradiction(ctx, name, constraints);
|
|
1886
|
+
checkTypeApplicability(ctx, name, type, constraints);
|
|
1887
|
+
}
|
|
1888
|
+
function validateElement(ctx, element) {
|
|
1889
|
+
switch (element.kind) {
|
|
1890
|
+
case "field":
|
|
1891
|
+
validateFieldNode(ctx, element);
|
|
1892
|
+
break;
|
|
1893
|
+
case "group":
|
|
1894
|
+
for (const child of element.elements) {
|
|
1895
|
+
validateElement(ctx, child);
|
|
1896
|
+
}
|
|
1897
|
+
break;
|
|
1898
|
+
case "conditional":
|
|
1899
|
+
for (const child of element.elements) {
|
|
1900
|
+
validateElement(ctx, child);
|
|
1901
|
+
}
|
|
1902
|
+
break;
|
|
1903
|
+
default: {
|
|
1904
|
+
const _exhaustive = element;
|
|
1905
|
+
throw new Error(`Unhandled element kind: ${_exhaustive.kind}`);
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
function validateIR(ir, options) {
|
|
1910
|
+
const ctx = {
|
|
1911
|
+
diagnostics: [],
|
|
1912
|
+
vendorPrefix: options?.vendorPrefix ?? "FORMSPEC",
|
|
1913
|
+
extensionRegistry: options?.extensionRegistry
|
|
1914
|
+
};
|
|
1915
|
+
for (const element of ir.elements) {
|
|
1916
|
+
validateElement(ctx, element);
|
|
1917
|
+
}
|
|
1918
|
+
return {
|
|
1919
|
+
diagnostics: ctx.diagnostics,
|
|
1920
|
+
valid: ctx.diagnostics.every((d) => d.severity !== "error")
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
// src/extensions/registry.ts
|
|
1925
|
+
function createExtensionRegistry(extensions) {
|
|
1926
|
+
const typeMap = /* @__PURE__ */ new Map();
|
|
1927
|
+
const constraintMap = /* @__PURE__ */ new Map();
|
|
1928
|
+
const annotationMap = /* @__PURE__ */ new Map();
|
|
1929
|
+
for (const ext of extensions) {
|
|
1930
|
+
if (ext.types !== void 0) {
|
|
1931
|
+
for (const type of ext.types) {
|
|
1932
|
+
const qualifiedId = `${ext.extensionId}/${type.typeName}`;
|
|
1933
|
+
if (typeMap.has(qualifiedId)) {
|
|
1934
|
+
throw new Error(`Duplicate custom type ID: "${qualifiedId}"`);
|
|
1935
|
+
}
|
|
1936
|
+
typeMap.set(qualifiedId, type);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
if (ext.constraints !== void 0) {
|
|
1940
|
+
for (const constraint of ext.constraints) {
|
|
1941
|
+
const qualifiedId = `${ext.extensionId}/${constraint.constraintName}`;
|
|
1942
|
+
if (constraintMap.has(qualifiedId)) {
|
|
1943
|
+
throw new Error(`Duplicate custom constraint ID: "${qualifiedId}"`);
|
|
1944
|
+
}
|
|
1945
|
+
constraintMap.set(qualifiedId, constraint);
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
if (ext.annotations !== void 0) {
|
|
1949
|
+
for (const annotation of ext.annotations) {
|
|
1950
|
+
const qualifiedId = `${ext.extensionId}/${annotation.annotationName}`;
|
|
1951
|
+
if (annotationMap.has(qualifiedId)) {
|
|
1952
|
+
throw new Error(`Duplicate custom annotation ID: "${qualifiedId}"`);
|
|
1953
|
+
}
|
|
1954
|
+
annotationMap.set(qualifiedId, annotation);
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
return {
|
|
1959
|
+
extensions,
|
|
1960
|
+
findType: (typeId) => typeMap.get(typeId),
|
|
1961
|
+
findConstraint: (constraintId) => constraintMap.get(constraintId),
|
|
1962
|
+
findAnnotation: (annotationId) => annotationMap.get(annotationId)
|
|
1212
1963
|
};
|
|
1213
|
-
const uiSchema2 = generateUiSchemaFromFields(uiElements);
|
|
1214
|
-
return { jsonSchema, uiSchema: uiSchema2 };
|
|
1215
1964
|
}
|
|
1216
1965
|
|
|
1217
1966
|
// src/generators/method-schema.ts
|
|
1967
|
+
import { IR_VERSION as IR_VERSION3 } from "@formspec/core";
|
|
1968
|
+
function typeToJsonSchema(type, checker) {
|
|
1969
|
+
const typeRegistry = {};
|
|
1970
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
1971
|
+
const typeNode = resolveTypeNode(type, checker, "", typeRegistry, visiting);
|
|
1972
|
+
const fieldProvenance = { surface: "tsdoc", file: "", line: 0, column: 0 };
|
|
1973
|
+
const ir = {
|
|
1974
|
+
kind: "form-ir",
|
|
1975
|
+
irVersion: IR_VERSION3,
|
|
1976
|
+
elements: [
|
|
1977
|
+
{
|
|
1978
|
+
kind: "field",
|
|
1979
|
+
name: "__result",
|
|
1980
|
+
type: typeNode,
|
|
1981
|
+
required: true,
|
|
1982
|
+
constraints: [],
|
|
1983
|
+
annotations: [],
|
|
1984
|
+
provenance: fieldProvenance
|
|
1985
|
+
}
|
|
1986
|
+
],
|
|
1987
|
+
typeRegistry,
|
|
1988
|
+
provenance: fieldProvenance
|
|
1989
|
+
};
|
|
1990
|
+
const schema = generateJsonSchemaFromIR(ir);
|
|
1991
|
+
const fieldSchema = schema.properties?.["__result"];
|
|
1992
|
+
if (fieldSchema) {
|
|
1993
|
+
if (schema.$defs && Object.keys(schema.$defs).length > 0) {
|
|
1994
|
+
return { ...fieldSchema, $defs: schema.$defs };
|
|
1995
|
+
}
|
|
1996
|
+
return fieldSchema;
|
|
1997
|
+
}
|
|
1998
|
+
return { type: "object" };
|
|
1999
|
+
}
|
|
1218
2000
|
function generateMethodSchemas(method, checker, loadedFormSpecs) {
|
|
1219
|
-
const returnType =
|
|
2001
|
+
const returnType = typeToJsonSchema(method.returnType, checker);
|
|
1220
2002
|
const params = generateParamsSchemas(method.parameters, checker, loadedFormSpecs);
|
|
1221
2003
|
return {
|
|
1222
2004
|
name: method.name,
|
|
@@ -1245,7 +2027,7 @@ function generateParamsSchemas(parameters, checker, loadedFormSpecs) {
|
|
|
1245
2027
|
}
|
|
1246
2028
|
if (parameters.length === 1 && parameters[0]) {
|
|
1247
2029
|
const param = parameters[0];
|
|
1248
|
-
const jsonSchema =
|
|
2030
|
+
const jsonSchema = typeToJsonSchema(param.type, checker);
|
|
1249
2031
|
return {
|
|
1250
2032
|
jsonSchema,
|
|
1251
2033
|
uiSchema: null,
|
|
@@ -1255,7 +2037,7 @@ function generateParamsSchemas(parameters, checker, loadedFormSpecs) {
|
|
|
1255
2037
|
const properties = {};
|
|
1256
2038
|
const required = [];
|
|
1257
2039
|
for (const param of parameters) {
|
|
1258
|
-
const paramSchema =
|
|
2040
|
+
const paramSchema = typeToJsonSchema(param.type, checker);
|
|
1259
2041
|
properties[param.name] = paramSchema;
|
|
1260
2042
|
if (!param.optional) {
|
|
1261
2043
|
required.push(param.name);
|
|
@@ -1283,16 +2065,21 @@ function collectFormSpecReferences(methods) {
|
|
|
1283
2065
|
return references;
|
|
1284
2066
|
}
|
|
1285
2067
|
export {
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
2068
|
+
analyzeClassToIR,
|
|
2069
|
+
analyzeInterfaceToIR,
|
|
2070
|
+
analyzeTypeAliasToIR,
|
|
2071
|
+
canonicalizeChainDSL,
|
|
2072
|
+
canonicalizeTSDoc,
|
|
1289
2073
|
collectFormSpecReferences,
|
|
2074
|
+
createExtensionRegistry,
|
|
1290
2075
|
createProgramContext,
|
|
1291
2076
|
findClassByName,
|
|
1292
2077
|
findInterfaceByName,
|
|
1293
2078
|
findTypeAliasByName,
|
|
1294
2079
|
generateClassSchemas,
|
|
2080
|
+
generateJsonSchemaFromIR,
|
|
1295
2081
|
generateMethodSchemas,
|
|
1296
|
-
|
|
2082
|
+
generateUiSchemaFromIR,
|
|
2083
|
+
validateIR
|
|
1297
2084
|
};
|
|
1298
2085
|
//# sourceMappingURL=internals.js.map
|