@yuaone/core 0.1.0
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/LICENSE +663 -0
- package/README.md +15 -0
- package/dist/__tests__/context-manager.test.d.ts +6 -0
- package/dist/__tests__/context-manager.test.d.ts.map +1 -0
- package/dist/__tests__/context-manager.test.js +220 -0
- package/dist/__tests__/context-manager.test.js.map +1 -0
- package/dist/__tests__/governor.test.d.ts +6 -0
- package/dist/__tests__/governor.test.d.ts.map +1 -0
- package/dist/__tests__/governor.test.js +210 -0
- package/dist/__tests__/governor.test.js.map +1 -0
- package/dist/__tests__/model-router.test.d.ts +6 -0
- package/dist/__tests__/model-router.test.d.ts.map +1 -0
- package/dist/__tests__/model-router.test.js +329 -0
- package/dist/__tests__/model-router.test.js.map +1 -0
- package/dist/agent-logger.d.ts +384 -0
- package/dist/agent-logger.d.ts.map +1 -0
- package/dist/agent-logger.js +820 -0
- package/dist/agent-logger.js.map +1 -0
- package/dist/agent-loop.d.ts +163 -0
- package/dist/agent-loop.d.ts.map +1 -0
- package/dist/agent-loop.js +609 -0
- package/dist/agent-loop.js.map +1 -0
- package/dist/agent-modes.d.ts +85 -0
- package/dist/agent-modes.d.ts.map +1 -0
- package/dist/agent-modes.js +418 -0
- package/dist/agent-modes.js.map +1 -0
- package/dist/approval.d.ts +137 -0
- package/dist/approval.d.ts.map +1 -0
- package/dist/approval.js +299 -0
- package/dist/approval.js.map +1 -0
- package/dist/async-completion-queue.d.ts +56 -0
- package/dist/async-completion-queue.d.ts.map +1 -0
- package/dist/async-completion-queue.js +77 -0
- package/dist/async-completion-queue.js.map +1 -0
- package/dist/auto-fix.d.ts +174 -0
- package/dist/auto-fix.d.ts.map +1 -0
- package/dist/auto-fix.js +319 -0
- package/dist/auto-fix.js.map +1 -0
- package/dist/codebase-context.d.ts +396 -0
- package/dist/codebase-context.d.ts.map +1 -0
- package/dist/codebase-context.js +1260 -0
- package/dist/codebase-context.js.map +1 -0
- package/dist/conflict-resolver.d.ts +191 -0
- package/dist/conflict-resolver.d.ts.map +1 -0
- package/dist/conflict-resolver.js +524 -0
- package/dist/conflict-resolver.js.map +1 -0
- package/dist/constants.d.ts +52 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +141 -0
- package/dist/constants.js.map +1 -0
- package/dist/context-budget.d.ts +435 -0
- package/dist/context-budget.d.ts.map +1 -0
- package/dist/context-budget.js +903 -0
- package/dist/context-budget.js.map +1 -0
- package/dist/context-compressor.d.ts +143 -0
- package/dist/context-compressor.d.ts.map +1 -0
- package/dist/context-compressor.js +511 -0
- package/dist/context-compressor.js.map +1 -0
- package/dist/context-manager.d.ts +112 -0
- package/dist/context-manager.d.ts.map +1 -0
- package/dist/context-manager.js +247 -0
- package/dist/context-manager.js.map +1 -0
- package/dist/continuous-reflection.d.ts +267 -0
- package/dist/continuous-reflection.d.ts.map +1 -0
- package/dist/continuous-reflection.js +338 -0
- package/dist/continuous-reflection.js.map +1 -0
- package/dist/cross-file-refactor.d.ts +352 -0
- package/dist/cross-file-refactor.d.ts.map +1 -0
- package/dist/cross-file-refactor.js +1544 -0
- package/dist/cross-file-refactor.js.map +1 -0
- package/dist/dag-orchestrator.d.ts +138 -0
- package/dist/dag-orchestrator.d.ts.map +1 -0
- package/dist/dag-orchestrator.js +379 -0
- package/dist/dag-orchestrator.js.map +1 -0
- package/dist/debate-orchestrator.d.ts +301 -0
- package/dist/debate-orchestrator.d.ts.map +1 -0
- package/dist/debate-orchestrator.js +719 -0
- package/dist/debate-orchestrator.js.map +1 -0
- package/dist/dependency-analyzer.d.ts +113 -0
- package/dist/dependency-analyzer.d.ts.map +1 -0
- package/dist/dependency-analyzer.js +444 -0
- package/dist/dependency-analyzer.js.map +1 -0
- package/dist/design-loop.d.ts +59 -0
- package/dist/design-loop.d.ts.map +1 -0
- package/dist/design-loop.js +344 -0
- package/dist/design-loop.js.map +1 -0
- package/dist/doc-intelligence.d.ts +383 -0
- package/dist/doc-intelligence.d.ts.map +1 -0
- package/dist/doc-intelligence.js +1307 -0
- package/dist/doc-intelligence.js.map +1 -0
- package/dist/dynamic-role-generator.d.ts +76 -0
- package/dist/dynamic-role-generator.d.ts.map +1 -0
- package/dist/dynamic-role-generator.js +194 -0
- package/dist/dynamic-role-generator.js.map +1 -0
- package/dist/errors.d.ts +69 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +102 -0
- package/dist/errors.js.map +1 -0
- package/dist/event-bus.d.ts +159 -0
- package/dist/event-bus.d.ts.map +1 -0
- package/dist/event-bus.js +305 -0
- package/dist/event-bus.js.map +1 -0
- package/dist/execution-engine.d.ts +425 -0
- package/dist/execution-engine.d.ts.map +1 -0
- package/dist/execution-engine.js +1555 -0
- package/dist/execution-engine.js.map +1 -0
- package/dist/git-intelligence.d.ts +306 -0
- package/dist/git-intelligence.d.ts.map +1 -0
- package/dist/git-intelligence.js +1099 -0
- package/dist/git-intelligence.js.map +1 -0
- package/dist/governor.d.ts +77 -0
- package/dist/governor.d.ts.map +1 -0
- package/dist/governor.js +161 -0
- package/dist/governor.js.map +1 -0
- package/dist/hierarchical-planner.d.ts +313 -0
- package/dist/hierarchical-planner.d.ts.map +1 -0
- package/dist/hierarchical-planner.js +981 -0
- package/dist/hierarchical-planner.js.map +1 -0
- package/dist/index.d.ts +121 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +123 -0
- package/dist/index.js.map +1 -0
- package/dist/intent-inference.d.ts +103 -0
- package/dist/intent-inference.d.ts.map +1 -0
- package/dist/intent-inference.js +605 -0
- package/dist/intent-inference.js.map +1 -0
- package/dist/interrupt-manager.d.ts +143 -0
- package/dist/interrupt-manager.d.ts.map +1 -0
- package/dist/interrupt-manager.js +196 -0
- package/dist/interrupt-manager.js.map +1 -0
- package/dist/kernel.d.ts +564 -0
- package/dist/kernel.d.ts.map +1 -0
- package/dist/kernel.js +1419 -0
- package/dist/kernel.js.map +1 -0
- package/dist/language-support.d.ts +232 -0
- package/dist/language-support.d.ts.map +1 -0
- package/dist/language-support.js +1134 -0
- package/dist/language-support.js.map +1 -0
- package/dist/llm-client.d.ts +82 -0
- package/dist/llm-client.d.ts.map +1 -0
- package/dist/llm-client.js +475 -0
- package/dist/llm-client.js.map +1 -0
- package/dist/mcp-client.d.ts +232 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +718 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/memory-manager.d.ts +200 -0
- package/dist/memory-manager.d.ts.map +1 -0
- package/dist/memory-manager.js +568 -0
- package/dist/memory-manager.js.map +1 -0
- package/dist/memory.d.ts +87 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +341 -0
- package/dist/memory.js.map +1 -0
- package/dist/model-router.d.ts +245 -0
- package/dist/model-router.d.ts.map +1 -0
- package/dist/model-router.js +632 -0
- package/dist/model-router.js.map +1 -0
- package/dist/parallel-executor.d.ts +125 -0
- package/dist/parallel-executor.d.ts.map +1 -0
- package/dist/parallel-executor.js +201 -0
- package/dist/parallel-executor.js.map +1 -0
- package/dist/perf-optimizer.d.ts +212 -0
- package/dist/perf-optimizer.d.ts.map +1 -0
- package/dist/perf-optimizer.js +721 -0
- package/dist/perf-optimizer.js.map +1 -0
- package/dist/persona.d.ts +305 -0
- package/dist/persona.d.ts.map +1 -0
- package/dist/persona.js +887 -0
- package/dist/persona.js.map +1 -0
- package/dist/planner.d.ts +70 -0
- package/dist/planner.d.ts.map +1 -0
- package/dist/planner.js +264 -0
- package/dist/planner.js.map +1 -0
- package/dist/qa-pipeline.d.ts +365 -0
- package/dist/qa-pipeline.d.ts.map +1 -0
- package/dist/qa-pipeline.js +1352 -0
- package/dist/qa-pipeline.js.map +1 -0
- package/dist/reasoning-adapter.d.ts +116 -0
- package/dist/reasoning-adapter.d.ts.map +1 -0
- package/dist/reasoning-adapter.js +187 -0
- package/dist/reasoning-adapter.js.map +1 -0
- package/dist/role-registry.d.ts +55 -0
- package/dist/role-registry.d.ts.map +1 -0
- package/dist/role-registry.js +192 -0
- package/dist/role-registry.js.map +1 -0
- package/dist/sandbox-tiers.d.ts +327 -0
- package/dist/sandbox-tiers.d.ts.map +1 -0
- package/dist/sandbox-tiers.js +928 -0
- package/dist/sandbox-tiers.js.map +1 -0
- package/dist/security-scanner.d.ts +222 -0
- package/dist/security-scanner.d.ts.map +1 -0
- package/dist/security-scanner.js +1129 -0
- package/dist/security-scanner.js.map +1 -0
- package/dist/security.d.ts +93 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +393 -0
- package/dist/security.js.map +1 -0
- package/dist/self-reflection.d.ts +397 -0
- package/dist/self-reflection.d.ts.map +1 -0
- package/dist/self-reflection.js +908 -0
- package/dist/self-reflection.js.map +1 -0
- package/dist/session-persistence.d.ts +191 -0
- package/dist/session-persistence.d.ts.map +1 -0
- package/dist/session-persistence.js +395 -0
- package/dist/session-persistence.js.map +1 -0
- package/dist/speculative-executor.d.ts +210 -0
- package/dist/speculative-executor.d.ts.map +1 -0
- package/dist/speculative-executor.js +618 -0
- package/dist/speculative-executor.js.map +1 -0
- package/dist/state-machine.d.ts +289 -0
- package/dist/state-machine.d.ts.map +1 -0
- package/dist/state-machine.js +695 -0
- package/dist/state-machine.js.map +1 -0
- package/dist/sub-agent.d.ts +177 -0
- package/dist/sub-agent.d.ts.map +1 -0
- package/dist/sub-agent.js +303 -0
- package/dist/sub-agent.js.map +1 -0
- package/dist/system-prompt.d.ts +26 -0
- package/dist/system-prompt.d.ts.map +1 -0
- package/dist/system-prompt.js +84 -0
- package/dist/system-prompt.js.map +1 -0
- package/dist/test-intelligence.d.ts +439 -0
- package/dist/test-intelligence.d.ts.map +1 -0
- package/dist/test-intelligence.js +1165 -0
- package/dist/test-intelligence.js.map +1 -0
- package/dist/types.d.ts +632 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/vector-index.d.ts +314 -0
- package/dist/vector-index.d.ts.map +1 -0
- package/dist/vector-index.js +618 -0
- package/dist/vector-index.js.map +1 -0
- package/package.json +41 -0
|
@@ -0,0 +1,1544 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module cross-file-refactor
|
|
3
|
+
* @description Cross-File Refactoring Engine — enables safe renaming, moving, extracting,
|
|
4
|
+
* and inlining symbols across multiple TypeScript/JavaScript files.
|
|
5
|
+
*
|
|
6
|
+
* Provides preview (dry-run) mode, rollback support, and safety checks (breaking change
|
|
7
|
+
* detection, risk assessment). Uses regex-based analysis consistent with the rest of yuan-core.
|
|
8
|
+
*/
|
|
9
|
+
import { readdir, readFile, writeFile, mkdir } from "node:fs/promises";
|
|
10
|
+
import { join, resolve, dirname, extname, relative } from "node:path";
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
// ─── Constants ───
|
|
13
|
+
const SOURCE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
14
|
+
const SKIP_DIRS = new Set([
|
|
15
|
+
"node_modules",
|
|
16
|
+
"dist",
|
|
17
|
+
".git",
|
|
18
|
+
"build",
|
|
19
|
+
"coverage",
|
|
20
|
+
".next",
|
|
21
|
+
".turbo",
|
|
22
|
+
"__pycache__",
|
|
23
|
+
]);
|
|
24
|
+
const MAX_ROLLBACKS = 10;
|
|
25
|
+
// ─── Regex patterns ───
|
|
26
|
+
/** Named import: import { X, Y } from "Z" */
|
|
27
|
+
const IMPORT_NAMED_RE = /import\s+(?:type\s+)?\{([^}]*)\}\s+from\s+["']([^"']+)["']/g;
|
|
28
|
+
/** Default import: import X from "Z" */
|
|
29
|
+
const IMPORT_DEFAULT_RE = /import\s+(\w+)\s+from\s+["']([^"']+)["']/g;
|
|
30
|
+
/** Namespace import: import * as X from "Z" */
|
|
31
|
+
const IMPORT_NAMESPACE_RE = /import\s+\*\s+as\s+(\w+)\s+from\s+["']([^"']+)["']/g;
|
|
32
|
+
/** Re-export: export { X } from "Z" */
|
|
33
|
+
const RE_EXPORT_RE = /export\s+(?:type\s+)?\{([^}]*)\}\s+from\s+["']([^"']+)["']/g;
|
|
34
|
+
/** Named export declaration */
|
|
35
|
+
const EXPORT_DECL_RE = /export\s+(?:declare\s+)?(?:abstract\s+)?(?:async\s+)?(?:function\s*\*?|class|const|let|var|interface|type|enum)\s+(\w+)/g;
|
|
36
|
+
/** Default export */
|
|
37
|
+
const EXPORT_DEFAULT_RE = /export\s+default\s+/;
|
|
38
|
+
/** Single-line comment */
|
|
39
|
+
const SINGLE_LINE_COMMENT_RE = /\/\/.*/g;
|
|
40
|
+
/** Multi-line comment (non-greedy) */
|
|
41
|
+
const MULTI_LINE_COMMENT_RE = /\/\*[\s\S]*?\*\//g;
|
|
42
|
+
/**
|
|
43
|
+
* Cross-File Refactoring Engine.
|
|
44
|
+
*
|
|
45
|
+
* Enables safe renaming, moving, extracting, and inlining of symbols
|
|
46
|
+
* across TypeScript/JavaScript projects. Supports preview (dry-run),
|
|
47
|
+
* rollback, and breaking change detection.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* const refactor = new CrossFileRefactor("/path/to/project");
|
|
52
|
+
* const preview = await refactor.renameSymbol("OldName", "NewName");
|
|
53
|
+
* if (preview.riskLevel !== "high") {
|
|
54
|
+
* const result = await refactor.apply({ type: "rename", symbolName: "OldName", newName: "NewName" });
|
|
55
|
+
* }
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export class CrossFileRefactor {
|
|
59
|
+
projectPath;
|
|
60
|
+
rollbacks;
|
|
61
|
+
rollbackOrder;
|
|
62
|
+
constructor(projectPath) {
|
|
63
|
+
this.projectPath = resolve(projectPath);
|
|
64
|
+
this.rollbacks = new Map();
|
|
65
|
+
this.rollbackOrder = [];
|
|
66
|
+
}
|
|
67
|
+
// ═══════════════════════════════════════════════════════════════
|
|
68
|
+
// Preview (dry run)
|
|
69
|
+
// ═══════════════════════════════════════════════════════════════
|
|
70
|
+
/**
|
|
71
|
+
* Generate a preview of the refactoring without making changes.
|
|
72
|
+
*
|
|
73
|
+
* @param request - The refactoring request
|
|
74
|
+
* @returns Preview with affected files, breaking changes, and risk assessment
|
|
75
|
+
*/
|
|
76
|
+
async preview(request) {
|
|
77
|
+
switch (request.type) {
|
|
78
|
+
case "rename":
|
|
79
|
+
return this.renameSymbol(request.symbolName ?? "", request.newName ?? "", request.file);
|
|
80
|
+
case "move":
|
|
81
|
+
return this.moveSymbol(request.symbolName ?? "", request.sourceFile ?? "", request.targetFile ?? "", request.addReExport);
|
|
82
|
+
case "extract_function":
|
|
83
|
+
return this.extractFunction(request.sourceFile ?? "", request.startLine ?? 0, request.endLine ?? 0, request.extractedName ?? "extractedFunction", request.targetFile);
|
|
84
|
+
case "extract_interface":
|
|
85
|
+
return this.extractInterface(request.symbolName ?? "", request.extractedName ?? "", request.sourceFile ?? "");
|
|
86
|
+
case "inline":
|
|
87
|
+
return this.inlineFunction(request.symbolName ?? "", request.sourceFile ?? "");
|
|
88
|
+
case "change_signature":
|
|
89
|
+
return this.changeSignature(request.symbolName ?? "", request.sourceFile ?? "", request.newParams ?? [], request.newReturnType);
|
|
90
|
+
default:
|
|
91
|
+
return {
|
|
92
|
+
type: request.type,
|
|
93
|
+
affectedFiles: [],
|
|
94
|
+
totalChanges: 0,
|
|
95
|
+
breakingChanges: [],
|
|
96
|
+
riskLevel: "high",
|
|
97
|
+
warnings: [`Unknown refactoring type: ${request.type}`],
|
|
98
|
+
canAutoApply: false,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// ═══════════════════════════════════════════════════════════════
|
|
103
|
+
// Apply
|
|
104
|
+
// ═══════════════════════════════════════════════════════════════
|
|
105
|
+
/**
|
|
106
|
+
* Apply a refactoring to disk. Snapshots all affected files first
|
|
107
|
+
* so the operation can be rolled back.
|
|
108
|
+
*
|
|
109
|
+
* @param request - The refactoring request
|
|
110
|
+
* @returns Result including applied changes and rollback ID
|
|
111
|
+
*/
|
|
112
|
+
async apply(request) {
|
|
113
|
+
const preview = await this.preview(request);
|
|
114
|
+
const filePaths = preview.affectedFiles.map((f) => f.file);
|
|
115
|
+
const rollbackId = await this.snapshotFiles(filePaths);
|
|
116
|
+
const appliedChanges = [];
|
|
117
|
+
const failedChanges = [];
|
|
118
|
+
for (const fc of preview.affectedFiles) {
|
|
119
|
+
try {
|
|
120
|
+
// Path traversal protection: ensure resolved path stays within project root
|
|
121
|
+
const resolvedPath = resolve(fc.file);
|
|
122
|
+
if (!resolvedPath.startsWith(this.projectPath)) {
|
|
123
|
+
failedChanges.push({
|
|
124
|
+
file: fc.file,
|
|
125
|
+
error: `Path traversal blocked: "${resolvedPath}" is outside project root "${this.projectPath}"`,
|
|
126
|
+
});
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (fc.isNewFile) {
|
|
130
|
+
// Create new file with the new text from changes
|
|
131
|
+
const dir = dirname(fc.file);
|
|
132
|
+
await mkdir(dir, { recursive: true });
|
|
133
|
+
const content = fc.changes.map((c) => c.newText).join("\n");
|
|
134
|
+
await writeFile(fc.file, content, "utf-8");
|
|
135
|
+
}
|
|
136
|
+
else if (fc.isDeletedFile) {
|
|
137
|
+
// We don't delete files — just clear content (safer)
|
|
138
|
+
await writeFile(fc.file, "", "utf-8");
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
const content = await this.readFile(fc.file);
|
|
142
|
+
const updated = this.applyChanges(content, fc.changes);
|
|
143
|
+
await writeFile(fc.file, updated, "utf-8");
|
|
144
|
+
}
|
|
145
|
+
appliedChanges.push(fc);
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
failedChanges.push({
|
|
149
|
+
file: fc.file,
|
|
150
|
+
error: err instanceof Error ? err.message : String(err),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
success: failedChanges.length === 0,
|
|
156
|
+
appliedChanges,
|
|
157
|
+
failedChanges,
|
|
158
|
+
rollbackAvailable: true,
|
|
159
|
+
rollbackId,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
// ═══════════════════════════════════════════════════════════════
|
|
163
|
+
// Rollback
|
|
164
|
+
// ═══════════════════════════════════════════════════════════════
|
|
165
|
+
/**
|
|
166
|
+
* Roll back a previously applied refactoring by restoring original file contents.
|
|
167
|
+
*
|
|
168
|
+
* @param rollbackId - The rollback ID returned by apply()
|
|
169
|
+
* @returns True if rollback was successful
|
|
170
|
+
*/
|
|
171
|
+
async rollback(rollbackId) {
|
|
172
|
+
const snapshot = this.rollbacks.get(rollbackId);
|
|
173
|
+
if (!snapshot)
|
|
174
|
+
return false;
|
|
175
|
+
try {
|
|
176
|
+
for (const [filePath, content] of snapshot) {
|
|
177
|
+
const dir = dirname(filePath);
|
|
178
|
+
await mkdir(dir, { recursive: true });
|
|
179
|
+
await writeFile(filePath, content, "utf-8");
|
|
180
|
+
}
|
|
181
|
+
this.rollbacks.delete(rollbackId);
|
|
182
|
+
this.rollbackOrder = this.rollbackOrder.filter((id) => id !== rollbackId);
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// ═══════════════════════════════════════════════════════════════
|
|
190
|
+
// Rename Symbol
|
|
191
|
+
// ═══════════════════════════════════════════════════════════════
|
|
192
|
+
/**
|
|
193
|
+
* Rename a symbol across all files that reference it.
|
|
194
|
+
* Updates the definition, all usages, and all import statements.
|
|
195
|
+
*
|
|
196
|
+
* @param symbolName - Current symbol name
|
|
197
|
+
* @param newName - Desired new name
|
|
198
|
+
* @param scopeFile - Optional file to restrict the rename to
|
|
199
|
+
* @returns Preview of all changes
|
|
200
|
+
*/
|
|
201
|
+
async renameSymbol(symbolName, newName, scopeFile) {
|
|
202
|
+
const warnings = [];
|
|
203
|
+
const breakingChanges = [];
|
|
204
|
+
if (!symbolName || !newName) {
|
|
205
|
+
return this.emptyPreview("rename", ["symbolName and newName are required"]);
|
|
206
|
+
}
|
|
207
|
+
if (symbolName === newName) {
|
|
208
|
+
return this.emptyPreview("rename", ["symbolName and newName are identical"]);
|
|
209
|
+
}
|
|
210
|
+
const usages = await this.findAllUsages(symbolName, scopeFile);
|
|
211
|
+
const imports = await this.findImports(symbolName);
|
|
212
|
+
if (usages.length === 0 && imports.length === 0) {
|
|
213
|
+
return this.emptyPreview("rename", [`No usages found for symbol "${symbolName}"`]);
|
|
214
|
+
}
|
|
215
|
+
// Group usages by file
|
|
216
|
+
const fileChangesMap = new Map();
|
|
217
|
+
// Process direct usages
|
|
218
|
+
for (const usage of usages) {
|
|
219
|
+
const changes = fileChangesMap.get(usage.file) ?? [];
|
|
220
|
+
const line = usage.context;
|
|
221
|
+
const newLine = this.replaceSymbolInLine(line, symbolName, newName);
|
|
222
|
+
if (newLine !== line) {
|
|
223
|
+
changes.push({
|
|
224
|
+
startLine: usage.line,
|
|
225
|
+
endLine: usage.line,
|
|
226
|
+
oldText: line,
|
|
227
|
+
newText: newLine,
|
|
228
|
+
reason: "rename usage",
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
fileChangesMap.set(usage.file, changes);
|
|
232
|
+
}
|
|
233
|
+
// Process import statements
|
|
234
|
+
for (const imp of imports) {
|
|
235
|
+
if (scopeFile && imp.file !== scopeFile)
|
|
236
|
+
continue;
|
|
237
|
+
const changes = fileChangesMap.get(imp.file) ?? [];
|
|
238
|
+
let newImport;
|
|
239
|
+
if (imp.isDefault) {
|
|
240
|
+
newImport = imp.importStatement.replace(new RegExp(`\\b${this.escapeRegex(symbolName)}\\b`), newName);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
// Named import: replace within braces
|
|
244
|
+
newImport = imp.importStatement.replace(new RegExp(`\\b${this.escapeRegex(symbolName)}\\b`), newName);
|
|
245
|
+
}
|
|
246
|
+
if (newImport !== imp.importStatement) {
|
|
247
|
+
changes.push({
|
|
248
|
+
startLine: imp.line,
|
|
249
|
+
endLine: imp.line,
|
|
250
|
+
oldText: imp.importStatement,
|
|
251
|
+
newText: newImport,
|
|
252
|
+
reason: "update import",
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
fileChangesMap.set(imp.file, changes);
|
|
256
|
+
}
|
|
257
|
+
// Deduplicate changes per file (same line)
|
|
258
|
+
const affectedFiles = [];
|
|
259
|
+
for (const [file, changes] of fileChangesMap) {
|
|
260
|
+
const deduped = this.deduplicateChanges(changes);
|
|
261
|
+
if (deduped.length > 0) {
|
|
262
|
+
affectedFiles.push({ file, changes: deduped, isNewFile: false, isDeletedFile: false });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// Check for breaking changes (exported symbol rename)
|
|
266
|
+
const isExported = await this.isSymbolExported(symbolName);
|
|
267
|
+
if (isExported) {
|
|
268
|
+
breakingChanges.push({
|
|
269
|
+
type: "api_change",
|
|
270
|
+
description: `Renaming exported symbol "${symbolName}" to "${newName}" — all consumers must update`,
|
|
271
|
+
file: isExported.file,
|
|
272
|
+
line: isExported.line,
|
|
273
|
+
severity: "warning",
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
const totalChanges = affectedFiles.reduce((sum, f) => sum + f.changes.length, 0);
|
|
277
|
+
const riskLevel = this.assessRisk(affectedFiles.length, breakingChanges.length, totalChanges);
|
|
278
|
+
return {
|
|
279
|
+
type: "rename",
|
|
280
|
+
affectedFiles,
|
|
281
|
+
totalChanges,
|
|
282
|
+
breakingChanges,
|
|
283
|
+
riskLevel,
|
|
284
|
+
warnings,
|
|
285
|
+
canAutoApply: riskLevel !== "high",
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
// ═══════════════════════════════════════════════════════════════
|
|
289
|
+
// Move Symbol
|
|
290
|
+
// ═══════════════════════════════════════════════════════════════
|
|
291
|
+
/**
|
|
292
|
+
* Move a symbol from one file to another. Updates all import statements
|
|
293
|
+
* across the project and optionally adds a re-export for backward compatibility.
|
|
294
|
+
*
|
|
295
|
+
* @param symbolName - Symbol to move
|
|
296
|
+
* @param sourceFile - Current file (absolute path)
|
|
297
|
+
* @param targetFile - Destination file (absolute path)
|
|
298
|
+
* @param addReExport - Whether to add a re-export in the source file
|
|
299
|
+
* @returns Preview of all changes
|
|
300
|
+
*/
|
|
301
|
+
async moveSymbol(symbolName, sourceFile, targetFile, addReExport) {
|
|
302
|
+
const warnings = [];
|
|
303
|
+
const breakingChanges = [];
|
|
304
|
+
if (!symbolName || !sourceFile || !targetFile) {
|
|
305
|
+
return this.emptyPreview("move", ["symbolName, sourceFile, and targetFile are required"]);
|
|
306
|
+
}
|
|
307
|
+
const absSource = resolve(this.projectPath, sourceFile);
|
|
308
|
+
const absTarget = resolve(this.projectPath, targetFile);
|
|
309
|
+
let sourceContent;
|
|
310
|
+
try {
|
|
311
|
+
sourceContent = await this.readFile(absSource);
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
return this.emptyPreview("move", [`Source file not found: ${absSource}`]);
|
|
315
|
+
}
|
|
316
|
+
// Find the symbol definition in source
|
|
317
|
+
const definition = this.extractDefinition(sourceContent, symbolName);
|
|
318
|
+
if (!definition) {
|
|
319
|
+
return this.emptyPreview("move", [`Symbol "${symbolName}" not found in ${sourceFile}`]);
|
|
320
|
+
}
|
|
321
|
+
const affectedFiles = [];
|
|
322
|
+
// 1. Remove from source file
|
|
323
|
+
const sourceLines = sourceContent.split("\n");
|
|
324
|
+
const sourceChanges = [];
|
|
325
|
+
sourceChanges.push({
|
|
326
|
+
startLine: definition.startLine,
|
|
327
|
+
endLine: definition.endLine,
|
|
328
|
+
oldText: sourceLines.slice(definition.startLine - 1, definition.endLine).join("\n"),
|
|
329
|
+
newText: "",
|
|
330
|
+
reason: "remove moved symbol",
|
|
331
|
+
});
|
|
332
|
+
// Optionally add re-export
|
|
333
|
+
if (addReExport) {
|
|
334
|
+
const relPath = this.computeRelativeImportPath(absSource, absTarget);
|
|
335
|
+
const reExportLine = `export { ${symbolName} } from "${relPath}";`;
|
|
336
|
+
sourceChanges.push({
|
|
337
|
+
startLine: definition.startLine,
|
|
338
|
+
endLine: definition.startLine,
|
|
339
|
+
oldText: "",
|
|
340
|
+
newText: reExportLine,
|
|
341
|
+
reason: "add re-export for backward compatibility",
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
affectedFiles.push({
|
|
345
|
+
file: absSource,
|
|
346
|
+
changes: sourceChanges,
|
|
347
|
+
isNewFile: false,
|
|
348
|
+
isDeletedFile: false,
|
|
349
|
+
});
|
|
350
|
+
// 2. Add to target file
|
|
351
|
+
let targetExists = true;
|
|
352
|
+
let targetContent = "";
|
|
353
|
+
try {
|
|
354
|
+
targetContent = await this.readFile(absTarget);
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
targetExists = false;
|
|
358
|
+
}
|
|
359
|
+
const symbolCode = definition.fullText;
|
|
360
|
+
const targetChanges = [];
|
|
361
|
+
if (targetExists) {
|
|
362
|
+
const targetLines = targetContent.split("\n");
|
|
363
|
+
const lastLine = targetLines.length;
|
|
364
|
+
targetChanges.push({
|
|
365
|
+
startLine: lastLine,
|
|
366
|
+
endLine: lastLine,
|
|
367
|
+
oldText: targetLines[lastLine - 1] ?? "",
|
|
368
|
+
newText: (targetLines[lastLine - 1] ?? "") + "\n\n" + symbolCode,
|
|
369
|
+
reason: "add moved symbol",
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
targetChanges.push({
|
|
374
|
+
startLine: 1,
|
|
375
|
+
endLine: 1,
|
|
376
|
+
oldText: "",
|
|
377
|
+
newText: symbolCode + "\n",
|
|
378
|
+
reason: "create file with moved symbol",
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
affectedFiles.push({
|
|
382
|
+
file: absTarget,
|
|
383
|
+
changes: targetChanges,
|
|
384
|
+
isNewFile: !targetExists,
|
|
385
|
+
isDeletedFile: false,
|
|
386
|
+
});
|
|
387
|
+
// 3. Update imports across all files
|
|
388
|
+
const allFiles = await this.collectSourceFiles(this.projectPath);
|
|
389
|
+
const importUpdates = this.generateImportUpdates(symbolName, absSource, absTarget, allFiles);
|
|
390
|
+
for (const update of importUpdates) {
|
|
391
|
+
// Find if we already have a FileChange for this file
|
|
392
|
+
const existing = affectedFiles.find((f) => f.file === update.file);
|
|
393
|
+
if (existing) {
|
|
394
|
+
existing.changes.push(update.change);
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
affectedFiles.push({
|
|
398
|
+
file: update.file,
|
|
399
|
+
changes: [update.change],
|
|
400
|
+
isNewFile: false,
|
|
401
|
+
isDeletedFile: false,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
// Breaking change if symbol was exported
|
|
406
|
+
if (definition.exported && !addReExport) {
|
|
407
|
+
breakingChanges.push({
|
|
408
|
+
type: "export_removed",
|
|
409
|
+
description: `Moving exported symbol "${symbolName}" without re-export — consumers importing from "${sourceFile}" will break`,
|
|
410
|
+
file: absSource,
|
|
411
|
+
line: definition.startLine,
|
|
412
|
+
severity: "error",
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
const totalChanges = affectedFiles.reduce((sum, f) => sum + f.changes.length, 0);
|
|
416
|
+
const riskLevel = this.assessRisk(affectedFiles.length, breakingChanges.length, totalChanges);
|
|
417
|
+
return {
|
|
418
|
+
type: "move",
|
|
419
|
+
affectedFiles,
|
|
420
|
+
totalChanges,
|
|
421
|
+
breakingChanges,
|
|
422
|
+
riskLevel,
|
|
423
|
+
warnings,
|
|
424
|
+
canAutoApply: riskLevel !== "high",
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
// ═══════════════════════════════════════════════════════════════
|
|
428
|
+
// Extract Function
|
|
429
|
+
// ═══════════════════════════════════════════════════════════════
|
|
430
|
+
/**
|
|
431
|
+
* Extract a range of lines into a new function. Infers parameters from
|
|
432
|
+
* variables used but defined outside the selection, and return values from
|
|
433
|
+
* variables modified inside the selection.
|
|
434
|
+
*
|
|
435
|
+
* @param sourceFile - File containing the code to extract
|
|
436
|
+
* @param startLine - First line to extract (1-based)
|
|
437
|
+
* @param endLine - Last line to extract (1-based, inclusive)
|
|
438
|
+
* @param functionName - Name for the new function
|
|
439
|
+
* @param targetFile - File to place the function in (same file if omitted)
|
|
440
|
+
* @returns Preview of all changes
|
|
441
|
+
*/
|
|
442
|
+
async extractFunction(sourceFile, startLine, endLine, functionName, targetFile) {
|
|
443
|
+
const warnings = [];
|
|
444
|
+
const breakingChanges = [];
|
|
445
|
+
if (!sourceFile || startLine <= 0 || endLine <= 0 || endLine < startLine) {
|
|
446
|
+
return this.emptyPreview("extract_function", ["Invalid sourceFile or line range"]);
|
|
447
|
+
}
|
|
448
|
+
const absSource = resolve(this.projectPath, sourceFile);
|
|
449
|
+
let content;
|
|
450
|
+
try {
|
|
451
|
+
content = await this.readFile(absSource);
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
return this.emptyPreview("extract_function", [`Source file not found: ${absSource}`]);
|
|
455
|
+
}
|
|
456
|
+
const lines = content.split("\n");
|
|
457
|
+
if (startLine > lines.length || endLine > lines.length) {
|
|
458
|
+
return this.emptyPreview("extract_function", ["Line range exceeds file length"]);
|
|
459
|
+
}
|
|
460
|
+
const selectedLines = lines.slice(startLine - 1, endLine);
|
|
461
|
+
const selectedCode = selectedLines.join("\n");
|
|
462
|
+
// Determine surrounding code for parameter inference
|
|
463
|
+
const beforeCode = lines.slice(0, startLine - 1).join("\n");
|
|
464
|
+
const afterCode = lines.slice(endLine).join("\n");
|
|
465
|
+
const surroundingCode = beforeCode + "\n" + afterCode;
|
|
466
|
+
// Infer parameters and return values
|
|
467
|
+
const params = this.inferParameters(selectedCode, surroundingCode);
|
|
468
|
+
const returnVars = this.inferReturnValues(selectedCode, afterCode);
|
|
469
|
+
// Build function signature
|
|
470
|
+
const paramStr = params.map((p) => `${p.name}: ${p.type}`).join(", ");
|
|
471
|
+
let returnType = "void";
|
|
472
|
+
let returnStatement = "";
|
|
473
|
+
if (returnVars.length === 1) {
|
|
474
|
+
returnType = returnVars[0].type;
|
|
475
|
+
returnStatement = `\n return ${returnVars[0].name};`;
|
|
476
|
+
}
|
|
477
|
+
else if (returnVars.length > 1) {
|
|
478
|
+
returnType = `{ ${returnVars.map((v) => `${v.name}: ${v.type}`).join("; ")} }`;
|
|
479
|
+
returnStatement = `\n return { ${returnVars.map((v) => v.name).join(", ")} };`;
|
|
480
|
+
}
|
|
481
|
+
// Determine indentation
|
|
482
|
+
const baseIndent = this.detectIndent(selectedLines[0] ?? "");
|
|
483
|
+
const dedentedCode = selectedLines.map((l) => l.replace(new RegExp(`^${baseIndent}`), " ")).join("\n");
|
|
484
|
+
const functionDef = `function ${functionName}(${paramStr}): ${returnType} {\n${dedentedCode}${returnStatement}\n}`;
|
|
485
|
+
// Build the call expression
|
|
486
|
+
const callArgs = params.map((p) => p.name).join(", ");
|
|
487
|
+
let callExpr;
|
|
488
|
+
if (returnVars.length === 0) {
|
|
489
|
+
callExpr = `${baseIndent}${functionName}(${callArgs});`;
|
|
490
|
+
}
|
|
491
|
+
else if (returnVars.length === 1) {
|
|
492
|
+
callExpr = `${baseIndent}const ${returnVars[0].name} = ${functionName}(${callArgs});`;
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
callExpr = `${baseIndent}const { ${returnVars.map((v) => v.name).join(", ")} } = ${functionName}(${callArgs});`;
|
|
496
|
+
}
|
|
497
|
+
const affectedFiles = [];
|
|
498
|
+
const sameFile = !targetFile || resolve(this.projectPath, targetFile) === absSource;
|
|
499
|
+
if (sameFile) {
|
|
500
|
+
// Replace selected lines with call, add function at end of file
|
|
501
|
+
affectedFiles.push({
|
|
502
|
+
file: absSource,
|
|
503
|
+
changes: [
|
|
504
|
+
{
|
|
505
|
+
startLine,
|
|
506
|
+
endLine,
|
|
507
|
+
oldText: selectedCode,
|
|
508
|
+
newText: callExpr,
|
|
509
|
+
reason: "replace with function call",
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
startLine: lines.length,
|
|
513
|
+
endLine: lines.length,
|
|
514
|
+
oldText: lines[lines.length - 1] ?? "",
|
|
515
|
+
newText: (lines[lines.length - 1] ?? "") + "\n\n" + functionDef,
|
|
516
|
+
reason: "add extracted function",
|
|
517
|
+
},
|
|
518
|
+
],
|
|
519
|
+
isNewFile: false,
|
|
520
|
+
isDeletedFile: false,
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
const absTarget = resolve(this.projectPath, targetFile);
|
|
525
|
+
const relPath = this.computeRelativeImportPath(absSource, absTarget);
|
|
526
|
+
const importLine = `import { ${functionName} } from "${relPath}";`;
|
|
527
|
+
// Source: replace selected lines, add import
|
|
528
|
+
const importInsertLine = this.findImportInsertLine(lines);
|
|
529
|
+
affectedFiles.push({
|
|
530
|
+
file: absSource,
|
|
531
|
+
changes: [
|
|
532
|
+
{
|
|
533
|
+
startLine: importInsertLine,
|
|
534
|
+
endLine: importInsertLine,
|
|
535
|
+
oldText: lines[importInsertLine - 1] ?? "",
|
|
536
|
+
newText: importLine + "\n" + (lines[importInsertLine - 1] ?? ""),
|
|
537
|
+
reason: "add import for extracted function",
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
startLine: startLine + 1, // +1 because we inserted a line above
|
|
541
|
+
endLine: endLine + 1,
|
|
542
|
+
oldText: selectedCode,
|
|
543
|
+
newText: callExpr,
|
|
544
|
+
reason: "replace with function call",
|
|
545
|
+
},
|
|
546
|
+
],
|
|
547
|
+
isNewFile: false,
|
|
548
|
+
isDeletedFile: false,
|
|
549
|
+
});
|
|
550
|
+
// Target: add the function
|
|
551
|
+
let targetExists = true;
|
|
552
|
+
try {
|
|
553
|
+
await this.readFile(absTarget);
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
targetExists = false;
|
|
557
|
+
}
|
|
558
|
+
affectedFiles.push({
|
|
559
|
+
file: absTarget,
|
|
560
|
+
changes: [
|
|
561
|
+
{
|
|
562
|
+
startLine: 1,
|
|
563
|
+
endLine: 1,
|
|
564
|
+
oldText: "",
|
|
565
|
+
newText: `export ${functionDef}\n`,
|
|
566
|
+
reason: "add extracted function",
|
|
567
|
+
},
|
|
568
|
+
],
|
|
569
|
+
isNewFile: !targetExists,
|
|
570
|
+
isDeletedFile: false,
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
const totalChanges = affectedFiles.reduce((sum, f) => sum + f.changes.length, 0);
|
|
574
|
+
const riskLevel = this.assessRisk(affectedFiles.length, breakingChanges.length, totalChanges);
|
|
575
|
+
return {
|
|
576
|
+
type: "extract_function",
|
|
577
|
+
affectedFiles,
|
|
578
|
+
totalChanges,
|
|
579
|
+
breakingChanges,
|
|
580
|
+
riskLevel,
|
|
581
|
+
warnings,
|
|
582
|
+
canAutoApply: riskLevel !== "high",
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
// ═══════════════════════════════════════════════════════════════
|
|
586
|
+
// Extract Interface
|
|
587
|
+
// ═══════════════════════════════════════════════════════════════
|
|
588
|
+
/**
|
|
589
|
+
* Extract an interface from a class's public members.
|
|
590
|
+
*
|
|
591
|
+
* @param className - Class to extract from
|
|
592
|
+
* @param interfaceName - Name for the new interface
|
|
593
|
+
* @param sourceFile - File containing the class
|
|
594
|
+
* @returns Preview of all changes
|
|
595
|
+
*/
|
|
596
|
+
async extractInterface(className, interfaceName, sourceFile) {
|
|
597
|
+
const warnings = [];
|
|
598
|
+
if (!className || !interfaceName || !sourceFile) {
|
|
599
|
+
return this.emptyPreview("extract_interface", ["className, interfaceName, and sourceFile are required"]);
|
|
600
|
+
}
|
|
601
|
+
const absSource = resolve(this.projectPath, sourceFile);
|
|
602
|
+
let content;
|
|
603
|
+
try {
|
|
604
|
+
content = await this.readFile(absSource);
|
|
605
|
+
}
|
|
606
|
+
catch {
|
|
607
|
+
return this.emptyPreview("extract_interface", [`Source file not found: ${absSource}`]);
|
|
608
|
+
}
|
|
609
|
+
// Find class definition and extract public methods/properties
|
|
610
|
+
const classInfo = this.extractClassMembers(content, className);
|
|
611
|
+
if (!classInfo) {
|
|
612
|
+
return this.emptyPreview("extract_interface", [`Class "${className}" not found in ${sourceFile}`]);
|
|
613
|
+
}
|
|
614
|
+
// Build interface from public members
|
|
615
|
+
const memberLines = [];
|
|
616
|
+
for (const member of classInfo.publicMembers) {
|
|
617
|
+
if (member.kind === "method") {
|
|
618
|
+
const paramStr = member.params ?? "";
|
|
619
|
+
const retType = member.returnType ?? "void";
|
|
620
|
+
memberLines.push(` ${member.name}(${paramStr}): ${retType};`);
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
const propType = member.type ?? "unknown";
|
|
624
|
+
memberLines.push(` ${member.name}: ${propType};`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
const interfaceDef = `export interface ${interfaceName} {\n${memberLines.join("\n")}\n}`;
|
|
628
|
+
// Add interface before the class and make class implement it
|
|
629
|
+
const lines = content.split("\n");
|
|
630
|
+
const affectedFiles = [];
|
|
631
|
+
const changes = [];
|
|
632
|
+
// Insert interface before class
|
|
633
|
+
changes.push({
|
|
634
|
+
startLine: classInfo.line,
|
|
635
|
+
endLine: classInfo.line,
|
|
636
|
+
oldText: lines[classInfo.line - 1],
|
|
637
|
+
newText: interfaceDef + "\n\n" + lines[classInfo.line - 1],
|
|
638
|
+
reason: "add extracted interface",
|
|
639
|
+
});
|
|
640
|
+
// Update class declaration to implement the interface
|
|
641
|
+
const classLine = lines[classInfo.line - 1];
|
|
642
|
+
const implementsMatch = classLine.match(/\bimplements\s+([^{]+)/);
|
|
643
|
+
if (implementsMatch) {
|
|
644
|
+
const updated = classLine.replace(/\bimplements\s+([^{]+)/, `implements ${implementsMatch[1].trim()}, ${interfaceName}`);
|
|
645
|
+
changes.push({
|
|
646
|
+
startLine: classInfo.line,
|
|
647
|
+
endLine: classInfo.line,
|
|
648
|
+
oldText: classLine,
|
|
649
|
+
newText: updated,
|
|
650
|
+
reason: "add implements clause",
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
const updated = classLine.replace(/\bclass\s+(\w+)(\s*(?:extends\s+\w+\s*)?)\{/, `class $1$2 implements ${interfaceName} {`);
|
|
655
|
+
if (updated !== classLine) {
|
|
656
|
+
// We already have the insert change at this line, so update the first change
|
|
657
|
+
changes[0].newText = interfaceDef + "\n\n" + updated;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
affectedFiles.push({
|
|
661
|
+
file: absSource,
|
|
662
|
+
changes: this.deduplicateChanges(changes),
|
|
663
|
+
isNewFile: false,
|
|
664
|
+
isDeletedFile: false,
|
|
665
|
+
});
|
|
666
|
+
const totalChanges = affectedFiles.reduce((sum, f) => sum + f.changes.length, 0);
|
|
667
|
+
return {
|
|
668
|
+
type: "extract_interface",
|
|
669
|
+
affectedFiles,
|
|
670
|
+
totalChanges,
|
|
671
|
+
breakingChanges: [],
|
|
672
|
+
riskLevel: "low",
|
|
673
|
+
warnings,
|
|
674
|
+
canAutoApply: true,
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
// ═══════════════════════════════════════════════════════════════
|
|
678
|
+
// Inline Function
|
|
679
|
+
// ═══════════════════════════════════════════════════════════════
|
|
680
|
+
/**
|
|
681
|
+
* Inline a simple function at all call sites (replace calls with the function body).
|
|
682
|
+
* Only works for simple single-expression or single-statement functions.
|
|
683
|
+
*
|
|
684
|
+
* @param functionName - Function to inline
|
|
685
|
+
* @param sourceFile - File where the function is defined
|
|
686
|
+
* @returns Preview of all changes
|
|
687
|
+
*/
|
|
688
|
+
async inlineFunction(functionName, sourceFile) {
|
|
689
|
+
const warnings = [];
|
|
690
|
+
if (!functionName || !sourceFile) {
|
|
691
|
+
return this.emptyPreview("inline", ["functionName and sourceFile are required"]);
|
|
692
|
+
}
|
|
693
|
+
const absSource = resolve(this.projectPath, sourceFile);
|
|
694
|
+
let content;
|
|
695
|
+
try {
|
|
696
|
+
content = await this.readFile(absSource);
|
|
697
|
+
}
|
|
698
|
+
catch {
|
|
699
|
+
return this.emptyPreview("inline", [`Source file not found: ${absSource}`]);
|
|
700
|
+
}
|
|
701
|
+
// Extract function body
|
|
702
|
+
const funcInfo = this.extractFunctionBody(content, functionName);
|
|
703
|
+
if (!funcInfo) {
|
|
704
|
+
return this.emptyPreview("inline", [`Function "${functionName}" not found or too complex to inline`]);
|
|
705
|
+
}
|
|
706
|
+
if (funcInfo.bodyLines > 5) {
|
|
707
|
+
warnings.push(`Function has ${funcInfo.bodyLines} lines — inlining may reduce readability`);
|
|
708
|
+
}
|
|
709
|
+
// Find all call sites across the project
|
|
710
|
+
const usages = await this.findAllUsages(functionName);
|
|
711
|
+
const affectedFiles = [];
|
|
712
|
+
// Group by file
|
|
713
|
+
const fileUsages = new Map();
|
|
714
|
+
for (const usage of usages) {
|
|
715
|
+
// Skip the definition itself
|
|
716
|
+
if (usage.file === absSource && usage.line >= funcInfo.startLine && usage.line <= funcInfo.endLine) {
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
const list = fileUsages.get(usage.file) ?? [];
|
|
720
|
+
list.push(usage);
|
|
721
|
+
fileUsages.set(usage.file, list);
|
|
722
|
+
}
|
|
723
|
+
for (const [file, fileUses] of fileUsages) {
|
|
724
|
+
const changes = [];
|
|
725
|
+
for (const usage of fileUses) {
|
|
726
|
+
// Check if this line contains a function call pattern
|
|
727
|
+
const callRe = new RegExp(`${this.escapeRegex(functionName)}\\s*\\(([^)]*)\\)`);
|
|
728
|
+
const callMatch = usage.context.match(callRe);
|
|
729
|
+
if (!callMatch)
|
|
730
|
+
continue;
|
|
731
|
+
const args = callMatch[1].split(",").map((a) => a.trim()).filter(Boolean);
|
|
732
|
+
let inlinedBody = funcInfo.body;
|
|
733
|
+
// Substitute parameters with arguments
|
|
734
|
+
for (let i = 0; i < funcInfo.params.length && i < args.length; i++) {
|
|
735
|
+
const paramRe = new RegExp(`\\b${this.escapeRegex(funcInfo.params[i])}\\b`, "g");
|
|
736
|
+
inlinedBody = inlinedBody.replace(paramRe, args[i]);
|
|
737
|
+
}
|
|
738
|
+
// For single-expression returns, unwrap
|
|
739
|
+
const returnMatch = inlinedBody.match(/^\s*return\s+(.+?);?\s*$/);
|
|
740
|
+
if (returnMatch) {
|
|
741
|
+
inlinedBody = returnMatch[1];
|
|
742
|
+
}
|
|
743
|
+
const newLine = usage.context.replace(callRe, inlinedBody);
|
|
744
|
+
changes.push({
|
|
745
|
+
startLine: usage.line,
|
|
746
|
+
endLine: usage.line,
|
|
747
|
+
oldText: usage.context,
|
|
748
|
+
newText: newLine,
|
|
749
|
+
reason: "inline function call",
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
if (changes.length > 0) {
|
|
753
|
+
affectedFiles.push({ file, changes, isNewFile: false, isDeletedFile: false });
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
// Remove the function definition from source
|
|
757
|
+
const sourceLines = content.split("\n");
|
|
758
|
+
const defText = sourceLines.slice(funcInfo.startLine - 1, funcInfo.endLine).join("\n");
|
|
759
|
+
affectedFiles.push({
|
|
760
|
+
file: absSource,
|
|
761
|
+
changes: [{
|
|
762
|
+
startLine: funcInfo.startLine,
|
|
763
|
+
endLine: funcInfo.endLine,
|
|
764
|
+
oldText: defText,
|
|
765
|
+
newText: "",
|
|
766
|
+
reason: "remove inlined function definition",
|
|
767
|
+
}],
|
|
768
|
+
isNewFile: false,
|
|
769
|
+
isDeletedFile: false,
|
|
770
|
+
});
|
|
771
|
+
const totalChanges = affectedFiles.reduce((sum, f) => sum + f.changes.length, 0);
|
|
772
|
+
const breakingChanges = [];
|
|
773
|
+
// If exported, it's a breaking change
|
|
774
|
+
if (funcInfo.exported) {
|
|
775
|
+
breakingChanges.push({
|
|
776
|
+
type: "export_removed",
|
|
777
|
+
description: `Inlining exported function "${functionName}" — external consumers will break`,
|
|
778
|
+
file: absSource,
|
|
779
|
+
line: funcInfo.startLine,
|
|
780
|
+
severity: "error",
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
const riskLevel = this.assessRisk(affectedFiles.length, breakingChanges.length, totalChanges);
|
|
784
|
+
return {
|
|
785
|
+
type: "inline",
|
|
786
|
+
affectedFiles,
|
|
787
|
+
totalChanges,
|
|
788
|
+
breakingChanges,
|
|
789
|
+
riskLevel,
|
|
790
|
+
warnings,
|
|
791
|
+
canAutoApply: riskLevel !== "high",
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
// ═══════════════════════════════════════════════════════════════
|
|
795
|
+
// Change Signature
|
|
796
|
+
// ═══════════════════════════════════════════════════════════════
|
|
797
|
+
/**
|
|
798
|
+
* Change a function's parameter list and/or return type. Updates the definition
|
|
799
|
+
* and all call sites.
|
|
800
|
+
*
|
|
801
|
+
* @param functionName - Function to modify
|
|
802
|
+
* @param sourceFile - File where the function is defined
|
|
803
|
+
* @param newParams - New parameter list
|
|
804
|
+
* @param newReturnType - New return type (optional)
|
|
805
|
+
* @returns Preview of all changes
|
|
806
|
+
*/
|
|
807
|
+
async changeSignature(functionName, sourceFile, newParams, newReturnType) {
|
|
808
|
+
const warnings = [];
|
|
809
|
+
const breakingChanges = [];
|
|
810
|
+
if (!functionName || !sourceFile) {
|
|
811
|
+
return this.emptyPreview("change_signature", ["functionName and sourceFile are required"]);
|
|
812
|
+
}
|
|
813
|
+
const absSource = resolve(this.projectPath, sourceFile);
|
|
814
|
+
let content;
|
|
815
|
+
try {
|
|
816
|
+
content = await this.readFile(absSource);
|
|
817
|
+
}
|
|
818
|
+
catch {
|
|
819
|
+
return this.emptyPreview("change_signature", [`Source file not found: ${absSource}`]);
|
|
820
|
+
}
|
|
821
|
+
// Find the function definition line
|
|
822
|
+
const lines = content.split("\n");
|
|
823
|
+
const funcDefRe = new RegExp(`(export\\s+)?(?:async\\s+)?function\\s+${this.escapeRegex(functionName)}\\s*\\(([^)]*)\\)(?:\\s*:\\s*([^{]+))?`);
|
|
824
|
+
let defLine = -1;
|
|
825
|
+
let defMatch = null;
|
|
826
|
+
for (let i = 0; i < lines.length; i++) {
|
|
827
|
+
defMatch = lines[i].match(funcDefRe);
|
|
828
|
+
if (defMatch) {
|
|
829
|
+
defLine = i + 1;
|
|
830
|
+
break;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
if (defLine === -1 || !defMatch) {
|
|
834
|
+
return this.emptyPreview("change_signature", [`Function "${functionName}" definition not found`]);
|
|
835
|
+
}
|
|
836
|
+
const oldParamStr = defMatch[2];
|
|
837
|
+
const oldReturnType = defMatch[3]?.trim();
|
|
838
|
+
const isExported = !!defMatch[1];
|
|
839
|
+
// Build new signature
|
|
840
|
+
const newParamStr = newParams
|
|
841
|
+
.map((p) => `${p.name}${p.optional ? "?" : ""}: ${p.type}`)
|
|
842
|
+
.join(", ");
|
|
843
|
+
const returnTypeStr = newReturnType
|
|
844
|
+
? `: ${newReturnType}`
|
|
845
|
+
: oldReturnType
|
|
846
|
+
? `: ${oldReturnType}`
|
|
847
|
+
: "";
|
|
848
|
+
const affectedFiles = [];
|
|
849
|
+
const sourceChanges = [];
|
|
850
|
+
// Update definition
|
|
851
|
+
const oldLine = lines[defLine - 1];
|
|
852
|
+
const newLine = oldLine
|
|
853
|
+
.replace(`(${oldParamStr})`, `(${newParamStr})`)
|
|
854
|
+
.replace(oldReturnType ? `: ${oldReturnType}` : /(?=\s*\{)/, returnTypeStr);
|
|
855
|
+
sourceChanges.push({
|
|
856
|
+
startLine: defLine,
|
|
857
|
+
endLine: defLine,
|
|
858
|
+
oldText: oldLine,
|
|
859
|
+
newText: newLine,
|
|
860
|
+
reason: "update function signature",
|
|
861
|
+
});
|
|
862
|
+
affectedFiles.push({
|
|
863
|
+
file: absSource,
|
|
864
|
+
changes: sourceChanges,
|
|
865
|
+
isNewFile: false,
|
|
866
|
+
isDeletedFile: false,
|
|
867
|
+
});
|
|
868
|
+
// Update call sites across project
|
|
869
|
+
const usages = await this.findAllUsages(functionName);
|
|
870
|
+
const oldParams = oldParamStr
|
|
871
|
+
.split(",")
|
|
872
|
+
.map((p) => p.trim().split(/[?:]/)[0].trim())
|
|
873
|
+
.filter(Boolean);
|
|
874
|
+
for (const usage of usages) {
|
|
875
|
+
if (usage.file === absSource && usage.line === defLine)
|
|
876
|
+
continue;
|
|
877
|
+
const callRe = new RegExp(`${this.escapeRegex(functionName)}\\s*\\(([^)]*)\\)`);
|
|
878
|
+
const callMatch = usage.context.match(callRe);
|
|
879
|
+
if (!callMatch)
|
|
880
|
+
continue;
|
|
881
|
+
const oldArgs = callMatch[1].split(",").map((a) => a.trim());
|
|
882
|
+
// Map old args to new params by name matching
|
|
883
|
+
const newArgs = [];
|
|
884
|
+
for (const param of newParams) {
|
|
885
|
+
const oldIdx = oldParams.indexOf(param.name);
|
|
886
|
+
if (oldIdx >= 0 && oldIdx < oldArgs.length) {
|
|
887
|
+
newArgs.push(oldArgs[oldIdx]);
|
|
888
|
+
}
|
|
889
|
+
else if (param.optional) {
|
|
890
|
+
// Skip optional params without old args
|
|
891
|
+
}
|
|
892
|
+
else {
|
|
893
|
+
newArgs.push(`/* TODO: ${param.name} */`);
|
|
894
|
+
warnings.push(`New required parameter "${param.name}" at ${usage.file}:${usage.line} needs a value`);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
const newCallExpr = `${functionName}(${newArgs.join(", ")})`;
|
|
898
|
+
const updatedLine = usage.context.replace(callRe, newCallExpr);
|
|
899
|
+
if (updatedLine !== usage.context) {
|
|
900
|
+
const existing = affectedFiles.find((f) => f.file === usage.file);
|
|
901
|
+
const change = {
|
|
902
|
+
startLine: usage.line,
|
|
903
|
+
endLine: usage.line,
|
|
904
|
+
oldText: usage.context,
|
|
905
|
+
newText: updatedLine,
|
|
906
|
+
reason: "update call site arguments",
|
|
907
|
+
};
|
|
908
|
+
if (existing) {
|
|
909
|
+
existing.changes.push(change);
|
|
910
|
+
}
|
|
911
|
+
else {
|
|
912
|
+
affectedFiles.push({
|
|
913
|
+
file: usage.file,
|
|
914
|
+
changes: [change],
|
|
915
|
+
isNewFile: false,
|
|
916
|
+
isDeletedFile: false,
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
if (isExported) {
|
|
922
|
+
breakingChanges.push({
|
|
923
|
+
type: "signature_change",
|
|
924
|
+
description: `Changing signature of exported function "${functionName}"`,
|
|
925
|
+
file: absSource,
|
|
926
|
+
line: defLine,
|
|
927
|
+
severity: "warning",
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
const totalChanges = affectedFiles.reduce((sum, f) => sum + f.changes.length, 0);
|
|
931
|
+
const riskLevel = this.assessRisk(affectedFiles.length, breakingChanges.length, totalChanges);
|
|
932
|
+
return {
|
|
933
|
+
type: "change_signature",
|
|
934
|
+
affectedFiles,
|
|
935
|
+
totalChanges,
|
|
936
|
+
breakingChanges,
|
|
937
|
+
riskLevel,
|
|
938
|
+
warnings,
|
|
939
|
+
canAutoApply: riskLevel !== "high",
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
// ═══════════════════════════════════════════════════════════════
|
|
943
|
+
// Safety
|
|
944
|
+
// ═══════════════════════════════════════════════════════════════
|
|
945
|
+
/**
|
|
946
|
+
* Perform safety analysis on a refactoring preview.
|
|
947
|
+
* Returns pre-check info and placeholder post-check (build verification
|
|
948
|
+
* must be run separately after apply).
|
|
949
|
+
*
|
|
950
|
+
* @param preview - The refactoring preview to analyze
|
|
951
|
+
* @returns Safety analysis result
|
|
952
|
+
*/
|
|
953
|
+
async checkSafety(preview) {
|
|
954
|
+
return {
|
|
955
|
+
preCheck: {
|
|
956
|
+
affectedFiles: preview.affectedFiles.map((f) => f.file),
|
|
957
|
+
breakingChanges: preview.breakingChanges,
|
|
958
|
+
riskLevel: preview.riskLevel,
|
|
959
|
+
},
|
|
960
|
+
postCheck: {
|
|
961
|
+
// These must be verified after apply() by running tsc/build
|
|
962
|
+
buildSuccess: false,
|
|
963
|
+
noNewErrors: false,
|
|
964
|
+
},
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
// ═══════════════════════════════════════════════════════════════
|
|
968
|
+
// Private: Find Usages
|
|
969
|
+
// ═══════════════════════════════════════════════════════════════
|
|
970
|
+
/**
|
|
971
|
+
* Find all usages of a symbol across the project using regex word boundary matching.
|
|
972
|
+
* Excludes occurrences inside comments and string literals.
|
|
973
|
+
*/
|
|
974
|
+
async findAllUsages(symbolName, scopeFile) {
|
|
975
|
+
const usages = [];
|
|
976
|
+
const files = scopeFile
|
|
977
|
+
? [resolve(this.projectPath, scopeFile)]
|
|
978
|
+
: await this.collectSourceFiles(this.projectPath);
|
|
979
|
+
const symbolRe = new RegExp(`(?<![.\\w])${this.escapeRegex(symbolName)}(?!\\w)`, "g");
|
|
980
|
+
for (const file of files) {
|
|
981
|
+
let content;
|
|
982
|
+
try {
|
|
983
|
+
content = await this.readFile(file);
|
|
984
|
+
}
|
|
985
|
+
catch {
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
const lines = content.split("\n");
|
|
989
|
+
for (let i = 0; i < lines.length; i++) {
|
|
990
|
+
const line = lines[i];
|
|
991
|
+
if (this.isCommentLine(line))
|
|
992
|
+
continue;
|
|
993
|
+
if (!symbolRe.test(line))
|
|
994
|
+
continue;
|
|
995
|
+
symbolRe.lastIndex = 0;
|
|
996
|
+
// Check that the match is not inside a string literal
|
|
997
|
+
if (this.isInStringLiteral(line, symbolName))
|
|
998
|
+
continue;
|
|
999
|
+
usages.push({
|
|
1000
|
+
file,
|
|
1001
|
+
line: i + 1,
|
|
1002
|
+
column: line.indexOf(symbolName),
|
|
1003
|
+
context: line,
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
return usages;
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Find all import statements that reference a symbol.
|
|
1011
|
+
*/
|
|
1012
|
+
async findImports(symbolName) {
|
|
1013
|
+
const results = [];
|
|
1014
|
+
const files = await this.collectSourceFiles(this.projectPath);
|
|
1015
|
+
for (const file of files) {
|
|
1016
|
+
let content;
|
|
1017
|
+
try {
|
|
1018
|
+
content = await this.readFile(file);
|
|
1019
|
+
}
|
|
1020
|
+
catch {
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
const lines = content.split("\n");
|
|
1024
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1025
|
+
const line = lines[i];
|
|
1026
|
+
// Named import: import { X, Y } from "Z"
|
|
1027
|
+
const namedRe = new RegExp(IMPORT_NAMED_RE.source, "g");
|
|
1028
|
+
let match;
|
|
1029
|
+
while ((match = namedRe.exec(line)) !== null) {
|
|
1030
|
+
const symbols = match[1].split(",").map((s) => s.trim().split(/\s+as\s+/)[0].trim());
|
|
1031
|
+
if (symbols.includes(symbolName)) {
|
|
1032
|
+
results.push({ file, line: i + 1, importStatement: line, isDefault: false });
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
// Default import: import X from "Z"
|
|
1036
|
+
const defaultRe = new RegExp(IMPORT_DEFAULT_RE.source, "g");
|
|
1037
|
+
while ((match = defaultRe.exec(line)) !== null) {
|
|
1038
|
+
if (match[1] === symbolName) {
|
|
1039
|
+
results.push({ file, line: i + 1, importStatement: line, isDefault: true });
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
// Re-export: export { X } from "Z"
|
|
1043
|
+
const reExportRe = new RegExp(RE_EXPORT_RE.source, "g");
|
|
1044
|
+
while ((match = reExportRe.exec(line)) !== null) {
|
|
1045
|
+
const symbols = match[1].split(",").map((s) => s.trim().split(/\s+as\s+/)[0].trim());
|
|
1046
|
+
if (symbols.includes(symbolName)) {
|
|
1047
|
+
results.push({ file, line: i + 1, importStatement: line, isDefault: false });
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
return results;
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Generate import path updates when a symbol moves between files.
|
|
1056
|
+
*/
|
|
1057
|
+
generateImportUpdates(symbolName, oldFile, newFile, files) {
|
|
1058
|
+
const results = [];
|
|
1059
|
+
// We need to synchronously scan files that have already been read.
|
|
1060
|
+
// For preview purposes, we'll do a simulated scan based on files list.
|
|
1061
|
+
// The actual file reading happens in apply(). For preview, we return
|
|
1062
|
+
// placeholder updates that will be resolved during apply.
|
|
1063
|
+
// This is a limitation of the synchronous generateImportUpdates interface.
|
|
1064
|
+
// In practice, this method is called after findImports has already scanned files.
|
|
1065
|
+
// We return empty here and let the moveSymbol method handle imports via findImports.
|
|
1066
|
+
return results;
|
|
1067
|
+
}
|
|
1068
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1069
|
+
// Private: Symbol Analysis
|
|
1070
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1071
|
+
/**
|
|
1072
|
+
* Check if a symbol is exported from any file.
|
|
1073
|
+
*/
|
|
1074
|
+
async isSymbolExported(symbolName) {
|
|
1075
|
+
const files = await this.collectSourceFiles(this.projectPath);
|
|
1076
|
+
const exportRe = new RegExp(`export\\s+(?:declare\\s+)?(?:abstract\\s+)?(?:async\\s+)?(?:function\\s*\\*?|class|const|let|var|interface|type|enum)\\s+${this.escapeRegex(symbolName)}\\b`);
|
|
1077
|
+
for (const file of files) {
|
|
1078
|
+
let content;
|
|
1079
|
+
try {
|
|
1080
|
+
content = await this.readFile(file);
|
|
1081
|
+
}
|
|
1082
|
+
catch {
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
const lines = content.split("\n");
|
|
1086
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1087
|
+
if (exportRe.test(lines[i])) {
|
|
1088
|
+
return { file, line: i + 1 };
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
return null;
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Extract a symbol's full definition (with JSDoc) from file content.
|
|
1096
|
+
*/
|
|
1097
|
+
extractDefinition(content, symbolName) {
|
|
1098
|
+
const lines = content.split("\n");
|
|
1099
|
+
// Pattern: [export] [async] function/class/interface/type/enum/const NAME
|
|
1100
|
+
const declRe = new RegExp(`^(\\s*)(export\\s+)?(?:declare\\s+)?(?:abstract\\s+)?(?:async\\s+)?(?:function\\s*\\*?|class|interface|type|enum|const|let|var)\\s+${this.escapeRegex(symbolName)}\\b`);
|
|
1101
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1102
|
+
if (!declRe.test(lines[i]))
|
|
1103
|
+
continue;
|
|
1104
|
+
const exported = /\bexport\b/.test(lines[i]);
|
|
1105
|
+
// Look backward for JSDoc
|
|
1106
|
+
let jsdocStart = i;
|
|
1107
|
+
if (i > 0 && lines[i - 1].trim().endsWith("*/")) {
|
|
1108
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
1109
|
+
jsdocStart = j;
|
|
1110
|
+
if (lines[j].trim().startsWith("/**") || lines[j].trim().startsWith("/*"))
|
|
1111
|
+
break;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
// Find end of definition by brace matching
|
|
1115
|
+
const endLine = this.findDefinitionEnd(lines, i);
|
|
1116
|
+
const fullText = lines.slice(jsdocStart, endLine).join("\n");
|
|
1117
|
+
return {
|
|
1118
|
+
fullText,
|
|
1119
|
+
startLine: jsdocStart + 1,
|
|
1120
|
+
endLine,
|
|
1121
|
+
exported,
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
return null;
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Find the end line of a definition by tracking brace depth.
|
|
1128
|
+
* For single-line type aliases or simple declarations, returns the same line.
|
|
1129
|
+
*/
|
|
1130
|
+
findDefinitionEnd(lines, startIdx) {
|
|
1131
|
+
let braceDepth = 0;
|
|
1132
|
+
let foundOpen = false;
|
|
1133
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
1134
|
+
const line = lines[i];
|
|
1135
|
+
for (const ch of line) {
|
|
1136
|
+
if (ch === "{") {
|
|
1137
|
+
braceDepth++;
|
|
1138
|
+
foundOpen = true;
|
|
1139
|
+
}
|
|
1140
|
+
else if (ch === "}") {
|
|
1141
|
+
braceDepth--;
|
|
1142
|
+
if (foundOpen && braceDepth === 0) {
|
|
1143
|
+
return i + 1; // 1-based end line
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
// For type aliases / single line declarations without braces
|
|
1148
|
+
if (!foundOpen && line.includes(";")) {
|
|
1149
|
+
return i + 1;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
// Fallback: return the start line
|
|
1153
|
+
return startIdx + 1;
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Extract public members from a class definition.
|
|
1157
|
+
*/
|
|
1158
|
+
extractClassMembers(content, className) {
|
|
1159
|
+
const lines = content.split("\n");
|
|
1160
|
+
const classRe = new RegExp(`(?:export\\s+)?(?:abstract\\s+)?class\\s+${this.escapeRegex(className)}\\b`);
|
|
1161
|
+
let classLine = -1;
|
|
1162
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1163
|
+
if (classRe.test(lines[i])) {
|
|
1164
|
+
classLine = i;
|
|
1165
|
+
break;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
if (classLine === -1)
|
|
1169
|
+
return null;
|
|
1170
|
+
const endLine = this.findDefinitionEnd(lines, classLine);
|
|
1171
|
+
const classBody = lines.slice(classLine + 1, endLine - 1);
|
|
1172
|
+
const publicMembers = [];
|
|
1173
|
+
// Method pattern: [async] methodName(params): ReturnType {
|
|
1174
|
+
const methodRe = /^\s*(?:async\s+)?(\w+)\s*\(([^)]*)\)(?:\s*:\s*([^{;]+))?\s*\{?/;
|
|
1175
|
+
// Property pattern: [readonly] propName: Type;
|
|
1176
|
+
const propRe = /^\s*(?:readonly\s+)?(\w+)(?:\?)?:\s*([^;=]+)/;
|
|
1177
|
+
for (const bodyLine of classBody) {
|
|
1178
|
+
const trimmed = bodyLine.trim();
|
|
1179
|
+
// Skip private/protected members
|
|
1180
|
+
if (trimmed.startsWith("private ") || trimmed.startsWith("protected ") || trimmed.startsWith("#")) {
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1183
|
+
// Skip constructor
|
|
1184
|
+
if (trimmed.startsWith("constructor"))
|
|
1185
|
+
continue;
|
|
1186
|
+
const mMatch = trimmed.match(methodRe);
|
|
1187
|
+
if (mMatch && mMatch[1] !== "get" && mMatch[1] !== "set") {
|
|
1188
|
+
publicMembers.push({
|
|
1189
|
+
name: mMatch[1],
|
|
1190
|
+
kind: "method",
|
|
1191
|
+
params: mMatch[2]?.trim(),
|
|
1192
|
+
returnType: mMatch[3]?.trim(),
|
|
1193
|
+
});
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
const pMatch = trimmed.match(propRe);
|
|
1197
|
+
if (pMatch) {
|
|
1198
|
+
publicMembers.push({
|
|
1199
|
+
name: pMatch[1],
|
|
1200
|
+
kind: "property",
|
|
1201
|
+
type: pMatch[2]?.trim(),
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
return { line: classLine + 1, publicMembers };
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Extract a function's body and metadata for inlining.
|
|
1209
|
+
*/
|
|
1210
|
+
extractFunctionBody(content, functionName) {
|
|
1211
|
+
const lines = content.split("\n");
|
|
1212
|
+
const funcRe = new RegExp(`^(\\s*)(export\\s+)?(?:async\\s+)?function\\s+${this.escapeRegex(functionName)}\\s*\\(([^)]*)\\)`);
|
|
1213
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1214
|
+
const match = lines[i].match(funcRe);
|
|
1215
|
+
if (!match)
|
|
1216
|
+
continue;
|
|
1217
|
+
const exported = !!match[2];
|
|
1218
|
+
const paramsStr = match[3];
|
|
1219
|
+
const params = paramsStr
|
|
1220
|
+
.split(",")
|
|
1221
|
+
.map((p) => p.trim().split(/[?:]/)[0].trim())
|
|
1222
|
+
.filter(Boolean);
|
|
1223
|
+
const endLine = this.findDefinitionEnd(lines, i);
|
|
1224
|
+
// Extract body (everything between first { and last })
|
|
1225
|
+
const fullText = lines.slice(i, endLine).join("\n");
|
|
1226
|
+
const braceStart = fullText.indexOf("{");
|
|
1227
|
+
const braceEnd = fullText.lastIndexOf("}");
|
|
1228
|
+
if (braceStart === -1 || braceEnd === -1)
|
|
1229
|
+
return null;
|
|
1230
|
+
const body = fullText.slice(braceStart + 1, braceEnd).trim();
|
|
1231
|
+
const bodyLines = body.split("\n").length;
|
|
1232
|
+
return {
|
|
1233
|
+
body,
|
|
1234
|
+
params,
|
|
1235
|
+
startLine: i + 1,
|
|
1236
|
+
endLine,
|
|
1237
|
+
bodyLines,
|
|
1238
|
+
exported,
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
return null;
|
|
1242
|
+
}
|
|
1243
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1244
|
+
// Private: Parameter Inference
|
|
1245
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1246
|
+
/**
|
|
1247
|
+
* Infer function parameters from a code block by finding variables
|
|
1248
|
+
* that are used but not defined within the selection.
|
|
1249
|
+
*/
|
|
1250
|
+
inferParameters(code, surroundingCode) {
|
|
1251
|
+
// Find all identifiers used in the code
|
|
1252
|
+
const identRe = /\b([a-zA-Z_$][\w$]*)\b/g;
|
|
1253
|
+
const usedIdents = new Set();
|
|
1254
|
+
let match;
|
|
1255
|
+
while ((match = identRe.exec(code)) !== null) {
|
|
1256
|
+
usedIdents.add(match[1]);
|
|
1257
|
+
}
|
|
1258
|
+
// Find identifiers defined within the code (const/let/var/function/class declarations)
|
|
1259
|
+
const declRe = /(?:const|let|var|function|class)\s+(\w+)/g;
|
|
1260
|
+
const declaredInCode = new Set();
|
|
1261
|
+
while ((match = declRe.exec(code)) !== null) {
|
|
1262
|
+
declaredInCode.add(match[1]);
|
|
1263
|
+
}
|
|
1264
|
+
// Find identifiers declared in surrounding code
|
|
1265
|
+
const declaredOutside = new Set();
|
|
1266
|
+
const outsideDeclRe = /(?:const|let|var|function|class)\s+(\w+)/g;
|
|
1267
|
+
while ((match = outsideDeclRe.exec(surroundingCode)) !== null) {
|
|
1268
|
+
declaredOutside.add(match[1]);
|
|
1269
|
+
}
|
|
1270
|
+
// Also look for parameter patterns: (name: Type)
|
|
1271
|
+
const paramDeclRe = /(\w+)\s*[?]?:\s*(\w[\w<>,\s|&[\]]*)/g;
|
|
1272
|
+
const typeMap = new Map();
|
|
1273
|
+
while ((match = paramDeclRe.exec(surroundingCode)) !== null) {
|
|
1274
|
+
typeMap.set(match[1], match[2].trim());
|
|
1275
|
+
}
|
|
1276
|
+
// Parameters = used in code, not declared in code, but declared outside
|
|
1277
|
+
const keywords = new Set([
|
|
1278
|
+
"const", "let", "var", "function", "class", "if", "else", "for",
|
|
1279
|
+
"while", "return", "import", "export", "from", "new", "this",
|
|
1280
|
+
"true", "false", "null", "undefined", "typeof", "instanceof",
|
|
1281
|
+
"void", "async", "await", "try", "catch", "throw", "switch",
|
|
1282
|
+
"case", "break", "continue", "default", "do", "in", "of",
|
|
1283
|
+
"delete", "yield", "super", "extends", "implements", "interface",
|
|
1284
|
+
"type", "enum", "as", "is", "keyof", "readonly", "declare",
|
|
1285
|
+
"abstract", "static", "public", "private", "protected",
|
|
1286
|
+
"console", "Math", "JSON", "Array", "Object", "String",
|
|
1287
|
+
"Number", "Boolean", "Date", "Error", "Promise", "Map", "Set",
|
|
1288
|
+
"RegExp", "Symbol", "BigInt", "Infinity", "NaN",
|
|
1289
|
+
]);
|
|
1290
|
+
const params = [];
|
|
1291
|
+
for (const ident of usedIdents) {
|
|
1292
|
+
if (declaredInCode.has(ident))
|
|
1293
|
+
continue;
|
|
1294
|
+
if (keywords.has(ident))
|
|
1295
|
+
continue;
|
|
1296
|
+
if (!declaredOutside.has(ident))
|
|
1297
|
+
continue;
|
|
1298
|
+
const type = typeMap.get(ident) ?? "unknown";
|
|
1299
|
+
params.push({ name: ident, type });
|
|
1300
|
+
}
|
|
1301
|
+
return params;
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Infer return values: variables modified inside the selection that are
|
|
1305
|
+
* used after the selection.
|
|
1306
|
+
*/
|
|
1307
|
+
inferReturnValues(code, afterCode) {
|
|
1308
|
+
// Find variables assigned in the code
|
|
1309
|
+
const assignRe = /(?:let|var)\s+(\w+)/g;
|
|
1310
|
+
const assignedVars = new Set();
|
|
1311
|
+
let match;
|
|
1312
|
+
while ((match = assignRe.exec(code)) !== null) {
|
|
1313
|
+
assignedVars.add(match[1]);
|
|
1314
|
+
}
|
|
1315
|
+
// Also find direct reassignments
|
|
1316
|
+
const reassignRe = /^(\s*)(\w+)\s*=[^=]/gm;
|
|
1317
|
+
while ((match = reassignRe.exec(code)) !== null) {
|
|
1318
|
+
assignedVars.add(match[2]);
|
|
1319
|
+
}
|
|
1320
|
+
// Check which are used after the selection
|
|
1321
|
+
const results = [];
|
|
1322
|
+
for (const varName of assignedVars) {
|
|
1323
|
+
const useRe = new RegExp(`\\b${this.escapeRegex(varName)}\\b`);
|
|
1324
|
+
if (useRe.test(afterCode)) {
|
|
1325
|
+
results.push({ name: varName, type: "unknown" });
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
return results;
|
|
1329
|
+
}
|
|
1330
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1331
|
+
// Private: File Utilities
|
|
1332
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1333
|
+
/**
|
|
1334
|
+
* Read a file's content as UTF-8 text.
|
|
1335
|
+
*/
|
|
1336
|
+
async readFile(filePath) {
|
|
1337
|
+
// Path containment check: ensure file is within project root
|
|
1338
|
+
const resolved = resolve(filePath);
|
|
1339
|
+
if (!resolved.startsWith(this.projectPath)) {
|
|
1340
|
+
throw new Error(`Path traversal blocked: "${resolved}" is outside project root`);
|
|
1341
|
+
}
|
|
1342
|
+
return readFile(filePath, "utf-8");
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Apply text changes to file content. Changes are applied in reverse line order
|
|
1346
|
+
* to preserve line numbers.
|
|
1347
|
+
*/
|
|
1348
|
+
applyChanges(content, changes) {
|
|
1349
|
+
const lines = content.split("\n");
|
|
1350
|
+
// Sort changes by startLine descending so later changes don't shift earlier ones
|
|
1351
|
+
const sorted = [...changes].sort((a, b) => b.startLine - a.startLine);
|
|
1352
|
+
for (const change of sorted) {
|
|
1353
|
+
const startIdx = change.startLine - 1;
|
|
1354
|
+
const endIdx = change.endLine; // exclusive in splice
|
|
1355
|
+
const count = endIdx - startIdx;
|
|
1356
|
+
if (change.newText === "") {
|
|
1357
|
+
// Delete the lines
|
|
1358
|
+
lines.splice(startIdx, count);
|
|
1359
|
+
}
|
|
1360
|
+
else {
|
|
1361
|
+
const newLines = change.newText.split("\n");
|
|
1362
|
+
lines.splice(startIdx, count, ...newLines);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
return lines.join("\n");
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Snapshot files for potential rollback. Returns a unique rollback ID.
|
|
1369
|
+
* Maintains a maximum of MAX_ROLLBACKS snapshots.
|
|
1370
|
+
*/
|
|
1371
|
+
async snapshotFiles(files) {
|
|
1372
|
+
const id = randomUUID();
|
|
1373
|
+
const snapshot = new Map();
|
|
1374
|
+
for (const file of files) {
|
|
1375
|
+
try {
|
|
1376
|
+
const content = await this.readFile(file);
|
|
1377
|
+
snapshot.set(file, content);
|
|
1378
|
+
}
|
|
1379
|
+
catch {
|
|
1380
|
+
// File doesn't exist yet — store empty to indicate deletion on rollback
|
|
1381
|
+
snapshot.set(file, "");
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
this.rollbacks.set(id, snapshot);
|
|
1385
|
+
this.rollbackOrder.push(id);
|
|
1386
|
+
// Evict oldest rollbacks if over limit
|
|
1387
|
+
while (this.rollbackOrder.length > MAX_ROLLBACKS) {
|
|
1388
|
+
const oldest = this.rollbackOrder.shift();
|
|
1389
|
+
this.rollbacks.delete(oldest);
|
|
1390
|
+
}
|
|
1391
|
+
return id;
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Recursively collect all TypeScript/JavaScript source files in a directory.
|
|
1395
|
+
*/
|
|
1396
|
+
async collectSourceFiles(dir) {
|
|
1397
|
+
const results = [];
|
|
1398
|
+
let entries;
|
|
1399
|
+
try {
|
|
1400
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
1401
|
+
}
|
|
1402
|
+
catch {
|
|
1403
|
+
return results;
|
|
1404
|
+
}
|
|
1405
|
+
for (const entry of entries) {
|
|
1406
|
+
// Skip symlinks to prevent escaping project root
|
|
1407
|
+
if (entry.isSymbolicLink())
|
|
1408
|
+
continue;
|
|
1409
|
+
const fullPath = join(dir, entry.name);
|
|
1410
|
+
// Ensure path stays within project root
|
|
1411
|
+
if (!resolve(fullPath).startsWith(this.projectPath))
|
|
1412
|
+
continue;
|
|
1413
|
+
if (entry.isDirectory()) {
|
|
1414
|
+
if (!SKIP_DIRS.has(entry.name)) {
|
|
1415
|
+
const sub = await this.collectSourceFiles(fullPath);
|
|
1416
|
+
results.push(...sub);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
else if (entry.isFile() && SOURCE_EXTENSIONS.has(extname(entry.name))) {
|
|
1420
|
+
results.push(fullPath);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
return results;
|
|
1424
|
+
}
|
|
1425
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1426
|
+
// Private: String / Line Utilities
|
|
1427
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1428
|
+
/**
|
|
1429
|
+
* Replace a symbol name in a line using word boundary matching.
|
|
1430
|
+
* Preserves string literals and comments.
|
|
1431
|
+
*/
|
|
1432
|
+
replaceSymbolInLine(line, oldName, newName) {
|
|
1433
|
+
const re = new RegExp(`(?<![.\\w])${this.escapeRegex(oldName)}(?!\\w)`, "g");
|
|
1434
|
+
return line.replace(re, newName);
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Check if a line is a comment (single-line // or starts inside a block comment).
|
|
1438
|
+
*/
|
|
1439
|
+
isCommentLine(line) {
|
|
1440
|
+
const trimmed = line.trim();
|
|
1441
|
+
return (trimmed.startsWith("//") ||
|
|
1442
|
+
trimmed.startsWith("*") ||
|
|
1443
|
+
trimmed.startsWith("/*") ||
|
|
1444
|
+
trimmed.startsWith("*/"));
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Basic heuristic: check if a symbol occurrence is inside a string literal.
|
|
1448
|
+
* Counts unescaped quotes before the symbol position.
|
|
1449
|
+
*/
|
|
1450
|
+
isInStringLiteral(line, symbolName) {
|
|
1451
|
+
const idx = line.indexOf(symbolName);
|
|
1452
|
+
if (idx === -1)
|
|
1453
|
+
return false;
|
|
1454
|
+
// Strip comments first
|
|
1455
|
+
const noComment = line.replace(/\/\/.*$/, "");
|
|
1456
|
+
if (idx >= noComment.length)
|
|
1457
|
+
return true; // symbol is in a comment
|
|
1458
|
+
// Count quotes before the symbol
|
|
1459
|
+
const before = noComment.slice(0, idx);
|
|
1460
|
+
const singleQuotes = (before.match(/(?<!\\)'/g) ?? []).length;
|
|
1461
|
+
const doubleQuotes = (before.match(/(?<!\\)"/g) ?? []).length;
|
|
1462
|
+
const backticks = (before.match(/(?<!\\)`/g) ?? []).length;
|
|
1463
|
+
// If any quote count is odd, we're inside a string
|
|
1464
|
+
return singleQuotes % 2 !== 0 || doubleQuotes % 2 !== 0 || backticks % 2 !== 0;
|
|
1465
|
+
}
|
|
1466
|
+
/**
|
|
1467
|
+
* Compute the relative import path from one file to another,
|
|
1468
|
+
* with .js extension (ESM convention).
|
|
1469
|
+
*/
|
|
1470
|
+
computeRelativeImportPath(fromFile, toFile) {
|
|
1471
|
+
const fromDir = dirname(fromFile);
|
|
1472
|
+
let rel = relative(fromDir, toFile);
|
|
1473
|
+
// Replace .ts/.tsx extension with .js
|
|
1474
|
+
rel = rel.replace(/\.tsx?$/, ".js");
|
|
1475
|
+
// Ensure it starts with ./
|
|
1476
|
+
if (!rel.startsWith(".")) {
|
|
1477
|
+
rel = "./" + rel;
|
|
1478
|
+
}
|
|
1479
|
+
return rel;
|
|
1480
|
+
}
|
|
1481
|
+
/**
|
|
1482
|
+
* Find the best line to insert a new import statement.
|
|
1483
|
+
* Returns the line number (1-based) after the last existing import.
|
|
1484
|
+
*/
|
|
1485
|
+
findImportInsertLine(lines) {
|
|
1486
|
+
let lastImportLine = 0;
|
|
1487
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1488
|
+
if (/^\s*import\s/.test(lines[i])) {
|
|
1489
|
+
lastImportLine = i + 1;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
return lastImportLine > 0 ? lastImportLine + 1 : 1;
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* Detect the leading whitespace (indentation) of a line.
|
|
1496
|
+
*/
|
|
1497
|
+
detectIndent(line) {
|
|
1498
|
+
const match = line.match(/^(\s*)/);
|
|
1499
|
+
return match ? match[1] : "";
|
|
1500
|
+
}
|
|
1501
|
+
/**
|
|
1502
|
+
* Escape special regex characters in a string.
|
|
1503
|
+
*/
|
|
1504
|
+
escapeRegex(str) {
|
|
1505
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1506
|
+
}
|
|
1507
|
+
/**
|
|
1508
|
+
* Deduplicate text changes for the same line, keeping only the last one.
|
|
1509
|
+
*/
|
|
1510
|
+
deduplicateChanges(changes) {
|
|
1511
|
+
const seen = new Map();
|
|
1512
|
+
for (const change of changes) {
|
|
1513
|
+
seen.set(change.startLine, change);
|
|
1514
|
+
}
|
|
1515
|
+
return [...seen.values()].sort((a, b) => a.startLine - b.startLine);
|
|
1516
|
+
}
|
|
1517
|
+
/**
|
|
1518
|
+
* Assess the risk level of a refactoring based on scope and breaking changes.
|
|
1519
|
+
*/
|
|
1520
|
+
assessRisk(fileCount, breakingCount, changeCount) {
|
|
1521
|
+
if (breakingCount > 0 && fileCount > 5)
|
|
1522
|
+
return "high";
|
|
1523
|
+
if (breakingCount > 0)
|
|
1524
|
+
return "medium";
|
|
1525
|
+
if (fileCount > 10 || changeCount > 50)
|
|
1526
|
+
return "medium";
|
|
1527
|
+
return "low";
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1530
|
+
* Create an empty preview with warnings (for error cases).
|
|
1531
|
+
*/
|
|
1532
|
+
emptyPreview(type, warnings) {
|
|
1533
|
+
return {
|
|
1534
|
+
type,
|
|
1535
|
+
affectedFiles: [],
|
|
1536
|
+
totalChanges: 0,
|
|
1537
|
+
breakingChanges: [],
|
|
1538
|
+
riskLevel: "low",
|
|
1539
|
+
warnings,
|
|
1540
|
+
canAutoApply: false,
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
//# sourceMappingURL=cross-file-refactor.js.map
|