@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.
@@ -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
+ }