@sshadows/tree-sitter-al 3.2.1 → 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/src/scanner.c CHANGED
@@ -71,65 +71,124 @@ 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 == '*') {
82
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
83
100
  }
84
- if (is_identifier_char(lexer->lookahead)) return false;
85
- return true;
101
+ return false; // a lone '/' — not a comment
86
102
  }
87
103
 
88
- // Skip whitespace and any #pragma lines, then peek for a keyword.
89
- // #pragma lines are transparent extras — we must skip them when scanning ahead
90
- // for split-construct patterns (e.g., PREPROC_SPLIT_BEGIN checking for #endif).
91
- static bool peek_keyword_ci_skip_pragma(TSLexer *lexer, const char *keyword) {
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') {
116
+ lexer->advance(lexer, false);
117
+ }
118
+ }
119
+
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) {
92
124
  while (true) {
93
- skip_whitespace(lexer);
94
- // Check for '#pragma' line — skip entire line
95
- if (lexer->lookahead == '#') {
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.
135
+ static const char *const TRANSPARENT_DIRECTIVES[] = {
136
+ "pragma", "endregion", "region", "define", "undef", NULL,
137
+ };
138
+
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).
148
+ //
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) {
157
+ while (true) {
158
+ if (!skip_whitespace_and_comments(lexer)) return false;
159
+ if (lexer->lookahead != '#') return false;
160
+
161
+ lexer->advance(lexer, false);
162
+ // Horizontal whitespace only: '# pragma' is one directive, but '#' and a
163
+ // word on the NEXT line are not (matching the extras regexes' `[ \t]*`).
164
+ while (lexer->lookahead == ' ' || lexer->lookahead == '\t') {
165
+ lexer->advance(lexer, false);
166
+ }
167
+
168
+ // Read the directive word. Longest AL directive is "endregion" (9).
169
+ char word[16];
170
+ size_t len = 0;
171
+ while (is_identifier_char(lexer->lookahead)) {
172
+ if (len < sizeof(word) - 1) word[len] = (char)towlower(lexer->lookahead);
173
+ len++;
174
+ lexer->advance(lexer, false);
175
+ }
176
+ if (len >= sizeof(word)) return false; // too long to be any directive
177
+ word[len] = '\0';
178
+
179
+ for (int i = 0; targets[i] != NULL; i++) {
180
+ if (strcmp(word, targets[i]) == 0) return true;
181
+ }
182
+
183
+ bool transparent = false;
184
+ for (int i = 0; TRANSPARENT_DIRECTIVES[i] != NULL; i++) {
185
+ if (strcmp(word, TRANSPARENT_DIRECTIVES[i]) == 0) { transparent = true; break; }
186
+ }
187
+ if (!transparent) return false;
188
+
189
+ // Skip the rest of this directive's line, then look again.
190
+ while (lexer->lookahead != '\0' && lexer->lookahead != '\n') {
96
191
  lexer->advance(lexer, false);
97
- // Check if 'pragma' follows
98
- const char *pragma = "pragma";
99
- bool is_pragma = true;
100
- for (int i = 0; pragma[i] != '\0'; i++) {
101
- if (towlower(lexer->lookahead) != pragma[i]) { is_pragma = false; break; }
102
- lexer->advance(lexer, false);
103
- }
104
- if (is_pragma && (lexer->lookahead == ' ' || lexer->lookahead == '\t' ||
105
- lexer->lookahead == '\r' || lexer->lookahead == '\n' ||
106
- lexer->lookahead == '\0')) {
107
- // Skip rest of this #pragma line
108
- while (lexer->lookahead != '\0' && lexer->lookahead != '\n') {
109
- lexer->advance(lexer, false);
110
- }
111
- continue; // loop back to skip more whitespace/#pragma lines
112
- } else {
113
- // '#' was followed by something other than 'pragma' — check for keyword
114
- // We already consumed '#'; now match rest of keyword (which starts with '#')
115
- // Since we consumed '#', match from position 1 of keyword
116
- if (keyword[0] != '#') return false;
117
- const char *rest = keyword + 1;
118
- for (int i = 0; rest[i] != '\0'; i++) {
119
- if (towlower(lexer->lookahead) != rest[i]) return false;
120
- lexer->advance(lexer, false);
121
- }
122
- if (is_identifier_char(lexer->lookahead)) return false;
123
- return true;
124
- }
125
- } else {
126
- // Not '#' — try to match keyword directly
127
- for (int i = 0; keyword[i] != '\0'; i++) {
128
- if (towlower(lexer->lookahead) != keyword[i]) return false;
129
- lexer->advance(lexer, false);
130
- }
131
- if (is_identifier_char(lexer->lookahead)) return false;
132
- return true;
133
192
  }
134
193
  }
135
194
  }
@@ -217,18 +276,21 @@ bool tree_sitter_al_external_scanner_scan(
217
276
  }
218
277
  }
219
278
 
220
- // 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)
221
281
  //
222
- // '#' handling: peek_keyword_ci is called with "#endif" (the full string
223
- // including '#'). PREPROC_OPEN/CLOSE manually advance past '#' before calling
224
- // 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.
225
286
  //
226
- // #pragma lines are transparent extras we skip them when scanning ahead for #endif.
287
+ // Comments, #pragma, #region, #define and friends are all extras, hence all
288
+ // transparent here (see skip_whitespace_and_comments/TRANSPARENT_DIRECTIVES).
227
289
  if (valid_symbols[PREPROC_SPLIT_BEGIN] && state->depth > 0) {
228
290
  skip_whitespace(lexer);
229
291
  if (read_keyword_ci(lexer, "begin")) {
230
292
  lexer->mark_end(lexer); // token covers only 'begin'
231
- if (peek_keyword_ci_skip_pragma(lexer, "#endif")) {
293
+ if (peek_directive_ci_skip_extras(lexer, DIRECTIVE_ENDIF)) {
232
294
  lexer->result_symbol = PREPROC_SPLIT_BEGIN;
233
295
  return true;
234
296
  }
@@ -246,26 +308,17 @@ bool tree_sitter_al_external_scanner_scan(
246
308
  skip_whitespace(lexer);
247
309
  if (read_keyword_ci(lexer, "end")) {
248
310
  lexer->mark_end(lexer); // token covers only 'end'
249
- // Check for ';' then whitespace then '#else' or '#endif'
250
- while (lexer->lookahead == ' ' || lexer->lookahead == '\t' ||
251
- lexer->lookahead == '\r' || lexer->lookahead == '\n' ||
252
- lexer->lookahead == '\f') {
253
- lexer->advance(lexer, false);
254
- }
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;
255
317
  if (lexer->lookahead == ';') {
256
318
  lexer->advance(lexer, false);
257
- // Now check for #else or #endif after whitespace
258
- while (lexer->lookahead == ' ' || lexer->lookahead == '\t' ||
259
- lexer->lookahead == '\r' || lexer->lookahead == '\n' ||
260
- lexer->lookahead == '\f') {
261
- lexer->advance(lexer, false);
262
- }
263
- if (lexer->lookahead == '#') {
264
- lexer->advance(lexer, false);
265
- if (read_keyword_ci(lexer, "else") || read_keyword_ci(lexer, "endif")) {
266
- lexer->result_symbol = PREPROC_SPLIT_END;
267
- return true;
268
- }
319
+ if (peek_directive_ci_skip_extras(lexer, DIRECTIVE_ELSE_ENDIF)) {
320
+ lexer->result_symbol = PREPROC_SPLIT_END;
321
+ return true;
269
322
  }
270
323
  }
271
324
  // 'end' found but not followed by ; then #else/#endif — return false.
@@ -286,9 +339,9 @@ bool tree_sitter_al_external_scanner_scan(
286
339
  lexer->mark_end(lexer);
287
340
 
288
341
  // Scan past the attribute content to find the closing ']'.
289
- // 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.
290
344
  int bracket_depth = 1;
291
- int paren_depth = 0;
292
345
  bool in_string = false;
293
346
 
294
347
  while (bracket_depth > 0 && lexer->lookahead != 0) {
@@ -304,12 +357,14 @@ bool tree_sitter_al_external_scanner_scan(
304
357
  continue;
305
358
  }
306
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
+ }
307
366
  if (lexer->lookahead == '\'') {
308
367
  in_string = true;
309
- } else if (lexer->lookahead == '(') {
310
- paren_depth++;
311
- } else if (lexer->lookahead == ')') {
312
- if (paren_depth > 0) paren_depth--;
313
368
  } else if (lexer->lookahead == '[') {
314
369
  bracket_depth++;
315
370
  } else if (lexer->lookahead == ']') {
@@ -358,6 +413,10 @@ bool tree_sitter_al_external_scanner_scan(
358
413
  continue;
359
414
  }
360
415
  } else {
416
+ if (lexer->lookahead == '/') {
417
+ skip_comment(lexer);
418
+ continue;
419
+ }
361
420
  if (lexer->lookahead == '\'') {
362
421
  inner_in_string = true;
363
422
  } else if (lexer->lookahead == '[') {
@@ -384,33 +443,28 @@ bool tree_sitter_al_external_scanner_scan(
384
443
  // (fall through to the identifier/quoted-identifier checks below)
385
444
  }
386
445
 
387
- if (lexer->lookahead == '"') {
388
- // Quoted identifier scan to closing '"', check for ':'
389
- lexer->advance(lexer, false);
390
- while (lexer->lookahead != 0 && lexer->lookahead != '"') {
391
- lexer->advance(lexer, false);
392
- }
393
- if (lexer->lookahead == '"') {
394
- lexer->advance(lexer, false);
395
- // Skip whitespace
396
- while (lexer->lookahead == ' ' || lexer->lookahead == '\t') {
397
- lexer->advance(lexer, false);
398
- }
399
- if (lexer->lookahead == ':') {
400
- lexer->result_symbol = VAR_ATTRIBUTE_OPEN;
401
- return true;
402
- }
403
- }
404
- return false;
405
- }
406
-
407
- if (is_identifier_start(lexer->lookahead)) {
408
- // Identifier — scan it, then check for ':' (or ',' for multi-variable decls)
409
- // 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)) {
410
452
  while (true) {
411
- while (is_identifier_char(lexer->lookahead)) {
453
+ if (lexer->lookahead == '"') {
412
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;
413
466
  }
467
+
414
468
  // Skip whitespace
415
469
  while (lexer->lookahead == ' ' || lexer->lookahead == '\t') {
416
470
  lexer->advance(lexer, false);
@@ -419,29 +473,12 @@ bool tree_sitter_al_external_scanner_scan(
419
473
  lexer->result_symbol = VAR_ATTRIBUTE_OPEN;
420
474
  return true;
421
475
  }
422
- if (lexer->lookahead == ',') {
423
- // 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') {
424
480
  lexer->advance(lexer, false);
425
- while (lexer->lookahead == ' ' || lexer->lookahead == '\t') {
426
- lexer->advance(lexer, false);
427
- }
428
- if (lexer->lookahead == '"') {
429
- // Quoted identifier in multi-var list
430
- lexer->advance(lexer, false);
431
- while (lexer->lookahead != 0 && lexer->lookahead != '"') {
432
- lexer->advance(lexer, false);
433
- }
434
- if (lexer->lookahead == '"') lexer->advance(lexer, false);
435
- while (lexer->lookahead == ' ' || lexer->lookahead == '\t') {
436
- lexer->advance(lexer, false);
437
- }
438
- // Loop back to check for ':', ',' etc.
439
- continue;
440
- }
441
- if (!is_identifier_start(lexer->lookahead)) return false;
442
- continue;
443
481
  }
444
- return false;
445
482
  }
446
483
  }
447
484
 
@@ -504,10 +541,13 @@ bool tree_sitter_al_external_scanner_scan(
504
541
  }
505
542
  }
506
543
 
507
- // Did not match continue_as_identifier
508
- // Note: we can't fall through to PROPERTY_NAME here because we've
509
- // already consumed characters. The scanner will be called again
510
- // 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).
511
551
  return false;
512
552
  }
513
553
 
@@ -534,11 +574,13 @@ bool tree_sitter_al_external_scanner_scan(
534
574
  // Mark end of identifier (before whitespace/equals)
535
575
  lexer->mark_end(lexer);
536
576
 
537
- // Skip whitespace
538
- while (lexer->lookahead == ' ' || lexer->lookahead == '\t' ||
539
- lexer->lookahead == '\r' || lexer->lookahead == '\f') {
540
- lexer->advance(lexer, false);
541
- }
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;
542
584
 
543
585
  // Check for = but not :=
544
586
  if (lexer->lookahead == '=') {
Binary file