@shapeshift-labs/frontier-lang-parser 0.3.34 → 0.3.35

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 CHANGED
@@ -230,11 +230,11 @@ import { inspectFrontierSourceSyntax } from '@shapeshift-labs/frontier-lang-pars
230
230
  const report = inspectFrontierSourceSyntax(source);
231
231
 
232
232
  if (report.summary.failClosed) {
233
- console.error(report.summary.unknownKinds);
233
+ console.error(report.summary.unknownKinds, report.diagnostics);
234
234
  }
235
235
  ```
236
236
 
237
- Nested child syntax such as `render` blocks inside a `view` is not reported as an unknown top-level block; the parent parser owns those child rows. The report is evidence about parser coverage only. It keeps `autoMergeClaim` and `semanticEquivalenceClaim` false.
237
+ Nested child syntax such as `render` blocks inside a `view` is not reported as an unknown top-level block; the parent parser owns those child rows. The report ignores braces inside quoted strings and comments when finding authored declaration boundaries, reports UTF-8 `sourceBytes`, and fails closed for malformed or unterminated blocks through `diagnostics`. The report is evidence about parser coverage only. It keeps `autoMergeClaim` and `semanticEquivalenceClaim` false.
238
238
 
239
239
  ## Authored view render graph syntax
240
240
 
package/dist/index.d.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  import type { FrontierLangDocument } from '@shapeshift-labs/frontier-lang-kernel';
2
2
  export interface ParseFrontierOptions { readonly id?: string; readonly name?: string; }
3
3
  export declare const FrontierSourceBlockKinds: readonly string[];
4
+ export interface FrontierSourceSyntaxDiagnostic {
5
+ readonly reason: 'unterminated-block' | 'unmatched-close-brace';
6
+ readonly message: string;
7
+ readonly location: { readonly line: number; readonly column: number; readonly offset: number };
8
+ }
4
9
  export interface FrontierSourceBlockSyntaxRecord {
5
10
  readonly kind: string;
6
11
  readonly name: string;
@@ -14,6 +19,8 @@ export interface FrontierSourceBlockSyntaxRecord {
14
19
  readonly moduleId?: string;
15
20
  readonly moduleName?: string;
16
21
  readonly recognized: boolean;
22
+ readonly malformed?: boolean;
23
+ readonly diagnostics?: readonly FrontierSourceSyntaxDiagnostic[];
17
24
  }
18
25
  export interface FrontierUnknownSourceBlockSyntaxRecord extends FrontierSourceBlockSyntaxRecord {
19
26
  readonly recognized: false;
@@ -31,11 +38,14 @@ export interface FrontierSourceSyntaxReport {
31
38
  readonly blockCount: number;
32
39
  readonly recognizedBlockCount: number;
33
40
  readonly unknownBlockCount: number;
41
+ readonly malformedBlockCount: number;
42
+ readonly diagnosticCount: number;
34
43
  readonly recognizedKinds: readonly string[];
35
44
  readonly unknownKinds: readonly string[];
36
45
  readonly failClosed: boolean;
37
46
  readonly unsupportedSyntax: boolean;
38
47
  };
48
+ readonly diagnostics: readonly FrontierSourceSyntaxDiagnostic[];
39
49
  readonly metadata: {
40
50
  readonly sourceBytes: number;
41
51
  readonly autoMergeClaim: false;
@@ -48,7 +48,8 @@ const FrontierSourceBlockKindSet = new Set(FrontierSourceBlockKinds);
48
48
  export function inspectFrontierSourceSyntax(source, options = {}) {
49
49
  const documentId = options.id ?? readId(source) ?? 'mod_frontier';
50
50
  const documentName = options.name ?? readName(source) ?? 'FrontierModule';
51
- const blocks = readCandidateDeclarationBlocks(source).map((block) => ({
51
+ const structure = scanFrontierStructure(source);
52
+ const blocks = readCandidateDeclarationBlocks(source, structure).map((block) => ({
52
53
  ...block,
53
54
  recognized: FrontierSourceBlockKindSet.has(block.kind)
54
55
  }));
@@ -57,6 +58,22 @@ export function inspectFrontierSourceSyntax(source, options = {}) {
57
58
  ...block,
58
59
  reason: 'unsupported-top-level-block'
59
60
  }));
61
+ const malformedBlockOpenOffsets = new Set(blocks.filter((block) => block.malformed).map((block) => block.bodyStartOffset - 1));
62
+ const diagnostics = [
63
+ ...blocks.flatMap((block) => block.diagnostics ?? []),
64
+ ...structure.unmatchedOpenBraces.filter((offset) => !malformedBlockOpenOffsets.has(offset)).map((offset) => ({
65
+ reason: 'unterminated-block',
66
+ message: 'Found an opening brace without a matching closing brace.',
67
+ location: sourcePosition(source, offset)
68
+ })),
69
+ ...structure.unmatchedCloseBraces.map((offset) => ({
70
+ reason: 'unmatched-close-brace',
71
+ message: 'Found a closing brace without a matching opening brace.',
72
+ location: sourcePosition(source, offset)
73
+ }))
74
+ ];
75
+ const malformedBlocks = blocks.filter((block) => block.malformed);
76
+ const failClosed = unknownBlocks.length > 0 || diagnostics.length > 0;
60
77
  return {
61
78
  kind: 'frontier.lang.sourceSyntaxReport',
62
79
  version: 1,
@@ -69,21 +86,24 @@ export function inspectFrontierSourceSyntax(source, options = {}) {
69
86
  blockCount: blocks.length,
70
87
  recognizedBlockCount: recognizedBlocks.length,
71
88
  unknownBlockCount: unknownBlocks.length,
89
+ malformedBlockCount: malformedBlocks.length,
90
+ diagnosticCount: diagnostics.length,
72
91
  recognizedKinds: unique(recognizedBlocks.map((block) => block.kind)),
73
92
  unknownKinds: unique(unknownBlocks.map((block) => block.kind)),
74
- failClosed: unknownBlocks.length > 0,
93
+ failClosed,
75
94
  unsupportedSyntax: unknownBlocks.length > 0
76
95
  },
96
+ diagnostics,
77
97
  metadata: {
78
- sourceBytes: source.length,
98
+ sourceBytes: utf8ByteLength(source),
79
99
  autoMergeClaim: false,
80
100
  semanticEquivalenceClaim: false
81
101
  }
82
102
  };
83
103
  }
84
104
 
85
- function readCandidateDeclarationBlocks(source) {
86
- const moduleRanges = readModuleRanges(source);
105
+ function readCandidateDeclarationBlocks(source, structure) {
106
+ const moduleRanges = readModuleRanges(source, structure);
87
107
  const blocks = [];
88
108
  const header = /(^|\n)\s*([A-Za-z_$][\w$]*)\s+([^{}\n]+)\{/g;
89
109
  let match;
@@ -94,8 +114,12 @@ function readCandidateDeclarationBlocks(source) {
94
114
  const kind = match[2];
95
115
  if (kind === 'module') continue;
96
116
  const open = header.lastIndex - 1;
97
- const close = findMatchingBrace(source, open);
98
- const depth = braceDepthBefore(source, start);
117
+ if (!structure.codeOffsets[start] || !structure.codeOffsets[open]) continue;
118
+ const close = findMatchingBrace(structure, open);
119
+ const malformed = close < 0;
120
+ const end = malformed ? source.length : close + 1;
121
+ const bodyEnd = malformed ? source.length : close;
122
+ const depth = braceDepthBefore(structure, start);
99
123
  const moduleRange = moduleRanges.find((range) => start > range.open && start < range.close);
100
124
  const declarationDepth = moduleRange ? moduleRange.depth + 1 : 0;
101
125
  if (depth !== declarationDepth) continue;
@@ -106,18 +130,24 @@ function readCandidateDeclarationBlocks(source) {
106
130
  id: idFrom(headerText),
107
131
  header: headerText,
108
132
  startOffset: start,
109
- endOffset: close + 1,
133
+ endOffset: end,
110
134
  bodyStartOffset: open + 1,
111
- bodyEndOffset: close,
135
+ bodyEndOffset: bodyEnd,
112
136
  location: sourcePosition(source, start),
113
137
  moduleId: moduleRange?.id,
114
- moduleName: moduleRange?.name
138
+ moduleName: moduleRange?.name,
139
+ malformed,
140
+ diagnostics: malformed ? [{
141
+ reason: 'unterminated-block',
142
+ message: `Block "${kind}" has no matching closing brace.`,
143
+ location: sourcePosition(source, open)
144
+ }] : []
115
145
  });
116
146
  }
117
147
  return blocks;
118
148
  }
119
149
 
120
- function readModuleRanges(source) {
150
+ function readModuleRanges(source, structure) {
121
151
  const ranges = [];
122
152
  const header = /(^|\n)\s*module\s+([^{}\n]+)\{/g;
123
153
  let match;
@@ -126,36 +156,120 @@ function readModuleRanges(source) {
126
156
  const leading = /^\s*/.exec(source.slice(fullStart))?.[0].length ?? 0;
127
157
  const start = fullStart + leading;
128
158
  const open = header.lastIndex - 1;
129
- const close = findMatchingBrace(source, open);
159
+ if (!structure.codeOffsets[start] || !structure.codeOffsets[open]) continue;
160
+ const close = findMatchingBrace(structure, open);
161
+ const malformed = close < 0;
130
162
  ranges.push({
131
163
  start,
132
164
  open,
133
- close,
134
- depth: braceDepthBefore(source, start),
165
+ close: malformed ? source.length : close,
166
+ depth: braceDepthBefore(structure, start),
135
167
  name: nameFrom(match[2].trim()),
136
- id: idFrom(match[2].trim())
168
+ id: idFrom(match[2].trim()),
169
+ malformed
137
170
  });
138
171
  }
139
172
  return ranges;
140
173
  }
141
174
 
142
- function findMatchingBrace(source, open) {
143
- let depth = 1;
144
- for (let index = open + 1; index < source.length; index++) {
145
- if (source[index] === '{') depth++;
146
- if (source[index] === '}') depth--;
147
- if (depth === 0) return index;
148
- }
149
- return source.length - 1;
175
+ function findMatchingBrace(structure, open) {
176
+ return structure.bracePairs.get(open) ?? -1;
177
+ }
178
+
179
+ function braceDepthBefore(structure, offset) {
180
+ return structure.depthBefore[Math.min(Math.max(offset, 0), structure.depthBefore.length - 1)] ?? 0;
150
181
  }
151
182
 
152
- function braceDepthBefore(source, offset) {
183
+ function scanFrontierStructure(source) {
184
+ const depthBefore = new Int32Array(source.length + 1);
185
+ const codeOffsets = new Uint8Array(source.length);
186
+ const stack = [];
187
+ const bracePairs = new Map();
188
+ const unmatchedCloseBraces = [];
153
189
  let depth = 0;
154
- for (let index = 0; index < offset; index++) {
155
- if (source[index] === '{') depth++;
156
- if (source[index] === '}') depth = Math.max(0, depth - 1);
190
+ let state = 'code';
191
+ let quote = '';
192
+ for (let index = 0; index < source.length; index++) {
193
+ depthBefore[index] = depth;
194
+ const char = source[index];
195
+ const next = source[index + 1];
196
+ if (state === 'line-comment') {
197
+ if (char === '\n') state = 'code';
198
+ continue;
199
+ }
200
+ if (state === 'block-comment') {
201
+ if (char === '*' && next === '/') {
202
+ depthBefore[index + 1] = depth;
203
+ index++;
204
+ state = 'code';
205
+ }
206
+ continue;
207
+ }
208
+ if (state === 'string') {
209
+ if (char === '\\') {
210
+ depthBefore[index + 1] = depth;
211
+ index++;
212
+ continue;
213
+ }
214
+ if (char === quote) {
215
+ state = 'code';
216
+ quote = '';
217
+ }
218
+ continue;
219
+ }
220
+ codeOffsets[index] = 1;
221
+ if (char === '/' && next === '/') {
222
+ codeOffsets[index + 1] = 0;
223
+ depthBefore[index + 1] = depth;
224
+ index++;
225
+ state = 'line-comment';
226
+ continue;
227
+ }
228
+ if (char === '/' && next === '*') {
229
+ codeOffsets[index + 1] = 0;
230
+ depthBefore[index + 1] = depth;
231
+ index++;
232
+ state = 'block-comment';
233
+ continue;
234
+ }
235
+ if (char === '#' && isLineLeadingWhitespace(source, index)) {
236
+ state = 'line-comment';
237
+ continue;
238
+ }
239
+ if (char === '"' || char === "'" || char === '`') {
240
+ state = 'string';
241
+ quote = char;
242
+ continue;
243
+ }
244
+ if (char === '{') {
245
+ stack.push(index);
246
+ depth++;
247
+ continue;
248
+ }
249
+ if (char === '}') {
250
+ const open = stack.pop();
251
+ if (open === undefined) {
252
+ unmatchedCloseBraces.push(index);
253
+ continue;
254
+ }
255
+ depth = Math.max(0, depth - 1);
256
+ bracePairs.set(open, index);
257
+ }
258
+ }
259
+ depthBefore[source.length] = depth;
260
+ return { bracePairs, codeOffsets, depthBefore, unmatchedCloseBraces, unmatchedOpenBraces: stack };
261
+ }
262
+
263
+ function isLineLeadingWhitespace(source, offset) {
264
+ for (let index = offset - 1; index >= 0 && source[index] !== '\n'; index--) {
265
+ if (!/\s/.test(source[index])) return false;
157
266
  }
158
- return depth;
267
+ return true;
268
+ }
269
+
270
+ function utf8ByteLength(source) {
271
+ if (typeof TextEncoder !== 'undefined') return new TextEncoder().encode(source).length;
272
+ return unescape(encodeURIComponent(source)).length;
159
273
  }
160
274
 
161
275
  function sourcePosition(source, offset) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shapeshift-labs/frontier-lang-parser",
3
- "version": "0.3.34",
3
+ "version": "0.3.35",
4
4
  "description": "Parser for the first Frontier Lang .frontier syntax slice.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",