ucn 3.4.7 → 3.4.8
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/core/discovery.js +2 -2
- package/core/project.js +22 -3
- package/package.json +1 -1
- package/test/parser.test.js +247 -0
package/core/discovery.js
CHANGED
|
@@ -95,7 +95,7 @@ const TEST_PATTERNS = {
|
|
|
95
95
|
python: [
|
|
96
96
|
/^test_.*\.py$/,
|
|
97
97
|
/.*_test\.py$/,
|
|
98
|
-
|
|
98
|
+
/(^|\/)tests?\//
|
|
99
99
|
],
|
|
100
100
|
go: [
|
|
101
101
|
/.*_test\.go$/
|
|
@@ -107,7 +107,7 @@ const TEST_PATTERNS = {
|
|
|
107
107
|
],
|
|
108
108
|
rust: [
|
|
109
109
|
/.*_test\.rs$/,
|
|
110
|
-
|
|
110
|
+
/(^|\/)tests\//
|
|
111
111
|
]
|
|
112
112
|
};
|
|
113
113
|
|
package/core/project.js
CHANGED
|
@@ -2127,12 +2127,22 @@ class ProjectIndex {
|
|
|
2127
2127
|
|
|
2128
2128
|
// Python: Magic/dunder methods are called by the interpreter, not user code
|
|
2129
2129
|
// test_* functions/methods are called by pytest/unittest via reflection
|
|
2130
|
+
// setUp/tearDown are unittest.TestCase framework methods called by test runner
|
|
2131
|
+
// pytest_* are pytest plugin hooks called by the framework
|
|
2130
2132
|
const isPythonEntryPoint = lang === 'python' &&
|
|
2131
|
-
(/^__\w+__$/.test(name) || /^test_/.test(name)
|
|
2133
|
+
(/^__\w+__$/.test(name) || /^test_/.test(name) ||
|
|
2134
|
+
/^(setUp|tearDown)(Class|Module)?$/.test(name) ||
|
|
2135
|
+
/^pytest_/.test(name));
|
|
2132
2136
|
|
|
2133
|
-
// Rust: main() is entry point, #[test] functions are called by test runner
|
|
2137
|
+
// Rust: main() is entry point, #[test] and #[bench] functions are called by test/bench runner
|
|
2134
2138
|
const isRustEntryPoint = lang === 'rust' &&
|
|
2135
|
-
(name === 'main' || mods.includes('test'));
|
|
2139
|
+
(name === 'main' || mods.includes('test') || mods.includes('bench'));
|
|
2140
|
+
|
|
2141
|
+
// Rust: trait impl methods are invoked via trait dispatch, not direct calls
|
|
2142
|
+
// They can never be "dead" - the trait contract requires them to exist
|
|
2143
|
+
// className for trait impls contains " for " (e.g., "PartialEq for Glob")
|
|
2144
|
+
const isRustTraitImpl = lang === 'rust' && symbol.isMethod &&
|
|
2145
|
+
symbol.className && symbol.className.includes(' for ');
|
|
2136
2146
|
|
|
2137
2147
|
// Go: Test*, Benchmark*, Example* functions are called by go test
|
|
2138
2148
|
const isGoTestFunc = lang === 'go' &&
|
|
@@ -2141,6 +2151,15 @@ class ProjectIndex {
|
|
|
2141
2151
|
// Java: @Test annotated methods are called by JUnit
|
|
2142
2152
|
const isJavaTestMethod = lang === 'java' && mods.includes('test');
|
|
2143
2153
|
|
|
2154
|
+
// Java: @Override methods are invoked via polymorphic dispatch
|
|
2155
|
+
// They implement interface/superclass contracts and can't be dead
|
|
2156
|
+
const isJavaOverride = lang === 'java' && mods.includes('override');
|
|
2157
|
+
|
|
2158
|
+
// Skip trait impl / @Override methods entirely - they're required by the type system
|
|
2159
|
+
if (isRustTraitImpl || isJavaOverride) {
|
|
2160
|
+
continue;
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2144
2163
|
const isEntryPoint = isGoEntryPoint || isGoTestFunc ||
|
|
2145
2164
|
isJavaEntryPoint || isJavaTestMethod ||
|
|
2146
2165
|
isPythonEntryPoint || isRustEntryPoint;
|
package/package.json
CHANGED
package/test/parser.test.js
CHANGED
|
@@ -6631,5 +6631,252 @@ module.exports = { usedUtil };
|
|
|
6631
6631
|
});
|
|
6632
6632
|
});
|
|
6633
6633
|
|
|
6634
|
+
// Regression: Rust trait impl methods should not appear as deadcode
|
|
6635
|
+
describe('Regression: deadcode skips Rust trait impl methods', () => {
|
|
6636
|
+
it('should not report trait impl methods as dead code', () => {
|
|
6637
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-test-rust-trait-'));
|
|
6638
|
+
try {
|
|
6639
|
+
fs.writeFileSync(path.join(tmpDir, 'Cargo.toml'), '[package]\nname = "test"\nversion = "0.1.0"');
|
|
6640
|
+
fs.mkdirSync(path.join(tmpDir, 'src'), { recursive: true });
|
|
6641
|
+
// A struct with an inherent impl and a trait impl
|
|
6642
|
+
fs.writeFileSync(path.join(tmpDir, 'src', 'main.rs'), `
|
|
6643
|
+
struct Foo {
|
|
6644
|
+
val: i32,
|
|
6645
|
+
}
|
|
6646
|
+
|
|
6647
|
+
impl Foo {
|
|
6648
|
+
fn new(val: i32) -> Self {
|
|
6649
|
+
Foo { val }
|
|
6650
|
+
}
|
|
6651
|
+
|
|
6652
|
+
fn unused_method(&self) -> i32 {
|
|
6653
|
+
self.val
|
|
6654
|
+
}
|
|
6655
|
+
}
|
|
6656
|
+
|
|
6657
|
+
impl std::fmt::Display for Foo {
|
|
6658
|
+
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
6659
|
+
write!(f, "{}", self.val)
|
|
6660
|
+
}
|
|
6661
|
+
}
|
|
6662
|
+
|
|
6663
|
+
impl PartialEq for Foo {
|
|
6664
|
+
fn eq(&self, other: &Self) -> bool {
|
|
6665
|
+
self.val == other.val
|
|
6666
|
+
}
|
|
6667
|
+
}
|
|
6668
|
+
|
|
6669
|
+
fn main() {
|
|
6670
|
+
let f = Foo::new(42);
|
|
6671
|
+
}
|
|
6672
|
+
`);
|
|
6673
|
+
const idx = new ProjectIndex(tmpDir);
|
|
6674
|
+
idx.build(null, { quiet: true });
|
|
6675
|
+
const dead = idx.deadcode();
|
|
6676
|
+
const deadNames = dead.map(d => d.name);
|
|
6677
|
+
|
|
6678
|
+
// Trait impl methods should NOT appear
|
|
6679
|
+
assert.ok(!deadNames.includes('fmt'), 'fmt (trait impl) should not be dead code');
|
|
6680
|
+
assert.ok(!deadNames.includes('eq'), 'eq (trait impl) should not be dead code');
|
|
6681
|
+
|
|
6682
|
+
// Genuinely unused inherent method SHOULD appear
|
|
6683
|
+
assert.ok(deadNames.includes('unused_method'), 'unused_method should be dead code');
|
|
6684
|
+
} finally {
|
|
6685
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
6686
|
+
}
|
|
6687
|
+
});
|
|
6688
|
+
});
|
|
6689
|
+
|
|
6690
|
+
// Regression: Rust #[bench] functions should be treated as entry points
|
|
6691
|
+
describe('Regression: deadcode treats Rust #[bench] as entry points', () => {
|
|
6692
|
+
it('should not report #[bench] functions as dead code', () => {
|
|
6693
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-test-rust-bench-'));
|
|
6694
|
+
try {
|
|
6695
|
+
fs.writeFileSync(path.join(tmpDir, 'Cargo.toml'), '[package]\nname = "test"\nversion = "0.1.0"');
|
|
6696
|
+
fs.mkdirSync(path.join(tmpDir, 'src'), { recursive: true });
|
|
6697
|
+
fs.mkdirSync(path.join(tmpDir, 'benches'), { recursive: true });
|
|
6698
|
+
fs.writeFileSync(path.join(tmpDir, 'src', 'main.rs'), `
|
|
6699
|
+
fn main() {}
|
|
6700
|
+
|
|
6701
|
+
fn helper() -> i32 { 42 }
|
|
6702
|
+
`);
|
|
6703
|
+
fs.writeFileSync(path.join(tmpDir, 'benches', 'my_bench.rs'), `
|
|
6704
|
+
#![feature(test)]
|
|
6705
|
+
extern crate test;
|
|
6706
|
+
use test::Bencher;
|
|
6707
|
+
|
|
6708
|
+
#[bench]
|
|
6709
|
+
fn bench_something(b: &mut Bencher) {
|
|
6710
|
+
b.iter(|| 1 + 1);
|
|
6711
|
+
}
|
|
6712
|
+
|
|
6713
|
+
fn unused_bench_helper() -> i32 {
|
|
6714
|
+
42
|
|
6715
|
+
}
|
|
6716
|
+
`);
|
|
6717
|
+
const idx = new ProjectIndex(tmpDir);
|
|
6718
|
+
idx.build(null, { quiet: true });
|
|
6719
|
+
const dead = idx.deadcode();
|
|
6720
|
+
const deadNames = dead.map(d => d.name);
|
|
6721
|
+
|
|
6722
|
+
// #[bench] should NOT appear as dead code
|
|
6723
|
+
assert.ok(!deadNames.includes('bench_something'), 'bench_something should not be dead code');
|
|
6724
|
+
|
|
6725
|
+
// Genuinely unused function SHOULD appear
|
|
6726
|
+
assert.ok(deadNames.includes('unused_bench_helper'), 'unused_bench_helper should be dead code');
|
|
6727
|
+
} finally {
|
|
6728
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
6729
|
+
}
|
|
6730
|
+
});
|
|
6731
|
+
});
|
|
6732
|
+
|
|
6733
|
+
// Regression: test file patterns should match relative paths starting with tests/
|
|
6734
|
+
describe('Regression: test file patterns match relative paths', () => {
|
|
6735
|
+
it('should detect tests/ at start of relative path for Python', () => {
|
|
6736
|
+
const { isTestFile } = require('../core/discovery');
|
|
6737
|
+
// Relative paths starting with tests/ should match
|
|
6738
|
+
assert.ok(isTestFile('tests/test_app.py', 'python'),
|
|
6739
|
+
'tests/test_app.py should be a test file');
|
|
6740
|
+
assert.ok(isTestFile('tests/helpers/factory.py', 'python'),
|
|
6741
|
+
'tests/helpers/factory.py should be a test file');
|
|
6742
|
+
// Subdirectory should still work
|
|
6743
|
+
assert.ok(isTestFile('src/tests/test_util.py', 'python'),
|
|
6744
|
+
'src/tests/test_util.py should be a test file');
|
|
6745
|
+
// Non-test paths should not match
|
|
6746
|
+
assert.ok(!isTestFile('src/utils.py', 'python'),
|
|
6747
|
+
'src/utils.py should not be a test file');
|
|
6748
|
+
});
|
|
6749
|
+
|
|
6750
|
+
it('should detect tests/ at start of relative path for Rust', () => {
|
|
6751
|
+
const { isTestFile } = require('../core/discovery');
|
|
6752
|
+
assert.ok(isTestFile('tests/integration.rs', 'rust'),
|
|
6753
|
+
'tests/integration.rs should be a test file');
|
|
6754
|
+
assert.ok(isTestFile('tests/examples/hello.rs', 'rust'),
|
|
6755
|
+
'tests/examples/hello.rs should be a test file');
|
|
6756
|
+
// Non-test paths should not match
|
|
6757
|
+
assert.ok(!isTestFile('src/lib.rs', 'rust'),
|
|
6758
|
+
'src/lib.rs should not be a test file');
|
|
6759
|
+
});
|
|
6760
|
+
});
|
|
6761
|
+
|
|
6762
|
+
// Regression: Java @Override methods should not appear as deadcode
|
|
6763
|
+
describe('Regression: deadcode skips Java @Override methods', () => {
|
|
6764
|
+
it('should not report @Override methods as dead code', () => {
|
|
6765
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-test-java-override-'));
|
|
6766
|
+
try {
|
|
6767
|
+
fs.mkdirSync(path.join(tmpDir, 'src'), { recursive: true });
|
|
6768
|
+
fs.writeFileSync(path.join(tmpDir, 'pom.xml'), '<project></project>');
|
|
6769
|
+
fs.writeFileSync(path.join(tmpDir, 'src', 'MyClass.java'), `
|
|
6770
|
+
public class MyClass implements Runnable {
|
|
6771
|
+
@Override
|
|
6772
|
+
public void run() {
|
|
6773
|
+
System.out.println("running");
|
|
6774
|
+
}
|
|
6775
|
+
|
|
6776
|
+
@Override
|
|
6777
|
+
public String toString() {
|
|
6778
|
+
return "MyClass";
|
|
6779
|
+
}
|
|
6780
|
+
|
|
6781
|
+
void unusedMethod() {
|
|
6782
|
+
System.out.println("unused");
|
|
6783
|
+
}
|
|
6784
|
+
|
|
6785
|
+
public static void main(String[] args) {
|
|
6786
|
+
new MyClass().run();
|
|
6787
|
+
}
|
|
6788
|
+
}
|
|
6789
|
+
`);
|
|
6790
|
+
const idx = new ProjectIndex(tmpDir);
|
|
6791
|
+
idx.build(null, { quiet: true });
|
|
6792
|
+
const dead = idx.deadcode();
|
|
6793
|
+
const deadNames = dead.map(d => d.name);
|
|
6794
|
+
|
|
6795
|
+
// @Override methods should NOT appear
|
|
6796
|
+
assert.ok(!deadNames.includes('run'), 'run (@Override) should not be dead code');
|
|
6797
|
+
assert.ok(!deadNames.includes('toString'), 'toString (@Override) should not be dead code');
|
|
6798
|
+
|
|
6799
|
+
// Genuinely unused method SHOULD appear
|
|
6800
|
+
assert.ok(deadNames.includes('unusedMethod'), 'unusedMethod should be dead code');
|
|
6801
|
+
} finally {
|
|
6802
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
6803
|
+
}
|
|
6804
|
+
});
|
|
6805
|
+
});
|
|
6806
|
+
|
|
6807
|
+
// Regression: Python setUp/tearDown and pytest_* should be entry points
|
|
6808
|
+
describe('Regression: deadcode treats Python framework methods as entry points', () => {
|
|
6809
|
+
it('should not report setUp/tearDown as dead code', () => {
|
|
6810
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-test-py-setup-'));
|
|
6811
|
+
try {
|
|
6812
|
+
fs.writeFileSync(path.join(tmpDir, 'pyproject.toml'), '[project]\nname = "test"');
|
|
6813
|
+
// setUp/tearDown in a non-test file (e.g., scripts/ directory)
|
|
6814
|
+
fs.writeFileSync(path.join(tmpDir, 'release_tests.py'), `
|
|
6815
|
+
import unittest
|
|
6816
|
+
|
|
6817
|
+
class TestFoo(unittest.TestCase):
|
|
6818
|
+
def setUp(self):
|
|
6819
|
+
self.x = 42
|
|
6820
|
+
|
|
6821
|
+
def tearDown(self):
|
|
6822
|
+
pass
|
|
6823
|
+
|
|
6824
|
+
def test_something(self):
|
|
6825
|
+
assert self.x == 42
|
|
6826
|
+
`);
|
|
6827
|
+
// Separate non-test file with genuinely unused code
|
|
6828
|
+
fs.writeFileSync(path.join(tmpDir, 'utils.py'), `
|
|
6829
|
+
def unused_helper():
|
|
6830
|
+
return 1
|
|
6831
|
+
`);
|
|
6832
|
+
const idx = new ProjectIndex(tmpDir);
|
|
6833
|
+
idx.build(null, { quiet: true });
|
|
6834
|
+
const dead = idx.deadcode();
|
|
6835
|
+
const deadNames = dead.map(d => d.name);
|
|
6836
|
+
|
|
6837
|
+
// Framework methods should NOT appear (even in non-test files)
|
|
6838
|
+
assert.ok(!deadNames.includes('setUp'), 'setUp should not be dead code');
|
|
6839
|
+
assert.ok(!deadNames.includes('tearDown'), 'tearDown should not be dead code');
|
|
6840
|
+
assert.ok(!deadNames.includes('test_something'), 'test_something should not be dead code');
|
|
6841
|
+
|
|
6842
|
+
// Genuinely unused function SHOULD appear
|
|
6843
|
+
assert.ok(deadNames.includes('unused_helper'), 'unused_helper should be dead code');
|
|
6844
|
+
} finally {
|
|
6845
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
6846
|
+
}
|
|
6847
|
+
});
|
|
6848
|
+
|
|
6849
|
+
it('should not report pytest_* hooks as dead code', () => {
|
|
6850
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-test-py-pytest-'));
|
|
6851
|
+
try {
|
|
6852
|
+
fs.writeFileSync(path.join(tmpDir, 'pyproject.toml'), '[project]\nname = "test"');
|
|
6853
|
+
fs.writeFileSync(path.join(tmpDir, 'conftest.py'), `
|
|
6854
|
+
def pytest_configure(config):
|
|
6855
|
+
config.addinivalue_line("markers", "slow: slow test")
|
|
6856
|
+
|
|
6857
|
+
def pytest_collection_modifyitems(config, items):
|
|
6858
|
+
pass
|
|
6859
|
+
|
|
6860
|
+
def unused_function():
|
|
6861
|
+
return 1
|
|
6862
|
+
`);
|
|
6863
|
+
const idx = new ProjectIndex(tmpDir);
|
|
6864
|
+
idx.build(null, { quiet: true });
|
|
6865
|
+
const dead = idx.deadcode();
|
|
6866
|
+
const deadNames = dead.map(d => d.name);
|
|
6867
|
+
|
|
6868
|
+
// pytest hooks should NOT appear
|
|
6869
|
+
assert.ok(!deadNames.includes('pytest_configure'), 'pytest_configure should not be dead code');
|
|
6870
|
+
assert.ok(!deadNames.includes('pytest_collection_modifyitems'),
|
|
6871
|
+
'pytest_collection_modifyitems should not be dead code');
|
|
6872
|
+
|
|
6873
|
+
// Genuinely unused function SHOULD appear
|
|
6874
|
+
assert.ok(deadNames.includes('unused_function'), 'unused_function should be dead code');
|
|
6875
|
+
} finally {
|
|
6876
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
6877
|
+
}
|
|
6878
|
+
});
|
|
6879
|
+
});
|
|
6880
|
+
|
|
6634
6881
|
console.log('UCN v3 Test Suite');
|
|
6635
6882
|
console.log('Run with: node --test test/parser.test.js');
|