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.
Files changed (43) hide show
  1. package/README.md +73 -46
  2. package/dist/checkCommand.cjs +3 -3
  3. package/dist/checkCommand.cjs.map +1 -1
  4. package/dist/checkCommand.d.ts +22 -1
  5. package/dist/checkCommand.js +3 -3
  6. package/dist/checkCommand.js.map +1 -1
  7. package/dist/cli.cjs +3 -3
  8. package/dist/cli.cjs.map +1 -1
  9. package/dist/cli.js +3 -3
  10. package/dist/cli.js.map +1 -1
  11. package/dist/index.d.ts +1 -1
  12. package/dist/metrics.cjs +1 -1
  13. package/dist/metrics.cjs.map +1 -1
  14. package/dist/metrics.js +1 -1
  15. package/dist/metrics.js.map +1 -1
  16. package/dist/nativeMetrics.cjs +1 -1
  17. package/dist/nativeMetrics.cjs.map +1 -1
  18. package/dist/nativeMetrics.d.ts +4 -2
  19. package/dist/nativeMetrics.js +1 -1
  20. package/dist/nativeMetrics.js.map +1 -1
  21. package/dist/thresholdCheck.cjs +1 -1
  22. package/dist/thresholdCheck.cjs.map +1 -1
  23. package/dist/thresholdCheck.d.ts +18 -4
  24. package/dist/thresholdCheck.js +1 -1
  25. package/dist/thresholdCheck.js.map +1 -1
  26. package/dist/thresholds.cjs +1 -1
  27. package/dist/thresholds.cjs.map +1 -1
  28. package/dist/thresholds.d.ts +5 -3
  29. package/dist/thresholds.js +1 -1
  30. package/dist/thresholds.js.map +1 -1
  31. package/dist/types.d.ts +37 -0
  32. package/dist/wasmBinding.cjs +1 -1
  33. package/dist/wasmBinding.cjs.map +1 -1
  34. package/dist/wasmBinding.js +1 -1
  35. package/dist/wasmBinding.js.map +1 -1
  36. package/native/code-gauge.wasm +0 -0
  37. package/native/src/complexity.rs +173 -19
  38. package/native/src/container.rs +413 -0
  39. package/native/src/functions.rs +2 -5
  40. package/native/src/lib.rs +2 -1
  41. package/native/src/measure.rs +24 -8
  42. package/native/src/types.rs +21 -0
  43. 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
+ }
@@ -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
- 8
32
+ 10
32
33
  }
33
34
 
34
35
  /// Measures code metrics for the given source, returning the NativeMetrics payload as JSON; with
@@ -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| INITIALIZER_NODE_TYPES.contains(&node.kind_name()))
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
 
@@ -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": "8.0.1",
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": "8.0.1",
96
- "code-gauge-linux-arm64-gnu": "8.0.1",
97
- "code-gauge-linux-x64-musl": "8.0.1",
98
- "code-gauge-linux-arm64-musl": "8.0.1",
99
- "code-gauge-darwin-x64": "8.0.1",
100
- "code-gauge-darwin-arm64": "8.0.1",
101
- "code-gauge-win32-x64-msvc": "8.0.1"
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
  }