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