@sun-asterisk/sunlint 1.2.2 → 1.3.1
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/CHANGELOG.md +107 -1
- package/CONTRIBUTING.md +1654 -66
- package/README.md +19 -6
- package/config/ci-cd.json +54 -0
- package/config/development.json +56 -0
- package/config/engines/engines-enhanced.json +86 -0
- package/config/engines/semantic-config.json +114 -0
- package/config/eslint-rule-mapping.json +50 -38
- package/config/large-project.json +143 -0
- package/config/presets/all.json +0 -1
- package/config/release.json +70 -0
- package/config/rule-analysis-strategies.js +23 -4
- package/config/rules/S027-categories.json +122 -0
- package/config/rules/enhanced-rules-registry.json +2564 -0
- package/config/rules/rules-registry-generated.json +785 -837
- package/config/rules/rules-registry.json +13 -1
- package/core/adapters/sunlint-rule-adapter.js +25 -30
- package/core/analysis-orchestrator.js +42 -2
- package/core/categories.js +52 -0
- package/core/category-constants.js +39 -0
- package/core/cli-action-handler.js +53 -32
- package/core/cli-program.js +11 -3
- package/core/config-manager.js +111 -0
- package/core/config-merger.js +88 -0
- package/core/constants/categories.js +168 -0
- package/core/constants/defaults.js +165 -0
- package/core/constants/engines.js +185 -0
- package/core/constants/index.js +30 -0
- package/core/constants/rules.js +215 -0
- package/core/enhanced-rules-registry.js +3 -3
- package/core/file-targeting-service.js +128 -7
- package/core/interfaces/rule-plugin.interface.js +207 -0
- package/core/plugin-manager.js +448 -0
- package/core/rule-selection-service.js +42 -15
- package/core/semantic-engine.js +658 -0
- package/core/semantic-rule-base.js +433 -0
- package/core/unified-rule-registry.js +484 -0
- package/docs/COMMAND-EXAMPLES.md +134 -0
- package/docs/CONSTANTS-ARCHITECTURE.md +288 -0
- package/docs/LARGE-PROJECT-GUIDE.md +324 -0
- package/engines/core/base-engine.js +249 -0
- package/engines/engine-factory.js +275 -0
- package/engines/eslint-engine.js +171 -19
- package/engines/heuristic-engine.js +569 -78
- package/integrations/eslint/plugin/index.js +26 -28
- package/origin-rules/common-en.md +8 -8
- package/package.json +10 -6
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +1 -1
- package/rules/common/C017_constructor_logic/analyzer.js +254 -17
- package/rules/common/C017_constructor_logic/semantic-analyzer.js +340 -0
- package/rules/common/C029_catch_block_logging/analyzer.js +17 -5
- package/rules/common/C033_separate_service_repository/README.md +78 -0
- package/rules/common/C033_separate_service_repository/analyzer.js +160 -0
- package/rules/common/C033_separate_service_repository/config.json +50 -0
- package/rules/common/C033_separate_service_repository/regex-based-analyzer.js +585 -0
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +368 -0
- package/rules/common/C035_error_logging_context/STRATEGY.md +99 -0
- package/rules/common/C035_error_logging_context/analyzer.js +230 -0
- package/rules/common/C035_error_logging_context/config.json +54 -0
- package/rules/common/C035_error_logging_context/regex-based-analyzer.js +299 -0
- package/rules/common/C035_error_logging_context/symbol-based-analyzer.js +454 -0
- package/rules/common/C040_centralized_validation/analyzer.js +165 -0
- package/rules/common/C040_centralized_validation/config.json +46 -0
- package/rules/common/C040_centralized_validation/regex-based-analyzer.js +243 -0
- package/rules/common/C040_centralized_validation/symbol-based-analyzer.js +416 -0
- package/rules/common/C047_no_duplicate_retry_logic/c047-semantic-rule.js +278 -0
- package/rules/common/C047_no_duplicate_retry_logic/symbol-analyzer-enhanced.js +968 -0
- package/rules/common/C047_no_duplicate_retry_logic/symbol-config.json +71 -0
- package/rules/common/{C076_single_test_behavior → C072_single_test_behavior}/analyzer.js +6 -6
- package/rules/common/C076_explicit_function_types/README.md +30 -0
- package/rules/common/C076_explicit_function_types/analyzer.js +172 -0
- package/rules/common/C076_explicit_function_types/config.json +15 -0
- package/rules/common/C076_explicit_function_types/semantic-analyzer.js +341 -0
- package/rules/index.js +8 -0
- package/rules/parser/rule-parser.js +13 -2
- package/rules/security/S005_no_origin_auth/README.md +226 -0
- package/rules/security/S005_no_origin_auth/analyzer.js +184 -0
- package/rules/security/S005_no_origin_auth/ast-analyzer.js +406 -0
- package/rules/security/S005_no_origin_auth/config.json +85 -0
- package/rules/security/S006_no_plaintext_recovery_codes/README.md +139 -0
- package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +306 -0
- package/rules/security/S006_no_plaintext_recovery_codes/config.json +48 -0
- package/rules/security/S007_no_plaintext_otp/README.md +198 -0
- package/rules/security/S007_no_plaintext_otp/analyzer.js +406 -0
- package/rules/security/S007_no_plaintext_otp/config.json +79 -0
- package/rules/security/S007_no_plaintext_otp/semantic-analyzer.js +609 -0
- package/rules/security/S007_no_plaintext_otp/semantic-config.json +195 -0
- package/rules/security/S007_no_plaintext_otp/semantic-wrapper.js +280 -0
- package/rules/security/S027_no_hardcoded_secrets/analyzer.js +180 -366
- package/rules/security/S027_no_hardcoded_secrets/categories.json +153 -0
- package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +250 -0
- package/scripts/category-manager.js +150 -0
- package/scripts/generate-rules-registry.js +88 -0
- package/scripts/migrate-rule-registry.js +157 -0
- package/scripts/prepare-release.sh +1 -1
- package/scripts/validate-system.js +48 -0
- package/.sunlint.json +0 -35
- package/config/README.md +0 -88
- package/config/engines/eslint-rule-mapping.json +0 -74
- package/config/schemas/sunlint-schema.json +0 -0
- package/config/testing/test-s005-working.ts +0 -22
- package/core/multi-rule-runner.js +0 -0
- package/docs/ESLINT-INTEGRATION-STRATEGY.md +0 -392
- package/docs/FUTURE_PACKAGES.md +0 -83
- package/docs/HEURISTIC_VS_AI.md +0 -113
- package/docs/PRODUCTION_DEPLOYMENT_ANALYSIS.md +0 -112
- package/docs/PRODUCTION_SIZE_IMPACT.md +0 -183
- package/docs/RELEASE_GUIDE.md +0 -230
- package/docs/STANDARDIZED-CATEGORY-FILTERING.md +0 -156
- package/engines/tree-sitter-parser.js +0 -0
- package/engines/universal-ast-engine.js +0 -0
- package/integrations/eslint/plugin/rules/common/c076-single-behavior-per-test.js +0 -254
- package/rules/common/C029_catch_block_logging/analyzer-backup.js +0 -426
- package/rules/common/C029_catch_block_logging/analyzer-fixed.js +0 -130
- package/rules/common/C029_catch_block_logging/analyzer-multi-tech.js +0 -487
- package/rules/common/C029_catch_block_logging/analyzer-simple.js +0 -110
- package/rules/common/C029_catch_block_logging/ast-analyzer-backup.js +0 -441
- package/rules/common/C029_catch_block_logging/ast-analyzer-new.js +0 -127
- package/rules/common/C029_catch_block_logging/ast-analyzer.js +0 -133
- package/rules/common/C029_catch_block_logging/cfg-analyzer.js +0 -408
- package/rules/common/C029_catch_block_logging/dataflow-analyzer.js +0 -454
- package/rules/common/C029_catch_block_logging/multi-language-ast-engine.js +0 -700
- package/rules/common/C029_catch_block_logging/pattern-learning-analyzer.js +0 -568
- package/rules/common/C029_catch_block_logging/semantic-analyzer.js +0 -459
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symbol-based analyzer for C033 - Advanced semantic analysis
|
|
3
|
+
* Purpose: Use AST + Data Flow to detect true database access violations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class C033SymbolBasedAnalyzer {
|
|
7
|
+
constructor(semanticEngine = null) {
|
|
8
|
+
this.ruleId = 'C033';
|
|
9
|
+
this.ruleName = 'Separate Service and Repository Logic (Symbol-Based)';
|
|
10
|
+
this.semanticEngine = semanticEngine;
|
|
11
|
+
this.verbose = false;
|
|
12
|
+
|
|
13
|
+
// Known database/ORM symbols and interfaces
|
|
14
|
+
this.databaseSymbols = [
|
|
15
|
+
'Repository', 'EntityManager', 'QueryBuilder', 'Connection',
|
|
16
|
+
'PrismaClient', 'Model', 'Collection', 'Table'
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
// ORM framework patterns
|
|
20
|
+
this.ormPatterns = {
|
|
21
|
+
typeorm: ['Repository', 'EntityManager', 'QueryRunner', 'QueryBuilder'],
|
|
22
|
+
prisma: ['PrismaClient', 'PrismaService'],
|
|
23
|
+
mongoose: ['Model', 'Document', 'Schema'],
|
|
24
|
+
sequelize: ['Model', 'Sequelize', 'QueryInterface'],
|
|
25
|
+
knex: ['Knex', 'QueryBuilder']
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async initialize(semanticEngine = null) {
|
|
30
|
+
if (semanticEngine) {
|
|
31
|
+
this.semanticEngine = semanticEngine;
|
|
32
|
+
}
|
|
33
|
+
this.verbose = semanticEngine?.verbose || false;
|
|
34
|
+
|
|
35
|
+
if (this.verbose) {
|
|
36
|
+
console.log(`[DEBUG] 🔧 C033 Symbol-Based: Analyzer initialized`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async analyze(files, language, options = {}) {
|
|
41
|
+
const violations = [];
|
|
42
|
+
|
|
43
|
+
if (!this.semanticEngine?.project) {
|
|
44
|
+
if (this.verbose) {
|
|
45
|
+
console.warn('[C033 Symbol-Based] No semantic engine available, skipping analysis');
|
|
46
|
+
}
|
|
47
|
+
return violations;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const filePath of files) {
|
|
51
|
+
try {
|
|
52
|
+
const fileViolations = await this.analyzeFileWithSymbols(filePath, options);
|
|
53
|
+
violations.push(...fileViolations);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (this.verbose) {
|
|
56
|
+
console.warn(`[C033 Symbol-Based] Analysis failed for ${filePath}:`, error.message);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return violations;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async analyzeFileWithSymbols(filePath, options = {}) {
|
|
65
|
+
const violations = [];
|
|
66
|
+
const sourceFile = this.semanticEngine.project.getSourceFileByFilePath(filePath);
|
|
67
|
+
|
|
68
|
+
if (!sourceFile) {
|
|
69
|
+
return violations;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 1. Classify file type using semantic analysis
|
|
73
|
+
const fileType = this.classifyFileSemanticType(sourceFile, filePath);
|
|
74
|
+
|
|
75
|
+
if (fileType !== 'service') {
|
|
76
|
+
return violations; // Only analyze Service files
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 2. Analyze call expressions in Service classes
|
|
80
|
+
const classes = sourceFile.getClasses();
|
|
81
|
+
|
|
82
|
+
for (const cls of classes) {
|
|
83
|
+
const className = cls.getName() || 'UnknownClass';
|
|
84
|
+
|
|
85
|
+
// Skip if not a Service class
|
|
86
|
+
if (!this.isServiceClass(cls)) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const methods = cls.getMethods();
|
|
91
|
+
|
|
92
|
+
for (const method of methods) {
|
|
93
|
+
const methodViolations = this.analyzeMethodForDatabaseCalls(
|
|
94
|
+
method, sourceFile, className, filePath
|
|
95
|
+
);
|
|
96
|
+
violations.push(...methodViolations);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return violations;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Analyze method for direct database calls using symbol resolution
|
|
105
|
+
*/
|
|
106
|
+
analyzeMethodForDatabaseCalls(method, sourceFile, className, filePath) {
|
|
107
|
+
const violations = [];
|
|
108
|
+
const methodName = method.getName();
|
|
109
|
+
|
|
110
|
+
// Get all call expressions in the method
|
|
111
|
+
const callExpressions = method.getDescendantsOfKind(this.getKind('CallExpression'));
|
|
112
|
+
|
|
113
|
+
for (const callExpr of callExpressions) {
|
|
114
|
+
const violation = this.analyzeCallExpression(callExpr, sourceFile, className, methodName, filePath);
|
|
115
|
+
if (violation) {
|
|
116
|
+
violations.push(violation);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return violations;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Analyze individual call expression using symbol resolution
|
|
125
|
+
*/
|
|
126
|
+
analyzeCallExpression(callExpr, sourceFile, className, methodName, filePath) {
|
|
127
|
+
const expression = callExpr.getExpression();
|
|
128
|
+
|
|
129
|
+
// Handle property access (obj.method())
|
|
130
|
+
if (expression.getKind() === this.getKind('PropertyAccessExpression')) {
|
|
131
|
+
const propertyAccess = expression;
|
|
132
|
+
const object = propertyAccess.getExpression();
|
|
133
|
+
const property = propertyAccess.getName();
|
|
134
|
+
|
|
135
|
+
// Get the symbol/type of the object being called
|
|
136
|
+
const objectSymbol = this.getObjectSymbol(object);
|
|
137
|
+
|
|
138
|
+
if (this.isDatabaseOperation(objectSymbol, property)) {
|
|
139
|
+
// Check if it's going through repository (acceptable)
|
|
140
|
+
if (this.isRepositoryAccess(objectSymbol)) {
|
|
141
|
+
return null; // OK: Service -> Repository -> Database
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Direct database access in Service (violation)
|
|
145
|
+
const lineNumber = callExpr.getStartLineNumber();
|
|
146
|
+
const columnNumber = callExpr.getStart() - sourceFile.getLineStartPos(lineNumber - 1) + 1;
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
ruleId: this.ruleId,
|
|
150
|
+
severity: 'warning',
|
|
151
|
+
message: `Service should not contain direct database calls`,
|
|
152
|
+
source: this.ruleId,
|
|
153
|
+
file: filePath,
|
|
154
|
+
line: lineNumber,
|
|
155
|
+
column: columnNumber,
|
|
156
|
+
description: `[SYMBOL-BASED] Direct database call '${property}()' on '${objectSymbol?.name || 'unknown'}' found in Service`,
|
|
157
|
+
suggestion: 'Use Repository pattern for data access',
|
|
158
|
+
category: 'architecture'
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get symbol information for an object expression
|
|
168
|
+
*/
|
|
169
|
+
getObjectSymbol(objectExpr) {
|
|
170
|
+
try {
|
|
171
|
+
// Try to get the symbol of the expression
|
|
172
|
+
const symbol = objectExpr.getSymbol();
|
|
173
|
+
if (symbol) {
|
|
174
|
+
return {
|
|
175
|
+
name: symbol.getName(),
|
|
176
|
+
type: this.getSymbolType(symbol),
|
|
177
|
+
isDatabase: this.isSymbolDatabase(symbol)
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Fallback: analyze by text patterns
|
|
182
|
+
const text = objectExpr.getText();
|
|
183
|
+
return {
|
|
184
|
+
name: text,
|
|
185
|
+
type: this.inferTypeFromText(text),
|
|
186
|
+
isDatabase: this.isTextDatabase(text)
|
|
187
|
+
};
|
|
188
|
+
} catch (error) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Check if operation is a database operation
|
|
195
|
+
*/
|
|
196
|
+
isDatabaseOperation(objectSymbol, methodName) {
|
|
197
|
+
if (!objectSymbol) return false;
|
|
198
|
+
|
|
199
|
+
// Exclude queue/job operations (Bull.js, agenda, etc.)
|
|
200
|
+
if (this.isQueueOperation(objectSymbol, methodName)) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Known database method patterns
|
|
205
|
+
const dbMethods = [
|
|
206
|
+
'findOneBy', 'findBy', 'findAndCount', 'findMany', 'findFirst',
|
|
207
|
+
'save', 'insert', 'create', 'upsert',
|
|
208
|
+
'update', 'patch', 'merge', 'set',
|
|
209
|
+
'delete', 'remove', 'destroy',
|
|
210
|
+
'query', 'execute', 'run',
|
|
211
|
+
'createQueryBuilder', 'getRepository'
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
return objectSymbol.isDatabase && dbMethods.includes(methodName);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Check if this is a queue/job operation (should be excluded)
|
|
219
|
+
*/
|
|
220
|
+
isQueueOperation(objectSymbol, methodName) {
|
|
221
|
+
if (!objectSymbol) return false;
|
|
222
|
+
|
|
223
|
+
const queueMethods = [
|
|
224
|
+
'remove', 'isFailed', 'isCompleted', 'isActive', 'isWaiting', 'isDelayed',
|
|
225
|
+
'getJob', 'getJobs', 'add', 'process', 'on', 'off',
|
|
226
|
+
'retry', 'moveToCompleted', 'moveToFailed'
|
|
227
|
+
];
|
|
228
|
+
|
|
229
|
+
const queueTypes = ['queue', 'job', 'bull'];
|
|
230
|
+
const objectName = objectSymbol.name.toLowerCase();
|
|
231
|
+
|
|
232
|
+
// Enhanced detection for Bull.js Job objects
|
|
233
|
+
const isQueueMethod = queueMethods.includes(methodName);
|
|
234
|
+
const isQueueObject = queueTypes.some(type => objectName.includes(type)) ||
|
|
235
|
+
/job/i.test(objectName) ||
|
|
236
|
+
/queue/i.test(objectName);
|
|
237
|
+
|
|
238
|
+
if (this.verbose && (isQueueMethod || isQueueObject)) {
|
|
239
|
+
console.log(`[DEBUG] Queue Check: object="${objectName}", method="${methodName}", isQueue=${isQueueMethod && isQueueObject}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return isQueueMethod && isQueueObject;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Check if access is through repository (acceptable)
|
|
247
|
+
*/
|
|
248
|
+
isRepositoryAccess(objectSymbol) {
|
|
249
|
+
if (!objectSymbol) return false;
|
|
250
|
+
|
|
251
|
+
const name = objectSymbol.name.toLowerCase();
|
|
252
|
+
return name.includes('repository') || name.includes('repo');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Check if symbol represents database object
|
|
257
|
+
*/
|
|
258
|
+
isSymbolDatabase(symbol) {
|
|
259
|
+
try {
|
|
260
|
+
const type = symbol.getType();
|
|
261
|
+
const typeName = type.getSymbol()?.getName() || '';
|
|
262
|
+
|
|
263
|
+
return this.databaseSymbols.some(dbSymbol =>
|
|
264
|
+
typeName.includes(dbSymbol)
|
|
265
|
+
);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Infer type from text patterns (fallback)
|
|
273
|
+
*/
|
|
274
|
+
inferTypeFromText(text) {
|
|
275
|
+
const lowerText = text.toLowerCase();
|
|
276
|
+
|
|
277
|
+
// Check for known database object patterns
|
|
278
|
+
if (/manager|connection|client|prisma/i.test(lowerText)) {
|
|
279
|
+
return 'database';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (/repository|repo/i.test(lowerText)) {
|
|
283
|
+
return 'repository';
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return 'unknown';
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Check if text represents database access
|
|
291
|
+
*/
|
|
292
|
+
isTextDatabase(text) {
|
|
293
|
+
const lowerText = text.toLowerCase();
|
|
294
|
+
return /manager|connection|client|prisma|entitymanager/i.test(lowerText) &&
|
|
295
|
+
!/repository|repo/i.test(lowerText);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Classify file type using semantic analysis
|
|
300
|
+
*/
|
|
301
|
+
classifyFileSemanticType(sourceFile, filePath) {
|
|
302
|
+
const fileName = sourceFile.getBaseName().toLowerCase();
|
|
303
|
+
|
|
304
|
+
// Check filename patterns
|
|
305
|
+
if (/service\.ts$|service\.js$/i.test(fileName)) return 'service';
|
|
306
|
+
if (/repository\.ts$|repository\.js$/i.test(fileName)) return 'repository';
|
|
307
|
+
|
|
308
|
+
// Check class patterns
|
|
309
|
+
const classes = sourceFile.getClasses();
|
|
310
|
+
for (const cls of classes) {
|
|
311
|
+
if (this.isServiceClass(cls)) return 'service';
|
|
312
|
+
if (this.isRepositoryClass(cls)) return 'repository';
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return 'unknown';
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Check if class is a Service class
|
|
320
|
+
*/
|
|
321
|
+
isServiceClass(cls) {
|
|
322
|
+
const className = cls.getName()?.toLowerCase() || '';
|
|
323
|
+
|
|
324
|
+
// Check class name
|
|
325
|
+
if (/service$/.test(className)) return true;
|
|
326
|
+
|
|
327
|
+
// Check decorators
|
|
328
|
+
const decorators = cls.getDecorators();
|
|
329
|
+
return decorators.some(decorator => {
|
|
330
|
+
const decoratorName = decorator.getName().toLowerCase();
|
|
331
|
+
return decoratorName.includes('service') || decoratorName === 'injectable';
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Check if class is a Repository class
|
|
337
|
+
*/
|
|
338
|
+
isRepositoryClass(cls) {
|
|
339
|
+
const className = cls.getName()?.toLowerCase() || '';
|
|
340
|
+
return /repository$|repo$/.test(className);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Get TypeScript SyntaxKind
|
|
345
|
+
*/
|
|
346
|
+
getKind(kindName) {
|
|
347
|
+
try {
|
|
348
|
+
const ts = require('typescript');
|
|
349
|
+
return ts.SyntaxKind[kindName];
|
|
350
|
+
} catch (error) {
|
|
351
|
+
// Fallback for ts-morph
|
|
352
|
+
return this.semanticEngine?.project?.getTypeChecker()?.compilerObject?.SyntaxKind?.[kindName] || 0;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Get symbol type information
|
|
358
|
+
*/
|
|
359
|
+
getSymbolType(symbol) {
|
|
360
|
+
try {
|
|
361
|
+
return symbol.getType().getText();
|
|
362
|
+
} catch (error) {
|
|
363
|
+
return 'unknown';
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
module.exports = C033SymbolBasedAnalyzer;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# C035 Analysis Strategy
|
|
2
|
+
|
|
3
|
+
## Rule Focus: Log Content Quality in Catch Blocks
|
|
4
|
+
|
|
5
|
+
### Detection Pipeline:
|
|
6
|
+
|
|
7
|
+
1. **Find Catch Blocks** (Symbol-based)
|
|
8
|
+
- Use AST to detect try-catch structures
|
|
9
|
+
- Extract catch parameter name (e, error, err, etc.)
|
|
10
|
+
|
|
11
|
+
2. **Locate Log Calls** (Symbol-based)
|
|
12
|
+
- Find all method calls within catch block
|
|
13
|
+
- Use symbol resolution to identify log methods:
|
|
14
|
+
- console.log, console.error, console.warn
|
|
15
|
+
- logger.log, logger.error, logger.warn, logger.info
|
|
16
|
+
- log.error, log.warn, log.info
|
|
17
|
+
- winston, bunyan, pino patterns
|
|
18
|
+
|
|
19
|
+
3. **Analyze Log Content** (AST + String Analysis)
|
|
20
|
+
- Check log call arguments
|
|
21
|
+
- Detect structured vs string logging
|
|
22
|
+
- Validate required context elements
|
|
23
|
+
- Check for sensitive data exposure
|
|
24
|
+
|
|
25
|
+
### Violations to Detect:
|
|
26
|
+
|
|
27
|
+
#### 1. **Missing Context Information**
|
|
28
|
+
```javascript
|
|
29
|
+
// ❌ Bad - No context
|
|
30
|
+
catch(e) {
|
|
31
|
+
logger.error(e.message);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ✅ Good - Has context
|
|
35
|
+
catch(e) {
|
|
36
|
+
logger.error('User creation failed', {
|
|
37
|
+
error: e.message,
|
|
38
|
+
stack: e.stack,
|
|
39
|
+
userId: user.id,
|
|
40
|
+
requestId: req.id
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
#### 2. **Non-structured Logging**
|
|
46
|
+
```javascript
|
|
47
|
+
// ❌ Bad - String concatenation
|
|
48
|
+
catch(e) {
|
|
49
|
+
logger.error("Error: " + e.message + " User: " + userId);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ✅ Good - Structured object
|
|
53
|
+
catch(e) {
|
|
54
|
+
logger.error('Operation failed', {
|
|
55
|
+
error: e.message,
|
|
56
|
+
userId: userId,
|
|
57
|
+
context: 'user-service'
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
#### 3. **Sensitive Data Exposure**
|
|
63
|
+
```javascript
|
|
64
|
+
// ❌ Bad - Exposes sensitive data
|
|
65
|
+
catch(e) {
|
|
66
|
+
logger.error('Login failed', {
|
|
67
|
+
password: user.password,
|
|
68
|
+
token: authToken
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ✅ Good - Sensitive data masked
|
|
73
|
+
catch(e) {
|
|
74
|
+
logger.error('Login failed', {
|
|
75
|
+
username: user.username,
|
|
76
|
+
password: user.password.substring(0,2) + '***',
|
|
77
|
+
requestId: req.id
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Required Context Elements:
|
|
83
|
+
- Error message/stack trace
|
|
84
|
+
- Request/operation identifier (requestId, transactionId)
|
|
85
|
+
- User/entity identifier (userId, entityId)
|
|
86
|
+
- Operation context (service name, method name)
|
|
87
|
+
|
|
88
|
+
### Sensitive Data Patterns to Flag:
|
|
89
|
+
- password, passwd, pwd
|
|
90
|
+
- token, jwt, auth, secret
|
|
91
|
+
- key, apikey, privatekey
|
|
92
|
+
- ssn, credit, card, cvv
|
|
93
|
+
- email (in some contexts)
|
|
94
|
+
|
|
95
|
+
### Implementation Priority:
|
|
96
|
+
1. **Phase 1**: Basic structure detection (structured vs string)
|
|
97
|
+
2. **Phase 2**: Context validation (required fields)
|
|
98
|
+
3. **Phase 3**: Sensitive data detection
|
|
99
|
+
4. **Phase 4**: Advanced patterns (custom loggers)
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C035 Main Analyzer - Error Logging Context Detection
|
|
3
|
+
* Primary: Symbol-based analysis (when available)
|
|
4
|
+
* Fallback: Regex-based for all other cases
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const C035SymbolBasedAnalyzer = require('./symbol-based-analyzer.js');
|
|
8
|
+
const C035RegexBasedAnalyzer = require('./regex-based-analyzer.js');
|
|
9
|
+
|
|
10
|
+
class C035Analyzer {
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
13
|
+
console.log(`🔧 [C035] Constructor called with options:`, !!options);
|
|
14
|
+
console.log(`🔧 [C035] Options type:`, typeof options, Object.keys(options || {}));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
this.ruleId = 'C035';
|
|
18
|
+
this.ruleName = 'Error Logging Context Analysis';
|
|
19
|
+
this.description = 'Khi xử lý lỗi, phải ghi log đầy đủ thông tin liên quan - structured logging with context';
|
|
20
|
+
this.semanticEngine = options.semanticEngine || null;
|
|
21
|
+
this.verbose = options.verbose || false;
|
|
22
|
+
|
|
23
|
+
// Configuration
|
|
24
|
+
this.config = {
|
|
25
|
+
useSymbolBased: true, // Primary approach
|
|
26
|
+
fallbackToRegex: true, // Only when symbol fails completely
|
|
27
|
+
symbolBasedOnly: false // Can be set to true for pure mode
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Initialize both analyzers
|
|
31
|
+
try {
|
|
32
|
+
this.symbolAnalyzer = new C035SymbolBasedAnalyzer(this.semanticEngine);
|
|
33
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
34
|
+
console.log(`🔧 [C035] Symbol analyzer created successfully`);
|
|
35
|
+
}
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error(`🔧 [C035] Error creating symbol analyzer:`, error);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
this.regexAnalyzer = new C035RegexBasedAnalyzer(this.semanticEngine);
|
|
42
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
43
|
+
console.log(`🔧 [C035] Regex analyzer created successfully`);
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error(`🔧 [C035] Error creating regex analyzer:`, error);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
50
|
+
console.log(`🔧 [C035] Constructor completed`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Initialize with semantic engine
|
|
56
|
+
*/
|
|
57
|
+
async initialize(semanticEngine = null) {
|
|
58
|
+
if (semanticEngine) {
|
|
59
|
+
this.semanticEngine = semanticEngine;
|
|
60
|
+
}
|
|
61
|
+
this.verbose = semanticEngine?.verbose || false;
|
|
62
|
+
|
|
63
|
+
// Initialize both analyzers
|
|
64
|
+
await this.symbolAnalyzer.initialize(semanticEngine);
|
|
65
|
+
await this.regexAnalyzer.initialize(semanticEngine);
|
|
66
|
+
|
|
67
|
+
// Ensure verbose flag is propagated
|
|
68
|
+
this.regexAnalyzer.verbose = this.verbose;
|
|
69
|
+
this.symbolAnalyzer.verbose = this.verbose;
|
|
70
|
+
|
|
71
|
+
if (this.verbose) {
|
|
72
|
+
console.log(`🔧 [C035 Hybrid] Analyzer initialized - verbose: ${this.verbose}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async analyze(files, language, options = {}) {
|
|
77
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
78
|
+
console.log(`🔧 [C035] analyze() method called with ${files.length} files, language: ${language}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const violations = [];
|
|
82
|
+
|
|
83
|
+
for (const filePath of files) {
|
|
84
|
+
try {
|
|
85
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
86
|
+
console.log(`🔧 [C035] Processing file: ${filePath}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const fileViolations = await this.analyzeFile(filePath, options);
|
|
90
|
+
violations.push(...fileViolations);
|
|
91
|
+
|
|
92
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
93
|
+
console.log(`🔧 [C035] File ${filePath}: Found ${fileViolations.length} violations`);
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.warn(`❌ [C035] Analysis failed for ${filePath}:`, error.message);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
101
|
+
console.log(`🔧 [C035] Total violations found: ${violations.length}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return violations;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async analyzeFile(filePath, options = {}) {
|
|
108
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
109
|
+
console.log(`🔧 [C035] analyzeFile() called for: ${filePath}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 1. Try Symbol-based analysis first (primary)
|
|
113
|
+
if (this.config.useSymbolBased &&
|
|
114
|
+
this.semanticEngine?.project &&
|
|
115
|
+
this.semanticEngine?.initialized) {
|
|
116
|
+
try {
|
|
117
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
118
|
+
console.log(`🔧 [C035] Trying symbol-based analysis...`);
|
|
119
|
+
}
|
|
120
|
+
const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
121
|
+
if (sourceFile) {
|
|
122
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
123
|
+
console.log(`🔧 [C035] Source file found, analyzing with symbol-based...`);
|
|
124
|
+
}
|
|
125
|
+
const violations = await this.symbolAnalyzer.analyzeFileWithSymbols(filePath, { ...options, verbose: options.verbose });
|
|
126
|
+
|
|
127
|
+
// Mark violations with analysis strategy
|
|
128
|
+
violations.forEach(v => v.analysisStrategy = 'symbol-based');
|
|
129
|
+
|
|
130
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
131
|
+
console.log(`✅ [C035] Symbol-based analysis: ${violations.length} violations`);
|
|
132
|
+
}
|
|
133
|
+
return violations; // Return even if 0 violations - symbol analysis completed successfully
|
|
134
|
+
} else {
|
|
135
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
136
|
+
console.log(`⚠️ [C035] Source file not found in project`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.warn(`⚠️ [C035] Symbol analysis failed: ${error.message}`);
|
|
141
|
+
// Continue to fallback
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
145
|
+
console.log(`🔄 [C035] Symbol analysis conditions check:`);
|
|
146
|
+
console.log(` - useSymbolBased: ${this.config.useSymbolBased}`);
|
|
147
|
+
console.log(` - semanticEngine: ${!!this.semanticEngine}`);
|
|
148
|
+
console.log(` - semanticEngine.project: ${!!this.semanticEngine?.project}`);
|
|
149
|
+
console.log(` - semanticEngine.initialized: ${this.semanticEngine?.initialized}`);
|
|
150
|
+
console.log(`🔄 [C035] Symbol analysis unavailable, using regex fallback`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 2. Fallback to regex-based analysis
|
|
155
|
+
if (this.config.fallbackToRegex) {
|
|
156
|
+
try {
|
|
157
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
158
|
+
console.log(`🔧 [C035] Trying regex-based analysis...`);
|
|
159
|
+
}
|
|
160
|
+
const violations = await this.regexAnalyzer.analyzeFileBasic(filePath, options);
|
|
161
|
+
|
|
162
|
+
// Mark violations with analysis strategy
|
|
163
|
+
violations.forEach(v => v.analysisStrategy = 'regex-fallback');
|
|
164
|
+
|
|
165
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
166
|
+
console.log(`🔄 [C035] Regex-based analysis: ${violations.length} violations`);
|
|
167
|
+
}
|
|
168
|
+
return violations;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.error(`❌ [C035] Regex analysis failed: ${error.message}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
console.log(`🔧 [C035] No analysis methods succeeded, returning empty`);
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async analyzeFileBasic(filePath, options = {}) {
|
|
179
|
+
console.log(`🔧 [C035] analyzeFileBasic() called for: ${filePath}`);
|
|
180
|
+
console.log(`🔧 [C035] semanticEngine exists: ${!!this.semanticEngine}`);
|
|
181
|
+
console.log(`🔧 [C035] symbolAnalyzer exists: ${!!this.symbolAnalyzer}`);
|
|
182
|
+
console.log(`🔧 [C035] regexAnalyzer exists: ${!!this.regexAnalyzer}`);
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
// Try symbol-based analysis first
|
|
186
|
+
if (this.semanticEngine?.isSymbolEngineReady?.() &&
|
|
187
|
+
this.semanticEngine.project) {
|
|
188
|
+
|
|
189
|
+
if (this.verbose) {
|
|
190
|
+
console.log(`🔍 [C035] Using symbol-based analysis for ${filePath}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const violations = await this.symbolAnalyzer.analyzeFileBasic(filePath, options);
|
|
194
|
+
return violations;
|
|
195
|
+
}
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (this.verbose) {
|
|
198
|
+
console.warn(`⚠️ [C035] Symbol analysis failed: ${error.message}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Fallback to regex-based analysis
|
|
203
|
+
if (this.verbose) {
|
|
204
|
+
console.log(`🔄 [C035] Using regex-based analysis (fallback) for ${filePath}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
console.log(`🔧 [C035] About to call regexAnalyzer.analyzeFileBasic()`);
|
|
208
|
+
try {
|
|
209
|
+
const result = await this.regexAnalyzer.analyzeFileBasic(filePath, options);
|
|
210
|
+
console.log(`🔧 [C035] Regex analyzer returned: ${result.length} violations`);
|
|
211
|
+
return result;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
console.error(`🔧 [C035] Error in regex analyzer:`, error);
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Methods for compatibility with different engine invocation patterns
|
|
220
|
+
*/
|
|
221
|
+
async analyzeFileWithSymbols(filePath, options = {}) {
|
|
222
|
+
return this.analyzeFile(filePath, options);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async analyzeWithSemantics(filePath, options = {}) {
|
|
226
|
+
return this.analyzeFile(filePath, options);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
module.exports = C035Analyzer;
|