@dekobon/tree-sitter-groovy 0.5.0 → 0.6.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
@@ -5,7 +5,16 @@
5
5
  // start with `/`; combining their dispatch keeps the in-grammar lexer
6
6
  // from partially consuming the leading `/`. The third is a look-ahead
7
7
  // decision the grammar cannot express: `record` is a keyword only
8
- // when a record header follows it.
8
+ // when a record header follows it. Also handles §6.8
9
+ // COMMAND_NAMED_KEY, an empty token before the key of a named
10
+ // argument in a parenthesis-free call; §6.9 SUBSCRIPT_OPEN, a `[` on
11
+ // the same line as the token before it; §6.11 CAST_CLOSE, a `)` with
12
+ // something after it on its line; and §6.12 NOT_IN, the `!in`
13
+ // operator. STRING_BODY (§6.10) is never emitted; it is valid only
14
+ // inside a string body, where the scanner emits nothing. ERROR_SENTINEL is never emitted either; the parser marks it
15
+ // valid only during error recovery, where COMMAND_NAMED_KEY,
16
+ // SUBSCRIPT_OPEN, CAST_CLOSE, NOT_IN and the STRING_BODY check are
17
+ // suppressed.
9
18
  //
10
19
  // Stateless — `create` returns NULL and the runtime calls
11
20
  // `destroy` / `serialize` / `deserialize` as no-ops. When a future
@@ -13,14 +22,113 @@
13
22
 
14
23
  #include "tree_sitter/parser.h"
15
24
 
25
+ #include <string.h>
26
+
16
27
  enum TokenType {
17
28
  SLASHY_STRING_START,
18
29
  LINE_COMMENT,
19
30
  BLOCK_COMMENT,
20
31
  GROOVYDOC_COMMENT,
21
32
  RECORD_KEYWORD,
33
+ COMMAND_NAMED_KEY,
34
+ SUBSCRIPT_OPEN,
35
+ CAST_CLOSE,
36
+ NOT_IN,
37
+ STRING_BODY,
38
+ ERROR_SENTINEL,
22
39
  };
23
40
 
41
+ // Wider than the grammar's `identifier` token (SPECIFICATION.md
42
+ // §5.2): every non-ASCII code point counts as an identifier
43
+ // character, because the scanner has no Unicode tables. That is safe
44
+ // because the scanner never emits a name. It only looks ahead, for
45
+ // the name of a record header and for the key of a named argument,
46
+ // and the in-grammar lexer then reads the name with the real class.
47
+ // On valid input the two agree: outside strings and comments, a
48
+ // non-ASCII character can only be part of a name. On other input the
49
+ // name the lexer reads is shorter, and the parse fails there.
50
+ static inline bool is_identifier_start(int32_t c) {
51
+ return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
52
+ || c == '_' || c == '$' || c > 0x7F;
53
+ }
54
+
55
+ static inline bool is_identifier_char(int32_t c) {
56
+ return is_identifier_start(c) || (c >= '0' && c <= '9');
57
+ }
58
+
59
+ // SPECIFICATION.md §5.10 — a line escape, `\` followed by a line
60
+ // break, is whitespace to Groovy's lexer (GroovyLexer.g4
61
+ // `WS : ([ \t]+ | LineEscape+)`), so the line after it continues the
62
+ // current one. Skips spaces, tabs and line escapes. Returns false at a
63
+ // `\` that does not start a line escape: the `\` is consumed by then
64
+ // and no token starts with one, so the caller must return false to
65
+ // make the lexer rewind.
66
+ static bool skip_same_line_space(TSLexer *lexer, bool skip) {
67
+ for (;;) {
68
+ if (lexer->lookahead == ' ' || lexer->lookahead == '\t') {
69
+ lexer->advance(lexer, skip);
70
+ continue;
71
+ }
72
+ if (lexer->lookahead != '\\') {
73
+ return true;
74
+ }
75
+ lexer->advance(lexer, skip);
76
+ if (lexer->lookahead == '\r') {
77
+ lexer->advance(lexer, skip);
78
+ }
79
+ if (lexer->lookahead != '\n') {
80
+ return false;
81
+ }
82
+ lexer->advance(lexer, skip);
83
+ }
84
+ }
85
+
86
+ // SPECIFICATION.md §6.8 — decides that the key of a named argument
87
+ // in a parenthesis-free call (`exclude group: 'a'`) starts here: a
88
+ // name on the current line, followed by `:` and not `::`. The token is
89
+ // empty. `mark_end` fixes it at the start of the name, and the
90
+ // in-grammar lexer reads the name itself, so the key is an identifier
91
+ // by the grammar's own definition, non-ASCII letters included. A
92
+ // newline before the key means a new statement (a Spock `where:`
93
+ // label, say), so the caller does not skip one.
94
+ //
95
+ // The caller has skipped the same-line space and seen an identifier
96
+ // start.
97
+ static bool scan_command_named_key(TSLexer *lexer) {
98
+ lexer->mark_end(lexer);
99
+ // Keep the first few chars to reject `case:` / `default:`, which
100
+ // are switch labels, not keys. Apache's `namedArgLabel` accepts both
101
+ // keywords, so `foo default: 1` diverges on purpose: the one-liner
102
+ // `switch (x) { case 1: foo default: bar }` in statements-switch.txt
103
+ // depends on the rejection. A non-ASCII character is stored as 0,
104
+ // which matches neither word; cut to a `char` it could.
105
+ char word[8];
106
+ unsigned len = 0;
107
+ while (is_identifier_char(lexer->lookahead)) {
108
+ if (len < sizeof(word)) {
109
+ word[len] = lexer->lookahead <= 0x7F ? (char)lexer->lookahead : 0;
110
+ }
111
+ len++;
112
+ lexer->advance(lexer, false);
113
+ }
114
+ if ((len == 4 && memcmp(word, "case", 4) == 0)
115
+ || (len == 7 && memcmp(word, "default", 7) == 0)) {
116
+ return false;
117
+ }
118
+ if (!skip_same_line_space(lexer, false)) {
119
+ return false;
120
+ }
121
+ if (lexer->lookahead != ':') {
122
+ return false;
123
+ }
124
+ lexer->advance(lexer, false);
125
+ if (lexer->lookahead == ':') {
126
+ return false;
127
+ }
128
+ lexer->result_symbol = COMMAND_NAMED_KEY;
129
+ return true;
130
+ }
131
+
24
132
  static inline void advance(TSLexer *lexer) {
25
133
  lexer->advance(lexer, false);
26
134
  }
@@ -33,26 +141,25 @@ static inline bool is_inline_space(int32_t c) {
33
141
  return c == ' ' || c == '\t';
34
142
  }
35
143
 
36
- // Mirrors the grammar's `identifier` token, /[A-Za-z_$][A-Za-z0-9_$]*/.
37
- static inline bool is_identifier_start(int32_t c) {
38
- return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
39
- || c == '_' || c == '$';
40
- }
41
-
42
- static inline bool is_identifier_char(int32_t c) {
43
- return is_identifier_start(c) || (c >= '0' && c <= '9');
144
+ static inline bool is_line_break(int32_t c) {
145
+ return c == '\n' || c == '\r';
44
146
  }
45
147
 
46
148
  // Skips the trivia Groovy's lexer hides inside a record header:
47
- // spaces, tabs and block comments, plus line breaks and line comments
48
- // when `newlines` is set. Returns false on a `/` that opens neither
49
- // comment kind (a slashy string or division cannot be part of a
50
- // header) and on an unterminated block comment.
149
+ // spaces, tabs, line escapes and block comments, plus line breaks and
150
+ // line comments when `newlines` is set. Returns false on a `/` that
151
+ // opens neither comment kind (a slashy string or division cannot be
152
+ // part of a header), on a `\` that is not a line escape, and on an
153
+ // unterminated block comment.
51
154
  static bool skip_header_trivia(TSLexer *lexer, bool newlines) {
52
155
  for (;;) {
53
- while (is_inline_space(lexer->lookahead)
54
- || (newlines
55
- && (lexer->lookahead == '\n' || lexer->lookahead == '\r'))) {
156
+ for (;;) {
157
+ if (!skip_same_line_space(lexer, false)) {
158
+ return false;
159
+ }
160
+ if (!newlines || !is_line_break(lexer->lookahead)) {
161
+ break;
162
+ }
56
163
  advance(lexer);
57
164
  }
58
165
  if (lexer->lookahead != '/') {
@@ -77,7 +184,7 @@ static bool skip_header_trivia(TSLexer *lexer, bool newlines) {
77
184
  }
78
185
  }
79
186
  } else if (newlines && lexer->lookahead == '/') {
80
- while (!lexer->eof(lexer) && lexer->lookahead != '\n') {
187
+ while (!lexer->eof(lexer) && !is_line_break(lexer->lookahead)) {
81
188
  advance(lexer);
82
189
  }
83
190
  } else {
@@ -112,7 +219,8 @@ static bool scan_record_header(TSLexer *lexer) {
112
219
  return false;
113
220
  }
114
221
  lexer->mark_end(lexer);
115
- if (!is_inline_space(lexer->lookahead) && lexer->lookahead != '/') {
222
+ if (!is_inline_space(lexer->lookahead) && lexer->lookahead != '/'
223
+ && lexer->lookahead != '\\') {
116
224
  return false;
117
225
  }
118
226
  if (!skip_header_trivia(lexer, false)) {
@@ -135,6 +243,57 @@ static bool scan_record_header(TSLexer *lexer) {
135
243
  return lexer->lookahead == '(';
136
244
  }
137
245
 
246
+ // SPECIFICATION.md §6.11 — whether something follows a `)` on its
247
+ // line. The `)` is already consumed when this is called.
248
+ // `castParExpression castOperandExpression` in GroovyParser.g4 has no
249
+ // `nls` between the two, so where a line break can end a statement,
250
+ // `x = (a)` followed by `foo()` on the next line is two statements.
251
+ // The grammar ranks a cast by this answer; it does not forbid one,
252
+ // because inside `(…)` and `[…]` a line break ends nothing. Spaces,
253
+ // tabs, line escapes and block comments that close on the line may
254
+ // come first. A line comment ends the line.
255
+ static bool cast_operand_follows(TSLexer *lexer) {
256
+ lexer->mark_end(lexer);
257
+ for (;;) {
258
+ if (!skip_same_line_space(lexer, false)) {
259
+ return true;
260
+ }
261
+ if (lexer->eof(lexer) || is_line_break(lexer->lookahead)) {
262
+ return false;
263
+ }
264
+ if (lexer->lookahead != '/') {
265
+ return true;
266
+ }
267
+ advance(lexer);
268
+ if (lexer->lookahead == '/') {
269
+ return false;
270
+ }
271
+ if (lexer->lookahead != '*') {
272
+ return true;
273
+ }
274
+ // A block comment that closes on this line is skipped, and the
275
+ // answer depends on what follows it. One that runs past the
276
+ // end of the line counts as something on the line: Groovy's
277
+ // lexer drops the comment with its line breaks, and reading
278
+ // on would scan an unterminated comment to the end of input.
279
+ advance(lexer);
280
+ for (;;) {
281
+ if (lexer->eof(lexer) || is_line_break(lexer->lookahead)) {
282
+ return true;
283
+ }
284
+ if (lexer->lookahead == '*') {
285
+ advance(lexer);
286
+ if (lexer->lookahead == '/') {
287
+ advance(lexer);
288
+ break;
289
+ }
290
+ } else {
291
+ advance(lexer);
292
+ }
293
+ }
294
+ }
295
+ }
296
+
138
297
  void *tree_sitter_groovy_external_scanner_create(void) {
139
298
  return NULL;
140
299
  }
@@ -169,6 +328,53 @@ bool tree_sitter_groovy_external_scanner_scan(
169
328
  ) {
170
329
  (void)payload;
171
330
 
331
+ // SPECIFICATION.md §6.10 — STRING_BODY is valid only between the
332
+ // quotes of a string whose body is lexed in pieces. Comments are
333
+ // extras, so the parser offers them there as well, and this
334
+ // function is called at every piece boundary. Without this check a
335
+ // body that starts with `//` or `/*`, or has one after an
336
+ // interpolation, lost that text to a comment token. The body
337
+ // belongs to the in-grammar lexer.
338
+ if (valid_symbols[STRING_BODY] && !valid_symbols[ERROR_SENTINEL]) {
339
+ return false;
340
+ }
341
+
342
+ // Same-line tokens: SUBSCRIPT_OPEN (§5.10) and COMMAND_NAMED_KEY
343
+ // (§5.5). Both skip only spaces, tabs and line escapes, the
344
+ // same-line whitespace of Groovy's lexer; at a newline we fall
345
+ // through to the branches below. Their first characters (`[` vs. an
346
+ // identifier start) are disjoint, so one whitespace skip serves
347
+ // both.
348
+ bool wants_subscript = valid_symbols[SUBSCRIPT_OPEN];
349
+ bool wants_named_key = valid_symbols[COMMAND_NAMED_KEY];
350
+ if ((wants_subscript || wants_named_key)
351
+ && !valid_symbols[ERROR_SENTINEL]) {
352
+ if (!skip_same_line_space(lexer, true)) {
353
+ return false;
354
+ }
355
+ // SPECIFICATION.md §5.10 — a `[` on the current line can continue
356
+ // the expression (subscript or array type). After a newline the
357
+ // in-grammar `[` starts a list or map literal instead.
358
+ if (wants_subscript && lexer->lookahead == '[') {
359
+ lexer->advance(lexer, false);
360
+ lexer->result_symbol = SUBSCRIPT_OPEN;
361
+ return true;
362
+ }
363
+ // An identifier is either the key or, once consumed, not something
364
+ // the code below could start (a comment or slashy string), so stop
365
+ // here. Falling through after the scan advanced past `b` in
366
+ // `foo a, b` and a newline made the comment branch emit `b` as a
367
+ // line_comment, since mark_end had already been called.
368
+ // RECORD_KEYWORD can be valid here too, after a statement-start
369
+ // identifier (`foo` then `record R(…)`). The key wins on the same
370
+ // line: `foo record: 1` is a named argument, while a record header
371
+ // there is not valid Groovy. A `record` on the next line is not
372
+ // reached here, since a newline is not skipped.
373
+ if (wants_named_key && is_identifier_start(lexer->lookahead)) {
374
+ return scan_command_named_key(lexer);
375
+ }
376
+ }
377
+
172
378
  // SPECIFICATION.md §6.6 — line / block / groovydoc comments
173
379
  // and §6.2 slashy strings. Both start with `/`; combining their
174
380
  // dispatch keeps the in-grammar lexer from partially consuming
@@ -184,12 +390,25 @@ bool tree_sitter_groovy_external_scanner_scan(
184
390
  || valid_symbols[GROOVYDOC_COMMENT];
185
391
  bool wants_slashy = valid_symbols[SLASHY_STRING_START];
186
392
  bool wants_record = valid_symbols[RECORD_KEYWORD];
187
- if (!wants_comment && !wants_slashy && !wants_record) {
393
+ bool wants_cast_close = valid_symbols[CAST_CLOSE]
394
+ && !valid_symbols[ERROR_SENTINEL];
395
+ bool wants_not_in = valid_symbols[NOT_IN]
396
+ && !valid_symbols[ERROR_SENTINEL];
397
+ if (!wants_comment && !wants_slashy && !wants_record
398
+ && !wants_cast_close && !wants_not_in) {
188
399
  return false;
189
400
  }
190
401
 
191
- while (is_whitespace(lexer->lookahead)) {
192
- lexer->advance(lexer, true);
402
+ // A line escape is whitespace too, so a comment, a slashy string or
403
+ // `record` may follow one.
404
+ while (is_whitespace(lexer->lookahead) || lexer->lookahead == '\\') {
405
+ if (lexer->lookahead == '\\') {
406
+ if (!skip_same_line_space(lexer, true)) {
407
+ return false;
408
+ }
409
+ } else {
410
+ lexer->advance(lexer, true);
411
+ }
193
412
  }
194
413
  if (lexer->lookahead == 'r' && wants_record) {
195
414
  // An `r` can never open a comment or a slashy string, so
@@ -209,6 +428,37 @@ bool tree_sitter_groovy_external_scanner_scan(
209
428
  lexer->result_symbol = RECORD_KEYWORD;
210
429
  return true;
211
430
  }
431
+ if (lexer->lookahead == '!' && wants_not_in) {
432
+ // SPECIFICATION.md §6.12 — `!in` is an operator only before
433
+ // whitespace or an opening bracket (GroovyLexer.g4 `NOT_IN`),
434
+ // so that `a !inside` is `!` applied to a name. The character
435
+ // after it is read and not taken: `a !in[1, 2]`.
436
+ static const char not_in[] = "!in";
437
+ for (const char *c = not_in; *c != '\0'; c++) {
438
+ if (lexer->lookahead != *c) {
439
+ return false;
440
+ }
441
+ advance(lexer);
442
+ }
443
+ int32_t next = lexer->lookahead;
444
+ if (!is_whitespace(next) && next != '[' && next != '('
445
+ && next != '{') {
446
+ return false;
447
+ }
448
+ lexer->result_symbol = NOT_IN;
449
+ return true;
450
+ }
451
+ if (lexer->lookahead == ')' && wants_cast_close) {
452
+ // On `false` the runtime rewinds and the in-grammar lexer reads
453
+ // a plain `)`. A cast that closes with one ranks below a
454
+ // parenthesized expression.
455
+ advance(lexer);
456
+ if (!cast_operand_follows(lexer)) {
457
+ return false;
458
+ }
459
+ lexer->result_symbol = CAST_CLOSE;
460
+ return true;
461
+ }
212
462
  if (lexer->lookahead != '/') {
213
463
  return false;
214
464
  }
@@ -227,8 +477,9 @@ bool tree_sitter_groovy_external_scanner_scan(
227
477
  advance(lexer);
228
478
  // EOF-bounded loop: lexer->eof() guarantees termination on
229
479
  // unterminated input. Do not tighten without preserving the
230
- // EOF guard.
231
- while (!lexer->eof(lexer) && lexer->lookahead != '\n') {
480
+ // EOF guard. A lone `\r` ends the line too, as in Groovy's
481
+ // lexer (`LineTerminator`), so the comment stops before it.
482
+ while (!lexer->eof(lexer) && !is_line_break(lexer->lookahead)) {
232
483
  advance(lexer);
233
484
  }
234
485
  lexer->result_symbol = LINE_COMMENT;
@@ -330,7 +581,7 @@ bool tree_sitter_groovy_external_scanner_scan(
330
581
  found_close = true;
331
582
  break;
332
583
  }
333
- if (lexer->lookahead == '\n' || lexer->lookahead == '\r') {
584
+ if (is_line_break(lexer->lookahead)) {
334
585
  break;
335
586
  }
336
587
  advance(lexer);
package/tree-sitter.json CHANGED
@@ -9,11 +9,12 @@
9
9
  "file-types": ["groovy", "gradle", "gvy", "gy", "jenkinsfile"],
10
10
  "injection-regex": "^groovy$",
11
11
  "highlights": "queries/groovy/highlights.scm",
12
+ "tags": "queries/groovy/tags.scm",
12
13
  "class-name": "TreeSitterGroovy"
13
14
  }
14
15
  ],
15
16
  "metadata": {
16
- "version": "0.5.0",
17
+ "version": "0.6.0",
17
18
  "license": "Apache-2.0 OR MIT",
18
19
  "description": "Apache Groovy grammar for tree-sitter (dekobon fork)",
19
20
  "authors": [