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 CHANGED
@@ -95,7 +95,7 @@ const TEST_PATTERNS = {
95
95
  python: [
96
96
  /^test_.*\.py$/,
97
97
  /.*_test\.py$/,
98
- /\/tests?\//
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
- /\/tests\//
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "3.4.7",
3
+ "version": "3.4.8",
4
4
  "description": "Code navigation built by AI, for AI. Reduces context usage when working with large codebases.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -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');