fluidattacks-blends-domain 0.41.0

Blends functional core: pure AST graph to syntax graph (no_std)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
//! Typed syntax nodes: one enum variant per syntax label type.

use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;

use strum::{EnumDiscriminants, IntoStaticStr};

use crate::NodeId;

/// One entry of a file's structure tree (its classes, methods and nesting).
#[derive(Clone, PartialEq, Eq, Debug)]
#[non_exhaustive]
pub struct FileStructData {
    /// Id of the AST node this entry was built from.
    pub node: NodeId,
    /// The entry's syntax-node label, e.g. `"MethodDeclaration"`.
    pub kind: String,
    /// The entry's payload: a leaf value or further nested entries.
    pub data: FileStructValue,
    /// Ids of every AST node this entry spans, when known.
    pub node_range: Option<Vec<NodeId>>,
}

/// A [`FileStructData`] entry's payload.
#[derive(Clone, PartialEq, Eq, Debug)]
#[non_exhaustive]
pub enum FileStructValue {
    /// A leaf: the entry is a method, named by its signature.
    MethodName(String),
    /// A branch: nested entries, keyed by name.
    Children(BTreeMap<String, FileStructData>),
}

/// One instance (object) found while extracting a file's structure.
#[derive(Clone, PartialEq, Eq, Debug)]
#[non_exhaustive]
pub struct FileInstanceData {
    /// The instance's variable or field name.
    pub object: String,
    /// The expression the instance was constructed from.
    pub source: String,
    /// The instance's constructed type name.
    pub source_type: String,
}

/// A tree-sitter-independent syntax node: one variant per label the readers emit.
#[derive(Clone, PartialEq, Eq, Debug, EnumDiscriminants)]
#[non_exhaustive]
#[strum_discriminants(
    name(SyntaxKind),
    derive(PartialOrd, Ord, Hash, IntoStaticStr),
    non_exhaustive
)]
pub enum SyntaxNode {
    /// A decorator/annotation applied to a declaration.
    Annotation {
        /// The node's name, as written.
        name: String,
        /// Id of the argument list node, if any.
        arguments_id: Option<NodeId>,
    },
    /// A single call argument.
    Argument,
    /// A call's argument list.
    ArgumentList,
    /// An array literal initializer.
    ArrayInitializer {
        /// The declared variable's type, as written, if any.
        variable_type: Option<String>,
    },
    /// An assignment expression.
    Assignment {
        /// Id of the declared variable's node.
        variable_id: NodeId,
        /// Id of the value node, if any.
        value_id: Option<NodeId>,
        /// The operator, as written, if any.
        operator: Option<String>,
    },
    /// An attribute applied to a declaration (non-annotation form).
    Attribute {
        /// The node's name, as written.
        name: String,
    },
    /// An `await` expression.
    AwaitExpression {
        /// Id of the expression node.
        expression_id: NodeId,
    },
    /// A binary operator expression.
    BinaryOperation {
        /// The operator, as written.
        operator: String,
        /// Id of the left-hand operand node, if any.
        left_id: Option<NodeId>,
        /// Id of the right-hand operand node, if any.
        right_id: Option<NodeId>,
    },
    /// A `break` statement.
    Break,
    /// A `catch` clause of a try statement.
    CatchClause {
        /// Id of the node's body block, if any.
        block_id: Option<NodeId>,
        /// Id of the exception variable declaration, if any.
        catch_declaration: Option<NodeId>,
    },
    /// The exception variable declared by a `catch` clause.
    CatchDeclaration,
    /// A class declaration.
    Class {
        /// The node's name, as written.
        name: String,
        /// Id of the node's body block, if any.
        block_id: Option<NodeId>,
        /// Id of the modifiers node, if any.
        modifiers_id: Option<NodeId>,
        /// The name of the class this one extends, if any.
        inherited_class: Option<String>,
        /// The access modifier keywords, as written, if any.
        access_modifiers: Option<String>,
    },
    /// A class's member list.
    ClassBody,
    /// A source comment.
    Comment {
        /// The comment text, including its delimiters.
        comment: String,
    },
    /// A `continue` statement.
    Continue,
    /// A generic (language-agnostic) declaration.
    Declaration {
        /// The node's name, as written.
        name: String,
        /// Id of the node's body block, if any.
        block_id: Option<NodeId>,
        /// The access modifier keywords, as written, if any.
        access_modifiers: Option<String>,
    },
    /// A block holding one or more declarations.
    DeclarationBlock,
    /// A `default` label or value.
    Default,
    /// A `do`/`while` loop.
    DoStatement {
        /// Id of the node's body block.
        block_id: NodeId,
        /// Id of the condition expression node.
        condition_id: NodeId,
    },
    /// An indexed element access (e.g. `a[i]`).
    ElementAccess {
        /// Id of the expression node.
        expression_id: NodeId,
        /// Id of the argument list node, if any.
        arguments_id: Option<NodeId>,
    },
    /// The `else` branch of a conditional.
    ElseClause {
        /// Id of the node's body block.
        block_id: NodeId,
    },
    /// A block of executable statements.
    ExecutionBlock,
    /// An export statement.
    Export {
        /// The expression, as written, if any.
        expression: Option<String>,
        /// Id of the declaration node, if any.
        declaration_id: Option<NodeId>,
        /// The bound local alias, if any.
        alias: Option<String>,
        /// Id of the default export's declaration node, if any.
        default_id: Option<NodeId>,
    },
    /// A statement that is a bare expression.
    ExpressionStatement,
    /// The file's root node.
    File,
    /// The `finally` clause of a try statement.
    FinallyClause {
        /// Id of the node's body block, if any.
        block_id: Option<NodeId>,
    },
    /// A `for-each`/`for-in` loop.
    ForEachStatement {
        /// Id of the declared variable's node.
        variable_id: NodeId,
        /// Id of the collection being iterated.
        iterable_item_id: NodeId,
        /// Id of the node's body block, if any.
        block_id: Option<NodeId>,
    },
    /// A classic C-style `for` loop.
    ForStatement {
        /// Id of the node's body block.
        block_id: NodeId,
        /// Id of the loop's initializer node, if any.
        initializer_id: Option<NodeId>,
        /// Id of the condition expression node, if any.
        condition_id: Option<NodeId>,
        /// Id of the loop's update node, if any.
        update_id: Option<NodeId>,
    },
    /// An `if` conditional.
    If {
        /// Id of the condition expression node.
        condition_id: NodeId,
        /// Id of the branch taken when the condition is true, if any.
        true_id: Option<NodeId>,
        /// Id of the branch taken when the condition is false, if any.
        false_id: Option<NodeId>,
        /// Id of the condition's initializer node (e.g. an `if let` binding), if any.
        initializer: Option<NodeId>,
    },
    /// An import statement.
    Import {
        /// The expression, as written, if any.
        expression: Option<String>,
        /// The bound local alias, if any.
        alias: Option<String>,
        /// The imported method's name, if any.
        method_name: Option<String>,
        /// The import's kind keyword (e.g. `type`), if any.
        import_type: Option<String>,
        /// The imported module's name, if any.
        module_name: Option<String>,
    },
    /// A JSX element.
    JsxElement,
    /// A lambda/closure's declared type.
    LambdaFunctionType,
    /// A literal value.
    Literal {
        /// The literal value, as written.
        value: String,
        /// The literal's type keyword.
        value_type: String,
    },
    /// A member access expression (e.g. `a.b`).
    MemberAccess {
        /// The accessed member's name.
        member: String,
        /// The expression, as written.
        expression: String,
        /// Id of the expression node.
        expression_id: NodeId,
        /// Id of the node that scopes this symbol lookup, if any.
        symbol_scope: Option<NodeId>,
    },
    /// A file's top-level metadata: structure, instances and imports.
    Metadata {
        /// The file's path.
        path: String,
        /// The file's top-level members, keyed by name.
        structure: BTreeMap<String, FileStructData>,
        /// Per-object metadata instances, keyed by qualified name.
        instances: BTreeMap<String, BTreeMap<String, FileInstanceData>>,
        /// The names this file imports.
        imports: Vec<String>,
        /// The file's package/namespace name, if any.
        package: Option<String>,
    },
    /// A method or function declaration.
    MethodDeclaration {
        /// The node's name, as written, if any.
        name: Option<String>,
        /// The access modifier keywords, as written, if any.
        access_modifiers: Option<String>,
        /// Id of the node's body block, if any.
        block_id: Option<NodeId>,
        /// Id of the modifiers node, if any.
        modifiers_id: Option<NodeId>,
        /// Id of the parameter list node, if any.
        parameters_id: Option<NodeId>,
    },
    /// A method or function call.
    MethodInvocation {
        /// The expression, as written.
        expression: String,
        /// The receiver's textual expression, if any.
        object: Option<String>,
        /// Id of the node that scopes this symbol lookup, if any.
        symbol_scope: Option<NodeId>,
        /// Id of the expression node, if any.
        expression_id: Option<NodeId>,
        /// Id of the argument list node, if any.
        arguments_id: Option<NodeId>,
        /// Id of the receiver node, if any.
        object_id: Option<NodeId>,
        /// Id of the node's body block, if any.
        block_id: Option<NodeId>,
        /// The receiver's fully qualified type name, when resolved, if any.
        receiver_type_fqn: Option<String>,
    },
    /// A node the grammar could not classify.
    MissingNode {
        /// The original (unmapped) tree-sitter node type.
        node_type: String,
    },
    /// A declaration's modifier list (e.g. `public static`).
    Modifiers,
    /// A whole-module import.
    ModuleImport {
        /// The expression, as written.
        expression: String,
        /// The bound local alias, if any.
        alias: Option<String>,
        /// The imported module's name, if any.
        module_name: Option<String>,
    },
    /// A call argument passed by name.
    NamedArgument {
        /// Id of the value node.
        value_id: NodeId,
        /// The named argument's parameter name, if any.
        argument_name: Option<String>,
    },
    /// A namespace/module declaration.
    Namespace {
        /// The node's name, as written.
        name: String,
        /// Id of the node's body block, if any.
        block_id: Option<NodeId>,
    },
    /// An object-construction expression (`new X(...)`).
    NewExpression {
        /// Id of the constructor being invoked.
        constructor_id: NodeId,
        /// Id of the argument list node, if any.
        arguments_id: Option<NodeId>,
    },
    /// An object literal or reference (e.g. a Terraform resource block).
    Object {
        /// The node's name, as written, if any.
        name: Option<String>,
        /// The Terraform block's reference expression, if any.
        tf_reference: Option<String>,
    },
    /// An object instantiation with an initializer.
    ObjectCreation {
        /// The node's name, as written.
        name: String,
        /// Id of the argument list node, if any.
        arguments_id: Option<NodeId>,
        /// Id of the loop's initializer node, if any.
        initializer_id: Option<NodeId>,
    },
    /// A key/value pair (e.g. in a map or object literal).
    Pair {
        /// Id of the pair's key node.
        key_id: NodeId,
        /// Id of the value node.
        value_id: NodeId,
    },
    /// A function/method parameter.
    Parameter {
        /// The declared variable's name, if any.
        variable: Option<String>,
        /// The declared variable's type, as written, if any.
        variable_type: Option<String>,
        /// Id of the value node, if any.
        value_id: Option<NodeId>,
        /// The parameter's passing mode keyword (e.g. `ref`), if any.
        parameter_mode: Option<String>,
    },
    /// A function/method's parameter list.
    ParameterList,
    /// A parenthesized expression.
    ParenthesizedExpression,
    /// A language reserved word used as a value (e.g. `self`).
    ReservedWord {
        /// The literal value, as written.
        value: String,
    },
    /// A rest/spread binding pattern (e.g. `...rest`).
    RestPattern {
        /// Id of the value node.
        value_id: NodeId,
    },
    /// A `return` statement.
    Return {
        /// Id of the value node, if any.
        value_id: Option<NodeId>,
    },
    /// A Go-style type selector.
    Selector {
        /// The selector's name, if any.
        selector_name: Option<String>,
    },
    /// A spread element in a call or literal (e.g. `...xs`).
    SpreadElement {
        /// Id of the value node.
        value_id: NodeId,
    },
    /// A `switch` statement's body.
    SwitchBody,
    /// One `case`/`when` section of a switch.
    SwitchSection {
        /// The case label's expression, as written.
        case_expression: String,
    },
    /// A `switch` statement.
    SwitchStatement {
        /// Id of the node's body block.
        block_id: NodeId,
        /// Id of the value node.
        value_id: NodeId,
    },
    /// A bare symbol reference.
    SymbolLookup {
        /// The looked-up symbol's name.
        symbol: String,
        /// Id of the node that scopes this symbol lookup, if any.
        symbol_scope: Option<NodeId>,
        /// The literal value, as written, if any.
        value: Option<String>,
    },
    /// A ternary conditional expression (`a ? b : c`).
    TernaryOperation {
        /// Id of the condition expression node.
        condition_id: NodeId,
        /// Id of the branch taken when the condition is true.
        true_id: NodeId,
        /// Id of the branch taken when the condition is false.
        false_id: NodeId,
    },
    /// A `this`/`self` reference.
    This {
        /// The literal value, as written.
        value: String,
    },
    /// A `throw`/`raise` statement.
    ThrowStatement {
        /// Id of the expression node, if any.
        expression_id: Option<NodeId>,
    },
    /// A `try` statement.
    TryStatement {
        /// Id of the node's body block.
        block_id: NodeId,
        /// Id of the try-with-resources declaration node, if any.
        resources_id: Option<NodeId>,
    },
    /// A unary operator expression.
    UnaryExpression {
        /// The operator, as written.
        operator: String,
        /// Id of the operand node.
        operand_id: NodeId,
    },
    /// A `using`/`with` resource-scoped statement.
    UsingStatement {
        /// Id of the node's body block.
        block_id: NodeId,
        /// Id of the declaration node, if any.
        declaration_id: Option<NodeId>,
    },
    /// A variable declaration.
    VariableDeclaration {
        /// The declared variable's name.
        variable: String,
        /// The declared variable's type, as written, if any.
        variable_type: Option<String>,
        /// Id of the value node, if any.
        value_id: Option<NodeId>,
        /// Id of the declared variable's node, if any.
        variable_id: Option<NodeId>,
        /// The access modifier keyword, if any.
        access_modifier: Option<String>,
    },
    /// A `while` loop.
    WhileStatement {
        /// Id of the node's body block.
        block_id: NodeId,
        /// Id of the condition expression node, if any.
        condition_id: Option<NodeId>,
    },
}

impl SyntaxNode {
    /// Id of this node's argument list, for variants that take one.
    #[must_use]
    pub const fn arguments_id(&self) -> Option<NodeId> {
        match self {
            Self::Annotation { arguments_id, .. }
            | Self::ElementAccess { arguments_id, .. }
            | Self::MethodInvocation { arguments_id, .. }
            | Self::NewExpression { arguments_id, .. }
            | Self::ObjectCreation { arguments_id, .. } => *arguments_id,
            _ => None,
        }
    }

    /// Id of this node's body block, for variants that have one.
    #[must_use]
    pub const fn block_id(&self) -> Option<NodeId> {
        match self {
            Self::CatchClause { block_id, .. }
            | Self::Class { block_id, .. }
            | Self::Declaration { block_id, .. }
            | Self::FinallyClause { block_id }
            | Self::ForEachStatement { block_id, .. }
            | Self::MethodDeclaration { block_id, .. }
            | Self::MethodInvocation { block_id, .. }
            | Self::Namespace { block_id, .. } => *block_id,
            Self::DoStatement { block_id, .. }
            | Self::ElseClause { block_id }
            | Self::ForStatement { block_id, .. }
            | Self::SwitchStatement { block_id, .. }
            | Self::TryStatement { block_id, .. }
            | Self::UsingStatement { block_id, .. }
            | Self::WhileStatement { block_id, .. } => Some(*block_id),
            _ => None,
        }
    }

    /// This node's declared name, for variants that are named.
    #[must_use]
    pub fn name(&self) -> Option<&str> {
        match self {
            Self::Annotation { name, .. }
            | Self::Attribute { name }
            | Self::Class { name, .. }
            | Self::Declaration { name, .. }
            | Self::Namespace { name, .. }
            | Self::ObjectCreation { name, .. } => Some(name.as_str()),
            Self::MethodDeclaration { name, .. } | Self::Object { name, .. } => name.as_deref(),
            _ => None,
        }
    }

    /// The declared variable's name, for variants that declare one.
    #[must_use]
    pub fn variable(&self) -> Option<&str> {
        match self {
            Self::VariableDeclaration { variable, .. } => Some(variable.as_str()),
            Self::Parameter { variable, .. } => variable.as_deref(),
            _ => None,
        }
    }

    /// The looked-up symbol's name, for [`Self::SymbolLookup`].
    #[must_use]
    pub const fn symbol(&self) -> Option<&str> {
        match self {
            Self::SymbolLookup { symbol, .. } => Some(symbol.as_str()),
            _ => None,
        }
    }

    /// This node's expression text, for variants that carry one.
    #[must_use]
    pub fn expression(&self) -> Option<&str> {
        match self {
            Self::MemberAccess { expression, .. }
            | Self::MethodInvocation { expression, .. }
            | Self::ModuleImport { expression, .. } => Some(expression.as_str()),
            Self::Export { expression, .. } | Self::Import { expression, .. } => {
                expression.as_deref()
            }
            _ => None,
        }
    }

    /// The bound local alias, for import/export variants that have one.
    #[must_use]
    pub fn alias(&self) -> Option<&str> {
        match self {
            Self::Export { alias, .. }
            | Self::Import { alias, .. }
            | Self::ModuleImport { alias, .. } => alias.as_deref(),
            _ => None,
        }
    }

    /// This node's literal value, as written, for variants that have one.
    #[must_use]
    pub fn value(&self) -> Option<&str> {
        match self {
            Self::Literal { value, .. } | Self::ReservedWord { value } | Self::This { value } => {
                Some(value.as_str())
            }
            Self::SymbolLookup { value, .. } => value.as_deref(),
            _ => None,
        }
    }

    /// Id of the call receiver node, for [`Self::MethodInvocation`].
    #[must_use]
    pub const fn object_id(&self) -> Option<NodeId> {
        match self {
            Self::MethodInvocation { object_id, .. } => *object_id,
            _ => None,
        }
    }

    /// Id of this node's value node, for variants that carry one.
    #[must_use]
    pub const fn value_id(&self) -> Option<NodeId> {
        match self {
            Self::Assignment { value_id, .. }
            | Self::Parameter { value_id, .. }
            | Self::Return { value_id }
            | Self::VariableDeclaration { value_id, .. } => *value_id,
            Self::NamedArgument { value_id, .. }
            | Self::Pair { value_id, .. }
            | Self::RestPattern { value_id }
            | Self::SpreadElement { value_id }
            | Self::SwitchStatement { value_id, .. } => Some(*value_id),
            _ => None,
        }
    }

    /// Id of the declared variable's node, for variants that declare one.
    #[must_use]
    pub const fn variable_id(&self) -> Option<NodeId> {
        match self {
            Self::VariableDeclaration { variable_id, .. } => *variable_id,
            Self::Assignment { variable_id, .. } | Self::ForEachStatement { variable_id, .. } => {
                Some(*variable_id)
            }
            _ => None,
        }
    }

    #[must_use]
    pub(crate) const fn symbol_scope(&self) -> Option<NodeId> {
        match self {
            Self::MemberAccess { symbol_scope, .. }
            | Self::MethodInvocation { symbol_scope, .. }
            | Self::SymbolLookup { symbol_scope, .. } => *symbol_scope,
            _ => None,
        }
    }

    /// This node's syntax-kind label, e.g. `"MethodDeclaration"`.
    #[must_use]
    pub fn label_type(&self) -> &'static str {
        SyntaxKind::from(self).into()
    }
}

#[cfg(test)]
mod tests {
    use super::SyntaxNode;
    use crate::NodeId;
    use alloc::borrow::ToOwned;
    use alloc::vec;

    const BLOCK: NodeId = NodeId(42);

    /// Every variant that owns a body must expose it through `block_id`.
    ///
    /// Symbolic evaluation descends into bodies through this accessor, so a
    /// variant silently dropped from the match arm stops the traversal without
    /// any other signal. The list is exhaustive on purpose: it is the only way
    /// a removed arm shows up as a failure.
    #[test]
    #[allow(
        clippy::too_many_lines,
        reason = "the variant list is the point of the test"
    )]
    fn every_variant_that_owns_a_body_exposes_it_as_its_block_id() {
        let nodes_with_body = vec![
            SyntaxNode::CatchClause {
                block_id: Some(BLOCK),
                catch_declaration: None,
            },
            SyntaxNode::Class {
                name: "Foo".to_owned(),
                block_id: Some(BLOCK),
                modifiers_id: None,
                inherited_class: None,
                access_modifiers: None,
            },
            SyntaxNode::Declaration {
                name: "Foo".to_owned(),
                block_id: Some(BLOCK),
                access_modifiers: None,
            },
            SyntaxNode::FinallyClause {
                block_id: Some(BLOCK),
            },
            SyntaxNode::ForEachStatement {
                variable_id: NodeId(1),
                iterable_item_id: NodeId(2),
                block_id: Some(BLOCK),
            },
            SyntaxNode::MethodDeclaration {
                name: None,
                access_modifiers: None,
                block_id: Some(BLOCK),
                modifiers_id: None,
                parameters_id: None,
            },
            SyntaxNode::MethodInvocation {
                expression: "foo".to_owned(),
                object: None,
                symbol_scope: None,
                expression_id: None,
                arguments_id: None,
                object_id: None,
                block_id: Some(BLOCK),
                receiver_type_fqn: None,
            },
            SyntaxNode::Namespace {
                name: "ns".to_owned(),
                block_id: Some(BLOCK),
            },
            SyntaxNode::DoStatement {
                block_id: BLOCK,
                condition_id: NodeId(1),
            },
            SyntaxNode::ElseClause { block_id: BLOCK },
            SyntaxNode::ForStatement {
                block_id: BLOCK,
                initializer_id: None,
                condition_id: None,
                update_id: None,
            },
            SyntaxNode::SwitchStatement {
                block_id: BLOCK,
                value_id: NodeId(1),
            },
            SyntaxNode::TryStatement {
                block_id: BLOCK,
                resources_id: None,
            },
            SyntaxNode::UsingStatement {
                block_id: BLOCK,
                declaration_id: None,
            },
            SyntaxNode::WhileStatement {
                block_id: BLOCK,
                condition_id: None,
            },
        ];

        for node in nodes_with_body {
            assert_eq!(
                node.block_id(),
                Some(BLOCK),
                "{} must expose its body",
                node.label_type()
            );
        }
    }

    /// A variant without a body must not borrow one from a neighbouring arm.
    #[test]
    fn a_variant_without_a_body_reports_no_block_id() {
        assert_eq!(SyntaxNode::Break.block_id(), None);
        assert_eq!(
            SyntaxNode::FinallyClause { block_id: None }.block_id(),
            None
        );
    }
}