Skip to main content

gdck_syntax/
lib.rs

1//! Lossless lexing and parsing for [GDScript](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/).
2//!
3//! The tree this crate produces keeps every byte of the input, including
4//! whitespace, comments and blank lines. That is a deliberate constraint rather
5//! than an implementation detail: a formatter that rewrites one declaration has
6//! to leave the comments around it exactly where they were, and a linter that
7//! reports a problem has to point at a real byte offset.
8//!
9//! Parsing never fails. Malformed input produces [`SyntaxKind::Error`] nodes
10//! and diagnostics on [`SyntaxTree::errors`], so tools can report several
11//! problems per run and editors can work with a half-typed buffer.
12//!
13//! # Example
14//!
15//! ```
16//! let tree = gdck_syntax::parse("func _ready() -> void:\n\tpass\n");
17//! assert!(!tree.has_errors());
18//! // The source is always recoverable from the tree.
19//! assert_eq!(tree.text(), "func _ready() -> void:\n\tpass\n");
20//! ```
21
22mod error;
23mod kind;
24mod lexer;
25mod parser;
26mod text;
27mod tree;
28
29pub use error::SyntaxError;
30pub use kind::SyntaxKind;
31pub use lexer::{LexResult, Token, tokenize};
32pub use parser::parse;
33pub use text::{LineCol, LineIndex, TextRange};
34pub use tree::{Checkpoint, Descendants, Element, NodeId, SyntaxNode, SyntaxTree, TreeBuilder};
35
36#[cfg(test)]
37mod tests {
38    use super::*;
39
40    /// The invariant every downstream tool relies on.
41    fn assert_round_trips(source: &str) {
42        let tree = parse(source);
43        assert_eq!(tree.text(), source, "tree must reproduce its input exactly");
44    }
45
46    fn assert_parses(source: &str) -> SyntaxTree {
47        let tree = parse(source);
48        assert!(
49            !tree.has_errors(),
50            "expected a clean parse of:\n{source}\ngot: {:?}",
51            tree.errors()
52        );
53        assert_eq!(tree.text(), source);
54        tree
55    }
56
57    /// Collect the kinds of every node in the tree, for structural assertions.
58    fn node_kinds(tree: &SyntaxTree) -> Vec<SyntaxKind> {
59        tree.root().descendants().map(SyntaxNode::kind).collect()
60    }
61
62    #[test]
63    fn parses_an_empty_file() {
64        let tree = assert_parses("");
65        assert_eq!(tree.root().kind(), SyntaxKind::SourceFile);
66    }
67
68    /// `match` and `when` are keywords Godot still accepts as names — `match`
69    /// because `String.match()` was on the engine's API first, `when` because
70    /// it arrived as a `match` guard after code was already using it. Godot
71    /// lists both in `Token::is_identifier`, and refusing them rejects real
72    /// code the engine compiles.
73    #[test]
74    fn the_keywords_godot_accepts_as_names_are_names_here_too() {
75        assert_parses("var when: int = 3\n");
76        assert_parses("func f(when: int) -> int:\n\treturn when\n");
77        assert_parses("func f(text: String) -> bool:\n\treturn text.match(\"*.gd\")\n");
78        assert_parses("var when := 1\nvar match := 2\n");
79        assert_parses("signal when\n");
80        assert_parses("enum when { A }\n");
81        assert_parses("func when() -> void:\n\tpass\n");
82        assert_parses("var entry := {}\nvar x = entry.when\n");
83    }
84
85    /// And they are still keywords where they are one. The guard below reads
86    /// `when` twice: once as the keyword introducing the guard, once as the
87    /// variable it tests.
88    #[test]
89    fn a_match_guard_still_reads_when_as_a_keyword() {
90        assert_parses(
91            "func f(x: int, when: int) -> String:\n\
92             \tmatch x:\n\
93             \t\t1 when when > 0:\n\
94             \t\t\treturn \"guarded\"\n\
95             \t\tvar bound when bound > 5:\n\
96             \t\t\treturn \"bound\"\n\
97             \t\t_:\n\
98             \t\t\treturn \"other\"\n",
99        );
100        assert_round_trips("match x:\n\t1 when y:\n\t\tpass\n");
101    }
102
103    #[test]
104    fn parses_class_level_declarations() {
105        let tree = assert_parses(
106            "@tool\nclass_name Player\nextends CharacterBody2D\n\n## The player.\nsignal died\nsignal hit(damage: int, source: Node)\n\nenum State { IDLE, WALKING = 2, }\n\nconst MAX_SPEED := 300.0\nconst GRAVITY: float = 9.8\n\n@export var health: int = 100\n@export_range(0, 10) var lives := 3\nvar _private_state: State = State.IDLE\n@onready var sprite: Sprite2D = $Sprite2D\n",
107        );
108        let kinds = node_kinds(&tree);
109        assert!(kinds.contains(&SyntaxKind::ClassNameDecl));
110        assert!(kinds.contains(&SyntaxKind::ExtendsDecl));
111        assert!(kinds.contains(&SyntaxKind::SignalDecl));
112        assert!(kinds.contains(&SyntaxKind::EnumDecl));
113        assert!(kinds.contains(&SyntaxKind::ConstDecl));
114        assert!(kinds.contains(&SyntaxKind::VarDecl));
115        assert!(kinds.contains(&SyntaxKind::Annotation));
116    }
117
118    #[test]
119    fn annotations_attach_to_the_declaration_they_modify() {
120        let tree = assert_parses("@export var health := 100\n");
121        let var_decl = tree
122            .root()
123            .descendants()
124            .find(|node| node.kind() == SyntaxKind::VarDecl)
125            .expect("a var declaration");
126        // `@export` must live inside the VarDecl, not beside it, so moving the
127        // declaration moves its annotations too.
128        assert!(
129            var_decl
130                .child_nodes()
131                .any(|child| child.kind() == SyntaxKind::Annotation),
132            "annotation should be a child of the declaration"
133        );
134    }
135
136    #[test]
137    fn file_level_annotations_stay_separate() {
138        let tree = assert_parses("@tool\nclass_name Foo\n");
139        let class_name = tree
140            .root()
141            .descendants()
142            .find(|node| node.kind() == SyntaxKind::ClassNameDecl)
143            .expect("a class_name declaration");
144        // `@tool` describes the file, not the class_name, so it must not be
145        // swallowed by it.
146        assert!(
147            !class_name
148                .child_nodes()
149                .any(|child| child.kind() == SyntaxKind::Annotation)
150        );
151    }
152
153    #[test]
154    fn parses_functions_and_control_flow() {
155        assert_parses(
156            "func _physics_process(delta: float) -> void:\n\tif health <= 0:\n\t\tdied.emit()\n\telif health < 20:\n\t\tblink()\n\telse:\n\t\tpass\n\n\tfor i in range(10):\n\t\tprint(i)\n\n\twhile true:\n\t\tbreak\n\n\tmatch state:\n\t\tState.IDLE:\n\t\t\tpass\n\t\tvar other when other > 2:\n\t\t\tpass\n\t\t_:\n\t\t\treturn\n",
157        );
158    }
159
160    #[test]
161    fn parses_static_and_abstract_members() {
162        assert_parses("static var count := 0\n\nstatic func reset() -> void:\n\tcount = 0\n");
163    }
164
165    #[test]
166    fn parses_inner_classes() {
167        let tree = assert_parses(
168            "class Inventory extends RefCounted:\n\tvar items: Array[String] = []\n\n\tfunc add(item: String) -> void:\n\t\titems.append(item)\n",
169        );
170        assert!(node_kinds(&tree).contains(&SyntaxKind::ClassDecl));
171    }
172
173    #[test]
174    fn parses_property_accessors() {
175        let tree = assert_parses(
176            "var health := 100:\n\tset(value):\n\t\thealth = maxi(value, 0)\n\tget:\n\t\treturn health\n",
177        );
178        let kinds = node_kinds(&tree);
179        assert!(kinds.contains(&SyntaxKind::Setter));
180        assert!(kinds.contains(&SyntaxKind::Getter));
181    }
182
183    #[test]
184    fn parses_expressions() {
185        assert_parses(
186            "func f():\n\tvar a = 1 + 2 * 3 - -4\n\tvar b = a > 1 and not a < 0 or false\n\tvar c = [1, 2, {\"key\": \"value\", other = 2}]\n\tvar d = c[0].method(1, 2).field\n\tvar e = \"yes\" if a else \"no\"\n\tvar g = a as float\n\tvar h = a is int\n\tvar i = await something()\n\tvar j = preload(\"res://x.tscn\")\n\tvar k = func(x): return x * 2\n\tvar m = 2 ** 3 ** 2\n",
187        );
188    }
189
190    #[test]
191    fn parses_assignment_operators() {
192        let tree = assert_parses(
193            "func f():\n\tx = 1\n\tx += 1\n\tx **= 2\n\tx >>= 1\n\tx[0] = 2\n\tx.y.z = 3\n",
194        );
195        let assignments = node_kinds(&tree)
196            .iter()
197            .filter(|kind| **kind == SyntaxKind::AssignStmt)
198            .count();
199        assert_eq!(assignments, 6);
200    }
201
202    #[test]
203    fn parses_inline_bodies_and_semicolons() {
204        assert_parses("func f():\n\tif true: pass\n\tvar a = 1; var b = 2\n");
205    }
206
207    #[test]
208    fn parses_wrapped_lines() {
209        assert_parses(
210            "func f():\n\tvar a = [\n\t\t1,\n\t\t2,\n\t]\n\tif a \\\n\t\t\tand true:\n\t\tpass\n\tvar b = (\n\t\t1\n\t\t+ 2\n\t)\n",
211        );
212    }
213
214    #[test]
215    fn round_trips_regardless_of_errors() {
216        // Losslessness must survive syntax errors, or a formatter could destroy
217        // a file it merely failed to understand.
218        for source in [
219            "func f(:\n\tpass\n",
220            "var = 5\n",
221            "class_name\n",
222            "func f():\n\treturn ]\n",
223            "@\n",
224            "if if if\n",
225            "enum { \n",
226            "var s = \"unterminated\n",
227            "\t\tweird_indent()\n",
228            "% ^ &\n",
229        ] {
230            assert_round_trips(source);
231        }
232    }
233
234    #[test]
235    fn reports_errors_without_giving_up() {
236        let tree = parse("func f(:\n\tpass\n\nfunc g():\n\tpass\n");
237        assert!(tree.has_errors());
238        // Recovery must find the second function despite the broken first one.
239        let functions = node_kinds(&tree)
240            .iter()
241            .filter(|kind| **kind == SyntaxKind::FuncDecl)
242            .count();
243        assert_eq!(functions, 2, "parser should recover and see both functions");
244    }
245
246    #[test]
247    fn preserves_comments_in_the_tree() {
248        let source = "# leading\nfunc f():  # trailing\n\t# inner\n\tpass\n";
249        let tree = assert_parses(source);
250        let comments: Vec<_> = tree
251            .root()
252            .descendants()
253            .flat_map(SyntaxNode::child_tokens)
254            .filter(|token| token.kind.is_comment())
255            .map(|token| token.text(source))
256            .collect();
257        assert_eq!(comments, vec!["# leading", "# trailing", "# inner"]);
258    }
259
260    #[test]
261    fn leading_comments_attach_to_what_they_document() {
262        let tree =
263            assert_parses("extends Node\n\n# Sets things up.\nfunc _ready() -> void:\n\tpass\n");
264        let func = tree
265            .root()
266            .descendants()
267            .find(|node| node.kind() == SyntaxKind::FuncDecl)
268            .expect("a function declaration");
269        // The comment describes `_ready`, so it must live inside FuncDecl and
270        // not trail the `extends` above it — otherwise reordering or
271        // reformatting the function would leave its comment behind.
272        assert!(
273            func.child_tokens().any(|token| token.kind.is_comment()),
274            "leading comment should belong to the declaration it precedes"
275        );
276    }
277
278    #[test]
279    fn distinguishes_inferred_from_explicit_types() {
280        let tree = assert_parses("var a := 1\nvar b: int = 1\n");
281        let has_type_hint = tree
282            .root()
283            .descendants()
284            .filter(|node| node.kind() == SyntaxKind::VarDecl)
285            .map(|node| {
286                node.child_nodes()
287                    .any(|child| child.kind() == SyntaxKind::TypeHint)
288            })
289            .collect::<Vec<_>>();
290        // `:=` carries no TypeHint; `: int` does. This is what the
291        // static-typing style rules key off.
292        assert_eq!(has_type_hint, vec![false, true]);
293    }
294
295    #[test]
296    fn parses_variadic_parameters() {
297        assert_parses("func foo(a, ...rest: Array) -> void:\n\tpass\n");
298    }
299
300    #[test]
301    fn parses_not_in_as_one_operator() {
302        let tree = assert_parses("func f():\n\tvar a = 1 not in [1] not in [true]\n");
303        // Two chained binary operators, not a prefix `not` and a stray `in`.
304        let binaries = node_kinds(&tree)
305            .iter()
306            .filter(|kind| **kind == SyntaxKind::BinaryExpr)
307            .count();
308        assert_eq!(binaries, 2);
309    }
310
311    #[test]
312    fn parses_abstract_annotations() {
313        // `@abstract` functions have no body at all.
314        assert_parses("@abstract\nclass_name Shape\n\n@abstract func area() -> float\n");
315        // `abstract` stays a normal identifier everywhere else.
316        assert_parses("func f():\n\tvar abstract = 1\n");
317    }
318
319    #[test]
320    fn parses_annotations_inside_function_bodies() {
321        assert_parses(
322            "func a():\n\t@warning_ignore(\"unused_variable\")\n\tvar x: Array[int] = [1, 2]\n\nfunc b():\n\t@warning_ignore(\"shadowed\") @warning_ignore(\"unused\") var y = 1\n",
323        );
324    }
325
326    #[test]
327    fn parses_semicolon_separated_declarations() {
328        assert_parses("const x = 1; const y = 2\nconst z = 3;\n");
329    }
330
331    #[test]
332    fn parses_docstrings_at_class_level() {
333        assert_parses("\"\"\"docstring\n\"\"\"\n\n\"another\"\n");
334    }
335
336    #[test]
337    fn parses_absolute_node_paths() {
338        assert_parses(
339            "func f():\n\t$/root.name = \"x\"\n\t$/root/A/B/C.free()\n\t$../Sibling.show()\n",
340        );
341    }
342
343    #[test]
344    fn sigil_literals_are_recognised_at_the_start_of_a_line() {
345        // The previous line ends in a value, which must not make `^` and `&`
346        // read as bitwise operators here.
347        assert_parses(
348            "func f():\n\tvar a = 1\n\t^\"node/path\"\n\t&\"string_name\"\n\t%Unique.show()\n",
349        );
350    }
351
352    #[test]
353    fn parses_inline_property_accessors() {
354        assert_parses(
355            "var p1: set = __set\nvar p2: set = __set, get = __get\nvar p3:\n\tget = __get,\n\tset = __set\n",
356        );
357    }
358
359    #[test]
360    fn parses_spaced_inference_operator() {
361        assert_parses("var is_enabled : = true\n");
362    }
363
364    #[test]
365    fn parses_match_patterns() {
366        assert_parses(
367            "func f(x):\n\tmatch x:\n\t\t[1, 2, [1, {1: 2, 2: var z, ..}]]:\n\t\t\tpass\n\t\t{\"name\", \"age\"}:\n\t\t\tpass\n\t\t{\"key\": \"v\", ..}:\n\t\t\tpass\n\t\t1 if true else 2:\n\t\t\tpass\n\t\tvar other when other > 2:\n\t\t\tpass\n\t\t_:\n\t\t\tpass\n",
368        );
369    }
370
371    #[test]
372    fn parses_multiline_lambdas_inside_brackets() {
373        // Indentation is meaningless inside brackets, except within a lambda
374        // body — the one case where the lexer has to turn it back on.
375        assert_parses(
376            "func f(source):\n\tstack(func():\n\t\tprint(\"foo\")\n\t\tif source == 1:\n\t\t\tpass)\n",
377        );
378        // A lambda body spanning lines inside a call spread over lines.
379        assert_parses(
380            "func f(button):\n\tbutton.pressed.connect(\n\t\tfunc() -> void:\n\t\t\tvar test := \"\"\n\t\t\tuse(test)\n\t)\n",
381        );
382        // Two lambdas in one array, the comma ending the first body.
383        assert_parses("func f():\n\tvar fs = [func():\n\t\treturn [1, 2, 3], func():\n\t\tpass]\n");
384        // A lambda whose body starts on the line it opened on.
385        assert_parses("func f():\n\tvar g = [func():\n\t\tpass]\n\tuse(g)\n");
386    }
387
388    #[test]
389    fn nested_lambdas_close_in_the_right_order() {
390        assert_parses(
391            "func f():\n\tvar a = [func():\n\t\tpass\n\t\tvar b = func():\n\t\t\tpass\n\t\t\tvar d = {\"f\": func():\n\t\t\t\tpass\n\t\t\t\treturn [1, 2, 3]}]\n",
392        );
393    }
394
395    #[test]
396    fn brackets_inside_a_lambda_body_still_suppress_indentation() {
397        // The array spans lines inside the lambda; only the lambda body itself
398        // gets indent handling back.
399        assert_parses(
400            "func f():\n\tcall(func():\n\t\tvar xs = [\n\t\t\t1,\n\t\t\t2,\n\t\t]\n\t\tuse(xs))\n",
401        );
402    }
403
404    #[test]
405    fn handles_crlf_and_missing_trailing_newline() {
406        assert_parses("var a = 1\r\nvar b = 2\r\n");
407        assert_parses("func f():\n\tpass");
408    }
409
410    #[test]
411    fn node_ranges_are_consistent_with_text() {
412        let source = "func hello() -> void:\n\tprint(\"hi\")\n";
413        let tree = parse(source);
414        for node in tree.root().descendants() {
415            let range = node.range();
416            assert!(
417                range.end() as usize <= source.len(),
418                "{:?} range {range} escapes the source",
419                node.kind()
420            );
421            assert_eq!(node.text(), range.slice(source));
422        }
423    }
424}