@sshadows/tree-sitter-al 3.2.1 → 3.3.0

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
@@ -85,44 +85,24 @@ static bool peek_keyword_ci(TSLexer *lexer, const char *keyword) {
85
85
  return true;
86
86
  }
87
87
 
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) {
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.
91
+ static const char *const TRANSPARENT_DIRECTIVES[] = {
92
+ "pragma", "endregion", "region", "define", "undef", NULL,
93
+ };
94
+
95
+ // Skip whitespace and any transparent-directive lines, then peek for a keyword.
96
+ //
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) {
92
103
  while (true) {
93
104
  skip_whitespace(lexer);
94
- // Check for '#pragma' line — skip entire line
95
- if (lexer->lookahead == '#') {
96
- 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 {
105
+ if (lexer->lookahead != '#') {
126
106
  // Not '#' — try to match keyword directly
127
107
  for (int i = 0; keyword[i] != '\0'; i++) {
128
108
  if (towlower(lexer->lookahead) != keyword[i]) return false;
@@ -131,6 +111,38 @@ static bool peek_keyword_ci_skip_pragma(TSLexer *lexer, const char *keyword) {
131
111
  if (is_identifier_char(lexer->lookahead)) return false;
132
112
  return true;
133
113
  }
114
+
115
+ lexer->advance(lexer, false);
116
+ // Horizontal whitespace only: '# pragma' is one directive, but '#' and a
117
+ // word on the NEXT line are not (matching the extras regexes' `[ \t]*`).
118
+ while (lexer->lookahead == ' ' || lexer->lookahead == '\t') {
119
+ lexer->advance(lexer, false);
120
+ }
121
+
122
+ // Read the directive word. Longest AL directive is "endregion" (9).
123
+ char word[16];
124
+ size_t len = 0;
125
+ while (is_identifier_char(lexer->lookahead)) {
126
+ if (len < sizeof(word) - 1) word[len] = (char)towlower(lexer->lookahead);
127
+ len++;
128
+ lexer->advance(lexer, false);
129
+ }
130
+ if (len >= sizeof(word)) return false; // too long to be any directive
131
+ word[len] = '\0';
132
+
133
+ // The caller's keyword includes the '#' we just consumed.
134
+ if (keyword[0] == '#' && strcmp(word, keyword + 1) == 0) return true;
135
+
136
+ bool transparent = false;
137
+ for (int i = 0; TRANSPARENT_DIRECTIVES[i] != NULL; i++) {
138
+ if (strcmp(word, TRANSPARENT_DIRECTIVES[i]) == 0) { transparent = true; break; }
139
+ }
140
+ if (!transparent) return false;
141
+
142
+ // Skip the rest of this directive's line, then look again.
143
+ while (lexer->lookahead != '\0' && lexer->lookahead != '\n') {
144
+ lexer->advance(lexer, false);
145
+ }
134
146
  }
135
147
  }
136
148
 
@@ -223,12 +235,13 @@ bool tree_sitter_al_external_scanner_scan(
223
235
  // including '#'). PREPROC_OPEN/CLOSE manually advance past '#' before calling
224
236
  // read_keyword_ci("if"/"endif"). These are DIFFERENT conventions — do not mix.
225
237
  //
226
- // #pragma lines are transparent extras — we skip them when scanning ahead for #endif.
238
+ // #pragma/#region/#define and friends are transparent extras — we skip them
239
+ // when scanning ahead for #endif (see TRANSPARENT_DIRECTIVES).
227
240
  if (valid_symbols[PREPROC_SPLIT_BEGIN] && state->depth > 0) {
228
241
  skip_whitespace(lexer);
229
242
  if (read_keyword_ci(lexer, "begin")) {
230
243
  lexer->mark_end(lexer); // token covers only 'begin'
231
- if (peek_keyword_ci_skip_pragma(lexer, "#endif")) {
244
+ if (peek_keyword_ci_skip_extras(lexer, "#endif")) {
232
245
  lexer->result_symbol = PREPROC_SPLIT_BEGIN;
233
246
  return true;
234
247
  }
Binary file