code-gauge 4.3.1 → 4.5.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 (45) hide show
  1. package/README.md +9 -4
  2. package/dist/crossFileDuplication.cjs +1 -1
  3. package/dist/crossFileDuplication.cjs.map +1 -1
  4. package/dist/crossFileDuplication.d.ts +11 -4
  5. package/dist/crossFileDuplication.js +1 -1
  6. package/dist/crossFileDuplication.js.map +1 -1
  7. package/dist/crossFileNearMiss.cjs +2 -0
  8. package/dist/crossFileNearMiss.cjs.map +1 -0
  9. package/dist/crossFileNearMiss.d.ts +27 -0
  10. package/dist/crossFileNearMiss.js +2 -0
  11. package/dist/crossFileNearMiss.js.map +1 -0
  12. package/dist/diffCommand.cjs +1 -1
  13. package/dist/diffCommand.cjs.map +1 -1
  14. package/dist/diffCommand.js +3 -3
  15. package/dist/diffCommand.js.map +1 -1
  16. package/dist/duplication.cjs +1 -1
  17. package/dist/duplication.cjs.map +1 -1
  18. package/dist/duplication.d.ts +11 -0
  19. package/dist/duplication.js +1 -1
  20. package/dist/duplication.js.map +1 -1
  21. package/dist/metrics.cjs +1 -1
  22. package/dist/metrics.cjs.map +1 -1
  23. package/dist/metrics.d.ts +10 -0
  24. package/dist/metrics.js +1 -1
  25. package/dist/metrics.js.map +1 -1
  26. package/dist/nativeMetrics.cjs +2 -2
  27. package/dist/nativeMetrics.cjs.map +1 -1
  28. package/dist/nativeMetrics.d.ts +7 -2
  29. package/dist/nativeMetrics.js +2 -2
  30. package/dist/nativeMetrics.js.map +1 -1
  31. package/dist/scan.cjs +1 -1
  32. package/dist/scan.cjs.map +1 -1
  33. package/dist/scan.d.ts +12 -1
  34. package/dist/scan.js +1 -1
  35. package/dist/scan.js.map +1 -1
  36. package/dist/types.d.ts +11 -1
  37. package/native/src/complexity.rs +23 -7
  38. package/native/src/dep_degree.rs +2 -3
  39. package/native/src/duplication.rs +170 -115
  40. package/native/src/functions.rs +1 -1
  41. package/native/src/languages.rs +17 -0
  42. package/native/src/lib.rs +6 -2
  43. package/native/src/measure.rs +118 -13
  44. package/native/src/types.rs +6 -0
  45. package/package.json +8 -8
@@ -7,6 +7,10 @@ pub struct LanguageDefinition {
7
7
  grammar_id: GrammarId,
8
8
  pub function_node_types: &'static [&'static str],
9
9
  pub decision_node_types: &'static [&'static str],
10
+ /// Whether every file's top-level code runs as a module body (a component of its own in the
11
+ /// file's cyclomatic complexity); C# and Kotlin decide per file by the presence of top-level
12
+ /// statements (never for a Kotlin file with parse errors).
13
+ pub executes_top_level: bool,
10
14
  pub nesting_node_types: &'static [&'static str],
11
15
  pub ncss_node_types: &'static [&'static str],
12
16
  pub ncss_container_node_types: &'static [&'static str],
@@ -656,6 +660,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
656
660
  grammar_id: GrammarId::JavaScript,
657
661
  function_node_types: COMMON_FUNCTION_NODES,
658
662
  decision_node_types: COMMON_DECISION_NODES,
663
+ executes_top_level: true,
659
664
  nesting_node_types: COMMON_NESTING_NODES,
660
665
  ncss_node_types: JS_NCSS_NODES,
661
666
  ncss_container_node_types: &[],
@@ -666,6 +671,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
666
671
  grammar_id: GrammarId::JavaScript,
667
672
  function_node_types: COMMON_FUNCTION_NODES,
668
673
  decision_node_types: COMMON_DECISION_NODES,
674
+ executes_top_level: true,
669
675
  nesting_node_types: COMMON_NESTING_NODES,
670
676
  ncss_node_types: JS_NCSS_NODES,
671
677
  ncss_container_node_types: &[],
@@ -676,6 +682,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
676
682
  grammar_id: GrammarId::TypeScript,
677
683
  function_node_types: COMMON_FUNCTION_NODES,
678
684
  decision_node_types: COMMON_DECISION_NODES,
685
+ executes_top_level: true,
679
686
  nesting_node_types: COMMON_NESTING_NODES,
680
687
  ncss_node_types: JS_NCSS_NODES,
681
688
  ncss_container_node_types: &[],
@@ -686,6 +693,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
686
693
  grammar_id: GrammarId::Tsx,
687
694
  function_node_types: COMMON_FUNCTION_NODES,
688
695
  decision_node_types: COMMON_DECISION_NODES,
696
+ executes_top_level: true,
689
697
  nesting_node_types: COMMON_NESTING_NODES,
690
698
  ncss_node_types: JS_NCSS_NODES,
691
699
  ncss_container_node_types: &[],
@@ -696,6 +704,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
696
704
  grammar_id: GrammarId::Python,
697
705
  function_node_types: COMMON_FUNCTION_NODES,
698
706
  decision_node_types: COMMON_DECISION_NODES,
707
+ executes_top_level: true,
699
708
  nesting_node_types: COMMON_NESTING_NODES,
700
709
  ncss_node_types: PYTHON_NCSS_NODES,
701
710
  ncss_container_node_types: &[],
@@ -706,6 +715,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
706
715
  grammar_id: GrammarId::Go,
707
716
  function_node_types: COMMON_FUNCTION_NODES,
708
717
  decision_node_types: GO_DECISION_NODES,
718
+ executes_top_level: false,
709
719
  nesting_node_types: GO_NESTING_NODES,
710
720
  ncss_node_types: GO_NCSS_NODES,
711
721
  ncss_container_node_types: &[],
@@ -716,6 +726,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
716
726
  grammar_id: GrammarId::Rust,
717
727
  function_node_types: COMMON_FUNCTION_NODES,
718
728
  decision_node_types: COMMON_DECISION_NODES,
729
+ executes_top_level: false,
719
730
  nesting_node_types: COMMON_NESTING_NODES,
720
731
  ncss_node_types: RUST_NCSS_NODES,
721
732
  ncss_container_node_types: RUST_NCSS_CONTAINERS,
@@ -726,6 +737,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
726
737
  grammar_id: GrammarId::Java,
727
738
  function_node_types: JAVA_FUNCTION_NODES,
728
739
  decision_node_types: JAVA_DECISION_NODES,
740
+ executes_top_level: false,
729
741
  nesting_node_types: JAVA_DECISION_NODES,
730
742
  ncss_node_types: JAVA_NCSS_NODES,
731
743
  ncss_container_node_types: &[],
@@ -736,6 +748,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
736
748
  grammar_id: GrammarId::Ruby,
737
749
  function_node_types: RUBY_FUNCTION_NODES,
738
750
  decision_node_types: RUBY_DECISION_NODES,
751
+ executes_top_level: true,
739
752
  nesting_node_types: RUBY_DECISION_NODES,
740
753
  ncss_node_types: RUBY_NCSS_NODES,
741
754
  ncss_container_node_types: RUBY_NCSS_CONTAINERS,
@@ -746,6 +759,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
746
759
  grammar_id: GrammarId::C,
747
760
  function_node_types: C_FUNCTION_NODES,
748
761
  decision_node_types: C_DECISION_NODES,
762
+ executes_top_level: false,
749
763
  nesting_node_types: C_DECISION_NODES,
750
764
  ncss_node_types: C_NCSS_NODES,
751
765
  ncss_container_node_types: &[],
@@ -756,6 +770,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
756
770
  grammar_id: GrammarId::Cpp,
757
771
  function_node_types: C_FUNCTION_NODES,
758
772
  decision_node_types: CPP_DECISION_NODES,
773
+ executes_top_level: false,
759
774
  nesting_node_types: CPP_DECISION_NODES,
760
775
  ncss_node_types: CPP_NCSS_NODES,
761
776
  ncss_container_node_types: &[],
@@ -766,6 +781,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
766
781
  grammar_id: GrammarId::CSharp,
767
782
  function_node_types: CSHARP_FUNCTION_NODES,
768
783
  decision_node_types: CSHARP_DECISION_NODES,
784
+ executes_top_level: false,
769
785
  nesting_node_types: CSHARP_DECISION_NODES,
770
786
  ncss_node_types: CSHARP_NCSS_NODES,
771
787
  ncss_container_node_types: &[],
@@ -776,6 +792,7 @@ pub const LANGUAGES: &[LanguageDefinition] = &[
776
792
  grammar_id: GrammarId::Kotlin,
777
793
  function_node_types: KOTLIN_FUNCTION_NODES,
778
794
  decision_node_types: KOTLIN_DECISION_NODES,
795
+ executes_top_level: false,
779
796
  nesting_node_types: KOTLIN_DECISION_NODES,
780
797
  ncss_node_types: KOTLIN_NCSS_NODES,
781
798
  ncss_container_node_types: KOTLIN_NCSS_CONTAINERS,
package/native/src/lib.rs CHANGED
@@ -21,10 +21,12 @@ mod util;
21
21
  /// together with `expectedPayloadVersion` in src/nativeMetrics.ts.
22
22
  #[napi]
23
23
  pub fn payload_version() -> u32 {
24
- 5
24
+ 7
25
25
  }
26
26
 
27
- /// Measures code metrics for the given source, returning the NativeMetrics payload as JSON.
27
+ /// Measures code metrics for the given source, returning the NativeMetrics payload as JSON; with
28
+ /// `include_cross_file_data`, the payload also carries the file's cross-file clone-detection
29
+ /// contribution from the same parse.
28
30
  /// The TypeScript wrapper derives the remaining float metrics (Halstead volume/effort/...): they
29
31
  /// involve transcendental functions whose last-bit results can differ between V8 and Rust's libm,
30
32
  /// and results must not depend on which side computes them.
@@ -36,6 +38,7 @@ pub fn measure_code_native(
36
38
  min_tokens: Option<u32>,
37
39
  max_gap_tokens: Option<u32>,
38
40
  min_similarity_percent: Option<u32>,
41
+ include_cross_file_data: Option<bool>,
39
42
  ) -> Result<String> {
40
43
  let definition = find_language(&language)?;
41
44
  let settings = to_duplication_settings(min_tokens, max_gap_tokens, min_similarity_percent);
@@ -43,6 +46,7 @@ pub fn measure_code_native(
43
46
  &code,
44
47
  definition,
45
48
  include_syntax_tree.unwrap_or(false),
49
+ include_cross_file_data.unwrap_or(false),
46
50
  &settings,
47
51
  )
48
52
  .map_err(Error::from_reason)?;
@@ -7,7 +7,8 @@ use crate::complexity::{
7
7
  };
8
8
  use crate::dep_degree::measure_dep_degree;
9
9
  use crate::duplication::{
10
- collect_cross_file_file_data, hash_text, measure_duplication, DuplicationSettings,
10
+ collect_cross_file_file_data, hash_text, measure_duplication, tokenize, DuplicationSettings,
11
+ TokenizedSource,
11
12
  };
12
13
  use crate::functions::{
13
14
  collect_nodes, count_parameters, find_function_name, is_implemented_function,
@@ -25,6 +26,7 @@ pub fn measure(
25
26
  code: &str,
26
27
  language: &LanguageDefinition,
27
28
  include_syntax_tree: bool,
29
+ include_cross_file_data: bool,
28
30
  duplication_settings: &DuplicationSettings,
29
31
  ) -> Result<NativeMetrics, String> {
30
32
  let source = Source::new(code);
@@ -38,11 +40,12 @@ pub fn measure(
38
40
  .filter(|node| !is_lambda_body_block(*node) && is_implemented_function(*node))
39
41
  .collect();
40
42
 
41
- let body_metrics_by_node_id = measure_function_body_metrics(root, &sets, code);
43
+ let body_metrics = measure_function_body_metrics(root, &sets, code);
42
44
  let function_metrics: Vec<FunctionMetrics> = functions
43
45
  .iter()
44
46
  .map(|node| {
45
- let body_metrics = body_metrics_by_node_id
47
+ let body_metrics = body_metrics
48
+ .by_function
46
49
  .get(&node.id())
47
50
  .expect("every collected function node opens a frame in the body-metrics pass");
48
51
  FunctionMetrics {
@@ -71,11 +74,22 @@ pub fn measure(
71
74
  let global_complexity = measure_complexity(root, &sets, code);
72
75
  let (lines, code_line_numbers) = classify_lines(code, root);
73
76
  let halstead_counts = measure_halstead(root, code);
77
+ let tokenized = tokenize(root, code);
74
78
 
75
79
  Ok(NativeMetrics {
76
80
  language: language.name.to_string(),
77
81
  bytes: code.code.len(),
78
82
  lines,
83
+ // McCabe's v = e - n + 2p over the file's components: every function, every initializer
84
+ // block, and the module body when the file runs top-level code; decisions outside functions
85
+ // belong to the file.
86
+ cyclomatic_complexity: function_metrics
87
+ .iter()
88
+ .map(|function| function.cyclomatic_complexity)
89
+ .sum::<u64>()
90
+ + body_metrics.top_level_decisions
91
+ + u64::from(language.executes_top_level || has_top_level_statements(root, language))
92
+ + count_initializer_blocks(root),
79
93
  cognitive_complexity: global_complexity.cognitive_complexity,
80
94
  max_cognitive_complexity: function_metrics
81
95
  .iter()
@@ -84,7 +98,14 @@ pub fn measure(
84
98
  .unwrap_or(0),
85
99
  nesting_depth: global_complexity.nesting_depth,
86
100
  ncss_count: crate::ncss::count_ncss(root, &sets.ncss_nodes, &sets.ncss_containers),
87
- duplication: measure_duplication(root, &code_line_numbers, code, duplication_settings),
101
+ duplication: measure_duplication(&tokenized, &code_line_numbers, duplication_settings),
102
+ cross_file_data: include_cross_file_data.then(|| {
103
+ to_cross_file_data(
104
+ &tokenized,
105
+ &code_line_numbers,
106
+ duplication_settings.min_tokens,
107
+ )
108
+ }),
88
109
  halstead_counts,
89
110
  functions: function_metrics,
90
111
  syntax_tree: if include_syntax_tree {
@@ -95,6 +116,76 @@ pub fn measure(
95
116
  })
96
117
  }
97
118
 
119
+ /// Initializer blocks run code of their own, so each is a component like a function: Java static
120
+ /// and instance initializers, Kotlin `init` blocks, and JavaScript/TypeScript class `static`
121
+ /// blocks. Their decisions already count as decisions outside functions.
122
+ fn count_initializer_blocks(root: Node<'_>) -> u64 {
123
+ let initializer_types: HashSet<&'static str> = [
124
+ "static_initializer",
125
+ "anonymous_initializer",
126
+ "class_static_block",
127
+ "block",
128
+ ]
129
+ .into_iter()
130
+ .collect();
131
+ collect_nodes(root, &initializer_types)
132
+ .into_iter()
133
+ .filter(|node| {
134
+ // A bare block is an initializer only as a direct member of a Java class or enum body.
135
+ node.kind() != "block"
136
+ || node.parent().is_some_and(|parent| {
137
+ matches!(parent.kind(), "class_body" | "enum_body_declarations")
138
+ })
139
+ })
140
+ .count() as u64
141
+ }
142
+
143
+ /// A C# top-level statement: a `global_statement`, or a statement inside a top-level `#if` block,
144
+ /// which the grammar does not wrap in `global_statement`; preprocessor blocks are transparent.
145
+ fn is_csharp_top_level_statement(node: Node<'_>) -> bool {
146
+ if node.kind().starts_with("preproc_") {
147
+ return named_children(node)
148
+ .into_iter()
149
+ .any(is_csharp_top_level_statement);
150
+ }
151
+ node.kind() == "global_statement"
152
+ || node.kind() == "block"
153
+ || node.kind().ends_with("_statement")
154
+ }
155
+
156
+ /// Whether a C# or Kotlin file runs top-level code: C# top-level statements, or a Kotlin script's
157
+ /// statements beside its declarations.
158
+ fn has_top_level_statements(root: Node<'_>, language: &LanguageDefinition) -> bool {
159
+ const KOTLIN_DECLARATIONS: &[&str] = &[
160
+ "package_header",
161
+ "import_list",
162
+ "class_declaration",
163
+ "object_declaration",
164
+ "function_declaration",
165
+ "property_declaration",
166
+ "type_alias",
167
+ "shebang_line",
168
+ "file_annotation",
169
+ "getter",
170
+ "setter",
171
+ ];
172
+ let children = named_children(root);
173
+ match language.name {
174
+ "csharp" => children.into_iter().any(is_csharp_top_level_statement),
175
+ // The grammar mis-parses some valid declarations (non-empty companion objects, `fun
176
+ // interface`) and leaves recovery residue at the top level, so a file with parse errors is
177
+ // never taken for a script.
178
+ "kotlin" => {
179
+ !root.has_error()
180
+ && children.iter().any(|child| {
181
+ !KOTLIN_DECLARATIONS.contains(&child.kind())
182
+ && !crate::ncss::COMMENT_NODE_TYPES.contains(&child.kind())
183
+ })
184
+ }
185
+ _ => false,
186
+ }
187
+ }
188
+
98
189
  /// Collects one file's cross-file clone-detection contribution; see CrossFileFileData.
99
190
  pub fn collect_cross_file_data(
100
191
  code: &str,
@@ -104,17 +195,30 @@ pub fn collect_cross_file_data(
104
195
  let source = Source::new(code);
105
196
  let tree = parse_source(&source, language)?;
106
197
  let root = tree.root_node();
107
- let (candidates, tokens, container_statements) =
108
- collect_cross_file_file_data(root, &source, min_tokens);
109
198
  let (_, code_line_numbers) = classify_lines(&source, root);
110
- let mut code_line_numbers: Vec<usize> = code_line_numbers.into_iter().collect();
199
+ Ok(to_cross_file_data(
200
+ &tokenize(root, &source),
201
+ &code_line_numbers,
202
+ min_tokens,
203
+ ))
204
+ }
205
+
206
+ fn to_cross_file_data(
207
+ tokenized: &TokenizedSource<'_>,
208
+ code_line_numbers: &HashSet<usize>,
209
+ min_tokens: usize,
210
+ ) -> CrossFileFileData {
211
+ let (candidates, tokens, container_statements, near_miss_blocks) =
212
+ collect_cross_file_file_data(tokenized, min_tokens);
213
+ let mut code_line_numbers: Vec<usize> = code_line_numbers.iter().copied().collect();
111
214
  code_line_numbers.sort_unstable();
112
- Ok(CrossFileFileData {
215
+ CrossFileFileData {
113
216
  candidates,
114
217
  tokens,
115
218
  container_statements,
219
+ near_miss_blocks,
116
220
  code_line_numbers,
117
- })
221
+ }
118
222
  }
119
223
 
120
224
  /// Name-carrying leaf types anonymized by tokenize_function so consistent renames still match.
@@ -133,7 +237,7 @@ const IDENTIFIER_LEAF_NODE_TYPES: &[&str] = &[
133
237
  ];
134
238
 
135
239
  /// Normalized token hash sequences of every function, index-parallel to the functions array of
136
- /// measure(); a faithful port of tokenizeFunction in src/metrics.ts.
240
+ /// measure().
137
241
  pub fn collect_function_token_sequences(
138
242
  code: &str,
139
243
  language: &LanguageDefinition,
@@ -245,8 +349,9 @@ struct CommentSpan {
245
349
  end_column: usize,
246
350
  }
247
351
 
248
- /// 1-based numbers of lines that are neither blank nor comment-only, matching classifyLines in
249
- /// metrics.ts so duplication line coverage and its code-line denominator agree.
352
+ /// Line metrics plus the 1-based numbers of lines that are neither blank nor comment-only, shared
353
+ /// by the line counts and duplication line coverage so the coverage and its code-line denominator
354
+ /// agree.
250
355
  fn classify_lines(code: &Source<'_>, root: Node<'_>) -> (LineMetrics, HashSet<usize>) {
251
356
  let source_lines = split_lines(code.code);
252
357
  // Spans are bucketed by line so classification stays linear.
@@ -525,7 +630,7 @@ const OPERAND_NODE_TYPES: &[&str] = &[
525
630
  "none",
526
631
  ];
527
632
 
528
- /// Non-leaf literals counted as one Halstead operand without descending; see metrics.ts.
633
+ /// Non-leaf literals counted as one Halstead operand without descending.
529
634
  /// `character_literal` is a leaf in Java and Kotlin but wraps a content node in C#; Kotlin's
530
635
  /// suffixed numbers (`1L`, `1u`) wrap the bare literal, so `1` and `1L` stay distinct.
531
636
  const ATOMIC_OPERAND_NODE_TYPES: &[&str] = &[
@@ -11,11 +11,15 @@ pub struct NativeMetrics {
11
11
  pub bytes: usize,
12
12
  pub lines: LineMetrics,
13
13
  pub functions: Vec<FunctionMetrics>,
14
+ pub cyclomatic_complexity: u64,
14
15
  pub cognitive_complexity: u64,
15
16
  pub max_cognitive_complexity: u64,
16
17
  pub nesting_depth: u64,
17
18
  pub ncss_count: u64,
18
19
  pub duplication: DuplicationMetrics,
20
+ /// Collected from the same parse on request, so a directory scan tokenizes each file once.
21
+ #[serde(skip_serializing_if = "Option::is_none")]
22
+ pub cross_file_data: Option<CrossFileFileData>,
19
23
  pub halstead_counts: HalsteadCounts,
20
24
  #[serde(skip_serializing_if = "Option::is_none")]
21
25
  pub syntax_tree: Option<String>,
@@ -79,6 +83,8 @@ pub struct CrossFileFileData {
79
83
  pub candidates: Vec<CrossFileCandidate>,
80
84
  pub tokens: Vec<CrossFileToken>,
81
85
  pub container_statements: Vec<Vec<CrossFileTokenRange>>,
86
+ /// The mutually disjoint blocks cross-file near-miss (Type-3) comparison considers.
87
+ pub near_miss_blocks: Vec<CrossFileTokenRange>,
82
88
  /// 1-based lines that are neither blank nor comment-only, sorted ascending.
83
89
  pub code_line_numbers: Vec<usize>,
84
90
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-gauge",
3
- "version": "4.3.1",
3
+ "version": "4.5.0",
4
4
  "description": "Measure code metrics with tree-sitter.",
5
5
  "keywords": [
6
6
  "cli",
@@ -89,12 +89,12 @@
89
89
  "registry": "https://registry.npmjs.org/"
90
90
  },
91
91
  "optionalDependencies": {
92
- "code-gauge-linux-x64-gnu": "4.3.1",
93
- "code-gauge-linux-arm64-gnu": "4.3.1",
94
- "code-gauge-linux-x64-musl": "4.3.1",
95
- "code-gauge-linux-arm64-musl": "4.3.1",
96
- "code-gauge-darwin-x64": "4.3.1",
97
- "code-gauge-darwin-arm64": "4.3.1",
98
- "code-gauge-win32-x64-msvc": "4.3.1"
92
+ "code-gauge-linux-x64-gnu": "4.5.0",
93
+ "code-gauge-linux-arm64-gnu": "4.5.0",
94
+ "code-gauge-linux-x64-musl": "4.5.0",
95
+ "code-gauge-linux-arm64-musl": "4.5.0",
96
+ "code-gauge-darwin-x64": "4.5.0",
97
+ "code-gauge-darwin-arm64": "4.5.0",
98
+ "code-gauge-win32-x64-msvc": "4.5.0"
99
99
  }
100
100
  }