@dekobon/tree-sitter-groovy 0.3.0 → 0.5.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,11 @@
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.
6
9
  //
7
10
  // Stateless — `create` returns NULL and the runtime calls
8
11
  // `destroy` / `serialize` / `deserialize` as no-ops. When a future
@@ -15,6 +18,7 @@ enum TokenType {
15
18
  LINE_COMMENT,
16
19
  BLOCK_COMMENT,
17
20
  GROOVYDOC_COMMENT,
21
+ RECORD_KEYWORD,
18
22
  };
19
23
 
20
24
  static inline void advance(TSLexer *lexer) {
@@ -25,6 +29,112 @@ static inline bool is_whitespace(int32_t c) {
25
29
  return c == ' ' || c == '\t' || c == '\n' || c == '\r';
26
30
  }
27
31
 
32
+ static inline bool is_inline_space(int32_t c) {
33
+ return c == ' ' || c == '\t';
34
+ }
35
+
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');
44
+ }
45
+
46
+ // 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.
51
+ static bool skip_header_trivia(TSLexer *lexer, bool newlines) {
52
+ for (;;) {
53
+ while (is_inline_space(lexer->lookahead)
54
+ || (newlines
55
+ && (lexer->lookahead == '\n' || lexer->lookahead == '\r'))) {
56
+ advance(lexer);
57
+ }
58
+ if (lexer->lookahead != '/') {
59
+ return true;
60
+ }
61
+ advance(lexer);
62
+ if (lexer->lookahead == '*') {
63
+ advance(lexer);
64
+ // EOF-bounded: an unterminated comment ends the scan here.
65
+ for (;;) {
66
+ if (lexer->eof(lexer)) {
67
+ return false;
68
+ }
69
+ if (lexer->lookahead == '*') {
70
+ advance(lexer);
71
+ if (lexer->lookahead == '/') {
72
+ advance(lexer);
73
+ break;
74
+ }
75
+ } else {
76
+ advance(lexer);
77
+ }
78
+ }
79
+ } else if (newlines && lexer->lookahead == '/') {
80
+ while (!lexer->eof(lexer) && lexer->lookahead != '\n') {
81
+ advance(lexer);
82
+ }
83
+ } else {
84
+ return false;
85
+ }
86
+ }
87
+ }
88
+
89
+ // SPECIFICATION.md §6.7 — `record` is a keyword only at the start of
90
+ // a record header: `record`, a name on the same line, then `(` with
91
+ // Groovy's lexer trivia allowed in between, or `<` straight after the
92
+ // name (type parameters; the grammar's `type_parameters` opener is
93
+ // `token.immediate` too). Everywhere else the word
94
+ // is an identifier (`record.key()`, `private Record record`,
95
+ // `public void record()`), as Apache's `identifier` rule allows. The
96
+ // grammar cannot make that choice: with `word: $.identifier` the
97
+ // keyword wins wherever it has an action, and once
98
+ // record_declaration is a class member that is every member start.
99
+ //
100
+ // The scanner has already consumed `record` when this is called.
101
+ // `mark_end` fixes the token there; the look-ahead that follows is
102
+ // read but not kept. Between `record` and the name only spaces, tabs
103
+ // and block comments may appear: `classDeclaration` in
104
+ // GroovyParser.g4 has no `nls` there, so `record` at the end of a line
105
+ // is never a keyword, which keeps `private Record record` followed by
106
+ // a constructor on the next line a field and a constructor. Before
107
+ // `(` the rule has `nls`, so line breaks and line comments are
108
+ // skipped there too. A command-style call `record Point(1, 2)`
109
+ // matches the pattern and misfires; §6.7 lists the residuals.
110
+ static bool scan_record_header(TSLexer *lexer) {
111
+ if (is_identifier_char(lexer->lookahead)) {
112
+ return false;
113
+ }
114
+ lexer->mark_end(lexer);
115
+ if (!is_inline_space(lexer->lookahead) && lexer->lookahead != '/') {
116
+ return false;
117
+ }
118
+ if (!skip_header_trivia(lexer, false)) {
119
+ return false;
120
+ }
121
+ if (!is_identifier_start(lexer->lookahead)) {
122
+ return false;
123
+ }
124
+ // EOF-bounded: lookahead is 0 at end of input, which fails the
125
+ // predicate.
126
+ while (is_identifier_char(lexer->lookahead)) {
127
+ advance(lexer);
128
+ }
129
+ if (lexer->lookahead == '<') {
130
+ return true;
131
+ }
132
+ if (!skip_header_trivia(lexer, true)) {
133
+ return false;
134
+ }
135
+ return lexer->lookahead == '(';
136
+ }
137
+
28
138
  void *tree_sitter_groovy_external_scanner_create(void) {
29
139
  return NULL;
30
140
  }
@@ -67,18 +177,38 @@ bool tree_sitter_groovy_external_scanner_scan(
67
177
  // in-grammar `\s` SKIP rule — without this, tree-sitter's lexer
68
178
  // eats `\n`, does NOT re-invoke the external scanner, then
69
179
  // fails on the following `/` and consumes it through error
70
- // recovery.
180
+ // recovery. §6.7 `record` shares the whitespace skip and is
181
+ // dispatched on `r` before the `/` branches.
71
182
  bool wants_comment = valid_symbols[LINE_COMMENT]
72
183
  || valid_symbols[BLOCK_COMMENT]
73
184
  || valid_symbols[GROOVYDOC_COMMENT];
74
185
  bool wants_slashy = valid_symbols[SLASHY_STRING_START];
75
- if (!wants_comment && !wants_slashy) {
186
+ bool wants_record = valid_symbols[RECORD_KEYWORD];
187
+ if (!wants_comment && !wants_slashy && !wants_record) {
76
188
  return false;
77
189
  }
78
190
 
79
191
  while (is_whitespace(lexer->lookahead)) {
80
192
  lexer->advance(lexer, true);
81
193
  }
194
+ if (lexer->lookahead == 'r' && wants_record) {
195
+ // An `r` can never open a comment or a slashy string, so
196
+ // every path out of this branch returns. On `false` the
197
+ // runtime rewinds to the token start and the in-grammar
198
+ // lexer reads the word as an identifier.
199
+ static const char keyword[] = "record";
200
+ for (const char *c = keyword; *c != '\0'; c++) {
201
+ if (lexer->lookahead != *c) {
202
+ return false;
203
+ }
204
+ advance(lexer);
205
+ }
206
+ if (!scan_record_header(lexer)) {
207
+ return false;
208
+ }
209
+ lexer->result_symbol = RECORD_KEYWORD;
210
+ return true;
211
+ }
82
212
  if (lexer->lookahead != '/') {
83
213
  return false;
84
214
  }
package/tree-sitter.json CHANGED
@@ -13,7 +13,7 @@
13
13
  }
14
14
  ],
15
15
  "metadata": {
16
- "version": "0.3.0",
16
+ "version": "0.5.0",
17
17
  "license": "Apache-2.0 OR MIT",
18
18
  "description": "Apache Groovy grammar for tree-sitter (dekobon fork)",
19
19
  "authors": [