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 +2 -2
- package/core/project.js +22 -3
- package/mcp/server.js +27 -27
- 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/mcp/server.js
CHANGED
|
@@ -756,7 +756,7 @@ function requireName(name) {
|
|
|
756
756
|
server.registerTool(
|
|
757
757
|
'ucn_toc',
|
|
758
758
|
{
|
|
759
|
-
description: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
|
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
|
|
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
|
|
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: "
|
|
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: '
|
|
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: '
|
|
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
|
|
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
|
|
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: '
|
|
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
|
|
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: '
|
|
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
|
|
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
|
|
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: '
|
|
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
|
|
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: '
|
|
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
|
|
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: '
|
|
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: '
|
|
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
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');
|