@shapeshift-labs/frontier-lang-parser 0.3.60 → 0.3.61
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.
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
import { readActionSyntaxChildren } from './action-syntax-children.js';
|
|
2
2
|
import { ROW_SYNTAX_CONFIG } from './source-syntax-row-config.js';
|
|
3
|
+
import { readTypeExpressionSyntaxChildren } from './source-syntax-type-expressions.js';
|
|
4
|
+
import { inspectTypeExpressionSyntax } from './type-expressions.js';
|
|
3
5
|
import { inspectVariantPayload } from './type-variants.js';
|
|
4
6
|
|
|
5
7
|
const ROW_NAME_PATTERN = '([A-Za-z_$@./:*+-][\\w$./@:*+-]*)';
|
|
6
8
|
|
|
7
9
|
export function readSourceSyntaxChildren(source, block, options = {}) {
|
|
10
|
+
let children = [];
|
|
8
11
|
if (block.malformed) return [];
|
|
9
12
|
if (block.kind === 'action') {
|
|
10
|
-
|
|
13
|
+
children = readActionSyntaxChildren(source, block, options);
|
|
14
|
+
} else if (block.kind === 'conversion' || block.kind === 'universalConversionPlan') {
|
|
15
|
+
children = readConversionSyntaxChildren(source, block, options);
|
|
16
|
+
} else if (block.kind === 'type') {
|
|
17
|
+
children = readTypeSyntaxChildren(source, block, options);
|
|
18
|
+
} else {
|
|
19
|
+
const rowConfig = ROW_SYNTAX_CONFIG[block.kind];
|
|
20
|
+
if (rowConfig) children = readGenericRowSyntaxChildren(source, block, options, rowConfig);
|
|
11
21
|
}
|
|
12
|
-
|
|
13
|
-
return readConversionSyntaxChildren(source, block, options);
|
|
14
|
-
}
|
|
15
|
-
if (block.kind === 'type') {
|
|
16
|
-
return readTypeSyntaxChildren(source, block, options);
|
|
17
|
-
}
|
|
18
|
-
const rowConfig = ROW_SYNTAX_CONFIG[block.kind];
|
|
19
|
-
if (rowConfig) return readGenericRowSyntaxChildren(source, block, options, rowConfig);
|
|
20
|
-
return [];
|
|
22
|
+
return [...children, ...readTypeExpressionSyntaxChildren(source, block, options)];
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
function readTypeSyntaxChildren(source, block, options) {
|
|
@@ -27,7 +29,7 @@ function readTypeSyntaxChildren(source, block, options) {
|
|
|
27
29
|
const variant = /^variant\s+([A-Za-z_$][\w$]*)(.*)$/.exec(line.text);
|
|
28
30
|
if (!variant) continue;
|
|
29
31
|
const [, name, rest] = variant;
|
|
30
|
-
const payload = inspectVariantPayload(rest ?? '');
|
|
32
|
+
const payload = inspectVariantPayload(rest ?? '', inspectTypeExpressionSyntax);
|
|
31
33
|
children.push(cleanRecord({
|
|
32
34
|
kind: 'typeVariant',
|
|
33
35
|
rowKind: 'variant',
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { inspectTypeExpressionSyntax } from './type-expressions.js';
|
|
2
|
+
|
|
3
|
+
export function readTypeExpressionSyntaxChildren(source, block, options = {}) {
|
|
4
|
+
const children = [];
|
|
5
|
+
for (const line of readBodyLines(source, block)) {
|
|
6
|
+
if (!line.text || line.text.startsWith('#')) continue;
|
|
7
|
+
for (const candidate of readTypeExpressionCandidates(block, line.text)) {
|
|
8
|
+
const inspected = inspectTypeExpressionSyntax(candidate.typeSource);
|
|
9
|
+
if (inspected.ok) continue;
|
|
10
|
+
children.push(cleanRecord({
|
|
11
|
+
kind: 'typeExpressionSyntax',
|
|
12
|
+
rowKind: candidate.rowKind,
|
|
13
|
+
normalizedRowKind: candidate.rowKind,
|
|
14
|
+
name: candidate.name,
|
|
15
|
+
id: `${block.id ?? block.kind}_${candidate.rowKind}_${safeId(candidate.name)}_${line.startOffset}`,
|
|
16
|
+
header: line.text,
|
|
17
|
+
startOffset: line.startOffset,
|
|
18
|
+
endOffset: line.endOffset,
|
|
19
|
+
location: sourcePosition(source, line.startOffset),
|
|
20
|
+
parentKind: block.kind,
|
|
21
|
+
parentId: block.id,
|
|
22
|
+
parentName: block.name,
|
|
23
|
+
moduleId: block.moduleId,
|
|
24
|
+
moduleName: block.moduleName,
|
|
25
|
+
sourceSpan: sourceSpan(source, block, line.startOffset, line.endOffset, options),
|
|
26
|
+
recognized: false,
|
|
27
|
+
reason: inspected.reason,
|
|
28
|
+
typeSource: candidate.typeSource
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return children;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readTypeExpressionCandidates(block, text) {
|
|
36
|
+
if (block.kind === 'type') return readTypeBlockTypeExpressionCandidates(block, text);
|
|
37
|
+
if (block.kind === 'entity') return readNamedTypeFieldCandidate(text, 'field', { stopAtMetadata: true });
|
|
38
|
+
if (block.kind === 'state') return readNamedTypeFieldCandidate(text, 'collection', { stopAtMetadata: true });
|
|
39
|
+
if (block.kind === 'view') return readViewTypeExpressionCandidates(text);
|
|
40
|
+
if (block.kind === 'action' || block.kind === 'effect' || block.kind === 'capability' || block.kind === 'extern') {
|
|
41
|
+
return readSignatureTypeExpressionCandidates(text);
|
|
42
|
+
}
|
|
43
|
+
if (block.kind === 'lattice') return readCarrierTypeExpressionCandidate(text);
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readTypeBlockTypeExpressionCandidates(block, text) {
|
|
48
|
+
const alias = /^=\s*(.+)$/.exec(text);
|
|
49
|
+
if (alias) return [{ rowKind: 'typeAlias', name: block.name ?? 'alias', typeSource: alias[1].trim() }];
|
|
50
|
+
if (text.startsWith('variant ')) return [];
|
|
51
|
+
return readNamedTypeFieldCandidate(text, 'typeField');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readViewTypeExpressionCandidates(text) {
|
|
55
|
+
const prop = /^prop\s+([A-Za-z_$][\w$.-]*)(?:\s+@id\(\s*["'][^"']+["']\s*\))?\s*:\s*(.+)$/.exec(text);
|
|
56
|
+
if (prop) {
|
|
57
|
+
const typeSource = prop[2].replace(/\s+optional\s*$/, '').trim();
|
|
58
|
+
return typeSource ? [{ rowKind: 'viewProp', name: prop[1], typeSource }] : [];
|
|
59
|
+
}
|
|
60
|
+
const event = /^event\s+([A-Za-z_$][\w$.-]*)(?:\s+@id\(\s*["'][^"']+["']\s*\))?(.*)$/.exec(text);
|
|
61
|
+
if (!event) return [];
|
|
62
|
+
const input = readInlineType('input', event[2]);
|
|
63
|
+
return input ? [{ rowKind: 'viewEventInput', name: event[1], typeSource: input }] : [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readSignatureTypeExpressionCandidates(text) {
|
|
67
|
+
const input = /^input\s*:?\s*(.+)$/.exec(text);
|
|
68
|
+
if (input) return [{ rowKind: 'input', name: 'input', typeSource: input[1].trim() }];
|
|
69
|
+
const returns = /^returns\s+(.+)$/.exec(text);
|
|
70
|
+
if (returns) return [{ rowKind: 'returns', name: 'returns', typeSource: returns[1].trim() }];
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function readCarrierTypeExpressionCandidate(text) {
|
|
75
|
+
const carrier = /^carrier\s+(.+)$/.exec(text);
|
|
76
|
+
return carrier ? [{ rowKind: 'carrier', name: 'carrier', typeSource: carrier[1].trim() }] : [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function readNamedTypeFieldCandidate(text, rowKind, options = {}) {
|
|
80
|
+
const field = /^([A-Za-z_$][\w$]*)(?:\s+@id\(\s*["'][^"']+["']\s*\))?\s*:\s*(.+)$/.exec(text);
|
|
81
|
+
if (!field) return [];
|
|
82
|
+
const typeSource = options.stopAtMetadata ? readTypeSourceBeforeMetadata(field[2]) : field[2].trim();
|
|
83
|
+
return typeSource ? [{ rowKind, name: field[1], typeSource }] : [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function readTypeSourceBeforeMetadata(source) {
|
|
87
|
+
let stack = [];
|
|
88
|
+
let quote = '';
|
|
89
|
+
let escaped = false;
|
|
90
|
+
for (let index = 0; index < source.length; index++) {
|
|
91
|
+
const char = source[index];
|
|
92
|
+
if (quote) {
|
|
93
|
+
if (escaped) {
|
|
94
|
+
escaped = false;
|
|
95
|
+
} else if (char === '\\') {
|
|
96
|
+
escaped = true;
|
|
97
|
+
} else if (char === quote) {
|
|
98
|
+
quote = '';
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (char === '"' || char === "'") {
|
|
103
|
+
quote = char;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (stack.length === 0 && char === '{') return source.slice(0, index).trim();
|
|
107
|
+
if (char === '<' || char === '(' || char === '[' || char === '{') {
|
|
108
|
+
stack.push(closingDelimiterFor(char));
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (char === '>' || char === ')' || char === ']' || char === '}') {
|
|
112
|
+
if (stack[stack.length - 1] === char) stack = stack.slice(0, -1);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (stack.length === 0 && /\s/.test(char) && source.slice(index).trimStart().startsWith('@')) {
|
|
116
|
+
return source.slice(0, index).trim();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return source.trim();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function readBodyLines(source, block) {
|
|
123
|
+
return readTextLines(source, block.bodyStartOffset, block.bodyEndOffset);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function readTextLines(source, startOffset, endOffset) {
|
|
127
|
+
const body = source.slice(startOffset, endOffset);
|
|
128
|
+
const lines = body.split('\n');
|
|
129
|
+
const records = [];
|
|
130
|
+
let lineStart = startOffset;
|
|
131
|
+
for (const rawLine of lines) {
|
|
132
|
+
const rawEnd = lineStart + rawLine.length;
|
|
133
|
+
const leading = /^\s*/.exec(rawLine)?.[0].length ?? 0;
|
|
134
|
+
const trailing = /\s*$/.exec(rawLine)?.[0].length ?? 0;
|
|
135
|
+
const startOffset = lineStart + leading;
|
|
136
|
+
const endOffset = Math.max(startOffset, rawEnd - trailing);
|
|
137
|
+
records.push({ text: rawLine.trim(), startOffset, endOffset });
|
|
138
|
+
lineStart = rawEnd + 1;
|
|
139
|
+
}
|
|
140
|
+
return records;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function sourcePosition(source, offset) {
|
|
144
|
+
const lines = source.slice(0, offset).split('\n');
|
|
145
|
+
return { line: lines.length, column: lines[lines.length - 1].length + 1, offset };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function sourceSpan(source, block, startOffset, endOffset, options = {}) {
|
|
149
|
+
return cleanRecord({
|
|
150
|
+
sourceId: options.documentId,
|
|
151
|
+
path: options.sourcePath,
|
|
152
|
+
blockId: block.id,
|
|
153
|
+
blockKind: block.kind,
|
|
154
|
+
startOffset,
|
|
155
|
+
endOffset,
|
|
156
|
+
start: sourcePosition(source, startOffset),
|
|
157
|
+
end: sourcePosition(source, endOffset)
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function readInlineType(label, text = '') { return new RegExp('(?:^|\\s)' + label + '\\s+(.+)$').exec(text)?.[1]?.trim(); }
|
|
162
|
+
function safeId(value) { return String(value).replace(/[^A-Za-z0-9_$-]+/g, '_').replace(/^_+|_+$/g, '') || 'row'; }
|
|
163
|
+
function cleanRecord(record) {
|
|
164
|
+
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined && (!Array.isArray(value) || value.length > 0)));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function closingDelimiterFor(char) {
|
|
168
|
+
if (char === '<') return '>';
|
|
169
|
+
if (char === '(') return ')';
|
|
170
|
+
if (char === '[') return ']';
|
|
171
|
+
return '}';
|
|
172
|
+
}
|
package/dist/type-expressions.js
CHANGED
|
@@ -8,17 +8,38 @@ export function parseTypeExpression(value) {
|
|
|
8
8
|
const text = value.trim();
|
|
9
9
|
const application = readTypeApplication(text);
|
|
10
10
|
if (!application) return text;
|
|
11
|
+
if (application.name === 'Record') return readRecordTypeExpression(application.body) ?? text;
|
|
12
|
+
if (application.name === 'Union') return readUnionTypeExpression(application.body) ?? text;
|
|
11
13
|
const args = splitTopLevelCommaList(application.body);
|
|
12
14
|
if (args.some((arg) => !arg)) return text;
|
|
13
15
|
const parsedArgs = args.map(parseTypeExpression);
|
|
14
16
|
if (application.name === 'Set' && parsedArgs.length === 1) return { kind: 'set', item: parsedArgs[0] };
|
|
15
17
|
if (application.name === 'List' && parsedArgs.length === 1) return { kind: 'list', item: parsedArgs[0] };
|
|
16
18
|
if (application.name === 'Map' && parsedArgs.length === 2) return { kind: 'map', key: parsedArgs[0], value: parsedArgs[1] };
|
|
17
|
-
if (application.name === 'Record') return readRecordTypeExpression(application.body) ?? { kind: 'ref', name: application.name, args: parsedArgs };
|
|
18
|
-
if (application.name === 'Union') return readUnionTypeExpression(application.body) ?? { kind: 'ref', name: application.name, args: parsedArgs };
|
|
19
19
|
return { kind: 'ref', name: application.name, args: parsedArgs };
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
export function inspectTypeExpressionSyntax(value) {
|
|
23
|
+
const text = value.trim();
|
|
24
|
+
if (!text) return { ok: false, reason: 'missing-type-expression' };
|
|
25
|
+
const application = readTypeApplication(text);
|
|
26
|
+
if (!application) {
|
|
27
|
+
return hasReservedStructuralTypeSyntax(text)
|
|
28
|
+
? { ok: false, reason: 'malformed-structural-type-expression' }
|
|
29
|
+
: { ok: true };
|
|
30
|
+
}
|
|
31
|
+
if (application.name === 'Record') return inspectRecordTypeExpression(application.body);
|
|
32
|
+
if (application.name === 'Union') return inspectUnionTypeExpression(application.body);
|
|
33
|
+
if (!hasReservedStructuralTypeSyntax(application.body)) return { ok: true };
|
|
34
|
+
const args = splitTopLevelCommaListStrict(application.body);
|
|
35
|
+
if (!args.ok) return { ok: false, reason: args.reason };
|
|
36
|
+
for (const arg of args.parts) {
|
|
37
|
+
const inspected = inspectTypeExpressionSyntax(arg);
|
|
38
|
+
if (!inspected.ok) return inspected;
|
|
39
|
+
}
|
|
40
|
+
return { ok: true };
|
|
41
|
+
}
|
|
42
|
+
|
|
22
43
|
function readTypeApplication(text) {
|
|
23
44
|
const match = /^([A-Za-z_$][\w$]*)</.exec(text);
|
|
24
45
|
if (!match || !text.endsWith('>')) return undefined;
|
|
@@ -35,34 +56,98 @@ function readTypeApplication(text) {
|
|
|
35
56
|
}
|
|
36
57
|
|
|
37
58
|
function readRecordTypeExpression(body) {
|
|
59
|
+
const split = splitTopLevelCommaListStrict(body);
|
|
60
|
+
if (!split.ok) return undefined;
|
|
38
61
|
const fields = [];
|
|
39
|
-
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
const seenNames = new Set();
|
|
64
|
+
for (const source of split.parts) {
|
|
40
65
|
const field = readInlineTypeField(source, 'record_field');
|
|
41
66
|
if (!field) return undefined;
|
|
67
|
+
if (seen.has(field.id)) return undefined;
|
|
68
|
+
seen.add(field.id);
|
|
69
|
+
if (seenNames.has(field.name)) return undefined;
|
|
70
|
+
seenNames.add(field.name);
|
|
42
71
|
fields.push(field);
|
|
43
72
|
}
|
|
44
73
|
return fields.length ? { kind: 'record', fields } : undefined;
|
|
45
74
|
}
|
|
46
75
|
|
|
47
76
|
function readUnionTypeExpression(body) {
|
|
77
|
+
const split = splitTopLevelCommaListStrict(body);
|
|
78
|
+
if (!split.ok) return undefined;
|
|
48
79
|
const variants = [];
|
|
49
|
-
|
|
80
|
+
const seenNames = new Set();
|
|
81
|
+
const seenIds = new Set();
|
|
82
|
+
for (const source of split.parts) {
|
|
50
83
|
const variant = readInlineTypeVariant(source);
|
|
51
84
|
if (!variant) return undefined;
|
|
85
|
+
if (seenNames.has(variant.name)) return undefined;
|
|
86
|
+
seenNames.add(variant.name);
|
|
87
|
+
if (variant.id) {
|
|
88
|
+
if (seenIds.has(variant.id)) return undefined;
|
|
89
|
+
seenIds.add(variant.id);
|
|
90
|
+
}
|
|
52
91
|
variants.push(variant);
|
|
53
92
|
}
|
|
54
93
|
return variants.length ? { kind: 'union', variants } : undefined;
|
|
55
94
|
}
|
|
56
95
|
|
|
96
|
+
function inspectRecordTypeExpression(body) {
|
|
97
|
+
const split = splitTopLevelCommaListStrict(body);
|
|
98
|
+
if (!split.ok) return { ok: false, reason: split.reason };
|
|
99
|
+
if (!split.parts.length) return { ok: false, reason: 'empty-structural-record-type' };
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
const seenNames = new Set();
|
|
102
|
+
for (const source of split.parts) {
|
|
103
|
+
const field = readInlineTypeField(source, 'record_field');
|
|
104
|
+
if (!field) return { ok: false, reason: 'malformed-structural-record-field' };
|
|
105
|
+
if (seen.has(field.id)) return { ok: false, reason: 'duplicate-structural-record-field-id' };
|
|
106
|
+
seen.add(field.id);
|
|
107
|
+
if (seenNames.has(field.name)) return { ok: false, reason: 'duplicate-structural-record-field-name' };
|
|
108
|
+
seenNames.add(field.name);
|
|
109
|
+
}
|
|
110
|
+
return { ok: true, fieldCount: split.parts.length };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function inspectUnionTypeExpression(body) {
|
|
114
|
+
const split = splitTopLevelCommaListStrict(body);
|
|
115
|
+
if (!split.ok) return { ok: false, reason: split.reason };
|
|
116
|
+
if (!split.parts.length) return { ok: false, reason: 'empty-structural-union-type' };
|
|
117
|
+
const seenNames = new Set();
|
|
118
|
+
const seenIds = new Set();
|
|
119
|
+
for (const source of split.parts) {
|
|
120
|
+
const inspected = inspectInlineTypeVariant(source);
|
|
121
|
+
if (!inspected.ok) return inspected;
|
|
122
|
+
const variant = readInlineTypeVariant(source);
|
|
123
|
+
if (seenNames.has(variant.name)) return { ok: false, reason: 'duplicate-structural-union-variant-name' };
|
|
124
|
+
seenNames.add(variant.name);
|
|
125
|
+
if (variant.id) {
|
|
126
|
+
if (seenIds.has(variant.id)) return { ok: false, reason: 'duplicate-structural-union-variant-id' };
|
|
127
|
+
seenIds.add(variant.id);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return { ok: true, variantCount: split.parts.length };
|
|
131
|
+
}
|
|
132
|
+
|
|
57
133
|
function readInlineTypeVariant(source) {
|
|
58
134
|
const match = /^\s*([A-Za-z_$][\w$]*)(?:\s+@id\(\s*["']([^"']+)["']\s*\))?(?:\((.*)\))?\s*$/.exec(source);
|
|
59
135
|
if (!match) return undefined;
|
|
60
136
|
const payload = match[3]?.trim();
|
|
61
137
|
const fields = [];
|
|
62
|
-
if (
|
|
63
|
-
|
|
138
|
+
if (match[3] !== undefined) {
|
|
139
|
+
if (!payload) return undefined;
|
|
140
|
+
const split = splitTopLevelCommaListStrict(payload);
|
|
141
|
+
if (!split.ok) return undefined;
|
|
142
|
+
const seen = new Set();
|
|
143
|
+
const seenNames = new Set();
|
|
144
|
+
for (const fieldSource of split.parts) {
|
|
64
145
|
const field = readInlineTypeField(fieldSource, `variant_field_${match[1]}`);
|
|
65
146
|
if (!field) return undefined;
|
|
147
|
+
if (seen.has(field.id)) return undefined;
|
|
148
|
+
seen.add(field.id);
|
|
149
|
+
if (seenNames.has(field.name)) return undefined;
|
|
150
|
+
seenNames.add(field.name);
|
|
66
151
|
fields.push(field);
|
|
67
152
|
}
|
|
68
153
|
}
|
|
@@ -73,14 +158,92 @@ function readInlineTypeVariant(source) {
|
|
|
73
158
|
};
|
|
74
159
|
}
|
|
75
160
|
|
|
161
|
+
function inspectInlineTypeVariant(source) {
|
|
162
|
+
const match = /^\s*([A-Za-z_$][\w$]*)(?:\s+@id\(\s*["']([^"']+)["']\s*\))?(?:\((.*)\))?\s*$/.exec(source);
|
|
163
|
+
if (!match) return { ok: false, reason: 'malformed-structural-union-variant' };
|
|
164
|
+
const payload = match[3]?.trim();
|
|
165
|
+
if (match[3] === undefined) return { ok: true };
|
|
166
|
+
if (!payload) return { ok: false, reason: 'empty-structural-union-variant-payload' };
|
|
167
|
+
const split = splitTopLevelCommaListStrict(payload);
|
|
168
|
+
if (!split.ok) return { ok: false, reason: split.reason };
|
|
169
|
+
const seen = new Set();
|
|
170
|
+
const seenNames = new Set();
|
|
171
|
+
for (const fieldSource of split.parts) {
|
|
172
|
+
const field = readInlineTypeField(fieldSource, `variant_field_${match[1]}`);
|
|
173
|
+
if (!field) return { ok: false, reason: 'malformed-structural-union-variant-field' };
|
|
174
|
+
if (seen.has(field.id)) return { ok: false, reason: 'duplicate-structural-union-variant-field-id' };
|
|
175
|
+
seen.add(field.id);
|
|
176
|
+
if (seenNames.has(field.name)) return { ok: false, reason: 'duplicate-structural-union-variant-field-name' };
|
|
177
|
+
seenNames.add(field.name);
|
|
178
|
+
}
|
|
179
|
+
return { ok: true };
|
|
180
|
+
}
|
|
181
|
+
|
|
76
182
|
function readInlineTypeField(source, idPrefix) {
|
|
77
183
|
const match = /^\s*([A-Za-z_$][\w$]*)(\?)?(?:\s+@id\(\s*["']([^"']+)["']\s*\))?(\?)?\s*:\s*(.+)\s*$/.exec(source);
|
|
78
184
|
if (!match || !match[5].trim()) return undefined;
|
|
185
|
+
const typeSource = match[5].trim();
|
|
186
|
+
if (!inspectTypeExpressionSyntax(typeSource).ok) return undefined;
|
|
79
187
|
const optional = Boolean(match[2] || match[4]);
|
|
80
188
|
return {
|
|
81
189
|
id: match[3] ?? `${idPrefix}_${match[1]}`,
|
|
82
190
|
name: match[1],
|
|
83
|
-
type: parseTypeExpression(
|
|
191
|
+
type: parseTypeExpression(typeSource),
|
|
84
192
|
...(optional ? { optional: true } : {})
|
|
85
193
|
};
|
|
86
194
|
}
|
|
195
|
+
|
|
196
|
+
function hasReservedStructuralTypeSyntax(text) {
|
|
197
|
+
return /\b(?:Record|Union)\s*</.test(text);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function splitTopLevelCommaListStrict(source) {
|
|
201
|
+
const parts = [];
|
|
202
|
+
const stack = [];
|
|
203
|
+
let start = 0;
|
|
204
|
+
let quote = '';
|
|
205
|
+
let escaped = false;
|
|
206
|
+
for (let index = 0; index < source.length; index++) {
|
|
207
|
+
const char = source[index];
|
|
208
|
+
if (quote) {
|
|
209
|
+
if (escaped) {
|
|
210
|
+
escaped = false;
|
|
211
|
+
} else if (char === '\\') {
|
|
212
|
+
escaped = true;
|
|
213
|
+
} else if (char === quote) {
|
|
214
|
+
quote = '';
|
|
215
|
+
}
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (char === '"' || char === "'") {
|
|
219
|
+
quote = char;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (char === '<' || char === '(' || char === '[' || char === '{') {
|
|
223
|
+
stack.push(closingDelimiterFor(char));
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (char === '>' || char === ')' || char === ']' || char === '}') {
|
|
227
|
+
if (stack.pop() !== char) return { ok: false, reason: 'unbalanced-structural-type-expression' };
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (char === ',' && stack.length === 0) {
|
|
231
|
+
const part = source.slice(start, index).trim();
|
|
232
|
+
if (!part) return { ok: false, reason: 'empty-structural-type-expression-part' };
|
|
233
|
+
parts.push(part);
|
|
234
|
+
start = index + 1;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (quote || stack.length) return { ok: false, reason: 'unbalanced-structural-type-expression' };
|
|
238
|
+
const last = source.slice(start).trim();
|
|
239
|
+
if (!last) return { ok: false, reason: 'empty-structural-type-expression-part' };
|
|
240
|
+
parts.push(last);
|
|
241
|
+
return { ok: true, parts };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function closingDelimiterFor(char) {
|
|
245
|
+
if (char === '<') return '>';
|
|
246
|
+
if (char === '(') return ')';
|
|
247
|
+
if (char === '[') return ']';
|
|
248
|
+
return '}';
|
|
249
|
+
}
|
package/dist/type-variants.js
CHANGED
|
@@ -12,18 +12,24 @@ export function readVariantPayloadFields(rest, variantName, parseTypeExpression)
|
|
|
12
12
|
return fields;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export function inspectVariantPayload(rest) {
|
|
15
|
+
export function inspectVariantPayload(rest, inspectTypeExpressionSyntax) {
|
|
16
16
|
const withoutId = stripLeadingVariantId(rest);
|
|
17
17
|
if (!withoutId) return { ok: true };
|
|
18
18
|
const payload = /^\((.*)\)$/.exec(withoutId);
|
|
19
19
|
if (!payload || !payload[1].trim()) return { ok: false, reason: 'malformed-type-variant-payload' };
|
|
20
20
|
const fieldIds = [];
|
|
21
21
|
const seen = new Set();
|
|
22
|
+
const seenNames = new Set();
|
|
22
23
|
for (const fieldSource of splitTopLevelCommaList(payload[1])) {
|
|
23
24
|
const field = parseVariantPayloadField(fieldSource);
|
|
24
25
|
if (!field) return { ok: false, reason: 'malformed-type-variant-payload' };
|
|
25
26
|
if (seen.has(field.id)) return { ok: false, reason: 'duplicate-type-variant-field-id' };
|
|
26
27
|
seen.add(field.id);
|
|
28
|
+
if (seenNames.has(field.name)) return { ok: false, reason: 'duplicate-type-variant-field-name' };
|
|
29
|
+
seenNames.add(field.name);
|
|
30
|
+
const typeSource = readVariantPayloadFieldTypeSource(fieldSource);
|
|
31
|
+
const inspected = inspectTypeExpressionSyntax?.(typeSource);
|
|
32
|
+
if (inspected && !inspected.ok) return { ok: false, reason: inspected.reason };
|
|
27
33
|
fieldIds.push(field.id);
|
|
28
34
|
}
|
|
29
35
|
return { ok: true, fieldCount: fieldIds.length, fieldIds };
|
|
@@ -41,6 +47,10 @@ function parseVariantPayloadField(source, variantName, parseTypeExpression) {
|
|
|
41
47
|
};
|
|
42
48
|
}
|
|
43
49
|
|
|
50
|
+
function readVariantPayloadFieldTypeSource(source) {
|
|
51
|
+
return /^\s*[A-Za-z_$][\w$]*(\?)?(?:\s+@id\(\s*["'][^"']+["']\s*\))?(\?)?\s*:\s*(.+)\s*$/.exec(source)?.[3]?.trim();
|
|
52
|
+
}
|
|
53
|
+
|
|
44
54
|
export function splitTopLevelCommaList(source) {
|
|
45
55
|
const parts = [];
|
|
46
56
|
let depth = 0;
|