code-gauge 5.0.0 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +204 -72
- package/THIRD-PARTY-NOTICES.txt +1152 -798
- package/dist/checkCommand.cjs +5 -0
- package/dist/checkCommand.cjs.map +1 -0
- package/dist/checkCommand.d.ts +12 -0
- package/dist/checkCommand.js +5 -0
- package/dist/checkCommand.js.map +1 -0
- package/dist/cli.cjs +3 -3
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +3 -3
- package/dist/cli.js.map +1 -1
- package/dist/cliConfig.cjs +1 -1
- package/dist/cliConfig.cjs.map +1 -1
- package/dist/cliConfig.d.ts +3 -11
- package/dist/cliConfig.js +1 -1
- package/dist/cliConfig.js.map +1 -1
- package/dist/duplication.cjs +1 -1
- package/dist/duplication.cjs.map +1 -1
- package/dist/duplication.js +1 -1
- package/dist/duplication.js.map +1 -1
- package/dist/exclusion.cjs +1 -1
- package/dist/exclusion.cjs.map +1 -1
- package/dist/exclusion.d.ts +2 -2
- package/dist/exclusion.js +1 -1
- package/dist/exclusion.js.map +1 -1
- package/dist/git.cjs +1 -1
- package/dist/git.cjs.map +1 -1
- package/dist/git.d.ts +13 -12
- package/dist/git.js +1 -1
- package/dist/git.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +0 -4
- package/dist/index.js +1 -1
- package/dist/metrics.cjs +1 -1
- package/dist/metrics.cjs.map +1 -1
- package/dist/metrics.d.ts +1 -2
- package/dist/metrics.js +1 -1
- package/dist/metrics.js.map +1 -1
- package/dist/nativeMetrics.cjs +3 -3
- package/dist/nativeMetrics.cjs.map +1 -1
- package/dist/nativeMetrics.js +3 -3
- package/dist/nativeMetrics.js.map +1 -1
- package/dist/scan.cjs +1 -1
- package/dist/scan.cjs.map +1 -1
- package/dist/scan.d.ts +10 -26
- package/dist/scan.js +1 -1
- package/dist/scan.js.map +1 -1
- package/dist/thresholdCheck.cjs +2 -0
- package/dist/thresholdCheck.cjs.map +1 -0
- package/dist/thresholdCheck.d.ts +36 -0
- package/dist/thresholdCheck.js +2 -0
- package/dist/thresholdCheck.js.map +1 -0
- package/dist/thresholds.cjs +2 -0
- package/dist/thresholds.cjs.map +1 -0
- package/dist/thresholds.d.ts +38 -0
- package/dist/thresholds.js +2 -0
- package/dist/thresholds.js.map +1 -0
- package/dist/types.d.ts +8 -4
- package/dist/worker.cjs +1 -1
- package/dist/worker.cjs.map +1 -1
- package/dist/worker.js +1 -1
- package/dist/worker.js.map +1 -1
- package/native/Cargo.lock +73 -73
- package/native/Cargo.toml +16 -21
- package/native/code-gauge.wasm +0 -0
- package/native/src/dep_degree.rs +6 -6
- package/native/src/duplication.rs +38 -12
- package/native/src/functions.rs +10 -7
- package/native/src/languages.rs +14 -13
- package/native/src/measure.rs +16 -8
- package/native/src/tree_index.rs +4 -4
- package/native/src/util.rs +9 -2
- package/package.json +10 -9
- package/dist/diffCommand.cjs +0 -5
- package/dist/diffCommand.cjs.map +0 -1
- package/dist/diffCommand.d.ts +0 -17
- package/dist/diffCommand.js +0 -5
- package/dist/diffCommand.js.map +0 -1
- package/dist/newDuplication.cjs +0 -2
- package/dist/newDuplication.cjs.map +0 -1
- package/dist/newDuplication.d.ts +0 -40
- package/dist/newDuplication.js +0 -2
- package/dist/newDuplication.js.map +0 -1
- package/dist/regressionGate.cjs +0 -2
- package/dist/regressionGate.cjs.map +0 -1
- package/dist/regressionGate.d.ts +0 -108
- package/dist/regressionGate.js +0 -2
- package/dist/regressionGate.js.map +0 -1
package/native/src/functions.rs
CHANGED
|
@@ -821,13 +821,16 @@ fn find_string_literal_content(literal: Node<'_>, code: &Source<'_>) -> Option<S
|
|
|
821
821
|
{
|
|
822
822
|
return None;
|
|
823
823
|
}
|
|
824
|
-
//
|
|
825
|
-
//
|
|
824
|
+
// Escapes alone do not spell the content out, so the concatenation is trusted only when a
|
|
825
|
+
// content node takes part in it.
|
|
826
826
|
let mut content = String::new();
|
|
827
827
|
let mut has_content_node = false;
|
|
828
828
|
for child in &children {
|
|
829
829
|
match child.kind_name() {
|
|
830
|
-
"string_content"
|
|
830
|
+
"string_content"
|
|
831
|
+
| "string_fragment"
|
|
832
|
+
| "interpreted_string_literal_content"
|
|
833
|
+
| "raw_string_literal_content" => {
|
|
831
834
|
has_content_node = true;
|
|
832
835
|
content.push_str(node_text(*child, code));
|
|
833
836
|
}
|
|
@@ -838,8 +841,8 @@ fn find_string_literal_content(literal: Node<'_>, code: &Source<'_>) -> Option<S
|
|
|
838
841
|
if has_content_node && !content.is_empty() {
|
|
839
842
|
return Some(content);
|
|
840
843
|
}
|
|
841
|
-
// A
|
|
842
|
-
//
|
|
844
|
+
// A literal without a content child (Go's rune literal) keeps its delimiters in the text; an
|
|
845
|
+
// empty literal is left without a name.
|
|
843
846
|
let text = node_text(literal, code);
|
|
844
847
|
let quote = text
|
|
845
848
|
.chars()
|
|
@@ -880,7 +883,7 @@ fn find_assignment_target_name(assignment: Node<'_>, code: &Source<'_>) -> Optio
|
|
|
880
883
|
"field_expression" => target.child_by_field_name("field")?,
|
|
881
884
|
"member_access_expression" => target.child_by_field_name("name")?,
|
|
882
885
|
"field_access" => target.child_by_field_name("field")?,
|
|
883
|
-
"navigation_expression" => target.named_child(target.named_child_count() - 1)?,
|
|
886
|
+
"navigation_expression" => target.named_child(target.named_child_count() as u32 - 1)?,
|
|
884
887
|
"qualified_identifier" => return unwrap_declarator_name(Some(target), code),
|
|
885
888
|
_ => return None,
|
|
886
889
|
};
|
|
@@ -975,7 +978,7 @@ fn is_value_of_parent(node: Node<'_>, parent: Node<'_>) -> bool {
|
|
|
975
978
|
.is_some_and(|child| child.id() == node.id())
|
|
976
979
|
{
|
|
977
980
|
return matches!(
|
|
978
|
-
parent.field_name_for_child(index
|
|
981
|
+
parent.field_name_for_child(index),
|
|
979
982
|
None | Some("value")
|
|
980
983
|
);
|
|
981
984
|
}
|
package/native/src/languages.rs
CHANGED
|
@@ -41,24 +41,24 @@ const GRAMMAR_COUNT: usize = GrammarId::Tsx as usize + 1;
|
|
|
41
41
|
impl LanguageDefinition {
|
|
42
42
|
pub fn grammar(&self) -> Language {
|
|
43
43
|
match self.grammar_id {
|
|
44
|
-
GrammarId::C => tree_sitter_c::
|
|
45
|
-
GrammarId::Cpp => tree_sitter_cpp::
|
|
46
|
-
GrammarId::CSharp => tree_sitter_c_sharp::
|
|
47
|
-
GrammarId::Go => tree_sitter_go::
|
|
48
|
-
GrammarId::Java => tree_sitter_java::
|
|
44
|
+
GrammarId::C => tree_sitter_c::LANGUAGE.into(),
|
|
45
|
+
GrammarId::Cpp => tree_sitter_cpp::LANGUAGE.into(),
|
|
46
|
+
GrammarId::CSharp => tree_sitter_c_sharp::LANGUAGE.into(),
|
|
47
|
+
GrammarId::Go => tree_sitter_go::LANGUAGE.into(),
|
|
48
|
+
GrammarId::Java => tree_sitter_java::LANGUAGE.into(),
|
|
49
49
|
GrammarId::JavaScript => tree_sitter_javascript::LANGUAGE.into(),
|
|
50
50
|
GrammarId::Kotlin => tree_sitter_kotlin::LANGUAGE.into(),
|
|
51
|
-
GrammarId::Python => tree_sitter_python::
|
|
52
|
-
GrammarId::Ruby => tree_sitter_ruby::
|
|
53
|
-
GrammarId::Rust => tree_sitter_rust::
|
|
54
|
-
GrammarId::TypeScript => tree_sitter_typescript::
|
|
55
|
-
GrammarId::Tsx => tree_sitter_typescript::
|
|
51
|
+
GrammarId::Python => tree_sitter_python::LANGUAGE.into(),
|
|
52
|
+
GrammarId::Ruby => tree_sitter_ruby::LANGUAGE.into(),
|
|
53
|
+
GrammarId::Rust => tree_sitter_rust::LANGUAGE.into(),
|
|
54
|
+
GrammarId::TypeScript => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
|
|
55
|
+
GrammarId::Tsx => tree_sitter_typescript::LANGUAGE_TSX.into(),
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/// The grammar's node kind names, indexed by kind id.
|
|
60
|
-
pub fn kind_names(&self) -> &'static [
|
|
61
|
-
static KIND_NAMES: [OnceLock<Vec
|
|
60
|
+
pub fn kind_names(&self) -> &'static [String] {
|
|
61
|
+
static KIND_NAMES: [OnceLock<Vec<String>>; GRAMMAR_COUNT] =
|
|
62
62
|
[const { OnceLock::new() }; GRAMMAR_COUNT];
|
|
63
63
|
KIND_NAMES[self.grammar_id as usize].get_or_init(|| {
|
|
64
64
|
let grammar = self.grammar();
|
|
@@ -68,6 +68,7 @@ impl LanguageDefinition {
|
|
|
68
68
|
.ok()
|
|
69
69
|
.and_then(|id| grammar.node_kind_for_id(id))
|
|
70
70
|
.unwrap_or_default()
|
|
71
|
+
.to_string()
|
|
71
72
|
})
|
|
72
73
|
.collect()
|
|
73
74
|
})
|
|
@@ -361,7 +362,6 @@ const PYTHON_NCSS_NODES: &[&str] = &[
|
|
|
361
362
|
"for_statement",
|
|
362
363
|
"while_statement",
|
|
363
364
|
"except_clause",
|
|
364
|
-
"except_group_clause",
|
|
365
365
|
"finally_clause",
|
|
366
366
|
"with_statement",
|
|
367
367
|
"match_statement",
|
|
@@ -595,6 +595,7 @@ const CSHARP_NCSS_NODES: &[&str] = &[
|
|
|
595
595
|
"interface_declaration",
|
|
596
596
|
"enum_declaration",
|
|
597
597
|
"record_declaration",
|
|
598
|
+
"extension_declaration",
|
|
598
599
|
"delegate_declaration",
|
|
599
600
|
"field_declaration",
|
|
600
601
|
"event_field_declaration",
|
package/native/src/measure.rs
CHANGED
|
@@ -16,7 +16,9 @@ use crate::tree_index::{NodeExt, TreeIndex};
|
|
|
16
16
|
use crate::types::{
|
|
17
17
|
CrossFileFileData, FunctionMetrics, HalsteadCounts, LineMetrics, NativeMetrics,
|
|
18
18
|
};
|
|
19
|
-
use crate::util::{
|
|
19
|
+
use crate::util::{
|
|
20
|
+
all_children, is_js_whitespace, is_token, named_children, node_text, split_lines, Source,
|
|
21
|
+
};
|
|
20
22
|
|
|
21
23
|
pub fn measure(
|
|
22
24
|
code: &str,
|
|
@@ -274,11 +276,11 @@ fn collect_token_symbols(
|
|
|
274
276
|
) {
|
|
275
277
|
return;
|
|
276
278
|
}
|
|
277
|
-
if
|
|
279
|
+
if is_atomic_operand(node) {
|
|
278
280
|
symbols.push(hash_text(node.kind_name()));
|
|
279
281
|
return;
|
|
280
282
|
}
|
|
281
|
-
if node
|
|
283
|
+
if !is_token(node) {
|
|
282
284
|
for child in all_children(node) {
|
|
283
285
|
collect_token_symbols(child, code, symbols, id_index_by_name);
|
|
284
286
|
}
|
|
@@ -584,8 +586,6 @@ const OPERAND_NODE_TYPES: &[&str] = &[
|
|
|
584
586
|
"hex_floating_point_literal",
|
|
585
587
|
"string",
|
|
586
588
|
"string_literal",
|
|
587
|
-
// Go raw strings are leaves with no content child, unlike Rust/C++ `raw_string_literal`s.
|
|
588
|
-
"raw_string_literal",
|
|
589
589
|
"verbatim_string_literal",
|
|
590
590
|
"string_fragment",
|
|
591
591
|
"multiline_string_fragment",
|
|
@@ -638,7 +638,7 @@ fn measure_halstead(root: Node<'_>, code: &Source<'_>) -> HalsteadCounts {
|
|
|
638
638
|
let mut operands: FxHashMap<&str, u64> = FxHashMap::default();
|
|
639
639
|
|
|
640
640
|
fn visit<'a>(
|
|
641
|
-
node: Node<'
|
|
641
|
+
node: Node<'a>,
|
|
642
642
|
code: &Source<'a>,
|
|
643
643
|
operators: &mut FxHashMap<&'a str, u64>,
|
|
644
644
|
operands: &mut FxHashMap<&'a str, u64>,
|
|
@@ -650,14 +650,14 @@ fn measure_halstead(root: Node<'_>, code: &Source<'_>) -> HalsteadCounts {
|
|
|
650
650
|
return;
|
|
651
651
|
}
|
|
652
652
|
|
|
653
|
-
if
|
|
653
|
+
if is_atomic_operand(node) {
|
|
654
654
|
*operands.entry(node_text(node, code)).or_insert(0) += 1;
|
|
655
655
|
return;
|
|
656
656
|
}
|
|
657
657
|
|
|
658
658
|
// Operators are counted from leaf tokens only: keyword-named nodes always contain a
|
|
659
659
|
// same-text anonymous keyword leaf, so counting the named node as well would double-count.
|
|
660
|
-
if node
|
|
660
|
+
if is_token(node) {
|
|
661
661
|
let text = node_text(node, code);
|
|
662
662
|
// Operands win over text matches so identifiers spelled like word operators stay operands;
|
|
663
663
|
// C# `nameof(x)` is the one keyword operator the grammar parses as a plain callee.
|
|
@@ -694,6 +694,14 @@ fn measure_halstead(root: Node<'_>, code: &Source<'_>) -> HalsteadCounts {
|
|
|
694
694
|
}
|
|
695
695
|
}
|
|
696
696
|
|
|
697
|
+
/// A literal counted as one operand without descending: ATOMIC_OPERAND_NODE_TYPES, or a Go raw
|
|
698
|
+
/// string, which wraps its content like Go's interpreted strings (the Rust and C++
|
|
699
|
+
/// `raw_string_literal`s count their content leaves instead).
|
|
700
|
+
fn is_atomic_operand(node: Node<'_>) -> bool {
|
|
701
|
+
atomic_operand_node_types().contains(node.kind_name())
|
|
702
|
+
|| (node.kind_name() == "raw_string_literal" && crate::tree_index::language_name() == "go")
|
|
703
|
+
}
|
|
704
|
+
|
|
697
705
|
/// tree-sitter-c-sharp parses `nameof(x)` as an invocation of an identifier named `nameof`.
|
|
698
706
|
fn is_csharp_nameof_callee(node: Node<'_>, text: &str) -> bool {
|
|
699
707
|
text == "nameof"
|
package/native/src/tree_index.rs
CHANGED
|
@@ -11,7 +11,7 @@ thread_local! {
|
|
|
11
11
|
/// nodes; TreeIndex borrows the tree and clears the map on drop, so no entry outlives it.
|
|
12
12
|
static PARENTS: RefCell<FxHashMap<usize, Node<'static>>> = RefCell::new(FxHashMap::default());
|
|
13
13
|
/// Node kind names of the indexed tree's language, indexed by kind id.
|
|
14
|
-
static KIND_NAMES: Cell<&'static [
|
|
14
|
+
static KIND_NAMES: Cell<&'static [String]> = const { Cell::new(&[]) };
|
|
15
15
|
/// Name of the indexed tree's language.
|
|
16
16
|
static LANGUAGE_NAME: Cell<&'static str> = const { Cell::new("") };
|
|
17
17
|
}
|
|
@@ -87,7 +87,7 @@ pub trait NodeExt<'t> {
|
|
|
87
87
|
/// `Node::parent`, answered from the TreeIndex when one is installed.
|
|
88
88
|
fn parent_node(self) -> Option<Node<'t>>;
|
|
89
89
|
/// `Node::kind`, answered from the TreeIndex when one is installed.
|
|
90
|
-
fn kind_name(self) -> &'
|
|
90
|
+
fn kind_name(self) -> &'t str;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
impl<'t> NodeExt<'t> for Node<'t> {
|
|
@@ -109,11 +109,11 @@ impl<'t> NodeExt<'t> for Node<'t> {
|
|
|
109
109
|
.or_else(|| self.parent())
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
fn kind_name(self) -> &'
|
|
112
|
+
fn kind_name(self) -> &'t str {
|
|
113
113
|
KIND_NAMES
|
|
114
114
|
.get()
|
|
115
115
|
.get(usize::from(self.kind_id()))
|
|
116
|
-
.
|
|
116
|
+
.map(String::as_str)
|
|
117
117
|
.unwrap_or_else(|| self.kind())
|
|
118
118
|
}
|
|
119
119
|
}
|
package/native/src/util.rs
CHANGED
|
@@ -61,6 +61,13 @@ pub fn is_kotlin_keyword_literal(node: Node<'_>, code: &Source<'_>) -> bool {
|
|
|
61
61
|
&& matches!(node_text(node, code), "true" | "false" | "null")
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/// Whether the node stands for one source token. C# spells a contextual keyword used as a name
|
|
65
|
+
/// (`type`, `field`, `alias`) as an `identifier` or `implicit_parameter` wrapping the keyword token,
|
|
66
|
+
/// which still names a variable like any other identifier.
|
|
67
|
+
pub fn is_token(node: Node<'_>) -> bool {
|
|
68
|
+
node.child_count() == 0 || matches!(node.kind_name(), "identifier" | "implicit_parameter")
|
|
69
|
+
}
|
|
70
|
+
|
|
64
71
|
pub fn named_children<'t>(node: Node<'t>) -> Vec<Node<'t>> {
|
|
65
72
|
let mut cursor = node.walk();
|
|
66
73
|
node.named_children(&mut cursor).collect()
|
|
@@ -77,13 +84,13 @@ pub fn all_children<'t>(node: Node<'t>) -> Vec<Node<'t>> {
|
|
|
77
84
|
/// them), which would desynchronize field-based extraction on malformed source.
|
|
78
85
|
pub fn find_children_by_field_name<'t>(node: Node<'t>, field_name: &str) -> Vec<Node<'t>> {
|
|
79
86
|
let mut children = Vec::new();
|
|
80
|
-
let mut preceding_structural_field
|
|
87
|
+
let mut preceding_structural_field = None;
|
|
81
88
|
for index in 0..node.child_count() {
|
|
82
89
|
if let Some(child) = node.child(index) {
|
|
83
90
|
let field = if child.is_extra() {
|
|
84
91
|
preceding_structural_field
|
|
85
92
|
} else {
|
|
86
|
-
let field = node.field_name_for_child(index
|
|
93
|
+
let field = node.field_name_for_child(index);
|
|
87
94
|
preceding_structural_field = field;
|
|
88
95
|
field
|
|
89
96
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-gauge",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "Measure code metrics with tree-sitter.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -56,12 +56,13 @@
|
|
|
56
56
|
"lint-fix": "bun wb lint --fix",
|
|
57
57
|
"prepare": "(bun lefthook install || true) && (test ! -f .lefthook/normalize-bun-lockfile.sh || bash .lefthook/normalize-bun-lockfile.sh)",
|
|
58
58
|
"test": "bun wb test",
|
|
59
|
-
"test/ci": "bun
|
|
59
|
+
"test/ci": "bun wb test-on-ci",
|
|
60
60
|
"typecheck": "bun wb typecheck",
|
|
61
61
|
"verify": "bun wb verify",
|
|
62
62
|
"verify-full": "bun wb verify --full"
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
+
"@willbooster/shared-lib": "10.18.0",
|
|
65
66
|
"commander": "15.0.0"
|
|
66
67
|
},
|
|
67
68
|
"devDependencies": {
|
|
@@ -94,12 +95,12 @@
|
|
|
94
95
|
"registry": "https://registry.npmjs.org/"
|
|
95
96
|
},
|
|
96
97
|
"optionalDependencies": {
|
|
97
|
-
"code-gauge-linux-x64-gnu": "
|
|
98
|
-
"code-gauge-linux-arm64-gnu": "
|
|
99
|
-
"code-gauge-linux-x64-musl": "
|
|
100
|
-
"code-gauge-linux-arm64-musl": "
|
|
101
|
-
"code-gauge-darwin-x64": "
|
|
102
|
-
"code-gauge-darwin-arm64": "
|
|
103
|
-
"code-gauge-win32-x64-msvc": "
|
|
98
|
+
"code-gauge-linux-x64-gnu": "6.0.0",
|
|
99
|
+
"code-gauge-linux-arm64-gnu": "6.0.0",
|
|
100
|
+
"code-gauge-linux-x64-musl": "6.0.0",
|
|
101
|
+
"code-gauge-linux-arm64-musl": "6.0.0",
|
|
102
|
+
"code-gauge-darwin-x64": "6.0.0",
|
|
103
|
+
"code-gauge-darwin-arm64": "6.0.0",
|
|
104
|
+
"code-gauge-win32-x64-msvc": "6.0.0"
|
|
104
105
|
}
|
|
105
106
|
}
|
package/dist/diffCommand.cjs
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
"use strict";const e=require("./_virtual/_rolldown/runtime.cjs"),t=require("./crossFileDuplication.cjs"),n=require("./metrics.cjs"),r=require("./newDuplication.cjs"),i=require("./regressionGate.cjs"),a=require("./cliConfig.cjs"),o=require("./git.cjs"),s=require("./exclusion.cjs"),c=require("./scan.cjs");let l=require("node:fs/promises"),u=require("node:path");u=e.__toESM(u,1);let d=require("node:os");d=e.__toESM(d,1);async function f(e,t){try{await p(e,t)}catch(e){c.writeStderr(`Error: ${c.formatError(e)}\n`),process.exitCode=2}}async function p(e,t){let n=c.resolveTarget(e),d=await a.loadConfig(t.config,await c.configSearchDirectory(n)),f=a.resolveOptions(t,d),p=a.resolveGateOptions(d),m=await(0,l.realpath)(await o.resolveRepoRoot(await O(await c.configSearchDirectory(n)))),y=await o.resolveMergeBase(m,t.base),b=await o.listChangedFiles(m,y),x=await o.listRepositoryFiles(m),S=await o.listSymlinkPathsAtRevision(m,y),{canonicalTarget:C,targetExists:w}=await _(n),T=(await(0,l.stat)(C).catch(()=>{}))?.isFile()?C:void 0,E=T&&u.default.relative(m,T).split(u.default.sep).join(`/`);E!==void 0&&!E.startsWith(`../`)&&!x.has(E)&&(x.add(E),b.push({status:`added`,headPath:E}));let D=g(T,m,b),k=b.some(e=>[e.headPath,e.basePath].some(e=>e!==void 0&&u.default.posix.basename(e)===`.gitattributes`)),N=b.flatMap(e=>e.basePath===void 0?[]:[e.basePath]),[P,F]=await Promise.all([s.loadRepositoryExclusion(m,x,f.exclude),s.loadRepositoryExclusion(m,k?[.../* @__PURE__ */ new Set([...N,...x])]:N,f.exclude,y)]),L=s.keepPaths(P,D),z=s.keepPaths(F,D),B=await c.scanListedFiles(m,x,{...f,loadExclusion:()=>Promise.resolve(L)},D);if(B.fatalError)throw Error(B.fatalError);let V=B.files.map(e=>({relativePath:c.formatPath(e.file,B.displayRoot),file:e}));k&&b.push(...await h(V,b,m,L,z));let U=new Set(b.flatMap(e=>[e.headPath,...e.basePath===void 0?[]:[e.basePath]]).filter(e=>c.isScannedPath(e,f,D.has(u.default.join(m,e))))),W=[],G=[...B.warnings];for(let e of B.errors)[...U].some(t=>e.startsWith(`${t}:`))?W.push(e):G.push(e);let K=await v(b,{repoRoot:m,mergeBase:y,canonicalTarget:C,options:f,scannedFiles:V,baseSymlinkPaths:S,scanErrors:[...W],headExclusion:L,baseExclusion:z,explicitFiles:D,generatedHeadFiles:new Set(B.generatedFiles)},W,G);if(!w&&!K.some(e=>e.gated))throw Error(`target "${e}" does not exist and matches no changed file`);let{baseCross:q,headCross:J}=A(K,V,f),Y=new Map(K.flatMap(({changed:e})=>e.status===`renamed`&&e.basePath!==void 0?[[e.basePath,e.headPath]]:[])),X=e=>Y.get(e)??e,Z=M(V),ee=r.findNewlyDuplicatedLines(K.map(e=>j(e,q,J,X,Z))),Q=K.map((e,t)=>I(e,ee[t]??[],J)),$=i.evaluateRegressionGate(Q,p);t.json?H(t,y,$,Q,W,G):R(t,y,$,W,G),W.length>0?process.exitCode=2:$.violations.length>0&&(process.exitCode=1)}const m=`/* @generated */`;async function h(e,t,n,r,i){let a=new Set(t.flatMap(e=>[e.headPath,e.basePath])),o=[];for(let{relativePath:t,file:s}of e){let e=u.default.join(n,t);if(a.has(t))continue;let c=i.isGeneratedCode(e,m)&&!r.isGeneratedCode(e,m);(i.isExcludedPath(e)||c&&i.isGeneratedCode(e,await(0,l.readFile)(s.file,`utf8`)))&&o.push({status:`modified`,basePath:t,headPath:t})}return o}function g(e,t,n){if(e===void 0)return/* @__PURE__ */ new Set;let r=/* @__PURE__ */ new Set([e]);for(let i of n)i.basePath!==void 0&&u.default.join(t,i.headPath)===e&&r.add(u.default.join(t,i.basePath));return r}async function _(e){try{return{canonicalTarget:await(0,l.realpath)(e),targetExists:!0}}catch{return{canonicalTarget:e,targetExists:!1}}}async function v(e,t,n,r){let i=new Map(t.scannedFiles.map(({relativePath:e,file:t})=>[e,t])),a=await y(e,d.default.availableParallelism()*2,async e=>{let n=[],r=[];return{file:await b(e,t,i,n,r),fileErrors:n,fileWarnings:r}}),o=[];for(let{file:e,fileErrors:t,fileWarnings:i}of a)n.push(...t),r.push(...i),e&&o.push(e);return o}async function y(e,t,n){let r=[],i=0;return await Promise.all(Array.from({length:Math.min(t,e.length)},async()=>{for(;i<e.length;){let t=i;i+=1,r[t]=await n(e[t])}})),r}async function b(e,t,n,r,i){let a=u.default.join(t.repoRoot,e.headPath),o=e.status!==`deleted`&&c.isScannedPath(e.headPath,t.options,t.explicitFiles.has(a))&&!t.headExclusion.isExcludedPath(a)&&!t.generatedHeadFiles.has(a)&&!await D(a),s=e.basePath===void 0?``:u.default.join(t.repoRoot,e.basePath),d=e.basePath!==void 0&&c.isScannedPath(e.basePath,t.options,t.explicitFiles.has(s))&&!t.baseExclusion.isExcludedPath(s)&&!t.baseSymlinkPaths.has(e.basePath);if(!o&&!d)return;let f=e.status===`deleted`?e.basePath:e.headPath,p=o?n.get(e.headPath):void 0;if(o&&!p){C(e.headPath,t.scanErrors,r);return}let m={changed:e,displayFile:f,gated:o||e.status===`deleted`?k(u.default.join(t.repoRoot,f),t.canonicalTarget):!1,headFile:p};if(d){let n=await T(m,e.basePath,t,r,i);if(n===`failed`||n===`generated`&&!p)return}try{p&&(m.headContent=await(0,l.readFile)(p.file,`utf8`),w(m,m.headContent,t,i)),m.hunks=await x(m,t)}catch(e){r.push(`${f}: ${c.formatError(e)}`);return}return m}async function x(e,t){let{changed:n,baseContent:r,headContent:i}=e;return r!==void 0&&i!==void 0?await o.listLineHunks(t.repoRoot,t.mergeBase,n.basePath,n.headPath):[{baseStart:1,baseCount:r===void 0?0:S(r).length,headStart:1,headCount:i===void 0?0:S(i).length}]}function S(e){return e.split(/\r\n|\n|\r/u)}function C(e,t,n){t.some(t=>t.startsWith(`${e}:`))||n.push(`${e}: changed file was not measured`)}function w(e,t,r,i){try{e.headFunctionTokens=n.collectFunctionTokenSequences(t,{language:E(e.changed.headPath,r),duplication:r.options.duplication})}catch(t){i.push(`${e.displayFile}: function token sequences unavailable: ${c.formatError(t)}`)}}async function T(e,t,r,i,a){let s={language:E(t,r),duplication:r.options.duplication},l;try{if(l=await o.readFileAtRevision(r.repoRoot,r.mergeBase,t),r.baseExclusion.isGeneratedCode(u.default.join(r.repoRoot,t),l))return`generated`;let n=await c.measureWithCrossFileData(l,s);e.baseMetrics=n.metrics,e.baseContent=l,e.baseCandidates=n.crossFileData,n.crossFileError!==void 0&&a.push(`${t} (at merge-base): duplication candidates unavailable: ${n.crossFileError}`)}catch(e){return i.push(`${t} (at merge-base): ${c.formatError(e)}`),`failed`}try{e.baseFunctionTokens=n.collectFunctionTokenSequences(l,s)}catch(e){a.push(`${t} (at merge-base): function token sequences unavailable: ${c.formatError(e)}`)}return`measured`}function E(e,t){let n=t.explicitFiles.has(u.default.join(t.repoRoot,e));return c.getLanguage(e,t.options,n)}async function D(e){return(await(0,l.lstat)(e).catch(()=>{}))?.isSymbolicLink()??!1}async function O(e){let t=e;for(;;){if((await(0,l.stat)(t).catch(()=>{}))?.isDirectory())return t;let e=u.default.dirname(t);if(e===t)return t;t=e}}function k(e,t){let n=u.default.relative(t,e);return n===``||!n.startsWith(`..${u.default.sep}`)&&n!==`..`&&!u.default.isAbsolute(n)}function A(e,n,r){let i=n.flatMap(({relativePath:e,file:t})=>t.duplicationCandidates?[{file:e,...t.duplicationCandidates}]:[]),a=new Set(e.flatMap(e=>e.changed.status===`deleted`?[]:[e.changed.headPath])),o=i.filter(e=>!a.has(e.file));for(let t of e)t.baseCandidates&&t.changed.basePath!==void 0&&o.push({file:t.changed.basePath,...t.baseCandidates});return{baseCross:o.length>=2?t.measureCrossFileDuplication(o,r.duplication):void 0,headCross:i.length>=2?t.measureCrossFileDuplication(i,r.duplication):void 0}}function j(e,t,n,r,i){let a=F(e.headFile?.metrics,n,e.changed.headPath);return{baseLines:e.baseContent===void 0?void 0:S(e.baseContent),headLines:e.headContent===void 0?void 0:S(e.headContent),baseDuplicatedLines:e.baseMetrics===void 0||e.changed.basePath===void 0?/* @__PURE__ */ new Set:c.collectDuplicatedLineNumbers(e.baseMetrics,t,e.changed.basePath),headDuplicatedLines:e.headFile===void 0?/* @__PURE__ */ new Set:P(c.collectDuplicatedLineNumbers(e.headFile.metrics,n,e.changed.headPath),e.changed.headPath,a,i),baseOccurrences:F(e.baseMetrics,t,e.changed.basePath,r),headOccurrences:a,hunks:e.hunks??[]}}function M(e){let t=new Map(e.map(({relativePath:e,file:t})=>[e,t.duplicationCandidates?.tokens])),n=/* @__PURE__ */ new Map;return e=>{if(!n.has(e)){let r=t.get(e);n.set(e,r&&N(r))}return n.get(e)}}function N(e){let t=/* @__PURE__ */ new Map,n=/* @__PURE__ */ new Map;for(let r of e){let e=r.startRow+1,i=r.text;if(r.kind===`id`){let t=n.get(e)??/* @__PURE__ */ new Map;n.set(e,t),t.has(r.text)||t.set(r.text,t.size),i=`\u0001${t.get(r.text)}`}t.set(e,`${t.get(e)??``}${i}\u0000`)}let r=/* @__PURE__ */ new Map;for(let e of t.values())r.set(e,(r.get(e)??0)+1);return{byLine:t,lineCounts:r}}function P(e,t,n,i){let a=i(t);if(a===void 0)return new Set(e);let o=(e,t)=>{if(t===``)return(a.lineCounts.get(e)??0)>1;let n=i(t);return n===void 0||n.lineCounts.has(e)},s=r.indexPartnersByLine(n);return new Set([...e].filter(e=>{let t=a.byLine.get(e);return t===void 0||[...s.get(e)??[]].some(e=>o(t,e))}))}function F(e,t,n,r=e=>e){if(e===void 0||n===void 0)return[];let i=e.duplication.duplicateBlockGroups.flatMap(e=>e.map(({startLine:e,endLine:t})=>({startLine:e,endLine:t,partners:[``]}))),a=(t?.groups??[]).flatMap(e=>{let t=e.occurrences.filter(e=>e.file===n),i=[...e.files.filter(e=>e!==n).map(r),...t.length>1?[``]:[]];return t.map(({startLine:e,endLine:t})=>({startLine:e,endLine:t,partners:i}))});return[...i,...a]}function I(e,t,n){return{file:e.displayFile,baseMetrics:e.baseMetrics,headMetrics:e.headFile?.metrics,baseFunctionTokens:e.baseFunctionTokens,headFunctionTokens:e.headFunctionTokens,newlyDuplicatedLines:t,duplicationPartners:L(e,t,n),gated:e.gated}}function L(e,t,n){let r=e.changed.headPath,i=e=>e.file===r&&t.some(t=>t>=e.startLine&&t<=e.endLine),a=/* @__PURE__ */ new Set;for(let e of n?.groups??[])if(e.occurrences.some(i))for(let t of e.files)t!==r&&a.add(t);return[...a].toSorted()}function R(e,t,n,r,i){let a=t.slice(0,12);r.length>0?(c.writeStdout(`Regression gate could not complete: ${r.length} measurement failures (details on stderr)${n.violations.length>0?`; ${n.violations.length} violations in the measured files`:``} (base ${e.base}, merge-base ${a}).\n`),z(n)):n.violations.length===0?c.writeStdout(`Regression gate passed: ${n.checkedFileCount} changed files, ${n.checkedFunctionCount} functions checked (base ${e.base}, merge-base ${a}).\n`):(c.writeStdout(`Regression gate vs ${e.base} (merge-base ${a}): ${n.violations.length} violations\n`),z(n)),e.full&&B(n);for(let e of i)c.writeStderr(`Warning: ${e}\n`);for(let e of r)c.writeStderr(`Error: ${e}\n`)}function z(e){for(let[t,n]of e.violations.entries())c.writeStdout(`${t+1}. ${n.message}\n`)}function B(e){if(e.checkedFunctions.length!==0){c.writeStdout(`
|
|
2
|
-
Checked functions (base -> head):
|
|
3
|
-
`);for(let t of e.checkedFunctions)c.writeStdout(`- ${V(t)}\n`)}}function V(e){let t=(t,n=String)=>{let r=n(t(e.head));return e.base?`${n(t(e.base))} -> ${r}`:r},n=[`cognitive ${t(e=>e.cognitiveComplexity)}`,`NCSS ${t(e=>e.ncss)}`,`nesting ${t(e=>e.nestingDepth)}`,`DepDegree ${t(e=>e.depDegree)}`,`volume ${t(e=>e.halsteadVolume,e=>e.toFixed(1))}`];return`${e.file}:${e.startLine}-${e.endLine} ${e.name}${e.base?``:` (new)`}: ${n.join(`, `)}`}function H(e,t,n,r,i,a){let o={base:e.base,mergeBase:t,passed:n.violations.length===0&&i.length===0,violations:n.violations,checkedFileCount:n.checkedFileCount,checkedFunctionCount:n.checkedFunctionCount,newFunctionCount:n.newFunctionCount,errors:i,warnings:a};e.full&&(o.files=r.filter(e=>e.gated!==!1).map(e=>({file:e.file,baseFunctionCount:e.baseMetrics?.functions.length??0,headFunctionCount:e.headMetrics?.functions.length??0,baseNcss:e.baseMetrics?.ncssCount??0,headNcss:e.headMetrics?.ncssCount??0,baseMaxCognitiveComplexity:e.baseMetrics?.maxCognitiveComplexity??0,headMaxCognitiveComplexity:e.headMetrics?.maxCognitiveComplexity??0,newlyDuplicatedLines:e.newlyDuplicatedLines,duplicationPartners:e.duplicationPartners,functions:n.checkedFunctions.filter(t=>t.file===e.file)}))),c.writeStdout(JSON.stringify(o,void 0,2)+`
|
|
4
|
-
`)}exports.runDiffCommand=f;
|
|
5
|
-
//# sourceMappingURL=diffCommand.cjs.map
|
package/dist/diffCommand.cjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"diffCommand.cjs","names":["formatError","resolveTarget","loadConfig","configSearchDirectory","resolveOptions","resolveGateOptions","realpath","resolveRepoRoot","resolveMergeBase","listChangedFiles","listRepositoryFiles","listSymlinkPathsAtRevision","stat","path","loadRepositoryExclusion","keepPaths","scanListedFiles","formatPath","isScannedPath","findNewlyDuplicatedLines","evaluateRegressionGate","readFile","os","listLineHunks","collectFunctionTokenSequences","readFileAtRevision","measureWithCrossFileData","getLanguage","lstat","measureCrossFileDuplication","collectDuplicatedLineNumbers","indexPartnersByLine"],"sources":["../src/diffCommand.ts"],"sourcesContent":["import { lstat, readFile, realpath, stat } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { loadConfig, resolveGateOptions, resolveOptions, type ResolvedOptions } from './cliConfig.js';\nimport {\n measureCrossFileDuplication,\n type CrossFileDuplicateOccurrence,\n type CrossFileDuplicationMetrics,\n} from './crossFileDuplication.js';\nimport type { CrossFileDuplicationFileData, Token } from './duplication.js';\nimport { keepPaths, loadRepositoryExclusion, type Exclusion } from './exclusion.js';\nimport {\n listChangedFiles,\n listLineHunks,\n listRepositoryFiles,\n listSymlinkPathsAtRevision,\n readFileAtRevision,\n resolveMergeBase,\n resolveRepoRoot,\n type ChangedFile,\n} from './git.js';\nimport { collectFunctionTokenSequences } from './metrics.js';\nimport {\n findNewlyDuplicatedLines,\n indexPartnersByLine,\n type CloneOccurrence,\n type DuplicationChange,\n type LineHunk,\n} from './newDuplication.js';\nimport {\n evaluateRegressionGate,\n type CheckedFunctionReport,\n type GateFileInput,\n type GateFunctionValues,\n type GateResult,\n} from './regressionGate.js';\nimport {\n collectDuplicatedLineNumbers,\n configSearchDirectory,\n formatError,\n formatPath,\n getLanguage,\n isScannedPath,\n measureWithCrossFileData,\n resolveTarget,\n scanListedFiles,\n writeStderr,\n writeStdout,\n type FileMetrics,\n} from './scan.js';\nimport type { CodeMetrics, LanguageName } from './types.js';\n\n/** Raw options of the `diff` subcommand; every field but base is undefined unless the flag was passed. */\nexport interface DiffCliOptions {\n base: string;\n config?: string;\n duplicationMinTokens?: number;\n duplicationMaxGapTokens?: number;\n duplicationMinSimilarityPercent?: number;\n includeTests?: boolean;\n json?: boolean;\n full?: boolean;\n}\n\n/** One changed file measured at both revisions, plus its duplication-universe contribution. */\ninterface PreparedFile {\n changed: ChangedFile;\n /** Repository-relative display path: the head path, or the base path for deleted files. */\n displayFile: string;\n /** Whether the file is gated (under the target directory); others only feed the base universe. */\n gated: boolean;\n headFile?: FileMetrics;\n headContent?: string;\n baseMetrics?: CodeMetrics;\n baseContent?: string;\n /**\n * The line diff from the measured base to the measured head; with one side unmeasured, every\n * line of the other counts as added or deleted.\n */\n hunks?: LineHunk[];\n baseCandidates?: CrossFileDuplicationFileData;\n baseFunctionTokens?: Int32Array[];\n headFunctionTokens?: Int32Array[];\n}\n\n/** A scanned file that git considers part of the project, keyed by its repository-relative path. */\ninterface ScannedFile {\n relativePath: string;\n file: FileMetrics;\n}\n\n/**\n * Runs the regression gate: measures the files changed relative to the merge-base with the base\n * ref, at both revisions (`git cat-file`; no checkout, no persisted baseline), and reports only\n * violations. Exit codes: 0 all gates passed, 1 violations, 2 changed files could not be measured.\n */\nexport async function runDiffCommand(target: string, cliOptions: DiffCliOptions): Promise<void> {\n try {\n await runGate(target, cliOptions);\n } catch (error) {\n writeStderr(`Error: ${formatError(error)}\\n`);\n process.exitCode = 2;\n }\n}\n\nasync function runGate(target: string, cliOptions: DiffCliOptions): Promise<void> {\n const resolvedTarget = resolveTarget(target);\n const config = await loadConfig(cliOptions.config, await configSearchDirectory(resolvedTarget));\n const options = resolveOptions(cliOptions, config);\n const gateOptions = resolveGateOptions(config);\n\n // The target may be a typo'd path whose ancestors don't exist either; repository discovery must\n // still run so the mistyped target gets its own diagnostic instead of a git spawn failure.\n const repoRoot = await realpath(\n await resolveRepoRoot(await firstExistingDirectory(await configSearchDirectory(resolvedTarget)))\n );\n const mergeBase = await resolveMergeBase(repoRoot, cliOptions.base);\n const changedFiles = await listChangedFiles(repoRoot, mergeBase);\n\n // Every git-visible file (tracked or untracked non-ignored) is measured at head: that provides\n // the head metrics of changed files and the project-wide duplication universe, so copy-paste\n // from unchanged code into changed files is caught. Scanning the explicit git list (instead of\n // walking the tree) keeps ignored artifact directories from ever being parsed: they exist in\n // neither the base commit nor CI, so they would only cost time and skew duplication counts.\n // Unchanged files are byte-identical at both revisions, so the base universe is the same scan\n // with the changed files' contents swapped for their merge-base blobs.\n const repositoryFiles = await listRepositoryFiles(repoRoot);\n const baseSymlinkPaths = await listSymlinkPathsAtRevision(repoRoot, mergeBase);\n const { canonicalTarget, targetExists } = await canonicalizeTarget(resolvedTarget);\n const targetStat = await stat(canonicalTarget).catch(() => {});\n const targetFile = targetStat?.isFile() ? canonicalTarget : undefined;\n // A targeted file git does not list (an ignored one) is still measured, as an addition.\n const unlistedTarget = targetFile && path.relative(repoRoot, targetFile).split(path.sep).join('/');\n if (unlistedTarget !== undefined && !unlistedTarget.startsWith('../') && !repositoryFiles.has(unlistedTarget)) {\n repositoryFiles.add(unlistedTarget);\n changedFiles.push({ status: 'added', headPath: unlistedTarget });\n }\n const explicitFiles = listExplicitlyTargetedPaths(targetFile, repoRoot, changedFiles);\n // Base blobs are excluded by the attributes of the revision they come from. A changed\n // .gitattributes can re-include files whose bytes did not change, so then every file's base\n // attributes are needed to find them.\n const attributesChanged = changedFiles.some((changed) =>\n [changed.headPath, changed.basePath].some(\n (file) => file !== undefined && path.posix.basename(file) === '.gitattributes'\n )\n );\n const basePaths = changedFiles.flatMap((changed) => (changed.basePath === undefined ? [] : [changed.basePath]));\n const [headAttributesExclusion, baseAttributesExclusion] = await Promise.all([\n loadRepositoryExclusion(repoRoot, repositoryFiles, options.exclude),\n loadRepositoryExclusion(\n repoRoot,\n attributesChanged ? [...new Set([...basePaths, ...repositoryFiles])] : basePaths,\n options.exclude,\n mergeBase\n ),\n ]);\n const headExclusion = keepPaths(headAttributesExclusion, explicitFiles);\n const baseExclusion = keepPaths(baseAttributesExclusion, explicitFiles);\n const scan = await scanListedFiles(\n repoRoot,\n repositoryFiles,\n { ...options, loadExclusion: () => Promise.resolve(headExclusion) },\n explicitFiles\n );\n // A run-wide failure (a missing native addon) invalidates the whole gate: surface it once as\n // the fatal error (exit 2) instead of diagnosing every changed file as unmeasured.\n if (scan.fatalError) {\n throw new Error(scan.fatalError);\n }\n const scannedFiles: ScannedFile[] = scan.files.map((file) => ({\n relativePath: formatPath(file.file, scan.displayRoot),\n file,\n }));\n if (attributesChanged) {\n changedFiles.push(\n ...(await listReincludedFiles(scannedFiles, changedFiles, repoRoot, headExclusion, baseExclusion))\n );\n }\n\n // A measurement failure on ANY scannable changed file forces exit 2 — deliberately including\n // files outside a scoped target, because cross-file function matching and the base duplication\n // universe for the gated files depend on them. Failures elsewhere (unchanged files) and\n // unsupported changed paths degrade to warnings.\n const changedPaths = new Set(\n changedFiles\n .flatMap((changed) => [changed.headPath, ...(changed.basePath === undefined ? [] : [changed.basePath])])\n .filter((changedPath) => isScannedPath(changedPath, options, explicitFiles.has(path.join(repoRoot, changedPath))))\n );\n const errors: string[] = [];\n const warnings = [...scan.warnings];\n for (const error of scan.errors) {\n if ([...changedPaths].some((changedPath) => error.startsWith(`${changedPath}:`))) {\n errors.push(error);\n } else {\n warnings.push(error);\n }\n }\n\n const prepared = await prepareChangedFiles(\n changedFiles,\n {\n repoRoot,\n mergeBase,\n canonicalTarget,\n options,\n scannedFiles,\n baseSymlinkPaths,\n scanErrors: [...errors],\n headExclusion,\n baseExclusion,\n explicitFiles,\n generatedHeadFiles: new Set(scan.generatedFiles),\n },\n errors,\n warnings\n );\n // A gate must not fail open on a mistyped target: a nonexistent path is only acceptable when it\n // still matches changed files (e.g. a fully deleted directory).\n if (!targetExists && !prepared.some((file) => file.gated)) {\n throw new Error(`target \"${target}\" does not exist and matches no changed file`);\n }\n\n // Non-gated files (outside the target, or renamed out of scan scope) still feed function\n // matching and the duplication universes; the evaluator reports nothing for them.\n const { baseCross, headCross } = measureDuplicationUniverses(prepared, scannedFiles, options);\n const renamedPaths = new Map(\n prepared.flatMap(({ changed }) =>\n changed.status === 'renamed' && changed.basePath !== undefined\n ? [[changed.basePath, changed.headPath] as const]\n : []\n )\n );\n const headPathOf = (basePath: string): string => renamedPaths.get(basePath) ?? basePath;\n const lineSignaturesOf = createLineSignatureIndex(scannedFiles);\n const newlyDuplicatedLines = findNewlyDuplicatedLines(\n prepared.map((file) => toDuplicationChange(file, baseCross, headCross, headPathOf, lineSignaturesOf))\n );\n const inputs = prepared.map((file, index) => toGateInput(file, newlyDuplicatedLines[index] ?? [], headCross));\n const result = evaluateRegressionGate(inputs, gateOptions);\n\n if (cliOptions.json) {\n printJsonReport(cliOptions, mergeBase, result, inputs, errors, warnings);\n } else {\n printTextReport(cliOptions, mergeBase, result, errors, warnings);\n }\n\n if (errors.length > 0) {\n process.exitCode = 2;\n } else if (result.violations.length > 0) {\n process.exitCode = 1;\n }\n}\n\n/** Content with a generated-code marker, to ask whether an exclusion honors the marker for a path. */\nconst markedCode = '/* @generated */';\n\n/**\n * Unchanged files the head scan measured but the merge-base would not have: excluded by its\n * attributes, or generated by content without the `-linguist-generated` override the working tree\n * adds. Their code is newly measured, so it gates as a modification from an unmeasurable base. The\n * head side comes from the scan, which already applied scope, symbolic-link, and exclusion rules.\n */\nasync function listReincludedFiles(\n scannedFiles: readonly ScannedFile[],\n changedFiles: readonly ChangedFile[],\n repoRoot: string,\n headExclusion: Exclusion,\n baseExclusion: Exclusion\n): Promise<ChangedFile[]> {\n const changedPaths = new Set(changedFiles.flatMap((changed) => [changed.headPath, changed.basePath]));\n const reincluded: ChangedFile[] = [];\n for (const { relativePath, file } of scannedFiles) {\n const absolutePath = path.join(repoRoot, relativePath);\n if (changedPaths.has(relativePath)) {\n continue;\n }\n // The same content is generated at one revision only when its override differs, so the\n // content is read just for files the working tree newly marks -linguist-generated.\n const overrideAdded =\n baseExclusion.isGeneratedCode(absolutePath, markedCode) &&\n !headExclusion.isGeneratedCode(absolutePath, markedCode);\n if (\n baseExclusion.isExcludedPath(absolutePath) ||\n (overrideAdded && baseExclusion.isGeneratedCode(absolutePath, await readFile(file.file, 'utf8')))\n ) {\n reincluded.push({ status: 'modified', basePath: relativePath, headPath: relativePath });\n }\n }\n return reincluded;\n}\n\n/**\n * An explicitly targeted file is measured even when excluded, like the ranking command's single\n * file target: the file itself, and its base path when it was renamed.\n */\nfunction listExplicitlyTargetedPaths(\n targetFile: string | undefined,\n repoRoot: string,\n changedFiles: ChangedFile[]\n): Set<string> {\n if (targetFile === undefined) {\n return new Set();\n }\n const paths = new Set([targetFile]);\n for (const changed of changedFiles) {\n if (changed.basePath !== undefined && path.join(repoRoot, changed.headPath) === targetFile) {\n paths.add(path.join(repoRoot, changed.basePath));\n }\n }\n return paths;\n}\n\n/** The target may not exist (e.g. only deleted files under it); fall back to the resolved path. */\nasync function canonicalizeTarget(resolvedTarget: string): Promise<{ canonicalTarget: string; targetExists: boolean }> {\n try {\n return { canonicalTarget: await realpath(resolvedTarget), targetExists: true };\n } catch {\n return { canonicalTarget: resolvedTarget, targetExists: false };\n }\n}\n\ninterface GateContext {\n repoRoot: string;\n mergeBase: string;\n canonicalTarget: string;\n options: ResolvedOptions;\n scannedFiles: ScannedFile[];\n /** Paths that are symbolic links at the merge-base; like head symlinks, they are not gated. */\n baseSymlinkPaths: Set<string>;\n /** Errors the head scan recorded against changed files. */\n scanErrors: readonly string[];\n headExclusion: Exclusion;\n baseExclusion: Exclusion;\n /** The explicitly targeted file (and its renamed base path), measured despite every exclusion. */\n explicitFiles: ReadonlySet<string>;\n /** Absolute paths the head scan skipped as generated code. */\n generatedHeadFiles: ReadonlySet<string>;\n}\n\nasync function prepareChangedFiles(\n changedFiles: ChangedFile[],\n context: GateContext,\n errors: string[],\n warnings: string[]\n): Promise<PreparedFile[]> {\n const headByPath = new Map(context.scannedFiles.map(({ relativePath, file }) => [relativePath, file]));\n // Files are prepared concurrently (git reads and base measurements); each records its diagnostics\n // on its own, and they are appended in changed-file order so the report stays deterministic.\n const results = await mapConcurrently(changedFiles, os.availableParallelism() * 2, async (changed) => {\n const fileErrors: string[] = [];\n const fileWarnings: string[] = [];\n const file = await prepareChangedFile(changed, context, headByPath, fileErrors, fileWarnings);\n return { file, fileErrors, fileWarnings };\n });\n const prepared: PreparedFile[] = [];\n for (const { file, fileErrors, fileWarnings } of results) {\n errors.push(...fileErrors);\n warnings.push(...fileWarnings);\n if (file) {\n prepared.push(file);\n }\n }\n return prepared;\n}\n\n/** `Promise.all(items.map(map))` with at most `limit` calls pending at once, results in input order. */\nasync function mapConcurrently<T, R>(items: readonly T[], limit: number, map: (item: T) => Promise<R>): Promise<R[]> {\n const results: R[] = [];\n let nextIndex = 0;\n const work = async (): Promise<void> => {\n while (nextIndex < items.length) {\n const index = nextIndex;\n nextIndex += 1;\n results[index] = await map(items[index] as T);\n }\n };\n await Promise.all(Array.from({ length: Math.min(limit, items.length) }, work));\n return results;\n}\n\nasync function prepareChangedFile(\n changed: ChangedFile,\n context: GateContext,\n headByPath: Map<string, FileMetrics>,\n errors: string[],\n warnings: string[]\n): Promise<PreparedFile | undefined> {\n // Symbolic links are skipped on both sides, mirroring scanListedFiles: git stores only the\n // target string, so a symlink blob is not measurable source.\n const absoluteHeadPath = path.join(context.repoRoot, changed.headPath);\n const headScannable =\n changed.status !== 'deleted' &&\n isScannedPath(changed.headPath, context.options, context.explicitFiles.has(absoluteHeadPath)) &&\n !context.headExclusion.isExcludedPath(absoluteHeadPath) &&\n !context.generatedHeadFiles.has(absoluteHeadPath) &&\n !(await isSymbolicLink(absoluteHeadPath));\n // A base path outside the scan scope (renamed from a test/ignored directory, or an unsupported\n // extension) was never measurable code: its content gates as new code instead of ratcheting\n // against a blob the scanner would not have measured.\n const absoluteBasePath = changed.basePath === undefined ? '' : path.join(context.repoRoot, changed.basePath);\n const baseScannable =\n changed.basePath !== undefined &&\n isScannedPath(changed.basePath, context.options, context.explicitFiles.has(absoluteBasePath)) &&\n !context.baseExclusion.isExcludedPath(absoluteBasePath) &&\n !context.baseSymlinkPaths.has(changed.basePath);\n if (!headScannable && !baseScannable) {\n return undefined;\n }\n\n const displayFile = changed.status === 'deleted' ? (changed.basePath as string) : changed.headPath;\n const headFile = headScannable ? headByPath.get(changed.headPath) : undefined;\n if (headScannable && !headFile) {\n reportUnmeasuredChangedFile(changed.headPath, context.scanErrors, errors);\n return undefined;\n }\n\n const file: PreparedFile = {\n changed,\n displayFile,\n // A file whose head left the scan scope still contributes its base functions to matching and\n // its base blob to the base universe, but nothing about it is gated or reported.\n gated:\n headScannable || changed.status === 'deleted'\n ? isWithinTarget(path.join(context.repoRoot, displayFile), context.canonicalTarget)\n : false,\n headFile,\n };\n\n if (baseScannable) {\n const outcome = await measureBaseRevision(file, changed.basePath as string, context, errors, warnings);\n // A base that turns out to be generated code gates its head as new code, like any other base\n // outside the scan scope.\n if (outcome === 'failed' || (outcome === 'generated' && !headFile)) {\n return undefined;\n }\n }\n\n try {\n if (headFile) {\n // The duplication gate needs the head lines, so an unreadable head fails like an unmeasured one.\n file.headContent = await readFile(headFile.file, 'utf8');\n collectHeadFunctionTokens(file, file.headContent, context, warnings);\n }\n file.hunks = await diffMeasuredRevisions(file, context);\n } catch (error) {\n errors.push(`${displayFile}: ${formatError(error)}`);\n return undefined;\n }\n return file;\n}\n\nasync function diffMeasuredRevisions(file: PreparedFile, context: GateContext): Promise<LineHunk[]> {\n const { changed, baseContent, headContent } = file;\n if (baseContent !== undefined && headContent !== undefined) {\n return await listLineHunks(context.repoRoot, context.mergeBase, changed.basePath as string, changed.headPath);\n }\n return [\n {\n baseStart: 1,\n baseCount: baseContent === undefined ? 0 : splitLines(baseContent).length,\n headStart: 1,\n headCount: headContent === undefined ? 0 : splitLines(headContent).length,\n },\n ];\n}\n\n/** Line numbering of the measured metrics (classifyLines splits on every line terminator). */\nfunction splitLines(content: string): string[] {\n return content.split(/\\r\\n|\\n|\\r/u);\n}\n\n/**\n * The scan covers exactly the git-visible list, so a scannable changed path can only be missing\n * after a measurement failure (already recorded as an error) or a silent exclusion (an alias of\n * an already-visited file, or absence from the git list). Failing loudly keeps the gate from\n * passing with the file unchecked.\n */\nfunction reportUnmeasuredChangedFile(headPath: string, scanErrors: readonly string[], errors: string[]): void {\n if (!scanErrors.some((error) => error.startsWith(`${headPath}:`))) {\n errors.push(`${headPath}: changed file was not measured`);\n }\n}\n\nfunction collectHeadFunctionTokens(\n file: PreparedFile,\n headContent: string,\n context: GateContext,\n warnings: string[]\n): void {\n try {\n file.headFunctionTokens = collectFunctionTokenSequences(headContent, {\n language: languageOf(file.changed.headPath, context),\n duplication: context.options.duplication,\n });\n } catch (error) {\n // Only rename re-matching degrades without token sequences; the head metrics still gate.\n warnings.push(`${file.displayFile}: function token sequences unavailable: ${formatError(error)}`);\n }\n}\n\n/**\n * Measures the merge-base blob into `file`; 'failed' (with an error recorded) only when the\n * metrics themselves cannot be measured, and 'generated' (nothing measured) for generated code.\n * The auxiliary collections (duplication candidates, token sequences) may fail independently of\n * the metrics, so their failure only degrades duplication data and rename re-matching — the\n * function-level ratchets still run.\n */\nasync function measureBaseRevision(\n file: PreparedFile,\n basePath: string,\n context: GateContext,\n errors: string[],\n warnings: string[]\n): Promise<'measured' | 'generated' | 'failed'> {\n const measureOptions = {\n language: languageOf(basePath, context),\n duplication: context.options.duplication,\n };\n let baseContent;\n try {\n baseContent = await readFileAtRevision(context.repoRoot, context.mergeBase, basePath);\n if (context.baseExclusion.isGeneratedCode(path.join(context.repoRoot, basePath), baseContent)) {\n return 'generated';\n }\n const measured = await measureWithCrossFileData(baseContent, measureOptions);\n file.baseMetrics = measured.metrics;\n file.baseContent = baseContent;\n file.baseCandidates = measured.crossFileData;\n if (measured.crossFileError !== undefined) {\n warnings.push(`${basePath} (at merge-base): duplication candidates unavailable: ${measured.crossFileError}`);\n }\n } catch (error) {\n errors.push(`${basePath} (at merge-base): ${formatError(error)}`);\n return 'failed';\n }\n try {\n file.baseFunctionTokens = collectFunctionTokenSequences(baseContent, measureOptions);\n } catch (error) {\n warnings.push(`${basePath} (at merge-base): function token sequences unavailable: ${formatError(error)}`);\n }\n return 'measured';\n}\n\n/** The language of a scannable path; an explicitly targeted file skips the test-file name rules. */\nfunction languageOf(relativePath: string, context: GateContext): LanguageName {\n const explicitTarget = context.explicitFiles.has(path.join(context.repoRoot, relativePath));\n return getLanguage(relativePath, context.options, explicitTarget) as LanguageName;\n}\n\nasync function isSymbolicLink(absolutePath: string): Promise<boolean> {\n const stats = await lstat(absolutePath).catch(() => {});\n return stats?.isSymbolicLink() ?? false;\n}\n\n/** Walks up to the nearest existing DIRECTORY, so git commands never spawn in a missing or non-directory cwd. */\nasync function firstExistingDirectory(directory: string): Promise<string> {\n let current = directory;\n while (true) {\n const stats = await stat(current).catch(() => {});\n if (stats?.isDirectory()) {\n return current;\n }\n const parent = path.dirname(current);\n if (parent === current) {\n return current;\n }\n current = parent;\n }\n}\n\nfunction isWithinTarget(candidate: string, targetDirectory: string): boolean {\n const relative = path.relative(targetDirectory, candidate);\n return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));\n}\n\nfunction measureDuplicationUniverses(\n prepared: PreparedFile[],\n scannedFiles: ScannedFile[],\n options: ResolvedOptions\n): { baseCross?: CrossFileDuplicationMetrics; headCross?: CrossFileDuplicationMetrics } {\n const headSources = scannedFiles.flatMap(({ relativePath, file }) =>\n file.duplicationCandidates ? [{ file: relativePath, ...file.duplicationCandidates }] : []\n );\n\n const changedHeadPaths = new Set(\n prepared.flatMap((file) => (file.changed.status === 'deleted' ? [] : [file.changed.headPath]))\n );\n const baseSources = headSources.filter((source) => !changedHeadPaths.has(source.file));\n for (const file of prepared) {\n if (file.baseCandidates && file.changed.basePath !== undefined) {\n baseSources.push({ file: file.changed.basePath, ...file.baseCandidates });\n }\n }\n\n return {\n baseCross: baseSources.length >= 2 ? measureCrossFileDuplication(baseSources, options.duplication) : undefined,\n headCross: headSources.length >= 2 ? measureCrossFileDuplication(headSources, options.duplication) : undefined,\n };\n}\n\nfunction toDuplicationChange(\n file: PreparedFile,\n baseCross: CrossFileDuplicationMetrics | undefined,\n headCross: CrossFileDuplicationMetrics | undefined,\n headPathOf: (basePath: string) => string,\n lineSignaturesOf: (file: string) => LineSignatures | undefined\n): DuplicationChange {\n const headOccurrences = collectOccurrences(file.headFile?.metrics, headCross, file.changed.headPath);\n return {\n baseLines: file.baseContent === undefined ? undefined : splitLines(file.baseContent),\n headLines: file.headContent === undefined ? undefined : splitLines(file.headContent),\n baseDuplicatedLines:\n file.baseMetrics === undefined || file.changed.basePath === undefined\n ? new Set()\n : collectDuplicatedLineNumbers(file.baseMetrics, baseCross, file.changed.basePath),\n headDuplicatedLines:\n file.headFile === undefined\n ? new Set()\n : keepRepeatedLines(\n collectDuplicatedLineNumbers(file.headFile.metrics, headCross, file.changed.headPath),\n file.changed.headPath,\n headOccurrences,\n lineSignaturesOf\n ),\n baseOccurrences: collectOccurrences(file.baseMetrics, baseCross, file.changed.basePath, headPathOf),\n headOccurrences,\n hunks: file.hunks ?? [],\n };\n}\n\n/** A file's per-line normalized token signatures and how many lines carry each. */\ninterface LineSignatures {\n byLine: Map<number, string>;\n lineCounts: Map<string, number>;\n}\n\n/** Line signatures of the scanned head files, computed on first use. */\nfunction createLineSignatureIndex(scannedFiles: ScannedFile[]): (file: string) => LineSignatures | undefined {\n const tokensByFile = new Map(\n scannedFiles.map(({ relativePath, file }) => [relativePath, file.duplicationCandidates?.tokens])\n );\n const signaturesByFile = new Map<string, LineSignatures | undefined>();\n return (file) => {\n if (!signaturesByFile.has(file)) {\n const tokens = tokensByFile.get(file);\n signaturesByFile.set(file, tokens && signLines(tokens));\n }\n return signaturesByFile.get(file);\n };\n}\n\n/**\n * Each line's tokens with identifiers numbered by first occurrence, as clone detection anonymizes\n * them.\n */\nfunction signLines(tokens: readonly Token[]): LineSignatures {\n const byLine = new Map<number, string>();\n const identifierIndexesByLine = new Map<number, Map<string, number>>();\n for (const token of tokens) {\n const line = token.startRow + 1;\n let text = token.text;\n if (token.kind === 'id') {\n // Numbered by first occurrence within the line, so `a + a` and `a + b` stay distinct.\n const identifierIndexes = identifierIndexesByLine.get(line) ?? new Map<string, number>();\n identifierIndexesByLine.set(line, identifierIndexes);\n if (!identifierIndexes.has(token.text)) {\n identifierIndexes.set(token.text, identifierIndexes.size);\n }\n text = `\\u0001${identifierIndexes.get(token.text)}`;\n }\n byLine.set(line, `${byLine.get(line) ?? ''}${text}\\u0000`);\n }\n const lineCounts = new Map<string, number>();\n for (const signature of byLine.values()) {\n lineCounts.set(signature, (lineCounts.get(signature) ?? 0) + 1);\n }\n return { byLine, lineCounts };\n}\n\n/**\n * The duplicated lines whose normalized tokens a partner file of their clones (the file itself,\n * on another line, for `''`) repeats on some line. Near-miss coverage also marks lines the copies\n * do not share, such as a new line inserted into an old near-miss clone, which copied nothing.\n */\nfunction keepRepeatedLines(\n lines: ReadonlySet<number>,\n file: string,\n occurrences: readonly CloneOccurrence[],\n lineSignaturesOf: (file: string) => LineSignatures | undefined\n): Set<number> {\n const own = lineSignaturesOf(file);\n if (own === undefined) {\n return new Set(lines);\n }\n const repeats = (signature: string, partner: string): boolean => {\n if (partner === '') {\n return (own.lineCounts.get(signature) ?? 0) > 1;\n }\n const partnerSignatures = lineSignaturesOf(partner);\n return partnerSignatures === undefined || partnerSignatures.lineCounts.has(signature);\n };\n const partnersByLine = indexPartnersByLine(occurrences);\n return new Set(\n [...lines].filter((line) => {\n const signature = own.byLine.get(line);\n return (\n signature === undefined || [...(partnersByLine.get(line) ?? [])].some((partner) => repeats(signature, partner))\n );\n })\n );\n}\n\n/**\n * The file's clone occurrences with their partner files (`''` for the file itself), partner paths\n * renamed by `renamePartner` so base partners compare with head ones.\n */\nfunction collectOccurrences(\n metrics: CodeMetrics | undefined,\n cross: CrossFileDuplicationMetrics | undefined,\n file: string | undefined,\n renamePartner: (partner: string) => string = (partner) => partner\n): CloneOccurrence[] {\n if (metrics === undefined || file === undefined) {\n return [];\n }\n const withinFile = metrics.duplication.duplicateBlockGroups.flatMap((group) =>\n group.map(({ startLine, endLine }) => ({ startLine, endLine, partners: [''] }))\n );\n const crossFile = (cross?.groups ?? []).flatMap((group) => {\n const own = group.occurrences.filter((occurrence) => occurrence.file === file);\n const partners = [\n ...group.files.filter((partner) => partner !== file).map(renamePartner),\n ...(own.length > 1 ? [''] : []),\n ];\n return own.map(({ startLine, endLine }) => ({ startLine, endLine, partners }));\n });\n return [...withinFile, ...crossFile];\n}\n\nfunction toGateInput(\n file: PreparedFile,\n newlyDuplicatedLines: number[],\n headCross: CrossFileDuplicationMetrics | undefined\n): GateFileInput {\n return {\n file: file.displayFile,\n baseMetrics: file.baseMetrics,\n headMetrics: file.headFile?.metrics,\n baseFunctionTokens: file.baseFunctionTokens,\n headFunctionTokens: file.headFunctionTokens,\n newlyDuplicatedLines,\n duplicationPartners: collectPartners(file, newlyDuplicatedLines, headCross),\n gated: file.gated,\n };\n}\n\n/** Other files sharing a cross-file clone that covers the given lines. */\nfunction collectPartners(\n file: PreparedFile,\n lines: number[],\n headCross: CrossFileDuplicationMetrics | undefined\n): string[] {\n const headPath = file.changed.headPath;\n const covers = (occurrence: CrossFileDuplicateOccurrence): boolean =>\n occurrence.file === headPath && lines.some((line) => line >= occurrence.startLine && line <= occurrence.endLine);\n const partners = new Set<string>();\n for (const group of headCross?.groups ?? []) {\n if (group.occurrences.some(covers)) {\n for (const partner of group.files) {\n if (partner !== headPath) {\n partners.add(partner);\n }\n }\n }\n }\n return [...partners].toSorted();\n}\n\nfunction printTextReport(\n cliOptions: DiffCliOptions,\n mergeBase: string,\n result: GateResult,\n errors: string[],\n warnings: string[]\n): void {\n const shortBase = mergeBase.slice(0, 12);\n if (errors.length > 0) {\n // Unmeasured files were not gated, so \"0 violations\" would be vacuous; never claim a pass.\n writeStdout(\n `Regression gate could not complete: ${errors.length} measurement failures (details on stderr)` +\n `${result.violations.length > 0 ? `; ${result.violations.length} violations in the measured files` : ''} (base ${cliOptions.base}, merge-base ${shortBase}).\\n`\n );\n printViolations(result);\n } else if (result.violations.length === 0) {\n writeStdout(\n `Regression gate passed: ${result.checkedFileCount} changed files, ${result.checkedFunctionCount} functions checked (base ${cliOptions.base}, merge-base ${shortBase}).\\n`\n );\n } else {\n writeStdout(\n `Regression gate vs ${cliOptions.base} (merge-base ${shortBase}): ${result.violations.length} violations\\n`\n );\n printViolations(result);\n }\n\n if (cliOptions.full) {\n printFullDetails(result);\n }\n\n for (const warning of warnings) {\n writeStderr(`Warning: ${warning}\\n`);\n }\n for (const error of errors) {\n writeStderr(`Error: ${error}\\n`);\n }\n}\n\nfunction printViolations(result: GateResult): void {\n for (const [index, violation] of result.violations.entries()) {\n writeStdout(`${index + 1}. ${violation.message}\\n`);\n }\n}\n\n/** Base -> head values of every checked function; kept behind --full for humans and trending. */\nfunction printFullDetails(result: GateResult): void {\n if (result.checkedFunctions.length === 0) {\n return;\n }\n writeStdout('\\nChecked functions (base -> head):\\n');\n for (const report of result.checkedFunctions) {\n writeStdout(`- ${formatFunctionReport(report)}\\n`);\n }\n}\n\nfunction formatFunctionReport(report: CheckedFunctionReport): string {\n const range = (\n select: (values: GateFunctionValues) => number,\n format: (value: number) => string = String\n ): string => {\n const head = format(select(report.head));\n return report.base ? `${format(select(report.base))} -> ${head}` : head;\n };\n const values = [\n `cognitive ${range((fn) => fn.cognitiveComplexity)}`,\n `NCSS ${range((fn) => fn.ncss)}`,\n `nesting ${range((fn) => fn.nestingDepth)}`,\n `DepDegree ${range((fn) => fn.depDegree)}`,\n `volume ${range(\n (fn) => fn.halsteadVolume,\n (value) => value.toFixed(1)\n )}`,\n ];\n return `${report.file}:${report.startLine}-${report.endLine} ${report.name}${report.base ? '' : ' (new)'}: ${values.join(', ')}`;\n}\n\nfunction printJsonReport(\n cliOptions: DiffCliOptions,\n mergeBase: string,\n result: GateResult,\n inputs: GateFileInput[],\n errors: string[],\n warnings: string[]\n): void {\n const report: Record<string, unknown> = {\n base: cliOptions.base,\n mergeBase,\n passed: result.violations.length === 0 && errors.length === 0,\n violations: result.violations,\n checkedFileCount: result.checkedFileCount,\n checkedFunctionCount: result.checkedFunctionCount,\n newFunctionCount: result.newFunctionCount,\n errors,\n warnings,\n };\n if (cliOptions.full) {\n report.files = inputs\n .filter((input) => input.gated !== false)\n .map((input) => ({\n file: input.file,\n baseFunctionCount: input.baseMetrics?.functions.length ?? 0,\n headFunctionCount: input.headMetrics?.functions.length ?? 0,\n baseNcss: input.baseMetrics?.ncssCount ?? 0,\n headNcss: input.headMetrics?.ncssCount ?? 0,\n baseMaxCognitiveComplexity: input.baseMetrics?.maxCognitiveComplexity ?? 0,\n headMaxCognitiveComplexity: input.headMetrics?.maxCognitiveComplexity ?? 0,\n newlyDuplicatedLines: input.newlyDuplicatedLines,\n duplicationPartners: input.duplicationPartners,\n functions: result.checkedFunctions.filter((fn) => fn.file === input.file),\n }));\n }\n writeStdout(JSON.stringify(report, undefined, 2) + '\\n');\n}\n"],"mappings":"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"}
|
package/dist/diffCommand.d.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/** Raw options of the `diff` subcommand; every field but base is undefined unless the flag was passed. */
|
|
2
|
-
export interface DiffCliOptions {
|
|
3
|
-
base: string;
|
|
4
|
-
config?: string;
|
|
5
|
-
duplicationMinTokens?: number;
|
|
6
|
-
duplicationMaxGapTokens?: number;
|
|
7
|
-
duplicationMinSimilarityPercent?: number;
|
|
8
|
-
includeTests?: boolean;
|
|
9
|
-
json?: boolean;
|
|
10
|
-
full?: boolean;
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Runs the regression gate: measures the files changed relative to the merge-base with the base
|
|
14
|
-
* ref, at both revisions (`git cat-file`; no checkout, no persisted baseline), and reports only
|
|
15
|
-
* violations. Exit codes: 0 all gates passed, 1 violations, 2 changed files could not be measured.
|
|
16
|
-
*/
|
|
17
|
-
export declare function runDiffCommand(target: string, cliOptions: DiffCliOptions): Promise<void>;
|
package/dist/diffCommand.js
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import{measureCrossFileDuplication as e}from"./crossFileDuplication.js";import{collectFunctionTokenSequences as t}from"./metrics.js";import{findNewlyDuplicatedLines as n,indexPartnersByLine as r}from"./newDuplication.js";import{evaluateRegressionGate as i}from"./regressionGate.js";import{loadConfig as a,resolveGateOptions as o,resolveOptions as s}from"./cliConfig.js";import{listChangedFiles as c,listLineHunks as l,listRepositoryFiles as u,listSymlinkPathsAtRevision as d,readFileAtRevision as f,resolveMergeBase as p,resolveRepoRoot as m}from"./git.js";import{keepPaths as h,loadRepositoryExclusion as g}from"./exclusion.js";import{collectDuplicatedLineNumbers as _,configSearchDirectory as v,formatError as y,formatPath as ee,getLanguage as b,isScannedPath as x,measureWithCrossFileData as S,resolveTarget as te,scanListedFiles as ne,writeStderr as C,writeStdout as w}from"./scan.js";import{lstat as T,readFile as E,realpath as D,stat as O}from"node:fs/promises";import k from"node:path";import A from"node:os";async function j(e,t){try{await M(e,t)}catch(e){C(`Error: ${y(e)}\n`),process.exitCode=2}}async function M(e,t){let r=te(e),l=await a(t.config,await v(r)),f=s(t,l),_=o(l),y=await D(await m(await oe(await v(r)))),b=await p(y,t.base),S=await c(y,b),C=await u(y),w=await d(y,b),{canonicalTarget:T,targetExists:E}=await ae(r),A=(await O(T).catch(()=>{}))?.isFile()?T:void 0,j=A&&k.relative(y,A).split(k.sep).join(`/`);j!==void 0&&!j.startsWith(`../`)&&!C.has(j)&&(C.add(j),S.push({status:`added`,headPath:j}));let M=ie(A,y,S),N=S.some(e=>[e.headPath,e.basePath].some(e=>e!==void 0&&k.posix.basename(e)===`.gitattributes`)),F=S.flatMap(e=>e.basePath===void 0?[]:[e.basePath]),[I,L]=await Promise.all([g(y,C,f.exclude),g(y,N?[.../* @__PURE__ */ new Set([...F,...C])]:F,f.exclude,b)]),R=h(I,M),z=h(L,M),B=await ne(y,C,{...f,loadExclusion:()=>Promise.resolve(R)},M);if(B.fatalError)throw Error(B.fatalError);let V=B.files.map(e=>({relativePath:ee(e.file,B.displayRoot),file:e}));N&&S.push(...await re(V,S,y,R,z));let H=new Set(S.flatMap(e=>[e.headPath,...e.basePath===void 0?[]:[e.basePath]]).filter(e=>x(e,f,M.has(k.join(y,e))))),U=[],W=[...B.warnings];for(let e of B.errors)[...H].some(t=>e.startsWith(`${t}:`))?U.push(e):W.push(e);let G=await P(S,{repoRoot:y,mergeBase:b,canonicalTarget:T,options:f,scannedFiles:V,baseSymlinkPaths:w,scanErrors:[...U],headExclusion:R,baseExclusion:z,explicitFiles:M,generatedHeadFiles:new Set(B.generatedFiles)},U,W);if(!E&&!G.some(e=>e.gated))throw Error(`target "${e}" does not exist and matches no changed file`);let{baseCross:K,headCross:q}=se(G,V,f),J=new Map(G.flatMap(({changed:e})=>e.status===`renamed`&&e.basePath!==void 0?[[e.basePath,e.headPath]]:[])),Y=e=>J.get(e)??e,X=le(V),Z=n(G.map(e=>ce(e,K,q,Y,X))),Q=G.map((e,t)=>ue(e,Z[t]??[],q)),$=i(Q,_);t.json?fe(t,b,$,Q,U,W):de(t,b,$,U,W),U.length>0?process.exitCode=2:$.violations.length>0&&(process.exitCode=1)}const N=`/* @generated */`;async function re(e,t,n,r,i){let a=new Set(t.flatMap(e=>[e.headPath,e.basePath])),o=[];for(let{relativePath:t,file:s}of e){let e=k.join(n,t);if(a.has(t))continue;let c=i.isGeneratedCode(e,N)&&!r.isGeneratedCode(e,N);(i.isExcludedPath(e)||c&&i.isGeneratedCode(e,await E(s.file,`utf8`)))&&o.push({status:`modified`,basePath:t,headPath:t})}return o}function ie(e,t,n){if(e===void 0)return/* @__PURE__ */ new Set;let r=/* @__PURE__ */ new Set([e]);for(let i of n)i.basePath!==void 0&&k.join(t,i.headPath)===e&&r.add(k.join(t,i.basePath));return r}async function ae(e){try{return{canonicalTarget:await D(e),targetExists:!0}}catch{return{canonicalTarget:e,targetExists:!1}}}async function P(e,t,n,r){let i=new Map(t.scannedFiles.map(({relativePath:e,file:t})=>[e,t])),a=await F(e,A.availableParallelism()*2,async e=>{let n=[],r=[];return{file:await I(e,t,i,n,r),fileErrors:n,fileWarnings:r}}),o=[];for(let{file:e,fileErrors:t,fileWarnings:i}of a)n.push(...t),r.push(...i),e&&o.push(e);return o}async function F(e,t,n){let r=[],i=0;return await Promise.all(Array.from({length:Math.min(t,e.length)},async()=>{for(;i<e.length;){let t=i;i+=1,r[t]=await n(e[t])}})),r}async function I(e,t,n,r,i){let a=k.join(t.repoRoot,e.headPath),o=e.status!==`deleted`&&x(e.headPath,t.options,t.explicitFiles.has(a))&&!t.headExclusion.isExcludedPath(a)&&!t.generatedHeadFiles.has(a)&&!await U(a),s=e.basePath===void 0?``:k.join(t.repoRoot,e.basePath),c=e.basePath!==void 0&&x(e.basePath,t.options,t.explicitFiles.has(s))&&!t.baseExclusion.isExcludedPath(s)&&!t.baseSymlinkPaths.has(e.basePath);if(!o&&!c)return;let l=e.status===`deleted`?e.basePath:e.headPath,u=o?n.get(e.headPath):void 0;if(o&&!u){z(e.headPath,t.scanErrors,r);return}let d={changed:e,displayFile:l,gated:o||e.status===`deleted`?W(k.join(t.repoRoot,l),t.canonicalTarget):!1,headFile:u};if(c){let n=await V(d,e.basePath,t,r,i);if(n===`failed`||n===`generated`&&!u)return}try{u&&(d.headContent=await E(u.file,`utf8`),B(d,d.headContent,t,i)),d.hunks=await L(d,t)}catch(e){r.push(`${l}: ${y(e)}`);return}return d}async function L(e,t){let{changed:n,baseContent:r,headContent:i}=e;return r!==void 0&&i!==void 0?await l(t.repoRoot,t.mergeBase,n.basePath,n.headPath):[{baseStart:1,baseCount:r===void 0?0:R(r).length,headStart:1,headCount:i===void 0?0:R(i).length}]}function R(e){return e.split(/\r\n|\n|\r/u)}function z(e,t,n){t.some(t=>t.startsWith(`${e}:`))||n.push(`${e}: changed file was not measured`)}function B(e,n,r,i){try{e.headFunctionTokens=t(n,{language:H(e.changed.headPath,r),duplication:r.options.duplication})}catch(t){i.push(`${e.displayFile}: function token sequences unavailable: ${y(t)}`)}}async function V(e,n,r,i,a){let o={language:H(n,r),duplication:r.options.duplication},s;try{if(s=await f(r.repoRoot,r.mergeBase,n),r.baseExclusion.isGeneratedCode(k.join(r.repoRoot,n),s))return`generated`;let t=await S(s,o);e.baseMetrics=t.metrics,e.baseContent=s,e.baseCandidates=t.crossFileData,t.crossFileError!==void 0&&a.push(`${n} (at merge-base): duplication candidates unavailable: ${t.crossFileError}`)}catch(e){return i.push(`${n} (at merge-base): ${y(e)}`),`failed`}try{e.baseFunctionTokens=t(s,o)}catch(e){a.push(`${n} (at merge-base): function token sequences unavailable: ${y(e)}`)}return`measured`}function H(e,t){let n=t.explicitFiles.has(k.join(t.repoRoot,e));return b(e,t.options,n)}async function U(e){return(await T(e).catch(()=>{}))?.isSymbolicLink()??!1}async function oe(e){let t=e;for(;;){if((await O(t).catch(()=>{}))?.isDirectory())return t;let e=k.dirname(t);if(e===t)return t;t=e}}function W(e,t){let n=k.relative(t,e);return n===``||!n.startsWith(`..${k.sep}`)&&n!==`..`&&!k.isAbsolute(n)}function se(t,n,r){let i=n.flatMap(({relativePath:e,file:t})=>t.duplicationCandidates?[{file:e,...t.duplicationCandidates}]:[]),a=new Set(t.flatMap(e=>e.changed.status===`deleted`?[]:[e.changed.headPath])),o=i.filter(e=>!a.has(e.file));for(let e of t)e.baseCandidates&&e.changed.basePath!==void 0&&o.push({file:e.changed.basePath,...e.baseCandidates});return{baseCross:o.length>=2?e(o,r.duplication):void 0,headCross:i.length>=2?e(i,r.duplication):void 0}}function ce(e,t,n,r,i){let a=q(e.headFile?.metrics,n,e.changed.headPath);return{baseLines:e.baseContent===void 0?void 0:R(e.baseContent),headLines:e.headContent===void 0?void 0:R(e.headContent),baseDuplicatedLines:e.baseMetrics===void 0||e.changed.basePath===void 0?/* @__PURE__ */ new Set:_(e.baseMetrics,t,e.changed.basePath),headDuplicatedLines:e.headFile===void 0?/* @__PURE__ */ new Set:K(_(e.headFile.metrics,n,e.changed.headPath),e.changed.headPath,a,i),baseOccurrences:q(e.baseMetrics,t,e.changed.basePath,r),headOccurrences:a,hunks:e.hunks??[]}}function le(e){let t=new Map(e.map(({relativePath:e,file:t})=>[e,t.duplicationCandidates?.tokens])),n=/* @__PURE__ */ new Map;return e=>{if(!n.has(e)){let r=t.get(e);n.set(e,r&&G(r))}return n.get(e)}}function G(e){let t=/* @__PURE__ */ new Map,n=/* @__PURE__ */ new Map;for(let r of e){let e=r.startRow+1,i=r.text;if(r.kind===`id`){let t=n.get(e)??/* @__PURE__ */ new Map;n.set(e,t),t.has(r.text)||t.set(r.text,t.size),i=`\u0001${t.get(r.text)}`}t.set(e,`${t.get(e)??``}${i}\u0000`)}let r=/* @__PURE__ */ new Map;for(let e of t.values())r.set(e,(r.get(e)??0)+1);return{byLine:t,lineCounts:r}}function K(e,t,n,i){let a=i(t);if(a===void 0)return new Set(e);let o=(e,t)=>{if(t===``)return(a.lineCounts.get(e)??0)>1;let n=i(t);return n===void 0||n.lineCounts.has(e)},s=r(n);return new Set([...e].filter(e=>{let t=a.byLine.get(e);return t===void 0||[...s.get(e)??[]].some(e=>o(t,e))}))}function q(e,t,n,r=e=>e){if(e===void 0||n===void 0)return[];let i=e.duplication.duplicateBlockGroups.flatMap(e=>e.map(({startLine:e,endLine:t})=>({startLine:e,endLine:t,partners:[``]}))),a=(t?.groups??[]).flatMap(e=>{let t=e.occurrences.filter(e=>e.file===n),i=[...e.files.filter(e=>e!==n).map(r),...t.length>1?[``]:[]];return t.map(({startLine:e,endLine:t})=>({startLine:e,endLine:t,partners:i}))});return[...i,...a]}function ue(e,t,n){return{file:e.displayFile,baseMetrics:e.baseMetrics,headMetrics:e.headFile?.metrics,baseFunctionTokens:e.baseFunctionTokens,headFunctionTokens:e.headFunctionTokens,newlyDuplicatedLines:t,duplicationPartners:J(e,t,n),gated:e.gated}}function J(e,t,n){let r=e.changed.headPath,i=e=>e.file===r&&t.some(t=>t>=e.startLine&&t<=e.endLine),a=/* @__PURE__ */ new Set;for(let e of n?.groups??[])if(e.occurrences.some(i))for(let t of e.files)t!==r&&a.add(t);return[...a].toSorted()}function de(e,t,n,r,i){let a=t.slice(0,12);r.length>0?(w(`Regression gate could not complete: ${r.length} measurement failures (details on stderr)${n.violations.length>0?`; ${n.violations.length} violations in the measured files`:``} (base ${e.base}, merge-base ${a}).\n`),Y(n)):n.violations.length===0?w(`Regression gate passed: ${n.checkedFileCount} changed files, ${n.checkedFunctionCount} functions checked (base ${e.base}, merge-base ${a}).\n`):(w(`Regression gate vs ${e.base} (merge-base ${a}): ${n.violations.length} violations\n`),Y(n)),e.full&&X(n);for(let e of i)C(`Warning: ${e}\n`);for(let e of r)C(`Error: ${e}\n`)}function Y(e){for(let[t,n]of e.violations.entries())w(`${t+1}. ${n.message}\n`)}function X(e){if(e.checkedFunctions.length!==0){w(`
|
|
2
|
-
Checked functions (base -> head):
|
|
3
|
-
`);for(let t of e.checkedFunctions)w(`- ${Z(t)}\n`)}}function Z(e){let t=(t,n=String)=>{let r=n(t(e.head));return e.base?`${n(t(e.base))} -> ${r}`:r},n=[`cognitive ${t(e=>e.cognitiveComplexity)}`,`NCSS ${t(e=>e.ncss)}`,`nesting ${t(e=>e.nestingDepth)}`,`DepDegree ${t(e=>e.depDegree)}`,`volume ${t(e=>e.halsteadVolume,e=>e.toFixed(1))}`];return`${e.file}:${e.startLine}-${e.endLine} ${e.name}${e.base?``:` (new)`}: ${n.join(`, `)}`}function fe(e,t,n,r,i,a){let o={base:e.base,mergeBase:t,passed:n.violations.length===0&&i.length===0,violations:n.violations,checkedFileCount:n.checkedFileCount,checkedFunctionCount:n.checkedFunctionCount,newFunctionCount:n.newFunctionCount,errors:i,warnings:a};e.full&&(o.files=r.filter(e=>e.gated!==!1).map(e=>({file:e.file,baseFunctionCount:e.baseMetrics?.functions.length??0,headFunctionCount:e.headMetrics?.functions.length??0,baseNcss:e.baseMetrics?.ncssCount??0,headNcss:e.headMetrics?.ncssCount??0,baseMaxCognitiveComplexity:e.baseMetrics?.maxCognitiveComplexity??0,headMaxCognitiveComplexity:e.headMetrics?.maxCognitiveComplexity??0,newlyDuplicatedLines:e.newlyDuplicatedLines,duplicationPartners:e.duplicationPartners,functions:n.checkedFunctions.filter(t=>t.file===e.file)}))),w(JSON.stringify(o,void 0,2)+`
|
|
4
|
-
`)}export{j as runDiffCommand};
|
|
5
|
-
//# sourceMappingURL=diffCommand.js.map
|
package/dist/diffCommand.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"diffCommand.js","names":[],"sources":["../src/diffCommand.ts"],"sourcesContent":["import { lstat, readFile, realpath, stat } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { loadConfig, resolveGateOptions, resolveOptions, type ResolvedOptions } from './cliConfig.js';\nimport {\n measureCrossFileDuplication,\n type CrossFileDuplicateOccurrence,\n type CrossFileDuplicationMetrics,\n} from './crossFileDuplication.js';\nimport type { CrossFileDuplicationFileData, Token } from './duplication.js';\nimport { keepPaths, loadRepositoryExclusion, type Exclusion } from './exclusion.js';\nimport {\n listChangedFiles,\n listLineHunks,\n listRepositoryFiles,\n listSymlinkPathsAtRevision,\n readFileAtRevision,\n resolveMergeBase,\n resolveRepoRoot,\n type ChangedFile,\n} from './git.js';\nimport { collectFunctionTokenSequences } from './metrics.js';\nimport {\n findNewlyDuplicatedLines,\n indexPartnersByLine,\n type CloneOccurrence,\n type DuplicationChange,\n type LineHunk,\n} from './newDuplication.js';\nimport {\n evaluateRegressionGate,\n type CheckedFunctionReport,\n type GateFileInput,\n type GateFunctionValues,\n type GateResult,\n} from './regressionGate.js';\nimport {\n collectDuplicatedLineNumbers,\n configSearchDirectory,\n formatError,\n formatPath,\n getLanguage,\n isScannedPath,\n measureWithCrossFileData,\n resolveTarget,\n scanListedFiles,\n writeStderr,\n writeStdout,\n type FileMetrics,\n} from './scan.js';\nimport type { CodeMetrics, LanguageName } from './types.js';\n\n/** Raw options of the `diff` subcommand; every field but base is undefined unless the flag was passed. */\nexport interface DiffCliOptions {\n base: string;\n config?: string;\n duplicationMinTokens?: number;\n duplicationMaxGapTokens?: number;\n duplicationMinSimilarityPercent?: number;\n includeTests?: boolean;\n json?: boolean;\n full?: boolean;\n}\n\n/** One changed file measured at both revisions, plus its duplication-universe contribution. */\ninterface PreparedFile {\n changed: ChangedFile;\n /** Repository-relative display path: the head path, or the base path for deleted files. */\n displayFile: string;\n /** Whether the file is gated (under the target directory); others only feed the base universe. */\n gated: boolean;\n headFile?: FileMetrics;\n headContent?: string;\n baseMetrics?: CodeMetrics;\n baseContent?: string;\n /**\n * The line diff from the measured base to the measured head; with one side unmeasured, every\n * line of the other counts as added or deleted.\n */\n hunks?: LineHunk[];\n baseCandidates?: CrossFileDuplicationFileData;\n baseFunctionTokens?: Int32Array[];\n headFunctionTokens?: Int32Array[];\n}\n\n/** A scanned file that git considers part of the project, keyed by its repository-relative path. */\ninterface ScannedFile {\n relativePath: string;\n file: FileMetrics;\n}\n\n/**\n * Runs the regression gate: measures the files changed relative to the merge-base with the base\n * ref, at both revisions (`git cat-file`; no checkout, no persisted baseline), and reports only\n * violations. Exit codes: 0 all gates passed, 1 violations, 2 changed files could not be measured.\n */\nexport async function runDiffCommand(target: string, cliOptions: DiffCliOptions): Promise<void> {\n try {\n await runGate(target, cliOptions);\n } catch (error) {\n writeStderr(`Error: ${formatError(error)}\\n`);\n process.exitCode = 2;\n }\n}\n\nasync function runGate(target: string, cliOptions: DiffCliOptions): Promise<void> {\n const resolvedTarget = resolveTarget(target);\n const config = await loadConfig(cliOptions.config, await configSearchDirectory(resolvedTarget));\n const options = resolveOptions(cliOptions, config);\n const gateOptions = resolveGateOptions(config);\n\n // The target may be a typo'd path whose ancestors don't exist either; repository discovery must\n // still run so the mistyped target gets its own diagnostic instead of a git spawn failure.\n const repoRoot = await realpath(\n await resolveRepoRoot(await firstExistingDirectory(await configSearchDirectory(resolvedTarget)))\n );\n const mergeBase = await resolveMergeBase(repoRoot, cliOptions.base);\n const changedFiles = await listChangedFiles(repoRoot, mergeBase);\n\n // Every git-visible file (tracked or untracked non-ignored) is measured at head: that provides\n // the head metrics of changed files and the project-wide duplication universe, so copy-paste\n // from unchanged code into changed files is caught. Scanning the explicit git list (instead of\n // walking the tree) keeps ignored artifact directories from ever being parsed: they exist in\n // neither the base commit nor CI, so they would only cost time and skew duplication counts.\n // Unchanged files are byte-identical at both revisions, so the base universe is the same scan\n // with the changed files' contents swapped for their merge-base blobs.\n const repositoryFiles = await listRepositoryFiles(repoRoot);\n const baseSymlinkPaths = await listSymlinkPathsAtRevision(repoRoot, mergeBase);\n const { canonicalTarget, targetExists } = await canonicalizeTarget(resolvedTarget);\n const targetStat = await stat(canonicalTarget).catch(() => {});\n const targetFile = targetStat?.isFile() ? canonicalTarget : undefined;\n // A targeted file git does not list (an ignored one) is still measured, as an addition.\n const unlistedTarget = targetFile && path.relative(repoRoot, targetFile).split(path.sep).join('/');\n if (unlistedTarget !== undefined && !unlistedTarget.startsWith('../') && !repositoryFiles.has(unlistedTarget)) {\n repositoryFiles.add(unlistedTarget);\n changedFiles.push({ status: 'added', headPath: unlistedTarget });\n }\n const explicitFiles = listExplicitlyTargetedPaths(targetFile, repoRoot, changedFiles);\n // Base blobs are excluded by the attributes of the revision they come from. A changed\n // .gitattributes can re-include files whose bytes did not change, so then every file's base\n // attributes are needed to find them.\n const attributesChanged = changedFiles.some((changed) =>\n [changed.headPath, changed.basePath].some(\n (file) => file !== undefined && path.posix.basename(file) === '.gitattributes'\n )\n );\n const basePaths = changedFiles.flatMap((changed) => (changed.basePath === undefined ? [] : [changed.basePath]));\n const [headAttributesExclusion, baseAttributesExclusion] = await Promise.all([\n loadRepositoryExclusion(repoRoot, repositoryFiles, options.exclude),\n loadRepositoryExclusion(\n repoRoot,\n attributesChanged ? [...new Set([...basePaths, ...repositoryFiles])] : basePaths,\n options.exclude,\n mergeBase\n ),\n ]);\n const headExclusion = keepPaths(headAttributesExclusion, explicitFiles);\n const baseExclusion = keepPaths(baseAttributesExclusion, explicitFiles);\n const scan = await scanListedFiles(\n repoRoot,\n repositoryFiles,\n { ...options, loadExclusion: () => Promise.resolve(headExclusion) },\n explicitFiles\n );\n // A run-wide failure (a missing native addon) invalidates the whole gate: surface it once as\n // the fatal error (exit 2) instead of diagnosing every changed file as unmeasured.\n if (scan.fatalError) {\n throw new Error(scan.fatalError);\n }\n const scannedFiles: ScannedFile[] = scan.files.map((file) => ({\n relativePath: formatPath(file.file, scan.displayRoot),\n file,\n }));\n if (attributesChanged) {\n changedFiles.push(\n ...(await listReincludedFiles(scannedFiles, changedFiles, repoRoot, headExclusion, baseExclusion))\n );\n }\n\n // A measurement failure on ANY scannable changed file forces exit 2 — deliberately including\n // files outside a scoped target, because cross-file function matching and the base duplication\n // universe for the gated files depend on them. Failures elsewhere (unchanged files) and\n // unsupported changed paths degrade to warnings.\n const changedPaths = new Set(\n changedFiles\n .flatMap((changed) => [changed.headPath, ...(changed.basePath === undefined ? [] : [changed.basePath])])\n .filter((changedPath) => isScannedPath(changedPath, options, explicitFiles.has(path.join(repoRoot, changedPath))))\n );\n const errors: string[] = [];\n const warnings = [...scan.warnings];\n for (const error of scan.errors) {\n if ([...changedPaths].some((changedPath) => error.startsWith(`${changedPath}:`))) {\n errors.push(error);\n } else {\n warnings.push(error);\n }\n }\n\n const prepared = await prepareChangedFiles(\n changedFiles,\n {\n repoRoot,\n mergeBase,\n canonicalTarget,\n options,\n scannedFiles,\n baseSymlinkPaths,\n scanErrors: [...errors],\n headExclusion,\n baseExclusion,\n explicitFiles,\n generatedHeadFiles: new Set(scan.generatedFiles),\n },\n errors,\n warnings\n );\n // A gate must not fail open on a mistyped target: a nonexistent path is only acceptable when it\n // still matches changed files (e.g. a fully deleted directory).\n if (!targetExists && !prepared.some((file) => file.gated)) {\n throw new Error(`target \"${target}\" does not exist and matches no changed file`);\n }\n\n // Non-gated files (outside the target, or renamed out of scan scope) still feed function\n // matching and the duplication universes; the evaluator reports nothing for them.\n const { baseCross, headCross } = measureDuplicationUniverses(prepared, scannedFiles, options);\n const renamedPaths = new Map(\n prepared.flatMap(({ changed }) =>\n changed.status === 'renamed' && changed.basePath !== undefined\n ? [[changed.basePath, changed.headPath] as const]\n : []\n )\n );\n const headPathOf = (basePath: string): string => renamedPaths.get(basePath) ?? basePath;\n const lineSignaturesOf = createLineSignatureIndex(scannedFiles);\n const newlyDuplicatedLines = findNewlyDuplicatedLines(\n prepared.map((file) => toDuplicationChange(file, baseCross, headCross, headPathOf, lineSignaturesOf))\n );\n const inputs = prepared.map((file, index) => toGateInput(file, newlyDuplicatedLines[index] ?? [], headCross));\n const result = evaluateRegressionGate(inputs, gateOptions);\n\n if (cliOptions.json) {\n printJsonReport(cliOptions, mergeBase, result, inputs, errors, warnings);\n } else {\n printTextReport(cliOptions, mergeBase, result, errors, warnings);\n }\n\n if (errors.length > 0) {\n process.exitCode = 2;\n } else if (result.violations.length > 0) {\n process.exitCode = 1;\n }\n}\n\n/** Content with a generated-code marker, to ask whether an exclusion honors the marker for a path. */\nconst markedCode = '/* @generated */';\n\n/**\n * Unchanged files the head scan measured but the merge-base would not have: excluded by its\n * attributes, or generated by content without the `-linguist-generated` override the working tree\n * adds. Their code is newly measured, so it gates as a modification from an unmeasurable base. The\n * head side comes from the scan, which already applied scope, symbolic-link, and exclusion rules.\n */\nasync function listReincludedFiles(\n scannedFiles: readonly ScannedFile[],\n changedFiles: readonly ChangedFile[],\n repoRoot: string,\n headExclusion: Exclusion,\n baseExclusion: Exclusion\n): Promise<ChangedFile[]> {\n const changedPaths = new Set(changedFiles.flatMap((changed) => [changed.headPath, changed.basePath]));\n const reincluded: ChangedFile[] = [];\n for (const { relativePath, file } of scannedFiles) {\n const absolutePath = path.join(repoRoot, relativePath);\n if (changedPaths.has(relativePath)) {\n continue;\n }\n // The same content is generated at one revision only when its override differs, so the\n // content is read just for files the working tree newly marks -linguist-generated.\n const overrideAdded =\n baseExclusion.isGeneratedCode(absolutePath, markedCode) &&\n !headExclusion.isGeneratedCode(absolutePath, markedCode);\n if (\n baseExclusion.isExcludedPath(absolutePath) ||\n (overrideAdded && baseExclusion.isGeneratedCode(absolutePath, await readFile(file.file, 'utf8')))\n ) {\n reincluded.push({ status: 'modified', basePath: relativePath, headPath: relativePath });\n }\n }\n return reincluded;\n}\n\n/**\n * An explicitly targeted file is measured even when excluded, like the ranking command's single\n * file target: the file itself, and its base path when it was renamed.\n */\nfunction listExplicitlyTargetedPaths(\n targetFile: string | undefined,\n repoRoot: string,\n changedFiles: ChangedFile[]\n): Set<string> {\n if (targetFile === undefined) {\n return new Set();\n }\n const paths = new Set([targetFile]);\n for (const changed of changedFiles) {\n if (changed.basePath !== undefined && path.join(repoRoot, changed.headPath) === targetFile) {\n paths.add(path.join(repoRoot, changed.basePath));\n }\n }\n return paths;\n}\n\n/** The target may not exist (e.g. only deleted files under it); fall back to the resolved path. */\nasync function canonicalizeTarget(resolvedTarget: string): Promise<{ canonicalTarget: string; targetExists: boolean }> {\n try {\n return { canonicalTarget: await realpath(resolvedTarget), targetExists: true };\n } catch {\n return { canonicalTarget: resolvedTarget, targetExists: false };\n }\n}\n\ninterface GateContext {\n repoRoot: string;\n mergeBase: string;\n canonicalTarget: string;\n options: ResolvedOptions;\n scannedFiles: ScannedFile[];\n /** Paths that are symbolic links at the merge-base; like head symlinks, they are not gated. */\n baseSymlinkPaths: Set<string>;\n /** Errors the head scan recorded against changed files. */\n scanErrors: readonly string[];\n headExclusion: Exclusion;\n baseExclusion: Exclusion;\n /** The explicitly targeted file (and its renamed base path), measured despite every exclusion. */\n explicitFiles: ReadonlySet<string>;\n /** Absolute paths the head scan skipped as generated code. */\n generatedHeadFiles: ReadonlySet<string>;\n}\n\nasync function prepareChangedFiles(\n changedFiles: ChangedFile[],\n context: GateContext,\n errors: string[],\n warnings: string[]\n): Promise<PreparedFile[]> {\n const headByPath = new Map(context.scannedFiles.map(({ relativePath, file }) => [relativePath, file]));\n // Files are prepared concurrently (git reads and base measurements); each records its diagnostics\n // on its own, and they are appended in changed-file order so the report stays deterministic.\n const results = await mapConcurrently(changedFiles, os.availableParallelism() * 2, async (changed) => {\n const fileErrors: string[] = [];\n const fileWarnings: string[] = [];\n const file = await prepareChangedFile(changed, context, headByPath, fileErrors, fileWarnings);\n return { file, fileErrors, fileWarnings };\n });\n const prepared: PreparedFile[] = [];\n for (const { file, fileErrors, fileWarnings } of results) {\n errors.push(...fileErrors);\n warnings.push(...fileWarnings);\n if (file) {\n prepared.push(file);\n }\n }\n return prepared;\n}\n\n/** `Promise.all(items.map(map))` with at most `limit` calls pending at once, results in input order. */\nasync function mapConcurrently<T, R>(items: readonly T[], limit: number, map: (item: T) => Promise<R>): Promise<R[]> {\n const results: R[] = [];\n let nextIndex = 0;\n const work = async (): Promise<void> => {\n while (nextIndex < items.length) {\n const index = nextIndex;\n nextIndex += 1;\n results[index] = await map(items[index] as T);\n }\n };\n await Promise.all(Array.from({ length: Math.min(limit, items.length) }, work));\n return results;\n}\n\nasync function prepareChangedFile(\n changed: ChangedFile,\n context: GateContext,\n headByPath: Map<string, FileMetrics>,\n errors: string[],\n warnings: string[]\n): Promise<PreparedFile | undefined> {\n // Symbolic links are skipped on both sides, mirroring scanListedFiles: git stores only the\n // target string, so a symlink blob is not measurable source.\n const absoluteHeadPath = path.join(context.repoRoot, changed.headPath);\n const headScannable =\n changed.status !== 'deleted' &&\n isScannedPath(changed.headPath, context.options, context.explicitFiles.has(absoluteHeadPath)) &&\n !context.headExclusion.isExcludedPath(absoluteHeadPath) &&\n !context.generatedHeadFiles.has(absoluteHeadPath) &&\n !(await isSymbolicLink(absoluteHeadPath));\n // A base path outside the scan scope (renamed from a test/ignored directory, or an unsupported\n // extension) was never measurable code: its content gates as new code instead of ratcheting\n // against a blob the scanner would not have measured.\n const absoluteBasePath = changed.basePath === undefined ? '' : path.join(context.repoRoot, changed.basePath);\n const baseScannable =\n changed.basePath !== undefined &&\n isScannedPath(changed.basePath, context.options, context.explicitFiles.has(absoluteBasePath)) &&\n !context.baseExclusion.isExcludedPath(absoluteBasePath) &&\n !context.baseSymlinkPaths.has(changed.basePath);\n if (!headScannable && !baseScannable) {\n return undefined;\n }\n\n const displayFile = changed.status === 'deleted' ? (changed.basePath as string) : changed.headPath;\n const headFile = headScannable ? headByPath.get(changed.headPath) : undefined;\n if (headScannable && !headFile) {\n reportUnmeasuredChangedFile(changed.headPath, context.scanErrors, errors);\n return undefined;\n }\n\n const file: PreparedFile = {\n changed,\n displayFile,\n // A file whose head left the scan scope still contributes its base functions to matching and\n // its base blob to the base universe, but nothing about it is gated or reported.\n gated:\n headScannable || changed.status === 'deleted'\n ? isWithinTarget(path.join(context.repoRoot, displayFile), context.canonicalTarget)\n : false,\n headFile,\n };\n\n if (baseScannable) {\n const outcome = await measureBaseRevision(file, changed.basePath as string, context, errors, warnings);\n // A base that turns out to be generated code gates its head as new code, like any other base\n // outside the scan scope.\n if (outcome === 'failed' || (outcome === 'generated' && !headFile)) {\n return undefined;\n }\n }\n\n try {\n if (headFile) {\n // The duplication gate needs the head lines, so an unreadable head fails like an unmeasured one.\n file.headContent = await readFile(headFile.file, 'utf8');\n collectHeadFunctionTokens(file, file.headContent, context, warnings);\n }\n file.hunks = await diffMeasuredRevisions(file, context);\n } catch (error) {\n errors.push(`${displayFile}: ${formatError(error)}`);\n return undefined;\n }\n return file;\n}\n\nasync function diffMeasuredRevisions(file: PreparedFile, context: GateContext): Promise<LineHunk[]> {\n const { changed, baseContent, headContent } = file;\n if (baseContent !== undefined && headContent !== undefined) {\n return await listLineHunks(context.repoRoot, context.mergeBase, changed.basePath as string, changed.headPath);\n }\n return [\n {\n baseStart: 1,\n baseCount: baseContent === undefined ? 0 : splitLines(baseContent).length,\n headStart: 1,\n headCount: headContent === undefined ? 0 : splitLines(headContent).length,\n },\n ];\n}\n\n/** Line numbering of the measured metrics (classifyLines splits on every line terminator). */\nfunction splitLines(content: string): string[] {\n return content.split(/\\r\\n|\\n|\\r/u);\n}\n\n/**\n * The scan covers exactly the git-visible list, so a scannable changed path can only be missing\n * after a measurement failure (already recorded as an error) or a silent exclusion (an alias of\n * an already-visited file, or absence from the git list). Failing loudly keeps the gate from\n * passing with the file unchecked.\n */\nfunction reportUnmeasuredChangedFile(headPath: string, scanErrors: readonly string[], errors: string[]): void {\n if (!scanErrors.some((error) => error.startsWith(`${headPath}:`))) {\n errors.push(`${headPath}: changed file was not measured`);\n }\n}\n\nfunction collectHeadFunctionTokens(\n file: PreparedFile,\n headContent: string,\n context: GateContext,\n warnings: string[]\n): void {\n try {\n file.headFunctionTokens = collectFunctionTokenSequences(headContent, {\n language: languageOf(file.changed.headPath, context),\n duplication: context.options.duplication,\n });\n } catch (error) {\n // Only rename re-matching degrades without token sequences; the head metrics still gate.\n warnings.push(`${file.displayFile}: function token sequences unavailable: ${formatError(error)}`);\n }\n}\n\n/**\n * Measures the merge-base blob into `file`; 'failed' (with an error recorded) only when the\n * metrics themselves cannot be measured, and 'generated' (nothing measured) for generated code.\n * The auxiliary collections (duplication candidates, token sequences) may fail independently of\n * the metrics, so their failure only degrades duplication data and rename re-matching — the\n * function-level ratchets still run.\n */\nasync function measureBaseRevision(\n file: PreparedFile,\n basePath: string,\n context: GateContext,\n errors: string[],\n warnings: string[]\n): Promise<'measured' | 'generated' | 'failed'> {\n const measureOptions = {\n language: languageOf(basePath, context),\n duplication: context.options.duplication,\n };\n let baseContent;\n try {\n baseContent = await readFileAtRevision(context.repoRoot, context.mergeBase, basePath);\n if (context.baseExclusion.isGeneratedCode(path.join(context.repoRoot, basePath), baseContent)) {\n return 'generated';\n }\n const measured = await measureWithCrossFileData(baseContent, measureOptions);\n file.baseMetrics = measured.metrics;\n file.baseContent = baseContent;\n file.baseCandidates = measured.crossFileData;\n if (measured.crossFileError !== undefined) {\n warnings.push(`${basePath} (at merge-base): duplication candidates unavailable: ${measured.crossFileError}`);\n }\n } catch (error) {\n errors.push(`${basePath} (at merge-base): ${formatError(error)}`);\n return 'failed';\n }\n try {\n file.baseFunctionTokens = collectFunctionTokenSequences(baseContent, measureOptions);\n } catch (error) {\n warnings.push(`${basePath} (at merge-base): function token sequences unavailable: ${formatError(error)}`);\n }\n return 'measured';\n}\n\n/** The language of a scannable path; an explicitly targeted file skips the test-file name rules. */\nfunction languageOf(relativePath: string, context: GateContext): LanguageName {\n const explicitTarget = context.explicitFiles.has(path.join(context.repoRoot, relativePath));\n return getLanguage(relativePath, context.options, explicitTarget) as LanguageName;\n}\n\nasync function isSymbolicLink(absolutePath: string): Promise<boolean> {\n const stats = await lstat(absolutePath).catch(() => {});\n return stats?.isSymbolicLink() ?? false;\n}\n\n/** Walks up to the nearest existing DIRECTORY, so git commands never spawn in a missing or non-directory cwd. */\nasync function firstExistingDirectory(directory: string): Promise<string> {\n let current = directory;\n while (true) {\n const stats = await stat(current).catch(() => {});\n if (stats?.isDirectory()) {\n return current;\n }\n const parent = path.dirname(current);\n if (parent === current) {\n return current;\n }\n current = parent;\n }\n}\n\nfunction isWithinTarget(candidate: string, targetDirectory: string): boolean {\n const relative = path.relative(targetDirectory, candidate);\n return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));\n}\n\nfunction measureDuplicationUniverses(\n prepared: PreparedFile[],\n scannedFiles: ScannedFile[],\n options: ResolvedOptions\n): { baseCross?: CrossFileDuplicationMetrics; headCross?: CrossFileDuplicationMetrics } {\n const headSources = scannedFiles.flatMap(({ relativePath, file }) =>\n file.duplicationCandidates ? [{ file: relativePath, ...file.duplicationCandidates }] : []\n );\n\n const changedHeadPaths = new Set(\n prepared.flatMap((file) => (file.changed.status === 'deleted' ? [] : [file.changed.headPath]))\n );\n const baseSources = headSources.filter((source) => !changedHeadPaths.has(source.file));\n for (const file of prepared) {\n if (file.baseCandidates && file.changed.basePath !== undefined) {\n baseSources.push({ file: file.changed.basePath, ...file.baseCandidates });\n }\n }\n\n return {\n baseCross: baseSources.length >= 2 ? measureCrossFileDuplication(baseSources, options.duplication) : undefined,\n headCross: headSources.length >= 2 ? measureCrossFileDuplication(headSources, options.duplication) : undefined,\n };\n}\n\nfunction toDuplicationChange(\n file: PreparedFile,\n baseCross: CrossFileDuplicationMetrics | undefined,\n headCross: CrossFileDuplicationMetrics | undefined,\n headPathOf: (basePath: string) => string,\n lineSignaturesOf: (file: string) => LineSignatures | undefined\n): DuplicationChange {\n const headOccurrences = collectOccurrences(file.headFile?.metrics, headCross, file.changed.headPath);\n return {\n baseLines: file.baseContent === undefined ? undefined : splitLines(file.baseContent),\n headLines: file.headContent === undefined ? undefined : splitLines(file.headContent),\n baseDuplicatedLines:\n file.baseMetrics === undefined || file.changed.basePath === undefined\n ? new Set()\n : collectDuplicatedLineNumbers(file.baseMetrics, baseCross, file.changed.basePath),\n headDuplicatedLines:\n file.headFile === undefined\n ? new Set()\n : keepRepeatedLines(\n collectDuplicatedLineNumbers(file.headFile.metrics, headCross, file.changed.headPath),\n file.changed.headPath,\n headOccurrences,\n lineSignaturesOf\n ),\n baseOccurrences: collectOccurrences(file.baseMetrics, baseCross, file.changed.basePath, headPathOf),\n headOccurrences,\n hunks: file.hunks ?? [],\n };\n}\n\n/** A file's per-line normalized token signatures and how many lines carry each. */\ninterface LineSignatures {\n byLine: Map<number, string>;\n lineCounts: Map<string, number>;\n}\n\n/** Line signatures of the scanned head files, computed on first use. */\nfunction createLineSignatureIndex(scannedFiles: ScannedFile[]): (file: string) => LineSignatures | undefined {\n const tokensByFile = new Map(\n scannedFiles.map(({ relativePath, file }) => [relativePath, file.duplicationCandidates?.tokens])\n );\n const signaturesByFile = new Map<string, LineSignatures | undefined>();\n return (file) => {\n if (!signaturesByFile.has(file)) {\n const tokens = tokensByFile.get(file);\n signaturesByFile.set(file, tokens && signLines(tokens));\n }\n return signaturesByFile.get(file);\n };\n}\n\n/**\n * Each line's tokens with identifiers numbered by first occurrence, as clone detection anonymizes\n * them.\n */\nfunction signLines(tokens: readonly Token[]): LineSignatures {\n const byLine = new Map<number, string>();\n const identifierIndexesByLine = new Map<number, Map<string, number>>();\n for (const token of tokens) {\n const line = token.startRow + 1;\n let text = token.text;\n if (token.kind === 'id') {\n // Numbered by first occurrence within the line, so `a + a` and `a + b` stay distinct.\n const identifierIndexes = identifierIndexesByLine.get(line) ?? new Map<string, number>();\n identifierIndexesByLine.set(line, identifierIndexes);\n if (!identifierIndexes.has(token.text)) {\n identifierIndexes.set(token.text, identifierIndexes.size);\n }\n text = `\\u0001${identifierIndexes.get(token.text)}`;\n }\n byLine.set(line, `${byLine.get(line) ?? ''}${text}\\u0000`);\n }\n const lineCounts = new Map<string, number>();\n for (const signature of byLine.values()) {\n lineCounts.set(signature, (lineCounts.get(signature) ?? 0) + 1);\n }\n return { byLine, lineCounts };\n}\n\n/**\n * The duplicated lines whose normalized tokens a partner file of their clones (the file itself,\n * on another line, for `''`) repeats on some line. Near-miss coverage also marks lines the copies\n * do not share, such as a new line inserted into an old near-miss clone, which copied nothing.\n */\nfunction keepRepeatedLines(\n lines: ReadonlySet<number>,\n file: string,\n occurrences: readonly CloneOccurrence[],\n lineSignaturesOf: (file: string) => LineSignatures | undefined\n): Set<number> {\n const own = lineSignaturesOf(file);\n if (own === undefined) {\n return new Set(lines);\n }\n const repeats = (signature: string, partner: string): boolean => {\n if (partner === '') {\n return (own.lineCounts.get(signature) ?? 0) > 1;\n }\n const partnerSignatures = lineSignaturesOf(partner);\n return partnerSignatures === undefined || partnerSignatures.lineCounts.has(signature);\n };\n const partnersByLine = indexPartnersByLine(occurrences);\n return new Set(\n [...lines].filter((line) => {\n const signature = own.byLine.get(line);\n return (\n signature === undefined || [...(partnersByLine.get(line) ?? [])].some((partner) => repeats(signature, partner))\n );\n })\n );\n}\n\n/**\n * The file's clone occurrences with their partner files (`''` for the file itself), partner paths\n * renamed by `renamePartner` so base partners compare with head ones.\n */\nfunction collectOccurrences(\n metrics: CodeMetrics | undefined,\n cross: CrossFileDuplicationMetrics | undefined,\n file: string | undefined,\n renamePartner: (partner: string) => string = (partner) => partner\n): CloneOccurrence[] {\n if (metrics === undefined || file === undefined) {\n return [];\n }\n const withinFile = metrics.duplication.duplicateBlockGroups.flatMap((group) =>\n group.map(({ startLine, endLine }) => ({ startLine, endLine, partners: [''] }))\n );\n const crossFile = (cross?.groups ?? []).flatMap((group) => {\n const own = group.occurrences.filter((occurrence) => occurrence.file === file);\n const partners = [\n ...group.files.filter((partner) => partner !== file).map(renamePartner),\n ...(own.length > 1 ? [''] : []),\n ];\n return own.map(({ startLine, endLine }) => ({ startLine, endLine, partners }));\n });\n return [...withinFile, ...crossFile];\n}\n\nfunction toGateInput(\n file: PreparedFile,\n newlyDuplicatedLines: number[],\n headCross: CrossFileDuplicationMetrics | undefined\n): GateFileInput {\n return {\n file: file.displayFile,\n baseMetrics: file.baseMetrics,\n headMetrics: file.headFile?.metrics,\n baseFunctionTokens: file.baseFunctionTokens,\n headFunctionTokens: file.headFunctionTokens,\n newlyDuplicatedLines,\n duplicationPartners: collectPartners(file, newlyDuplicatedLines, headCross),\n gated: file.gated,\n };\n}\n\n/** Other files sharing a cross-file clone that covers the given lines. */\nfunction collectPartners(\n file: PreparedFile,\n lines: number[],\n headCross: CrossFileDuplicationMetrics | undefined\n): string[] {\n const headPath = file.changed.headPath;\n const covers = (occurrence: CrossFileDuplicateOccurrence): boolean =>\n occurrence.file === headPath && lines.some((line) => line >= occurrence.startLine && line <= occurrence.endLine);\n const partners = new Set<string>();\n for (const group of headCross?.groups ?? []) {\n if (group.occurrences.some(covers)) {\n for (const partner of group.files) {\n if (partner !== headPath) {\n partners.add(partner);\n }\n }\n }\n }\n return [...partners].toSorted();\n}\n\nfunction printTextReport(\n cliOptions: DiffCliOptions,\n mergeBase: string,\n result: GateResult,\n errors: string[],\n warnings: string[]\n): void {\n const shortBase = mergeBase.slice(0, 12);\n if (errors.length > 0) {\n // Unmeasured files were not gated, so \"0 violations\" would be vacuous; never claim a pass.\n writeStdout(\n `Regression gate could not complete: ${errors.length} measurement failures (details on stderr)` +\n `${result.violations.length > 0 ? `; ${result.violations.length} violations in the measured files` : ''} (base ${cliOptions.base}, merge-base ${shortBase}).\\n`\n );\n printViolations(result);\n } else if (result.violations.length === 0) {\n writeStdout(\n `Regression gate passed: ${result.checkedFileCount} changed files, ${result.checkedFunctionCount} functions checked (base ${cliOptions.base}, merge-base ${shortBase}).\\n`\n );\n } else {\n writeStdout(\n `Regression gate vs ${cliOptions.base} (merge-base ${shortBase}): ${result.violations.length} violations\\n`\n );\n printViolations(result);\n }\n\n if (cliOptions.full) {\n printFullDetails(result);\n }\n\n for (const warning of warnings) {\n writeStderr(`Warning: ${warning}\\n`);\n }\n for (const error of errors) {\n writeStderr(`Error: ${error}\\n`);\n }\n}\n\nfunction printViolations(result: GateResult): void {\n for (const [index, violation] of result.violations.entries()) {\n writeStdout(`${index + 1}. ${violation.message}\\n`);\n }\n}\n\n/** Base -> head values of every checked function; kept behind --full for humans and trending. */\nfunction printFullDetails(result: GateResult): void {\n if (result.checkedFunctions.length === 0) {\n return;\n }\n writeStdout('\\nChecked functions (base -> head):\\n');\n for (const report of result.checkedFunctions) {\n writeStdout(`- ${formatFunctionReport(report)}\\n`);\n }\n}\n\nfunction formatFunctionReport(report: CheckedFunctionReport): string {\n const range = (\n select: (values: GateFunctionValues) => number,\n format: (value: number) => string = String\n ): string => {\n const head = format(select(report.head));\n return report.base ? `${format(select(report.base))} -> ${head}` : head;\n };\n const values = [\n `cognitive ${range((fn) => fn.cognitiveComplexity)}`,\n `NCSS ${range((fn) => fn.ncss)}`,\n `nesting ${range((fn) => fn.nestingDepth)}`,\n `DepDegree ${range((fn) => fn.depDegree)}`,\n `volume ${range(\n (fn) => fn.halsteadVolume,\n (value) => value.toFixed(1)\n )}`,\n ];\n return `${report.file}:${report.startLine}-${report.endLine} ${report.name}${report.base ? '' : ' (new)'}: ${values.join(', ')}`;\n}\n\nfunction printJsonReport(\n cliOptions: DiffCliOptions,\n mergeBase: string,\n result: GateResult,\n inputs: GateFileInput[],\n errors: string[],\n warnings: string[]\n): void {\n const report: Record<string, unknown> = {\n base: cliOptions.base,\n mergeBase,\n passed: result.violations.length === 0 && errors.length === 0,\n violations: result.violations,\n checkedFileCount: result.checkedFileCount,\n checkedFunctionCount: result.checkedFunctionCount,\n newFunctionCount: result.newFunctionCount,\n errors,\n warnings,\n };\n if (cliOptions.full) {\n report.files = inputs\n .filter((input) => input.gated !== false)\n .map((input) => ({\n file: input.file,\n baseFunctionCount: input.baseMetrics?.functions.length ?? 0,\n headFunctionCount: input.headMetrics?.functions.length ?? 0,\n baseNcss: input.baseMetrics?.ncssCount ?? 0,\n headNcss: input.headMetrics?.ncssCount ?? 0,\n baseMaxCognitiveComplexity: input.baseMetrics?.maxCognitiveComplexity ?? 0,\n headMaxCognitiveComplexity: input.headMetrics?.maxCognitiveComplexity ?? 0,\n newlyDuplicatedLines: input.newlyDuplicatedLines,\n duplicationPartners: input.duplicationPartners,\n functions: result.checkedFunctions.filter((fn) => fn.file === input.file),\n }));\n }\n writeStdout(JSON.stringify(report, undefined, 2) + '\\n');\n}\n"],"mappings":"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"}
|