ucn 3.4.7 → 3.4.9

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/mcp/server.js CHANGED
@@ -756,7 +756,7 @@ function requireName(name) {
756
756
  server.registerTool(
757
757
  'ucn_toc',
758
758
  {
759
- description: 'Project overview: file counts, line counts, function/class counts per file. Use detailed=true to list all symbols. Works on JS/TS, Python, Go, Rust, Java.',
759
+ description: 'Get a quick overview of a project you haven\'t seen before. Shows file counts, line counts, function/class counts per file, largest files, and entry points. Use detailed=true to list every function and class. Start here when orienting in a new codebase, then use ucn_about or ucn_find to dive into specific symbols.',
760
760
  inputSchema: z.object({
761
761
  project_dir: projectDirParam,
762
762
  detailed: z.boolean().optional().describe('Show full symbol listing per file')
@@ -777,7 +777,7 @@ server.registerTool(
777
777
  server.registerTool(
778
778
  'ucn_find',
779
779
  {
780
- description: 'Find where a symbol is defined. Returns top matches sorted by usage count with signatures.',
780
+ description: 'Locate where a function, class, or method is defined. Use when you know the name but not the file. Returns top matches ranked by usage count with full signatures. Use file parameter to narrow results in large projects with common names.',
781
781
  inputSchema: z.object({
782
782
  project_dir: projectDirParam,
783
783
  name: nameParam,
@@ -807,7 +807,7 @@ server.registerTool(
807
807
  server.registerTool(
808
808
  'ucn_about',
809
809
  {
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).',
810
+ description: 'Your first stop when investigating any function or class. Returns everything in one call: definition with source code, who calls it, what it calls, and related tests. Replaces 3-4 grep+read cycles. Use this instead of reading files and grepping for callers manually. For narrower views: ucn_context (just callers/callees, no code), ucn_smart (code + dependencies inline), or ucn_impact (call sites with arguments, 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: '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.',
835
+ description: 'Quick answer to "who calls this function and what does it call?" without pulling source code. Lighter than ucn_about when you don\'t need the full picture. Results are numbered — drill into any item with ucn_expand to see its code. For classes/structs, shows all methods instead of callers/callees.',
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: '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.',
869
+ description: 'Shows every place a function is called, with the actual arguments passed at each call site. Essential before changing a function signature — tells you exactly what will break and what needs updating. Grouped by file for easy navigation. For a lighter caller overview without arguments, use ucn_context instead.',
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. 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.',
893
+ description: 'Get a function\'s source code with all its helper functions expanded inline. Use when you need to understand or modify a function and its dependencies in one read — saves opening multiple files. Better than reading whole files when you only need one function and its callees. For just the caller/callee list without 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 from a function downward. Maps architectureshows which modules a pipeline touches. For file-level dependency trees, use ucn_graph instead.',
925
+ description: 'Visualize the execution flow from a function downward as a call tree. Use when you need to understand "what happens when X runs" maps which modules and functions a pipeline touches without reading any files. Set depth to control how deep to trace (default: 3). For file-level import/export dependencies, use ucn_graph instead.',
926
926
  inputSchema: z.object({
927
927
  project_dir: projectDirParam,
928
928
  name: nameParam,
@@ -947,7 +947,7 @@ server.registerTool(
947
947
  server.registerTool(
948
948
  'ucn_usages',
949
949
  {
950
- description: 'All usages of a symbol grouped by type: definitions, calls, imports, references.',
950
+ description: 'See every usage of a symbol across the project, organized by type: definitions, calls, imports, and references. Use when you need the complete picture of how something is used — not just callers (ucn_context) or call sites (ucn_impact), but also imports and non-call references. Use code_only=true to skip matches in comments and strings.',
951
951
  inputSchema: z.object({
952
952
  project_dir: projectDirParam,
953
953
  name: nameParam,
@@ -981,7 +981,7 @@ server.registerTool(
981
981
  server.registerTool(
982
982
  'ucn_deadcode',
983
983
  {
984
- description: 'Find unused functions and classes with zero callers across the project.',
984
+ description: 'Find dead code: functions and classes with zero callers anywhere in the project. Use during cleanup to identify code that can be safely deleted. By default excludes exported symbols (they may be used externally) and test files — set include_exported=true to audit everything, or include_tests=true to check test helpers too.',
985
985
  inputSchema: z.object({
986
986
  project_dir: projectDirParam,
987
987
  include_exported: z.boolean().optional().describe('Include exported symbols (excluded by default)'),
@@ -1006,7 +1006,7 @@ server.registerTool(
1006
1006
  server.registerTool(
1007
1007
  'ucn_fn',
1008
1008
  {
1009
- description: "Extract a single function's source code from the project. Use file parameter to disambiguate when multiple functions share the same name.",
1009
+ description: "Extract just one function's source code. Use instead of reading an entire file when you only need a specific function — avoids pulling thousands of irrelevant lines. Use file parameter to disambiguate when multiple functions share the same name (e.g. file='parser' to get the one in parser.js).",
1010
1010
  inputSchema: z.object({
1011
1011
  project_dir: projectDirParam,
1012
1012
  name: nameParam,
@@ -1045,7 +1045,7 @@ server.registerTool(
1045
1045
  server.registerTool(
1046
1046
  'ucn_class',
1047
1047
  {
1048
- description: 'Extract a single class/struct/interface source code from the project.',
1048
+ description: 'Extract a single class, struct, or interface with all its methods. Use instead of reading an entire file when you only need one class definition. Handles all supported types: JS/TS classes, Python classes, Go structs, Rust structs/traits, Java classes/interfaces.',
1049
1049
  inputSchema: z.object({
1050
1050
  project_dir: projectDirParam,
1051
1051
  name: nameParam,
@@ -1091,7 +1091,7 @@ server.registerTool(
1091
1091
  server.registerTool(
1092
1092
  'ucn_verify',
1093
1093
  {
1094
- description: "Check all call sites match a function's parameter count. Use before changing a signature.",
1094
+ description: "Safety check before changing a function signature. Verifies that every call site passes the right number of arguments. Shows valid calls, mismatches, and uncertain cases. Run this before adding/removing parameters to catch breakage early — pair with ucn_plan to preview the refactoring.",
1095
1095
  inputSchema: z.object({
1096
1096
  project_dir: projectDirParam,
1097
1097
  name: nameParam,
@@ -1115,7 +1115,7 @@ server.registerTool(
1115
1115
  server.registerTool(
1116
1116
  'ucn_imports',
1117
1117
  {
1118
- description: 'What does a file import? Shows all import dependencies with resolved paths.',
1118
+ description: 'List all imports in a file with resolved file paths. Use to understand what a module depends on before modifying or moving it. Resolves relative imports, package imports, and language-specific patterns (Go modules, Rust crate paths, Java packages).',
1119
1119
  inputSchema: z.object({
1120
1120
  project_dir: projectDirParam,
1121
1121
  file: z.string().describe('File path (relative to project root or absolute) to analyze imports for')
@@ -1136,7 +1136,7 @@ server.registerTool(
1136
1136
  server.registerTool(
1137
1137
  'ucn_exporters',
1138
1138
  {
1139
- description: 'Who imports this file? Shows all files that depend on it.',
1139
+ description: 'Find every file that imports/depends on a given file. Use before moving, renaming, or deleting a file to see what would break. The reverse of ucn_imports — shows dependents rather than dependencies.',
1140
1140
  inputSchema: z.object({
1141
1141
  project_dir: projectDirParam,
1142
1142
  file: z.string().describe('File path (relative to project root or absolute) to find importers of')
@@ -1157,7 +1157,7 @@ server.registerTool(
1157
1157
  server.registerTool(
1158
1158
  'ucn_tests',
1159
1159
  {
1160
- description: 'Find test files and test cases for a function or file. Shows test-case matches, imports, and call sites in test files.',
1160
+ description: 'Find existing tests for a function. Shows which test files cover it, matching test case names, and how the function is called in tests. Use to check test coverage before modifying a function, or to find example test patterns to follow when writing new tests.',
1161
1161
  inputSchema: z.object({
1162
1162
  project_dir: projectDirParam,
1163
1163
  name: nameParam
@@ -1180,7 +1180,7 @@ server.registerTool(
1180
1180
  server.registerTool(
1181
1181
  'ucn_related',
1182
1182
  {
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.',
1183
+ description: 'Find sibling functions that are structurally related: same file, similar names, or shared callers/callees. Use to discover companion functions you might need to update together (e.g., finding serialize when you\'re changing deserialize, or findAll when modifying findOne). Name-based and structural, not semantic.',
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: '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.',
1207
+ description: 'Visualize how files depend on each other through imports/exports. Use to understand module architecture — which files form a cluster, what the dependency chain looks like. Set direction to "imports" (what this file uses), "importers" (who uses this file), or "both". Can be noisy — 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'),
@@ -1227,7 +1227,7 @@ server.registerTool(
1227
1227
  server.registerTool(
1228
1228
  'ucn_file_exports',
1229
1229
  {
1230
- description: "Show what a file exports (its public API). Lists exported functions, classes, and variables with signatures.",
1230
+ description: "Show a file's public API: all exported functions, classes, and variables with their signatures. Use to understand what a module offers before importing from it, or to review the surface area of a file you're about to refactor.",
1231
1231
  inputSchema: z.object({
1232
1232
  project_dir: projectDirParam,
1233
1233
  file: z.string().describe('File path (relative to project root or absolute) to list exports for')
@@ -1248,7 +1248,7 @@ server.registerTool(
1248
1248
  server.registerTool(
1249
1249
  'ucn_search',
1250
1250
  {
1251
- description: 'Text search across project files. Respects project ignores. Optionally filter to code only (exclude comments/strings).',
1251
+ description: 'Plain text search across all project files (like grep, but respects .gitignore and project excludes). Use for non-semantic searches: TODOs, error messages, config keys, string literals. For semantic code queries (callers, usages, definitions), prefer ucn_context/ucn_usages/ucn_find. Set code_only=true to skip matches in comments and strings.',
1252
1252
  inputSchema: z.object({
1253
1253
  project_dir: projectDirParam,
1254
1254
  term: z.string().describe('Search term (plain text, not regex)'),
@@ -1277,7 +1277,7 @@ server.registerTool(
1277
1277
  server.registerTool(
1278
1278
  'ucn_plan',
1279
1279
  {
1280
- description: 'Preview a refactoring: add/remove parameter or rename function. Shows before/after signatures and all call sites that need updating.',
1280
+ description: 'Preview a refactoring before doing it. Shows before/after signatures and every call site that needs updating. Supports three operations: add a parameter (with optional default value for backward compatibility), remove a parameter, or rename the function. Pair with ucn_verify to check current state first.',
1281
1281
  inputSchema: z.object({
1282
1282
  project_dir: projectDirParam,
1283
1283
  name: nameParam,
@@ -1312,7 +1312,7 @@ server.registerTool(
1312
1312
  server.registerTool(
1313
1313
  'ucn_typedef',
1314
1314
  {
1315
- description: 'Find type/interface/enum/struct/trait definitions matching a name.',
1315
+ description: 'Find type definitions: interfaces, enums, structs, traits, or type aliases matching a name. Use when you need to see the shape of a type — what fields a struct has, what methods an interface requires, or what values an enum contains.',
1316
1316
  inputSchema: z.object({
1317
1317
  project_dir: projectDirParam,
1318
1318
  name: nameParam
@@ -1335,7 +1335,7 @@ server.registerTool(
1335
1335
  server.registerTool(
1336
1336
  'ucn_stacktrace',
1337
1337
  {
1338
- description: 'Parse a stack trace and show source code context for each frame. Supports JS, Python, Go, Rust, Java stack formats.',
1338
+ description: 'Paste a stack trace and get source code context for each frame. Automatically parses JS, Python, Go, Rust, and Java stack trace formats. Use when debugging an error — shows the relevant code at each level of the call stack without manually opening files.',
1339
1339
  inputSchema: z.object({
1340
1340
  project_dir: projectDirParam,
1341
1341
  stack: z.string().describe('The stack trace text to parse')
@@ -1359,7 +1359,7 @@ server.registerTool(
1359
1359
  server.registerTool(
1360
1360
  'ucn_example',
1361
1361
  {
1362
- description: 'Find the best usage example of a function. Scores call sites by quality (typed assignment, destructuring, context) and returns the best one with surrounding code.',
1362
+ description: 'Find the best real-world example of how a function is used. Automatically scores all call sites by quality (typed assignments, destructured results, documented calls rank highest) and returns the top one with surrounding code for context. Use when you need to understand the expected calling pattern before using a function yourself.',
1363
1363
  inputSchema: z.object({
1364
1364
  project_dir: projectDirParam,
1365
1365
  name: nameParam
@@ -1381,7 +1381,7 @@ server.registerTool(
1381
1381
  server.registerTool(
1382
1382
  'ucn_expand',
1383
1383
  {
1384
- description: 'Show source code for a numbered item from the last ucn_context call. Run ucn_context first to get numbered callers/callees, then use this to drill into any item.',
1384
+ description: 'Drill into a numbered item from the last ucn_context result. Context returns numbered callers/callees use this to see the full source code of any one of them without a separate find+read cycle. Must run ucn_context first.',
1385
1385
  inputSchema: z.object({
1386
1386
  project_dir: projectDirParam,
1387
1387
  item: z.number().describe('Item number from ucn_context output (e.g. 1, 2, 3)')
@@ -1430,7 +1430,7 @@ server.registerTool(
1430
1430
  server.registerTool(
1431
1431
  'ucn_lines',
1432
1432
  {
1433
- description: 'Extract a range of lines from a project file. Supports "10-20" or single line "15".',
1433
+ description: 'Extract specific lines from a file (e.g., "10-20" or just "15"). Use when you know the exact line range you need — more precise than reading an entire file. File paths can be relative to the project root.',
1434
1434
  inputSchema: z.object({
1435
1435
  project_dir: projectDirParam,
1436
1436
  file: z.string().describe('File path (relative to project root or absolute)'),
@@ -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. 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.',
1493
+ description: 'List the public API surface of a project or file: all exported/public symbols with signatures. Use to understand what a library exposes before using it. Works best with JS/TS (export), Go (capitalized names), Rust (pub), Java (public). Python requires __all__ — use ucn_toc instead for Python projects without it.',
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)')
@@ -1512,7 +1512,7 @@ server.registerTool(
1512
1512
  server.registerTool(
1513
1513
  'ucn_stats',
1514
1514
  {
1515
- description: 'Show project statistics: file counts, symbol counts, lines of code, breakdown by language and type.',
1515
+ description: 'Quick project stats: file counts, symbol counts, lines of code, broken down by language and symbol type. Use for a high-level size check — how big is this codebase, what languages does it use, how many functions/classes exist.',
1516
1516
  inputSchema: z.object({
1517
1517
  project_dir: projectDirParam
1518
1518
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "3.4.7",
3
+ "version": "3.4.9",
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');