Skip to content

Commit f93cbf2

Browse files
committed
Рефакторинг и навигация: 13 багфиксов, фильтрация символов, enum constants
Рефакторинг: - rename: переименование конструкторов при rename класса (Java/Kotlin) - rename: фильтрация unrelated параметров при rename поля (over-scope fix) - extract_variable: изоляция выражений из statement (var decl, return, if) - inline: fallback поиск локальных переменных через AST (extractOuterScopeVariables) - delete: scope по умолчанию 'project' при handleReferences != 'error' - wrap: throw RuntimeException вместо printStackTrace при оборачивании return - generate: поддержка options.overwrite для перегенерации существующих методов - CodeRefactorTool: 6 новых параметров в схеме, исправление what enum Навигация: - CodeNavigateTool: kind фильтр и brief режим для компактного вывода символов - CodeNavigateTool: авторазрешение class vs constructor при ambiguity - JavaSymbolExtractor: извлечение enum constants как CONSTANT символов
1 parent f48d964 commit f93cbf2

17 files changed

Lines changed: 699 additions & 184 deletions

app/src/main/java/ru/nts/tools/mcp/tools/navigation/CodeNavigateTool.java

Lines changed: 119 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -90,16 +90,23 @@ public String getDescription() {
9090
- 'directory' (medium)
9191
- 'project' (slowest, scans up to 500 files)
9292
93+
SYMBOLS FILTERING (action='symbols' only):
94+
- kind: filter by symbol kind (class, method, field, constructor, constant, etc.)
95+
- brief: true for compact output (name + line only, no signatures/tokens)
96+
- Combine both for minimal output: kind='method', brief=true
97+
9398
PERFORMANCE TIPS:
9499
- Use scope='file' first, expand if needed
95100
- Prefer line/column over symbol name when possible
96101
- If a later edit depends on navigation, prefer strict=true to reject fallback cursor resolution
97102
- Large projects (1000+ files): expect delays for scope=project
103+
- For large files (100+ symbols): use kind filter or brief=true to reduce output
98104
99105
EXAMPLES:
100106
{"action":"definition", "path":"User.java", "line":42, "column":15}
101107
{"action":"references", "path":"User.java", "symbol":"getUserById", "scope":"file"}
102108
{"action":"symbols", "path":"User.java"}
109+
{"action":"symbols", "path":"User.java", "kind":"method", "brief":true}
103110
{"action":"hover", "path":"User.java", "line":10}
104111
105112
LANGUAGES: Java, Kotlin, JS/TS/TSX, Python, Go, Rust, C/C++, C#, PHP, HTML
@@ -144,6 +151,13 @@ public JsonNode getInputSchema() {
144151
props.putObject("strict").put("type", "boolean").put("description",
145152
"For definition/references by position: require exact cursor resolution. If fallback or enclosing-symbol logic would be used, return no result.");
146153

154+
props.putObject("kind").put("type", "string").put("description",
155+
"For 'symbols': filter by symbol kind. Values: class, interface, method, function, field, " +
156+
"variable, constructor, constant, enum, property. Omit to show all symbols.");
157+
props.putObject("brief").put("type", "boolean").put("description",
158+
"For 'symbols': compact output — name and line only, no signatures, types, or tokens. " +
159+
"Reduces output size for large files. Default: false.");
160+
147161
var required = schema.putArray("required");
148162
required.add("action");
149163
required.add("path");
@@ -178,7 +192,7 @@ public JsonNode execute(JsonNode params) throws Exception {
178192
case "definition" -> executeDefinition(path, params);
179193
case "references" -> executeReferences(path, params);
180194
case "hover" -> executeHover(path, params);
181-
case "symbols" -> executeSymbols(path);
195+
case "symbols" -> executeSymbols(path, params);
182196
default -> throw new NtsException(NtsErrorCode.PARAM_INVALID, "action", action);
183197
};
184198
}
@@ -292,7 +306,11 @@ private JsonNode executeReferences(Path path, JsonNode params) throws IOExceptio
292306
if (symbolName != null && !symbolName.isEmpty()) {
293307
resolution = resolver.resolveDefinitionByName(path, symbolName);
294308
if ("ambiguous".equals(resolution.status())) {
295-
return createAmbiguousResponse("references", resolution);
309+
// Auto-resolve: if one candidate is CLASS and others are CONSTRUCTOR, pick the CLASS
310+
resolution = tryAutoResolveClassVsConstructor(resolution);
311+
if ("ambiguous".equals(resolution.status())) {
312+
return createAmbiguousResponse("references", resolution);
313+
}
296314
}
297315
references = resolution.target() != null
298316
? resolver.findReferencesByHandle(resolution.target(), scope, includeDeclaration)
@@ -306,7 +324,10 @@ private JsonNode executeReferences(Path path, JsonNode params) throws IOExceptio
306324
resolution);
307325
}
308326
if ("ambiguous".equals(resolution.status())) {
309-
return createAmbiguousResponse("references", resolution);
327+
resolution = tryAutoResolveClassVsConstructor(resolution);
328+
if ("ambiguous".equals(resolution.status())) {
329+
return createAmbiguousResponse("references", resolution);
330+
}
310331
}
311332
references = resolution.target() != null
312333
? resolver.findReferencesByHandle(resolution.target(), scope, includeDeclaration)
@@ -394,7 +415,10 @@ private JsonNode executeHover(Path path, JsonNode params) throws IOException {
394415
if (symbolName != null && !symbolName.isEmpty()) {
395416
resolution = resolver.resolveDefinitionByName(path, symbolName);
396417
if ("ambiguous".equals(resolution.status())) {
397-
return createAmbiguousResponse("hover", resolution);
418+
resolution = tryAutoResolveClassVsConstructor(resolution);
419+
if ("ambiguous".equals(resolution.status())) {
420+
return createAmbiguousResponse("hover", resolution);
421+
}
398422
}
399423
symbol = resolver.hoverByName(path, symbolName);
400424
} else if (line > 0) {
@@ -451,17 +475,34 @@ private JsonNode executeHover(Path path, JsonNode params) throws IOException {
451475

452476
/**
453477
* List Symbols: все символы в файле.
478+
* Supports optional filtering by 'kind' and compact output via 'brief'.
454479
*/
455-
private JsonNode executeSymbols(Path path) throws IOException {
480+
private JsonNode executeSymbols(Path path, JsonNode params) throws IOException {
456481
List<SymbolInfo> symbols = resolver.listSymbols(path);
457482

458483
if (symbols.isEmpty()) {
459484
return createTextResponse("No symbols found in " + path.getFileName());
460485
}
461486

487+
// Apply kind filter if specified
488+
String kindFilter = params.path("kind").asText(null);
489+
if (kindFilter != null && !kindFilter.isEmpty()) {
490+
symbols = symbols.stream()
491+
.filter(s -> matchesSymbolKind(s.kind(), kindFilter))
492+
.toList();
493+
if (symbols.isEmpty()) {
494+
return createTextResponse("No " + kindFilter + " symbols found in " + path.getFileName());
495+
}
496+
}
497+
498+
boolean brief = params.path("brief").asBoolean(false);
499+
462500
StringBuilder sb = new StringBuilder();
463-
sb.append("**Symbols in ").append(path.getFileName()).append("** (")
464-
.append(symbols.size()).append(" total)\n\n");
501+
sb.append("**Symbols in ").append(path.getFileName()).append("**");
502+
if (kindFilter != null) {
503+
sb.append(" [kind=").append(kindFilter).append("]");
504+
}
505+
sb.append(" (").append(symbols.size()).append(" total)\n\n");
465506

466507
// Группируем по типу
467508
Map<SymbolKind, List<SymbolInfo>> byKind = symbols.stream()
@@ -475,18 +516,23 @@ private JsonNode executeSymbols(Path path) throws IOException {
475516

476517
for (SymbolInfo sym : syms) {
477518
Location loc = sym.location();
478-
// Регистрируем токен ТОЛЬКО для строки сигнатуры (не для всего файла)
479-
String token = registerAccessForRange(path, loc.startLine(), loc.startLine());
480-
481519
String indent = sym.parentName() != null ? " " : "";
482-
sb.append(indent).append("- `").append(sym.name()).append("`");
483-
if (sym.signature() != null) {
484-
sb.append(" — `").append(sym.signature()).append("`");
485-
} else if (sym.type() != null) {
486-
sb.append(": ").append(sym.type());
520+
521+
if (brief) {
522+
// Compact: name + line only
523+
sb.append(indent).append("- `").append(sym.name()).append("` L").append(loc.startLine()).append("\n");
524+
} else {
525+
// Full: name + signature/type + line + token
526+
String token = registerAccessForRange(path, loc.startLine(), loc.startLine());
527+
sb.append(indent).append("- `").append(sym.name()).append("`");
528+
if (sym.signature() != null) {
529+
sb.append(" — `").append(sym.signature()).append("`");
530+
} else if (sym.type() != null) {
531+
sb.append(": ").append(sym.type());
532+
}
533+
sb.append(" (line ").append(loc.startLine());
534+
sb.append(" | TOKEN: `").append(token).append("`)\n");
487535
}
488-
sb.append(" (line ").append(loc.startLine());
489-
sb.append(" | TOKEN: `").append(token).append("`)\n");
490536
}
491537
sb.append("\n");
492538
}
@@ -497,6 +543,61 @@ private JsonNode executeSymbols(Path path) throws IOException {
497543
return createTextResponse(sb.toString());
498544
}
499545

546+
/**
547+
* Auto-resolves ambiguity when candidates are a CLASS and its CONSTRUCTORs.
548+
* In Java/Kotlin, class name and constructor name are identical, causing false ambiguity.
549+
* Selects the CLASS candidate since that's what users almost always want.
550+
*/
551+
private ResolutionResult tryAutoResolveClassVsConstructor(ResolutionResult resolution) {
552+
if (resolution.candidates() == null || resolution.candidates().size() < 2) {
553+
return resolution;
554+
}
555+
556+
SymbolHandle classCandidate = null;
557+
boolean allSameNameAndClassOrCtor = true;
558+
String firstName = resolution.candidates().get(0).name();
559+
560+
for (SymbolHandle candidate : resolution.candidates()) {
561+
if (!candidate.name().equals(firstName)) {
562+
allSameNameAndClassOrCtor = false;
563+
break;
564+
}
565+
SymbolInfo.SymbolKind kind = candidate.kind();
566+
if (kind == SymbolInfo.SymbolKind.CLASS || kind == SymbolInfo.SymbolKind.INTERFACE
567+
|| kind == SymbolInfo.SymbolKind.ENUM) {
568+
classCandidate = candidate;
569+
} else if (kind != SymbolInfo.SymbolKind.CONSTRUCTOR) {
570+
allSameNameAndClassOrCtor = false;
571+
break;
572+
}
573+
}
574+
575+
if (allSameNameAndClassOrCtor && classCandidate != null) {
576+
return ResolutionResult.exact(classCandidate, "auto_resolved_class_vs_constructor",
577+
resolution.resolutionKind());
578+
}
579+
return resolution;
580+
}
581+
582+
/**
583+
* Checks if a SymbolKind matches the user-specified kind filter string.
584+
*/
585+
private boolean matchesSymbolKind(SymbolKind kind, String filter) {
586+
return switch (filter.toLowerCase()) {
587+
case "class" -> kind == SymbolKind.CLASS;
588+
case "interface" -> kind == SymbolKind.INTERFACE;
589+
case "enum" -> kind == SymbolKind.ENUM;
590+
case "method" -> kind == SymbolKind.METHOD;
591+
case "function" -> kind == SymbolKind.FUNCTION;
592+
case "constructor" -> kind == SymbolKind.CONSTRUCTOR;
593+
case "field" -> kind == SymbolKind.FIELD;
594+
case "variable" -> kind == SymbolKind.VARIABLE;
595+
case "constant" -> kind == SymbolKind.CONSTANT;
596+
case "property" -> kind == SymbolKind.PROPERTY;
597+
default -> true;
598+
};
599+
}
600+
500601
// ===================== ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ =====================
501602

502603
/**
@@ -582,7 +683,7 @@ private List<int[]> groupLocationsIntoRanges(List<Location> locations) {
582683
return Collections.emptyList();
583684
}
584685

585-
List<int[]> ranges = new ArrayList<>();
686+
List<int[]> ranges = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>();
586687
int[] current = null;
587688

588689
for (Location loc : locations) {

app/src/main/java/ru/nts/tools/mcp/tools/refactoring/CodeRefactorTool.java

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,16 +81,21 @@ public String getDescription() {
8181
8282
GENERATE:
8383
path + symbol (class name) + what (accessors|constructor|builder|toString)
84+
Supports options: {overwrite: true} to regenerate existing implementations (e.g., replace old toString).
8485
8586
LANGUAGES: Java, Kotlin, JS/TS/TSX, Python, Go, Rust, C/C++, C#, PHP, HTML
8687
8788
EXTRACT_VARIABLE (inverse of inline):
8889
path + startLine + variableName [+ endLine] [+ startColumn/endColumn] [+ variableType] [+ replaceAll]
8990
Extracts expression into a named variable. Auto-detects type for most languages.
91+
startColumn/endColumn enable sub-line precision for selecting expressions within a line.
9092
replaceAll=true (default): replaces ALL identical expressions in the same scope.
9193
Example: extract_variable(path='Foo.java', startLine=15, startColumn=20, endColumn=45, variableName='maxRetries')
9294
93-
OTHER: delete, wrap, extract_method, inline, move, batch
95+
INLINE:
96+
Supports keepDeclaration=true to preserve the original variable/method declaration after inlining.
97+
98+
OTHER: delete, wrap, extract_method, move, batch
9499
95100
BATCH INTEGRATION:
96101
Returns 'affectedFiles' array with access tokens for each modified file.
@@ -132,6 +137,10 @@ public JsonNode getInputSchema() {
132137
symbol.put("type", "string");
133138
symbol.put("description", "Symbol name (alternative to line/column). Use 'kind' to disambiguate if multiple matches.");
134139

140+
// signature (for overload disambiguation)
141+
properties.putObject("signature").put("type", "string").put("description",
142+
"Method signature for overload disambiguation. Format: '(Type1, Type2)' or '()'. Used with rename/delete to target specific overload.");
143+
135144
// newName (for rename)
136145
ObjectNode newName = properties.putObject("newName");
137146
newName.put("type", "string");
@@ -145,7 +154,7 @@ public JsonNode getInputSchema() {
145154
scopeEnum.add("directory");
146155
scopeEnum.add("project");
147156
scope.put("default", "project");
148-
scope.put("description", "Scope for reference search (default: project)");
157+
scope.put("description", "Scope for reference search (default: project). For rename: 'file', 'directory', or 'project'. For delete: defaults to 'project' when handleReferences is 'comment' or 'remove', otherwise 'file'.");
149158

150159
// kind (optional symbol kind filter)
151160
ObjectNode kind = properties.putObject("kind");
@@ -178,6 +187,12 @@ public JsonNode getInputSchema() {
178187
endLine.put("type", "integer");
179188
endLine.put("description", "End line for range operations");
180189

190+
// startColumn/endColumn for sub-line precision (extract_variable)
191+
properties.putObject("startColumn").put("type", "integer").put("description",
192+
"Start column (1-based) for extract_variable. Enables precise sub-line expression selection.");
193+
properties.putObject("endColumn").put("type", "integer").put("description",
194+
"End column (1-based) for extract_variable. Enables precise sub-line expression selection.");
195+
181196
// what (for generate)
182197
ObjectNode what = properties.putObject("what");
183198
what.put("type", "string");
@@ -191,6 +206,8 @@ public JsonNode getInputSchema() {
191206
whatEnum.add("builder");
192207
whatEnum.add("equals_hashcode");
193208
whatEnum.add("toString");
209+
whatEnum.add("all_args_constructor");
210+
whatEnum.add("no_args_constructor");
194211
what.put("description", "What to generate (generate operation)");
195212

196213
// fields (for generate)
@@ -216,11 +233,30 @@ public JsonNode getInputSchema() {
216233
wrapperEnum.add("custom");
217234
wrapper.put("description", "Wrapper type for wrap operation");
218235

236+
// template (for wrap with custom wrapper)
237+
properties.putObject("template").put("type", "string").put("description",
238+
"Custom wrapper template for wrap action with wrapper='custom'. Use ${code} as placeholder for wrapped code.");
239+
219240
// methodName (for extract_method)
220241
ObjectNode methodName = properties.putObject("methodName");
221242
methodName.put("type", "string");
222243
methodName.put("description", "Name for the extracted method");
223244

245+
// codePattern (for extract_method)
246+
ObjectNode codePattern = properties.putObject("codePattern");
247+
codePattern.put("type", "string");
248+
codePattern.put("description", "For extract_method: regex pattern to find the code block to extract (alternative to startLine/endLine).");
249+
250+
// accessModifier (for extract_method, change_signature)
251+
ObjectNode accessModifier = properties.putObject("accessModifier");
252+
accessModifier.put("type", "string");
253+
accessModifier.put("description", "For extract_method, change_signature: access modifier for generated method (e.g., 'public', 'private', 'protected').");
254+
255+
// returnType (for extract_method, change_signature)
256+
ObjectNode returnType = properties.putObject("returnType");
257+
returnType.put("type", "string");
258+
returnType.put("description", "For extract_method, change_signature: return type override.");
259+
224260
// variableName (for extract_variable)
225261
ObjectNode variableName = properties.putObject("variableName");
226262
variableName.put("type", "string");
@@ -238,11 +274,25 @@ public JsonNode getInputSchema() {
238274
replaceAll.put("default", true);
239275
replaceAll.put("description", "[extract_variable] Replace ALL occurrences of expression in scope (default: true)");
240276

277+
// keepDeclaration (for inline)
278+
properties.putObject("keepDeclaration").put("type", "boolean").put("description",
279+
"For inline: keep the original variable/method declaration after inlining usages. Default: false (delete declaration).");
280+
241281
// targetPath (for move)
242282
ObjectNode targetPath = properties.putObject("targetPath");
243283
targetPath.put("type", "string");
244284
targetPath.put("description", "Target file path for move operation");
245285

286+
// targetClass (for move)
287+
ObjectNode targetClass = properties.putObject("targetClass");
288+
targetClass.put("type", "string");
289+
targetClass.put("description", "For move: target class name (alternative to targetPath for inner-class moves).");
290+
291+
// position (for move)
292+
ObjectNode position = properties.putObject("position");
293+
position.put("type", "string");
294+
position.put("description", "For move: insertion position in target ('before', 'after', or line number).");
295+
246296
// handleReferences (for delete)
247297
ObjectNode handleReferences = properties.putObject("handleReferences");
248298
handleReferences.put("type", "string");
@@ -256,7 +306,7 @@ public JsonNode getInputSchema() {
256306
// options (generic options object)
257307
ObjectNode options = properties.putObject("options");
258308
options.put("type", "object");
259-
options.put("description", "Additional options specific to operation");
309+
options.put("description", "Additional options. For generate: {\"overwrite\": true} to regenerate existing methods. For wrap: {\"exceptionType\": \"IOException\"}.");
260310

261311
// params (for change_signature)
262312
ObjectNode csParams = properties.putObject("params");
@@ -286,6 +336,12 @@ public JsonNode getInputSchema() {
286336
opsItems.put("type", "object");
287337
operations.put("description", "Array of operations for batch execution");
288338

339+
// continueOnError (for batch)
340+
ObjectNode continueOnError = properties.putObject("continueOnError");
341+
continueOnError.put("type", "boolean");
342+
continueOnError.put("default", false);
343+
continueOnError.put("description", "For batch: continue executing remaining operations if one fails. Default: false.");
344+
289345
// preview
290346
ObjectNode preview = properties.putObject("preview");
291347
preview.put("type", "boolean");

0 commit comments

Comments
 (0)