@sshadows/tree-sitter-al 3.3.0 → 3.3.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sshadows/tree-sitter-al",
3
- "version": "3.3.0",
3
+ "version": "3.3.1",
4
4
  "description": "AL for Business Central",
5
5
  "repository": {
6
6
  "type": "git",
package/src/parser.c CHANGED
@@ -722358,7 +722358,7 @@ TS_PUBLIC const TSLanguage *tree_sitter_al(void) {
722358
722358
  .metadata = {
722359
722359
  .major_version = 3,
722360
722360
  .minor_version = 3,
722361
- .patch_version = 0,
722361
+ .patch_version = 1,
722362
722362
  },
722363
722363
  };
722364
722364
  return &language;
package/src/scanner.c CHANGED
@@ -71,46 +71,92 @@ static bool read_keyword_ci(TSLexer *lexer, const char *keyword) {
71
71
  return true;
72
72
  }
73
73
 
74
- // Peek (without advancing) whether the keyword follows at current position.
75
- // Skips whitespace first. Returns true if the keyword is found as a whole word.
76
- static bool peek_keyword_ci(TSLexer *lexer, const char *keyword) {
77
- // We can't actually peek without advancing in the tree-sitter API.
78
- // This function advances freely — on false returns tree-sitter resets the lexer.
79
- skip_whitespace(lexer);
80
- for (int i = 0; keyword[i] != '\0'; i++) {
81
- if (towlower(lexer->lookahead) != keyword[i]) return false;
74
+ // Consume a comment beginning at the current '/'. The '/' is consumed either
75
+ // way; the return value says whether it actually opened a comment, so a caller
76
+ // that cannot tolerate a bare '/' can decline. AL block comments do not nest
77
+ // (grammar.js's multiline_comment is the classic non-nesting C form).
78
+ static bool skip_comment(TSLexer *lexer) {
79
+ lexer->advance(lexer, false); // past the leading '/'
80
+ if (lexer->lookahead == '/') {
81
+ while (lexer->lookahead != 0 && lexer->lookahead != '\n') {
82
+ lexer->advance(lexer, false);
83
+ }
84
+ return true;
85
+ }
86
+ if (lexer->lookahead == '*') {
87
+ lexer->advance(lexer, false);
88
+ while (lexer->lookahead != 0) {
89
+ if (lexer->lookahead == '*') {
90
+ lexer->advance(lexer, false);
91
+ if (lexer->lookahead == '/') {
92
+ lexer->advance(lexer, false);
93
+ return true;
94
+ }
95
+ continue;
96
+ }
97
+ lexer->advance(lexer, false);
98
+ }
99
+ return true; // unterminated block comment runs to EOF
100
+ }
101
+ return false; // a lone '/' — not a comment
102
+ }
103
+
104
+ // Skip whitespace WITHOUT marking it skippable.
105
+ //
106
+ // advance(lexer, true) unconditionally resets the token's START position to the
107
+ // current offset. That is right for LEADING whitespace, and catastrophic
108
+ // afterwards: once the token text has been consumed (or mark_end called), a
109
+ // marking skip drags the start past the end and the node collapses to zero
110
+ // width at the later position. Every skip that runs after the token text must
111
+ // use this, never skip_whitespace.
112
+ static void skip_whitespace_nomark(TSLexer *lexer) {
113
+ while (lexer->lookahead == ' ' || lexer->lookahead == '\t' ||
114
+ lexer->lookahead == '\r' || lexer->lookahead == '\n' ||
115
+ lexer->lookahead == '\f') {
82
116
  lexer->advance(lexer, false);
83
117
  }
84
- if (is_identifier_char(lexer->lookahead)) return false;
85
- return true;
86
118
  }
87
119
 
88
- // Directives that grammar.js declares as `extras`. They are transparent to the
89
- // parse tree, so a lookahead scanning for a structural directive must step over
90
- // them rather than stop on them. Keep in sync with the `extras` array.
120
+ // Skip whitespace and comments, without marking. Returns false if a bare '/'
121
+ // was hit (already consumed), which no lookahead in this scanner can make
122
+ // sense of.
123
+ static bool skip_whitespace_and_comments(TSLexer *lexer) {
124
+ while (true) {
125
+ skip_whitespace_nomark(lexer);
126
+ if (lexer->lookahead != '/') return true;
127
+ if (!skip_comment(lexer)) return false;
128
+ }
129
+ }
130
+
131
+ // Directives that grammar.js declares as `extras`. Comments are extras too, but
132
+ // they are handled by skip_whitespace_and_comments rather than listed here.
133
+ // Everything transparent to the parse tree must be stepped over by a lookahead
134
+ // scanning for a structural directive. Keep in sync with the `extras` array.
91
135
  static const char *const TRANSPARENT_DIRECTIVES[] = {
92
136
  "pragma", "endregion", "region", "define", "undef", NULL,
93
137
  };
94
138
 
95
- // Skip whitespace and any transparent-directive lines, then peek for a keyword.
139
+ // Target sets for peek_directive_ci_skip_extras. Bare words, no '#'.
140
+ static const char *const DIRECTIVE_ENDIF[] = { "endif", NULL };
141
+ static const char *const DIRECTIVE_ELSE_ENDIF[] = { "else", "endif", NULL };
142
+
143
+ // Skip whitespace, comments and transparent-directive lines, then test whether
144
+ // what follows is a '#' directive named by one of `targets`.
145
+ //
146
+ // Used when scanning ahead for split-construct patterns (PREPROC_SPLIT_BEGIN
147
+ // looking for #endif, PREPROC_SPLIT_END looking for #else/#endif).
96
148
  //
97
- // Used when scanning ahead for split-construct patterns (e.g.
98
- // PREPROC_SPLIT_BEGIN checking for #endif). Consuming '#' is irreversible
99
- // within one scan, so the directive word after '#' is read ONCE into a buffer
100
- // and then classified matching candidates one after another would burn the
101
- // shared prefix of e.g. "endif"/"endregion" on the first failed attempt.
102
- static bool peek_keyword_ci_skip_extras(TSLexer *lexer, const char *keyword) {
149
+ // EVERY target is tested against a SINGLE buffered read of the directive word.
150
+ // Never match candidates one after another here: consuming '#' is irreversible
151
+ // within one scan, and so is consuming the 'end' prefix shared by "endif" and
152
+ // "endregion", so a failed first attempt silently destroys the later ones. An
153
+ // earlier `read_keyword_ci(lexer, "else") || read_keyword_ci(lexer, "endif")`
154
+ // in PREPROC_SPLIT_END made the "endif" arm permanently unreachable exactly
155
+ // this way.
156
+ static bool peek_directive_ci_skip_extras(TSLexer *lexer, const char *const *targets) {
103
157
  while (true) {
104
- skip_whitespace(lexer);
105
- if (lexer->lookahead != '#') {
106
- // Not '#' — try to match keyword directly
107
- for (int i = 0; keyword[i] != '\0'; i++) {
108
- if (towlower(lexer->lookahead) != keyword[i]) return false;
109
- lexer->advance(lexer, false);
110
- }
111
- if (is_identifier_char(lexer->lookahead)) return false;
112
- return true;
113
- }
158
+ if (!skip_whitespace_and_comments(lexer)) return false;
159
+ if (lexer->lookahead != '#') return false;
114
160
 
115
161
  lexer->advance(lexer, false);
116
162
  // Horizontal whitespace only: '# pragma' is one directive, but '#' and a
@@ -130,8 +176,9 @@ static bool peek_keyword_ci_skip_extras(TSLexer *lexer, const char *keyword) {
130
176
  if (len >= sizeof(word)) return false; // too long to be any directive
131
177
  word[len] = '\0';
132
178
 
133
- // The caller's keyword includes the '#' we just consumed.
134
- if (keyword[0] == '#' && strcmp(word, keyword + 1) == 0) return true;
179
+ for (int i = 0; targets[i] != NULL; i++) {
180
+ if (strcmp(word, targets[i]) == 0) return true;
181
+ }
135
182
 
136
183
  bool transparent = false;
137
184
  for (int i = 0; TRANSPARENT_DIRECTIVES[i] != NULL; i++) {
@@ -229,19 +276,21 @@ bool tree_sitter_al_external_scanner_scan(
229
276
  }
230
277
  }
231
278
 
232
- // PREPROC_SPLIT_BEGIN: 'begin' at depth > 0, before #endif (possibly with #pragma lines between)
279
+ // PREPROC_SPLIT_BEGIN: 'begin' at depth > 0, before #endif (possibly with
280
+ // comments or transparent directive lines between)
233
281
  //
234
- // '#' handling: peek_keyword_ci is called with "#endif" (the full string
235
- // including '#'). PREPROC_OPEN/CLOSE manually advance past '#' before calling
236
- // read_keyword_ci("if"/"endif"). These are DIFFERENT conventions — do not mix.
282
+ // '#' handling: peek_directive_ci_skip_extras takes BARE directive words and
283
+ // consumes the '#' itself. PREPROC_OPEN/CLOSE manually advance past '#'
284
+ // before calling read_keyword_ci("if"/"endif"). These are DIFFERENT
285
+ // conventions — do not mix.
237
286
  //
238
- // #pragma/#region/#define and friends are transparent extras we skip them
239
- // when scanning ahead for #endif (see TRANSPARENT_DIRECTIVES).
287
+ // Comments, #pragma, #region, #define and friends are all extras, hence all
288
+ // transparent here (see skip_whitespace_and_comments/TRANSPARENT_DIRECTIVES).
240
289
  if (valid_symbols[PREPROC_SPLIT_BEGIN] && state->depth > 0) {
241
290
  skip_whitespace(lexer);
242
291
  if (read_keyword_ci(lexer, "begin")) {
243
292
  lexer->mark_end(lexer); // token covers only 'begin'
244
- if (peek_keyword_ci_skip_extras(lexer, "#endif")) {
293
+ if (peek_directive_ci_skip_extras(lexer, DIRECTIVE_ENDIF)) {
245
294
  lexer->result_symbol = PREPROC_SPLIT_BEGIN;
246
295
  return true;
247
296
  }
@@ -259,26 +308,17 @@ bool tree_sitter_al_external_scanner_scan(
259
308
  skip_whitespace(lexer);
260
309
  if (read_keyword_ci(lexer, "end")) {
261
310
  lexer->mark_end(lexer); // token covers only 'end'
262
- // Check for ';' then whitespace then '#else' or '#endif'
263
- while (lexer->lookahead == ' ' || lexer->lookahead == '\t' ||
264
- lexer->lookahead == '\r' || lexer->lookahead == '\n' ||
265
- lexer->lookahead == '\f') {
266
- lexer->advance(lexer, false);
267
- }
311
+ // Check for ';' then #else or #endif. Comments and transparent directive
312
+ // lines may sit at either gap and must not stop the lookahead before
313
+ // this skipped nothing, a single trailing `// note` after the `end;`
314
+ // silently dropped the token and the run reparsed as a call_statement
315
+ // with NO error nodes.
316
+ if (!skip_whitespace_and_comments(lexer)) return false;
268
317
  if (lexer->lookahead == ';') {
269
318
  lexer->advance(lexer, false);
270
- // Now check for #else or #endif after whitespace
271
- while (lexer->lookahead == ' ' || lexer->lookahead == '\t' ||
272
- lexer->lookahead == '\r' || lexer->lookahead == '\n' ||
273
- lexer->lookahead == '\f') {
274
- lexer->advance(lexer, false);
275
- }
276
- if (lexer->lookahead == '#') {
277
- lexer->advance(lexer, false);
278
- if (read_keyword_ci(lexer, "else") || read_keyword_ci(lexer, "endif")) {
279
- lexer->result_symbol = PREPROC_SPLIT_END;
280
- return true;
281
- }
319
+ if (peek_directive_ci_skip_extras(lexer, DIRECTIVE_ELSE_ENDIF)) {
320
+ lexer->result_symbol = PREPROC_SPLIT_END;
321
+ return true;
282
322
  }
283
323
  }
284
324
  // 'end' found but not followed by ; then #else/#endif — return false.
@@ -299,9 +339,9 @@ bool tree_sitter_al_external_scanner_scan(
299
339
  lexer->mark_end(lexer);
300
340
 
301
341
  // Scan past the attribute content to find the closing ']'.
302
- // Track bracket and paren depth for nested constructs like [Obsolete('msg', '24.0')]
342
+ // Bracket depth handles nesting; strings and comments are skipped whole so
343
+ // a ']' inside either cannot close the scan early.
303
344
  int bracket_depth = 1;
304
- int paren_depth = 0;
305
345
  bool in_string = false;
306
346
 
307
347
  while (bracket_depth > 0 && lexer->lookahead != 0) {
@@ -317,12 +357,14 @@ bool tree_sitter_al_external_scanner_scan(
317
357
  continue;
318
358
  }
319
359
  } else {
360
+ if (lexer->lookahead == '/') {
361
+ // Consumes the '/' whether or not a comment opened, so the loop
362
+ // always makes progress.
363
+ skip_comment(lexer);
364
+ continue;
365
+ }
320
366
  if (lexer->lookahead == '\'') {
321
367
  in_string = true;
322
- } else if (lexer->lookahead == '(') {
323
- paren_depth++;
324
- } else if (lexer->lookahead == ')') {
325
- if (paren_depth > 0) paren_depth--;
326
368
  } else if (lexer->lookahead == '[') {
327
369
  bracket_depth++;
328
370
  } else if (lexer->lookahead == ']') {
@@ -371,6 +413,10 @@ bool tree_sitter_al_external_scanner_scan(
371
413
  continue;
372
414
  }
373
415
  } else {
416
+ if (lexer->lookahead == '/') {
417
+ skip_comment(lexer);
418
+ continue;
419
+ }
374
420
  if (lexer->lookahead == '\'') {
375
421
  inner_in_string = true;
376
422
  } else if (lexer->lookahead == '[') {
@@ -397,33 +443,28 @@ bool tree_sitter_al_external_scanner_scan(
397
443
  // (fall through to the identifier/quoted-identifier checks below)
398
444
  }
399
445
 
400
- if (lexer->lookahead == '"') {
401
- // Quoted identifier scan to closing '"', check for ':'
402
- lexer->advance(lexer, false);
403
- while (lexer->lookahead != 0 && lexer->lookahead != '"') {
404
- lexer->advance(lexer, false);
405
- }
406
- if (lexer->lookahead == '"') {
407
- lexer->advance(lexer, false);
408
- // Skip whitespace
409
- while (lexer->lookahead == ' ' || lexer->lookahead == '\t') {
410
- lexer->advance(lexer, false);
411
- }
412
- if (lexer->lookahead == ':') {
413
- lexer->result_symbol = VAR_ATTRIBUTE_OPEN;
414
- return true;
415
- }
416
- }
417
- return false;
418
- }
419
-
420
- if (is_identifier_start(lexer->lookahead)) {
421
- // Identifier — scan it, then check for ':' (or ',' for multi-variable decls)
422
- // Pattern: identifier (',' identifier)* ':'
446
+ // Variable declaration pattern: name (',' name)* ':' — where each name
447
+ // is a bare identifier or a quoted identifier, in ANY position. Handling
448
+ // quoted and bare names in one loop is what lets a quoted name lead a
449
+ // multi-name declaration; the previous split branches accepted a quoted
450
+ // name only when it was solo or in a later position.
451
+ if (lexer->lookahead == '"' || is_identifier_start(lexer->lookahead)) {
423
452
  while (true) {
424
- while (is_identifier_char(lexer->lookahead)) {
453
+ if (lexer->lookahead == '"') {
425
454
  lexer->advance(lexer, false);
455
+ while (lexer->lookahead != 0 && lexer->lookahead != '"') {
456
+ lexer->advance(lexer, false);
457
+ }
458
+ if (lexer->lookahead != '"') return false; // unterminated
459
+ lexer->advance(lexer, false);
460
+ } else if (is_identifier_start(lexer->lookahead)) {
461
+ while (is_identifier_char(lexer->lookahead)) {
462
+ lexer->advance(lexer, false);
463
+ }
464
+ } else {
465
+ return false;
426
466
  }
467
+
427
468
  // Skip whitespace
428
469
  while (lexer->lookahead == ' ' || lexer->lookahead == '\t') {
429
470
  lexer->advance(lexer, false);
@@ -432,29 +473,12 @@ bool tree_sitter_al_external_scanner_scan(
432
473
  lexer->result_symbol = VAR_ATTRIBUTE_OPEN;
433
474
  return true;
434
475
  }
435
- if (lexer->lookahead == ',') {
436
- // Multi-variable: identifier, identifier, ... : Type
476
+ if (lexer->lookahead != ',') return false;
477
+
478
+ lexer->advance(lexer, false); // past the ','
479
+ while (lexer->lookahead == ' ' || lexer->lookahead == '\t') {
437
480
  lexer->advance(lexer, false);
438
- while (lexer->lookahead == ' ' || lexer->lookahead == '\t') {
439
- lexer->advance(lexer, false);
440
- }
441
- if (lexer->lookahead == '"') {
442
- // Quoted identifier in multi-var list
443
- lexer->advance(lexer, false);
444
- while (lexer->lookahead != 0 && lexer->lookahead != '"') {
445
- lexer->advance(lexer, false);
446
- }
447
- if (lexer->lookahead == '"') lexer->advance(lexer, false);
448
- while (lexer->lookahead == ' ' || lexer->lookahead == '\t') {
449
- lexer->advance(lexer, false);
450
- }
451
- // Loop back to check for ':', ',' etc.
452
- continue;
453
- }
454
- if (!is_identifier_start(lexer->lookahead)) return false;
455
- continue;
456
481
  }
457
- return false;
458
482
  }
459
483
  }
460
484
 
@@ -517,10 +541,13 @@ bool tree_sitter_al_external_scanner_scan(
517
541
  }
518
542
  }
519
543
 
520
- // Did not match continue_as_identifier
521
- // Note: we can't fall through to PROPERTY_NAME here because we've
522
- // already consumed characters. The scanner will be called again
523
- // for the same position if we return false.
544
+ // Did not match continue_as_identifier. We can't fall through to
545
+ // PROPERTY_NAME because characters are already consumed and the external
546
+ // scanner is NOT re-entered for the same position after a false return —
547
+ // tree-sitter discards the advances and runs the internal lexer instead.
548
+ // This is only safe because the grammar never makes CONTINUE_AS_IDENTIFIER
549
+ // and PROPERTY_NAME valid in the same state (properties live in object and
550
+ // section bodies, `continue :=` in statement bodies).
524
551
  return false;
525
552
  }
526
553
 
@@ -547,11 +574,13 @@ bool tree_sitter_al_external_scanner_scan(
547
574
  // Mark end of identifier (before whitespace/equals)
548
575
  lexer->mark_end(lexer);
549
576
 
550
- // Skip whitespace
551
- while (lexer->lookahead == ' ' || lexer->lookahead == '\t' ||
552
- lexer->lookahead == '\r' || lexer->lookahead == '\f') {
553
- lexer->advance(lexer, false);
554
- }
577
+ // Skip whitespace and comments. '\n' belongs here just as much as '\r' —
578
+ // the leading skip above already accepts it, and alc accepts a property
579
+ // whose '=' sits on the next line (verified). Omitting it made
580
+ // `Caption\n = 'Test';` an ERROR that the compiler compiles fine.
581
+ // A bare '/' is not a comment and is not '=', so declining on it loses
582
+ // nothing.
583
+ if (!skip_whitespace_and_comments(lexer)) return false;
555
584
 
556
585
  // Check for = but not :=
557
586
  if (lexer->lookahead == '=') {
Binary file