code-gauge 10.3.0 → 10.4.1
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 +4 -4
- package/THIRD-PARTY-NOTICES.txt +36 -12
- package/dist/crossFileNearMiss.cjs +1 -1
- package/dist/crossFileNearMiss.cjs.map +1 -1
- package/dist/crossFileNearMiss.js +1 -1
- package/dist/crossFileNearMiss.js.map +1 -1
- package/native/Cargo.lock +24 -24
- package/native/Cargo.toml +12 -12
- package/native/code-gauge.wasm +0 -0
- package/native/src/complexity.rs +62 -55
- package/native/src/container.rs +7 -8
- package/native/src/dep_degree.rs +190 -97
- package/native/src/duplication.rs +37 -3
- package/native/src/functions.rs +15 -15
- package/native/src/languages.rs +8 -4
- package/native/src/measure.rs +157 -83
- package/native/src/ncss.rs +45 -36
- package/native/src/tree_index.rs +145 -44
- package/native/src/util.rs +42 -5
- package/package.json +13 -13
|
@@ -234,6 +234,7 @@ const STRING_FRAGMENT_TYPES: &[&str] = &[
|
|
|
234
234
|
"raw_string_content",
|
|
235
235
|
"interpreted_string_literal_content",
|
|
236
236
|
"raw_string_literal_content",
|
|
237
|
+
"format_specifier",
|
|
237
238
|
"raw_string_start",
|
|
238
239
|
"raw_string_end",
|
|
239
240
|
"escape_sequence",
|
|
@@ -299,6 +300,7 @@ const STRING_CONTENT_FRAGMENT_TYPES: &[&str] = &[
|
|
|
299
300
|
"raw_string_content",
|
|
300
301
|
"interpreted_string_literal_content",
|
|
301
302
|
"raw_string_literal_content",
|
|
303
|
+
"format_specifier",
|
|
302
304
|
"escape_sequence",
|
|
303
305
|
"heredoc_content",
|
|
304
306
|
];
|
|
@@ -570,6 +572,9 @@ fn collect_tokens<'a>(
|
|
|
570
572
|
statement_list_statements: &mut Vec<TokenRange>,
|
|
571
573
|
) -> TokenRange {
|
|
572
574
|
let start_token_index = tokens.len();
|
|
575
|
+
// The span of the node's tokens, which can be narrower than the node: a Python block
|
|
576
|
+
// starts at its header's colon, and a Rust declaration starts at its doc comment.
|
|
577
|
+
let mut token_span: Option<SourceSpan> = None;
|
|
573
578
|
let is_token = is_token(node);
|
|
574
579
|
let atomic_kind = if is_token {
|
|
575
580
|
None
|
|
@@ -602,6 +607,7 @@ fn collect_tokens<'a>(
|
|
|
602
607
|
container_statement_ranges,
|
|
603
608
|
&mut child_statements,
|
|
604
609
|
);
|
|
610
|
+
token_span = widen_token_span(token_span, &child_range);
|
|
605
611
|
// A Kotlin lambda's parameters sit beside its statements.
|
|
606
612
|
if is_container
|
|
607
613
|
&& child.is_named()
|
|
@@ -625,13 +631,19 @@ fn collect_tokens<'a>(
|
|
|
625
631
|
}
|
|
626
632
|
}
|
|
627
633
|
|
|
628
|
-
let
|
|
629
|
-
start_token_index,
|
|
630
|
-
end_token_index: tokens.len(),
|
|
634
|
+
let span = token_span.unwrap_or(SourceSpan {
|
|
631
635
|
start_index: node.start_byte(),
|
|
632
636
|
end_index: node.end_byte(),
|
|
633
637
|
start_line: node.start_position().row + 1,
|
|
634
638
|
end_line: node.end_position().row + 1,
|
|
639
|
+
});
|
|
640
|
+
let range = TokenRange {
|
|
641
|
+
start_token_index,
|
|
642
|
+
end_token_index: tokens.len(),
|
|
643
|
+
start_index: span.start_index,
|
|
644
|
+
end_index: span.end_index,
|
|
645
|
+
start_line: span.start_line,
|
|
646
|
+
end_line: span.end_line,
|
|
635
647
|
};
|
|
636
648
|
if is_duplicate_block(node) {
|
|
637
649
|
block_ranges.push(TokenRange { ..range });
|
|
@@ -649,6 +661,28 @@ fn collect_tokens<'a>(
|
|
|
649
661
|
);
|
|
650
662
|
}
|
|
651
663
|
|
|
664
|
+
#[derive(Clone, Copy)]
|
|
665
|
+
struct SourceSpan {
|
|
666
|
+
start_index: usize,
|
|
667
|
+
end_index: usize,
|
|
668
|
+
start_line: usize,
|
|
669
|
+
end_line: usize,
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/// `span` extended to the end of `child`'s tokens, or `child`'s token span when `span` is empty;
|
|
673
|
+
/// a child without tokens leaves it as it is.
|
|
674
|
+
fn widen_token_span(span: Option<SourceSpan>, child: &TokenRange) -> Option<SourceSpan> {
|
|
675
|
+
if child.start_token_index == child.end_token_index {
|
|
676
|
+
return span;
|
|
677
|
+
}
|
|
678
|
+
Some(SourceSpan {
|
|
679
|
+
start_index: span.map_or(child.start_index, |span| span.start_index),
|
|
680
|
+
end_index: child.end_index,
|
|
681
|
+
start_line: span.map_or(child.start_line, |span| span.start_line),
|
|
682
|
+
end_line: child.end_line,
|
|
683
|
+
})
|
|
684
|
+
}
|
|
685
|
+
|
|
652
686
|
/// Comments and dependency declarations contribute no tokens, so no clone consists of them; the
|
|
653
687
|
/// tokens around them stay adjacent, so a clone may still extend across one.
|
|
654
688
|
fn is_tokenless(node: Node<'_>, code: &Source<'_>) -> bool {
|
package/native/src/functions.rs
CHANGED
|
@@ -4,6 +4,8 @@ use tree_sitter::Node;
|
|
|
4
4
|
use crate::tree_index::NodeExt;
|
|
5
5
|
use crate::util::{all_children, find_children_by_field_name, named_children, node_text, Source};
|
|
6
6
|
|
|
7
|
+
// code-gauge-allow-file fileNcss 648 -- holds the function-recognition and naming rules of every language
|
|
8
|
+
|
|
7
9
|
/// Declarations without a body have no control flow, so they are signatures, not functions: C++
|
|
8
10
|
/// pure-virtual/`= default`/`= delete` members, abstract/interface/extern methods (Java, C#,
|
|
9
11
|
/// Kotlin), Go assembly-backed function declarations, and Rust trait method signatures, matching
|
|
@@ -71,11 +73,13 @@ pub fn count_parameters(node: Node<'_>, code: &Source<'_>) -> usize {
|
|
|
71
73
|
return 0;
|
|
72
74
|
};
|
|
73
75
|
|
|
74
|
-
// A Java bare lambda parameter (`x -> x + 1`) puts a lone identifier
|
|
75
|
-
// a C# one (`x => x + 1`) is an
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
// A Java bare lambda parameter (`x -> x + 1`, `_ -> 1`) puts a lone identifier or unnamed
|
|
77
|
+
// `underscore_pattern` in the `parameters` field; a C# one (`x => x + 1`) is an
|
|
78
|
+
// `implicit_parameter` leaf.
|
|
79
|
+
if matches!(
|
|
80
|
+
parameters_node.kind_name(),
|
|
81
|
+
"identifier" | "underscore_pattern" | "implicit_parameter"
|
|
82
|
+
) {
|
|
79
83
|
return 1;
|
|
80
84
|
}
|
|
81
85
|
// Kotlin default values (`x: Int = 0`) are siblings of their `parameter`, not children.
|
|
@@ -127,12 +131,7 @@ pub fn count_parameters(node: Node<'_>, code: &Source<'_>) -> usize {
|
|
|
127
131
|
1
|
|
128
132
|
};
|
|
129
133
|
}
|
|
130
|
-
|
|
131
|
-
let anonymous_variadic_count = all_children(parameters_node)
|
|
132
|
-
.iter()
|
|
133
|
-
.filter(|child| !child.is_named() && node_text(**child, code) == "...")
|
|
134
|
-
.count();
|
|
135
|
-
count + anonymous_variadic_count + usize::from(!csharp_params_array_ids.is_empty())
|
|
134
|
+
count + usize::from(!csharp_params_array_ids.is_empty())
|
|
136
135
|
}
|
|
137
136
|
|
|
138
137
|
/// C/C++ `int f(void)` has a `parameter_declaration` whose type is a bare `void` with no declarator.
|
|
@@ -197,11 +196,11 @@ fn find_parameters_node(node: Node<'_>) -> Option<Node<'_>> {
|
|
|
197
196
|
})
|
|
198
197
|
}
|
|
199
198
|
|
|
200
|
-
pub fn collect_nodes<'t>(root: Node<'t>, matches: impl Fn(
|
|
199
|
+
pub fn collect_nodes<'t>(root: Node<'t>, matches: impl Fn(Node<'t>) -> bool) -> Vec<Node<'t>> {
|
|
201
200
|
let mut nodes = Vec::new();
|
|
202
201
|
|
|
203
|
-
fn visit<'t>(node: Node<'t>, matches: &impl Fn(
|
|
204
|
-
if matches(node
|
|
202
|
+
fn visit<'t>(node: Node<'t>, matches: &impl Fn(Node<'t>) -> bool, nodes: &mut Vec<Node<'t>>) {
|
|
203
|
+
if matches(node) {
|
|
205
204
|
nodes.push(node);
|
|
206
205
|
}
|
|
207
206
|
|
|
@@ -907,7 +906,8 @@ pub fn find_string_literal_content(literal: Node<'_>, code: &Source<'_>) -> Opti
|
|
|
907
906
|
"string_content"
|
|
908
907
|
| "string_fragment"
|
|
909
908
|
| "interpreted_string_literal_content"
|
|
910
|
-
| "raw_string_literal_content"
|
|
909
|
+
| "raw_string_literal_content"
|
|
910
|
+
| "format_specifier" => {
|
|
911
911
|
has_content_node = true;
|
|
912
912
|
content.push_str(node_text(*child, code));
|
|
913
913
|
}
|
package/native/src/languages.rs
CHANGED
|
@@ -36,7 +36,7 @@ enum GrammarId {
|
|
|
36
36
|
Tsx,
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
const GRAMMAR_COUNT: usize = GrammarId::Tsx as usize + 1;
|
|
39
|
+
pub const GRAMMAR_COUNT: usize = GrammarId::Tsx as usize + 1;
|
|
40
40
|
|
|
41
41
|
impl LanguageDefinition {
|
|
42
42
|
pub fn grammar(&self) -> Language {
|
|
@@ -56,6 +56,11 @@ impl LanguageDefinition {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/// Distinct for each grammar, below GRAMMAR_COUNT.
|
|
60
|
+
pub fn grammar_index(&self) -> usize {
|
|
61
|
+
self.grammar_id as usize
|
|
62
|
+
}
|
|
63
|
+
|
|
59
64
|
/// The grammar's node kind names, indexed by kind id.
|
|
60
65
|
pub fn kind_names(&self) -> &'static [String] {
|
|
61
66
|
static KIND_NAMES: [OnceLock<Vec<String>>; GRAMMAR_COUNT] =
|
|
@@ -434,9 +439,8 @@ const RUST_NCSS_NODES: &[&str] = &[
|
|
|
434
439
|
"else_clause",
|
|
435
440
|
"match_arm",
|
|
436
441
|
];
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
const RUST_NCSS_CONTAINERS: &[&str] = &["block", "else_clause"];
|
|
442
|
+
|
|
443
|
+
const RUST_NCSS_CONTAINERS: &[&str] = &["block"];
|
|
440
444
|
|
|
441
445
|
const JAVA_NCSS_NODES: &[&str] = &[
|
|
442
446
|
"package_declaration",
|
package/native/src/measure.rs
CHANGED
|
@@ -3,7 +3,7 @@ use std::sync::OnceLock;
|
|
|
3
3
|
use tree_sitter::Node;
|
|
4
4
|
|
|
5
5
|
use crate::complexity::{is_lambda_body_block, measure_function_body_metrics, LanguageSets};
|
|
6
|
-
use crate::dep_degree::
|
|
6
|
+
use crate::dep_degree::measure_dep_degrees;
|
|
7
7
|
use crate::duplication::{
|
|
8
8
|
collect_cross_file_file_data, hash_text, measure_duplication, tokenize, DuplicationSettings,
|
|
9
9
|
TokenizedSource,
|
|
@@ -12,7 +12,7 @@ use crate::functions::{
|
|
|
12
12
|
collect_nodes, count_parameters, find_function_name, is_implemented_function,
|
|
13
13
|
};
|
|
14
14
|
use crate::languages::LanguageDefinition;
|
|
15
|
-
use crate::tree_index::{NodeExt, TreeIndex};
|
|
15
|
+
use crate::tree_index::{KindSet, NodeExt, TreeIndex};
|
|
16
16
|
use crate::types::{
|
|
17
17
|
CrossFileFileData, FunctionMetrics, HalsteadCounts, LineMetrics, NativeMetrics,
|
|
18
18
|
};
|
|
@@ -20,6 +20,7 @@ use crate::util::{
|
|
|
20
20
|
all_children, is_js_whitespace, is_token, named_children, node_text, split_lines, Source,
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
+
// code-gauge-allow functionNcss 32 -- assembles the file's metrics from every pass in one place
|
|
23
24
|
pub fn measure(
|
|
24
25
|
code: &str,
|
|
25
26
|
language: &LanguageDefinition,
|
|
@@ -32,23 +33,26 @@ pub fn measure(
|
|
|
32
33
|
let _index = TreeIndex::new(&tree, language)?;
|
|
33
34
|
let root = tree.root_node();
|
|
34
35
|
let code = &source;
|
|
35
|
-
let sets = LanguageSets::
|
|
36
|
+
let sets = LanguageSets::of(language);
|
|
36
37
|
|
|
37
38
|
// One walk collects both function and initializer-block candidates (Ruby's `block` is both).
|
|
38
|
-
let candidates = collect_nodes(root, |
|
|
39
|
-
sets.function_nodes.contains(
|
|
39
|
+
let candidates = collect_nodes(root, |node| {
|
|
40
|
+
sets.function_nodes.contains(node) || INITIALIZER_KINDS.contains(node)
|
|
40
41
|
});
|
|
41
42
|
let initializer_block_count = count_initializer_blocks(&candidates);
|
|
42
43
|
let functions: Vec<Node<'_>> = candidates
|
|
43
44
|
.into_iter()
|
|
44
45
|
.filter(|node| {
|
|
45
|
-
sets.function_nodes.contains(node
|
|
46
|
+
sets.function_nodes.contains(*node)
|
|
46
47
|
&& !is_lambda_body_block(*node)
|
|
47
48
|
&& is_implemented_function(*node)
|
|
48
49
|
})
|
|
49
50
|
.collect();
|
|
50
51
|
|
|
51
|
-
let body_metrics = measure_function_body_metrics(root,
|
|
52
|
+
let body_metrics = measure_function_body_metrics(root, sets, code);
|
|
53
|
+
let (halstead_counts, mut halstead_counts_by_function) =
|
|
54
|
+
measure_halstead(root, &functions, code);
|
|
55
|
+
let dep_degrees = measure_dep_degrees(root, &functions, code, &sets.function_nodes);
|
|
52
56
|
let inline_namespaces = crate::container::find_inline_namespaces(root, code);
|
|
53
57
|
let function_metrics: Vec<FunctionMetrics> = functions
|
|
54
58
|
.iter()
|
|
@@ -86,14 +90,15 @@ pub fn measure(
|
|
|
86
90
|
nesting_depth: body_metrics.nesting_depth,
|
|
87
91
|
ncss: body_metrics.ncss,
|
|
88
92
|
parameter_count: count_parameters(*node, code),
|
|
89
|
-
halstead_counts:
|
|
90
|
-
|
|
93
|
+
halstead_counts: halstead_counts_by_function
|
|
94
|
+
.remove(&node.id())
|
|
95
|
+
.expect("every function's Halstead counts are measured"),
|
|
96
|
+
dep_degree: dep_degrees[&node.id()],
|
|
91
97
|
}
|
|
92
98
|
})
|
|
93
99
|
.collect();
|
|
94
100
|
|
|
95
101
|
let (lines, code_line_numbers) = classify_lines(code, root);
|
|
96
|
-
let halstead_counts = measure_halstead(root, code);
|
|
97
102
|
let tokenized = tokenize(root, code);
|
|
98
103
|
|
|
99
104
|
Ok(NativeMetrics {
|
|
@@ -144,16 +149,16 @@ pub fn measure(
|
|
|
144
149
|
/// Initializer blocks run code of their own, so each is a component like a function: Java static
|
|
145
150
|
/// and instance initializers, Kotlin `init` blocks, and JavaScript/TypeScript class `static`
|
|
146
151
|
/// blocks. Their decisions already count as decisions outside functions.
|
|
147
|
-
|
|
152
|
+
static INITIALIZER_KINDS: KindSet = KindSet::new(&[
|
|
148
153
|
"static_initializer",
|
|
149
154
|
"anonymous_initializer",
|
|
150
155
|
"class_static_block",
|
|
151
156
|
"block",
|
|
152
|
-
];
|
|
157
|
+
]);
|
|
153
158
|
|
|
154
159
|
/// A block of a class that runs when the class or an instance is initialized.
|
|
155
160
|
pub fn is_initializer_block(node: Node<'_>) -> bool {
|
|
156
|
-
|
|
161
|
+
INITIALIZER_KINDS.contains(node)
|
|
157
162
|
// A bare block is an initializer only as a direct member of a Java class or enum body.
|
|
158
163
|
&& (node.kind_name() != "block"
|
|
159
164
|
|| node.parent_node().is_some_and(|parent| {
|
|
@@ -270,9 +275,9 @@ pub fn collect_function_token_sequences(
|
|
|
270
275
|
let tree = parse_source(&source, language)?;
|
|
271
276
|
let _index = TreeIndex::new(&tree, language)?;
|
|
272
277
|
let root = tree.root_node();
|
|
273
|
-
let sets = LanguageSets::
|
|
278
|
+
let sets = LanguageSets::of(language);
|
|
274
279
|
Ok(
|
|
275
|
-
collect_nodes(root, |
|
|
280
|
+
collect_nodes(root, |node| sets.function_nodes.contains(node))
|
|
276
281
|
.into_iter()
|
|
277
282
|
.filter(|node| !is_lambda_body_block(*node) && is_implemented_function(*node))
|
|
278
283
|
.map(|node| {
|
|
@@ -321,13 +326,11 @@ fn collect_token_symbols(
|
|
|
321
326
|
}
|
|
322
327
|
// Remaining operand leaves are literals, normalized by kind; everything else (keywords,
|
|
323
328
|
// operators, punctuation) is kept verbatim.
|
|
324
|
-
symbols.push(hash_text(
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
},
|
|
330
|
-
));
|
|
329
|
+
symbols.push(hash_text(if OPERAND_KINDS.contains(node) {
|
|
330
|
+
node.kind_name()
|
|
331
|
+
} else {
|
|
332
|
+
node_text(node, code)
|
|
333
|
+
}));
|
|
331
334
|
}
|
|
332
335
|
|
|
333
336
|
/// Parses the source from UTF-16, matching node-tree-sitter's JavaScript string semantics:
|
|
@@ -566,7 +569,7 @@ const OPERATOR_TEXTS: &[&str] = &[
|
|
|
566
569
|
"continue",
|
|
567
570
|
];
|
|
568
571
|
|
|
569
|
-
|
|
572
|
+
static OPERAND_KINDS: KindSet = KindSet::new(&[
|
|
570
573
|
"identifier",
|
|
571
574
|
"implicit_parameter",
|
|
572
575
|
"property_identifier",
|
|
@@ -624,11 +627,11 @@ const OPERAND_NODE_TYPES: &[&str] = &[
|
|
|
624
627
|
"undefined",
|
|
625
628
|
"nil",
|
|
626
629
|
"none",
|
|
627
|
-
];
|
|
630
|
+
]);
|
|
628
631
|
|
|
629
632
|
/// Non-leaf literals counted as one Halstead operand without descending.
|
|
630
633
|
/// `character_literal` is a leaf in Java but wraps its content in C# and Kotlin.
|
|
631
|
-
|
|
634
|
+
static ATOMIC_OPERAND_KINDS: KindSet = KindSet::new(&[
|
|
632
635
|
"interpreted_string_literal",
|
|
633
636
|
"character_literal",
|
|
634
637
|
"regex",
|
|
@@ -637,89 +640,160 @@ const ATOMIC_OPERAND_NODE_TYPES: &[&str] = &[
|
|
|
637
640
|
"floating_point_type",
|
|
638
641
|
"sized_type_specifier",
|
|
639
642
|
"placeholder_type_specifier",
|
|
640
|
-
];
|
|
643
|
+
]);
|
|
641
644
|
|
|
642
645
|
fn operator_texts() -> &'static FxHashSet<&'static str> {
|
|
643
646
|
static SET: OnceLock<FxHashSet<&'static str>> = OnceLock::new();
|
|
644
647
|
SET.get_or_init(|| OPERATOR_TEXTS.iter().copied().collect())
|
|
645
648
|
}
|
|
646
649
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
SET.get_or_init(|| OPERAND_NODE_TYPES.iter().copied().collect())
|
|
650
|
-
}
|
|
650
|
+
/// Node kinds spelled like an operator.
|
|
651
|
+
static OPERATOR_KINDS: KindSet = KindSet::new(OPERATOR_TEXTS);
|
|
651
652
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
653
|
+
/// Halstead operator and operand occurrences by text.
|
|
654
|
+
#[derive(Default)]
|
|
655
|
+
struct HalsteadTally<'a> {
|
|
656
|
+
operators: FxHashMap<&'a str, u64>,
|
|
657
|
+
operands: FxHashMap<&'a str, u64>,
|
|
655
658
|
}
|
|
656
659
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
operators: &mut FxHashMap<&'a str, u64>,
|
|
665
|
-
operands: &mut FxHashMap<&'a str, u64>,
|
|
666
|
-
) {
|
|
667
|
-
if matches!(
|
|
668
|
-
node.kind_name(),
|
|
669
|
-
"comment" | "line_comment" | "block_comment"
|
|
670
|
-
) {
|
|
671
|
-
return;
|
|
660
|
+
impl<'a> HalsteadTally<'a> {
|
|
661
|
+
fn counts(&self) -> HalsteadCounts {
|
|
662
|
+
HalsteadCounts {
|
|
663
|
+
distinct_operators: self.operators.len(),
|
|
664
|
+
distinct_operands: self.operands.len(),
|
|
665
|
+
total_operators: self.operators.values().sum(),
|
|
666
|
+
total_operands: self.operands.values().sum(),
|
|
672
667
|
}
|
|
668
|
+
}
|
|
673
669
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
670
|
+
fn absorb(&mut self, other: HalsteadTally<'a>) {
|
|
671
|
+
for (text, count) in other.operators {
|
|
672
|
+
*self.operators.entry(text).or_insert(0) += count;
|
|
673
|
+
}
|
|
674
|
+
for (text, count) in other.operands {
|
|
675
|
+
*self.operands.entry(text).or_insert(0) += count;
|
|
677
676
|
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
678
679
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
680
|
+
/// Halstead counts of the subtree of `root` and, by node id, of each function in `functions`.
|
|
681
|
+
fn measure_halstead<'a>(
|
|
682
|
+
root: Node<'a>,
|
|
683
|
+
functions: &[Node<'a>],
|
|
684
|
+
code: &Source<'a>,
|
|
685
|
+
) -> (HalsteadCounts, FxHashMap<usize, HalsteadCounts>) {
|
|
686
|
+
let (counts, mut by_function) =
|
|
687
|
+
count_halstead(root, &functions.iter().map(Node::id).collect(), code);
|
|
688
|
+
for function in functions {
|
|
689
|
+
// The walk counts atomic operands and tokens without descending into them and skips
|
|
690
|
+
// comments, any of which can hold a function (a C++ lambda in a constrained `auto` type).
|
|
691
|
+
by_function
|
|
692
|
+
.entry(function.id())
|
|
693
|
+
.or_insert_with(|| count_halstead(*function, &FxHashSet::default(), code).0);
|
|
694
|
+
}
|
|
695
|
+
(counts, by_function)
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/// Halstead counts of the subtree of `root` and, by node id, of each function in `function_ids`
|
|
699
|
+
/// whose subtree the walk enters: a function's tokens are counted the same wherever the walk
|
|
700
|
+
/// starts, so its tally is kept apart while its subtree is walked and then joins the enclosing one.
|
|
701
|
+
fn count_halstead<'a>(
|
|
702
|
+
root: Node<'a>,
|
|
703
|
+
function_ids: &FxHashSet<usize>,
|
|
704
|
+
code: &Source<'a>,
|
|
705
|
+
) -> (HalsteadCounts, FxHashMap<usize, HalsteadCounts>) {
|
|
706
|
+
struct Walk<'a, 'w> {
|
|
707
|
+
code: &'w Source<'a>,
|
|
708
|
+
function_ids: &'w FxHashSet<usize>,
|
|
709
|
+
/// The tally of the walked subtree, then that of each function being walked.
|
|
710
|
+
tallies: Vec<HalsteadTally<'a>>,
|
|
711
|
+
by_function: FxHashMap<usize, HalsteadCounts>,
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
impl<'a> Walk<'a, '_> {
|
|
715
|
+
fn visit(&mut self, node: Node<'a>) {
|
|
716
|
+
let tally = self.tallies.last_mut().expect("the walked subtree's tally");
|
|
717
|
+
if !count_halstead_token(node, self.code, tally) {
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
let is_function = self.function_ids.contains(&node.id());
|
|
721
|
+
if is_function {
|
|
722
|
+
self.tallies.push(HalsteadTally::default());
|
|
723
|
+
}
|
|
724
|
+
for child in all_children(node) {
|
|
725
|
+
self.visit(child);
|
|
726
|
+
}
|
|
727
|
+
if is_function {
|
|
728
|
+
let tally = self.tallies.pop().expect("pushed above");
|
|
729
|
+
self.by_function.insert(node.id(), tally.counts());
|
|
730
|
+
self.tallies
|
|
731
|
+
.last_mut()
|
|
732
|
+
.expect("the walked subtree's tally")
|
|
733
|
+
.absorb(tally);
|
|
699
734
|
}
|
|
700
|
-
return;
|
|
701
735
|
}
|
|
736
|
+
}
|
|
702
737
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
738
|
+
let mut walk = Walk {
|
|
739
|
+
code,
|
|
740
|
+
function_ids,
|
|
741
|
+
tallies: vec![HalsteadTally::default()],
|
|
742
|
+
by_function: FxHashMap::default(),
|
|
743
|
+
};
|
|
744
|
+
walk.visit(root);
|
|
745
|
+
(walk.tallies[0].counts(), walk.by_function)
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/// Counts the node into `tally` if it is a token or an atomic operand, returning whether its
|
|
749
|
+
/// children are to be counted instead.
|
|
750
|
+
fn count_halstead_token<'a>(
|
|
751
|
+
node: Node<'a>,
|
|
752
|
+
code: &Source<'a>,
|
|
753
|
+
tally: &mut HalsteadTally<'a>,
|
|
754
|
+
) -> bool {
|
|
755
|
+
if matches!(
|
|
756
|
+
node.kind_name(),
|
|
757
|
+
"comment" | "line_comment" | "block_comment"
|
|
758
|
+
) {
|
|
759
|
+
return false;
|
|
706
760
|
}
|
|
707
761
|
|
|
708
|
-
|
|
762
|
+
if is_atomic_operand(node) {
|
|
763
|
+
*tally.operands.entry(node_text(node, code)).or_insert(0) += 1;
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
709
766
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
767
|
+
// Operators are counted from leaf tokens only: keyword-named nodes always contain a
|
|
768
|
+
// same-text anonymous keyword leaf, so counting the named node as well would double-count.
|
|
769
|
+
if !is_token(node) {
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
772
|
+
let text = node_text(node, code);
|
|
773
|
+
// Operands win over text matches so identifiers spelled like word operators stay operands;
|
|
774
|
+
// C# `nameof(x)` is the one keyword operator the grammar parses as a plain callee.
|
|
775
|
+
if is_csharp_nameof_callee(node, text) {
|
|
776
|
+
*tally.operators.entry(text).or_insert(0) += 1;
|
|
777
|
+
} else if OPERAND_KINDS.contains(node) {
|
|
778
|
+
*tally.operands.entry(text).or_insert(0) += 1;
|
|
779
|
+
} else if (operator_texts().contains(text) || OPERATOR_KINDS.contains(node))
|
|
780
|
+
&& is_countable_contextual_token(node, text)
|
|
781
|
+
{
|
|
782
|
+
let key = if text.is_empty() {
|
|
783
|
+
node.kind_name()
|
|
784
|
+
} else {
|
|
785
|
+
text
|
|
786
|
+
};
|
|
787
|
+
*tally.operators.entry(key).or_insert(0) += 1;
|
|
715
788
|
}
|
|
789
|
+
false
|
|
716
790
|
}
|
|
717
791
|
|
|
718
|
-
/// A literal counted as one operand without descending:
|
|
792
|
+
/// A literal counted as one operand without descending: ATOMIC_OPERAND_KINDS, or a Go raw
|
|
719
793
|
/// string, which wraps its content like Go's interpreted strings (the Rust and C++
|
|
720
794
|
/// `raw_string_literal`s count their content leaves instead).
|
|
721
795
|
fn is_atomic_operand(node: Node<'_>) -> bool {
|
|
722
|
-
|
|
796
|
+
ATOMIC_OPERAND_KINDS.contains(node)
|
|
723
797
|
|| (node.kind_name() == "raw_string_literal" && crate::tree_index::language_name() == "go")
|
|
724
798
|
}
|
|
725
799
|
|