@dekobon/tree-sitter-groovy 0.4.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
@@ -1,8 +1,20 @@
1
1
  // Hand-written external scanner for tree-sitter-groovy.
2
2
  //
3
- // Handles §6.2 slashy-string-start and §6.6 line/block/groovydoc
4
- // comments. Both start with `/`; combining the dispatch keeps the
5
- // in-grammar lexer from partially consuming the leading `/`.
3
+ // Handles §6.2 slashy-string-start, §6.6 line/block/groovydoc
4
+ // comments, and the §6.7 contextual `record` keyword. The first two
5
+ // start with `/`; combining their dispatch keeps the in-grammar lexer
6
+ // from partially consuming the leading `/`. The third is a look-ahead
7
+ // decision the grammar cannot express: `record` is a keyword only
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.
6
18
  //
7
19
  // Stateless — `create` returns NULL and the runtime calls
8
20
  // `destroy` / `serialize` / `deserialize` as no-ops. When a future
@@ -10,13 +22,113 @@
10
22
 
11
23
  #include "tree_sitter/parser.h"
12
24
 
25
+ #include <string.h>
26
+
13
27
  enum TokenType {
14
28
  SLASHY_STRING_START,
15
29
  LINE_COMMENT,
16
30
  BLOCK_COMMENT,
17
31
  GROOVYDOC_COMMENT,
32
+ RECORD_KEYWORD,
33
+ COMMAND_NAMED_KEY,
34
+ SUBSCRIPT_OPEN,
35
+ CAST_CLOSE,
36
+ NOT_IN,
37
+ STRING_BODY,
38
+ ERROR_SENTINEL,
18
39
  };
19
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
+
20
132
  static inline void advance(TSLexer *lexer) {
21
133
  lexer->advance(lexer, false);
22
134
  }
@@ -25,6 +137,163 @@ static inline bool is_whitespace(int32_t c) {
25
137
  return c == ' ' || c == '\t' || c == '\n' || c == '\r';
26
138
  }
27
139
 
140
+ static inline bool is_inline_space(int32_t c) {
141
+ return c == ' ' || c == '\t';
142
+ }
143
+
144
+ static inline bool is_line_break(int32_t c) {
145
+ return c == '\n' || c == '\r';
146
+ }
147
+
148
+ // Skips the trivia Groovy's lexer hides inside a record header:
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.
154
+ static bool skip_header_trivia(TSLexer *lexer, bool newlines) {
155
+ for (;;) {
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
+ }
163
+ advance(lexer);
164
+ }
165
+ if (lexer->lookahead != '/') {
166
+ return true;
167
+ }
168
+ advance(lexer);
169
+ if (lexer->lookahead == '*') {
170
+ advance(lexer);
171
+ // EOF-bounded: an unterminated comment ends the scan here.
172
+ for (;;) {
173
+ if (lexer->eof(lexer)) {
174
+ return false;
175
+ }
176
+ if (lexer->lookahead == '*') {
177
+ advance(lexer);
178
+ if (lexer->lookahead == '/') {
179
+ advance(lexer);
180
+ break;
181
+ }
182
+ } else {
183
+ advance(lexer);
184
+ }
185
+ }
186
+ } else if (newlines && lexer->lookahead == '/') {
187
+ while (!lexer->eof(lexer) && !is_line_break(lexer->lookahead)) {
188
+ advance(lexer);
189
+ }
190
+ } else {
191
+ return false;
192
+ }
193
+ }
194
+ }
195
+
196
+ // SPECIFICATION.md §6.7 — `record` is a keyword only at the start of
197
+ // a record header: `record`, a name on the same line, then `(` with
198
+ // Groovy's lexer trivia allowed in between, or `<` straight after the
199
+ // name (type parameters; the grammar's `type_parameters` opener is
200
+ // `token.immediate` too). Everywhere else the word
201
+ // is an identifier (`record.key()`, `private Record record`,
202
+ // `public void record()`), as Apache's `identifier` rule allows. The
203
+ // grammar cannot make that choice: with `word: $.identifier` the
204
+ // keyword wins wherever it has an action, and once
205
+ // record_declaration is a class member that is every member start.
206
+ //
207
+ // The scanner has already consumed `record` when this is called.
208
+ // `mark_end` fixes the token there; the look-ahead that follows is
209
+ // read but not kept. Between `record` and the name only spaces, tabs
210
+ // and block comments may appear: `classDeclaration` in
211
+ // GroovyParser.g4 has no `nls` there, so `record` at the end of a line
212
+ // is never a keyword, which keeps `private Record record` followed by
213
+ // a constructor on the next line a field and a constructor. Before
214
+ // `(` the rule has `nls`, so line breaks and line comments are
215
+ // skipped there too. A command-style call `record Point(1, 2)`
216
+ // matches the pattern and misfires; §6.7 lists the residuals.
217
+ static bool scan_record_header(TSLexer *lexer) {
218
+ if (is_identifier_char(lexer->lookahead)) {
219
+ return false;
220
+ }
221
+ lexer->mark_end(lexer);
222
+ if (!is_inline_space(lexer->lookahead) && lexer->lookahead != '/'
223
+ && lexer->lookahead != '\\') {
224
+ return false;
225
+ }
226
+ if (!skip_header_trivia(lexer, false)) {
227
+ return false;
228
+ }
229
+ if (!is_identifier_start(lexer->lookahead)) {
230
+ return false;
231
+ }
232
+ // EOF-bounded: lookahead is 0 at end of input, which fails the
233
+ // predicate.
234
+ while (is_identifier_char(lexer->lookahead)) {
235
+ advance(lexer);
236
+ }
237
+ if (lexer->lookahead == '<') {
238
+ return true;
239
+ }
240
+ if (!skip_header_trivia(lexer, true)) {
241
+ return false;
242
+ }
243
+ return lexer->lookahead == '(';
244
+ }
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
+
28
297
  void *tree_sitter_groovy_external_scanner_create(void) {
29
298
  return NULL;
30
299
  }
@@ -59,6 +328,53 @@ bool tree_sitter_groovy_external_scanner_scan(
59
328
  ) {
60
329
  (void)payload;
61
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
+
62
378
  // SPECIFICATION.md §6.6 — line / block / groovydoc comments
63
379
  // and §6.2 slashy strings. Both start with `/`; combining their
64
380
  // dispatch keeps the in-grammar lexer from partially consuming
@@ -67,17 +383,81 @@ bool tree_sitter_groovy_external_scanner_scan(
67
383
  // in-grammar `\s` SKIP rule — without this, tree-sitter's lexer
68
384
  // eats `\n`, does NOT re-invoke the external scanner, then
69
385
  // fails on the following `/` and consumes it through error
70
- // recovery.
386
+ // recovery. §6.7 `record` shares the whitespace skip and is
387
+ // dispatched on `r` before the `/` branches.
71
388
  bool wants_comment = valid_symbols[LINE_COMMENT]
72
389
  || valid_symbols[BLOCK_COMMENT]
73
390
  || valid_symbols[GROOVYDOC_COMMENT];
74
391
  bool wants_slashy = valid_symbols[SLASHY_STRING_START];
75
- if (!wants_comment && !wants_slashy) {
392
+ bool wants_record = valid_symbols[RECORD_KEYWORD];
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) {
76
399
  return false;
77
400
  }
78
401
 
79
- while (is_whitespace(lexer->lookahead)) {
80
- 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
+ }
412
+ }
413
+ if (lexer->lookahead == 'r' && wants_record) {
414
+ // An `r` can never open a comment or a slashy string, so
415
+ // every path out of this branch returns. On `false` the
416
+ // runtime rewinds to the token start and the in-grammar
417
+ // lexer reads the word as an identifier.
418
+ static const char keyword[] = "record";
419
+ for (const char *c = keyword; *c != '\0'; c++) {
420
+ if (lexer->lookahead != *c) {
421
+ return false;
422
+ }
423
+ advance(lexer);
424
+ }
425
+ if (!scan_record_header(lexer)) {
426
+ return false;
427
+ }
428
+ lexer->result_symbol = RECORD_KEYWORD;
429
+ return true;
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;
81
461
  }
82
462
  if (lexer->lookahead != '/') {
83
463
  return false;
@@ -97,8 +477,9 @@ bool tree_sitter_groovy_external_scanner_scan(
97
477
  advance(lexer);
98
478
  // EOF-bounded loop: lexer->eof() guarantees termination on
99
479
  // unterminated input. Do not tighten without preserving the
100
- // EOF guard.
101
- 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)) {
102
483
  advance(lexer);
103
484
  }
104
485
  lexer->result_symbol = LINE_COMMENT;
@@ -200,7 +581,7 @@ bool tree_sitter_groovy_external_scanner_scan(
200
581
  found_close = true;
201
582
  break;
202
583
  }
203
- if (lexer->lookahead == '\n' || lexer->lookahead == '\r') {
584
+ if (is_line_break(lexer->lookahead)) {
204
585
  break;
205
586
  }
206
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.4.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": [