@shapeshift-labs/frontier-lang-parser 0.3.36 → 0.3.37

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.
@@ -133,13 +133,15 @@ export const FAMILIES = Object.freeze(Object.fromEntries(FAMILY_ROWS.flatMap(([n
133
133
  return [name, ...aliases].map((alias) => [alias, config]);
134
134
  })));
135
135
 
136
- export function parseConstraintRecord(name, text, role) {
136
+ export function parseConstraintRecord(name, text, role, context = {}) {
137
137
  const kind = wordAny(text, 'kind', 'constraintKind');
138
138
  return cleanRecord({
139
139
  id: wordAny(text, 'recordId') ?? idFrom(text, `constraint_record_${name}`),
140
140
  role,
141
141
  kind,
142
142
  name: wordAny(text, 'name') ?? name,
143
+ sourceSpan: context.sourceSpan,
144
+ authoredSourceSpan: context.authoredSourceSpan,
143
145
  constraintKind: wordAny(text, 'constraintKind'),
144
146
  constraintKinds: listAny(text, 'constraint', 'constraints', 'constraintKind', 'constraintKinds') ?? (kind ? [kind] : undefined),
145
147
  ...readMapped(text, WORD_FIELDS, wordAny),
@@ -3,8 +3,8 @@ import { FAMILIES, parseConstraintRecord } from './conversion-constraint-record.
3
3
  export function parseConversionBlock(block) {
4
4
  const name = nameFrom(block.header);
5
5
  const plan = { id: idFrom(block.header, `conversion_${name}`), targets: [], metadata: { name } };
6
- for (const rawLine of block.body.split('\n')) {
7
- const line = rawLine.trim();
6
+ for (const authoredLine of readAuthoredLines(block)) {
7
+ const line = authoredLine.text;
8
8
  if (!line || line.startsWith('#')) continue;
9
9
  const target = /^target\s+([^\s,]+)/.exec(line)?.[1];
10
10
  const sourceLanguage = /^sourceLanguage\s+([^\s,]+)/.exec(line)?.[1] ?? /^source\s+([^\s,]+)/.exec(line)?.[1];
@@ -21,7 +21,7 @@ export function parseConversionBlock(block) {
21
21
  else if (runtimeRequirement) addRuntimeRequirement(plan, runtimeRequirement[1], runtimeRequirement[2]);
22
22
  else if (dialect) addDialectRecord(plan, dialect[1], dialect[2], false);
23
23
  else if (extern) addDialectRecord(plan, extern[1], extern[2], true);
24
- else if (constraint) addConstraint(plan, constraint[1], constraint[2], constraint[3]);
24
+ else if (constraint) addConstraint(plan, constraint[1], constraint[2], constraint[3], authoredLine);
25
25
  }
26
26
  return cleanRecord({ ...plan, targets: unique(plan.targets) });
27
27
  }
@@ -79,15 +79,19 @@ function addDialectRecord(plan, name, text, isExtern) {
79
79
  }
80
80
  }
81
81
 
82
- function addConstraint(plan, family, name, text) {
82
+ function addConstraint(plan, family, name, text, authoredLine = {}) {
83
83
  const config = FAMILIES[family] ?? { field: family.endsWith('s') ? family : `${family}Constraints`, sourceKey: 'sourceRecords', targetKey: 'targetRecords' };
84
84
  const role = readInlineWord('role', text) ?? 'source';
85
- const record = parseConstraintRecord(name, text, role);
85
+ const explicitSourceSpan = parseSpan(readInlineWord('sourceSpan', text), readInlineWord('sourcePath', text) ?? readInlineWord('path', text));
86
+ const sourceSpan = explicitSourceSpan ?? authoredLine.sourceSpan;
87
+ const record = parseConstraintRecord(name, text, role, { sourceSpan, authoredSourceSpan: authoredLine.sourceSpan });
86
88
  const entry = cleanRecord({
87
89
  id: idFrom(text, `${config.field}_${name}`),
88
90
  sourceLanguage: readInlineWord('sourceLanguage', text) ?? plan.sourceLanguage,
89
91
  target: readInlineWord('targetLanguage', text) ?? plan.targets[0],
90
92
  mode: readInlineWord('mode', text),
93
+ sourceSpan,
94
+ authoredSourceSpan: authoredLine.sourceSpan,
91
95
  sourceMapIds: readInlineList(text, 'sourceMap', 'sourceMaps', 'sourceMapId', 'sourceMapIds'),
92
96
  sourceMapMappingIds: readInlineList(text, 'sourceMapMapping', 'sourceMapMappings', 'sourceMapMappingId', 'sourceMapMappingIds'),
93
97
  proofObligationIds: readInlineList(text, 'proofObligation', 'proofObligations', 'proofObligationId', 'proofObligationIds', 'obligation', 'obligations'),
@@ -111,12 +115,16 @@ function resourceGraphFromRecord(record, entry) {
111
115
  const tokens = lowerTokens(record);
112
116
  const resourceId = record.resourceId ?? record.symbolId ?? `${record.id}_resource`;
113
117
  const evidenceIds = record.evidenceIds ?? entry.evidenceIds;
118
+ const sourceSpan = record.sourceSpan ?? entry.sourceSpan;
119
+ const authoredSourceSpan = record.authoredSourceSpan ?? entry.authoredSourceSpan;
114
120
  return cleanRecord({
115
121
  id: `${record.id}_resource_graph`,
116
122
  sourceLanguage: entry.sourceLanguage,
117
123
  target: entry.target,
118
124
  sourcePath: record.sourcePath,
119
125
  sourceHash: record.sourceHash,
126
+ sourceSpan,
127
+ authoredSourceSpan,
120
128
  evidenceIds,
121
129
  sourceMapIds: record.sourceMapIds ?? entry.sourceMapIds,
122
130
  sourceMapMappingIds: record.sourceMapMappingIds ?? entry.sourceMapMappingIds,
@@ -129,6 +137,8 @@ function resourceGraphFromRecord(record, entry) {
129
137
  resourceKind: record.resourceKind ?? record.kind ?? record.constraintKind,
130
138
  sourcePath: record.sourcePath,
131
139
  sourceHash: record.sourceHash,
140
+ sourceSpan,
141
+ authoredSourceSpan,
132
142
  evidenceIds,
133
143
  metadata: { factKinds: record.factKinds, constraintKinds: record.constraintKinds }
134
144
  }],
@@ -137,6 +147,8 @@ function resourceGraphFromRecord(record, entry) {
137
147
  resourceId,
138
148
  ownerId: record.ownerId ?? record.symbolId ?? `${resourceId}:owner`,
139
149
  ownerKind: record.ownerKind ?? tokenKind(tokens, /owner|single-owner/) ?? 'owner',
150
+ sourceSpan,
151
+ authoredSourceSpan,
140
152
  evidenceIds
141
153
  }] : undefined,
142
154
  loans: needsLoan(tokens, record) ? [{
@@ -144,30 +156,40 @@ function resourceGraphFromRecord(record, entry) {
144
156
  resourceId,
145
157
  mode: record.mode ?? loanMode(tokens),
146
158
  lifetimeRegionId: record.lifetimeRegionId,
159
+ sourceSpan,
160
+ authoredSourceSpan,
147
161
  evidenceIds
148
162
  }] : undefined,
149
163
  aliases: tokenMatches(tokens, /alias/) ? [{
150
164
  id: `${record.id}_alias`,
151
165
  resourceId,
152
166
  aliasKind: record.aliasKind ?? tokenKind(tokens, /alias/) ?? 'alias',
167
+ sourceSpan,
168
+ authoredSourceSpan,
153
169
  evidenceIds
154
170
  }] : undefined,
155
171
  moves: tokenMatches(tokens, /move|transfer/) ? [{
156
172
  id: `${record.id}_move`,
157
173
  resourceId,
158
174
  moveKind: record.moveKind ?? tokenKind(tokens, /move|transfer/) ?? 'move',
175
+ sourceSpan,
176
+ authoredSourceSpan,
159
177
  evidenceIds
160
178
  }] : undefined,
161
179
  drops: tokenMatches(tokens, /drop/) ? [{
162
180
  id: `${record.id}_drop`,
163
181
  resourceId,
164
182
  dropKind: record.dropKind ?? tokenKind(tokens, /drop/) ?? 'drop',
183
+ sourceSpan,
184
+ authoredSourceSpan,
165
185
  evidenceIds
166
186
  }] : undefined,
167
187
  lifetimeRegions: record.lifetimeKind || record.lifetimeRegionId || tokenMatches(tokens, /lifetime/) ? [{
168
188
  id: record.lifetimeRegionId ?? `${record.id}_lifetime`,
169
189
  resourceId,
170
190
  lifetimeKind: record.lifetimeKind ?? tokenKind(tokens, /lifetime/) ?? record.regionKind,
191
+ sourceSpan,
192
+ authoredSourceSpan,
171
193
  evidenceIds
172
194
  }] : undefined,
173
195
  borrowScopes: needsLoan(tokens, record) || record.scopeKind ? [{
@@ -176,17 +198,42 @@ function resourceGraphFromRecord(record, entry) {
176
198
  scopeKind: record.scopeKind ?? record.kind,
177
199
  lifetimeRegionId: record.lifetimeRegionId,
178
200
  constraintKinds: record.constraintKinds,
201
+ sourceSpan,
202
+ authoredSourceSpan,
179
203
  evidenceIds
180
204
  }] : undefined,
181
205
  unsafeBoundaries: tokenMatches(tokens, /unsafe|raw/) ? [{
182
206
  id: `${record.id}_unsafe`,
183
207
  resourceId,
184
208
  kind: tokenKind(tokens, /unsafe|raw/) ?? 'unsafe-boundary',
209
+ sourceSpan,
210
+ authoredSourceSpan,
185
211
  evidenceIds
186
212
  }] : undefined
187
213
  });
188
214
  }
189
215
 
216
+ function readAuthoredLines(block) {
217
+ const lines = block.body.split('\n');
218
+ const records = [];
219
+ let lineStart = block.syntax?.bodyStartOffset ?? 0;
220
+ for (const rawLine of lines) {
221
+ const rawEnd = lineStart + rawLine.length;
222
+ const leading = /^\s*/.exec(rawLine)?.[0].length ?? 0;
223
+ const trailing = /\s*$/.exec(rawLine)?.[0].length ?? 0;
224
+ const startOffset = lineStart + leading;
225
+ const endOffset = Math.max(startOffset, rawEnd - trailing);
226
+ records.push({
227
+ text: rawLine.trim(),
228
+ startOffset,
229
+ endOffset,
230
+ sourceSpan: typeof block.sourceSpan === 'function' ? block.sourceSpan(startOffset, endOffset) : undefined
231
+ });
232
+ lineStart = rawEnd + 1;
233
+ }
234
+ return records;
235
+ }
236
+
190
237
  function lowerTokens(record) {
191
238
  return unique([record.kind, record.constraintKind, record.scopeKind, record.resourceKind, record.ownerKind, record.mode, record.aliasKind, record.moveKind, record.dropKind, record.lifetimeKind, record.regionKind, record.flowKind, record.controlFlowKind, ...(record.constraintKinds ?? []), ...(record.factKinds ?? [])].filter(Boolean).map((value) => String(value).toLowerCase()));
192
239
  }
@@ -213,6 +260,18 @@ function readInlineList(text, ...labels) {
213
260
  }
214
261
  return undefined;
215
262
  }
263
+ function parseSpan(value, fallbackPath) {
264
+ if (!value) return undefined;
265
+ const match = /^(.+):(\d+):(\d+)-(\d+):(\d+)$/.exec(value);
266
+ if (!match) return undefined;
267
+ return cleanRecord({
268
+ path: match[1] || fallbackPath,
269
+ startLine: Number(match[2]),
270
+ startColumn: Number(match[3]),
271
+ endLine: Number(match[4]),
272
+ endColumn: Number(match[5])
273
+ });
274
+ }
216
275
  function cleanRecord(record) {
217
276
  return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined && (!Array.isArray(value) || value.length > 0)));
218
277
  }
package/dist/index.d.ts CHANGED
@@ -1,10 +1,43 @@
1
1
  import type { FrontierLangDocument } from '@shapeshift-labs/frontier-lang-kernel';
2
- export interface ParseFrontierOptions { readonly id?: string; readonly name?: string; }
2
+ export interface ParseFrontierOptions { readonly id?: string; readonly name?: string; readonly sourcePath?: string; }
3
3
  export declare const FrontierSourceBlockKinds: readonly string[];
4
+ export interface FrontierSourcePosition {
5
+ readonly line: number;
6
+ readonly column: number;
7
+ readonly offset: number;
8
+ }
9
+ export interface FrontierSourceSpan {
10
+ readonly sourceId?: string;
11
+ readonly path?: string;
12
+ readonly blockId?: string;
13
+ readonly blockKind?: string;
14
+ readonly startOffset: number;
15
+ readonly endOffset: number;
16
+ readonly start: FrontierSourcePosition;
17
+ readonly end: FrontierSourcePosition;
18
+ }
4
19
  export interface FrontierSourceSyntaxDiagnostic {
5
20
  readonly reason: 'unterminated-block' | 'unmatched-close-brace';
6
21
  readonly message: string;
7
- readonly location: { readonly line: number; readonly column: number; readonly offset: number };
22
+ readonly location: FrontierSourcePosition;
23
+ }
24
+ export interface FrontierSourceChildSyntaxRecord {
25
+ readonly kind: 'conversionConstraint';
26
+ readonly name: string;
27
+ readonly id?: string;
28
+ readonly family?: string;
29
+ readonly role?: string;
30
+ readonly header: string;
31
+ readonly startOffset: number;
32
+ readonly endOffset: number;
33
+ readonly location: FrontierSourcePosition;
34
+ readonly parentKind?: string;
35
+ readonly parentId?: string;
36
+ readonly parentName?: string;
37
+ readonly moduleId?: string;
38
+ readonly moduleName?: string;
39
+ readonly sourceSpan?: FrontierSourceSpan;
40
+ readonly recognized: boolean;
8
41
  }
9
42
  export interface FrontierSourceBlockSyntaxRecord {
10
43
  readonly kind: string;
@@ -15,12 +48,13 @@ export interface FrontierSourceBlockSyntaxRecord {
15
48
  readonly endOffset: number;
16
49
  readonly bodyStartOffset: number;
17
50
  readonly bodyEndOffset: number;
18
- readonly location: { readonly line: number; readonly column: number; readonly offset: number };
51
+ readonly location: FrontierSourcePosition;
19
52
  readonly moduleId?: string;
20
53
  readonly moduleName?: string;
21
54
  readonly recognized: boolean;
22
55
  readonly malformed?: boolean;
23
56
  readonly diagnostics?: readonly FrontierSourceSyntaxDiagnostic[];
57
+ readonly children?: readonly FrontierSourceChildSyntaxRecord[];
24
58
  }
25
59
  export interface FrontierUnknownSourceBlockSyntaxRecord extends FrontierSourceBlockSyntaxRecord {
26
60
  readonly recognized: false;
@@ -39,8 +73,11 @@ export interface FrontierSourceSyntaxReport {
39
73
  readonly recognizedBlockCount: number;
40
74
  readonly unknownBlockCount: number;
41
75
  readonly malformedBlockCount: number;
76
+ readonly childCount: number;
77
+ readonly recognizedChildCount: number;
42
78
  readonly diagnosticCount: number;
43
79
  readonly recognizedKinds: readonly string[];
80
+ readonly recognizedChildKinds: readonly string[];
44
81
  readonly unknownKinds: readonly string[];
45
82
  readonly failClosed: boolean;
46
83
  readonly unsupportedSyntax: boolean;
package/dist/index.js CHANGED
@@ -72,7 +72,7 @@ export function parseFrontierSource(source, options = {}) {
72
72
  return createDocument({ id: documentId, name: documentName, nodes, ...(metadata ? { metadata } : {}) });
73
73
  }
74
74
 
75
- export function parseFrontierFile(name, source) { return parseFrontierSource(source, { name: name.replace(/\.frontier$/, '') }); }
75
+ export function parseFrontierFile(name, source) { return parseFrontierSource(source, { name: name.replace(/\.frontier$/, ''), sourcePath: name }); }
76
76
 
77
77
  function readName(source) { return /module\s+([A-Za-z_$][\w$]*)/.exec(source)?.[1]; }
78
78
  function readId(source) { return /module\s+[A-Za-z_$][\w$]*\s+@id\(\s*["']([^"']+)["']\s*\)/.exec(source)?.[1]; }
@@ -0,0 +1,44 @@
1
+ export const FrontierSourceBlockKinds = Object.freeze([
2
+ 'entity',
3
+ 'state',
4
+ 'action',
5
+ 'view',
6
+ 'migration',
7
+ 'capability',
8
+ 'effect',
9
+ 'type',
10
+ 'extern',
11
+ 'lattice',
12
+ 'nativeSource',
13
+ 'target',
14
+ 'proof',
15
+ 'paradigm',
16
+ 'paradigmSemantics',
17
+ 'operations',
18
+ 'semanticOperations',
19
+ 'conversion',
20
+ 'universalConversionPlan',
21
+ 'constraintSpace',
22
+ 'possibilitySpace',
23
+ 'decisionGraph',
24
+ 'admissionGraph',
25
+ 'dialectRegistry',
26
+ 'universalDialectRegistry',
27
+ 'interlingua',
28
+ 'universalInterlingua',
29
+ 'resourceGraph',
30
+ 'semanticResourceGraph',
31
+ 'packageManifest',
32
+ 'packageGraph',
33
+ 'packageSurface',
34
+ 'canvasSurface',
35
+ 'canvasGraph',
36
+ 'applicationSurface',
37
+ 'appHost',
38
+ 'plugin',
39
+ 'pluginSurface',
40
+ 'pluginContract',
41
+ 'runtimeCapabilities',
42
+ 'runtimeCapabilityMatrix',
43
+ 'runtimeHosts'
44
+ ]);
@@ -0,0 +1,77 @@
1
+ export function readSourceSyntaxChildren(source, block, options = {}) {
2
+ if (block.malformed) return [];
3
+ if (block.kind === 'conversion' || block.kind === 'universalConversionPlan') {
4
+ return readConversionSyntaxChildren(source, block, options);
5
+ }
6
+ return [];
7
+ }
8
+
9
+ function readConversionSyntaxChildren(source, block, options) {
10
+ const children = [];
11
+ for (const line of readBodyLines(source, block)) {
12
+ if (!line.text || line.text.startsWith('#')) continue;
13
+ const constraint = /^constraint\s+([A-Za-z_$][\w$-]*)\s+([A-Za-z_$][\w$-]*)(.*)$/.exec(line.text);
14
+ if (!constraint) continue;
15
+ const [, family, name, rest] = constraint;
16
+ children.push(cleanRecord({
17
+ kind: 'conversionConstraint',
18
+ name,
19
+ id: idFrom(rest, `conversion_constraint_${family}_${name}`),
20
+ family,
21
+ role: readInlineWord('role', rest) ?? 'source',
22
+ header: line.text,
23
+ startOffset: line.startOffset,
24
+ endOffset: line.endOffset,
25
+ location: sourcePosition(source, line.startOffset),
26
+ parentKind: block.kind,
27
+ parentId: block.id,
28
+ parentName: block.name,
29
+ moduleId: block.moduleId,
30
+ moduleName: block.moduleName,
31
+ sourceSpan: sourceSpan(source, block, line.startOffset, line.endOffset, options),
32
+ recognized: true
33
+ }));
34
+ }
35
+ return children;
36
+ }
37
+
38
+ function readBodyLines(source, block) {
39
+ const body = source.slice(block.bodyStartOffset, block.bodyEndOffset);
40
+ const lines = body.split('\n');
41
+ const records = [];
42
+ let lineStart = block.bodyStartOffset;
43
+ for (const rawLine of lines) {
44
+ const rawEnd = lineStart + rawLine.length;
45
+ const leading = /^\s*/.exec(rawLine)?.[0].length ?? 0;
46
+ const trailing = /\s*$/.exec(rawLine)?.[0].length ?? 0;
47
+ const startOffset = lineStart + leading;
48
+ const endOffset = Math.max(startOffset, rawEnd - trailing);
49
+ records.push({ text: rawLine.trim(), startOffset, endOffset });
50
+ lineStart = rawEnd + 1;
51
+ }
52
+ return records;
53
+ }
54
+
55
+ function sourcePosition(source, offset) {
56
+ const lines = source.slice(0, offset).split('\n');
57
+ return { line: lines.length, column: lines[lines.length - 1].length + 1, offset };
58
+ }
59
+
60
+ function sourceSpan(source, block, startOffset, endOffset, options = {}) {
61
+ return cleanRecord({
62
+ sourceId: options.documentId,
63
+ path: options.sourcePath,
64
+ blockId: block.id,
65
+ blockKind: block.kind,
66
+ startOffset,
67
+ endOffset,
68
+ start: sourcePosition(source, startOffset),
69
+ end: sourcePosition(source, endOffset)
70
+ });
71
+ }
72
+
73
+ function idFrom(header, fallback) { return /@id\(\s*["']([^"']+)["']\s*\)/.exec(header)?.[1] ?? fallback; }
74
+ function readInlineWord(label, text) { return new RegExp('(?:^|\\s)' + label + '\\s+([^\\s,]+)').exec(text)?.[1]?.trim(); }
75
+ function cleanRecord(record) {
76
+ return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined && (!Array.isArray(value) || value.length > 0)));
77
+ }
@@ -1,47 +1,7 @@
1
- export const FrontierSourceBlockKinds = Object.freeze([
2
- 'entity',
3
- 'state',
4
- 'action',
5
- 'view',
6
- 'migration',
7
- 'capability',
8
- 'effect',
9
- 'type',
10
- 'extern',
11
- 'lattice',
12
- 'nativeSource',
13
- 'target',
14
- 'proof',
15
- 'paradigm',
16
- 'paradigmSemantics',
17
- 'operations',
18
- 'semanticOperations',
19
- 'conversion',
20
- 'universalConversionPlan',
21
- 'constraintSpace',
22
- 'possibilitySpace',
23
- 'decisionGraph',
24
- 'admissionGraph',
25
- 'dialectRegistry',
26
- 'universalDialectRegistry',
27
- 'interlingua',
28
- 'universalInterlingua',
29
- 'resourceGraph',
30
- 'semanticResourceGraph',
31
- 'packageManifest',
32
- 'packageGraph',
33
- 'packageSurface',
34
- 'canvasSurface',
35
- 'canvasGraph',
36
- 'applicationSurface',
37
- 'appHost',
38
- 'plugin',
39
- 'pluginSurface',
40
- 'pluginContract',
41
- 'runtimeCapabilities',
42
- 'runtimeCapabilityMatrix',
43
- 'runtimeHosts'
44
- ]);
1
+ import { readSourceSyntaxChildren } from './source-syntax-children.js';
2
+ import { FrontierSourceBlockKinds } from './source-block-kinds.js';
3
+
4
+ export { FrontierSourceBlockKinds };
45
5
 
46
6
  const FrontierSourceBlockKindSet = new Set(FrontierSourceBlockKinds);
47
7
 
@@ -51,13 +11,18 @@ export function inspectFrontierSourceSyntax(source, options = {}) {
51
11
  const structure = scanFrontierStructure(source);
52
12
  const blocks = readCandidateDeclarationBlocks(source, structure).map((block) => ({
53
13
  ...block,
54
- recognized: FrontierSourceBlockKindSet.has(block.kind)
14
+ recognized: FrontierSourceBlockKindSet.has(block.kind),
15
+ children: readSourceSyntaxChildren(source, block, { ...options, documentId })
55
16
  }));
17
+ for (const block of blocks) {
18
+ if (!block.children.length) delete block.children;
19
+ }
56
20
  const recognizedBlocks = blocks.filter((block) => block.recognized);
57
21
  const unknownBlocks = blocks.filter((block) => !block.recognized).map((block) => ({
58
22
  ...block,
59
23
  reason: 'unsupported-top-level-block'
60
24
  }));
25
+ const childRecords = blocks.flatMap((block) => block.children ?? []);
61
26
  const malformedBlockOpenOffsets = new Set(blocks.filter((block) => block.malformed).map((block) => block.bodyStartOffset - 1));
62
27
  const diagnostics = [
63
28
  ...blocks.flatMap((block) => block.diagnostics ?? []),
@@ -87,8 +52,11 @@ export function inspectFrontierSourceSyntax(source, options = {}) {
87
52
  recognizedBlockCount: recognizedBlocks.length,
88
53
  unknownBlockCount: unknownBlocks.length,
89
54
  malformedBlockCount: malformedBlocks.length,
55
+ childCount: childRecords.length,
56
+ recognizedChildCount: childRecords.filter((child) => child.recognized).length,
90
57
  diagnosticCount: diagnostics.length,
91
58
  recognizedKinds: unique(recognizedBlocks.map((block) => block.kind)),
59
+ recognizedChildKinds: unique(childRecords.filter((child) => child.recognized).map((child) => child.kind)),
92
60
  unknownKinds: unique(unknownBlocks.map((block) => block.kind)),
93
61
  failClosed,
94
62
  unsupportedSyntax: unknownBlocks.length > 0
@@ -108,7 +76,8 @@ export function readFrontierSourceBlocks(source, options = {}) {
108
76
  kind: block.kind,
109
77
  header: block.header,
110
78
  body: source.slice(block.bodyStartOffset, block.bodyEndOffset),
111
- syntax: block
79
+ syntax: block,
80
+ sourceSpan: (startOffset, endOffset) => sourceSpan(source, block, startOffset, endOffset, { ...options, documentId: report.documentId })
112
81
  }));
113
82
  }
114
83
 
@@ -310,9 +279,25 @@ function sourcePosition(source, offset) {
310
279
  return { line: lines.length, column: lines[lines.length - 1].length + 1, offset };
311
280
  }
312
281
 
282
+ function sourceSpan(source, block, startOffset, endOffset, options = {}) {
283
+ return cleanRecord({
284
+ sourceId: options.documentId,
285
+ path: options.sourcePath,
286
+ blockId: block.id,
287
+ blockKind: block.kind,
288
+ startOffset,
289
+ endOffset,
290
+ start: sourcePosition(source, startOffset),
291
+ end: sourcePosition(source, endOffset)
292
+ });
293
+ }
294
+
313
295
  function readName(source) { return /module\s+([A-Za-z_$][\w$]*)/.exec(source)?.[1]; }
314
296
  function readId(source) { return /module\s+[A-Za-z_$][\w$]*\s+@id\(\s*["']([^"']+)["']\s*\)/.exec(source)?.[1]; }
315
- function idFrom(header) { return /@id\(\s*["']([^"']+)["']\s*\)/.exec(header)?.[1]; }
297
+ function idFrom(header, fallback) { return /@id\(\s*["']([^"']+)["']\s*\)/.exec(header)?.[1] ?? fallback; }
316
298
  function nameFrom(header) { return /^([A-Za-z_$][\w$]*)/.exec(header)?.[1] ?? 'Unnamed'; }
317
299
  function unique(values) { return [...new Set(values.filter(Boolean))]; }
318
300
  function escapeRegExp(value) { return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
301
+ function cleanRecord(record) {
302
+ return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined && (!Array.isArray(value) || value.length > 0)));
303
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shapeshift-labs/frontier-lang-parser",
3
- "version": "0.3.36",
3
+ "version": "0.3.37",
4
4
  "description": "Parser for the first Frontier Lang .frontier syntax slice.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",