Skip to main content

big_code_analysis_ast/getter/
csharp.rs

1//! `Getter` implementation for C#.
2#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
3
4use super::*;
5
6const GENERIC_ANGLES: GenericAngleKinds = GenericAngleKinds {
7    lists: &[
8        Csharp::TypeArgumentList as u16,
9        Csharp::TypeParameterList as u16,
10    ],
11    openers: &[Csharp::LT as u16],
12    closers: &[Csharp::GT as u16],
13    is_misparse: csharp_comparison_misparse,
14};
15
16// Whether `list` holds the two comparisons of a call argument
17// (`F(a < b, c > d)`) that tree-sitter-c-sharp reads as a declaration
18// typed `a<b, c>` and named `d`. The C# specification keeps the type
19// arguments only where the token after `>` allows it (§6.2.5), and an
20// identifier allows it only after `is`, `case`, `out` or in a tuple —
21// so a declaration typed by a generic name is two comparisons when it is
22// a call or indexer argument without `out`. `F(out List<int> x)`,
23// `(List<int> p, int q) = …` and `o is List<int> x` keep their pair. A
24// member access before the `<` (`F(x.a < b, c > d)`) wraps the generic
25// name in `qualified_name`s, which the climb passes. The `>=` spelling
26// (`F(a < b, c >= d)`) parses as an assignment instead and is not undone.
27fn csharp_comparison_misparse<'a>(list: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
28    let mut up = ancestors.iter(list).map(|(node, _)| node);
29    if up
30        .next()
31        .is_none_or(|name| name.kind_id() != Csharp::GenericName as u16)
32    {
33        return false;
34    }
35    let mut up = up.skip_while(|node| node.kind_id() == Csharp::QualifiedName as u16);
36    let (Some(declaration), Some(argument), Some(arguments)) = (up.next(), up.next(), up.next())
37    else {
38        return false;
39    };
40    declaration.kind_id() == Csharp::DeclarationExpression as u16
41        && argument.kind_id() == Csharp::Argument as u16
42        && !argument.is_child(Csharp::Out as u16)
43        && matches!(
44            arguments.kind_id().into(),
45            Csharp::ArgumentList | Csharp::BracketedArgumentList
46        )
47}
48
49impl Getter for CsharpCode {
50    fn get_space_kind(node: &Node) -> SpaceKind {
51        use Csharp::*;
52
53        // `EnumDeclaration` maps to `SpaceKind::Class` for cross-language
54        // parity with Java/PHP/Groovy (issue #429): a C# enum opens a
55        // FuncSpace via `is_func_space`, so it must classify here too or
56        // it falls through to `_ => SpaceKind::Unknown`.
57        // A bodied indexer (#464) or property (#472) defers to its
58        // `accessor_declaration` children; only the accessor-less
59        // expression-bodied form opens a Function space directly. Keep this
60        // gate in lockstep with `CsharpCode::is_func` / `is_func_space` so the
61        // walker and the space-kind classifier agree on which nodes promote.
62        if matches!(
63            node.kind_id().into(),
64            IndexerDeclaration | PropertyDeclaration
65        ) {
66            return if crate::checker::csharp_member_has_accessors(node) {
67                SpaceKind::Unknown
68            } else {
69                SpaceKind::Function
70            };
71        }
72        match node.kind_id().into() {
73            ClassDeclaration | StructDeclaration | RecordDeclaration | EnumDeclaration => {
74                SpaceKind::Class
75            }
76            InterfaceDeclaration => SpaceKind::Interface,
77            MethodDeclaration
78            | ConstructorDeclaration
79            | DestructorDeclaration
80            | LocalFunctionStatement
81            | LambdaExpression
82            | AnonymousMethodExpression
83            | AccessorDeclaration
84            | OperatorDeclaration
85            | ConversionOperatorDeclaration => SpaceKind::Function,
86            CompilationUnit => SpaceKind::Unit,
87            _ => SpaceKind::Unknown,
88        }
89    }
90
91    fn get_op_type<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> TokenRole {
92        use Csharp::*;
93
94        if GENERIC_ANGLES.is_closer(node, ancestors) {
95            return TokenRole::Unknown;
96        }
97
98        match node.kind_id().into() {
99            // Control-flow keywords
100            If | Else | Switch | Case | Default | Try | Catch | Finally | Throw
101            | Return | Yield | Break | Continue | Goto | For | Foreach | While | Do
102            // Declaration / namespace keywords
103            | Class | Struct | Interface | Enum | Record | Delegate | Namespace | Using
104            // Modifiers
105            | Public | Private | Protected | Internal | Static | Abstract | Virtual
106            | Override | Sealed | Partial | Readonly | Const | Extern | Unsafe
107            | Volatile | Async | Required | File | New | Fixed | Implicit | Explicit
108            // Expression-keyword operators
109            | Await | Is | As | Typeof | Sizeof | Checked | Unchecked | Ref | Out | In
110            | Params | Lock | Stackalloc | Where | With | When | Operator
111            | Scoped | Not | And | Or
112            // Property/event accessor keywords
113            | Get | Set | Init | Add | Remove
114            // Structural punctuation
115            | LBRACE | LBRACK | LPAREN | COMMA | SEMI | COLON | COLONCOLON | DOT
116            | DOTDOT | EQGT | DASHGT | QMARK
117            // Arithmetic / comparison / logical / bitwise / assignment operators
118            | EQ | EQEQ | BANGEQ | LT | GT | LTEQ | GTEQ
119            | PLUS | DASH | STAR | SLASH | PERCENT
120            | AMP | PIPE | CARET | TILDE | BANG
121            | AMPAMP | PIPEPIPE | QMARKQMARK
122            | LTLT | GTGT | GTGTGT
123            | PLUSPLUS | DASHDASH
124            | PLUSEQ | DASHEQ | STAREQ | SLASHEQ | PERCENTEQ
125            | AMPEQ | PIPEEQ | CARETEQ | LTLTEQ | GTGTEQ | GTGTGTEQ | QMARKQMARKEQ
126            // Predefined / primitive types
127            | PredefinedType
128                => TokenRole::Operator,
129            // `boolean_literal: choice('true', 'false')` wraps the
130            // keyword leaf, so a literal reaches the walker twice;
131            // listing both kinds inflated `N2` by one per literal while
132            // the text-keyed `n2` hid it (#1253). The leaf cannot simply
133            // be dropped: the overloadable-operator list emits a bare
134            // `true` / `false` under `operator_declaration` — the
135            // grammar's only unwrapped position. There the keyword
136            // *names* an operator, exactly as the `+` of `operator +`
137            // does, so it is billed as one (#1296); C# merely spells two
138            // of its overloadable operators with keywords instead of
139            // punctuation. The fallback keeps any other unwrapped
140            // position an operand, the role the keyword has as a value.
141            True | False => match ancestors.parent(node).map(|p| p.kind_id().into()) {
142                Some(BooleanLiteral) => TokenRole::Unknown,
143                Some(OperatorDeclaration) => TokenRole::Operator,
144                _ => TokenRole::Operand,
145            },
146            // `this` is a self-reference everywhere (`this.x`,
147            // `this[i]`, `: this(1)`, `f(this)`) except directly under
148            // an `indexer_declaration`, where the keyword *names* the
149            // member being declared rather than denoting a value —
150            // `public int this[int i] { … }`. That position keeps its
151            // operator classification alongside the `operator` keyword
152            // of an overload declaration, which this match already
153            // bills as an operator (#1380). The extension-method
154            // receiver (`static void M(this Foo f)`) is a childless
155            // `modifier` node, kind 249, not this kind at all; it is
156            // classified — as an operand, so the two positions agree —
157            // by `get_op_type_with_code` below (#1418).
158            //
159            // Java's receiver parameter (`void m(J J.this)`) is the
160            // other declarator use of the keyword in this workspace and
161            // `java.rs` calls it an operand — a parameter *name* is an
162            // operand in every language here, where a member named by a
163            // keyword belongs with `operator +`.
164            This => match ancestors.parent(node).map(|p| p.kind_id().into()) {
165                Some(IndexerDeclaration) => TokenRole::Operator,
166                _ => TokenRole::Operand,
167            },
168            // Operands: identifiers and literals. `NullLiteral` is a
169            // childless leaf, so it needs no such guard.
170            //
171            // `QualifiedName` (`System.Text`), `GenericName`
172            // (`List<int>`) and `AliasQualifiedName` (`global::Foo`)
173            // were listed here until #1263. Each is a container whose
174            // every part the walker already reaches and classifies —
175            // the identifier leaves as operands, `.` / `::` / `<` / `>`
176            // and any `predefined_type` argument as operators — so
177            // listing the container too billed one occurrence twice
178            // (grammar-dispatch section 5). Probed with `bca ops`:
179            // dropping them leaves `System`+`Text`+`.`,
180            // `List`+`<`+`int`+`>`, and `global`+`::`+`Foo` intact.
181            //
182            // `Base` joined this list in #1380 for the same structural
183            // reason `This` did: it is the receiver of `base.M()` /
184            // `base[i]` / `: base(x)`. The `base_list` container that
185            // spells inheritance (`class D : B`) holds the base type's
186            // identifier and no `base` keyword, so nothing bills this
187            // text twice.
188            //
189            // `ImplicitParameter` is the parameter of `x => x`: a
190            // childless leaf where `(x) => x` puts an `identifier` inside
191            // a `parameter_list`, so the bare spelling dropped its name
192            // from both Halstead halves (#1544). Nothing containing it
193            // is classified.
194            Identifier | Base | ImplicitParameter
195            | IntegerLiteral | RealLiteral | BooleanLiteral | NullLiteral
196            | CharacterLiteral | StringLiteral | VerbatimStringLiteral | RawStringLiteral
197                => TokenRole::Operand,
198            // `$"..."` counts as one operand when inert. When it carries
199            // any `Interpolation` child the inner expressions are
200            // already walked and classified as operands; counting the
201            // wrapping literal too would double-count the inner
202            // identifiers' contribution to `N2` (issue #183, same
203            // pattern as #180 for Elixir/Bash).
204            InterpolatedStringExpression => {
205                Self::string_operand_type(node, &[Interpolation as u16])
206            }
207            _ => TokenRole::Unknown,
208        }
209    }
210
211    // The parameter-position spelling of six keywords (#1418). The
212    // grammar's `_parameter_type_with_modifiers` rule aliases the bare
213    // tokens `this scoped ref out in readonly` to `$.modifier`, so in
214    // that one position each is a **childless** `modifier` node rather
215    // than the kind its own arm above lists. Nothing billed them, so an
216    // extension receiver, a `ref` parameter and an `out` parameter each
217    // contributed to neither Halstead half.
218    //
219    // A blanket `Modifier` arm is not available: the `modifier` *rule*
220    // is a wrapper around a `public` / `static` / `async` /
221    // field-`readonly` keyword leaf this match already classifies, so
222    // listing the wrapper too would bill those twice (grammar-dispatch
223    // section 5). The gate is child-presence, which section 6 prescribes
224    // over deleting either side, and which
225    // `csharp_is_aliased_modifier` reads as "aliased bare token" rather
226    // than "parameter modifier" — see its doc for the second alias site.
227    //
228    // `Checker::is_bare_param` deliberately refuses to infer from
229    // `child_count() == 0`, and this is the case that refusal excludes
230    // rather than a contradiction of it: there, childlessness would be
231    // a *guess* that a MISSING or zero-width ERROR node is a real
232    // parameter. Here the grammar aliases a bare token onto this named
233    // kind, so having no children is the shape the alias produces, not
234    // evidence about a broken parse.
235    //
236    // The role follows the text, never the position: each keyword keeps
237    // the role its own kind carries elsewhere in this match, so
238    // `M(ref Foo f)` and `f(ref x)` agree, and `this` stays the operand
239    // #1380 made it. That decision's `IndexerDeclaration` carve-out
240    // cannot arise here — an indexer declarator is not a parameter, and
241    // this arm is reached only from `_parameter_type_with_modifiers`.
242    //
243    // Reading the bytes needs this spelling, which is the one the walk
244    // calls (`compute_halstead` -> `get_op_type_with_code`), so the
245    // classification reaches every count; `get_op_type` above stays the
246    // byte-less answer for the callers that have no source to offer
247    // (grammar-dispatch section 7).
248    //
249    // `CsharpCode::is_primitive` routes the operator half through the
250    // lexeme-keyed map, because these seven keywords have two kind
251    // spellings for one operator; its comment carries that argument.
252    fn get_op_type_with_code<'a>(
253        node: &Node<'a>,
254        code: &[u8],
255        ancestors: Ancestors<'a, '_>,
256    ) -> TokenRole {
257        if !crate::checker::csharp_is_aliased_modifier(node) {
258            return Self::get_op_type(node, ancestors);
259        }
260        match node_text(code, node) {
261            Some("this") => TokenRole::Operand,
262            // `_lambda_expression_init` and `anonymous_method_expression`
263            // alias a bare `static` / `async` onto `modifier` too, so
264            // `static (int x) => x` arrives here. The declaration spelling
265            // of both keywords is billed as an operator through its leaf,
266            // so the lambda spelling is billed the same way (#1482).
267            Some("ref" | "out" | "in" | "scoped" | "readonly" | "static" | "async") => {
268                TokenRole::Operator
269            }
270            // A grammar bump that adds another aliased spelling lands
271            // here unclassified rather than billed as whichever arm
272            // happened to be last.
273            _ => TokenRole::Unknown,
274        }
275    }
276
277    fn get_operator_spelling<'a>(
278        node: &Node<'a>,
279        ancestors: Ancestors<'a, '_>,
280    ) -> Option<&'static str> {
281        GENERIC_ANGLES.opener_spelling(node, ancestors)
282    }
283
284    get_operator!(Csharp);
285}