code-gauge 8.0.1 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -46
- package/dist/checkCommand.cjs +3 -3
- package/dist/checkCommand.cjs.map +1 -1
- package/dist/checkCommand.d.ts +22 -1
- package/dist/checkCommand.js +3 -3
- package/dist/checkCommand.js.map +1 -1
- package/dist/cli.cjs +3 -3
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +3 -3
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/metrics.cjs +1 -1
- package/dist/metrics.cjs.map +1 -1
- package/dist/metrics.js +1 -1
- package/dist/metrics.js.map +1 -1
- package/dist/nativeMetrics.cjs +1 -1
- package/dist/nativeMetrics.cjs.map +1 -1
- package/dist/nativeMetrics.d.ts +4 -2
- package/dist/nativeMetrics.js +1 -1
- package/dist/nativeMetrics.js.map +1 -1
- package/dist/thresholdCheck.cjs +1 -1
- package/dist/thresholdCheck.cjs.map +1 -1
- package/dist/thresholdCheck.d.ts +18 -4
- package/dist/thresholdCheck.js +1 -1
- package/dist/thresholdCheck.js.map +1 -1
- package/dist/thresholds.cjs +1 -1
- package/dist/thresholds.cjs.map +1 -1
- package/dist/thresholds.d.ts +5 -3
- package/dist/thresholds.js +1 -1
- package/dist/thresholds.js.map +1 -1
- package/dist/types.d.ts +37 -0
- package/dist/wasmBinding.cjs +1 -1
- package/dist/wasmBinding.cjs.map +1 -1
- package/dist/wasmBinding.js +1 -1
- package/dist/wasmBinding.js.map +1 -1
- package/native/code-gauge.wasm +0 -0
- package/native/src/complexity.rs +173 -19
- package/native/src/container.rs +413 -0
- package/native/src/functions.rs +2 -5
- package/native/src/lib.rs +2 -1
- package/native/src/measure.rs +24 -8
- package/native/src/types.rs +21 -0
- package/package.json +9 -8
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
use rustc_hash::FxHashSet;
|
|
2
|
+
use tree_sitter::Node;
|
|
3
|
+
|
|
4
|
+
use crate::functions::next_declarator;
|
|
5
|
+
use crate::tree_index::NodeExt;
|
|
6
|
+
use crate::util::{named_children, node_text, Source};
|
|
7
|
+
|
|
8
|
+
const CONTAINER_NODE_TYPES: &[&str] = &[
|
|
9
|
+
"abstract_class_declaration",
|
|
10
|
+
"class",
|
|
11
|
+
"class_declaration",
|
|
12
|
+
"class_definition",
|
|
13
|
+
"class_specifier",
|
|
14
|
+
"enum_declaration",
|
|
15
|
+
"impl_item",
|
|
16
|
+
"interface_declaration",
|
|
17
|
+
"internal_module",
|
|
18
|
+
"mod_item",
|
|
19
|
+
"module",
|
|
20
|
+
"namespace_definition",
|
|
21
|
+
"object_declaration",
|
|
22
|
+
"record_declaration",
|
|
23
|
+
"struct_declaration",
|
|
24
|
+
"struct_specifier",
|
|
25
|
+
"trait_item",
|
|
26
|
+
"union_specifier",
|
|
27
|
+
];
|
|
28
|
+
/// Whether the function is declared, as a method or a named function is, rather than written as a
|
|
29
|
+
/// value (a lambda, a closure, a function expression).
|
|
30
|
+
fn is_function_declaration(node: Node<'_>) -> bool {
|
|
31
|
+
let kind = node.kind_name();
|
|
32
|
+
kind.ends_with("_declaration")
|
|
33
|
+
|| kind.ends_with("_definition")
|
|
34
|
+
|| matches!(
|
|
35
|
+
kind,
|
|
36
|
+
"function_item"
|
|
37
|
+
| "getter"
|
|
38
|
+
| "local_function_statement"
|
|
39
|
+
| "method"
|
|
40
|
+
| "secondary_constructor"
|
|
41
|
+
| "setter"
|
|
42
|
+
| "singleton_method"
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const CPP_SCOPE_NODE_TYPES: &[&str] = &[
|
|
47
|
+
"class_specifier",
|
|
48
|
+
"namespace_definition",
|
|
49
|
+
"struct_specifier",
|
|
50
|
+
"union_specifier",
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/// The names of the C++ namespaces and classes around `node`, outermost first, up to the function
|
|
54
|
+
/// enclosing it.
|
|
55
|
+
fn enclosing_cpp_scopes<'s>(
|
|
56
|
+
node: Node<'_>,
|
|
57
|
+
function_nodes: &FxHashSet<&'static str>,
|
|
58
|
+
code: &Source<'s>,
|
|
59
|
+
namespaces_only: bool,
|
|
60
|
+
) -> Vec<&'s str> {
|
|
61
|
+
let mut scopes = Vec::new();
|
|
62
|
+
let mut current = node.parent_node();
|
|
63
|
+
while let Some(ancestor) = current {
|
|
64
|
+
if crate::complexity::is_function_boundary(ancestor, function_nodes) {
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
let is_namespace = ancestor.kind_name() == "namespace_definition";
|
|
68
|
+
if CPP_SCOPE_NODE_TYPES.contains(&ancestor.kind_name())
|
|
69
|
+
&& (is_namespace || !namespaces_only)
|
|
70
|
+
&& !is_transparent_namespace(ancestor)
|
|
71
|
+
{
|
|
72
|
+
match ancestor.child_by_field_name("name") {
|
|
73
|
+
// `namespace a::b` names two scopes at once.
|
|
74
|
+
Some(name) => scopes.extend(scope_names(name, code).into_iter().rev()),
|
|
75
|
+
// An unnamed class ends the owner, as its members are reached through no name.
|
|
76
|
+
None => break,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
current = ancestor.parent_node();
|
|
80
|
+
}
|
|
81
|
+
scopes.reverse();
|
|
82
|
+
scopes
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// The object a Ruby singleton method or singleton class names, unless it is `self`.
|
|
86
|
+
fn explicit_ruby_owner(object: Option<Node<'_>>, code: &Source<'_>) -> Option<String> {
|
|
87
|
+
let text = node_text(object?, code);
|
|
88
|
+
(text != "self").then(|| text.to_string())
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// The name of the declaration whose member a declared function is: the nearest class-like
|
|
92
|
+
/// declaration enclosing it, or the owner the function names itself (a Go receiver, a C++ qualified
|
|
93
|
+
/// declarator, the object of a Ruby singleton method).
|
|
94
|
+
pub fn find_container_name(
|
|
95
|
+
node: Node<'_>,
|
|
96
|
+
function_nodes: &FxHashSet<&'static str>,
|
|
97
|
+
code: &Source<'_>,
|
|
98
|
+
inline_namespaces: &InlineNamespaces<'_>,
|
|
99
|
+
) -> Option<String> {
|
|
100
|
+
// A function written as a value belongs to whatever it is passed or assigned to, which the
|
|
101
|
+
// syntax cannot follow reliably; it keeps the name of what it is bound to and has no owner.
|
|
102
|
+
if !is_function_declaration(node) {
|
|
103
|
+
return None;
|
|
104
|
+
}
|
|
105
|
+
if let Some(receiver) = node.child_by_field_name("receiver") {
|
|
106
|
+
return go_receiver_type(receiver, code);
|
|
107
|
+
}
|
|
108
|
+
if let Some(qualified) = find_qualified_declarator(node) {
|
|
109
|
+
return cpp_spelled_owner(node, qualified, function_nodes, code, inline_namespaces);
|
|
110
|
+
}
|
|
111
|
+
// A Ruby `def Other.decide` belongs to the object it names rather than to the class around it.
|
|
112
|
+
explicit_ruby_owner(node.child_by_field_name("object"), code)
|
|
113
|
+
.or_else(|| enclosing_owner(node, function_nodes, code, inline_namespaces))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// A Go method is declared outside its type and names it as its receiver, `(r *Rules)` or
|
|
117
|
+
/// `(r Rules[T])`.
|
|
118
|
+
fn go_receiver_type(receiver: Node<'_>, code: &Source<'_>) -> Option<String> {
|
|
119
|
+
let mut receiver_type = named_children(receiver)
|
|
120
|
+
.into_iter()
|
|
121
|
+
.find_map(|parameter| parameter.child_by_field_name("type"))?;
|
|
122
|
+
while receiver_type.kind_name() != "type_identifier" {
|
|
123
|
+
receiver_type = receiver_type
|
|
124
|
+
.child_by_field_name("type")
|
|
125
|
+
.or_else(|| named_children(receiver_type).into_iter().next())?;
|
|
126
|
+
}
|
|
127
|
+
Some(node_text(receiver_type, code).to_string())
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// A C++ method defined outside its class names it in its declarator, `int Rules::decide()`.
|
|
131
|
+
fn find_qualified_declarator(node: Node<'_>) -> Option<Node<'_>> {
|
|
132
|
+
let mut declarator = node.child_by_field_name("declarator");
|
|
133
|
+
while let Some(inner) = declarator {
|
|
134
|
+
if inner.kind_name() == "qualified_identifier" {
|
|
135
|
+
return Some(inner);
|
|
136
|
+
}
|
|
137
|
+
declarator = next_declarator(inner);
|
|
138
|
+
}
|
|
139
|
+
None
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fn cpp_spelled_owner(
|
|
143
|
+
node: Node<'_>,
|
|
144
|
+
qualified: Node<'_>,
|
|
145
|
+
function_nodes: &FxHashSet<&'static str>,
|
|
146
|
+
code: &Source<'_>,
|
|
147
|
+
inline_namespaces: &InlineNamespaces<'_>,
|
|
148
|
+
) -> Option<String> {
|
|
149
|
+
// A nested name (`ns::Rules::decide`) nests in its `name` field; the scopes on the way to the
|
|
150
|
+
// innermost name are the owner. The name itself may hold `::` too (`operator std::string`), so
|
|
151
|
+
// the text is not split.
|
|
152
|
+
let mut scopes = Vec::new();
|
|
153
|
+
// `::ns::Rules::decide` starts at the global namespace, whatever encloses it.
|
|
154
|
+
let is_absolute = qualified.child_by_field_name("scope").is_none();
|
|
155
|
+
let mut current = qualified;
|
|
156
|
+
while current.kind_name() == "qualified_identifier" {
|
|
157
|
+
if let Some(scope) = current.child_by_field_name("scope") {
|
|
158
|
+
// `Rules<T>::decide` belongs to the class its definition names `Rules`.
|
|
159
|
+
let named = scope.child_by_field_name("name").unwrap_or(scope);
|
|
160
|
+
scopes.push(node_text(named, code));
|
|
161
|
+
}
|
|
162
|
+
current = current.child_by_field_name("name")?;
|
|
163
|
+
}
|
|
164
|
+
// A definition inside `namespace ns { ... }` belongs to that namespace too, which it may also
|
|
165
|
+
// spell out (`ns::Rules::decide` inside `namespace ns`).
|
|
166
|
+
let owner = if is_absolute {
|
|
167
|
+
Vec::new()
|
|
168
|
+
} else {
|
|
169
|
+
enclosing_cpp_scopes(node, function_nodes, code, false)
|
|
170
|
+
};
|
|
171
|
+
spell_cpp_scopes(merge_cpp_scopes(owner, scopes), inline_namespaces)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// The scopes as an owner, without the inline namespaces the file declares: one adds nothing when
|
|
175
|
+
/// spelled (`mylib::v2::Thing` is `mylib::Thing`), as it adds nothing around a definition, while a
|
|
176
|
+
/// plain namespace of the same name elsewhere stays.
|
|
177
|
+
fn spell_cpp_scopes(scopes: Vec<&str>, inline_namespaces: &InlineNamespaces<'_>) -> Option<String> {
|
|
178
|
+
let mut resolved: Vec<&str> = Vec::new();
|
|
179
|
+
for scope in scopes {
|
|
180
|
+
let is_inline = inline_namespaces
|
|
181
|
+
.iter()
|
|
182
|
+
.any(|(path, name)| *name == scope && *path == resolved);
|
|
183
|
+
if !is_inline {
|
|
184
|
+
resolved.push(scope);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
(!resolved.is_empty()).then(|| resolved.join("::"))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// The inline C++ namespaces a file declares, each as the scopes around it and its name.
|
|
191
|
+
pub type InlineNamespaces<'s> = Vec<(Vec<&'s str>, &'s str)>;
|
|
192
|
+
|
|
193
|
+
/// Collected once per file, as every owner in it is checked against them. Namespaces nest only in
|
|
194
|
+
/// one another and in the wrappers below, so the search descends through those alone.
|
|
195
|
+
pub fn find_inline_namespaces<'s>(root: Node<'_>, code: &Source<'s>) -> InlineNamespaces<'s> {
|
|
196
|
+
let mut found = Vec::new();
|
|
197
|
+
let mut pending = vec![(root, Vec::new())];
|
|
198
|
+
while let Some((scope, path)) = pending.pop() {
|
|
199
|
+
for child in named_children(scope) {
|
|
200
|
+
if holds_namespaces_without_scope(child) {
|
|
201
|
+
pending.push((child, path.clone()));
|
|
202
|
+
}
|
|
203
|
+
if child.kind_name() != "namespace_definition" {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
let inner_path = enter_namespace(child, &path, &mut found, code);
|
|
207
|
+
if let Some(body) = child.child_by_field_name("body") {
|
|
208
|
+
pending.push((body, inner_path));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
found
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/// The enclosing scopes followed by the spelled ones, which take over from the innermost
|
|
216
|
+
/// enclosing scope they start with, as name lookup finds that one first: `ns::Rules` spelled
|
|
217
|
+
/// inside `namespace ns` is `ns::Rules`.
|
|
218
|
+
fn merge_cpp_scopes<'s>(mut enclosing: Vec<&'s str>, spelled: Vec<&'s str>) -> Vec<&'s str> {
|
|
219
|
+
let restart = enclosing
|
|
220
|
+
.iter()
|
|
221
|
+
.rposition(|scope| Some(scope) == spelled.first());
|
|
222
|
+
enclosing.truncate(restart.unwrap_or(enclosing.len()));
|
|
223
|
+
enclosing.extend(spelled);
|
|
224
|
+
enclosing
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/// Records the inline parts of the namespace's name and returns the path of its members.
|
|
228
|
+
fn enter_namespace<'s>(
|
|
229
|
+
namespace: Node<'_>,
|
|
230
|
+
path: &[&'s str],
|
|
231
|
+
found: &mut InlineNamespaces<'s>,
|
|
232
|
+
code: &Source<'s>,
|
|
233
|
+
) -> Vec<&'s str> {
|
|
234
|
+
let mut inner_path = path.to_vec();
|
|
235
|
+
let Some(name) = namespace.child_by_field_name("name") else {
|
|
236
|
+
return inner_path;
|
|
237
|
+
};
|
|
238
|
+
for (part, is_inline) in namespace_parts(name, is_transparent_namespace(namespace), code) {
|
|
239
|
+
if is_inline {
|
|
240
|
+
found.push((inner_path.clone(), part));
|
|
241
|
+
} else {
|
|
242
|
+
inner_path.push(part);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
inner_path
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/// The parts of a namespace's name, each with whether it is inline: the namespace itself when
|
|
249
|
+
/// declared `inline namespace v2`, or a part of C++20's `namespace a::inline v2`.
|
|
250
|
+
fn namespace_parts<'s>(name: Node<'_>, is_inline: bool, code: &Source<'s>) -> Vec<(&'s str, bool)> {
|
|
251
|
+
if name.kind_name() != "nested_namespace_specifier" {
|
|
252
|
+
return vec![(node_text(name, code), is_inline)];
|
|
253
|
+
}
|
|
254
|
+
let mut parts = Vec::new();
|
|
255
|
+
let mut follows_inline = false;
|
|
256
|
+
for child in crate::util::all_children(name) {
|
|
257
|
+
// A comment between `inline` and the name is no part and does not end the marking.
|
|
258
|
+
if child.is_extra() {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if child.is_named() {
|
|
262
|
+
parts.extend(namespace_parts(child, follows_inline, code));
|
|
263
|
+
}
|
|
264
|
+
follows_inline = child.kind_name() == "inline";
|
|
265
|
+
}
|
|
266
|
+
parts
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/// An include guard, an `extern "C"` block, or a module's `export` holds namespaces without being
|
|
270
|
+
/// a scope.
|
|
271
|
+
fn holds_namespaces_without_scope(node: Node<'_>) -> bool {
|
|
272
|
+
node.kind_name().starts_with("preproc_")
|
|
273
|
+
|| matches!(
|
|
274
|
+
node.kind_name(),
|
|
275
|
+
"declaration_list" | "export_declaration" | "linkage_specification"
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/// The nearest class-like declaration around the function, unless something between them makes
|
|
280
|
+
/// the function no member of it.
|
|
281
|
+
fn enclosing_owner(
|
|
282
|
+
node: Node<'_>,
|
|
283
|
+
function_nodes: &FxHashSet<&'static str>,
|
|
284
|
+
code: &Source<'_>,
|
|
285
|
+
inline_namespaces: &InlineNamespaces<'_>,
|
|
286
|
+
) -> Option<String> {
|
|
287
|
+
let mut current = node.parent_node();
|
|
288
|
+
// A C++ friend defined in a class belongs to the scopes around that class.
|
|
289
|
+
let mut is_friend = false;
|
|
290
|
+
while let Some(ancestor) = current {
|
|
291
|
+
is_friend |= ancestor.kind_name() == "friend_declaration";
|
|
292
|
+
// A method inside `class << Other` belongs to Other; `class << self` leaves the class
|
|
293
|
+
// around it.
|
|
294
|
+
if ancestor.kind_name() == "singleton_class" {
|
|
295
|
+
if let Some(owner) = explicit_ruby_owner(ancestor.child_by_field_name("value"), code) {
|
|
296
|
+
return Some(owner);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if crate::complexity::is_function_boundary(ancestor, function_nodes)
|
|
300
|
+
|| ends_membership(ancestor)
|
|
301
|
+
{
|
|
302
|
+
return None;
|
|
303
|
+
}
|
|
304
|
+
if is_named_container(ancestor) {
|
|
305
|
+
return spell_owner(ancestor, is_friend, function_nodes, code, inline_namespaces);
|
|
306
|
+
}
|
|
307
|
+
current = ancestor.parent_node();
|
|
308
|
+
}
|
|
309
|
+
None
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
fn spell_owner(
|
|
313
|
+
container: Node<'_>,
|
|
314
|
+
is_friend: bool,
|
|
315
|
+
function_nodes: &FxHashSet<&'static str>,
|
|
316
|
+
code: &Source<'_>,
|
|
317
|
+
inline_namespaces: &InlineNamespaces<'_>,
|
|
318
|
+
) -> Option<String> {
|
|
319
|
+
let name = scope_names(container_name_node(container)?, code);
|
|
320
|
+
if name.iter().any(|part| part.is_empty()) {
|
|
321
|
+
return None;
|
|
322
|
+
}
|
|
323
|
+
if !CPP_SCOPE_NODE_TYPES.contains(&container.kind_name()) {
|
|
324
|
+
return Some(name.join("::"));
|
|
325
|
+
}
|
|
326
|
+
// C++ spells an owner with the namespaces and classes around it (`ns::Rules`), as a definition
|
|
327
|
+
// outside them has to. A friend has namespace scope, whatever classes its declaring class is
|
|
328
|
+
// nested in.
|
|
329
|
+
let mut owner = enclosing_cpp_scopes(container, function_nodes, code, is_friend);
|
|
330
|
+
// Only a class spelled with a qualifier (`class ns::Box<int>`) refers to scopes that exist; a
|
|
331
|
+
// plain name, or the parts of `namespace a::b`, declare new ones even when they repeat an
|
|
332
|
+
// enclosing name.
|
|
333
|
+
let is_qualified_class = name.len() > 1 && container.kind_name() != "namespace_definition";
|
|
334
|
+
if is_qualified_class && !is_friend {
|
|
335
|
+
owner = merge_cpp_scopes(owner, name);
|
|
336
|
+
} else if !is_friend {
|
|
337
|
+
owner.extend(name);
|
|
338
|
+
}
|
|
339
|
+
spell_cpp_scopes(owner, inline_namespaces)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/// A function local to an initializer block, or a member of an object literal or of an anonymous
|
|
343
|
+
/// class (a class body whose parent declares no type) is no
|
|
344
|
+
/// member of the named type around it.
|
|
345
|
+
fn ends_membership(ancestor: Node<'_>) -> bool {
|
|
346
|
+
ancestor.kind_name() == "object"
|
|
347
|
+
|| crate::measure::is_initializer_block(ancestor)
|
|
348
|
+
|| (ancestor.kind_name() == "class_body"
|
|
349
|
+
&& ancestor
|
|
350
|
+
.parent_node()
|
|
351
|
+
// A Kotlin companion's members are reached through the class it accompanies.
|
|
352
|
+
.is_none_or(|owner| {
|
|
353
|
+
owner.kind_name() != "companion_object"
|
|
354
|
+
&& !CONTAINER_NODE_TYPES.contains(&owner.kind_name())
|
|
355
|
+
}))
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
fn is_named_container(ancestor: Node<'_>) -> bool {
|
|
359
|
+
ancestor.is_named()
|
|
360
|
+
&& !is_transparent_namespace(ancestor)
|
|
361
|
+
&& CONTAINER_NODE_TYPES.contains(&ancestor.kind_name())
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/// The members of an unnamed or inline C++ namespace are reached through the namespace around it,
|
|
365
|
+
/// which is how a definition outside it spells them.
|
|
366
|
+
fn is_transparent_namespace(node: Node<'_>) -> bool {
|
|
367
|
+
node.kind_name() == "namespace_definition"
|
|
368
|
+
&& (node.child_by_field_name("name").is_none()
|
|
369
|
+
|| node
|
|
370
|
+
.child(0)
|
|
371
|
+
.is_some_and(|first| first.kind_name() == "inline"))
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/// The identifiers a name consists of: one, or those of a C++ `a::b`, whose separators, spacing,
|
|
375
|
+
/// and comments are no part of it.
|
|
376
|
+
fn scope_names<'s>(name: Node<'_>, code: &Source<'s>) -> Vec<&'s str> {
|
|
377
|
+
// A specialization `class Box<int>` is named by its template, as `Box<int>::get` is spelled.
|
|
378
|
+
if name.kind_name() == "template_type" {
|
|
379
|
+
if let Some(template) = name.child_by_field_name("name") {
|
|
380
|
+
return scope_names(template, code);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// `class ns::Box<int>` spells its namespace in front of the name.
|
|
384
|
+
if name.kind_name() == "qualified_identifier" {
|
|
385
|
+
return ["scope", "name"]
|
|
386
|
+
.into_iter()
|
|
387
|
+
.filter_map(|field| name.child_by_field_name(field))
|
|
388
|
+
.flat_map(|part| scope_names(part, code))
|
|
389
|
+
.collect();
|
|
390
|
+
}
|
|
391
|
+
let parts: Vec<Node<'_>> = named_children(name)
|
|
392
|
+
.into_iter()
|
|
393
|
+
.filter(|part| !part.is_extra())
|
|
394
|
+
.collect();
|
|
395
|
+
if name.kind_name() != "nested_namespace_specifier" || parts.is_empty() {
|
|
396
|
+
return vec![node_text(name, code)];
|
|
397
|
+
}
|
|
398
|
+
parts
|
|
399
|
+
.into_iter()
|
|
400
|
+
.flat_map(|part| scope_names(part, code))
|
|
401
|
+
.collect()
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
fn container_name_node(container: Node<'_>) -> Option<Node<'_>> {
|
|
405
|
+
// A Rust `impl` names its type in the `type` field, where `Pass<'a>` in turn wraps the name in
|
|
406
|
+
// a `generic_type`.
|
|
407
|
+
container.child_by_field_name("name").or_else(|| {
|
|
408
|
+
let implemented = container.child_by_field_name("type")?;
|
|
409
|
+
implemented
|
|
410
|
+
.child_by_field_name("type")
|
|
411
|
+
.or(Some(implemented))
|
|
412
|
+
})
|
|
413
|
+
}
|
package/native/src/functions.rs
CHANGED
|
@@ -977,10 +977,7 @@ fn is_value_of_parent(node: Node<'_>, parent: Node<'_>) -> bool {
|
|
|
977
977
|
.child(index)
|
|
978
978
|
.is_some_and(|child| child.id() == node.id())
|
|
979
979
|
{
|
|
980
|
-
return matches!(
|
|
981
|
-
parent.field_name_for_child(index),
|
|
982
|
-
None | Some("value")
|
|
983
|
-
);
|
|
980
|
+
return matches!(parent.field_name_for_child(index), None | Some("value"));
|
|
984
981
|
}
|
|
985
982
|
}
|
|
986
983
|
false
|
|
@@ -1312,7 +1309,7 @@ fn unwrap_declarator_name(declarator: Option<Node<'_>>, code: &Source<'_>) -> Op
|
|
|
1312
1309
|
|
|
1313
1310
|
/// Steps into the inner declarator; `reference_declarator` and `parenthesized_declarator` do not
|
|
1314
1311
|
/// expose a `declarator` field in tree-sitter-cpp, so their sole named child is the inner node.
|
|
1315
|
-
fn next_declarator(node: Node<'_>) -> Option<Node<'_>> {
|
|
1312
|
+
pub(crate) fn next_declarator(node: Node<'_>) -> Option<Node<'_>> {
|
|
1316
1313
|
if let Some(direct) = node.child_by_field_name("declarator") {
|
|
1317
1314
|
return Some(direct);
|
|
1318
1315
|
}
|
package/native/src/lib.rs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
use crate::duplication::DuplicationSettings;
|
|
4
4
|
|
|
5
5
|
mod complexity;
|
|
6
|
+
mod container;
|
|
6
7
|
mod dep_degree;
|
|
7
8
|
mod duplication;
|
|
8
9
|
mod functions;
|
|
@@ -28,7 +29,7 @@ mod worker_pool;
|
|
|
28
29
|
/// src/nativeMetrics.ts.
|
|
29
30
|
/// scripts/installNative.mjs parses the literal from this function's source.
|
|
30
31
|
pub fn payload_version() -> u32 {
|
|
31
|
-
|
|
32
|
+
10
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
/// Measures code metrics for the given source, returning the NativeMetrics payload as JSON; with
|
package/native/src/measure.rs
CHANGED
|
@@ -49,6 +49,7 @@ pub fn measure(
|
|
|
49
49
|
.collect();
|
|
50
50
|
|
|
51
51
|
let body_metrics = measure_function_body_metrics(root, &sets, code);
|
|
52
|
+
let inline_namespaces = crate::container::find_inline_namespaces(root, code);
|
|
52
53
|
let function_metrics: Vec<FunctionMetrics> = functions
|
|
53
54
|
.iter()
|
|
54
55
|
.map(|node| {
|
|
@@ -58,6 +59,12 @@ pub fn measure(
|
|
|
58
59
|
.expect("every collected function node opens a frame in the body-metrics pass");
|
|
59
60
|
FunctionMetrics {
|
|
60
61
|
name: find_function_name(*node, code),
|
|
62
|
+
container_name: crate::container::find_container_name(
|
|
63
|
+
*node,
|
|
64
|
+
&sets.function_nodes,
|
|
65
|
+
code,
|
|
66
|
+
&inline_namespaces,
|
|
67
|
+
),
|
|
61
68
|
node_type: node.kind_name().to_string(),
|
|
62
69
|
start_line: node.start_position().row + 1,
|
|
63
70
|
// The tree is parsed from UTF-16, so columns are UTF-16 code units x 2 — halving
|
|
@@ -70,6 +77,7 @@ pub fn measure(
|
|
|
70
77
|
// cycle, but this is intentionally not implemented (issue #22): mainstream
|
|
71
78
|
// implementations (PMD, SonarQube analyzers) omit it.
|
|
72
79
|
cognitive_complexity: body_metrics.cognitive_complexity,
|
|
80
|
+
cognitive_blocks: body_metrics.cognitive_blocks.clone(),
|
|
73
81
|
nesting_depth: body_metrics.nesting_depth,
|
|
74
82
|
ncss: body_metrics.ncss,
|
|
75
83
|
parameter_count: count_parameters(*node, code),
|
|
@@ -87,6 +95,11 @@ pub fn measure(
|
|
|
87
95
|
language: language.name.to_string(),
|
|
88
96
|
bytes: code.code.len(),
|
|
89
97
|
lines,
|
|
98
|
+
code_line_numbers: {
|
|
99
|
+
let mut sorted: Vec<usize> = code_line_numbers.iter().copied().collect();
|
|
100
|
+
sorted.sort_unstable();
|
|
101
|
+
sorted
|
|
102
|
+
},
|
|
90
103
|
// McCabe's v = e - n + 2p over the file's components: every function, every initializer
|
|
91
104
|
// block, and the module body when the file runs top-level code; decisions outside functions
|
|
92
105
|
// belong to the file.
|
|
@@ -133,17 +146,20 @@ const INITIALIZER_NODE_TYPES: &[&str] = &[
|
|
|
133
146
|
"block",
|
|
134
147
|
];
|
|
135
148
|
|
|
149
|
+
/// A block of a class that runs when the class or an instance is initialized.
|
|
150
|
+
pub fn is_initializer_block(node: Node<'_>) -> bool {
|
|
151
|
+
INITIALIZER_NODE_TYPES.contains(&node.kind_name())
|
|
152
|
+
// A bare block is an initializer only as a direct member of a Java class or enum body.
|
|
153
|
+
&& (node.kind_name() != "block"
|
|
154
|
+
|| node.parent_node().is_some_and(|parent| {
|
|
155
|
+
matches!(parent.kind_name(), "class_body" | "enum_body_declarations")
|
|
156
|
+
}))
|
|
157
|
+
}
|
|
158
|
+
|
|
136
159
|
fn count_initializer_blocks(candidates: &[Node<'_>]) -> u64 {
|
|
137
160
|
candidates
|
|
138
161
|
.iter()
|
|
139
|
-
.filter(|node|
|
|
140
|
-
.filter(|node| {
|
|
141
|
-
// A bare block is an initializer only as a direct member of a Java class or enum body.
|
|
142
|
-
node.kind_name() != "block"
|
|
143
|
-
|| node.parent_node().is_some_and(|parent| {
|
|
144
|
-
matches!(parent.kind_name(), "class_body" | "enum_body_declarations")
|
|
145
|
-
})
|
|
146
|
-
})
|
|
162
|
+
.filter(|node| is_initializer_block(**node))
|
|
147
163
|
.count() as u64
|
|
148
164
|
}
|
|
149
165
|
|
package/native/src/types.rs
CHANGED
|
@@ -10,6 +10,8 @@ pub struct NativeMetrics {
|
|
|
10
10
|
pub language: String,
|
|
11
11
|
pub bytes: usize,
|
|
12
12
|
pub lines: LineMetrics,
|
|
13
|
+
/// The 1-based lines counted in `lines.code`, sorted ascending.
|
|
14
|
+
pub code_line_numbers: Vec<usize>,
|
|
13
15
|
pub functions: Vec<FunctionMetrics>,
|
|
14
16
|
pub cyclomatic_complexity: u64,
|
|
15
17
|
pub cognitive_complexity: u64,
|
|
@@ -39,6 +41,9 @@ pub struct LineMetrics {
|
|
|
39
41
|
pub struct FunctionMetrics {
|
|
40
42
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
41
43
|
pub name: Option<String>,
|
|
44
|
+
/// The class-like declaration the function is a member of.
|
|
45
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
46
|
+
pub container_name: Option<String>,
|
|
42
47
|
/// The tree-sitter node type of the function node (e.g. `method_declaration`, `arrow_function`).
|
|
43
48
|
pub node_type: String,
|
|
44
49
|
pub start_line: usize,
|
|
@@ -47,6 +52,9 @@ pub struct FunctionMetrics {
|
|
|
47
52
|
pub end_column: usize,
|
|
48
53
|
pub cyclomatic_complexity: u64,
|
|
49
54
|
pub cognitive_complexity: u64,
|
|
55
|
+
/// The blocks adding the most cognitive complexity, largest first.
|
|
56
|
+
#[serde(skip_serializing_if = "Vec::is_empty")]
|
|
57
|
+
pub cognitive_blocks: Vec<CognitiveBlock>,
|
|
50
58
|
pub nesting_depth: u64,
|
|
51
59
|
pub ncss: u64,
|
|
52
60
|
pub parameter_count: usize,
|
|
@@ -55,6 +63,19 @@ pub struct FunctionMetrics {
|
|
|
55
63
|
pub dep_degree: u64,
|
|
56
64
|
}
|
|
57
65
|
|
|
66
|
+
/// A branching construct or nested function inside a function body, with the cognitive complexity
|
|
67
|
+
/// its whole subtree adds to that function.
|
|
68
|
+
#[derive(Clone, Serialize)]
|
|
69
|
+
#[serde(rename_all = "camelCase")]
|
|
70
|
+
pub struct CognitiveBlock {
|
|
71
|
+
/// The name of the nested function the block is.
|
|
72
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
73
|
+
pub name: Option<String>,
|
|
74
|
+
pub start_line: usize,
|
|
75
|
+
pub end_line: usize,
|
|
76
|
+
pub cognitive_complexity: u64,
|
|
77
|
+
}
|
|
78
|
+
|
|
58
79
|
#[derive(Serialize)]
|
|
59
80
|
#[serde(rename_all = "camelCase")]
|
|
60
81
|
pub struct DuplicateBlockOccurrence {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-gauge",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "10.0.0",
|
|
4
4
|
"description": "Measure code metrics with tree-sitter.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"build": "build-ts lib -i src/index.ts -i src/cli.ts -i src/worker.ts --external ../native/code-gauge.wasm",
|
|
49
49
|
"build-native": "node scripts/buildNative.mjs",
|
|
50
50
|
"build-wasm": "node scripts/buildWasm.mjs",
|
|
51
|
+
"calibrate-thresholds": "bun scripts/calibrateThresholds.ts",
|
|
51
52
|
"cleanup": "bun wb lint --fix --format",
|
|
52
53
|
"format": "bun wb lint --format",
|
|
53
54
|
"lint": "bun wb lint",
|
|
@@ -92,12 +93,12 @@
|
|
|
92
93
|
"registry": "https://registry.npmjs.org/"
|
|
93
94
|
},
|
|
94
95
|
"optionalDependencies": {
|
|
95
|
-
"code-gauge-linux-x64-gnu": "
|
|
96
|
-
"code-gauge-linux-arm64-gnu": "
|
|
97
|
-
"code-gauge-linux-x64-musl": "
|
|
98
|
-
"code-gauge-linux-arm64-musl": "
|
|
99
|
-
"code-gauge-darwin-x64": "
|
|
100
|
-
"code-gauge-darwin-arm64": "
|
|
101
|
-
"code-gauge-win32-x64-msvc": "
|
|
96
|
+
"code-gauge-linux-x64-gnu": "10.0.0",
|
|
97
|
+
"code-gauge-linux-arm64-gnu": "10.0.0",
|
|
98
|
+
"code-gauge-linux-x64-musl": "10.0.0",
|
|
99
|
+
"code-gauge-linux-arm64-musl": "10.0.0",
|
|
100
|
+
"code-gauge-darwin-x64": "10.0.0",
|
|
101
|
+
"code-gauge-darwin-arm64": "10.0.0",
|
|
102
|
+
"code-gauge-win32-x64-msvc": "10.0.0"
|
|
102
103
|
}
|
|
103
104
|
}
|