@sun-asterisk/sunlint 1.3.16 โ 1.3.17
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/rule-analysis-strategies.js +3 -3
- package/config/rules/enhanced-rules-registry.json +40 -20
- package/core/cli-action-handler.js +2 -2
- package/core/config-merger.js +28 -6
- package/core/constants/defaults.js +1 -1
- package/core/file-targeting-service.js +72 -4
- package/core/output-service.js +21 -4
- package/engines/heuristic-engine.js +5 -0
- package/package.json +1 -1
- package/rules/common/C002_no_duplicate_code/README.md +115 -0
- package/rules/common/C002_no_duplicate_code/analyzer.js +615 -219
- package/rules/common/C002_no_duplicate_code/test-cases/api-handlers.ts +64 -0
- package/rules/common/C002_no_duplicate_code/test-cases/data-processor.ts +46 -0
- package/rules/common/C002_no_duplicate_code/test-cases/good-example.tsx +40 -0
- package/rules/common/C002_no_duplicate_code/test-cases/product-service.ts +57 -0
- package/rules/common/C002_no_duplicate_code/test-cases/user-service.ts +49 -0
- package/rules/common/C008/analyzer.js +40 -0
- package/rules/common/C008/config.json +20 -0
- package/rules/common/C008/ts-morph-analyzer.js +1067 -0
- package/rules/common/C018_no_throw_generic_error/analyzer.js +1 -1
- package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +27 -3
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +504 -162
- package/rules/common/C029_catch_block_logging/analyzer.js +499 -89
- package/rules/common/C033_separate_service_repository/README.md +131 -20
- package/rules/common/C033_separate_service_repository/analyzer.js +1 -1
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +417 -274
- package/rules/common/C041_no_sensitive_hardcode/analyzer.js +144 -254
- package/rules/common/C041_no_sensitive_hardcode/config.json +50 -0
- package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +575 -0
- package/rules/common/C067_no_hardcoded_config/analyzer.js +17 -16
- package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +3477 -659
- package/rules/docs/C002_no_duplicate_code.md +276 -11
- package/rules/index.js +5 -1
- package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +266 -88
- package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +805 -0
- package/rules/security/S010_no_insecure_encryption/README.md +78 -0
- package/rules/security/S010_no_insecure_encryption/analyzer.js +463 -398
- package/rules/security/S013_tls_enforcement/README.md +51 -0
- package/rules/security/S013_tls_enforcement/analyzer.js +99 -0
- package/rules/security/S013_tls_enforcement/config.json +41 -0
- package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +339 -0
- package/rules/security/S014_tls_version_enforcement/README.md +354 -0
- package/rules/security/S014_tls_version_enforcement/analyzer.js +118 -0
- package/rules/security/S014_tls_version_enforcement/config.json +56 -0
- package/rules/security/S014_tls_version_enforcement/symbol-based-analyzer.js +194 -0
- package/rules/security/S055_content_type_validation/analyzer.js +121 -279
- package/rules/security/S055_content_type_validation/symbol-based-analyzer.js +346 -0
- package/rules/tests/C002_no_duplicate_code.test.js +111 -22
- package/rules/common/C029_catch_block_logging/analyzer-smart-pipeline.js +0 -755
- package/rules/common/C041_no_sensitive_hardcode/ast-analyzer.js +0 -296
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
````markdown
|
|
1
2
|
# C002_no_duplicate_code - CODING Rule
|
|
2
3
|
|
|
3
4
|
## ๐ Overview
|
|
@@ -5,42 +6,289 @@
|
|
|
5
6
|
**Rule ID**: `C002_no_duplicate_code`
|
|
6
7
|
**Category**: coding
|
|
7
8
|
**Severity**: Error
|
|
8
|
-
**Status**:
|
|
9
|
+
**Status**: active
|
|
9
10
|
|
|
10
11
|
## ๐ฏ Description
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
Detects and warns about duplicate code blocks of 10 or more lines across functions and classes. This rule enforces the **DRY (Don't Repeat Yourself)** principle to maintain clean, refactorable code and reduce maintenance burden.
|
|
13
14
|
|
|
15
|
+
**Key Objectives:**
|
|
16
|
+
- ๐ฏ Avoid messy, hard-to-refactor code
|
|
17
|
+
- โป๏ธ Apply DRY principle consistently
|
|
18
|
+
- ๐ง Improve code maintainability
|
|
19
|
+
- ๐งช Enhance testability
|
|
20
|
+
|
|
21
|
+
**Detection Criteria:**
|
|
22
|
+
- Warns when duplicate code โฅ 10 lines found in functions/classes
|
|
23
|
+
- Ignores comments and whitespace variations
|
|
24
|
+
- Uses similarity threshold (85%) to detect near-duplicates
|
|
25
|
+
- Cross-file duplicate detection
|
|
14
26
|
|
|
15
27
|
## ๐ Migration Info
|
|
16
28
|
|
|
17
29
|
**ESLint Rule**: `c002-no-duplicate-code`
|
|
18
|
-
**Compatibility**:
|
|
19
|
-
**Priority**:
|
|
30
|
+
**Compatibility**: Full
|
|
31
|
+
**Priority**: High
|
|
20
32
|
|
|
21
33
|
|
|
22
34
|
## โ
Valid Code Examples
|
|
23
35
|
|
|
24
|
-
|
|
25
|
-
|
|
36
|
+
### Example 1: Extract Common Logic into Utility Functions
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// โ
GOOD: Extracted validation logic into reusable functions
|
|
40
|
+
function validateEmail(email: string): void {
|
|
41
|
+
if (!email || !email.includes('@')) {
|
|
42
|
+
throw new ValidationError('Invalid email format');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function validatePassword(password: string): void {
|
|
47
|
+
if (!password || password.length < 8) {
|
|
48
|
+
throw new ValidationError('Password must be at least 8 characters');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function registerUser(userData: UserData): void {
|
|
53
|
+
validateEmail(userData.email);
|
|
54
|
+
validatePassword(userData.password);
|
|
55
|
+
// Registration logic
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function updateUserProfile(userId: string, profileData: ProfileData): void {
|
|
59
|
+
if (profileData.email) {
|
|
60
|
+
validateEmail(profileData.email);
|
|
61
|
+
}
|
|
62
|
+
if (profileData.password) {
|
|
63
|
+
validatePassword(profileData.password);
|
|
64
|
+
}
|
|
65
|
+
// Update logic
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Example 2: Using Inheritance for Shared Behavior
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// โ
GOOD: Base class with common logic
|
|
73
|
+
abstract class BaseRepository<T> {
|
|
74
|
+
protected async findById(id: string): Promise<T | null> {
|
|
75
|
+
const result = await this.db.query(
|
|
76
|
+
`SELECT * FROM ${this.tableName} WHERE id = ?`,
|
|
77
|
+
[id]
|
|
78
|
+
);
|
|
79
|
+
return result.length > 0 ? this.mapToEntity(result[0]) : null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
protected async save(entity: T): Promise<void> {
|
|
83
|
+
const data = this.mapToData(entity);
|
|
84
|
+
await this.db.query(
|
|
85
|
+
`INSERT INTO ${this.tableName} VALUES (?, ?, ?)`,
|
|
86
|
+
Object.values(data)
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
protected abstract mapToEntity(row: any): T;
|
|
91
|
+
protected abstract mapToData(entity: T): any;
|
|
92
|
+
protected abstract tableName: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
class UserRepository extends BaseRepository<User> {
|
|
96
|
+
protected tableName = 'users';
|
|
97
|
+
protected mapToEntity(row: any): User { /* ... */ }
|
|
98
|
+
protected mapToData(entity: User): any { /* ... */ }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
class ProductRepository extends BaseRepository<Product> {
|
|
102
|
+
protected tableName = 'products';
|
|
103
|
+
protected mapToEntity(row: any): Product { /* ... */ }
|
|
104
|
+
protected mapToData(entity: Product): any { /* ... */ }
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Example 3: Composition for Shared Logic
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
// โ
GOOD: Shared service for common operations
|
|
112
|
+
class ErrorLogger {
|
|
113
|
+
logError(context: string, error: Error): void {
|
|
114
|
+
console.error(`[${context}] ${error.message}`);
|
|
115
|
+
this.sendToMonitoring(error);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private sendToMonitoring(error: Error): void {
|
|
119
|
+
// Send to monitoring service
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
class UserService {
|
|
124
|
+
constructor(private errorLogger: ErrorLogger) {}
|
|
125
|
+
|
|
126
|
+
async getUser(id: string): Promise<User> {
|
|
127
|
+
try {
|
|
128
|
+
return await this.userRepo.findById(id);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
this.errorLogger.logError('UserService.getUser', error);
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
class OrderService {
|
|
137
|
+
constructor(private errorLogger: ErrorLogger) {}
|
|
138
|
+
|
|
139
|
+
async getOrder(id: string): Promise<Order> {
|
|
140
|
+
try {
|
|
141
|
+
return await this.orderRepo.findById(id);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
this.errorLogger.logError('OrderService.getOrder', error);
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
26
148
|
```
|
|
27
149
|
|
|
28
150
|
## โ Invalid Code Examples
|
|
29
151
|
|
|
30
|
-
|
|
31
|
-
|
|
152
|
+
### Example 1: Duplicated Validation Logic
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// โ BAD: Duplicated validation in multiple functions
|
|
156
|
+
function registerUser(userData: UserData): void {
|
|
157
|
+
// Duplicated validation logic
|
|
158
|
+
if (!userData.email || !userData.email.includes('@')) {
|
|
159
|
+
throw new ValidationError('Invalid email format');
|
|
160
|
+
}
|
|
161
|
+
if (!userData.password || userData.password.length < 8) {
|
|
162
|
+
throw new ValidationError('Password must be at least 8 characters');
|
|
163
|
+
}
|
|
164
|
+
// Registration logic
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function updateUserProfile(userId: string, profileData: ProfileData): void {
|
|
168
|
+
// Same validation logic duplicated here
|
|
169
|
+
if (profileData.email && (!profileData.email || !profileData.email.includes('@'))) {
|
|
170
|
+
throw new ValidationError('Invalid email format');
|
|
171
|
+
}
|
|
172
|
+
if (profileData.password && (!profileData.password || profileData.password.length < 8)) {
|
|
173
|
+
throw new ValidationError('Password must be at least 8 characters');
|
|
174
|
+
}
|
|
175
|
+
// Update logic
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Violation Message:**
|
|
32
180
|
```
|
|
181
|
+
Duplicate function detected (12 non-comment lines). Extract into a shared utility
|
|
182
|
+
module or helper file. Found in 2 locations: registerUser.ts:5-16, updateUserProfile.ts:10-21
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Example 2: Duplicated Repository Logic
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
// โ BAD: Same database logic repeated in multiple repositories
|
|
189
|
+
class UserRepository {
|
|
190
|
+
async findById(id: string): Promise<User | null> {
|
|
191
|
+
const result = await this.db.query(
|
|
192
|
+
'SELECT * FROM users WHERE id = ?',
|
|
193
|
+
[id]
|
|
194
|
+
);
|
|
195
|
+
if (result.length === 0) return null;
|
|
196
|
+
return {
|
|
197
|
+
id: result[0].id,
|
|
198
|
+
name: result[0].name,
|
|
199
|
+
email: result[0].email
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
class ProductRepository {
|
|
205
|
+
async findById(id: string): Promise<Product | null> {
|
|
206
|
+
const result = await this.db.query(
|
|
207
|
+
'SELECT * FROM products WHERE id = ?',
|
|
208
|
+
[id]
|
|
209
|
+
);
|
|
210
|
+
if (result.length === 0) return null;
|
|
211
|
+
return {
|
|
212
|
+
id: result[0].id,
|
|
213
|
+
name: result[0].name,
|
|
214
|
+
price: result[0].price
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**Suggested Fix:**
|
|
221
|
+
- Use inheritance to share common behavior
|
|
222
|
+
- Extract shared logic into a base class or mixin
|
|
33
223
|
|
|
34
224
|
## โ๏ธ Configuration
|
|
35
225
|
|
|
36
226
|
```json
|
|
37
227
|
{
|
|
38
228
|
"rules": {
|
|
39
|
-
"C002_no_duplicate_code": "error"
|
|
229
|
+
"C002_no_duplicate_code": ["error", {
|
|
230
|
+
"minLines": 10,
|
|
231
|
+
"similarityThreshold": 0.85,
|
|
232
|
+
"ignoreComments": true,
|
|
233
|
+
"ignoreWhitespace": true,
|
|
234
|
+
"ignoreEmptyLines": true
|
|
235
|
+
}]
|
|
40
236
|
}
|
|
41
237
|
}
|
|
42
238
|
```
|
|
43
239
|
|
|
240
|
+
**Configuration Options:**
|
|
241
|
+
- `minLines` (default: 10): Minimum number of lines to consider as duplicate
|
|
242
|
+
- `similarityThreshold` (default: 0.85): Similarity percentage (0-1) to detect near-duplicates
|
|
243
|
+
- `ignoreComments` (default: true): Ignore comments when comparing code
|
|
244
|
+
- `ignoreWhitespace` (default: true): Ignore whitespace differences
|
|
245
|
+
- `ignoreEmptyLines` (default: true): Ignore empty lines
|
|
246
|
+
|
|
247
|
+
## ๐ง Refactoring Strategies
|
|
248
|
+
|
|
249
|
+
### 1. Extract Functions/Utilities
|
|
250
|
+
When you have duplicated logic in multiple functions:
|
|
251
|
+
```typescript
|
|
252
|
+
// Extract common logic into a utility function
|
|
253
|
+
const validateUserInput = (email: string, password: string) => {
|
|
254
|
+
validateEmail(email);
|
|
255
|
+
validatePassword(password);
|
|
256
|
+
};
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### 2. Use Inheritance
|
|
260
|
+
When you have duplicated logic in multiple classes with similar responsibilities:
|
|
261
|
+
```typescript
|
|
262
|
+
// Create a base class with common behavior
|
|
263
|
+
abstract class BaseService {
|
|
264
|
+
protected abstract entityName: string;
|
|
265
|
+
|
|
266
|
+
protected async findById(id: string) {
|
|
267
|
+
// Common implementation
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### 3. Use Composition
|
|
273
|
+
When inheritance doesn't fit or you need more flexibility:
|
|
274
|
+
```typescript
|
|
275
|
+
// Create a shared service that can be injected
|
|
276
|
+
class CommonOperations {
|
|
277
|
+
logError(context: string, error: Error) { /* ... */ }
|
|
278
|
+
validateInput(data: any) { /* ... */ }
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### 4. Create Shared Libraries
|
|
283
|
+
For cross-cutting concerns used across multiple modules:
|
|
284
|
+
```typescript
|
|
285
|
+
// utils/validation.ts
|
|
286
|
+
export const validators = {
|
|
287
|
+
email: (email: string) => { /* ... */ },
|
|
288
|
+
password: (password: string) => { /* ... */ }
|
|
289
|
+
};
|
|
290
|
+
```
|
|
291
|
+
|
|
44
292
|
## ๐งช Testing
|
|
45
293
|
|
|
46
294
|
```bash
|
|
@@ -49,9 +297,26 @@ npm test -- c002_no_duplicate_code
|
|
|
49
297
|
|
|
50
298
|
# Test with SunLint CLI
|
|
51
299
|
sunlint --rules=C002_no_duplicate_code --input=examples/
|
|
300
|
+
|
|
301
|
+
# Analyze specific files
|
|
302
|
+
sunlint analyze --rules=C002 src/**/*.ts
|
|
52
303
|
```
|
|
53
304
|
|
|
305
|
+
## ๐ Related Rules
|
|
306
|
+
|
|
307
|
+
- **C005**: Each function should do one thing (Single Responsibility)
|
|
308
|
+
- **C047**: Retry logic must not be duplicated
|
|
309
|
+
- **C014**: Use Dependency Injection instead of direct instantiation
|
|
310
|
+
|
|
311
|
+
## ๐ References
|
|
312
|
+
|
|
313
|
+
- [DRY Principle](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself)
|
|
314
|
+
- [Code Duplication (Martin Fowler)](https://refactoring.guru/smells/duplicate-code)
|
|
315
|
+
- [SonarQube: Duplicated Blocks](https://rules.sonarsource.com/java/RSPEC-1192)
|
|
316
|
+
|
|
54
317
|
---
|
|
55
318
|
|
|
56
|
-
**Migration Status**:
|
|
57
|
-
**Last Updated**: 2025-
|
|
319
|
+
**Migration Status**: active
|
|
320
|
+
**Last Updated**: 2025-10-16
|
|
321
|
+
|
|
322
|
+
````
|
package/rules/index.js
CHANGED
|
@@ -48,6 +48,7 @@ function loadRuleConfig(category, ruleId) {
|
|
|
48
48
|
|
|
49
49
|
// ๐น Common Rules (C-series) - General coding standards
|
|
50
50
|
const commonRules = {
|
|
51
|
+
C002: loadRule('common', 'C002_no_duplicate_code'),
|
|
51
52
|
C006: loadRule('common', 'C006_function_naming'),
|
|
52
53
|
C012: loadRule('common', 'C012_command_query_separation'),
|
|
53
54
|
C013: loadRule('common', 'C013_no_dead_code'),
|
|
@@ -59,17 +60,20 @@ const commonRules = {
|
|
|
59
60
|
C024: loadRule('common', 'C024_no_scatter_hardcoded_constants'),
|
|
60
61
|
C029: loadRule('common', 'C029_catch_block_logging'),
|
|
61
62
|
C031: loadRule('common', 'C031_validation_separation'),
|
|
63
|
+
C033: loadRule('common', 'C033_separate_service_repository'),
|
|
62
64
|
C041: loadRule('common', 'C041_no_sensitive_hardcode'),
|
|
63
65
|
C042: loadRule('common', 'C042_boolean_name_prefix'),
|
|
66
|
+
// C047: loadRule('common', 'C047_no_duplicate_retry_logic'),
|
|
64
67
|
C048: loadRule('common', 'C048_no_bypass_architectural_layers'),
|
|
65
68
|
C052: loadRule('common', 'C052_parsing_or_data_transformation'),
|
|
66
|
-
C047: loadRule('common', 'C047_no_duplicate_retry_logic'),
|
|
67
69
|
C060: loadRule('common', 'C060_no_override_superclass'),
|
|
70
|
+
C067: loadRule('common', 'C067_no_hardcoded_config'),
|
|
68
71
|
};
|
|
69
72
|
|
|
70
73
|
// ๐ Security Rules (S-series) - Ready for migration
|
|
71
74
|
const securityRules = {
|
|
72
75
|
S006: loadRule('security', 'S006_no_plaintext_recovery_codes'),
|
|
76
|
+
S010: loadRule('security', 'S010_no_insecure_encryption'),
|
|
73
77
|
S015: loadRule('security', 'S015_insecure_tls_certificate'),
|
|
74
78
|
S023: loadRule('security', 'S023_no_json_injection'),
|
|
75
79
|
S026: loadRule('security', 'S026_json_schema_validation'),
|