@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,358 @@
1
+ /**
2
+ * C020 ts-morph Analyzer - Unused Imports
3
+ *
4
+ * Detects imports that are declared but never used in the code.
5
+ * Supports default imports, named imports, and namespace imports.
6
+ *
7
+ * Following Rule C005: Single responsibility - unused import detection only
8
+ * Following Rule C006: Verb-noun naming
9
+ */
10
+
11
+ const { Project, SyntaxKind, Node } = require('ts-morph');
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ class C020TsMorphAnalyzer {
16
+ constructor(semanticEngine = null, options = {}) {
17
+ this.ruleId = 'C020';
18
+ this.ruleName = 'Unused Imports';
19
+ this.description = 'Detect unused imports';
20
+ this.semanticEngine = semanticEngine;
21
+ this.project = null;
22
+ this.verbose = false;
23
+
24
+ // Load config
25
+ this.config = this.loadConfig();
26
+ }
27
+
28
+ loadConfig() {
29
+ try {
30
+ const configPath = path.join(__dirname, 'config.json');
31
+ const configData = fs.readFileSync(configPath, 'utf8');
32
+ return JSON.parse(configData).config;
33
+ } catch (error) {
34
+ console.warn('[C020] Could not load config, using defaults');
35
+ return {
36
+ checkDefaultImports: true,
37
+ checkNamedImports: true,
38
+ checkNamespaceImports: true,
39
+ ignoreTypeImports: false,
40
+ allowedUnusedPatterns: ['^_']
41
+ };
42
+ }
43
+ }
44
+
45
+ async initialize(semanticEngine = null) {
46
+ if (semanticEngine) {
47
+ this.semanticEngine = semanticEngine;
48
+ }
49
+ this.verbose = semanticEngine?.verbose || false;
50
+
51
+ // Use semantic engine's project if available
52
+ if (this.semanticEngine?.project) {
53
+ this.project = this.semanticEngine.project;
54
+ if (this.verbose) {
55
+ console.log('[DEBUG] 🎯 C020: Using semantic engine project');
56
+ }
57
+ } else {
58
+ this.project = new Project({
59
+ compilerOptions: {
60
+ target: 99,
61
+ module: 99,
62
+ allowJs: true,
63
+ checkJs: false,
64
+ jsx: 2,
65
+ },
66
+ });
67
+ if (this.verbose) {
68
+ console.log('[DEBUG] 🎯 C020: Created standalone ts-morph project');
69
+ }
70
+ }
71
+ }
72
+
73
+ async analyze(files, language, options = {}) {
74
+ this.verbose = options.verbose || this.verbose;
75
+
76
+ if (!this.project) {
77
+ await this.initialize();
78
+ }
79
+
80
+ const violations = [];
81
+
82
+ for (const filePath of files) {
83
+ try {
84
+ const fileViolations = await this.analyzeFile(filePath, options);
85
+ violations.push(...fileViolations);
86
+ } catch (error) {
87
+ if (this.verbose) {
88
+ console.warn(`[C020] Error analyzing ${filePath}:`, error.message);
89
+ }
90
+ }
91
+ }
92
+
93
+ return violations;
94
+ }
95
+
96
+ async analyzeFile(filePath, options = {}) {
97
+ // Get or add source file
98
+ let sourceFile = this.project.getSourceFile(filePath);
99
+
100
+ if (!sourceFile && fs.existsSync(filePath)) {
101
+ sourceFile = this.project.addSourceFileAtPath(filePath);
102
+ }
103
+
104
+ if (!sourceFile) {
105
+ return [];
106
+ }
107
+
108
+ const violations = [];
109
+
110
+ // Get all imports
111
+ const imports = this.getAllImports(sourceFile);
112
+
113
+ // Check each import for usage
114
+ for (const importInfo of imports) {
115
+ if (this.isImportUnused(importInfo, sourceFile)) {
116
+ violations.push(this.createViolation(importInfo, sourceFile));
117
+ }
118
+ }
119
+
120
+ return violations;
121
+ }
122
+
123
+ /**
124
+ * Get all imports from a source file
125
+ * Returns array of import info objects
126
+ */
127
+ getAllImports(sourceFile) {
128
+ const imports = [];
129
+
130
+ // Get all import declarations
131
+ const importDeclarations = sourceFile.getImportDeclarations();
132
+
133
+ for (const importDecl of importDeclarations) {
134
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
135
+
136
+ // Check if it's a type-only import
137
+ const isTypeOnly = importDecl.isTypeOnly();
138
+
139
+ // Skip type imports if configured
140
+ if (isTypeOnly && this.config.ignoreTypeImports) {
141
+ continue;
142
+ }
143
+
144
+ // Get default import (e.g., import fs from 'fs')
145
+ const defaultImport = importDecl.getDefaultImport();
146
+ if (defaultImport && this.config.checkDefaultImports) {
147
+ imports.push({
148
+ type: 'default',
149
+ name: defaultImport.getText(),
150
+ node: importDecl,
151
+ nameNode: defaultImport,
152
+ moduleSpecifier,
153
+ isTypeOnly,
154
+ position: {
155
+ line: importDecl.getStartLineNumber(),
156
+ column: importDecl.getStart() - importDecl.getStartLinePos() + 1
157
+ }
158
+ });
159
+ }
160
+
161
+ // Get namespace import (e.g., import * as utils from './utils')
162
+ const namespaceImport = importDecl.getNamespaceImport();
163
+ if (namespaceImport && this.config.checkNamespaceImports) {
164
+ imports.push({
165
+ type: 'namespace',
166
+ name: namespaceImport.getText(),
167
+ node: importDecl,
168
+ nameNode: namespaceImport,
169
+ moduleSpecifier,
170
+ isTypeOnly,
171
+ position: {
172
+ line: importDecl.getStartLineNumber(),
173
+ column: importDecl.getStart() - importDecl.getStartLinePos() + 1
174
+ }
175
+ });
176
+ }
177
+
178
+ // Get named imports (e.g., import { User, Order } from './models')
179
+ const namedImports = importDecl.getNamedImports();
180
+ if (namedImports.length > 0 && this.config.checkNamedImports) {
181
+ for (const namedImport of namedImports) {
182
+ const importName = namedImport.getName();
183
+ const aliasNode = namedImport.getAliasNode();
184
+ const actualName = aliasNode ? aliasNode.getText() : importName;
185
+
186
+ imports.push({
187
+ type: 'named',
188
+ name: actualName,
189
+ originalName: importName,
190
+ node: importDecl,
191
+ nameNode: namedImport,
192
+ moduleSpecifier,
193
+ isTypeOnly: isTypeOnly || namedImport.isTypeOnly(),
194
+ position: {
195
+ line: importDecl.getStartLineNumber(),
196
+ column: namedImport.getStart() - importDecl.getStartLinePos() + 1
197
+ }
198
+ });
199
+ }
200
+ }
201
+ }
202
+
203
+ return imports;
204
+ }
205
+
206
+ /**
207
+ * Check if an import is unused
208
+ */
209
+ isImportUnused(importInfo, sourceFile) {
210
+ const { name, isTypeOnly, moduleSpecifier } = importInfo;
211
+
212
+ // Special case: React imports in JSX/TSX files
213
+ // React is used implicitly for JSX transform even if not directly referenced
214
+ if (name === 'React' && moduleSpecifier === 'react') {
215
+ if (this.fileHasJSX(sourceFile)) {
216
+ return false; // React is used implicitly for JSX
217
+ }
218
+ }
219
+
220
+ // Check if import name matches allowed unused patterns (e.g., starts with _)
221
+ for (const pattern of this.config.allowedUnusedPatterns) {
222
+ const regex = new RegExp(pattern);
223
+ if (regex.test(name)) {
224
+ return false; // Allowed to be unused
225
+ }
226
+ }
227
+
228
+ // Find all references to this import
229
+ const nameNode = importInfo.nameNode;
230
+
231
+ // Get the identifier for the import
232
+ let identifier;
233
+ if (Node.isImportSpecifier(nameNode)) {
234
+ // Named import
235
+ identifier = nameNode.getNameNode();
236
+ if (nameNode.getAliasNode()) {
237
+ identifier = nameNode.getAliasNode();
238
+ }
239
+ } else {
240
+ // Default or namespace import
241
+ identifier = nameNode;
242
+ }
243
+
244
+ // Find all references using ts-morph's reference finding
245
+ const referencedSymbols = identifier.findReferencesAsNodes();
246
+
247
+ // Filter out the import declaration itself
248
+ const usages = referencedSymbols.filter(ref => {
249
+ // Check if reference is in the same file
250
+ if (ref.getSourceFile().getFilePath() !== sourceFile.getFilePath()) {
251
+ return false;
252
+ }
253
+
254
+ // Check if it's not the import declaration itself
255
+ const parent = ref.getParent();
256
+ if (Node.isImportSpecifier(parent) ||
257
+ Node.isImportClause(parent) ||
258
+ Node.isNamespaceImport(parent)) {
259
+ return false;
260
+ }
261
+
262
+ return true;
263
+ });
264
+
265
+ // For type-only imports, also check if used in type positions
266
+ if (isTypeOnly && usages.length === 0) {
267
+ // Check type references
268
+ const typeReferences = this.findTypeReferences(name, sourceFile);
269
+ return typeReferences.length === 0;
270
+ }
271
+
272
+ return usages.length === 0;
273
+ }
274
+
275
+ /**
276
+ * Find type references for type-only imports
277
+ */
278
+ findTypeReferences(typeName, sourceFile) {
279
+ const typeReferences = [];
280
+
281
+ // Find all type references
282
+ const typeReferences_nodes = sourceFile.getDescendantsOfKind(SyntaxKind.TypeReference);
283
+
284
+ for (const typeRef of typeReferences_nodes) {
285
+ const typeName_node = typeRef.getTypeName();
286
+ if (Node.isIdentifier(typeName_node) && typeName_node.getText() === typeName) {
287
+ typeReferences.push(typeRef);
288
+ }
289
+ }
290
+
291
+ return typeReferences;
292
+ }
293
+
294
+ /**
295
+ * Check if a file contains JSX elements
296
+ * Used to determine if React import is actually used (implicitly)
297
+ */
298
+ fileHasJSX(sourceFile) {
299
+ // Check for JSX elements: <div>...</div>
300
+ const jsxElements = sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement);
301
+
302
+ // Check for self-closing JSX: <Component />
303
+ const jsxSelfClosing = sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement);
304
+
305
+ // Check for JSX fragments: <>...</>
306
+ const jsxFragment = sourceFile.getDescendantsOfKind(SyntaxKind.JsxFragment);
307
+
308
+ return jsxElements.length > 0 || jsxSelfClosing.length > 0 || jsxFragment.length > 0;
309
+ }
310
+
311
+ /**
312
+ * Create violation object
313
+ */
314
+ createViolation(importInfo, sourceFile) {
315
+ const { type, name, originalName, moduleSpecifier, isTypeOnly, position } = importInfo;
316
+
317
+ let importText = '';
318
+ if (type === 'default') {
319
+ importText = `import ${name} from '${moduleSpecifier}'`;
320
+ } else if (type === 'namespace') {
321
+ importText = `import * as ${name} from '${moduleSpecifier}'`;
322
+ } else if (type === 'named') {
323
+ if (originalName !== name) {
324
+ importText = `import { ${originalName} as ${name} } from '${moduleSpecifier}'`;
325
+ } else {
326
+ importText = `import { ${name} } from '${moduleSpecifier}'`;
327
+ }
328
+ }
329
+
330
+ const typePrefix = isTypeOnly ? 'Type import' : 'Import';
331
+
332
+ return {
333
+ ruleId: this.ruleId,
334
+ message: `${typePrefix} '${name}' is declared but never used`,
335
+ severity: 'warning',
336
+ filePath: sourceFile.getFilePath(),
337
+ location: {
338
+ start: {
339
+ line: position.line,
340
+ column: position.column
341
+ },
342
+ end: {
343
+ line: importInfo.node.getEndLineNumber(),
344
+ column: importInfo.node.getEnd() - importInfo.node.getStartLinePos() + 1
345
+ }
346
+ },
347
+ context: {
348
+ importName: name,
349
+ importType: type,
350
+ moduleSpecifier,
351
+ isTypeOnly,
352
+ suggestedFix: `Remove unused import: ${importText}`
353
+ }
354
+ };
355
+ }
356
+ }
357
+
358
+ module.exports = C020TsMorphAnalyzer;
@@ -0,0 +1,88 @@
1
+ const C021TsMorphAnalyzer = require('./ts-morph-analyzer.js');
2
+
3
+ class C021Analyzer {
4
+ constructor(semanticEngine = null) {
5
+ this.ruleId = 'C021';
6
+ this.ruleName = 'Import Organization';
7
+ this.description = 'Enforce organized imports with grouping and sorting';
8
+ this.semanticEngine = semanticEngine;
9
+ this.verbose = false;
10
+
11
+ // Initialize ts-morph analyzer
12
+ this.tsMorphAnalyzer = new C021TsMorphAnalyzer(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] 🎯 C021: 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] 🎯 C021: ts-morph analysis found ${tsMorphViolations.length} violations`);
40
+ }
41
+ } catch (tsMorphError) {
42
+ if (this.verbose) {
43
+ console.warn(`[DEBUG] ⚠️ C021: 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] ❌ C021: Analysis failed: ${error.message}`);
53
+ }
54
+ throw new Error(`C021 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(`C021: 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 = C021Analyzer;
@@ -0,0 +1,77 @@
1
+ {
2
+ "ruleId": "C021",
3
+ "name": "Import Organization",
4
+ "description": "Tổ chức và sắp xếp imports theo nhóm và thứ tự alphabet",
5
+ "category": "code-quality",
6
+ "severity": "info",
7
+ "languages": ["typescript", "javascript"],
8
+ "version": "1.0.0",
9
+ "status": "stable",
10
+ "tags": ["imports", "organization", "readability"],
11
+ "config": {
12
+ "groups": [
13
+ {
14
+ "name": "builtin",
15
+ "description": "Built-in Node.js modules",
16
+ "patterns": ["^(assert|buffer|child_process|cluster|crypto|dgram|dns|domain|events|fs|http|https|net|os|path|punycode|querystring|readline|repl|stream|string_decoder|tls|tty|url|util|v8|vm|zlib)$"]
17
+ },
18
+ {
19
+ "name": "internal",
20
+ "description": "Internal project modules",
21
+ "patterns": ["^\\.", "^@/", "^~/"]
22
+ },
23
+ {
24
+ "name": "external",
25
+ "description": "External dependencies from node_modules",
26
+ "patterns": ["^[^.~@]"]
27
+ }
28
+ ],
29
+ "sortOrder": {
30
+ "groupOrder": ["builtin", "external", "internal"],
31
+ "withinGroup": "alphabetical"
32
+ },
33
+ "spacing": {
34
+ "requireBlankLineBetweenGroups": true,
35
+ "maxBlankLinesBetweenGroups": 1
36
+ },
37
+ "typeImports": {
38
+ "separateTypeImports": false,
39
+ "typeImportPosition": "together"
40
+ }
41
+ },
42
+ "examples": {
43
+ "violations": [
44
+ {
45
+ "language": "typescript",
46
+ "code": "import { User } from './models/User';\nimport express from 'express';\nimport fs from 'fs';\n\n// Wrong order: internal, external, builtin",
47
+ "reason": "Imports are not in correct group order"
48
+ },
49
+ {
50
+ "language": "typescript",
51
+ "code": "import express from 'express';\nimport axios from 'axios';\nimport { User } from './models/User';\nimport { Order } from './models/Order';\n\n// No blank line between groups",
52
+ "reason": "Missing blank line between external and internal groups"
53
+ },
54
+ {
55
+ "language": "typescript",
56
+ "code": "import { Order } from './models/Order';\nimport { User } from './models/User';\nimport { Customer } from './models/Customer';\n\n// Not sorted alphabetically: Order, User, Customer",
57
+ "reason": "Internal imports not sorted alphabetically"
58
+ }
59
+ ],
60
+ "valid": [
61
+ {
62
+ "language": "typescript",
63
+ "code": "import fs from 'fs';\nimport path from 'path';\n\nimport axios from 'axios';\nimport express from 'express';\n\nimport { Customer } from './models/Customer';\nimport { Order } from './models/Order';\nimport { User } from './models/User';",
64
+ "reason": "Correct grouping, sorting, and spacing"
65
+ }
66
+ ]
67
+ },
68
+ "fixes": {
69
+ "autoFixable": true,
70
+ "suggestions": [
71
+ "Group imports: builtin → external → internal",
72
+ "Sort imports alphabetically within each group",
73
+ "Add blank lines between groups",
74
+ "Use consistent import style (named vs default)"
75
+ ]
76
+ }
77
+ }