code-gauge 3.1.0 → 4.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 (63) hide show
  1. package/README.md +16 -15
  2. package/dist/crossFileDuplication.cjs +1 -1
  3. package/dist/crossFileDuplication.cjs.map +1 -1
  4. package/dist/crossFileDuplication.js +1 -1
  5. package/dist/crossFileDuplication.js.map +1 -1
  6. package/dist/diffCommand.cjs +1 -1
  7. package/dist/diffCommand.cjs.map +1 -1
  8. package/dist/diffCommand.js +1 -1
  9. package/dist/diffCommand.js.map +1 -1
  10. package/dist/duplication.cjs +1 -1
  11. package/dist/duplication.cjs.map +1 -1
  12. package/dist/duplication.d.ts +14 -28
  13. package/dist/duplication.js +1 -1
  14. package/dist/duplication.js.map +1 -1
  15. package/dist/index.cjs +1 -1
  16. package/dist/index.d.ts +0 -1
  17. package/dist/index.js +1 -1
  18. package/dist/languages.cjs +1 -1
  19. package/dist/languages.cjs.map +1 -1
  20. package/dist/languages.d.ts +5 -0
  21. package/dist/languages.js +1 -1
  22. package/dist/languages.js.map +1 -1
  23. package/dist/metrics.cjs +1 -1
  24. package/dist/metrics.cjs.map +1 -1
  25. package/dist/metrics.d.ts +14 -12
  26. package/dist/metrics.js +1 -1
  27. package/dist/metrics.js.map +1 -1
  28. package/dist/nativeMetrics.cjs +3 -1
  29. package/dist/nativeMetrics.cjs.map +1 -1
  30. package/dist/nativeMetrics.d.ts +24 -9
  31. package/dist/nativeMetrics.js +3 -1
  32. package/dist/nativeMetrics.js.map +1 -1
  33. package/dist/scan.cjs +1 -1
  34. package/dist/scan.cjs.map +1 -1
  35. package/dist/scan.js +1 -1
  36. package/dist/scan.js.map +1 -1
  37. package/dist/types.d.ts +5 -13
  38. package/native/Cargo.lock +523 -0
  39. package/native/Cargo.toml +45 -0
  40. package/native/build.rs +3 -0
  41. package/native/src/complexity.rs +627 -0
  42. package/native/src/dep_degree.rs +253 -0
  43. package/native/src/duplication.rs +2007 -0
  44. package/native/src/functions.rs +345 -0
  45. package/native/src/languages.rs +647 -0
  46. package/native/src/lib.rs +101 -0
  47. package/native/src/measure.rs +590 -0
  48. package/native/src/ncss.rs +263 -0
  49. package/native/src/types.rs +135 -0
  50. package/native/src/util.rs +139 -0
  51. package/package.json +16 -19
  52. package/scripts/buildNative.mjs +25 -0
  53. package/scripts/installNative.mjs +96 -0
  54. package/dist/depDegree.cjs +0 -2
  55. package/dist/depDegree.cjs.map +0 -1
  56. package/dist/depDegree.d.ts +0 -12
  57. package/dist/depDegree.js +0 -2
  58. package/dist/depDegree.js.map +0 -1
  59. package/dist/ncss.cjs +0 -2
  60. package/dist/ncss.cjs.map +0 -1
  61. package/dist/ncss.d.ts +0 -17
  62. package/dist/ncss.js +0 -2
  63. package/dist/ncss.js.map +0 -1
@@ -0,0 +1,263 @@
1
+ use std::collections::HashSet;
2
+ use tree_sitter::Node;
3
+
4
+ use crate::util::{all_children, find_children_by_field_name};
5
+
6
+ pub const COMMENT_NODE_TYPES: &[&str] = &["comment", "line_comment", "block_comment"];
7
+
8
+ /// Nodes never counted positionally inside NCSS containers: metadata, empty statements, Ruby
9
+ /// heredoc bodies (tree-sitter emits them as siblings of the statement that opened the heredoc),
10
+ /// and Ruby statement parentheses (transparent wrappers whose children count instead).
11
+ const POSITIONAL_EXCLUSION_TYPES: &[&str] = &[
12
+ "attribute_item",
13
+ "inner_attribute_item",
14
+ "empty_statement",
15
+ "heredoc_body",
16
+ "parenthesized_statements",
17
+ ];
18
+
19
+ /// TypeScript interface members count like Java interface members, but the same node types appear
20
+ /// inside object-type annotations (`let x: { a: number }`), which are part of one declaration, so
21
+ /// they only count directly under an interface body.
22
+ const INTERFACE_MEMBER_NODE_TYPES: &[&str] = &[
23
+ "property_signature",
24
+ "method_signature",
25
+ "index_signature",
26
+ "construct_signature",
27
+ "call_signature",
28
+ ];
29
+
30
+ /// An if-branch wrapped in one of these already counts through ncss_node_types; a bare
31
+ /// `alternative` (Java/Go put the else branch directly in the field) needs the extra `else` count.
32
+ const ELSE_CLAUSE_NODE_TYPES: &[&str] = &["else_clause", "elif_clause", "else", "elsif"];
33
+
34
+ const IF_NODE_TYPES: &[&str] = &["if_statement", "if_expression"];
35
+
36
+ /// C/C++ type specifiers only declare something when they carry a body (`struct S { ... }`);
37
+ /// without one they are mere type references inside other declarations.
38
+ const BODYLESS_NCSS_SPECIFIER_TYPES: &[&str] = &[
39
+ "struct_specifier",
40
+ "enum_specifier",
41
+ "union_specifier",
42
+ "class_specifier",
43
+ ];
44
+
45
+ /// Counts non-commenting source statements (NCSS) in the subtree, PMD-style: one per declaration,
46
+ /// statement, and clause (`else`, `case`/`default` label, `catch`, `finally`, try-with-resources
47
+ /// resource); `try` itself, braces, blank lines, and comments count 0.
48
+ pub fn count_ncss(
49
+ node: Node<'_>,
50
+ countable: &HashSet<&'static str>,
51
+ containers: &HashSet<&'static str>,
52
+ ) -> u64 {
53
+ fn visit(
54
+ current: Node<'_>,
55
+ countable: &HashSet<&'static str>,
56
+ containers: &HashSet<&'static str>,
57
+ count: &mut u64,
58
+ ) {
59
+ *count += ncss_contribution(current, countable, containers);
60
+ for child in all_children(current) {
61
+ visit(child, countable, containers, count);
62
+ }
63
+ }
64
+
65
+ let mut count = 0;
66
+ visit(node, countable, containers, &mut count);
67
+ count
68
+ }
69
+
70
+ pub fn ncss_contribution(
71
+ node: Node<'_>,
72
+ countable: &HashSet<&'static str>,
73
+ containers: &HashSet<&'static str>,
74
+ ) -> u64 {
75
+ if !node.is_named() || COMMENT_NODE_TYPES.contains(&node.kind()) || is_for_header_node(node) {
76
+ return 0;
77
+ }
78
+
79
+ let mut contribution = 0;
80
+ let positional = is_in_container_position(node, containers)
81
+ && !containers.contains(node.kind())
82
+ && !POSITIONAL_EXCLUSION_TYPES.contains(&node.kind());
83
+ if (counts_through_node_type(node, countable) || positional || counts_contextually(node))
84
+ && !is_declaration_wrapper(node, countable)
85
+ {
86
+ contribution += 1;
87
+ }
88
+
89
+ // A bare else branch (Java/Go `alternative:` without an else-clause wrapper) counts 1 like the
90
+ // `else` keyword does in PMD; an `else if` chain charges the nested if separately on top.
91
+ if IF_NODE_TYPES.contains(&node.kind()) {
92
+ contribution += count_bare_alternatives(node);
93
+ }
94
+
95
+ contribution
96
+ }
97
+
98
+ fn counts_through_node_type(node: Node<'_>, countable: &HashSet<&'static str>) -> bool {
99
+ if !countable.contains(node.kind()) {
100
+ return false;
101
+ }
102
+ if BODYLESS_NCSS_SPECIFIER_TYPES.contains(&node.kind()) {
103
+ return node.child_by_field_name("body").is_some();
104
+ }
105
+ // A try-with-resources `resource` counts only when it declares a variable; `try (r)` reuses an
106
+ // existing one and adds no statement (matching PMD).
107
+ if node.kind() == "resource" {
108
+ return node.child_by_field_name("name").is_some();
109
+ }
110
+ true
111
+ }
112
+
113
+ /// Direct container children count positionally; Ruby's `(foo; bar)` statement parentheses are
114
+ /// transparent, so their children count when the parentheses themselves sit in a container.
115
+ fn is_in_container_position(node: Node<'_>, containers: &HashSet<&'static str>) -> bool {
116
+ let mut ancestor = node.parent();
117
+ while let Some(current) = ancestor {
118
+ if current.kind() != "parenthesized_statements" {
119
+ return containers.contains(current.kind());
120
+ }
121
+ ancestor = current.parent();
122
+ }
123
+ false
124
+ }
125
+
126
+ /// `export const x = 1` nests a countable declaration inside `export_statement`; only the inner
127
+ /// declaration counts, mirroring how PMD counts one statement per declared entity.
128
+ fn is_declaration_wrapper(node: Node<'_>, countable: &HashSet<&'static str>) -> bool {
129
+ if node.kind() != "export_statement" {
130
+ return false;
131
+ }
132
+ node.child_by_field_name("declaration")
133
+ .is_some_and(|declaration| {
134
+ countable.contains(declaration.kind())
135
+ || declaration.kind() == "internal_module"
136
+ || declaration.kind() == "ambient_declaration"
137
+ })
138
+ }
139
+
140
+ const FOR_HEADER_FIELD_NAMES: &[&str] =
141
+ &["init", "initializer", "condition", "update", "increment"];
142
+
143
+ /// Statement-shaped nodes in a `for` header (`for (int i = 0; i < n; i++)`) are part of the loop
144
+ /// statement, which already counts; PMD does not count them separately. JavaScript parses the
145
+ /// condition as an `expression_statement` and Go parses the update as an `inc_statement`, so all
146
+ /// header fields must be excluded, not just the initializer.
147
+ fn is_for_header_node(node: Node<'_>) -> bool {
148
+ let Some(parent) = node.parent() else {
149
+ return false;
150
+ };
151
+ // C++20 range-for initializers nest one level deeper: for_range_loop > init_statement > node.
152
+ if parent.kind() == "init_statement"
153
+ && parent
154
+ .parent()
155
+ .is_some_and(|grandparent| grandparent.kind() == "for_range_loop")
156
+ {
157
+ return true;
158
+ }
159
+ if parent.kind() != "for_statement"
160
+ && parent.kind() != "for_clause"
161
+ && parent.kind() != "for_range_loop"
162
+ {
163
+ return false;
164
+ }
165
+ FOR_HEADER_FIELD_NAMES.iter().any(|field_name| {
166
+ find_children_by_field_name(parent, field_name)
167
+ .iter()
168
+ .any(|child| child.id() == node.id())
169
+ })
170
+ }
171
+
172
+ /// Statements only countable by their position: constructs without a dedicated statement node.
173
+ fn counts_contextually(node: Node<'_>) -> bool {
174
+ let parent_kind = node.parent().map(|parent| parent.kind());
175
+ // A Java instance initializer is a bare `block` in the class body; PMD counts it like the
176
+ // `static_initializer` declaration it parallels.
177
+ if node.kind() == "block" && parent_kind == Some("class_body") {
178
+ return true;
179
+ }
180
+ // TypeScript interface members (see INTERFACE_MEMBER_NODE_TYPES).
181
+ if INTERFACE_MEMBER_NODE_TYPES.contains(&node.kind()) && parent_kind == Some("interface_body") {
182
+ return true;
183
+ }
184
+ // A braceless Rust match-arm body (`1 => foo()`) has no expression_statement wrapper; count the
185
+ // value expression so braced and unbraced arms measure alike.
186
+ if parent_kind == Some("match_arm")
187
+ && node.kind() != "block"
188
+ && is_field_of_parent(node, "value")
189
+ {
190
+ return true;
191
+ }
192
+ // A TypeScript class-body method overload signature declares a member like its interface twin.
193
+ if node.kind() == "method_signature" && parent_kind == Some("class_body") {
194
+ return true;
195
+ }
196
+ // An ambient `declare namespace M { ... }` is a bare `internal_module`; the non-ambient
197
+ // `namespace N { ... }` is wrapped in an `expression_statement`, which already counts.
198
+ if node.kind() == "internal_module" && parent_kind != Some("expression_statement") {
199
+ return true;
200
+ }
201
+ // A Ruby endless method (`def f(x) = expr`) stores its single-statement body directly in the
202
+ // `body` field instead of a positional `body_statement` container.
203
+ if (parent_kind == Some("method") || parent_kind == Some("singleton_method"))
204
+ && node.kind() != "body_statement"
205
+ && is_field_of_parent(node, "body")
206
+ {
207
+ return true;
208
+ }
209
+ // C++ `friend class X;` declares on its own; `friend void g() { ... }` merely wraps a counted
210
+ // definition.
211
+ if node.kind() == "friend_declaration" {
212
+ return !crate::util::named_children(node)
213
+ .iter()
214
+ .any(|child| child.kind() == "declaration" || child.kind() == "function_definition");
215
+ }
216
+ // A Rust item-position macro invocation (`foo! {}` at module level) has no expression_statement
217
+ // wrapper; the semicolon form does and already counts through it.
218
+ if node.kind() == "macro_invocation"
219
+ && (parent_kind == Some("source_file") || parent_kind == Some("declaration_list"))
220
+ {
221
+ return true;
222
+ }
223
+ // Go struct fields and interface members count like other languages' member declarations, but
224
+ // only inside a named type declaration; inline anonymous types (`var x struct{ ... }`,
225
+ // `func f(h interface{ ... })`) are part of one declaration.
226
+ if node.kind() == "field_declaration" && parent_kind == Some("field_declaration_list") {
227
+ return is_go_declared_type_body(
228
+ node.parent().and_then(|parent| parent.parent()),
229
+ "struct_type",
230
+ );
231
+ }
232
+ if node.kind() == "method_elem" || node.kind() == "method_spec" || node.kind() == "type_elem" {
233
+ return is_go_declared_type_body(node.parent(), "interface_type");
234
+ }
235
+ false
236
+ }
237
+
238
+ fn is_go_declared_type_body(type_node: Option<Node<'_>>, expected_kind: &str) -> bool {
239
+ let Some(type_node) = type_node.filter(|type_node| type_node.kind() == expected_kind) else {
240
+ return false;
241
+ };
242
+ type_node
243
+ .parent()
244
+ .is_some_and(|parent| parent.kind() == "type_spec" || parent.kind() == "type_alias")
245
+ }
246
+
247
+ fn is_field_of_parent(node: Node<'_>, field_name: &str) -> bool {
248
+ let Some(parent) = node.parent() else {
249
+ return false;
250
+ };
251
+ find_children_by_field_name(parent, field_name)
252
+ .iter()
253
+ .any(|child| child.id() == node.id())
254
+ }
255
+
256
+ fn count_bare_alternatives(node: Node<'_>) -> u64 {
257
+ // Extras (comments) inherit the preceding sibling's field in find_children_by_field_name, so a
258
+ // comment between an `elif_clause` and `else_clause` must not be miscounted as a bare branch.
259
+ find_children_by_field_name(node, "alternative")
260
+ .iter()
261
+ .filter(|child| !child.is_extra() && !ELSE_CLAUSE_NODE_TYPES.contains(&child.kind()))
262
+ .count() as u64
263
+ }
@@ -0,0 +1,135 @@
1
+ use serde::Serialize;
2
+
3
+ /// Result payload of the native measurer. Mirrors CodeMetrics from src/types.ts, except that
4
+ /// Halstead's derived values are computed on the TypeScript side: they involve transcendental
5
+ /// functions (log/log2) whose last-bit results can differ between V8 and Rust's libm, and results
6
+ /// must not vary with the platform's libm build.
7
+ #[derive(Serialize)]
8
+ #[serde(rename_all = "camelCase")]
9
+ pub struct NativeMetrics {
10
+ pub language: String,
11
+ pub bytes: usize,
12
+ pub lines: LineMetrics,
13
+ pub functions: Vec<FunctionMetrics>,
14
+ pub cognitive_complexity: u64,
15
+ pub max_cognitive_complexity: u64,
16
+ pub nesting_depth: u64,
17
+ pub ncss_count: u64,
18
+ pub duplication: DuplicationMetrics,
19
+ pub halstead_counts: HalsteadCounts,
20
+ #[serde(skip_serializing_if = "Option::is_none")]
21
+ pub syntax_tree: Option<String>,
22
+ }
23
+
24
+ #[derive(Serialize)]
25
+ #[serde(rename_all = "camelCase")]
26
+ pub struct LineMetrics {
27
+ pub total: usize,
28
+ pub code: usize,
29
+ pub comment: usize,
30
+ pub blank: usize,
31
+ }
32
+
33
+ #[derive(Serialize)]
34
+ #[serde(rename_all = "camelCase")]
35
+ pub struct FunctionMetrics {
36
+ #[serde(skip_serializing_if = "Option::is_none")]
37
+ pub name: Option<String>,
38
+ /// The tree-sitter node type of the function node (e.g. `method_declaration`, `arrow_function`).
39
+ pub node_type: String,
40
+ pub start_line: usize,
41
+ pub start_column: usize,
42
+ pub end_line: usize,
43
+ pub cognitive_complexity: u64,
44
+ pub nesting_depth: u64,
45
+ pub ncss: u64,
46
+ pub parameter_count: usize,
47
+ /// Base counts of the function's whole subtree; derived floats are computed in TypeScript.
48
+ pub halstead_counts: HalsteadCounts,
49
+ pub dep_degree: u64,
50
+ }
51
+
52
+ #[derive(Serialize)]
53
+ #[serde(rename_all = "camelCase")]
54
+ pub struct DuplicateBlockOccurrence {
55
+ pub end_line: usize,
56
+ pub start_line: usize,
57
+ }
58
+
59
+ #[derive(Serialize)]
60
+ #[serde(rename_all = "camelCase")]
61
+ pub struct DuplicationMetrics {
62
+ pub duplicate_block_count: usize,
63
+ pub duplicate_block_group_count: usize,
64
+ pub duplicate_block_groups: Vec<Vec<DuplicateBlockOccurrence>>,
65
+ pub duplicate_line_count: usize,
66
+ /// The 1-based lines behind duplicate_line_count, sorted ascending.
67
+ pub duplicate_line_numbers: Vec<usize>,
68
+ pub duplication_ratio: f64,
69
+ pub max_duplicate_block_size: usize,
70
+ }
71
+
72
+ /// One file's contribution to cross-file clone detection. Mirrors CrossFileDuplicationFileData in
73
+ /// src/duplication.ts; `code_line_numbers` is revived into a Set on the TypeScript side.
74
+ #[derive(Serialize)]
75
+ #[serde(rename_all = "camelCase")]
76
+ pub struct CrossFileFileData {
77
+ pub candidates: Vec<CrossFileCandidate>,
78
+ pub tokens: Vec<CrossFileToken>,
79
+ pub container_statements: Vec<Vec<CrossFileTokenRange>>,
80
+ /// 1-based lines that are neither blank nor comment-only, sorted ascending.
81
+ pub code_line_numbers: Vec<usize>,
82
+ }
83
+
84
+ #[derive(Serialize)]
85
+ #[serde(rename_all = "camelCase")]
86
+ pub struct CrossFileCandidate {
87
+ pub fingerprint: String,
88
+ pub token_count: usize,
89
+ pub start_token_index: usize,
90
+ pub end_token_index: usize,
91
+ pub start_index: usize,
92
+ pub end_index: usize,
93
+ pub start_line: usize,
94
+ pub end_line: usize,
95
+ }
96
+
97
+ /// A normalized token as consumed by the TypeScript project-level matcher (the Token interface in
98
+ /// src/duplication.ts). Optional fields are omitted rather than null: the TypeScript side
99
+ /// distinguishes absent from undefined-valued keys with `!== undefined` checks.
100
+ #[derive(Serialize)]
101
+ #[serde(rename_all = "camelCase")]
102
+ pub struct CrossFileToken {
103
+ pub kind: &'static str,
104
+ pub text: String,
105
+ pub text_hash: i32,
106
+ pub text_hash2: i32,
107
+ #[serde(skip_serializing_if = "Option::is_none")]
108
+ pub literal_hash: Option<i32>,
109
+ #[serde(skip_serializing_if = "Option::is_none")]
110
+ pub literal_hash2: Option<i32>,
111
+ #[serde(skip_serializing_if = "std::ops::Not::not")]
112
+ pub is_name: bool,
113
+ pub start_row: usize,
114
+ pub end_row: usize,
115
+ }
116
+
117
+ #[derive(Serialize)]
118
+ #[serde(rename_all = "camelCase")]
119
+ pub struct CrossFileTokenRange {
120
+ pub start_token_index: usize,
121
+ pub end_token_index: usize,
122
+ pub start_index: usize,
123
+ pub end_index: usize,
124
+ pub start_line: usize,
125
+ pub end_line: usize,
126
+ }
127
+
128
+ #[derive(Serialize)]
129
+ #[serde(rename_all = "camelCase")]
130
+ pub struct HalsteadCounts {
131
+ pub distinct_operators: usize,
132
+ pub distinct_operands: usize,
133
+ pub total_operators: u64,
134
+ pub total_operands: u64,
135
+ }
@@ -0,0 +1,139 @@
1
+ use tree_sitter::Node;
2
+
3
+ /// The measured source. Trees are parsed from UTF-16 (matching node-tree-sitter, which parses
4
+ /// JavaScript strings as UTF-16 — tree-sitter's error recovery can differ between encodings), so
5
+ /// node "byte" offsets and columns are UTF-16 code units x 2; this maps them back to UTF-8 slices
6
+ /// of the original string without allocating per node.
7
+ pub struct Source<'a> {
8
+ pub code: &'a str,
9
+ /// UTF-16 unit -> UTF-8 byte offset. None for pure-ASCII sources, where the two coincide, so
10
+ /// the table's 4-bytes-per-unit cost is only paid for sources that actually need mapping.
11
+ utf8_offset_by_unit: Option<Vec<u32>>,
12
+ }
13
+
14
+ impl<'a> Source<'a> {
15
+ pub fn new(code: &'a str) -> Source<'a> {
16
+ if code.is_ascii() {
17
+ return Source {
18
+ code,
19
+ utf8_offset_by_unit: None,
20
+ };
21
+ }
22
+ let mut utf8_offset_by_unit = Vec::with_capacity(code.len() + 1);
23
+ for (offset, character) in code.char_indices() {
24
+ utf8_offset_by_unit.push(offset as u32);
25
+ // Both halves of a surrogate pair map to the character start; tree-sitter node
26
+ // boundaries always align to whole code points, so the halves are never split.
27
+ if character.len_utf16() == 2 {
28
+ utf8_offset_by_unit.push(offset as u32);
29
+ }
30
+ }
31
+ utf8_offset_by_unit.push(code.len() as u32);
32
+ Source {
33
+ code,
34
+ utf8_offset_by_unit: Some(utf8_offset_by_unit),
35
+ }
36
+ }
37
+
38
+ pub fn to_utf16(&self) -> Vec<u16> {
39
+ self.code.encode_utf16().collect()
40
+ }
41
+
42
+ fn utf8_offset(&self, node_byte: usize) -> usize {
43
+ match &self.utf8_offset_by_unit {
44
+ None => node_byte / 2,
45
+ Some(map) => map[node_byte / 2] as usize,
46
+ }
47
+ }
48
+ }
49
+
50
+ pub fn node_text<'a>(node: Node<'_>, code: &Source<'a>) -> &'a str {
51
+ &code.code[code.utf8_offset(node.start_byte())..code.utf8_offset(node.end_byte())]
52
+ }
53
+
54
+ pub fn named_children<'t>(node: Node<'t>) -> Vec<Node<'t>> {
55
+ let mut cursor = node.walk();
56
+ node.named_children(&mut cursor).collect()
57
+ }
58
+
59
+ pub fn all_children<'t>(node: Node<'t>) -> Vec<Node<'t>> {
60
+ let mut cursor = node.walk();
61
+ node.children(&mut cursor).collect()
62
+ }
63
+
64
+ /// Children carrying the field, with node-tree-sitter's vendored-core semantics: extra children
65
+ /// (error-recovery nodes, comments) inherit the field of the preceding structural sibling.
66
+ /// tree-sitter 0.22.6 instead reports no field for extras (ts_node_field_name_for_child gained an
67
+ /// is_extra early return), which would desynchronize field-based extraction on malformed source.
68
+ pub fn find_children_by_field_name<'t>(node: Node<'t>, field_name: &str) -> Vec<Node<'t>> {
69
+ let mut children = Vec::new();
70
+ let mut preceding_structural_field: Option<&'static str> = None;
71
+ for index in 0..node.child_count() {
72
+ if let Some(child) = node.child(index) {
73
+ let field = if child.is_extra() {
74
+ preceding_structural_field
75
+ } else {
76
+ let field = node.field_name_for_child(index as u32);
77
+ preceding_structural_field = field;
78
+ field
79
+ };
80
+ if field == Some(field_name) {
81
+ children.push(child);
82
+ }
83
+ }
84
+ }
85
+ children
86
+ }
87
+
88
+ /// Splits like JavaScript's `code.split(/\r\n|\n|\r/)`, with `[]` for empty input as in classifyLines.
89
+ pub fn split_lines(code: &str) -> Vec<&str> {
90
+ if code.is_empty() {
91
+ return Vec::new();
92
+ }
93
+ let bytes = code.as_bytes();
94
+ let mut lines = Vec::new();
95
+ let mut start = 0;
96
+ let mut index = 0;
97
+ while index < bytes.len() {
98
+ match bytes[index] {
99
+ b'\r' => {
100
+ lines.push(&code[start..index]);
101
+ index += if bytes.get(index + 1) == Some(&b'\n') {
102
+ 2
103
+ } else {
104
+ 1
105
+ };
106
+ start = index;
107
+ }
108
+ b'\n' => {
109
+ lines.push(&code[start..index]);
110
+ index += 1;
111
+ start = index;
112
+ }
113
+ _ => index += 1,
114
+ }
115
+ }
116
+ lines.push(&code[start..]);
117
+ lines
118
+ }
119
+
120
+ /// JavaScript's `\s` character class (WhiteSpace + LineTerminator), which `String.prototype.trim`
121
+ /// also uses; Rust's `char::is_whitespace` differs (it excludes U+FEFF), so this is spelled out.
122
+ pub fn is_js_whitespace(character: char) -> bool {
123
+ matches!(
124
+ character,
125
+ '\t' | '\n' | '\u{000B}' | '\u{000C}' | '\r' | ' ' | '\u{00A0}' | '\u{1680}' | '\u{2000}'
126
+ ..='\u{200A}'
127
+ | '\u{2028}'
128
+ | '\u{2029}'
129
+ | '\u{202F}'
130
+ | '\u{205F}'
131
+ | '\u{3000}'
132
+ | '\u{FEFF}'
133
+ )
134
+ }
135
+
136
+ /// JavaScript ToInt32 for integer-valued numbers (all hash arithmetic stays below 2^53).
137
+ pub fn to_int32(value: i64) -> i32 {
138
+ value as i32
139
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-gauge",
3
- "version": "3.1.0",
3
+ "version": "4.0.0",
4
4
  "description": "Measure code metrics with tree-sitter.",
5
5
  "keywords": [
6
6
  "cli",
@@ -31,7 +31,13 @@
31
31
  "types": "dist/index.d.ts",
32
32
  "bin": "dist/cli.js",
33
33
  "files": [
34
- "dist/"
34
+ "dist/",
35
+ "native/build.rs",
36
+ "native/Cargo.lock",
37
+ "native/Cargo.toml",
38
+ "native/src/",
39
+ "scripts/buildNative.mjs",
40
+ "scripts/installNative.mjs"
35
41
  ],
36
42
  "scripts": {
37
43
  "benchmark": "node scripts/benchmark.mjs",
@@ -39,27 +45,18 @@
39
45
  "build-native": "node scripts/buildNative.mjs",
40
46
  "cleanup": "bun wb lint --fix --format",
41
47
  "format": "bun wb lint --format",
48
+ "postinstall": "node scripts/installNative.mjs",
42
49
  "lint": "bun wb lint",
43
50
  "lint-fix": "bun wb lint --fix",
44
- "prepare": "lefthook install || true",
51
+ "prepare": "bun lefthook install || true",
45
52
  "test": "bun wb test",
46
- "test/ci": "bun run build-native && CODE_GAUGE_EXPECT_NATIVE=1 vitest",
53
+ "test/ci": "bun run build-native && vitest",
47
54
  "typecheck": "bun wb typecheck",
48
55
  "verify": "bun wb verify",
49
56
  "verify-full": "bun wb verify --full"
50
57
  },
51
58
  "dependencies": {
52
- "commander": "15.0.0",
53
- "tree-sitter": "^0.21.1",
54
- "tree-sitter-c": "^0.21.0",
55
- "tree-sitter-cpp": "^0.22.0",
56
- "tree-sitter-go": "^0.21.2",
57
- "tree-sitter-java": "^0.21.0",
58
- "tree-sitter-javascript": "^0.21.4",
59
- "tree-sitter-python": "^0.21.0",
60
- "tree-sitter-ruby": "^0.21.0",
61
- "tree-sitter-rust": "^0.21.0",
62
- "tree-sitter-typescript": "^0.21.2"
59
+ "commander": "15.0.0"
63
60
  },
64
61
  "devDependencies": {
65
62
  "@tsconfig/bun": "1.0.10",
@@ -69,12 +66,12 @@
69
66
  "@types/node": "25.9.4",
70
67
  "@willbooster/oxfmt-config": "1.2.2",
71
68
  "@willbooster/oxlint-config": "1.4.8",
72
- "@willbooster/wb": "21.1.0",
73
- "build-ts": "21.0.4",
69
+ "@willbooster/wb": "21.5.0",
70
+ "build-ts": "21.0.6",
74
71
  "conventional-changelog-conventionalcommits": "9.3.1",
75
72
  "lefthook": "2.1.10",
76
- "oxfmt": "0.61.0",
77
- "oxlint": "1.76.0",
73
+ "oxfmt": "0.62.0",
74
+ "oxlint": "1.77.0",
78
75
  "oxlint-tsgolint": "7.0.2001",
79
76
  "semantic-release": "25.0.5",
80
77
  "sort-package-json": "4.0.0",
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ // Builds the native Rust addon and places it at native/code-gauge.node, where the TypeScript
3
+ // loader picks it up. Requires a Rust toolchain.
4
+
5
+ import { execFileSync } from 'node:child_process';
6
+ import { copyFileSync, existsSync } from 'node:fs';
7
+ import path from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const repoRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
11
+ const nativeDir = path.join(repoRoot, 'native');
12
+
13
+ execFileSync('cargo', ['build', '--release'], { cwd: nativeDir, stdio: 'inherit' });
14
+
15
+ const libraryNames = ['libcode_gauge_native.dylib', 'libcode_gauge_native.so', 'code_gauge_native.dll'];
16
+ const builtLibrary = libraryNames
17
+ .map((name) => path.join(nativeDir, 'target', 'release', name))
18
+ .find((candidate) => existsSync(candidate));
19
+ if (!builtLibrary) {
20
+ throw new Error('cargo build succeeded but no native library was found in native/target/release');
21
+ }
22
+
23
+ const output = path.join(nativeDir, 'code-gauge.node');
24
+ copyFileSync(builtLibrary, output);
25
+ console.log(`Built ${output}`);