ucn 3.4.6 → 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/.claude/skills/ucn/SKILL.md +3 -3
- package/core/discovery.js +2 -2
- package/core/project.js +22 -3
- package/mcp/server.js +8 -8
- package/package.json +3 -3
- package/test/parser.test.js +247 -0
|
@@ -81,15 +81,15 @@ ucn deadcode --exclude=test # Skip test files (most useful)
|
|
|
81
81
|
|
|
82
82
|
| Situation | Command | What it does |
|
|
83
83
|
|-----------|---------|-------------|
|
|
84
|
-
| Need function + all its helpers inline | `ucn smart <name>` | Returns function source with every helper it calls expanded below it |
|
|
84
|
+
| Need function + all its helpers inline | `ucn smart <name>` | Returns function source with every helper it calls expanded below it. Use instead of `about` when you need code, not metadata |
|
|
85
85
|
| Checking if a refactor broke signatures | `ucn verify <name>` | Validates all call sites match the function's parameter count |
|
|
86
86
|
| Understanding a file's role in the project | `ucn imports <file>` | What it depends on |
|
|
87
87
|
| Understanding who depends on a file | `ucn exporters <file>` | Which files import it |
|
|
88
88
|
| Quick project overview | `ucn toc` | Every file with function/class counts and line counts |
|
|
89
89
|
| Finding all usages (not just calls) | `ucn usages <name>` | Groups into: definitions, calls, imports, type references |
|
|
90
|
-
| Finding related
|
|
90
|
+
| Finding sibling/related functions | `ucn related <name>` | Name-based + structural matching (same file, shared deps). Not semantic — best for parse/format pairs |
|
|
91
91
|
| Preview a rename or param change | `ucn plan <name> --rename-to=new_name` | Shows what would change without doing it |
|
|
92
|
-
|
|
|
92
|
+
| File-level dependency tree | `ucn graph <file> --depth=1` | Visual import tree. Can be noisy — use depth=1 for large/tightly-coupled projects. For function-level flow, use `trace` instead |
|
|
93
93
|
| Find which tests cover a function | `ucn tests <name>` | Test files and test function names |
|
|
94
94
|
|
|
95
95
|
## Command Format
|
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/mcp/server.js
CHANGED
|
@@ -807,7 +807,7 @@ server.registerTool(
|
|
|
807
807
|
server.registerTool(
|
|
808
808
|
'ucn_about',
|
|
809
809
|
{
|
|
810
|
-
description: 'Everything about a
|
|
810
|
+
description: 'Everything about a symbol in one call: definition, source code, callers, callees, tests. START HERE when investigating any function or class — replaces 3-4 grep+read cycles. For narrower views, use ucn_context (callers/callees only), ucn_smart (code + dependencies), or ucn_impact (call sites for refactoring).',
|
|
811
811
|
inputSchema: z.object({
|
|
812
812
|
project_dir: projectDirParam,
|
|
813
813
|
name: nameParam,
|
|
@@ -832,7 +832,7 @@ server.registerTool(
|
|
|
832
832
|
server.registerTool(
|
|
833
833
|
'ucn_context',
|
|
834
834
|
{
|
|
835
|
-
description: '
|
|
835
|
+
description: 'Lightweight caller/callee list with numbered items. Use when you just need "who calls X and what does X call" without full source code. Items are numbered — use ucn_expand to drill into any item. For the full picture (code + tests + everything), use ucn_about instead.',
|
|
836
836
|
inputSchema: z.object({
|
|
837
837
|
project_dir: projectDirParam,
|
|
838
838
|
name: nameParam,
|
|
@@ -866,7 +866,7 @@ server.registerTool(
|
|
|
866
866
|
server.registerTool(
|
|
867
867
|
'ucn_impact',
|
|
868
868
|
{
|
|
869
|
-
description: '
|
|
869
|
+
description: 'Every call site of a function, grouped by file, with the actual arguments used at each site. Use BEFORE changing a function signature — shows exactly what will break. For a lighter caller list without arguments, use ucn_context.',
|
|
870
870
|
inputSchema: z.object({
|
|
871
871
|
project_dir: projectDirParam,
|
|
872
872
|
name: nameParam,
|
|
@@ -890,7 +890,7 @@ server.registerTool(
|
|
|
890
890
|
server.registerTool(
|
|
891
891
|
'ucn_smart',
|
|
892
892
|
{
|
|
893
|
-
description: 'Function source code with all its dependencies expanded inline.
|
|
893
|
+
description: 'Function source code with all its dependencies expanded inline. Use when you need to read or modify a function and want its helpers included — saves multiple file reads. For call relationships without source code, use ucn_context.',
|
|
894
894
|
inputSchema: z.object({
|
|
895
895
|
project_dir: projectDirParam,
|
|
896
896
|
name: nameParam,
|
|
@@ -922,7 +922,7 @@ server.registerTool(
|
|
|
922
922
|
server.registerTool(
|
|
923
923
|
'ucn_trace',
|
|
924
924
|
{
|
|
925
|
-
description: 'Call tree visualization showing execution flow.
|
|
925
|
+
description: 'Call tree visualization showing execution flow from a function downward. Maps architecture — shows which modules a pipeline touches. For file-level dependency trees, use ucn_graph instead.',
|
|
926
926
|
inputSchema: z.object({
|
|
927
927
|
project_dir: projectDirParam,
|
|
928
928
|
name: nameParam,
|
|
@@ -1180,7 +1180,7 @@ server.registerTool(
|
|
|
1180
1180
|
server.registerTool(
|
|
1181
1181
|
'ucn_related',
|
|
1182
1182
|
{
|
|
1183
|
-
description: 'Find
|
|
1183
|
+
description: 'Find structurally related functions: same file, similar names, shared callers/callees. Results are name-based and structural, not semantic — best for finding sibling functions (e.g. parse/format pairs) rather than conceptually related code.',
|
|
1184
1184
|
inputSchema: z.object({
|
|
1185
1185
|
project_dir: projectDirParam,
|
|
1186
1186
|
name: nameParam,
|
|
@@ -1204,7 +1204,7 @@ server.registerTool(
|
|
|
1204
1204
|
server.registerTool(
|
|
1205
1205
|
'ucn_graph',
|
|
1206
1206
|
{
|
|
1207
|
-
description: '
|
|
1207
|
+
description: 'File-level dependency graph showing import/export relationships between files. Best for understanding module structure. Can be noisy in tightly-coupled projects — use depth=1 for large codebases. For function-level execution flow, use ucn_trace instead.',
|
|
1208
1208
|
inputSchema: z.object({
|
|
1209
1209
|
project_dir: projectDirParam,
|
|
1210
1210
|
file: z.string().describe('File path (relative to project root or absolute) to graph dependencies for'),
|
|
@@ -1490,7 +1490,7 @@ server.registerTool(
|
|
|
1490
1490
|
server.registerTool(
|
|
1491
1491
|
'ucn_api',
|
|
1492
1492
|
{
|
|
1493
|
-
description: 'Show exported/public symbols in the project
|
|
1493
|
+
description: 'Show exported/public symbols in the project. Works best with JS/TS (export keyword), Go (capitalized names), Rust (pub), Java (public). For Python, requires __all__ — projects without it will return empty results. Use ucn_toc for a general overview instead.',
|
|
1494
1494
|
inputSchema: z.object({
|
|
1495
1495
|
project_dir: projectDirParam,
|
|
1496
1496
|
file: z.string().optional().describe('Optional file path to show exports for (relative to project root)')
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "3.4.
|
|
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": {
|
|
7
|
-
"ucn": "
|
|
8
|
-
"ucn-mcp": "
|
|
7
|
+
"ucn": "cli/index.js",
|
|
8
|
+
"ucn-mcp": "mcp/server.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"test": "node --test test/parser.test.js"
|
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');
|