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
//! `Getter` implementation for C#.
#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
use super::*;
const GENERIC_ANGLES: GenericAngleKinds = GenericAngleKinds {
lists: &[
Csharp::TypeArgumentList as u16,
Csharp::TypeParameterList as u16,
],
openers: &[Csharp::LT as u16],
closers: &[Csharp::GT as u16],
is_misparse: csharp_comparison_misparse,
};
// Whether `list` holds the two comparisons of a call argument
// (`F(a < b, c > d)`) that tree-sitter-c-sharp reads as a declaration
// typed `a<b, c>` and named `d`. The C# specification keeps the type
// arguments only where the token after `>` allows it (§6.2.5), and an
// identifier allows it only after `is`, `case`, `out` or in a tuple —
// so a declaration typed by a generic name is two comparisons when it is
// a call or indexer argument without `out`. `F(out List<int> x)`,
// `(List<int> p, int q) = …` and `o is List<int> x` keep their pair. A
// member access before the `<` (`F(x.a < b, c > d)`) wraps the generic
// name in `qualified_name`s, which the climb passes. The `>=` spelling
// (`F(a < b, c >= d)`) parses as an assignment instead and is not undone.
fn csharp_comparison_misparse<'a>(list: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
let mut up = ancestors.iter(list).map(|(node, _)| node);
if up
.next()
.is_none_or(|name| name.kind_id() != Csharp::GenericName as u16)
{
return false;
}
let mut up = up.skip_while(|node| node.kind_id() == Csharp::QualifiedName as u16);
let (Some(declaration), Some(argument), Some(arguments)) = (up.next(), up.next(), up.next())
else {
return false;
};
declaration.kind_id() == Csharp::DeclarationExpression as u16
&& argument.kind_id() == Csharp::Argument as u16
&& !argument.is_child(Csharp::Out as u16)
&& matches!(
arguments.kind_id().into(),
Csharp::ArgumentList | Csharp::BracketedArgumentList
)
}
impl Getter for CsharpCode {
fn get_space_kind(node: &Node) -> SpaceKind {
use Csharp::*;
// `EnumDeclaration` maps to `SpaceKind::Class` for cross-language
// parity with Java/PHP/Groovy (issue #429): a C# enum opens a
// FuncSpace via `is_func_space`, so it must classify here too or
// it falls through to `_ => SpaceKind::Unknown`.
// A bodied indexer (#464) or property (#472) defers to its
// `accessor_declaration` children; only the accessor-less
// expression-bodied form opens a Function space directly. Keep this
// gate in lockstep with `CsharpCode::is_func` / `is_func_space` so the
// walker and the space-kind classifier agree on which nodes promote.
if matches!(
node.kind_id().into(),
IndexerDeclaration | PropertyDeclaration
) {
return if crate::checker::csharp_member_has_accessors(node) {
SpaceKind::Unknown
} else {
SpaceKind::Function
};
}
match node.kind_id().into() {
ClassDeclaration | StructDeclaration | RecordDeclaration | EnumDeclaration => {
SpaceKind::Class
}
InterfaceDeclaration => SpaceKind::Interface,
MethodDeclaration
| ConstructorDeclaration
| DestructorDeclaration
| LocalFunctionStatement
| LambdaExpression
| AnonymousMethodExpression
| AccessorDeclaration
| OperatorDeclaration
| ConversionOperatorDeclaration => SpaceKind::Function,
CompilationUnit => SpaceKind::Unit,
_ => SpaceKind::Unknown,
}
}
fn get_op_type<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> TokenRole {
use Csharp::*;
if GENERIC_ANGLES.is_closer(node, ancestors) {
return TokenRole::Unknown;
}
match node.kind_id().into() {
// Control-flow keywords
If | Else | Switch | Case | Default | Try | Catch | Finally | Throw
| Return | Yield | Break | Continue | Goto | For | Foreach | While | Do
// Declaration / namespace keywords
| Class | Struct | Interface | Enum | Record | Delegate | Namespace | Using
// Modifiers
| Public | Private | Protected | Internal | Static | Abstract | Virtual
| Override | Sealed | Partial | Readonly | Const | Extern | Unsafe
| Volatile | Async | Required | File | New | Fixed | Implicit | Explicit
// Expression-keyword operators
| Await | Is | As | Typeof | Sizeof | Checked | Unchecked | Ref | Out | In
| Params | Lock | Stackalloc | Where | With | When | Operator
| Scoped | Not | And | Or
// Property/event accessor keywords
| Get | Set | Init | Add | Remove
// Structural punctuation
| LBRACE | LBRACK | LPAREN | COMMA | SEMI | COLON | COLONCOLON | DOT
| DOTDOT | EQGT | DASHGT | QMARK
// Arithmetic / comparison / logical / bitwise / assignment operators
| EQ | EQEQ | BANGEQ | LT | GT | LTEQ | GTEQ
| PLUS | DASH | STAR | SLASH | PERCENT
| AMP | PIPE | CARET | TILDE | BANG
| AMPAMP | PIPEPIPE | QMARKQMARK
| LTLT | GTGT | GTGTGT
| PLUSPLUS | DASHDASH
| PLUSEQ | DASHEQ | STAREQ | SLASHEQ | PERCENTEQ
| AMPEQ | PIPEEQ | CARETEQ | LTLTEQ | GTGTEQ | GTGTGTEQ | QMARKQMARKEQ
// Predefined / primitive types
| PredefinedType
=> TokenRole::Operator,
// `boolean_literal: choice('true', 'false')` wraps the
// keyword leaf, so a literal reaches the walker twice;
// listing both kinds inflated `N2` by one per literal while
// the text-keyed `n2` hid it (#1253). The leaf cannot simply
// be dropped: the overloadable-operator list emits a bare
// `true` / `false` under `operator_declaration` — the
// grammar's only unwrapped position. There the keyword
// *names* an operator, exactly as the `+` of `operator +`
// does, so it is billed as one (#1296); C# merely spells two
// of its overloadable operators with keywords instead of
// punctuation. The fallback keeps any other unwrapped
// position an operand, the role the keyword has as a value.
True | False => match ancestors.parent(node).map(|p| p.kind_id().into()) {
Some(BooleanLiteral) => TokenRole::Unknown,
Some(OperatorDeclaration) => TokenRole::Operator,
_ => TokenRole::Operand,
},
// `this` is a self-reference everywhere (`this.x`,
// `this[i]`, `: this(1)`, `f(this)`) except directly under
// an `indexer_declaration`, where the keyword *names* the
// member being declared rather than denoting a value —
// `public int this[int i] { … }`. That position keeps its
// operator classification alongside the `operator` keyword
// of an overload declaration, which this match already
// bills as an operator (#1380). The extension-method
// receiver (`static void M(this Foo f)`) is a childless
// `modifier` node, kind 249, not this kind at all; it is
// classified — as an operand, so the two positions agree —
// by `get_op_type_with_code` below (#1418).
//
// Java's receiver parameter (`void m(J J.this)`) is the
// other declarator use of the keyword in this workspace and
// `java.rs` calls it an operand — a parameter *name* is an
// operand in every language here, where a member named by a
// keyword belongs with `operator +`.
This => match ancestors.parent(node).map(|p| p.kind_id().into()) {
Some(IndexerDeclaration) => TokenRole::Operator,
_ => TokenRole::Operand,
},
// Operands: identifiers and literals. `NullLiteral` is a
// childless leaf, so it needs no such guard.
//
// `QualifiedName` (`System.Text`), `GenericName`
// (`List<int>`) and `AliasQualifiedName` (`global::Foo`)
// were listed here until #1263. Each is a container whose
// every part the walker already reaches and classifies —
// the identifier leaves as operands, `.` / `::` / `<` / `>`
// and any `predefined_type` argument as operators — so
// listing the container too billed one occurrence twice
// (grammar-dispatch section 5). Probed with `bca ops`:
// dropping them leaves `System`+`Text`+`.`,
// `List`+`<`+`int`+`>`, and `global`+`::`+`Foo` intact.
//
// `Base` joined this list in #1380 for the same structural
// reason `This` did: it is the receiver of `base.M()` /
// `base[i]` / `: base(x)`. The `base_list` container that
// spells inheritance (`class D : B`) holds the base type's
// identifier and no `base` keyword, so nothing bills this
// text twice.
//
// `ImplicitParameter` is the parameter of `x => x`: a
// childless leaf where `(x) => x` puts an `identifier` inside
// a `parameter_list`, so the bare spelling dropped its name
// from both Halstead halves (#1544). Nothing containing it
// is classified.
Identifier | Base | ImplicitParameter
| IntegerLiteral | RealLiteral | BooleanLiteral | NullLiteral
| CharacterLiteral | StringLiteral | VerbatimStringLiteral | RawStringLiteral
=> TokenRole::Operand,
// `$"..."` counts as one operand when inert. When it carries
// any `Interpolation` child the inner expressions are
// already walked and classified as operands; counting the
// wrapping literal too would double-count the inner
// identifiers' contribution to `N2` (issue #183, same
// pattern as #180 for Elixir/Bash).
InterpolatedStringExpression => {
Self::string_operand_type(node, &[Interpolation as u16])
}
_ => TokenRole::Unknown,
}
}
// The parameter-position spelling of six keywords (#1418). The
// grammar's `_parameter_type_with_modifiers` rule aliases the bare
// tokens `this scoped ref out in readonly` to `$.modifier`, so in
// that one position each is a **childless** `modifier` node rather
// than the kind its own arm above lists. Nothing billed them, so an
// extension receiver, a `ref` parameter and an `out` parameter each
// contributed to neither Halstead half.
//
// A blanket `Modifier` arm is not available: the `modifier` *rule*
// is a wrapper around a `public` / `static` / `async` /
// field-`readonly` keyword leaf this match already classifies, so
// listing the wrapper too would bill those twice (grammar-dispatch
// section 5). The gate is child-presence, which section 6 prescribes
// over deleting either side, and which
// `csharp_is_aliased_modifier` reads as "aliased bare token" rather
// than "parameter modifier" — see its doc for the second alias site.
//
// `Checker::is_bare_param` deliberately refuses to infer from
// `child_count() == 0`, and this is the case that refusal excludes
// rather than a contradiction of it: there, childlessness would be
// a *guess* that a MISSING or zero-width ERROR node is a real
// parameter. Here the grammar aliases a bare token onto this named
// kind, so having no children is the shape the alias produces, not
// evidence about a broken parse.
//
// The role follows the text, never the position: each keyword keeps
// the role its own kind carries elsewhere in this match, so
// `M(ref Foo f)` and `f(ref x)` agree, and `this` stays the operand
// #1380 made it. That decision's `IndexerDeclaration` carve-out
// cannot arise here — an indexer declarator is not a parameter, and
// this arm is reached only from `_parameter_type_with_modifiers`.
//
// Reading the bytes needs this spelling, which is the one the walk
// calls (`compute_halstead` -> `get_op_type_with_code`), so the
// classification reaches every count; `get_op_type` above stays the
// byte-less answer for the callers that have no source to offer
// (grammar-dispatch section 7).
//
// `CsharpCode::is_primitive` routes the operator half through the
// lexeme-keyed map, because these seven keywords have two kind
// spellings for one operator; its comment carries that argument.
fn get_op_type_with_code<'a>(
node: &Node<'a>,
code: &[u8],
ancestors: Ancestors<'a, '_>,
) -> TokenRole {
if !crate::checker::csharp_is_aliased_modifier(node) {
return Self::get_op_type(node, ancestors);
}
match node_text(code, node) {
Some("this") => TokenRole::Operand,
// `_lambda_expression_init` and `anonymous_method_expression`
// alias a bare `static` / `async` onto `modifier` too, so
// `static (int x) => x` arrives here. The declaration spelling
// of both keywords is billed as an operator through its leaf,
// so the lambda spelling is billed the same way (#1482).
Some("ref" | "out" | "in" | "scoped" | "readonly" | "static" | "async") => {
TokenRole::Operator
}
// A grammar bump that adds another aliased spelling lands
// here unclassified rather than billed as whichever arm
// happened to be last.
_ => TokenRole::Unknown,
}
}
fn get_operator_spelling<'a>(
node: &Node<'a>,
ancestors: Ancestors<'a, '_>,
) -> Option<&'static str> {
GENERIC_ANGLES.opener_spelling(node, ancestors)
}
get_operator!(Csharp);
}