@sun-asterisk/sunlint 1.3.9 → 1.3.10
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/rules/enhanced-rules-registry.json +18 -0
- package/core/cli-program.js +1 -1
- package/core/semantic-engine.js +4 -2
- package/package.json +1 -1
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +116 -10
- package/rules/common/C060_no_override_superclass/analyzer.js +180 -0
- package/rules/common/C060_no_override_superclass/config.json +50 -0
- package/rules/common/C060_no_override_superclass/symbol-based-analyzer.js +220 -0
- package/rules/index.js +1 -0
|
@@ -375,6 +375,24 @@
|
|
|
375
375
|
}
|
|
376
376
|
}
|
|
377
377
|
},
|
|
378
|
+
"C060": {
|
|
379
|
+
"name": "Do not override superclass methods and ignore critical logic",
|
|
380
|
+
"description": "Preserve important behavior or lifecycle logic defined in the superclass to ensure correctness and prevent silent errors.",
|
|
381
|
+
"category": "logging",
|
|
382
|
+
"severity": "warning",
|
|
383
|
+
"languages": ["typescript", "javascript", "dart"],
|
|
384
|
+
"analyzer": "./rules/common/C060_no_override_superclass/analyzer.js",
|
|
385
|
+
"version": "1.0.0",
|
|
386
|
+
"status": "stable",
|
|
387
|
+
"tags": ["logging", "production", "debugging", "console"],
|
|
388
|
+
"strategy": {
|
|
389
|
+
"preferred": "regex",
|
|
390
|
+
"fallbacks": ["regex"],
|
|
391
|
+
"accuracy": {
|
|
392
|
+
"regex": 90
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
},
|
|
378
396
|
"S001": {
|
|
379
397
|
"name": "Fail Securely",
|
|
380
398
|
"description": "Verify that if there is an error in access control, the system fails securely",
|
package/core/cli-program.js
CHANGED
|
@@ -71,7 +71,7 @@ function createCliProgram() {
|
|
|
71
71
|
.option('--debug', 'Enable debug mode')
|
|
72
72
|
.option('--ai', 'Enable AI-powered analysis')
|
|
73
73
|
.option('--no-ai', 'Force disable AI analysis')
|
|
74
|
-
.option('--max-semantic-files <number>', 'Symbol table file limit for TypeScript analysis (default:
|
|
74
|
+
.option('--max-semantic-files <number>', 'Symbol table file limit for TypeScript analysis (default: 1000, -1 for unlimited)')
|
|
75
75
|
.option('--list-engines', 'List available analysis engines');
|
|
76
76
|
|
|
77
77
|
// ESLint Integration options
|
package/core/semantic-engine.js
CHANGED
|
@@ -102,7 +102,7 @@ class SemanticEngine {
|
|
|
102
102
|
);
|
|
103
103
|
|
|
104
104
|
if (targetFiles) {
|
|
105
|
-
console.log(`🎯 Targeted files received: ${targetFiles.length} total, ${semanticFiles.length} TS/JS
|
|
105
|
+
console.log(`🎯 Targeted files received: ${targetFiles.length} total, ${semanticFiles.length} TypeScript/JavaScript files (TS/TSX/JS/JSX)`);
|
|
106
106
|
if (semanticFiles.length < 10) {
|
|
107
107
|
console.log(` Files: ${semanticFiles.map(f => path.basename(f)).join(', ')}`);
|
|
108
108
|
}
|
|
@@ -119,7 +119,9 @@ class SemanticEngine {
|
|
|
119
119
|
} else if (userMaxFiles === 0) {
|
|
120
120
|
// Disable semantic analysis
|
|
121
121
|
maxFiles = 0;
|
|
122
|
-
console.log(`🔧 Semantic Engine config: DISABLED semantic analysis (heuristic only)`);
|
|
122
|
+
console.log(`🔧 Semantic Engine config: DISABLED semantic analysis (heuristic only mode)`);
|
|
123
|
+
console.log(` 💡 Semantic analysis explicitly disabled with --max-semantic-files=0`);
|
|
124
|
+
console.log(` 💡 To enable: omit the option (default: 1000) or use --max-semantic-files=1000 (or higher)`);
|
|
123
125
|
} else if (userMaxFiles > 0) {
|
|
124
126
|
// User-specified limit
|
|
125
127
|
maxFiles = Math.min(userMaxFiles, semanticFiles.length);
|
package/package.json
CHANGED
|
@@ -48,6 +48,14 @@ class C024SymbolBasedAnalyzer {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
try {
|
|
51
|
+
// skip ignored files
|
|
52
|
+
if (this.isIgnoredFile(filePath)) {
|
|
53
|
+
if (verbose) {
|
|
54
|
+
console.log(`🔍 [C024 Symbol-Based] Skipping ignored file: ${filePath}`);
|
|
55
|
+
}
|
|
56
|
+
return violations;
|
|
57
|
+
}
|
|
58
|
+
|
|
51
59
|
const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
52
60
|
if (!sourceFile) {
|
|
53
61
|
return violations;
|
|
@@ -99,9 +107,7 @@ class C024SymbolBasedAnalyzer {
|
|
|
99
107
|
const kind = node.getKind();
|
|
100
108
|
if (
|
|
101
109
|
kind === SyntaxKind.StringLiteral ||
|
|
102
|
-
kind === SyntaxKind.NumericLiteral
|
|
103
|
-
kind === SyntaxKind.TrueKeyword ||
|
|
104
|
-
kind === SyntaxKind.FalseKeyword
|
|
110
|
+
kind === SyntaxKind.NumericLiteral
|
|
105
111
|
) {
|
|
106
112
|
const text = node.getText().replace(/['"`]/g, ""); // strip quotes
|
|
107
113
|
if (this.isAllowedLiteral(node, text)) return;
|
|
@@ -120,9 +126,27 @@ class C024SymbolBasedAnalyzer {
|
|
|
120
126
|
const kind = node.getKind();
|
|
121
127
|
if (kind === SyntaxKind.VariableDeclaration) {
|
|
122
128
|
const parentKind = node.getParent()?.getKind();
|
|
129
|
+
// Skip detection for `for ... of` loop variable
|
|
130
|
+
const loopAncestor = node.getFirstAncestor((ancestor) => {
|
|
131
|
+
const kind = ancestor.getKind?.();
|
|
132
|
+
return (
|
|
133
|
+
kind === SyntaxKind.ForOfStatement ||
|
|
134
|
+
kind === SyntaxKind.ForInStatement ||
|
|
135
|
+
kind === SyntaxKind.ForStatement ||
|
|
136
|
+
kind === SyntaxKind.WhileStatement ||
|
|
137
|
+
kind === SyntaxKind.DoStatement ||
|
|
138
|
+
kind === SyntaxKind.SwitchStatement
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (loopAncestor) {
|
|
143
|
+
return; // skip for all loop/switch contexts, no matter how nested
|
|
144
|
+
}
|
|
145
|
+
|
|
123
146
|
if (
|
|
124
147
|
parentKind === SyntaxKind.VariableDeclarationList &&
|
|
125
|
-
node.getParent().getDeclarationKind() === "const"
|
|
148
|
+
node.getParent().getDeclarationKind() === "const" &&
|
|
149
|
+
!node.getInitializer()
|
|
126
150
|
) {
|
|
127
151
|
this.pushViolation(
|
|
128
152
|
violations,
|
|
@@ -154,27 +178,109 @@ class C024SymbolBasedAnalyzer {
|
|
|
154
178
|
|
|
155
179
|
// --- helper: allow safe literals ---
|
|
156
180
|
isAllowedLiteral(node, text) {
|
|
157
|
-
|
|
158
|
-
|
|
181
|
+
const parent = node.getParent();
|
|
182
|
+
|
|
183
|
+
// 1 Skip imports/exports
|
|
184
|
+
if (parent?.getKind() === SyntaxKind.ImportDeclaration) return true;
|
|
185
|
+
if (parent?.getKind() === SyntaxKind.ExportDeclaration) return true;
|
|
186
|
+
|
|
187
|
+
// 2 Skip literals that are inside call expressions (direct or nested)
|
|
188
|
+
if (
|
|
189
|
+
parent?.getKind() === SyntaxKind.CallExpression ||
|
|
190
|
+
parent?.getFirstAncestorByKind(SyntaxKind.CallExpression)
|
|
191
|
+
) {
|
|
159
192
|
return true;
|
|
160
193
|
}
|
|
161
194
|
|
|
162
|
-
|
|
195
|
+
if (
|
|
196
|
+
parent?.getKind() === SyntaxKind.ElementAccessExpression &&
|
|
197
|
+
parent.getArgumentExpression?.() === node
|
|
198
|
+
) {
|
|
199
|
+
return true; // skip array/object key
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 3 Allow short strings
|
|
163
203
|
if (typeof text === "string" && text.length <= 1) return true;
|
|
164
204
|
|
|
165
|
-
//
|
|
205
|
+
// 4 Allow sentinel numbers
|
|
166
206
|
if (text === "0" || text === "1" || text === "-1") return true;
|
|
167
207
|
|
|
168
|
-
//
|
|
208
|
+
// 5 Allow known safe strings (like "UNKNOWN")
|
|
169
209
|
if (this.safeStrings.includes(text)) return true;
|
|
170
210
|
|
|
211
|
+
// 6 Allow SQL-style placeholders (:variable) inside string/template
|
|
212
|
+
if (typeof text === "string" && /:\w+/.test(text)) {
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
|
|
171
216
|
return false;
|
|
172
217
|
}
|
|
173
218
|
|
|
174
219
|
// helper to check if file is a constants file
|
|
175
220
|
isConstantsFile(filePath) {
|
|
176
221
|
const lower = filePath.toLowerCase();
|
|
177
|
-
|
|
222
|
+
|
|
223
|
+
// common suffixes/patterns for utility or structural files
|
|
224
|
+
const ignoredSuffixes = [
|
|
225
|
+
".constants.ts",
|
|
226
|
+
".const.ts",
|
|
227
|
+
".enum.ts",
|
|
228
|
+
".interface.ts",
|
|
229
|
+
".response.ts",
|
|
230
|
+
".request.ts",
|
|
231
|
+
".res.ts",
|
|
232
|
+
".req.ts",
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
// 1 direct suffix match
|
|
236
|
+
if (ignoredSuffixes.some(suffix => lower.endsWith(suffix))) {
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 2 matches dto.xxx.ts (multi-dot dto files)
|
|
241
|
+
if (/\.dto\.[^.]+\.ts$/.test(lower)) {
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 3 matches folder-based conventions
|
|
246
|
+
if (
|
|
247
|
+
lower.includes("/constants/") ||
|
|
248
|
+
lower.includes("/enums/") ||
|
|
249
|
+
lower.includes("/interfaces/")
|
|
250
|
+
) {
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
isIgnoredFile(filePath) {
|
|
259
|
+
const ignoredPatterns = [
|
|
260
|
+
/\.test\./i,
|
|
261
|
+
/\.tests\./i,
|
|
262
|
+
/\.spec\./i,
|
|
263
|
+
/\.mock\./i,
|
|
264
|
+
/\.css$/i,
|
|
265
|
+
/\.scss$/i,
|
|
266
|
+
/\.html$/i,
|
|
267
|
+
/\.json$/i,
|
|
268
|
+
/\.md$/i,
|
|
269
|
+
/\.svg$/i,
|
|
270
|
+
/\.png$/i,
|
|
271
|
+
/\.jpg$/i,
|
|
272
|
+
/\.jpeg$/i,
|
|
273
|
+
/\.gif$/i,
|
|
274
|
+
/\.bmp$/i,
|
|
275
|
+
/\.ico$/i,
|
|
276
|
+
/\.lock$/i,
|
|
277
|
+
/\.log$/i,
|
|
278
|
+
/\/test\//i,
|
|
279
|
+
/\/tests\//i,
|
|
280
|
+
/\/spec\//i
|
|
281
|
+
];
|
|
282
|
+
|
|
283
|
+
return ignoredPatterns.some((regex) => regex.test(filePath));
|
|
178
284
|
}
|
|
179
285
|
}
|
|
180
286
|
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C060 Main Analyzer - Do not override superclass methods and ignore critical logic
|
|
3
|
+
* Primary: Preserve important behavior or lifecycle logic defined in the superclass to ensure correctness and prevent silent errors.
|
|
4
|
+
* Fallback: Regex-based for all other cases
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const C060SymbolBasedAnalyzer = require('./symbol-based-analyzer');
|
|
8
|
+
|
|
9
|
+
class C060Analyzer {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
12
|
+
console.log(`🔧 [C060] Constructor called with options:`, !!options);
|
|
13
|
+
console.log(`🔧 [C060] Options type:`, typeof options, Object.keys(options || {}));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
this.ruleId = 'C060';
|
|
17
|
+
this.ruleName = 'Do not override superclass methods and ignore critical logic';
|
|
18
|
+
this.description = 'Preserve important behavior or lifecycle logic defined in the superclass to ensure correctness and prevent silent errors.';
|
|
19
|
+
this.semanticEngine = options.semanticEngine || null;
|
|
20
|
+
this.verbose = options.verbose || false;
|
|
21
|
+
|
|
22
|
+
// Configuration
|
|
23
|
+
this.config = {
|
|
24
|
+
useSymbolBased: true, // Primary approach
|
|
25
|
+
fallbackToRegex: false, // Only when symbol fails completely
|
|
26
|
+
symbolBasedOnly: false // Can be set to true for pure mode
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Initialize both analyzers
|
|
30
|
+
try {
|
|
31
|
+
this.symbolAnalyzer = new C060SymbolBasedAnalyzer(this.semanticEngine);
|
|
32
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
33
|
+
console.log(`🔧 [C060] Symbol analyzer created successfully`);
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error(`🔧 [C060] Error creating symbol analyzer:`, error);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Initialize with semantic engine
|
|
42
|
+
*/
|
|
43
|
+
async initialize(semanticEngine = null) {
|
|
44
|
+
if (semanticEngine) {
|
|
45
|
+
this.semanticEngine = semanticEngine;
|
|
46
|
+
}
|
|
47
|
+
this.verbose = semanticEngine?.verbose || false;
|
|
48
|
+
|
|
49
|
+
// Initialize both analyzers
|
|
50
|
+
await this.symbolAnalyzer.initialize(semanticEngine);
|
|
51
|
+
|
|
52
|
+
// Ensure verbose flag is propagated
|
|
53
|
+
this.symbolAnalyzer.verbose = this.verbose;
|
|
54
|
+
|
|
55
|
+
if (this.verbose) {
|
|
56
|
+
console.log(`🔧 [C060 Hybrid] Analyzer initialized - verbose: ${this.verbose}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async analyze(files, language, options = {}) {
|
|
61
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
62
|
+
console.log(`🔧 [C060] analyze() method called with ${files.length} files, language: ${language}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const violations = [];
|
|
66
|
+
|
|
67
|
+
for (const filePath of files) {
|
|
68
|
+
try {
|
|
69
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
70
|
+
console.log(`🔧 [C060] Processing file: ${filePath}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const fileViolations = await this.analyzeFile(filePath, options);
|
|
74
|
+
violations.push(...fileViolations);
|
|
75
|
+
|
|
76
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
77
|
+
console.log(`🔧 [C060] File ${filePath}: Found ${fileViolations.length} violations`);
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.warn(`❌ [C060] Analysis failed for ${filePath}:`, error.message);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
85
|
+
console.log(`🔧 [C060] Total violations found: ${violations.length}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return violations;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async analyzeFile(filePath, options = {}) {
|
|
92
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
93
|
+
console.log(`🔧 [C060] analyzeFile() called for: ${filePath}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 1. Try Symbol-based analysis first (primary)
|
|
97
|
+
if (this.config.useSymbolBased &&
|
|
98
|
+
this.semanticEngine?.project &&
|
|
99
|
+
this.semanticEngine?.initialized) {
|
|
100
|
+
try {
|
|
101
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
102
|
+
console.log(`🔧 [C060] Trying symbol-based analysis...`);
|
|
103
|
+
}
|
|
104
|
+
const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
105
|
+
if (sourceFile) {
|
|
106
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
107
|
+
console.log(`🔧 [C060] Source file found, analyzing with symbol-based...`);
|
|
108
|
+
}
|
|
109
|
+
const violations = await this.symbolAnalyzer.analyzeFileWithSymbols(filePath, { ...options, verbose: options.verbose });
|
|
110
|
+
|
|
111
|
+
// Mark violations with analysis strategy
|
|
112
|
+
violations.forEach(v => v.analysisStrategy = 'symbol-based');
|
|
113
|
+
|
|
114
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
115
|
+
console.log(`✅ [C060] Symbol-based analysis: ${violations.length} violations`);
|
|
116
|
+
}
|
|
117
|
+
return violations; // Return even if 0 violations - symbol analysis completed successfully
|
|
118
|
+
} else {
|
|
119
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
120
|
+
console.log(`⚠️ [C060] Source file not found in project`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.warn(`⚠️ [C060] Symbol analysis failed: ${error.message}`);
|
|
125
|
+
// Continue to fallback
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
129
|
+
console.log(`🔄 [C060] Symbol analysis conditions check:`);
|
|
130
|
+
console.log(` - useSymbolBased: ${this.config.useSymbolBased}`);
|
|
131
|
+
console.log(` - semanticEngine: ${!!this.semanticEngine}`);
|
|
132
|
+
console.log(` - semanticEngine.project: ${!!this.semanticEngine?.project}`);
|
|
133
|
+
console.log(` - semanticEngine.initialized: ${this.semanticEngine?.initialized}`);
|
|
134
|
+
console.log(`🔄 [C060] Symbol analysis unavailable, using regex fallback`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (options?.verbose) {
|
|
139
|
+
console.log(`🔧 [C060] No analysis methods succeeded, returning empty`);
|
|
140
|
+
}
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async analyzeFileBasic(filePath, options = {}) {
|
|
145
|
+
console.log(`🔧 [C060] analyzeFileBasic() called for: ${filePath}`);
|
|
146
|
+
console.log(`🔧 [C060] semanticEngine exists: ${!!this.semanticEngine}`);
|
|
147
|
+
console.log(`🔧 [C060] symbolAnalyzer exists: ${!!this.symbolAnalyzer}`);
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
// Try symbol-based analysis first
|
|
151
|
+
if (this.semanticEngine?.isSymbolEngineReady?.() &&
|
|
152
|
+
this.semanticEngine.project) {
|
|
153
|
+
|
|
154
|
+
if (this.verbose) {
|
|
155
|
+
console.log(`🔍 [C060] Using symbol-based analysis for ${filePath}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const violations = await this.symbolAnalyzer.analyzeFileBasic(filePath, options);
|
|
159
|
+
return violations;
|
|
160
|
+
}
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (this.verbose) {
|
|
163
|
+
console.warn(`⚠️ [C060] Symbol analysis failed: ${error.message}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Methods for compatibility with different engine invocation patterns
|
|
170
|
+
*/
|
|
171
|
+
async analyzeFileWithSymbols(filePath, options = {}) {
|
|
172
|
+
return this.analyzeFile(filePath, options);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async analyzeWithSemantics(filePath, options = {}) {
|
|
176
|
+
return this.analyzeFile(filePath, options);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = C060Analyzer;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "C060",
|
|
3
|
+
"name": "C060_do_not_override_superclass_methods_and_ignore_critical_logic",
|
|
4
|
+
"category": "architecture",
|
|
5
|
+
"description": "C060 - Do not override superclass methods and ignore critical logic",
|
|
6
|
+
"severity": "warning",
|
|
7
|
+
"enabled": true,
|
|
8
|
+
"semantic": {
|
|
9
|
+
"enabled": true,
|
|
10
|
+
"priority": "high",
|
|
11
|
+
"fallback": "heuristic"
|
|
12
|
+
},
|
|
13
|
+
"patterns": {
|
|
14
|
+
"include": [
|
|
15
|
+
"**/*.js",
|
|
16
|
+
"**/*.ts",
|
|
17
|
+
"**/*.jsx",
|
|
18
|
+
"**/*.tsx"
|
|
19
|
+
],
|
|
20
|
+
"exclude": [
|
|
21
|
+
"**/*.test.*",
|
|
22
|
+
"**/*.spec.*",
|
|
23
|
+
"**/*.mock.*",
|
|
24
|
+
"**/test/**",
|
|
25
|
+
"**/tests/**",
|
|
26
|
+
"**/spec/**"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
"options": {
|
|
30
|
+
"strictMode": false,
|
|
31
|
+
"allowedDbMethods": [],
|
|
32
|
+
"repositoryPatterns": [
|
|
33
|
+
"*Repository*",
|
|
34
|
+
"*Repo*",
|
|
35
|
+
"*DAO*",
|
|
36
|
+
"*Store*"
|
|
37
|
+
],
|
|
38
|
+
"servicePatterns": [
|
|
39
|
+
"*Service*",
|
|
40
|
+
"*UseCase*",
|
|
41
|
+
"*Handler*",
|
|
42
|
+
"*Manager*"
|
|
43
|
+
],
|
|
44
|
+
"complexityThreshold": {
|
|
45
|
+
"methodLength": 200,
|
|
46
|
+
"cyclomaticComplexity": 5,
|
|
47
|
+
"nestedDepth": 3
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C060 Symbol-based Analyzer - Advanced Do not scatter hardcoded constants throughout the logic
|
|
3
|
+
* Purpose: The rule prevents scattering hardcoded constants throughout the logic. Instead, constants should be defined in a single place to improve maintainability and readability.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { SyntaxKind } = require('ts-morph');
|
|
7
|
+
|
|
8
|
+
class C060SymbolBasedAnalyzer {
|
|
9
|
+
constructor(semanticEngine = null) {
|
|
10
|
+
this.ruleId = 'C060';
|
|
11
|
+
this.ruleName = 'Do not override superclass methods (Symbol-Based)';
|
|
12
|
+
this.semanticEngine = semanticEngine;
|
|
13
|
+
this.verbose = false;
|
|
14
|
+
|
|
15
|
+
// === Ignore Configuration ===
|
|
16
|
+
this.ignoredClasses = [
|
|
17
|
+
"React.Component", // React components
|
|
18
|
+
"React.PureComponent",
|
|
19
|
+
"BaseMock", // Custom test mocks
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
this.ignoredMethods = [
|
|
23
|
+
"render", // React render
|
|
24
|
+
"componentDidMount", // Some lifecycle hooks
|
|
25
|
+
"componentWillUnmount",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
this.ignoredFilePatterns = [
|
|
29
|
+
/node_modules/,
|
|
30
|
+
/\.d\.ts$/,
|
|
31
|
+
/\.test\.ts$/,
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async initialize(semanticEngine = null) {
|
|
36
|
+
if (semanticEngine) {
|
|
37
|
+
this.semanticEngine = semanticEngine;
|
|
38
|
+
}
|
|
39
|
+
this.verbose = semanticEngine?.verbose || false;
|
|
40
|
+
|
|
41
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
42
|
+
console.log(`🔧 [C060 Symbol-Based] Analyzer initialized, verbose: ${this.verbose}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async analyzeFileBasic(filePath, options = {}) {
|
|
47
|
+
// This is the main entry point called by the hybrid analyzer
|
|
48
|
+
return await this.analyzeFileWithSymbols(filePath, options);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async analyzeFileWithSymbols(filePath, options = {}) {
|
|
52
|
+
const violations = [];
|
|
53
|
+
|
|
54
|
+
// Enable verbose mode if requested
|
|
55
|
+
const verbose = options.verbose || this.verbose;
|
|
56
|
+
|
|
57
|
+
if (!this.semanticEngine?.project) {
|
|
58
|
+
if (verbose) {
|
|
59
|
+
console.warn('[C060 Symbol-Based] No semantic engine available, skipping analysis');
|
|
60
|
+
}
|
|
61
|
+
return violations;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (this.shouldIgnoreFile(filePath)) {
|
|
65
|
+
if (verbose) console.log(`[${this.ruleId}] Ignoring ${filePath}`);
|
|
66
|
+
return violations;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (verbose) {
|
|
70
|
+
console.log(`🔍 [C060 Symbol-Based] Starting analysis for ${filePath}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
75
|
+
if (!sourceFile) {
|
|
76
|
+
return violations;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const classDeclarations = sourceFile.getClasses();
|
|
80
|
+
for (const classDeclaration of classDeclarations) {
|
|
81
|
+
const classViolations = this.analyzeClass(classDeclaration, filePath, verbose);
|
|
82
|
+
violations.push(...classViolations);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (verbose) {
|
|
86
|
+
console.log(`🔍 [C060 Symbol-Based] Total violations found: ${violations.length}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return violations;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (verbose) {
|
|
92
|
+
console.warn(`[C060 Symbol-Based] Analysis failed for ${filePath}:`, error.message);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return violations;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Analyze one class for override violations
|
|
101
|
+
*/
|
|
102
|
+
analyzeClass(classDeclaration, filePath, verbose) {
|
|
103
|
+
const violations = [];
|
|
104
|
+
const baseClass = classDeclaration.getBaseClass();
|
|
105
|
+
if (!baseClass) return violations;
|
|
106
|
+
|
|
107
|
+
// Check if this class should be ignored
|
|
108
|
+
if (this.shouldIgnoreClass(baseClass)) {
|
|
109
|
+
if (verbose) {
|
|
110
|
+
console.log(`[${this.ruleId}] Skipping ignored base class: ${baseClass.getName()}`);
|
|
111
|
+
}
|
|
112
|
+
return violations;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const baseMethods = this.collectBaseMethods(baseClass);
|
|
116
|
+
|
|
117
|
+
for (const method of classDeclaration.getMethods()) {
|
|
118
|
+
// Skip ignored methods
|
|
119
|
+
if (this.shouldIgnoreMethod(method)) {
|
|
120
|
+
if (verbose) {
|
|
121
|
+
console.log(`[${this.ruleId}] Skipping ignored method: ${method.getName()}`);
|
|
122
|
+
}
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (baseMethods.has(method.getName())) {
|
|
127
|
+
const violation = this.checkMethodOverride(method, classDeclaration, filePath, verbose);
|
|
128
|
+
if (violation) {
|
|
129
|
+
violations.push(violation);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return violations;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Collect all method names from base class
|
|
139
|
+
*/
|
|
140
|
+
collectBaseMethods(baseClass) {
|
|
141
|
+
return new Set(baseClass.getMethods().map((method) => method.getName()));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Check if overridden method properly calls super.method()
|
|
146
|
+
*/
|
|
147
|
+
checkMethodOverride(method, classDeclaration, filePath, verbose) {
|
|
148
|
+
const body = method.getBodyText();
|
|
149
|
+
if (!body) return null;
|
|
150
|
+
|
|
151
|
+
const methodName = method.getName();
|
|
152
|
+
const baseClass = classDeclaration.getBaseClass();
|
|
153
|
+
if (!baseClass) return null;
|
|
154
|
+
const baseClassName = baseClass.getName();
|
|
155
|
+
|
|
156
|
+
// 1 Get all CallExpressions in the method body
|
|
157
|
+
const calls = method.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
158
|
+
|
|
159
|
+
// 2 Check for super.method() or BaseClass.prototype.method.call(this)
|
|
160
|
+
const hasSuperCall = calls.some((call) => {
|
|
161
|
+
const expression = call.getExpression().getText();
|
|
162
|
+
return expression === `super.${methodName}`;
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const hasBaseCall = calls.some((call) => {
|
|
166
|
+
const expression = call.getExpression().getText();
|
|
167
|
+
return expression === `${baseClassName}.prototype.${methodName}.call`;
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (!hasSuperCall && !hasBaseCall) {
|
|
171
|
+
const violation = {
|
|
172
|
+
ruleId: this.ruleId,
|
|
173
|
+
severity: 'warning',
|
|
174
|
+
message: `Overridden method '${method.getName()}' in '${classDeclaration.getName()}' does not call 'super.${method.getName()}()', potentially skipping lifecycle/resource logic.`,
|
|
175
|
+
source: this.ruleId,
|
|
176
|
+
file: filePath,
|
|
177
|
+
line: method.getStartLineNumber(),
|
|
178
|
+
column: method.getStart() - method.getStartLinePos(),
|
|
179
|
+
description: `[SYMBOL-BASED] Overriding methods should call the superclass implementation to ensure proper behavior.`,
|
|
180
|
+
suggestion: `Add a call to 'super.${method.getName()}()' within the method body.`,
|
|
181
|
+
category: 'best-practices',
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
if (verbose) {
|
|
185
|
+
console.log(
|
|
186
|
+
`⚠️ [${this.ruleId}] Violation in ${filePath}: Method '${method.getName()}' overrides superclass method but does not call super.`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return violation;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Ignore logic for classes
|
|
198
|
+
*/
|
|
199
|
+
shouldIgnoreClass(baseClass) {
|
|
200
|
+
const baseName = baseClass.getName();
|
|
201
|
+
return this.ignoredClasses.includes(baseName);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Ignore logic for methods
|
|
206
|
+
*/
|
|
207
|
+
shouldIgnoreMethod(method) {
|
|
208
|
+
const methodName = method.getName();
|
|
209
|
+
return this.ignoredMethods.includes(methodName);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Ignore files based on patterns
|
|
214
|
+
*/
|
|
215
|
+
shouldIgnoreFile(filePath) {
|
|
216
|
+
return this.ignoredFilePatterns.some((pattern) => pattern.test(filePath));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = C060SymbolBasedAnalyzer;
|
package/rules/index.js
CHANGED
|
@@ -64,6 +64,7 @@ const commonRules = {
|
|
|
64
64
|
C048: loadRule('common', 'C048_no_bypass_architectural_layers'),
|
|
65
65
|
C052: loadRule('common', 'C052_parsing_or_data_transformation'),
|
|
66
66
|
C047: loadRule('common', 'C047_no_duplicate_retry_logic'),
|
|
67
|
+
C060: loadRule('common', 'C060_no_override_superclass'),
|
|
67
68
|
};
|
|
68
69
|
|
|
69
70
|
// 🔒 Security Rules (S-series) - Ready for migration
|