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