driftdetect-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/dist/analyzers/ast-analyzer.d.ts +251 -0
- package/dist/analyzers/ast-analyzer.d.ts.map +1 -0
- package/dist/analyzers/ast-analyzer.js +548 -0
- package/dist/analyzers/ast-analyzer.js.map +1 -0
- package/dist/analyzers/flow-analyzer.d.ts +241 -0
- package/dist/analyzers/flow-analyzer.d.ts.map +1 -0
- package/dist/analyzers/flow-analyzer.js +1219 -0
- package/dist/analyzers/flow-analyzer.js.map +1 -0
- package/dist/analyzers/index.d.ts +18 -0
- package/dist/analyzers/index.d.ts.map +1 -0
- package/dist/analyzers/index.js +19 -0
- package/dist/analyzers/index.js.map +1 -0
- package/dist/analyzers/semantic-analyzer.d.ts +252 -0
- package/dist/analyzers/semantic-analyzer.d.ts.map +1 -0
- package/dist/analyzers/semantic-analyzer.js +1182 -0
- package/dist/analyzers/semantic-analyzer.js.map +1 -0
- package/dist/analyzers/type-analyzer.d.ts +289 -0
- package/dist/analyzers/type-analyzer.d.ts.map +1 -0
- package/dist/analyzers/type-analyzer.js +1269 -0
- package/dist/analyzers/type-analyzer.js.map +1 -0
- package/dist/analyzers/types.d.ts +537 -0
- package/dist/analyzers/types.d.ts.map +1 -0
- package/dist/analyzers/types.js +11 -0
- package/dist/analyzers/types.js.map +1 -0
- package/dist/config/config-loader.d.ts +166 -0
- package/dist/config/config-loader.d.ts.map +1 -0
- package/dist/config/config-loader.js +429 -0
- package/dist/config/config-loader.js.map +1 -0
- package/dist/config/config-validator.d.ts +204 -0
- package/dist/config/config-validator.d.ts.map +1 -0
- package/dist/config/config-validator.js +632 -0
- package/dist/config/config-validator.js.map +1 -0
- package/dist/config/defaults.d.ts +8 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +26 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +10 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/types.d.ts +47 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +7 -0
- package/dist/config/types.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest/exporter.d.ts +21 -0
- package/dist/manifest/exporter.d.ts.map +1 -0
- package/dist/manifest/exporter.js +339 -0
- package/dist/manifest/exporter.js.map +1 -0
- package/dist/manifest/index.d.ts +14 -0
- package/dist/manifest/index.d.ts.map +1 -0
- package/dist/manifest/index.js +15 -0
- package/dist/manifest/index.js.map +1 -0
- package/dist/manifest/manifest-store.d.ts +111 -0
- package/dist/manifest/manifest-store.d.ts.map +1 -0
- package/dist/manifest/manifest-store.js +418 -0
- package/dist/manifest/manifest-store.js.map +1 -0
- package/dist/manifest/types.d.ts +238 -0
- package/dist/manifest/types.d.ts.map +1 -0
- package/dist/manifest/types.js +11 -0
- package/dist/manifest/types.js.map +1 -0
- package/dist/matcher/confidence-scorer.d.ts +188 -0
- package/dist/matcher/confidence-scorer.d.ts.map +1 -0
- package/dist/matcher/confidence-scorer.js +302 -0
- package/dist/matcher/confidence-scorer.js.map +1 -0
- package/dist/matcher/index.d.ts +24 -0
- package/dist/matcher/index.d.ts.map +1 -0
- package/dist/matcher/index.js +26 -0
- package/dist/matcher/index.js.map +1 -0
- package/dist/matcher/outlier-detector.d.ts +252 -0
- package/dist/matcher/outlier-detector.d.ts.map +1 -0
- package/dist/matcher/outlier-detector.js +544 -0
- package/dist/matcher/outlier-detector.js.map +1 -0
- package/dist/matcher/pattern-matcher.d.ts +169 -0
- package/dist/matcher/pattern-matcher.d.ts.map +1 -0
- package/dist/matcher/pattern-matcher.js +692 -0
- package/dist/matcher/pattern-matcher.js.map +1 -0
- package/dist/matcher/types.d.ts +476 -0
- package/dist/matcher/types.d.ts.map +1 -0
- package/dist/matcher/types.js +36 -0
- package/dist/matcher/types.js.map +1 -0
- package/dist/parsers/base-parser.d.ts +282 -0
- package/dist/parsers/base-parser.d.ts.map +1 -0
- package/dist/parsers/base-parser.js +421 -0
- package/dist/parsers/base-parser.js.map +1 -0
- package/dist/parsers/css-parser.d.ts +225 -0
- package/dist/parsers/css-parser.d.ts.map +1 -0
- package/dist/parsers/css-parser.js +477 -0
- package/dist/parsers/css-parser.js.map +1 -0
- package/dist/parsers/index.d.ts +15 -0
- package/dist/parsers/index.d.ts.map +1 -0
- package/dist/parsers/index.js +15 -0
- package/dist/parsers/index.js.map +1 -0
- package/dist/parsers/json-parser.d.ts +219 -0
- package/dist/parsers/json-parser.d.ts.map +1 -0
- package/dist/parsers/json-parser.js +602 -0
- package/dist/parsers/json-parser.js.map +1 -0
- package/dist/parsers/markdown-parser.d.ts +276 -0
- package/dist/parsers/markdown-parser.d.ts.map +1 -0
- package/dist/parsers/markdown-parser.js +731 -0
- package/dist/parsers/markdown-parser.js.map +1 -0
- package/dist/parsers/parser-manager.d.ts +294 -0
- package/dist/parsers/parser-manager.d.ts.map +1 -0
- package/dist/parsers/parser-manager.js +738 -0
- package/dist/parsers/parser-manager.js.map +1 -0
- package/dist/parsers/python-parser.d.ts +204 -0
- package/dist/parsers/python-parser.d.ts.map +1 -0
- package/dist/parsers/python-parser.js +517 -0
- package/dist/parsers/python-parser.js.map +1 -0
- package/dist/parsers/types.d.ts +43 -0
- package/dist/parsers/types.d.ts.map +1 -0
- package/dist/parsers/types.js +7 -0
- package/dist/parsers/types.js.map +1 -0
- package/dist/parsers/typescript-parser.d.ts +264 -0
- package/dist/parsers/typescript-parser.d.ts.map +1 -0
- package/dist/parsers/typescript-parser.js +658 -0
- package/dist/parsers/typescript-parser.js.map +1 -0
- package/dist/rules/evaluator.d.ts +305 -0
- package/dist/rules/evaluator.d.ts.map +1 -0
- package/dist/rules/evaluator.js +579 -0
- package/dist/rules/evaluator.js.map +1 -0
- package/dist/rules/index.d.ts +13 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +13 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/quick-fix-generator.d.ts +334 -0
- package/dist/rules/quick-fix-generator.d.ts.map +1 -0
- package/dist/rules/quick-fix-generator.js +1075 -0
- package/dist/rules/quick-fix-generator.js.map +1 -0
- package/dist/rules/rule-engine.d.ts +241 -0
- package/dist/rules/rule-engine.d.ts.map +1 -0
- package/dist/rules/rule-engine.js +585 -0
- package/dist/rules/rule-engine.js.map +1 -0
- package/dist/rules/severity-manager.d.ts +394 -0
- package/dist/rules/severity-manager.d.ts.map +1 -0
- package/dist/rules/severity-manager.js +619 -0
- package/dist/rules/severity-manager.js.map +1 -0
- package/dist/rules/types.d.ts +370 -0
- package/dist/rules/types.d.ts.map +1 -0
- package/dist/rules/types.js +133 -0
- package/dist/rules/types.js.map +1 -0
- package/dist/rules/variant-manager.d.ts +388 -0
- package/dist/rules/variant-manager.d.ts.map +1 -0
- package/dist/rules/variant-manager.js +777 -0
- package/dist/rules/variant-manager.js.map +1 -0
- package/dist/scanner/change-detector.d.ts +164 -0
- package/dist/scanner/change-detector.d.ts.map +1 -0
- package/dist/scanner/change-detector.js +263 -0
- package/dist/scanner/change-detector.js.map +1 -0
- package/dist/scanner/dependency-graph.d.ts +270 -0
- package/dist/scanner/dependency-graph.d.ts.map +1 -0
- package/dist/scanner/dependency-graph.js +436 -0
- package/dist/scanner/dependency-graph.js.map +1 -0
- package/dist/scanner/file-walker.d.ts +127 -0
- package/dist/scanner/file-walker.d.ts.map +1 -0
- package/dist/scanner/file-walker.js +526 -0
- package/dist/scanner/file-walker.js.map +1 -0
- package/dist/scanner/index.d.ts +12 -0
- package/dist/scanner/index.d.ts.map +1 -0
- package/dist/scanner/index.js +12 -0
- package/dist/scanner/index.js.map +1 -0
- package/dist/scanner/types.d.ts +218 -0
- package/dist/scanner/types.d.ts.map +1 -0
- package/dist/scanner/types.js +10 -0
- package/dist/scanner/types.js.map +1 -0
- package/dist/scanner/worker-pool.d.ts +317 -0
- package/dist/scanner/worker-pool.d.ts.map +1 -0
- package/dist/scanner/worker-pool.js +571 -0
- package/dist/scanner/worker-pool.js.map +1 -0
- package/dist/store/cache-manager.d.ts +179 -0
- package/dist/store/cache-manager.d.ts.map +1 -0
- package/dist/store/cache-manager.js +391 -0
- package/dist/store/cache-manager.js.map +1 -0
- package/dist/store/history-store.d.ts +314 -0
- package/dist/store/history-store.d.ts.map +1 -0
- package/dist/store/history-store.js +707 -0
- package/dist/store/history-store.js.map +1 -0
- package/dist/store/index.d.ts +20 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +26 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/lock-file-manager.d.ts +202 -0
- package/dist/store/lock-file-manager.d.ts.map +1 -0
- package/dist/store/lock-file-manager.js +475 -0
- package/dist/store/lock-file-manager.js.map +1 -0
- package/dist/store/pattern-store.d.ts +289 -0
- package/dist/store/pattern-store.d.ts.map +1 -0
- package/dist/store/pattern-store.js +936 -0
- package/dist/store/pattern-store.js.map +1 -0
- package/dist/store/schema-validator.d.ts +159 -0
- package/dist/store/schema-validator.d.ts.map +1 -0
- package/dist/store/schema-validator.js +1096 -0
- package/dist/store/schema-validator.js.map +1 -0
- package/dist/store/types.d.ts +585 -0
- package/dist/store/types.d.ts.map +1 -0
- package/dist/store/types.js +82 -0
- package/dist/store/types.js.map +1 -0
- package/dist/types/analysis.d.ts +19 -0
- package/dist/types/analysis.d.ts.map +1 -0
- package/dist/types/analysis.js +5 -0
- package/dist/types/analysis.js.map +1 -0
- package/dist/types/common.d.ts +7 -0
- package/dist/types/common.d.ts.map +1 -0
- package/dist/types/common.js +5 -0
- package/dist/types/common.js.map +1 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +10 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/patterns.d.ts +40 -0
- package/dist/types/patterns.d.ts.map +1 -0
- package/dist/types/patterns.js +7 -0
- package/dist/types/patterns.js.map +1 -0
- package/dist/types/violations.d.ts +7 -0
- package/dist/types/violations.d.ts.map +1 -0
- package/dist/types/violations.js +7 -0
- package/dist/types/violations.js.map +1 -0
- package/package.json +46 -0
|
@@ -0,0 +1,1075 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quick Fix Generator - Code transformation generation for violations
|
|
3
|
+
*
|
|
4
|
+
* Generates code transformations (quick fixes) for pattern violations.
|
|
5
|
+
* Supports multiple fix types: replace, wrap, extract, import, rename, move, delete.
|
|
6
|
+
* Provides preview of changes before applying.
|
|
7
|
+
*
|
|
8
|
+
* @requirements 25.1 - THE Quick_Fix_System SHALL generate code transformations for fixable violations
|
|
9
|
+
* @requirements 25.2 - THE Quick_Fix SHALL include a preview of the change before applying
|
|
10
|
+
* @requirements 25.3 - THE Quick_Fix SHALL support fix types: replace, wrap, extract, import, rename, move, delete
|
|
11
|
+
* @requirements 25.4 - WHEN multiple fixes are available, THE Quick_Fix_System SHALL rank by confidence
|
|
12
|
+
* @requirements 25.5 - THE Quick_Fix_System SHALL mark the preferred fix for one-click application
|
|
13
|
+
*/
|
|
14
|
+
import { createTextEdit, createInsertEdit, createDeleteEdit, createWorkspaceEdit, createPosition, } from './types.js';
|
|
15
|
+
/**
|
|
16
|
+
* Default QuickFixGenerator configuration
|
|
17
|
+
*/
|
|
18
|
+
export const DEFAULT_QUICK_FIX_GENERATOR_CONFIG = {
|
|
19
|
+
minConfidence: 0.5,
|
|
20
|
+
maxFixesPerViolation: 5,
|
|
21
|
+
generatePreviews: true,
|
|
22
|
+
validateFixes: true,
|
|
23
|
+
};
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Replace Fix Strategy
|
|
26
|
+
// ============================================================================
|
|
27
|
+
/**
|
|
28
|
+
* Strategy for generating replace fixes
|
|
29
|
+
* Replaces text at a specific range with new text
|
|
30
|
+
*/
|
|
31
|
+
export class ReplaceFixStrategy {
|
|
32
|
+
type = 'replace';
|
|
33
|
+
canHandle(violation) {
|
|
34
|
+
// Replace can handle most violations where we know what to replace with
|
|
35
|
+
return violation.expected !== undefined && violation.expected.length > 0;
|
|
36
|
+
}
|
|
37
|
+
getConfidence(context) {
|
|
38
|
+
// Higher confidence if expected and actual are clearly different
|
|
39
|
+
if (context.expected === context.actual) {
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
// Base confidence for replace operations
|
|
43
|
+
return 0.8;
|
|
44
|
+
}
|
|
45
|
+
generate(context) {
|
|
46
|
+
const { violation, expected } = context;
|
|
47
|
+
if (!expected || expected.length === 0) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const edit = createTextEdit(violation.range, expected);
|
|
51
|
+
const workspaceEdit = createWorkspaceEdit(violation.file, [edit]);
|
|
52
|
+
return {
|
|
53
|
+
title: `Replace with: ${this.truncateText(expected, 50)}`,
|
|
54
|
+
kind: 'quickfix',
|
|
55
|
+
edit: workspaceEdit,
|
|
56
|
+
isPreferred: true,
|
|
57
|
+
confidence: this.getConfidence(context),
|
|
58
|
+
preview: this.generatePreview(context, expected),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
truncateText(text, maxLength) {
|
|
62
|
+
if (text.length <= maxLength) {
|
|
63
|
+
return text;
|
|
64
|
+
}
|
|
65
|
+
return text.substring(0, maxLength - 3) + '...';
|
|
66
|
+
}
|
|
67
|
+
generatePreview(context, newText) {
|
|
68
|
+
return `Replace "${this.truncateText(context.actual, 30)}" with "${this.truncateText(newText, 30)}"`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// Wrap Fix Strategy
|
|
73
|
+
// ============================================================================
|
|
74
|
+
/**
|
|
75
|
+
* Strategy for generating wrap fixes
|
|
76
|
+
* Wraps code with additional structure (e.g., try/catch, function wrapper)
|
|
77
|
+
*/
|
|
78
|
+
export class WrapFixStrategy {
|
|
79
|
+
type = 'wrap';
|
|
80
|
+
canHandle(violation) {
|
|
81
|
+
// Wrap can handle violations that suggest wrapping code
|
|
82
|
+
const wrapKeywords = ['wrap', 'surround', 'enclose', 'try', 'catch', 'async'];
|
|
83
|
+
const message = violation.message.toLowerCase();
|
|
84
|
+
return wrapKeywords.some(keyword => message.includes(keyword));
|
|
85
|
+
}
|
|
86
|
+
getConfidence(_context) {
|
|
87
|
+
// Wrap operations have moderate confidence
|
|
88
|
+
return 0.7;
|
|
89
|
+
}
|
|
90
|
+
generate(context) {
|
|
91
|
+
const { violation, content } = context;
|
|
92
|
+
// Extract the code to wrap
|
|
93
|
+
const codeToWrap = this.extractCode(content, violation.range);
|
|
94
|
+
if (!codeToWrap) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
// Determine wrap type based on violation message
|
|
98
|
+
const wrapType = this.determineWrapType(violation);
|
|
99
|
+
const wrappedCode = this.wrapCode(codeToWrap, wrapType);
|
|
100
|
+
const edit = createTextEdit(violation.range, wrappedCode);
|
|
101
|
+
const workspaceEdit = createWorkspaceEdit(violation.file, [edit]);
|
|
102
|
+
return {
|
|
103
|
+
title: `Wrap with ${wrapType}`,
|
|
104
|
+
kind: 'refactor',
|
|
105
|
+
edit: workspaceEdit,
|
|
106
|
+
isPreferred: false,
|
|
107
|
+
confidence: this.getConfidence(context),
|
|
108
|
+
preview: `Wrap code with ${wrapType} block`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
extractCode(content, range) {
|
|
112
|
+
const lines = content.split('\n');
|
|
113
|
+
const startLine = range.start.line;
|
|
114
|
+
const endLine = range.end.line;
|
|
115
|
+
if (startLine < 0 || endLine >= lines.length) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
if (startLine === endLine) {
|
|
119
|
+
const line = lines[startLine];
|
|
120
|
+
if (line === undefined)
|
|
121
|
+
return null;
|
|
122
|
+
return line.substring(range.start.character, range.end.character);
|
|
123
|
+
}
|
|
124
|
+
const extractedLines = [];
|
|
125
|
+
for (let i = startLine; i <= endLine; i++) {
|
|
126
|
+
const line = lines[i];
|
|
127
|
+
if (line === undefined)
|
|
128
|
+
continue;
|
|
129
|
+
if (i === startLine) {
|
|
130
|
+
extractedLines.push(line.substring(range.start.character));
|
|
131
|
+
}
|
|
132
|
+
else if (i === endLine) {
|
|
133
|
+
extractedLines.push(line.substring(0, range.end.character));
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
extractedLines.push(line);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return extractedLines.join('\n');
|
|
140
|
+
}
|
|
141
|
+
determineWrapType(violation) {
|
|
142
|
+
const message = violation.message.toLowerCase();
|
|
143
|
+
if (message.includes('try') || message.includes('catch') || message.includes('error')) {
|
|
144
|
+
return 'try-catch';
|
|
145
|
+
}
|
|
146
|
+
if (message.includes('async') || message.includes('await')) {
|
|
147
|
+
return 'async';
|
|
148
|
+
}
|
|
149
|
+
if (message.includes('function')) {
|
|
150
|
+
return 'function';
|
|
151
|
+
}
|
|
152
|
+
return 'block';
|
|
153
|
+
}
|
|
154
|
+
wrapCode(code, wrapType) {
|
|
155
|
+
const indent = this.detectIndent(code);
|
|
156
|
+
switch (wrapType) {
|
|
157
|
+
case 'try-catch':
|
|
158
|
+
return `try {\n${indent} ${code}\n${indent}} catch (error) {\n${indent} throw error;\n${indent}}`;
|
|
159
|
+
case 'async':
|
|
160
|
+
return `(async () => {\n${indent} ${code}\n${indent}})()`;
|
|
161
|
+
case 'function':
|
|
162
|
+
return `function wrapper() {\n${indent} ${code}\n${indent}}`;
|
|
163
|
+
default:
|
|
164
|
+
return `{\n${indent} ${code}\n${indent}}`;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
detectIndent(code) {
|
|
168
|
+
const match = code.match(/^(\s*)/);
|
|
169
|
+
return match && match[1] ? match[1] : '';
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// ============================================================================
|
|
173
|
+
// Extract Fix Strategy
|
|
174
|
+
// ============================================================================
|
|
175
|
+
/**
|
|
176
|
+
* Strategy for generating extract fixes
|
|
177
|
+
* Extracts code into a new location (e.g., extract function, extract variable)
|
|
178
|
+
*/
|
|
179
|
+
export class ExtractFixStrategy {
|
|
180
|
+
type = 'extract';
|
|
181
|
+
canHandle(violation) {
|
|
182
|
+
// Extract can handle violations suggesting code extraction
|
|
183
|
+
const extractKeywords = ['extract', 'refactor', 'duplicate', 'repeated', 'abstract'];
|
|
184
|
+
const message = violation.message.toLowerCase();
|
|
185
|
+
return extractKeywords.some(keyword => message.includes(keyword));
|
|
186
|
+
}
|
|
187
|
+
getConfidence(_context) {
|
|
188
|
+
// Extract operations have moderate confidence
|
|
189
|
+
return 0.65;
|
|
190
|
+
}
|
|
191
|
+
generate(context) {
|
|
192
|
+
const { violation, content } = context;
|
|
193
|
+
// Extract the code to extract
|
|
194
|
+
const codeToExtract = this.extractCode(content, violation.range);
|
|
195
|
+
if (!codeToExtract) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
// Generate extracted function/variable
|
|
199
|
+
const extractedName = this.generateExtractedName(violation);
|
|
200
|
+
const { extractedCode, replacement } = this.createExtraction(codeToExtract, extractedName);
|
|
201
|
+
// Create edits: replace original with reference, add extracted code
|
|
202
|
+
const replaceEdit = createTextEdit(violation.range, replacement);
|
|
203
|
+
// Insert extracted code at the beginning of the file (simplified)
|
|
204
|
+
const insertPosition = createPosition(0, 0);
|
|
205
|
+
const insertEdit = createInsertEdit(insertPosition, extractedCode + '\n\n');
|
|
206
|
+
const workspaceEdit = {
|
|
207
|
+
changes: {
|
|
208
|
+
[violation.file]: [insertEdit, replaceEdit],
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
return {
|
|
212
|
+
title: `Extract to ${extractedName}`,
|
|
213
|
+
kind: 'refactor',
|
|
214
|
+
edit: workspaceEdit,
|
|
215
|
+
isPreferred: false,
|
|
216
|
+
confidence: this.getConfidence(context),
|
|
217
|
+
preview: `Extract code to function "${extractedName}"`,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
extractCode(content, range) {
|
|
221
|
+
const lines = content.split('\n');
|
|
222
|
+
const startLine = range.start.line;
|
|
223
|
+
const endLine = range.end.line;
|
|
224
|
+
if (startLine < 0 || endLine >= lines.length) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
if (startLine === endLine) {
|
|
228
|
+
const line = lines[startLine];
|
|
229
|
+
if (line === undefined)
|
|
230
|
+
return null;
|
|
231
|
+
return line.substring(range.start.character, range.end.character);
|
|
232
|
+
}
|
|
233
|
+
const extractedLines = [];
|
|
234
|
+
for (let i = startLine; i <= endLine; i++) {
|
|
235
|
+
const line = lines[i];
|
|
236
|
+
if (line === undefined)
|
|
237
|
+
continue;
|
|
238
|
+
if (i === startLine) {
|
|
239
|
+
extractedLines.push(line.substring(range.start.character));
|
|
240
|
+
}
|
|
241
|
+
else if (i === endLine) {
|
|
242
|
+
extractedLines.push(line.substring(0, range.end.character));
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
extractedLines.push(line);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return extractedLines.join('\n');
|
|
249
|
+
}
|
|
250
|
+
generateExtractedName(violation) {
|
|
251
|
+
// Generate a name based on the pattern or violation
|
|
252
|
+
const patternId = violation.patternId;
|
|
253
|
+
const sanitized = patternId.replace(/[^a-zA-Z0-9]/g, '_');
|
|
254
|
+
return `extracted_${sanitized}`;
|
|
255
|
+
}
|
|
256
|
+
createExtraction(code, name) {
|
|
257
|
+
// Create a function extraction
|
|
258
|
+
const extractedCode = `function ${name}() {\n ${code}\n}`;
|
|
259
|
+
const replacement = `${name}()`;
|
|
260
|
+
return { extractedCode, replacement };
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// ============================================================================
|
|
264
|
+
// Import Fix Strategy
|
|
265
|
+
// ============================================================================
|
|
266
|
+
/**
|
|
267
|
+
* Strategy for generating import fixes
|
|
268
|
+
* Adds import statements for missing dependencies
|
|
269
|
+
*/
|
|
270
|
+
export class ImportFixStrategy {
|
|
271
|
+
type = 'import';
|
|
272
|
+
canHandle(violation) {
|
|
273
|
+
// Import can handle violations about missing imports
|
|
274
|
+
const importKeywords = ['import', 'require', 'missing', 'undefined', 'not found', 'module'];
|
|
275
|
+
const message = violation.message.toLowerCase();
|
|
276
|
+
return importKeywords.some(keyword => message.includes(keyword));
|
|
277
|
+
}
|
|
278
|
+
getConfidence(_context) {
|
|
279
|
+
// Import operations have high confidence when we know what to import
|
|
280
|
+
return 0.85;
|
|
281
|
+
}
|
|
282
|
+
generate(context) {
|
|
283
|
+
const { violation, expected } = context;
|
|
284
|
+
// Try to extract import information from expected
|
|
285
|
+
const importStatement = this.generateImportStatement(expected, violation);
|
|
286
|
+
if (!importStatement) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
// Insert import at the top of the file
|
|
290
|
+
const insertPosition = createPosition(0, 0);
|
|
291
|
+
const insertEdit = createInsertEdit(insertPosition, importStatement + '\n');
|
|
292
|
+
const workspaceEdit = createWorkspaceEdit(violation.file, [insertEdit]);
|
|
293
|
+
return {
|
|
294
|
+
title: `Add import: ${this.truncateText(importStatement, 50)}`,
|
|
295
|
+
kind: 'quickfix',
|
|
296
|
+
edit: workspaceEdit,
|
|
297
|
+
isPreferred: true,
|
|
298
|
+
confidence: this.getConfidence(context),
|
|
299
|
+
preview: `Add import statement at top of file`,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
generateImportStatement(expected, violation) {
|
|
303
|
+
// Try to parse expected as an import statement
|
|
304
|
+
if (expected.startsWith('import ')) {
|
|
305
|
+
return expected;
|
|
306
|
+
}
|
|
307
|
+
// Try to extract module name from violation message
|
|
308
|
+
const moduleMatch = violation.message.match(/['"]([^'"]+)['"]/);
|
|
309
|
+
if (moduleMatch && moduleMatch[1]) {
|
|
310
|
+
return `import { } from '${moduleMatch[1]}';`;
|
|
311
|
+
}
|
|
312
|
+
// Try to extract from expected
|
|
313
|
+
if (expected.includes('/') || expected.includes('@')) {
|
|
314
|
+
return `import { } from '${expected}';`;
|
|
315
|
+
}
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
truncateText(text, maxLength) {
|
|
319
|
+
if (text.length <= maxLength) {
|
|
320
|
+
return text;
|
|
321
|
+
}
|
|
322
|
+
return text.substring(0, maxLength - 3) + '...';
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// ============================================================================
|
|
326
|
+
// Rename Fix Strategy
|
|
327
|
+
// ============================================================================
|
|
328
|
+
/**
|
|
329
|
+
* Strategy for generating rename fixes
|
|
330
|
+
* Renames symbols to match naming conventions
|
|
331
|
+
*/
|
|
332
|
+
export class RenameFixStrategy {
|
|
333
|
+
type = 'rename';
|
|
334
|
+
canHandle(violation) {
|
|
335
|
+
// Rename can handle violations about naming conventions
|
|
336
|
+
const renameKeywords = ['rename', 'naming', 'convention', 'case', 'camel', 'pascal', 'snake', 'kebab'];
|
|
337
|
+
const message = violation.message.toLowerCase();
|
|
338
|
+
return renameKeywords.some(keyword => message.includes(keyword));
|
|
339
|
+
}
|
|
340
|
+
getConfidence(_context) {
|
|
341
|
+
// Rename operations have high confidence for naming violations
|
|
342
|
+
return 0.9;
|
|
343
|
+
}
|
|
344
|
+
generate(context) {
|
|
345
|
+
const { violation, expected, actual } = context;
|
|
346
|
+
if (!expected || expected === actual) {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
// Generate the new name based on expected pattern
|
|
350
|
+
const newName = this.generateNewName(actual, expected, violation);
|
|
351
|
+
if (!newName || newName === actual) {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
const edit = createTextEdit(violation.range, newName);
|
|
355
|
+
const workspaceEdit = createWorkspaceEdit(violation.file, [edit]);
|
|
356
|
+
return {
|
|
357
|
+
title: `Rename to "${newName}"`,
|
|
358
|
+
kind: 'quickfix',
|
|
359
|
+
edit: workspaceEdit,
|
|
360
|
+
isPreferred: true,
|
|
361
|
+
confidence: this.getConfidence(context),
|
|
362
|
+
preview: `Rename "${actual}" to "${newName}"`,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
generateNewName(actual, expected, violation) {
|
|
366
|
+
const message = violation.message.toLowerCase();
|
|
367
|
+
// If expected is a specific name, use it
|
|
368
|
+
if (!expected.includes('case') && !expected.includes('convention')) {
|
|
369
|
+
return expected;
|
|
370
|
+
}
|
|
371
|
+
// Convert based on naming convention
|
|
372
|
+
if (message.includes('camelcase') || message.includes('camel case')) {
|
|
373
|
+
return this.toCamelCase(actual);
|
|
374
|
+
}
|
|
375
|
+
if (message.includes('pascalcase') || message.includes('pascal case')) {
|
|
376
|
+
return this.toPascalCase(actual);
|
|
377
|
+
}
|
|
378
|
+
if (message.includes('snake_case') || message.includes('snake case')) {
|
|
379
|
+
return this.toSnakeCase(actual);
|
|
380
|
+
}
|
|
381
|
+
if (message.includes('kebab-case') || message.includes('kebab case')) {
|
|
382
|
+
return this.toKebabCase(actual);
|
|
383
|
+
}
|
|
384
|
+
return expected;
|
|
385
|
+
}
|
|
386
|
+
toCamelCase(str) {
|
|
387
|
+
return str
|
|
388
|
+
.replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ''))
|
|
389
|
+
.replace(/^[A-Z]/, c => c.toLowerCase());
|
|
390
|
+
}
|
|
391
|
+
toPascalCase(str) {
|
|
392
|
+
return str
|
|
393
|
+
.replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ''))
|
|
394
|
+
.replace(/^[a-z]/, c => c.toUpperCase());
|
|
395
|
+
}
|
|
396
|
+
toSnakeCase(str) {
|
|
397
|
+
return str
|
|
398
|
+
.replace(/([A-Z])/g, '_$1')
|
|
399
|
+
.replace(/[-\s]+/g, '_')
|
|
400
|
+
.toLowerCase()
|
|
401
|
+
.replace(/^_/, '');
|
|
402
|
+
}
|
|
403
|
+
toKebabCase(str) {
|
|
404
|
+
return str
|
|
405
|
+
.replace(/([A-Z])/g, '-$1')
|
|
406
|
+
.replace(/[_\s]+/g, '-')
|
|
407
|
+
.toLowerCase()
|
|
408
|
+
.replace(/^-/, '');
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// ============================================================================
|
|
412
|
+
// Move Fix Strategy
|
|
413
|
+
// ============================================================================
|
|
414
|
+
/**
|
|
415
|
+
* Strategy for generating move fixes
|
|
416
|
+
* Moves code to a different location (e.g., different file, different position)
|
|
417
|
+
*/
|
|
418
|
+
export class MoveFixStrategy {
|
|
419
|
+
type = 'move';
|
|
420
|
+
canHandle(violation) {
|
|
421
|
+
// Move can handle violations about code location
|
|
422
|
+
const moveKeywords = ['move', 'relocate', 'location', 'position', 'place', 'organize'];
|
|
423
|
+
const message = violation.message.toLowerCase();
|
|
424
|
+
return moveKeywords.some(keyword => message.includes(keyword));
|
|
425
|
+
}
|
|
426
|
+
getConfidence(_context) {
|
|
427
|
+
// Move operations have lower confidence due to complexity
|
|
428
|
+
return 0.6;
|
|
429
|
+
}
|
|
430
|
+
generate(context) {
|
|
431
|
+
const { violation, content, expected } = context;
|
|
432
|
+
// Extract the code to move
|
|
433
|
+
const codeToMove = this.extractCode(content, violation.range);
|
|
434
|
+
if (!codeToMove) {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
// Determine target location
|
|
438
|
+
const targetLocation = this.determineTargetLocation(violation, expected);
|
|
439
|
+
if (!targetLocation) {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
// Create edits: delete from original, insert at target
|
|
443
|
+
const deleteEdit = createDeleteEdit(violation.range);
|
|
444
|
+
const insertEdit = createInsertEdit(targetLocation, codeToMove);
|
|
445
|
+
const workspaceEdit = {
|
|
446
|
+
changes: {
|
|
447
|
+
[violation.file]: [deleteEdit, insertEdit],
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
return {
|
|
451
|
+
title: `Move code to line ${targetLocation.line + 1}`,
|
|
452
|
+
kind: 'refactor',
|
|
453
|
+
edit: workspaceEdit,
|
|
454
|
+
isPreferred: false,
|
|
455
|
+
confidence: this.getConfidence(context),
|
|
456
|
+
preview: `Move code from line ${violation.range.start.line + 1} to line ${targetLocation.line + 1}`,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
extractCode(content, range) {
|
|
460
|
+
const lines = content.split('\n');
|
|
461
|
+
const startLine = range.start.line;
|
|
462
|
+
const endLine = range.end.line;
|
|
463
|
+
if (startLine < 0 || endLine >= lines.length) {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
if (startLine === endLine) {
|
|
467
|
+
const line = lines[startLine];
|
|
468
|
+
if (line === undefined)
|
|
469
|
+
return null;
|
|
470
|
+
return line.substring(range.start.character, range.end.character);
|
|
471
|
+
}
|
|
472
|
+
const extractedLines = [];
|
|
473
|
+
for (let i = startLine; i <= endLine; i++) {
|
|
474
|
+
const line = lines[i];
|
|
475
|
+
if (line === undefined)
|
|
476
|
+
continue;
|
|
477
|
+
if (i === startLine) {
|
|
478
|
+
extractedLines.push(line.substring(range.start.character));
|
|
479
|
+
}
|
|
480
|
+
else if (i === endLine) {
|
|
481
|
+
extractedLines.push(line.substring(0, range.end.character));
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
extractedLines.push(line);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return extractedLines.join('\n');
|
|
488
|
+
}
|
|
489
|
+
determineTargetLocation(_violation, expected) {
|
|
490
|
+
// Try to parse target line from expected
|
|
491
|
+
const lineMatch = expected.match(/line\s*(\d+)/i);
|
|
492
|
+
if (lineMatch && lineMatch[1]) {
|
|
493
|
+
const targetLine = parseInt(lineMatch[1], 10) - 1; // Convert to 0-indexed
|
|
494
|
+
return createPosition(targetLine, 0);
|
|
495
|
+
}
|
|
496
|
+
// Default: move to beginning of file
|
|
497
|
+
return createPosition(0, 0);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
// ============================================================================
|
|
501
|
+
// Delete Fix Strategy
|
|
502
|
+
// ============================================================================
|
|
503
|
+
/**
|
|
504
|
+
* Strategy for generating delete fixes
|
|
505
|
+
* Deletes code that should be removed
|
|
506
|
+
*/
|
|
507
|
+
export class DeleteFixStrategy {
|
|
508
|
+
type = 'delete';
|
|
509
|
+
canHandle(violation) {
|
|
510
|
+
// Delete can handle violations about unnecessary code
|
|
511
|
+
const deleteKeywords = ['delete', 'remove', 'unused', 'unnecessary', 'redundant', 'dead code'];
|
|
512
|
+
const message = violation.message.toLowerCase();
|
|
513
|
+
return deleteKeywords.some(keyword => message.includes(keyword));
|
|
514
|
+
}
|
|
515
|
+
getConfidence(_context) {
|
|
516
|
+
// Delete operations have moderate confidence
|
|
517
|
+
return 0.75;
|
|
518
|
+
}
|
|
519
|
+
generate(context) {
|
|
520
|
+
const { violation } = context;
|
|
521
|
+
const deleteEdit = createDeleteEdit(violation.range);
|
|
522
|
+
const workspaceEdit = createWorkspaceEdit(violation.file, [deleteEdit]);
|
|
523
|
+
return {
|
|
524
|
+
title: 'Delete code',
|
|
525
|
+
kind: 'quickfix',
|
|
526
|
+
edit: workspaceEdit,
|
|
527
|
+
isPreferred: false,
|
|
528
|
+
confidence: this.getConfidence(context),
|
|
529
|
+
preview: `Delete code at lines ${violation.range.start.line + 1}-${violation.range.end.line + 1}`,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
// ============================================================================
|
|
534
|
+
// Quick Fix Generator Class
|
|
535
|
+
// ============================================================================
|
|
536
|
+
/**
|
|
537
|
+
* QuickFixGenerator class for generating code transformations for violations.
|
|
538
|
+
*
|
|
539
|
+
* The generator:
|
|
540
|
+
* - Takes violations and generates appropriate quick fixes
|
|
541
|
+
* - Supports multiple fix types: replace, wrap, extract, import, rename, move, delete
|
|
542
|
+
* - Ranks fixes by confidence and marks preferred fix
|
|
543
|
+
* - Provides preview of changes before applying
|
|
544
|
+
*
|
|
545
|
+
* @requirements 25.1 - Generate code transformations for fixable violations
|
|
546
|
+
* @requirements 25.3 - Support fix types: replace, wrap, extract, import, rename, move, delete
|
|
547
|
+
* @requirements 25.4 - Rank fixes by confidence
|
|
548
|
+
* @requirements 25.5 - Mark preferred fix for one-click application
|
|
549
|
+
*/
|
|
550
|
+
export class QuickFixGenerator {
|
|
551
|
+
config;
|
|
552
|
+
strategies;
|
|
553
|
+
fixIdCounter;
|
|
554
|
+
/**
|
|
555
|
+
* Create a new QuickFixGenerator instance.
|
|
556
|
+
*
|
|
557
|
+
* @param config - Optional configuration options
|
|
558
|
+
*/
|
|
559
|
+
constructor(config) {
|
|
560
|
+
this.config = {
|
|
561
|
+
...DEFAULT_QUICK_FIX_GENERATOR_CONFIG,
|
|
562
|
+
...config,
|
|
563
|
+
};
|
|
564
|
+
// Initialize all fix strategies
|
|
565
|
+
this.strategies = [
|
|
566
|
+
new ReplaceFixStrategy(),
|
|
567
|
+
new WrapFixStrategy(),
|
|
568
|
+
new ExtractFixStrategy(),
|
|
569
|
+
new ImportFixStrategy(),
|
|
570
|
+
new RenameFixStrategy(),
|
|
571
|
+
new MoveFixStrategy(),
|
|
572
|
+
new DeleteFixStrategy(),
|
|
573
|
+
];
|
|
574
|
+
this.fixIdCounter = 0;
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Generate quick fixes for a violation.
|
|
578
|
+
*
|
|
579
|
+
* @param violation - The violation to generate fixes for
|
|
580
|
+
* @param content - The file content
|
|
581
|
+
* @returns Fix generation result with ranked fixes
|
|
582
|
+
*
|
|
583
|
+
* @requirements 25.1 - Generate code transformations
|
|
584
|
+
* @requirements 25.4 - Rank by confidence
|
|
585
|
+
*/
|
|
586
|
+
generateFixes(violation, content) {
|
|
587
|
+
const errors = [];
|
|
588
|
+
const fixes = [];
|
|
589
|
+
const context = {
|
|
590
|
+
violation,
|
|
591
|
+
content,
|
|
592
|
+
expected: violation.expected,
|
|
593
|
+
actual: violation.actual,
|
|
594
|
+
};
|
|
595
|
+
// Try each strategy
|
|
596
|
+
for (const strategy of this.strategies) {
|
|
597
|
+
try {
|
|
598
|
+
if (strategy.canHandle(violation)) {
|
|
599
|
+
const fix = strategy.generate(context);
|
|
600
|
+
if (fix && fix.confidence >= this.config.minConfidence) {
|
|
601
|
+
const fixWithMetadata = this.addMetadata(fix, violation, strategy.type);
|
|
602
|
+
fixes.push(fixWithMetadata);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
catch (error) {
|
|
607
|
+
errors.push(`Strategy ${strategy.type} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
// Sort by confidence (highest first)
|
|
611
|
+
fixes.sort((a, b) => b.confidence - a.confidence);
|
|
612
|
+
// Limit number of fixes
|
|
613
|
+
const limitedFixes = fixes.slice(0, this.config.maxFixesPerViolation);
|
|
614
|
+
// Mark preferred fix
|
|
615
|
+
if (limitedFixes.length > 0) {
|
|
616
|
+
const firstFix = limitedFixes[0];
|
|
617
|
+
if (firstFix) {
|
|
618
|
+
firstFix.isPreferred = true;
|
|
619
|
+
}
|
|
620
|
+
for (let i = 1; i < limitedFixes.length; i++) {
|
|
621
|
+
const fix = limitedFixes[i];
|
|
622
|
+
if (fix) {
|
|
623
|
+
fix.isPreferred = false;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
const result = {
|
|
628
|
+
violationId: violation.id,
|
|
629
|
+
fixes: limitedFixes,
|
|
630
|
+
hasFixs: limitedFixes.length > 0,
|
|
631
|
+
errors,
|
|
632
|
+
};
|
|
633
|
+
if (limitedFixes.length > 0 && limitedFixes[0]) {
|
|
634
|
+
result.preferredFix = limitedFixes[0];
|
|
635
|
+
}
|
|
636
|
+
return result;
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Generate quick fixes for multiple violations.
|
|
640
|
+
*
|
|
641
|
+
* @param violations - Array of violations
|
|
642
|
+
* @param content - The file content
|
|
643
|
+
* @returns Array of fix generation results
|
|
644
|
+
*/
|
|
645
|
+
generateFixesForAll(violations, content) {
|
|
646
|
+
return violations.map(violation => this.generateFixes(violation, content));
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Generate a specific type of fix for a violation.
|
|
650
|
+
*
|
|
651
|
+
* @param violation - The violation to fix
|
|
652
|
+
* @param content - The file content
|
|
653
|
+
* @param fixType - The type of fix to generate
|
|
654
|
+
* @returns The generated fix or null if not applicable
|
|
655
|
+
*/
|
|
656
|
+
generateFixOfType(violation, content, fixType) {
|
|
657
|
+
const strategy = this.strategies.find(s => s.type === fixType);
|
|
658
|
+
if (!strategy) {
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
const context = {
|
|
662
|
+
violation,
|
|
663
|
+
content,
|
|
664
|
+
expected: violation.expected,
|
|
665
|
+
actual: violation.actual,
|
|
666
|
+
};
|
|
667
|
+
if (!strategy.canHandle(violation)) {
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
return strategy.generate(context);
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Generate a preview of a fix before applying.
|
|
674
|
+
*
|
|
675
|
+
* @param fix - The quick fix to preview
|
|
676
|
+
* @param content - The original file content
|
|
677
|
+
* @returns Preview string showing the change
|
|
678
|
+
*
|
|
679
|
+
* @requirements 25.2 - Include preview of change before applying
|
|
680
|
+
*/
|
|
681
|
+
generatePreview(fix, content) {
|
|
682
|
+
if (fix.preview) {
|
|
683
|
+
return fix.preview;
|
|
684
|
+
}
|
|
685
|
+
// Generate a diff-like preview
|
|
686
|
+
const lines = [];
|
|
687
|
+
const fileEdits = Object.entries(fix.edit.changes);
|
|
688
|
+
for (const [file, edits] of fileEdits) {
|
|
689
|
+
lines.push(`File: ${file}`);
|
|
690
|
+
lines.push('---');
|
|
691
|
+
for (const edit of edits) {
|
|
692
|
+
const originalText = this.extractTextFromRange(content, edit.range);
|
|
693
|
+
if (edit.newText === '') {
|
|
694
|
+
lines.push(`- ${originalText}`);
|
|
695
|
+
}
|
|
696
|
+
else if (originalText === '') {
|
|
697
|
+
lines.push(`+ ${edit.newText}`);
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
lines.push(`- ${originalText}`);
|
|
701
|
+
lines.push(`+ ${edit.newText}`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return lines.join('\n');
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Apply a fix to content and return the result.
|
|
709
|
+
*
|
|
710
|
+
* This method is idempotent: applying the same fix twice will result in
|
|
711
|
+
* no additional changes after the first application.
|
|
712
|
+
*
|
|
713
|
+
* @param fix - The quick fix to apply
|
|
714
|
+
* @param content - The original file content
|
|
715
|
+
* @returns The modified content after applying the fix
|
|
716
|
+
*
|
|
717
|
+
* @requirements 25.1 - Generate code transformations for fixable violations
|
|
718
|
+
*/
|
|
719
|
+
applyFix(fix, content) {
|
|
720
|
+
// Get edits for the file (assuming single file for now)
|
|
721
|
+
const fileEdits = Object.values(fix.edit.changes)[0];
|
|
722
|
+
if (!fileEdits || fileEdits.length === 0) {
|
|
723
|
+
return content;
|
|
724
|
+
}
|
|
725
|
+
// Sort edits by position (reverse order to apply from end to start)
|
|
726
|
+
const sortedEdits = [...fileEdits].sort((a, b) => {
|
|
727
|
+
if (a.range.start.line !== b.range.start.line) {
|
|
728
|
+
return b.range.start.line - a.range.start.line;
|
|
729
|
+
}
|
|
730
|
+
return b.range.start.character - a.range.start.character;
|
|
731
|
+
});
|
|
732
|
+
// Apply all edits
|
|
733
|
+
let result = content;
|
|
734
|
+
for (const edit of sortedEdits) {
|
|
735
|
+
if (edit.newText === '') {
|
|
736
|
+
// Delete operation: check if the text at the range is empty
|
|
737
|
+
// If it is, the delete has already been applied
|
|
738
|
+
const textAtRange = this.extractTextFromRange(result, edit.range);
|
|
739
|
+
if (textAtRange === '') {
|
|
740
|
+
// Range is already empty, skip this edit
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
// Replace/insert operation: check if the text at the start position
|
|
746
|
+
// already matches the newText. If so, skip this edit.
|
|
747
|
+
const textAtStart = this.extractTextFromPosition(result, edit.range.start, edit.newText.length);
|
|
748
|
+
if (textAtStart === edit.newText) {
|
|
749
|
+
// Text already matches, skip this edit
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
result = this.applyTextEdit(result, edit);
|
|
754
|
+
}
|
|
755
|
+
return result;
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Extract text from a position for a given length.
|
|
759
|
+
* Used for idempotence checking.
|
|
760
|
+
*/
|
|
761
|
+
extractTextFromPosition(content, start, length) {
|
|
762
|
+
const lines = content.split('\n');
|
|
763
|
+
if (start.line < 0 || start.line >= lines.length) {
|
|
764
|
+
return '';
|
|
765
|
+
}
|
|
766
|
+
let result = '';
|
|
767
|
+
let currentLine = start.line;
|
|
768
|
+
let currentChar = start.character;
|
|
769
|
+
let remaining = length;
|
|
770
|
+
while (remaining > 0 && currentLine < lines.length) {
|
|
771
|
+
const line = lines[currentLine];
|
|
772
|
+
if (line === undefined)
|
|
773
|
+
break;
|
|
774
|
+
const availableChars = line.length - currentChar;
|
|
775
|
+
if (availableChars <= 0) {
|
|
776
|
+
// Move to next line, add newline character
|
|
777
|
+
if (remaining > 0 && currentLine < lines.length - 1) {
|
|
778
|
+
result += '\n';
|
|
779
|
+
remaining--;
|
|
780
|
+
currentLine++;
|
|
781
|
+
currentChar = 0;
|
|
782
|
+
}
|
|
783
|
+
else {
|
|
784
|
+
break;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
else {
|
|
788
|
+
const charsToTake = Math.min(availableChars, remaining);
|
|
789
|
+
result += line.substring(currentChar, currentChar + charsToTake);
|
|
790
|
+
remaining -= charsToTake;
|
|
791
|
+
currentChar += charsToTake;
|
|
792
|
+
// If we've consumed the line and need more, add newline and move to next
|
|
793
|
+
if (remaining > 0 && currentChar >= line.length && currentLine < lines.length - 1) {
|
|
794
|
+
result += '\n';
|
|
795
|
+
remaining--;
|
|
796
|
+
currentLine++;
|
|
797
|
+
currentChar = 0;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
return result;
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Check if a fix is idempotent (applying twice has no additional effect).
|
|
805
|
+
*
|
|
806
|
+
* A fix is idempotent if after applying it once, the content at the
|
|
807
|
+
* fix's range already matches the newText, so applying again has no effect.
|
|
808
|
+
*
|
|
809
|
+
* @param fix - The quick fix to check
|
|
810
|
+
* @param content - The original file content
|
|
811
|
+
* @returns True if the fix is idempotent
|
|
812
|
+
*/
|
|
813
|
+
isIdempotent(fix, content) {
|
|
814
|
+
// Apply the fix once
|
|
815
|
+
const afterFirst = this.applyFix(fix, content);
|
|
816
|
+
// Check if the content at each edit range now matches the newText
|
|
817
|
+
// If so, applying again would have no effect
|
|
818
|
+
for (const [_file, edits] of Object.entries(fix.edit.changes)) {
|
|
819
|
+
for (const edit of edits) {
|
|
820
|
+
const currentText = this.extractTextFromRange(afterFirst, edit.range);
|
|
821
|
+
// If the range is now out of bounds or the text doesn't match newText,
|
|
822
|
+
// the fix might not be idempotent in the traditional sense
|
|
823
|
+
// But for our purposes, we check if applying twice gives same result
|
|
824
|
+
if (currentText !== edit.newText) {
|
|
825
|
+
// The range content changed, check if applying again changes anything
|
|
826
|
+
const afterSecond = this.applyFix(fix, afterFirst);
|
|
827
|
+
return afterFirst === afterSecond;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return true;
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Validate a fix before applying.
|
|
835
|
+
*
|
|
836
|
+
* @param fix - The quick fix to validate
|
|
837
|
+
* @param content - The file content
|
|
838
|
+
* @returns Validation result with any errors
|
|
839
|
+
*/
|
|
840
|
+
validateFix(fix, content) {
|
|
841
|
+
const errors = [];
|
|
842
|
+
// Check that all ranges are within bounds
|
|
843
|
+
const lines = content.split('\n');
|
|
844
|
+
for (const [_file, edits] of Object.entries(fix.edit.changes)) {
|
|
845
|
+
for (const edit of edits) {
|
|
846
|
+
if (edit.range.start.line < 0 || edit.range.start.line >= lines.length) {
|
|
847
|
+
errors.push(`Invalid start line: ${edit.range.start.line}`);
|
|
848
|
+
}
|
|
849
|
+
if (edit.range.end.line < 0 || edit.range.end.line >= lines.length) {
|
|
850
|
+
errors.push(`Invalid end line: ${edit.range.end.line}`);
|
|
851
|
+
}
|
|
852
|
+
if (edit.range.start.line > edit.range.end.line) {
|
|
853
|
+
errors.push('Start line is after end line');
|
|
854
|
+
}
|
|
855
|
+
if (edit.range.start.line === edit.range.end.line &&
|
|
856
|
+
edit.range.start.character > edit.range.end.character) {
|
|
857
|
+
errors.push('Start character is after end character');
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return {
|
|
862
|
+
valid: errors.length === 0,
|
|
863
|
+
errors,
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Get available fix types for a violation.
|
|
868
|
+
*
|
|
869
|
+
* @param violation - The violation to check
|
|
870
|
+
* @returns Array of fix types that can handle this violation
|
|
871
|
+
*/
|
|
872
|
+
getAvailableFixTypes(violation) {
|
|
873
|
+
return this.strategies
|
|
874
|
+
.filter(strategy => strategy.canHandle(violation))
|
|
875
|
+
.map(strategy => strategy.type);
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Register a custom fix strategy.
|
|
879
|
+
*
|
|
880
|
+
* @param strategy - The fix strategy to register
|
|
881
|
+
*/
|
|
882
|
+
registerStrategy(strategy) {
|
|
883
|
+
// Remove existing strategy of same type
|
|
884
|
+
this.strategies = this.strategies.filter(s => s.type !== strategy.type);
|
|
885
|
+
this.strategies.push(strategy);
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Get all registered fix strategies.
|
|
889
|
+
*
|
|
890
|
+
* @returns Array of registered strategies
|
|
891
|
+
*/
|
|
892
|
+
getStrategies() {
|
|
893
|
+
return [...this.strategies];
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Calculate the impact of a fix.
|
|
897
|
+
*
|
|
898
|
+
* @param fix - The quick fix to assess
|
|
899
|
+
* @param content - The file content
|
|
900
|
+
* @returns Impact assessment
|
|
901
|
+
*/
|
|
902
|
+
calculateImpact(fix, content) {
|
|
903
|
+
let filesAffected = 0;
|
|
904
|
+
let linesChanged = 0;
|
|
905
|
+
for (const [_file, edits] of Object.entries(fix.edit.changes)) {
|
|
906
|
+
filesAffected++;
|
|
907
|
+
for (const edit of edits) {
|
|
908
|
+
const originalLines = edit.range.end.line - edit.range.start.line + 1;
|
|
909
|
+
const newLines = edit.newText.split('\n').length;
|
|
910
|
+
linesChanged += Math.max(originalLines, newLines);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
// Determine risk level based on changes
|
|
914
|
+
let riskLevel = 'low';
|
|
915
|
+
if (linesChanged > 50 || filesAffected > 3) {
|
|
916
|
+
riskLevel = 'high';
|
|
917
|
+
}
|
|
918
|
+
else if (linesChanged > 10 || filesAffected > 1) {
|
|
919
|
+
riskLevel = 'medium';
|
|
920
|
+
}
|
|
921
|
+
// Check for breaking changes (simplified heuristic)
|
|
922
|
+
const breakingChange = this.mightBeBreaking(fix, content);
|
|
923
|
+
return {
|
|
924
|
+
filesAffected,
|
|
925
|
+
linesChanged,
|
|
926
|
+
riskLevel,
|
|
927
|
+
breakingChange,
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
// ============================================================================
|
|
931
|
+
// Private Methods
|
|
932
|
+
// ============================================================================
|
|
933
|
+
/**
|
|
934
|
+
* Add metadata to a quick fix.
|
|
935
|
+
*/
|
|
936
|
+
addMetadata(fix, violation, fixType) {
|
|
937
|
+
this.fixIdCounter++;
|
|
938
|
+
return {
|
|
939
|
+
...fix,
|
|
940
|
+
id: `fix-${Date.now()}-${this.fixIdCounter}`,
|
|
941
|
+
fixType,
|
|
942
|
+
violationId: violation.id,
|
|
943
|
+
patternId: violation.patternId,
|
|
944
|
+
validated: this.config.validateFixes ? this.validateFix(fix, '').valid : false,
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Extract text from a range in content.
|
|
949
|
+
*/
|
|
950
|
+
extractTextFromRange(content, range) {
|
|
951
|
+
const lines = content.split('\n');
|
|
952
|
+
const startLine = range.start.line;
|
|
953
|
+
const endLine = range.end.line;
|
|
954
|
+
if (startLine < 0 || endLine >= lines.length) {
|
|
955
|
+
return '';
|
|
956
|
+
}
|
|
957
|
+
if (startLine === endLine) {
|
|
958
|
+
const line = lines[startLine];
|
|
959
|
+
if (line === undefined)
|
|
960
|
+
return '';
|
|
961
|
+
return line.substring(range.start.character, range.end.character);
|
|
962
|
+
}
|
|
963
|
+
const extractedLines = [];
|
|
964
|
+
for (let i = startLine; i <= endLine; i++) {
|
|
965
|
+
const line = lines[i];
|
|
966
|
+
if (line === undefined)
|
|
967
|
+
continue;
|
|
968
|
+
if (i === startLine) {
|
|
969
|
+
extractedLines.push(line.substring(range.start.character));
|
|
970
|
+
}
|
|
971
|
+
else if (i === endLine) {
|
|
972
|
+
extractedLines.push(line.substring(0, range.end.character));
|
|
973
|
+
}
|
|
974
|
+
else {
|
|
975
|
+
extractedLines.push(line);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return extractedLines.join('\n');
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Apply a text edit to content.
|
|
982
|
+
*/
|
|
983
|
+
applyTextEdit(content, edit) {
|
|
984
|
+
const lines = content.split('\n');
|
|
985
|
+
const startLine = edit.range.start.line;
|
|
986
|
+
const endLine = edit.range.end.line;
|
|
987
|
+
if (startLine < 0 || startLine >= lines.length) {
|
|
988
|
+
return content;
|
|
989
|
+
}
|
|
990
|
+
// Handle single-line edit
|
|
991
|
+
if (startLine === endLine) {
|
|
992
|
+
const line = lines[startLine];
|
|
993
|
+
if (line === undefined)
|
|
994
|
+
return content;
|
|
995
|
+
const before = line.substring(0, edit.range.start.character);
|
|
996
|
+
const after = line.substring(edit.range.end.character);
|
|
997
|
+
lines[startLine] = before + edit.newText + after;
|
|
998
|
+
return lines.join('\n');
|
|
999
|
+
}
|
|
1000
|
+
// Handle multi-line edit
|
|
1001
|
+
const firstLine = lines[startLine];
|
|
1002
|
+
const lastLine = lines[endLine];
|
|
1003
|
+
if (firstLine === undefined || lastLine === undefined)
|
|
1004
|
+
return content;
|
|
1005
|
+
const before = firstLine.substring(0, edit.range.start.character);
|
|
1006
|
+
const after = lastLine.substring(edit.range.end.character);
|
|
1007
|
+
// Remove lines between start and end
|
|
1008
|
+
lines.splice(startLine, endLine - startLine + 1, before + edit.newText + after);
|
|
1009
|
+
return lines.join('\n');
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Check if a fix might be breaking.
|
|
1013
|
+
*/
|
|
1014
|
+
mightBeBreaking(fix, _content) {
|
|
1015
|
+
// Check for deletions of significant code
|
|
1016
|
+
for (const [_file, edits] of Object.entries(fix.edit.changes)) {
|
|
1017
|
+
for (const edit of edits) {
|
|
1018
|
+
// Deletion of multiple lines might be breaking
|
|
1019
|
+
if (edit.newText === '' && edit.range.end.line - edit.range.start.line > 5) {
|
|
1020
|
+
return true;
|
|
1021
|
+
}
|
|
1022
|
+
// Renaming exports might be breaking
|
|
1023
|
+
if (edit.newText.includes('export') || edit.newText.includes('module.exports')) {
|
|
1024
|
+
return true;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
return false;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
// ============================================================================
|
|
1032
|
+
// Factory Functions
|
|
1033
|
+
// ============================================================================
|
|
1034
|
+
/**
|
|
1035
|
+
* Create a QuickFixGenerator with default configuration.
|
|
1036
|
+
*
|
|
1037
|
+
* @returns New QuickFixGenerator instance
|
|
1038
|
+
*/
|
|
1039
|
+
export function createQuickFixGenerator() {
|
|
1040
|
+
return new QuickFixGenerator();
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Create a QuickFixGenerator with custom configuration.
|
|
1044
|
+
*
|
|
1045
|
+
* @param config - Configuration options
|
|
1046
|
+
* @returns New QuickFixGenerator instance
|
|
1047
|
+
*/
|
|
1048
|
+
export function createQuickFixGeneratorWithConfig(config) {
|
|
1049
|
+
return new QuickFixGenerator(config);
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Create a QuickFixGenerator with high confidence threshold.
|
|
1053
|
+
*
|
|
1054
|
+
* @returns New QuickFixGenerator instance with high confidence threshold
|
|
1055
|
+
*/
|
|
1056
|
+
export function createHighConfidenceQuickFixGenerator() {
|
|
1057
|
+
return new QuickFixGenerator({
|
|
1058
|
+
minConfidence: 0.8,
|
|
1059
|
+
maxFixesPerViolation: 3,
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Create a QuickFixGenerator with all fixes enabled.
|
|
1064
|
+
*
|
|
1065
|
+
* @returns New QuickFixGenerator instance with all fixes
|
|
1066
|
+
*/
|
|
1067
|
+
export function createFullQuickFixGenerator() {
|
|
1068
|
+
return new QuickFixGenerator({
|
|
1069
|
+
minConfidence: 0.0,
|
|
1070
|
+
maxFixesPerViolation: 10,
|
|
1071
|
+
generatePreviews: true,
|
|
1072
|
+
validateFixes: true,
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
//# sourceMappingURL=quick-fix-generator.js.map
|