@sun-asterisk/sunlint 1.3.23 → 1.3.25
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 +32 -0
- package/core/github-annotate-service.js +1 -4
- package/package.json +1 -1
- package/rules/common/C010_limit_block_nesting/symbol-based-analyzer.js +40 -11
- package/rules/common/C013_no_dead_code/symbol-based-analyzer.js +104 -28
- package/rules/common/C019_log_level_usage/analyzer.js +30 -27
- package/rules/common/C019_log_level_usage/config.json +4 -2
- package/rules/common/C019_log_level_usage/ts-morph-analyzer.js +274 -0
- package/rules/common/C020_unused_imports/analyzer.js +88 -0
- package/rules/common/C020_unused_imports/config.json +64 -0
- package/rules/common/C020_unused_imports/ts-morph-analyzer.js +358 -0
- package/rules/common/C021_import_organization/analyzer.js +88 -0
- package/rules/common/C021_import_organization/config.json +77 -0
- package/rules/common/C021_import_organization/ts-morph-analyzer.js +373 -0
- package/rules/common/C029_catch_block_logging/analyzer.js +106 -31
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +377 -87
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C019 ts-morph Analyzer - Log Level Usage
|
|
3
|
+
*
|
|
4
|
+
* Detects inappropriate use of ERROR log level for business logic errors.
|
|
5
|
+
* Business logic errors (validation, not found, unauthorized, etc.) should use WARN or INFO.
|
|
6
|
+
* System errors (database, network, crashes) should use ERROR.
|
|
7
|
+
*
|
|
8
|
+
* Following Rule C005: Single responsibility - log level detection only
|
|
9
|
+
* Following Rule C006: Verb-noun naming
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { Project, SyntaxKind, Node } = require('ts-morph');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
class C019TsMorphAnalyzer {
|
|
17
|
+
constructor(semanticEngine = null, options = {}) {
|
|
18
|
+
this.ruleId = 'C019';
|
|
19
|
+
this.ruleName = 'Log Level Usage';
|
|
20
|
+
this.description = 'Detect inappropriate ERROR log level for business logic errors';
|
|
21
|
+
this.semanticEngine = semanticEngine;
|
|
22
|
+
this.project = null;
|
|
23
|
+
this.verbose = false;
|
|
24
|
+
|
|
25
|
+
// Load config
|
|
26
|
+
this.config = this.loadConfig();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
loadConfig() {
|
|
30
|
+
try {
|
|
31
|
+
const configPath = path.join(__dirname, 'config.json');
|
|
32
|
+
const configData = fs.readFileSync(configPath, 'utf8');
|
|
33
|
+
return JSON.parse(configData).config;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.warn('[C019] Could not load config, using defaults');
|
|
36
|
+
return {
|
|
37
|
+
errorKeywords: ['not found', 'invalid', 'unauthorized', 'validation'],
|
|
38
|
+
legitimateErrorKeywords: ['exception', 'crash', 'database', 'connection']
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async initialize(semanticEngine = null) {
|
|
44
|
+
if (semanticEngine) {
|
|
45
|
+
this.semanticEngine = semanticEngine;
|
|
46
|
+
}
|
|
47
|
+
this.verbose = semanticEngine?.verbose || false;
|
|
48
|
+
|
|
49
|
+
// Use semantic engine's project if available
|
|
50
|
+
if (this.semanticEngine?.project) {
|
|
51
|
+
this.project = this.semanticEngine.project;
|
|
52
|
+
if (this.verbose) {
|
|
53
|
+
console.log('[DEBUG] 🎯 C019: Using semantic engine project');
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
this.project = new Project({
|
|
57
|
+
compilerOptions: {
|
|
58
|
+
target: 99,
|
|
59
|
+
module: 99,
|
|
60
|
+
allowJs: true,
|
|
61
|
+
checkJs: false,
|
|
62
|
+
jsx: 2,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
if (this.verbose) {
|
|
66
|
+
console.log('[DEBUG] 🎯 C019: Created standalone ts-morph project');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async analyze(files, language, options = {}) {
|
|
72
|
+
this.verbose = options.verbose || this.verbose;
|
|
73
|
+
|
|
74
|
+
if (!this.project) {
|
|
75
|
+
await this.initialize();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const violations = [];
|
|
79
|
+
|
|
80
|
+
for (const filePath of files) {
|
|
81
|
+
try {
|
|
82
|
+
const fileViolations = await this.analyzeFile(filePath, options);
|
|
83
|
+
violations.push(...fileViolations);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (this.verbose) {
|
|
86
|
+
console.warn(`[C019] Error analyzing ${filePath}:`, error.message);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return violations;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async analyzeFile(filePath, options = {}) {
|
|
95
|
+
// Get or add source file
|
|
96
|
+
let sourceFile = this.project.getSourceFile(filePath);
|
|
97
|
+
|
|
98
|
+
if (!sourceFile && fs.existsSync(filePath)) {
|
|
99
|
+
sourceFile = this.project.addSourceFileAtPath(filePath);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!sourceFile) {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const violations = [];
|
|
107
|
+
|
|
108
|
+
// Find all error-level log calls
|
|
109
|
+
const errorLogCalls = this.findErrorLogCalls(sourceFile);
|
|
110
|
+
|
|
111
|
+
for (const logCall of errorLogCalls) {
|
|
112
|
+
const violation = this.checkLogCall(logCall, sourceFile);
|
|
113
|
+
if (violation) {
|
|
114
|
+
violations.push(violation);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return violations;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Find all log calls with ERROR level
|
|
123
|
+
* Supports: logger.error(), console.error(), log.error(), etc.
|
|
124
|
+
*/
|
|
125
|
+
findErrorLogCalls(sourceFile) {
|
|
126
|
+
const errorCalls = [];
|
|
127
|
+
|
|
128
|
+
// Find all call expressions
|
|
129
|
+
const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
130
|
+
|
|
131
|
+
for (const call of callExpressions) {
|
|
132
|
+
const expression = call.getExpression();
|
|
133
|
+
|
|
134
|
+
// Check if it's a property access (logger.error, console.error, etc.)
|
|
135
|
+
if (Node.isPropertyAccessExpression(expression)) {
|
|
136
|
+
const methodName = expression.getName();
|
|
137
|
+
|
|
138
|
+
// Check if method name is "error" or "e" (Log.e, Timber.e)
|
|
139
|
+
if (methodName === 'error' || methodName === 'e') {
|
|
140
|
+
const object = expression.getExpression().getText();
|
|
141
|
+
|
|
142
|
+
// Common logger patterns
|
|
143
|
+
const loggerPatterns = [
|
|
144
|
+
'logger', 'log', 'console', 'Logger',
|
|
145
|
+
'winston', 'bunyan', 'Log', 'Timber',
|
|
146
|
+
'_logger', 'this.logger'
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
if (loggerPatterns.some(pattern =>
|
|
150
|
+
object.toLowerCase().includes(pattern.toLowerCase())
|
|
151
|
+
)) {
|
|
152
|
+
errorCalls.push({
|
|
153
|
+
node: call,
|
|
154
|
+
object: object,
|
|
155
|
+
method: methodName,
|
|
156
|
+
position: {
|
|
157
|
+
line: call.getStartLineNumber(),
|
|
158
|
+
column: call.getStart() - call.getStartLinePos() + 1
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return errorCalls;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Check if error log call is inappropriate
|
|
171
|
+
*/
|
|
172
|
+
checkLogCall(logCall, sourceFile) {
|
|
173
|
+
// Skip if in catch block (legitimate exception handling)
|
|
174
|
+
if (this.isInCatchBlock(logCall.node)) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Get the log message (first argument)
|
|
179
|
+
const args = logCall.node.getArguments();
|
|
180
|
+
if (args.length === 0) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const messageArg = args[0];
|
|
185
|
+
let messageText = '';
|
|
186
|
+
|
|
187
|
+
// Extract message text
|
|
188
|
+
if (Node.isStringLiteral(messageArg) || Node.isNoSubstitutionTemplateLiteral(messageArg)) {
|
|
189
|
+
messageText = messageArg.getLiteralText();
|
|
190
|
+
} else if (Node.isTemplateExpression(messageArg)) {
|
|
191
|
+
messageText = messageArg.getText();
|
|
192
|
+
} else {
|
|
193
|
+
// Complex expression, get surrounding context
|
|
194
|
+
const parent = logCall.node.getParent();
|
|
195
|
+
if (parent) {
|
|
196
|
+
messageText = parent.getText().substring(0, 200); // First 200 chars
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
messageText = messageText.toLowerCase();
|
|
201
|
+
|
|
202
|
+
// Check if message contains business logic error keywords
|
|
203
|
+
const hasBusinessLogicError = this.config.errorKeywords.some(keyword =>
|
|
204
|
+
messageText.includes(keyword.toLowerCase())
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
if (!hasBusinessLogicError) {
|
|
208
|
+
return null; // Not a business logic error
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Check if it's actually a legitimate system error
|
|
212
|
+
const hasLegitimateError = this.config.legitimateErrorKeywords.some(keyword =>
|
|
213
|
+
messageText.includes(keyword.toLowerCase())
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
if (hasLegitimateError) {
|
|
217
|
+
return null; // Legitimate system error
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// This is a violation: business logic error using ERROR level
|
|
221
|
+
return {
|
|
222
|
+
ruleId: this.ruleId,
|
|
223
|
+
message: `Log level "error" is inappropriate for business logic errors. Use "warn" or "info" for: ${this.extractKeyword(messageText)}`,
|
|
224
|
+
severity: 'warning',
|
|
225
|
+
filePath: sourceFile.getFilePath(),
|
|
226
|
+
location: {
|
|
227
|
+
start: {
|
|
228
|
+
line: logCall.position.line,
|
|
229
|
+
column: logCall.position.column
|
|
230
|
+
},
|
|
231
|
+
end: {
|
|
232
|
+
line: logCall.node.getEndLineNumber(),
|
|
233
|
+
column: logCall.node.getEnd() - logCall.node.getStartLinePos() + 1
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
context: {
|
|
237
|
+
logLevel: 'error',
|
|
238
|
+
suggestedLevel: 'warn',
|
|
239
|
+
logObject: logCall.object,
|
|
240
|
+
messagePreview: messageText.substring(0, 50)
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Check if node is inside a catch block
|
|
247
|
+
*/
|
|
248
|
+
isInCatchBlock(node) {
|
|
249
|
+
let current = node.getParent();
|
|
250
|
+
|
|
251
|
+
while (current) {
|
|
252
|
+
if (Node.isCatchClause(current)) {
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
current = current.getParent();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Extract the matched keyword from message for better error message
|
|
263
|
+
*/
|
|
264
|
+
extractKeyword(messageText) {
|
|
265
|
+
for (const keyword of this.config.errorKeywords) {
|
|
266
|
+
if (messageText.includes(keyword.toLowerCase())) {
|
|
267
|
+
return keyword;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return 'business logic error';
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
module.exports = C019TsMorphAnalyzer;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const C020TsMorphAnalyzer = require('./ts-morph-analyzer.js');
|
|
2
|
+
|
|
3
|
+
class C020Analyzer {
|
|
4
|
+
constructor(semanticEngine = null) {
|
|
5
|
+
this.ruleId = 'C020';
|
|
6
|
+
this.ruleName = 'Unused Imports';
|
|
7
|
+
this.description = 'Detect unused imports';
|
|
8
|
+
this.semanticEngine = semanticEngine;
|
|
9
|
+
this.verbose = false;
|
|
10
|
+
|
|
11
|
+
// Initialize ts-morph analyzer
|
|
12
|
+
this.tsMorphAnalyzer = new C020TsMorphAnalyzer(semanticEngine);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async initialize(semanticEngine = null) {
|
|
16
|
+
if (semanticEngine) {
|
|
17
|
+
this.semanticEngine = semanticEngine;
|
|
18
|
+
}
|
|
19
|
+
this.verbose = semanticEngine?.verbose || false;
|
|
20
|
+
|
|
21
|
+
await this.tsMorphAnalyzer.initialize(semanticEngine);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async analyzeFileBasic(filePath, options = {}) {
|
|
25
|
+
const allViolations = [];
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// Use ts-morph analysis
|
|
29
|
+
if (this.semanticEngine?.isSymbolEngineReady?.() && this.semanticEngine.project) {
|
|
30
|
+
if (this.verbose) {
|
|
31
|
+
console.log(`[DEBUG] 🎯 C020: Using ts-morph analysis for ${filePath.split('/').pop()}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const tsMorphViolations = await this.tsMorphAnalyzer.analyzeFile(filePath, options);
|
|
36
|
+
allViolations.push(...tsMorphViolations);
|
|
37
|
+
|
|
38
|
+
if (this.verbose) {
|
|
39
|
+
console.log(`[DEBUG] 🎯 C020: ts-morph analysis found ${tsMorphViolations.length} violations`);
|
|
40
|
+
}
|
|
41
|
+
} catch (tsMorphError) {
|
|
42
|
+
if (this.verbose) {
|
|
43
|
+
console.warn(`[DEBUG] ⚠️ C020: ts-morph analysis failed: ${tsMorphError.message}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return allViolations;
|
|
49
|
+
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (this.verbose) {
|
|
52
|
+
console.error(`[DEBUG] ❌ C020: Analysis failed: ${error.message}`);
|
|
53
|
+
}
|
|
54
|
+
throw new Error(`C020 analysis failed: ${error.message}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async analyzeFiles(files, options = {}) {
|
|
59
|
+
const allViolations = [];
|
|
60
|
+
for (const filePath of files) {
|
|
61
|
+
try {
|
|
62
|
+
const violations = await this.analyzeFileBasic(filePath, options);
|
|
63
|
+
allViolations.push(...violations);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.warn(`C020: Skipping ${filePath}: ${error.message}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return allViolations;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Legacy method for backward compatibility
|
|
72
|
+
async analyze(files, language, config = {}) {
|
|
73
|
+
const allViolations = [];
|
|
74
|
+
|
|
75
|
+
for (const filePath of files) {
|
|
76
|
+
try {
|
|
77
|
+
const fileViolations = await this.analyzeFileBasic(filePath, config);
|
|
78
|
+
allViolations.push(...fileViolations);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error(`Error analyzing file ${filePath}:`, error.message);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return allViolations;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = C020Analyzer;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ruleId": "C020",
|
|
3
|
+
"name": "Unused Imports",
|
|
4
|
+
"description": "Không import các module hoặc symbol không sử dụng",
|
|
5
|
+
"category": "code-quality",
|
|
6
|
+
"severity": "warning",
|
|
7
|
+
"languages": ["typescript", "javascript"],
|
|
8
|
+
"version": "1.0.0",
|
|
9
|
+
"status": "stable",
|
|
10
|
+
"tags": ["imports", "cleanup", "unused-code"],
|
|
11
|
+
"config": {
|
|
12
|
+
"checkDefaultImports": true,
|
|
13
|
+
"checkNamedImports": true,
|
|
14
|
+
"checkNamespaceImports": true,
|
|
15
|
+
"ignoreTypeImports": false,
|
|
16
|
+
"allowedUnusedPatterns": [
|
|
17
|
+
"^_"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"examples": {
|
|
21
|
+
"violations": [
|
|
22
|
+
{
|
|
23
|
+
"language": "typescript",
|
|
24
|
+
"code": "import { Order } from './models/Order';\n\n// Order is never used",
|
|
25
|
+
"reason": "Imported 'Order' but never used in the code"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"language": "typescript",
|
|
29
|
+
"code": "import fs from 'fs';\nimport path from 'path';\n\n// Only path is used\nconsole.log(path.join('a', 'b'));",
|
|
30
|
+
"reason": "Imported 'fs' but never used"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"language": "typescript",
|
|
34
|
+
"code": "import * as utils from './utils';\n\n// utils namespace is never used",
|
|
35
|
+
"reason": "Imported namespace 'utils' but never used"
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"valid": [
|
|
39
|
+
{
|
|
40
|
+
"language": "typescript",
|
|
41
|
+
"code": "import { User } from './models/User';\n\nconst user = new User();",
|
|
42
|
+
"reason": "User is imported and used"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"language": "typescript",
|
|
46
|
+
"code": "import type { Order } from './types';\n\nconst order: Order = { id: 1 };",
|
|
47
|
+
"reason": "Type import is used in type annotation"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"language": "typescript",
|
|
51
|
+
"code": "import { _debugHelper } from './utils';\n\n// Allowed: starts with underscore",
|
|
52
|
+
"reason": "Import starting with _ is allowed as intentionally unused"
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
"fixes": {
|
|
57
|
+
"autoFixable": true,
|
|
58
|
+
"suggestions": [
|
|
59
|
+
"Remove unused imports to keep code clean",
|
|
60
|
+
"Use IDE auto-import features to avoid manual imports",
|
|
61
|
+
"Prefix intentionally unused imports with underscore (_)"
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
}
|