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}