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
//! `Checker` implementation for C#.
#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
use super::*;
use crate::lang_helpers::doc_comment::{csharp_doc, node_bytes};
/// A preprocessor directive that is trivia to Roslyn: one that wraps no
/// code, so the declaration after it keeps the doc comment above it.
fn csharp_directive(node: &Node) -> bool {
matches!(
node.kind_id().into(),
Csharp::PreprocRegion
| Csharp::PreprocEndregion
| Csharp::PreprocLine
| Csharp::PreprocPragma
| Csharp::PreprocNullable
| Csharp::PreprocError
| Csharp::PreprocWarning
| Csharp::PreprocDefine
| Csharp::PreprocUndef
)
}
impl Checker for CsharpCode {
fn is_comment(node: &Node) -> bool {
node.kind_id() == Csharp::Comment
}
fn is_doc_comment(node: &Node, code: &[u8]) -> bool {
Self::is_comment(node) && csharp_doc(node_bytes(node, code))
}
// Roslyn's `GetDocumentationCommentTriviaFromSyntaxNode` reads a
// member's leading trivia backwards: past plain comments and
// directives to the nearest doc comment, then over every doc comment
// before it that only blank rows separate, all of which it
// concatenates. A plain comment or a directive in that run ends it, and
// every doc comment it leaves is CS1587 (#1613). A directive that
// wraps code (`#if`) is a node the stretch cannot read past, so a doc
// comment above one stays a plain comment, where Roslyn would give it
// to the first member inside. A doc comment after code on its row
// documents the member after it too.
const DOC_BINDING: Option<DocBinding> = Some(DocBinding {
skips: |node, _| csharp_directive(node),
..DocBinding::new(KeptDocs::Leading {
hidden: |node, code| !Self::is_doc_comment(node, code),
joins: |_, _, _| true,
after_code: |_| true,
})
});
// The members `DocumentationCommentCompiler` writes: a type, a
// delegate, a member and an enum member. A namespace (block or
// file-scoped), a `using`, an assembly attribute, a top-level
// statement and an accessor take none; checked with `dotnet build`
// and CS1587.
fn is_doc_target<'a>(node: &Node<'a>, _code: &[u8], _: Ancestors<'a, '_>) -> bool {
matches!(
node.kind_id().into(),
Csharp::ClassDeclaration
| Csharp::StructDeclaration
| Csharp::RecordDeclaration
| Csharp::InterfaceDeclaration
| Csharp::EnumDeclaration
| Csharp::DelegateDeclaration
| Csharp::MethodDeclaration
| Csharp::ConstructorDeclaration
| Csharp::DestructorDeclaration
| Csharp::OperatorDeclaration
| Csharp::ConversionOperatorDeclaration
| Csharp::FieldDeclaration
| Csharp::EventFieldDeclaration
| Csharp::EventDeclaration
| Csharp::PropertyDeclaration
| Csharp::IndexerDeclaration
| Csharp::EnumMemberDeclaration
)
}
// A property or indexer holds every doc comment in it but those in its
// accessor list: Roslyn binds one only before the member, so a comment
// in its header or initializer documents nothing, as one after a
// method's attribute does. Decided by the node on the comment's path
// rather than by `is_func`, whose accessor search rescans every
// comment in the list or the header for each of them (#1612).
fn holds_doc_comment_in_body<'a>(
ancestor: &Node<'a>,
ancestors: Ancestors<'a, '_>,
below: &Node<'a>,
) -> bool {
if matches!(
ancestor.kind_id().into(),
Csharp::IndexerDeclaration | Csharp::PropertyDeclaration
) {
return below.kind_id() != Csharp::AccessorList;
}
Self::is_func(ancestor, ancestors) || Self::is_closure(ancestor, ancestors)
}
// A bodied indexer (`this[int i] { get; set; }`) or property
// (`int X { get; set; }`) defers to its `accessor_declaration` children
// for its function spaces — counting it here too double-counts (property,
// #472) or triple-counts (indexer, #464) the member in nom/wmc. Only the
// accessor-less expression-bodied form (`this[int i] => _d[i];` /
// `int W => _w;`) opens a space directly, matching the npm `.max(1)`
// fallback and the way the implicit getter is the sole callable.
fn is_func_space(node: &Node) -> bool {
if matches!(
node.kind_id().into(),
Csharp::IndexerDeclaration | Csharp::PropertyDeclaration
) {
return !csharp_member_has_accessors(node);
}
matches!(
node.kind_id().into(),
Csharp::CompilationUnit
| Csharp::ClassDeclaration
| Csharp::StructDeclaration
| Csharp::RecordDeclaration
| Csharp::InterfaceDeclaration
| Csharp::EnumDeclaration
| Csharp::MethodDeclaration
| Csharp::ConstructorDeclaration
| Csharp::DestructorDeclaration
| Csharp::LocalFunctionStatement
| Csharp::LambdaExpression
| Csharp::AnonymousMethodExpression
| Csharp::AccessorDeclaration
| Csharp::OperatorDeclaration
| Csharp::ConversionOperatorDeclaration
)
}
fn is_func<'a>(node: &Node<'a>, _ancestors: Ancestors<'a, '_>) -> bool {
if matches!(
node.kind_id().into(),
Csharp::IndexerDeclaration | Csharp::PropertyDeclaration
) {
return !csharp_member_has_accessors(node);
}
matches!(
node.kind_id().into(),
Csharp::MethodDeclaration
| Csharp::ConstructorDeclaration
| Csharp::DestructorDeclaration
| Csharp::LocalFunctionStatement
| Csharp::AccessorDeclaration
| Csharp::OperatorDeclaration
| Csharp::ConversionOperatorDeclaration
)
}
fn is_closure<'a>(node: &Node<'a>, _ancestors: Ancestors<'a, '_>) -> bool {
matches!(
node.kind_id().into(),
Csharp::LambdaExpression | Csharp::AnonymousMethodExpression
)
}
// Invocations only, by the contract on `Checker::is_call`: C#'s three
// other call-shaped constructs — `new T(…)`, the `: base(x)` /
// `: this(a)` constructor initializer, and the C# 12 primary
// constructor's `: Base(x)` — are object construction and constructor
// delegation, which ABC's Fitzpatrick branch rule counts and this
// filter deliberately does not. The matching arm on the other side of
// that split is `csharp_count_token_branch` in
// `src/metrics/abc/csharp.rs`; `csharp_is_call_excludes_constructors`
// (`checker.rs` tests) pins the exclusion (#1456).
fn is_call(node: &Node) -> bool {
// The C# grammar emits three aliased `kind_id`s for
// `invocation_expression`; matching only the unsuffixed variant
// silently drops the rest (lesson #2 in lessons_learned.md).
matches!(node.kind_id().into(), csharp_invocation_expr_kinds!())
}
/// `x => x + 1` puts a childless `implicit_parameter` in the
/// `parameters` field, where `(x) => …` puts a `parameter_list`
/// (#1185). Same shape as Java's bare-identifier lambda.
fn is_bare_param(node: &Node) -> bool {
node.kind_id() == Csharp::ImplicitParameter as u16
}
fn is_non_arg(node: &Node) -> bool {
matches!(
node.kind_id().into(),
Csharp::LPAREN | Csharp::COMMA | Csharp::RPAREN
)
}
impl_simple_is_string!(
Csharp,
StringLiteral,
VerbatimStringLiteral,
RawStringLiteral,
InterpolatedStringExpression,
);
// tree-sitter-c-sharp models `else if` as an `Else` keyword token
// followed by a nested `if_statement` (no wrapping `else_clause` node).
impl_is_else_if_prev_sibling!(Csharp, IfStatement, Else);
#[inline]
fn is_primitive(node: &Node) -> bool {
// Without this, every `PredefinedType` keyword (`int`, `string`,
// `bool`, `object`, …) collapses into a single Halstead operator
// because they share one `kind_id`. Returning `true` here routes
// them through the lexeme-keyed `primitive_operators` map so
// distinct keywords count as distinct operators (issue #286).
//
// Every keyword the grammar aliases to a childless `modifier`
// joins it for the mirror reason: the five parameter modifiers
// (#1418) and a lambda's `static` / `async` (#1482). Each has
// *two* kind spellings for one operator: its own token, and the
// aliased `modifier`. Keying only the alias by text would split
// one `ref` or `static` across the two maps and count it twice
// in `n1` — the #453 shape — while keying the alias by `kind_id`
// would instead collapse every spelling into one operator that
// `bca ops` renders as the word `modifier`, which appears
// nowhere in the source. Text is the only key under which the
// two positions are the same operator, so the bare kind of every
// keyword the getter bills through the alias must be listed too.
//
// Listing the bare kinds moves no existing count: each renders
// exactly one text through `get_operator_id_as_str` and no other
// kind renders that text, so the vocabulary entry only changes
// which map holds it.
//
// The shared predicate also holds for the aliased `this`
// receiver, which `get_op_type_with_code` bills as an operand.
// That is no disagreement, because `compute_halstead` consults
// this method only under its `Operator` arm.
matches!(
node.kind_id().into(),
Csharp::PredefinedType
| Csharp::Ref
| Csharp::Out
| Csharp::In
| Csharp::Readonly
| Csharp::Scoped
| Csharp::Static
| Csharp::Async
) || csharp_is_aliased_modifier(node)
}
}