@sun-asterisk/sunlint 1.3.16 → 1.3.18
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/config/rule-analysis-strategies.js +3 -3
- package/config/rules/enhanced-rules-registry.json +40 -20
- package/core/analysis-orchestrator.js +11 -3
- package/core/cli-action-handler.js +2 -2
- package/core/config-merger.js +28 -6
- package/core/constants/defaults.js +1 -1
- package/core/file-targeting-service.js +72 -4
- package/core/output-service.js +48 -13
- package/core/summary-report-service.js +21 -3
- package/engines/heuristic-engine.js +5 -0
- package/package.json +1 -1
- package/rules/common/C002_no_duplicate_code/README.md +115 -0
- package/rules/common/C002_no_duplicate_code/analyzer.js +615 -219
- package/rules/common/C002_no_duplicate_code/test-cases/api-handlers.ts +64 -0
- package/rules/common/C002_no_duplicate_code/test-cases/data-processor.ts +46 -0
- package/rules/common/C002_no_duplicate_code/test-cases/good-example.tsx +40 -0
- package/rules/common/C002_no_duplicate_code/test-cases/product-service.ts +57 -0
- package/rules/common/C002_no_duplicate_code/test-cases/user-service.ts +49 -0
- package/rules/common/C008/analyzer.js +40 -0
- package/rules/common/C008/config.json +20 -0
- package/rules/common/C008/ts-morph-analyzer.js +1067 -0
- package/rules/common/C018_no_throw_generic_error/analyzer.js +1 -1
- package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +27 -3
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +504 -162
- package/rules/common/C029_catch_block_logging/analyzer.js +499 -89
- package/rules/common/C033_separate_service_repository/README.md +131 -20
- package/rules/common/C033_separate_service_repository/analyzer.js +1 -1
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +417 -274
- package/rules/common/C041_no_sensitive_hardcode/analyzer.js +144 -254
- package/rules/common/C041_no_sensitive_hardcode/config.json +50 -0
- package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +575 -0
- package/rules/common/C047_no_duplicate_retry_logic/analyzer.js +96 -40
- package/rules/common/C047_no_duplicate_retry_logic/symbol-analyzer-enhanced.js +17 -2
- package/rules/common/C067_no_hardcoded_config/analyzer.js +17 -16
- package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +3477 -659
- package/rules/docs/C002_no_duplicate_code.md +276 -11
- package/rules/index.js +5 -1
- package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +266 -88
- package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +805 -0
- package/rules/security/S010_no_insecure_encryption/README.md +78 -0
- package/rules/security/S010_no_insecure_encryption/analyzer.js +463 -398
- package/rules/security/S013_tls_enforcement/README.md +51 -0
- package/rules/security/S013_tls_enforcement/analyzer.js +99 -0
- package/rules/security/S013_tls_enforcement/config.json +41 -0
- package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +339 -0
- package/rules/security/S014_tls_version_enforcement/README.md +354 -0
- package/rules/security/S014_tls_version_enforcement/analyzer.js +118 -0
- package/rules/security/S014_tls_version_enforcement/config.json +56 -0
- package/rules/security/S014_tls_version_enforcement/symbol-based-analyzer.js +194 -0
- package/rules/security/S055_content_type_validation/analyzer.js +121 -279
- package/rules/security/S055_content_type_validation/symbol-based-analyzer.js +346 -0
- package/rules/tests/C002_no_duplicate_code.test.js +111 -22
- package/docs/CONSTANTS-ARCHITECTURE.md +0 -288
- package/docs/DEPLOYMENT-STRATEGIES.md +0 -270
- package/docs/ESLINT_INTEGRATION.md +0 -238
- package/docs/PERFORMANCE_MIGRATION_GUIDE.md +0 -368
- package/docs/PERFORMANCE_OPTIMIZATION_PLAN.md +0 -255
- package/rules/common/C029_catch_block_logging/analyzer-smart-pipeline.js +0 -755
- package/rules/common/C041_no_sensitive_hardcode/ast-analyzer.js +0 -296
|
@@ -1,174 +1,316 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Category: coding
|
|
2
|
+
* C002 - No Duplicate Code Analyzer (AST-based with ts-morph)
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* This analyzer uses TypeScript Compiler API via ts-morph for accurate
|
|
5
|
+
* semantic analysis and duplicate detection.
|
|
6
|
+
*
|
|
7
|
+
* Advantages over regex-based approach:
|
|
8
|
+
* - 95-99% accuracy vs 70-80%
|
|
9
|
+
* - True semantic understanding via AST
|
|
10
|
+
* - Precise function/class extraction
|
|
11
|
+
* - Better handling of edge cases
|
|
12
|
+
*
|
|
13
|
+
* Trade-offs:
|
|
14
|
+
* - TypeScript/JavaScript only (no multi-language support)
|
|
15
|
+
* - Slightly slower (~30% more time)
|
|
16
|
+
* - Requires ts-morph dependency
|
|
7
17
|
*/
|
|
8
18
|
|
|
9
19
|
const fs = require('fs');
|
|
10
20
|
const path = require('path');
|
|
11
|
-
const {
|
|
21
|
+
const { Project, SyntaxKind } = require('ts-morph');
|
|
12
22
|
|
|
13
|
-
class
|
|
23
|
+
class C002AnalyzerAST {
|
|
14
24
|
constructor(config = {}) {
|
|
15
25
|
this.config = {
|
|
16
|
-
minLines: config.minLines ||
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
similarityThreshold: config.similarityThreshold || 0.80, // 80% similarity
|
|
21
|
-
...config
|
|
26
|
+
minLines: config.minLines || 10,
|
|
27
|
+
// Stricter threshold (95%) to detect only true monkey coding (copy-paste)
|
|
28
|
+
// This filters out intentional boilerplate patterns (87-90% similarity)
|
|
29
|
+
similarityThreshold: config.similarityThreshold || 0.95,
|
|
22
30
|
};
|
|
23
|
-
this.codeBlocks = new Map();
|
|
24
|
-
this.reportedBlocks = new Set();
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
/**
|
|
28
|
-
* Analyze files for duplicate code
|
|
29
|
-
* @param {
|
|
30
|
-
* @param {string} language -
|
|
31
|
-
* @param {Object} options - Analysis options
|
|
34
|
+
* Analyze files for duplicate code
|
|
35
|
+
* @param {string[]} filePaths - Array of file paths to analyze
|
|
36
|
+
* @param {string} language - Language (typescript/javascript)
|
|
32
37
|
* @returns {Array} Array of violations
|
|
33
38
|
*/
|
|
34
|
-
analyze(
|
|
35
|
-
const violations = [];
|
|
36
|
-
|
|
39
|
+
analyze(filePaths, language = 'typescript') {
|
|
37
40
|
try {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
for (const filePath of files) {
|
|
47
|
-
console.log(`[C002 DEBUG] Processing file: ${filePath}`);
|
|
48
|
-
const content = this.readFileContent(filePath);
|
|
49
|
-
if (content) {
|
|
50
|
-
console.log(`[C002 DEBUG] File content length: ${content.length}`);
|
|
51
|
-
const codeBlocks = this.extractCodeBlocks(content, filePath);
|
|
52
|
-
console.log(`[C002 DEBUG] Extracted ${codeBlocks.length} code blocks from ${filePath}`);
|
|
53
|
-
codeBlocks.forEach((block, i) => {
|
|
54
|
-
console.log(`[C002 DEBUG] Block ${i}: ${block.type} at lines ${block.startLine}-${block.endLine} (${block.lineCount} lines)`);
|
|
55
|
-
});
|
|
56
|
-
allCodeBlocks.push(...codeBlocks);
|
|
57
|
-
}
|
|
41
|
+
// Filter TypeScript/JavaScript files only
|
|
42
|
+
const validFiles = filePaths.filter(fp =>
|
|
43
|
+
/\.(ts|tsx|js|jsx)$/i.test(fp)
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
if (validFiles.length === 0) {
|
|
47
|
+
return [];
|
|
58
48
|
}
|
|
49
|
+
|
|
50
|
+
// Create fresh project instance for each analyze() call
|
|
51
|
+
// This prevents file caching issues
|
|
52
|
+
this.project = new Project({
|
|
53
|
+
compilerOptions: {
|
|
54
|
+
target: 99, // ESNext
|
|
55
|
+
allowJs: true,
|
|
56
|
+
},
|
|
57
|
+
skipAddingFilesFromTsConfig: true,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Add files to ts-morph project (in batches for performance)
|
|
61
|
+
const sourceFiles = [];
|
|
62
|
+
const batchSize = 10;
|
|
59
63
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
console.log(`[C002 DEBUG] Created ${fileViolations.length} violations for ${filePath}`);
|
|
71
|
-
violations.push(...fileViolations);
|
|
64
|
+
for (let i = 0; i < validFiles.length; i += batchSize) {
|
|
65
|
+
const batch = validFiles.slice(i, i + batchSize);
|
|
66
|
+
|
|
67
|
+
batch.forEach(fp => {
|
|
68
|
+
try {
|
|
69
|
+
const sourceFile = this.project.addSourceFileAtPath(fp);
|
|
70
|
+
sourceFiles.push(sourceFile);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
// Silently skip unparseable files
|
|
73
|
+
}
|
|
72
74
|
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (sourceFiles.length === 0) {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Extract code blocks from all files
|
|
82
|
+
const allBlocks = [];
|
|
83
|
+
sourceFiles.forEach(sourceFile => {
|
|
84
|
+
const blocks = this.extractCodeBlocks(sourceFile);
|
|
85
|
+
allBlocks.push(...blocks);
|
|
73
86
|
});
|
|
74
87
|
|
|
88
|
+
// Find duplicates
|
|
89
|
+
const duplicateGroups = this.findDuplicates(allBlocks);
|
|
90
|
+
|
|
91
|
+
// Create violations
|
|
92
|
+
const violations = this.createViolations(duplicateGroups);
|
|
93
|
+
|
|
94
|
+
return violations;
|
|
95
|
+
|
|
75
96
|
} catch (error) {
|
|
76
|
-
console.
|
|
97
|
+
console.error('Error analyzing files with C002-AST:', error.message);
|
|
98
|
+
return [];
|
|
77
99
|
}
|
|
100
|
+
}
|
|
78
101
|
|
|
79
|
-
|
|
80
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Extract code blocks from source file using AST
|
|
104
|
+
* @param {SourceFile} sourceFile - ts-morph source file
|
|
105
|
+
* @returns {Array} Array of code blocks
|
|
106
|
+
*/
|
|
107
|
+
extractCodeBlocks(sourceFile) {
|
|
108
|
+
const blocks = [];
|
|
109
|
+
const filePath = sourceFile.getFilePath();
|
|
110
|
+
|
|
111
|
+
// Extract functions
|
|
112
|
+
const functions = sourceFile.getFunctions();
|
|
113
|
+
functions.forEach(fn => {
|
|
114
|
+
const block = this.createBlockFromNode(fn, 'function', filePath);
|
|
115
|
+
if (block && block.nonCommentLines >= this.config.minLines) {
|
|
116
|
+
blocks.push(block);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Extract arrow functions and function expressions
|
|
121
|
+
const variableDeclarations = sourceFile.getVariableDeclarations();
|
|
122
|
+
variableDeclarations.forEach(varDecl => {
|
|
123
|
+
const initializer = varDecl.getInitializer();
|
|
124
|
+
if (initializer) {
|
|
125
|
+
const kind = initializer.getKind();
|
|
126
|
+
if (kind === SyntaxKind.ArrowFunction ||
|
|
127
|
+
kind === SyntaxKind.FunctionExpression) {
|
|
128
|
+
const block = this.createBlockFromNode(initializer, 'arrow-function', filePath, varDecl.getName());
|
|
129
|
+
if (block && block.nonCommentLines >= this.config.minLines) {
|
|
130
|
+
blocks.push(block);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Extract classes
|
|
137
|
+
const classes = sourceFile.getClasses();
|
|
138
|
+
classes.forEach(cls => {
|
|
139
|
+
const block = this.createBlockFromNode(cls, 'class', filePath);
|
|
140
|
+
if (block && block.nonCommentLines >= this.config.minLines) {
|
|
141
|
+
blocks.push(block);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Extract methods from class
|
|
145
|
+
const methods = cls.getMethods();
|
|
146
|
+
methods.forEach(method => {
|
|
147
|
+
const block = this.createBlockFromNode(method, 'method', filePath);
|
|
148
|
+
if (block && block.nonCommentLines >= this.config.minLines) {
|
|
149
|
+
blocks.push(block);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Extract interfaces
|
|
155
|
+
const interfaces = sourceFile.getInterfaces();
|
|
156
|
+
interfaces.forEach(iface => {
|
|
157
|
+
const block = this.createBlockFromNode(iface, 'interface', filePath);
|
|
158
|
+
if (block && block.nonCommentLines >= this.config.minLines) {
|
|
159
|
+
blocks.push(block);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return blocks;
|
|
81
164
|
}
|
|
82
165
|
|
|
83
166
|
/**
|
|
84
|
-
*
|
|
85
|
-
* @param {
|
|
86
|
-
* @
|
|
167
|
+
* Create code block from AST node
|
|
168
|
+
* @param {Node} node - AST node
|
|
169
|
+
* @param {string} type - Block type
|
|
170
|
+
* @param {string} filePath - File path
|
|
171
|
+
* @param {string} customName - Custom name (for arrow functions)
|
|
172
|
+
* @returns {Object} Code block
|
|
87
173
|
*/
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
174
|
+
createBlockFromNode(node, type, filePath, customName = null) {
|
|
175
|
+
const startLine = node.getStartLineNumber();
|
|
176
|
+
const endLine = node.getEndLineNumber();
|
|
177
|
+
const fullText = node.getText();
|
|
178
|
+
|
|
179
|
+
// Count non-comment lines
|
|
180
|
+
const lines = fullText.split('\n');
|
|
181
|
+
const nonCommentLines = this.countNonCommentLines(lines);
|
|
182
|
+
|
|
183
|
+
if (nonCommentLines < this.config.minLines) {
|
|
93
184
|
return null;
|
|
94
185
|
}
|
|
186
|
+
|
|
187
|
+
// Get name
|
|
188
|
+
let name = customName;
|
|
189
|
+
if (!name && typeof node.getName === 'function') {
|
|
190
|
+
name = node.getName();
|
|
191
|
+
}
|
|
192
|
+
if (!name) {
|
|
193
|
+
name = 'anonymous';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Get normalized code for comparison
|
|
197
|
+
const normalizedCode = this.normalizeCode(fullText);
|
|
198
|
+
|
|
199
|
+
// Extract semantic tokens (identifiers)
|
|
200
|
+
const tokens = this.extractTokensFromNode(node);
|
|
201
|
+
|
|
202
|
+
// Extract structure (skeleton)
|
|
203
|
+
const structure = this.extractStructureFromNode(node);
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
startLine,
|
|
207
|
+
endLine,
|
|
208
|
+
lineCount: endLine - startLine + 1,
|
|
209
|
+
nonCommentLines,
|
|
210
|
+
filePath,
|
|
211
|
+
type,
|
|
212
|
+
name,
|
|
213
|
+
fullText,
|
|
214
|
+
normalizedCode,
|
|
215
|
+
tokens,
|
|
216
|
+
structure,
|
|
217
|
+
node // Keep reference to AST node
|
|
218
|
+
};
|
|
95
219
|
}
|
|
96
220
|
|
|
97
221
|
/**
|
|
98
|
-
*
|
|
99
|
-
* @param {string}
|
|
100
|
-
* @
|
|
101
|
-
* @returns {Array} Array of code blocks with metadata
|
|
222
|
+
* Count non-comment lines in code
|
|
223
|
+
* @param {string[]} lines - Array of lines
|
|
224
|
+
* @returns {number} Count
|
|
102
225
|
*/
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
let currentBlock = null;
|
|
110
|
-
let braceLevel = 0;
|
|
111
|
-
|
|
112
|
-
lines.forEach((line, index) => {
|
|
113
|
-
const lineNum = index + 1;
|
|
114
|
-
const trimmedLine = line.trim();
|
|
226
|
+
countNonCommentLines(lines) {
|
|
227
|
+
let count = 0;
|
|
228
|
+
let inBlockComment = false;
|
|
229
|
+
|
|
230
|
+
for (const line of lines) {
|
|
231
|
+
const trimmed = line.trim();
|
|
115
232
|
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
|
|
233
|
+
// Check block comment start/end
|
|
234
|
+
if (trimmed.startsWith('/*')) inBlockComment = true;
|
|
235
|
+
if (inBlockComment) {
|
|
236
|
+
if (trimmed.endsWith('*/')) inBlockComment = false;
|
|
237
|
+
continue;
|
|
120
238
|
}
|
|
121
239
|
|
|
122
|
-
// Skip empty lines
|
|
123
|
-
if (
|
|
124
|
-
|
|
240
|
+
// Skip single-line comments and empty lines
|
|
241
|
+
if (trimmed.startsWith('//') || trimmed.length === 0) {
|
|
242
|
+
continue;
|
|
125
243
|
}
|
|
126
244
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (braceLevel <= 0) {
|
|
142
|
-
currentBlock.endLine = lineNum;
|
|
143
|
-
currentBlock.lineCount = currentBlock.lines.length;
|
|
144
|
-
|
|
145
|
-
// Only consider blocks that meet minimum line requirement
|
|
146
|
-
if (currentBlock.lineCount >= this.config.minLines) {
|
|
147
|
-
currentBlock.normalizedCode = this.normalizeCode(currentBlock.lines.join('\n'));
|
|
148
|
-
if (currentBlock.normalizedCode.length > 20) { // Skip if too short after normalization
|
|
149
|
-
blocks.push(currentBlock);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
currentBlock = null;
|
|
153
|
-
braceLevel = 0;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
});
|
|
245
|
+
count++;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return count;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Extract tokens (identifiers) from AST node (optimized)
|
|
253
|
+
* @param {Node} node - AST node
|
|
254
|
+
* @returns {Array<string>} Array of tokens
|
|
255
|
+
*/
|
|
256
|
+
extractTokensFromNode(node) {
|
|
257
|
+
const tokens = [];
|
|
258
|
+
const identifiers = node.getDescendantsOfKind(SyntaxKind.Identifier);
|
|
157
259
|
|
|
158
|
-
|
|
260
|
+
// Limit to first 100 identifiers for performance
|
|
261
|
+
const limit = Math.min(identifiers.length, 100);
|
|
262
|
+
|
|
263
|
+
for (let i = 0; i < limit; i++) {
|
|
264
|
+
const text = identifiers[i].getText();
|
|
265
|
+
// Skip TypeScript/JavaScript keywords
|
|
266
|
+
if (!this.isKeyword(text) && text.length > 1) {
|
|
267
|
+
tokens.push(text);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return tokens;
|
|
159
272
|
}
|
|
160
273
|
|
|
161
274
|
/**
|
|
162
|
-
*
|
|
163
|
-
* @param {string}
|
|
164
|
-
* @returns {
|
|
275
|
+
* Check if string is a keyword
|
|
276
|
+
* @param {string} text - Text to check
|
|
277
|
+
* @returns {boolean} True if keyword
|
|
165
278
|
*/
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
279
|
+
isKeyword(text) {
|
|
280
|
+
const keywords = new Set([
|
|
281
|
+
'function', 'const', 'let', 'var', 'if', 'else', 'return', 'for', 'while',
|
|
282
|
+
'class', 'interface', 'extends', 'implements', 'import', 'export', 'from',
|
|
283
|
+
'async', 'await', 'try', 'catch', 'throw', 'new', 'this', 'super',
|
|
284
|
+
'true', 'false', 'null', 'undefined', 'typeof', 'instanceof', 'of', 'in'
|
|
285
|
+
]);
|
|
286
|
+
return keywords.has(text);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Extract structure (skeleton) from AST node
|
|
291
|
+
* @param {Node} node - AST node
|
|
292
|
+
* @returns {string} Structure string
|
|
293
|
+
*/
|
|
294
|
+
extractStructureFromNode(node) {
|
|
295
|
+
// Get syntax kind path for structure comparison (optimized)
|
|
296
|
+
const structure = [];
|
|
297
|
+
let depth = 0;
|
|
298
|
+
const maxDepth = 20; // Limit depth for performance
|
|
299
|
+
|
|
300
|
+
node.forEachDescendant((descendant, traversal) => {
|
|
301
|
+
depth++;
|
|
302
|
+
|
|
303
|
+
// Stop if too deep or too many nodes
|
|
304
|
+
if (depth > maxDepth || structure.length > 200) {
|
|
305
|
+
traversal.stop();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const kind = descendant.getKindName();
|
|
310
|
+
structure.push(kind);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
return structure.join('|');
|
|
172
314
|
}
|
|
173
315
|
|
|
174
316
|
/**
|
|
@@ -178,186 +320,440 @@ class C002_no_duplicate_codeAnalyzer {
|
|
|
178
320
|
*/
|
|
179
321
|
normalizeCode(code) {
|
|
180
322
|
let normalized = code;
|
|
181
|
-
|
|
323
|
+
|
|
182
324
|
if (this.config.ignoreComments) {
|
|
183
|
-
// Remove single line comments (// comments)
|
|
184
325
|
normalized = normalized.replace(/\/\/.*$/gm, '');
|
|
185
|
-
// Remove multi-line comments (/* comments */)
|
|
186
326
|
normalized = normalized.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
187
|
-
// Remove # comments (for other languages)
|
|
188
|
-
normalized = normalized.replace(/#.*$/gm, '');
|
|
189
327
|
}
|
|
190
|
-
|
|
328
|
+
|
|
191
329
|
if (this.config.ignoreWhitespace) {
|
|
192
|
-
// Normalize whitespace
|
|
193
330
|
normalized = normalized
|
|
194
|
-
.replace(/\s+/g, ' ')
|
|
195
|
-
.replace(/\s*{\s*/g, '{')
|
|
331
|
+
.replace(/\s+/g, ' ')
|
|
332
|
+
.replace(/\s*{\s*/g, '{')
|
|
196
333
|
.replace(/\s*}\s*/g, '}')
|
|
197
|
-
.replace(/\s*;\s*/g, ';')
|
|
198
|
-
.replace(/\s*,\s*/g, ',')
|
|
334
|
+
.replace(/\s*;\s*/g, ';')
|
|
335
|
+
.replace(/\s*,\s*/g, ',')
|
|
199
336
|
.trim();
|
|
200
337
|
}
|
|
201
|
-
|
|
338
|
+
|
|
202
339
|
if (this.config.ignoreEmptyLines) {
|
|
203
|
-
// Remove empty lines
|
|
204
340
|
normalized = normalized
|
|
205
341
|
.split('\n')
|
|
206
342
|
.filter(line => line.trim().length > 0)
|
|
207
343
|
.join('\n');
|
|
208
344
|
}
|
|
209
|
-
|
|
210
|
-
console.log(`[C002 DEBUG] Normalized code block:
|
|
211
|
-
${normalized}
|
|
212
|
-
---`);
|
|
213
|
-
|
|
345
|
+
|
|
214
346
|
return normalized;
|
|
215
347
|
}
|
|
216
348
|
|
|
217
349
|
/**
|
|
218
|
-
* Find duplicate code blocks
|
|
350
|
+
* Find duplicate code blocks using AST-based semantic analysis
|
|
219
351
|
* @param {Array} blocks - Array of code blocks
|
|
220
352
|
* @returns {Array} Array of duplicate groups
|
|
221
353
|
*/
|
|
222
354
|
findDuplicates(blocks) {
|
|
223
355
|
const duplicateGroups = [];
|
|
224
356
|
const processedBlocks = new Set();
|
|
225
|
-
|
|
357
|
+
|
|
226
358
|
for (let i = 0; i < blocks.length; i++) {
|
|
227
359
|
if (processedBlocks.has(i)) continue;
|
|
228
|
-
|
|
360
|
+
|
|
229
361
|
const currentBlock = blocks[i];
|
|
230
362
|
const duplicates = [currentBlock];
|
|
231
|
-
|
|
363
|
+
|
|
232
364
|
for (let j = i + 1; j < blocks.length; j++) {
|
|
233
365
|
if (processedBlocks.has(j)) continue;
|
|
234
|
-
|
|
366
|
+
|
|
235
367
|
const otherBlock = blocks[j];
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
368
|
+
|
|
369
|
+
// Calculate similarity
|
|
370
|
+
const similarity = this.calculateSimilarity(currentBlock, otherBlock);
|
|
371
|
+
|
|
241
372
|
if (similarity >= this.config.similarityThreshold) {
|
|
373
|
+
// Check if this is an intentional pattern
|
|
374
|
+
if (this.isIntentionalPattern(currentBlock, otherBlock)) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
|
|
242
378
|
duplicates.push(otherBlock);
|
|
243
379
|
processedBlocks.add(j);
|
|
244
380
|
}
|
|
245
381
|
}
|
|
246
|
-
|
|
382
|
+
|
|
247
383
|
if (duplicates.length > 1) {
|
|
248
384
|
duplicateGroups.push(duplicates);
|
|
249
385
|
processedBlocks.add(i);
|
|
250
386
|
}
|
|
251
387
|
}
|
|
252
|
-
|
|
388
|
+
|
|
253
389
|
return duplicateGroups;
|
|
254
390
|
}
|
|
255
391
|
|
|
256
392
|
/**
|
|
257
|
-
* Calculate similarity between two
|
|
258
|
-
* @param {
|
|
259
|
-
* @param {
|
|
260
|
-
* @returns {number} Similarity
|
|
393
|
+
* Calculate similarity between two blocks using AST (optimized)
|
|
394
|
+
* @param {Object} block1 - First block
|
|
395
|
+
* @param {Object} block2 - Second block
|
|
396
|
+
* @returns {number} Similarity (0-1)
|
|
261
397
|
*/
|
|
262
|
-
calculateSimilarity(
|
|
263
|
-
if
|
|
398
|
+
calculateSimilarity(block1, block2) {
|
|
399
|
+
// Quick rejection: if structures are very different, skip expensive comparison
|
|
400
|
+
if (Math.abs(block1.structure.length - block2.structure.length) > 100) {
|
|
401
|
+
return 0;
|
|
402
|
+
}
|
|
264
403
|
|
|
265
|
-
// Use
|
|
266
|
-
const
|
|
267
|
-
const shorter = code1.length > code2.length ? code2 : code1;
|
|
404
|
+
// Use structure similarity as primary metric
|
|
405
|
+
const structureSim = this.calculateStructureSimilarity(block1.structure, block2.structure);
|
|
268
406
|
|
|
269
|
-
if
|
|
407
|
+
// Early exit if structure is too different
|
|
408
|
+
if (structureSim < 0.5) {
|
|
409
|
+
return structureSim * 0.6; // Don't waste time on code comparison
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Use normalized code as secondary metric only if structure is similar
|
|
413
|
+
const codeSim = this.calculateStringSimilarity(
|
|
414
|
+
block1.normalizedCode.substring(0, 500), // Limit string length
|
|
415
|
+
block2.normalizedCode.substring(0, 500)
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
// Weighted average: 60% structure, 40% code
|
|
419
|
+
return structureSim * 0.6 + codeSim * 0.4;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Calculate structure similarity
|
|
424
|
+
* @param {string} structure1 - First structure
|
|
425
|
+
* @param {string} structure2 - Second structure
|
|
426
|
+
* @returns {number} Similarity (0-1)
|
|
427
|
+
*/
|
|
428
|
+
calculateStructureSimilarity(structure1, structure2) {
|
|
429
|
+
if (structure1 === structure2) return 1.0;
|
|
430
|
+
|
|
431
|
+
const tokens1 = structure1.split('|');
|
|
432
|
+
const tokens2 = structure2.split('|');
|
|
433
|
+
|
|
434
|
+
const maxLen = Math.max(tokens1.length, tokens2.length);
|
|
435
|
+
if (maxLen === 0) return 1.0;
|
|
270
436
|
|
|
437
|
+
let matches = 0;
|
|
438
|
+
const minLen = Math.min(tokens1.length, tokens2.length);
|
|
439
|
+
|
|
440
|
+
for (let i = 0; i < minLen; i++) {
|
|
441
|
+
if (tokens1[i] === tokens2[i]) matches++;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return matches / maxLen;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Calculate string similarity using Levenshtein distance
|
|
449
|
+
* @param {string} str1 - First string
|
|
450
|
+
* @param {string} str2 - Second string
|
|
451
|
+
* @returns {number} Similarity (0-1)
|
|
452
|
+
*/
|
|
453
|
+
calculateStringSimilarity(str1, str2) {
|
|
454
|
+
if (str1 === str2) return 1.0;
|
|
455
|
+
|
|
456
|
+
const longer = str1.length > str2.length ? str1 : str2;
|
|
457
|
+
const shorter = str1.length > str2.length ? str2 : str1;
|
|
458
|
+
|
|
459
|
+
if (longer.length === 0) return 1.0;
|
|
460
|
+
|
|
271
461
|
const distance = this.levenshteinDistance(longer, shorter);
|
|
272
462
|
return (longer.length - distance) / longer.length;
|
|
273
463
|
}
|
|
274
464
|
|
|
275
465
|
/**
|
|
276
|
-
* Calculate Levenshtein distance
|
|
466
|
+
* Calculate Levenshtein distance
|
|
277
467
|
* @param {string} str1 - First string
|
|
278
468
|
* @param {string} str2 - Second string
|
|
279
|
-
* @returns {number}
|
|
469
|
+
* @returns {number} Distance
|
|
280
470
|
*/
|
|
281
471
|
levenshteinDistance(str1, str2) {
|
|
282
472
|
const matrix = Array(str2.length + 1).fill().map(() => Array(str1.length + 1).fill(0));
|
|
283
|
-
|
|
473
|
+
|
|
284
474
|
for (let i = 0; i <= str1.length; i++) matrix[0][i] = i;
|
|
285
475
|
for (let j = 0; j <= str2.length; j++) matrix[j][0] = j;
|
|
286
|
-
|
|
476
|
+
|
|
287
477
|
for (let j = 1; j <= str2.length; j++) {
|
|
288
478
|
for (let i = 1; i <= str1.length; i++) {
|
|
289
479
|
const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
|
|
290
480
|
matrix[j][i] = Math.min(
|
|
291
|
-
matrix[j - 1]
|
|
292
|
-
matrix[j
|
|
293
|
-
matrix[j - 1][i - 1] + cost
|
|
481
|
+
matrix[j][i - 1] + 1,
|
|
482
|
+
matrix[j - 1][i] + 1,
|
|
483
|
+
matrix[j - 1][i - 1] + cost
|
|
294
484
|
);
|
|
295
485
|
}
|
|
296
486
|
}
|
|
297
|
-
|
|
487
|
+
|
|
298
488
|
return matrix[str2.length][str1.length];
|
|
299
489
|
}
|
|
300
490
|
|
|
301
491
|
/**
|
|
302
|
-
*
|
|
303
|
-
* @param {
|
|
304
|
-
* @param {
|
|
305
|
-
* @returns {
|
|
492
|
+
* Check if two blocks are intentional patterns using AST analysis
|
|
493
|
+
* @param {Object} block1 - First block
|
|
494
|
+
* @param {Object} block2 - Second block
|
|
495
|
+
* @returns {boolean} True if intentional pattern
|
|
306
496
|
*/
|
|
307
|
-
|
|
308
|
-
|
|
497
|
+
isIntentionalPattern(block1, block2) {
|
|
498
|
+
// 0. Skip if same location (should not happen, but safety check)
|
|
499
|
+
if (block1.filePath === block2.filePath &&
|
|
500
|
+
block1.startLine === block2.startLine &&
|
|
501
|
+
block1.endLine === block2.endLine) {
|
|
502
|
+
return true; // Treat as intentional to skip
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// 1. Simple JSX/HTML wrapper detection (no business logic)
|
|
506
|
+
// Pattern: function Component({ ...props }) { return <span {...props} /> }
|
|
507
|
+
if (this.isSimpleJSXWrapper(block1.fullText) &&
|
|
508
|
+
this.isSimpleJSXWrapper(block2.fullText)) {
|
|
509
|
+
|
|
510
|
+
// If function names are different, it's intentional pattern
|
|
511
|
+
if (block1.name !== block2.name &&
|
|
512
|
+
block1.name !== 'anonymous' &&
|
|
513
|
+
block2.name !== 'anonymous') {
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
309
517
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
518
|
+
// 2. React component wrapper detection (library wrappers)
|
|
519
|
+
// Pattern: function Component({ ...props }) { return <Primitive.X {...props} /> }
|
|
520
|
+
if (this.isReactWrapperComponent(block1.fullText) &&
|
|
521
|
+
this.isReactWrapperComponent(block2.fullText)) {
|
|
313
522
|
|
|
314
|
-
|
|
315
|
-
|
|
523
|
+
// Extract wrapped component names
|
|
524
|
+
const wrapped1 = this.extractWrappedComponentName(block1.fullText);
|
|
525
|
+
const wrapped2 = this.extractWrappedComponentName(block2.fullText);
|
|
316
526
|
|
|
317
|
-
|
|
527
|
+
// If wrapping different components, it's intentional
|
|
528
|
+
if (wrapped1 && wrapped2 && wrapped1 !== wrapped2) {
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// 3. Token-based analysis
|
|
534
|
+
const tokenSimilarity = this.calculateTokenSimilarity(block1.tokens, block2.tokens);
|
|
535
|
+
const structureSimilarity = this.calculateStructureSimilarity(block1.structure, block2.structure);
|
|
536
|
+
|
|
537
|
+
// High structure similarity + Low token similarity = Intentional pattern
|
|
538
|
+
if (structureSimilarity >= 0.90 && tokenSimilarity <= 0.65) {
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// 4. Unique token ratio
|
|
543
|
+
const uniqueTokens1 = block1.tokens.filter(t => !block2.tokens.includes(t));
|
|
544
|
+
const uniqueTokens2 = block2.tokens.filter(t => !block1.tokens.includes(t));
|
|
545
|
+
const totalTokens = Math.max(block1.tokens.length, block2.tokens.length);
|
|
546
|
+
|
|
547
|
+
if (totalTokens > 0) {
|
|
548
|
+
const uniqueRatio = (uniqueTokens1.length + uniqueTokens2.length) / (2 * totalTokens);
|
|
549
|
+
|
|
550
|
+
// 10-40% different tokens + high structure similarity = Intentional
|
|
551
|
+
if (uniqueRatio >= 0.10 && uniqueRatio <= 0.40 && structureSimilarity >= 0.85) {
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// 5. Function name completely different = intentional pattern
|
|
557
|
+
if (block1.name !== 'anonymous' && block2.name !== 'anonymous' && block1.name !== block2.name) {
|
|
558
|
+
const nameSimilarity = this.calculateStringSimilarity(
|
|
559
|
+
block1.name.toLowerCase(),
|
|
560
|
+
block2.name.toLowerCase()
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
// Names less than 50% similar = completely different purposes
|
|
564
|
+
if (nameSimilarity < 0.50) {
|
|
565
|
+
return true;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Check if code is a simple JSX/HTML wrapper (no business logic)
|
|
574
|
+
* @param {string} code - Full code text
|
|
575
|
+
* @returns {boolean} True if it's a simple wrapper
|
|
576
|
+
*/
|
|
577
|
+
isSimpleJSXWrapper(code) {
|
|
578
|
+
// Pattern: function that just returns JSX with props spreading, no logic
|
|
579
|
+
const lines = code.split('\n').filter(l => l.trim() && !l.trim().startsWith('//')).length;
|
|
580
|
+
|
|
581
|
+
// Simple wrappers are typically 5-20 lines
|
|
582
|
+
if (lines < 5 || lines > 20) {
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Must have {...props} spread
|
|
587
|
+
if (!code.includes('{...props}')) {
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Must NOT have business logic
|
|
592
|
+
const hasLogic = /\b(if|else|switch|for|while|map|filter|reduce|forEach)\b/.test(code);
|
|
593
|
+
if (hasLogic) {
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Must have single return statement with JSX
|
|
598
|
+
const returnCount = (code.match(/\breturn\b/g) || []).length;
|
|
599
|
+
if (returnCount !== 1) {
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Must return JSX element (either component or HTML element)
|
|
604
|
+
const hasJSXReturn = /<[a-zA-Z]/.test(code);
|
|
605
|
+
return hasJSXReturn;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Check if code is a React component wrapper
|
|
610
|
+
* @param {string} code - Full code text
|
|
611
|
+
* @returns {boolean} True if it's a wrapper
|
|
612
|
+
*/
|
|
613
|
+
isReactWrapperComponent(code) {
|
|
614
|
+
// Pattern: very short function that just returns <Component {...props} />
|
|
615
|
+
const lines = code.split('\n').filter(l => l.trim() && !l.trim().startsWith('//')).length;
|
|
616
|
+
|
|
617
|
+
// Wrapper components are typically 5-15 lines
|
|
618
|
+
if (lines < 5 || lines > 20) {
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Must have {...props} spread
|
|
623
|
+
if (!code.includes('{...props}')) {
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Must return JSX with a component (starts with uppercase)
|
|
628
|
+
const returnMatch = code.match(/return\s*\(?<([A-Z][a-zA-Z0-9.]*)/);
|
|
629
|
+
return returnMatch !== null;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Extract wrapped component name from wrapper code
|
|
634
|
+
* @param {string} code - Full code text
|
|
635
|
+
* @returns {string|null} Component name or null
|
|
636
|
+
*/
|
|
637
|
+
extractWrappedComponentName(code) {
|
|
638
|
+
// Match: return <ComponentName or return <Component.SubComponent
|
|
639
|
+
const match = code.match(/return\s*\(?<([A-Z][a-zA-Z0-9.]*)/);
|
|
640
|
+
return match ? match[1] : null;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Calculate token similarity (Jaccard index)
|
|
645
|
+
* @param {Array<string>} tokens1 - First token array
|
|
646
|
+
* @param {Array<string>} tokens2 - Second token array
|
|
647
|
+
* @returns {number} Similarity (0-1)
|
|
648
|
+
*/
|
|
649
|
+
calculateTokenSimilarity(tokens1, tokens2) {
|
|
650
|
+
if (tokens1.length === 0 && tokens2.length === 0) return 1.0;
|
|
651
|
+
if (tokens1.length === 0 || tokens2.length === 0) return 0.0;
|
|
652
|
+
|
|
653
|
+
const set1 = new Set(tokens1);
|
|
654
|
+
const set2 = new Set(tokens2);
|
|
655
|
+
|
|
656
|
+
const intersection = new Set([...set1].filter(x => set2.has(x)));
|
|
657
|
+
const union = new Set([...set1, ...set2]);
|
|
658
|
+
|
|
659
|
+
return intersection.size / union.size;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Create violations from duplicate groups
|
|
664
|
+
* @param {Array} duplicateGroups - Array of duplicate groups
|
|
665
|
+
* @returns {Array} Array of violations
|
|
666
|
+
*/
|
|
667
|
+
createViolations(duplicateGroups) {
|
|
668
|
+
const violations = [];
|
|
669
|
+
|
|
670
|
+
duplicateGroups.forEach(group => {
|
|
671
|
+
const firstBlock = group[0];
|
|
318
672
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
673
|
+
// Use relative path from project root to avoid confusion
|
|
674
|
+
const locations = group.map(block => {
|
|
675
|
+
// Try to get relative path, fallback to absolute if not possible
|
|
676
|
+
let displayPath = block.filePath;
|
|
677
|
+
|
|
678
|
+
// Find common project root (where package.json or node_modules exists)
|
|
679
|
+
const parts = block.filePath.split(path.sep);
|
|
680
|
+
const projectRootIndex = parts.findIndex(p => p === 'components' || p === 'hooks' || p === 'lib' || p === 'src' || p === 'app');
|
|
681
|
+
|
|
682
|
+
if (projectRootIndex > 0) {
|
|
683
|
+
displayPath = parts.slice(projectRootIndex).join('/');
|
|
684
|
+
} else {
|
|
685
|
+
// Fallback: show last 2-3 path segments
|
|
686
|
+
displayPath = parts.slice(-3).join('/');
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
return `${displayPath}:${block.startLine}-${block.endLine}`;
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
// Generate suggestions based on context
|
|
693
|
+
const suggestions = this.generateRefactoringSuggestions(group);
|
|
694
|
+
|
|
695
|
+
const violation = {
|
|
696
|
+
ruleId: 'C002', // Changed from 'rule' to 'ruleId' for consistency with output-service.js
|
|
697
|
+
severity: 'warning',
|
|
698
|
+
message: `Duplicate ${firstBlock.type} detected (${firstBlock.nonCommentLines} non-comment lines). ${suggestions[0]} Found in ${group.length} locations: ${locations.join(', ')}`,
|
|
699
|
+
file: firstBlock.filePath,
|
|
700
|
+
line: firstBlock.startLine,
|
|
324
701
|
column: 1,
|
|
325
|
-
endLine:
|
|
326
|
-
endColumn: 1,
|
|
327
|
-
filePath: filePath, // Add filePath field for engine compatibility
|
|
702
|
+
endLine: firstBlock.endLine,
|
|
328
703
|
data: {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
704
|
+
duplicateCount: group.length,
|
|
705
|
+
nonCommentLines: firstBlock.nonCommentLines,
|
|
706
|
+
locations: locations,
|
|
707
|
+
blockType: firstBlock.type,
|
|
708
|
+
suggestions: suggestions
|
|
333
709
|
}
|
|
334
|
-
}
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
violations.push(violation);
|
|
335
713
|
});
|
|
336
|
-
|
|
714
|
+
|
|
337
715
|
return violations;
|
|
338
716
|
}
|
|
339
717
|
|
|
340
718
|
/**
|
|
341
|
-
*
|
|
719
|
+
* Generate refactoring suggestions based on duplicate context
|
|
720
|
+
* @param {Array} group - Duplicate group
|
|
721
|
+
* @returns {Array<string>} Array of suggestions
|
|
342
722
|
*/
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
723
|
+
generateRefactoringSuggestions(group) {
|
|
724
|
+
const suggestions = [];
|
|
725
|
+
const firstBlock = group[0];
|
|
726
|
+
const files = new Set(group.map(b => b.filePath));
|
|
347
727
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
728
|
+
if (files.size === 1) {
|
|
729
|
+
// Same file duplicates
|
|
730
|
+
suggestions.push('Extract into a shared utility function in this file');
|
|
731
|
+
} else {
|
|
732
|
+
// Cross-file duplicates
|
|
733
|
+
suggestions.push('Extract into a shared utility module or helper file');
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Type-specific suggestions
|
|
737
|
+
if (firstBlock.type === 'function' || firstBlock.type === 'arrow-function') {
|
|
738
|
+
suggestions.push('Consider using function composition or higher-order functions');
|
|
739
|
+
} else if (firstBlock.type === 'class') {
|
|
740
|
+
suggestions.push('Use inheritance or composition to share common behavior');
|
|
741
|
+
} else if (firstBlock.type === 'method') {
|
|
742
|
+
suggestions.push('Extract common logic into a base class or mixin');
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
return suggestions;
|
|
360
746
|
}
|
|
361
747
|
}
|
|
362
748
|
|
|
363
|
-
|
|
749
|
+
// Export both class and SunLint-compatible API
|
|
750
|
+
module.exports = C002AnalyzerAST;
|
|
751
|
+
|
|
752
|
+
// SunLint integration: Export check() method
|
|
753
|
+
module.exports.check = async function check(filePaths, language) {
|
|
754
|
+
console.log(`🔍 C002 Analyzer called with ${filePaths.length} files, language: ${language}`);
|
|
755
|
+
const analyzer = new C002AnalyzerAST();
|
|
756
|
+
const violations = await analyzer.analyze(filePaths);
|
|
757
|
+
console.log(`✅ C002 Analyzer found ${violations.length} violations`);
|
|
758
|
+
return violations;
|
|
759
|
+
};
|