@sun-asterisk/sunlint 1.3.45 → 1.3.47
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/core/adapters/sunlint-rule-adapter.js +16 -0
- package/core/cli-action-handler.js +3 -0
- package/core/cli-program.js +7 -0
- package/core/rule-selection-service.js +96 -3
- package/engines/heuristic-engine.js +6 -1
- package/package.json +2 -2
- package/skill-assets/sunlint-code-quality/rules/ruby/C006-verb-noun-functions.md +63 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C013-no-dead-code.md +48 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C014-dependency-injection.md +42 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C017-no-constructor-logic.md +42 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C018-generic-errors.md +41 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C019-error-log-level.md +41 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C020-no-unused-imports.md +36 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C022-no-unused-variables.md +31 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C023-no-duplicate-names.md +39 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C024-centralize-constants.md +35 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C029-catch-log-root-cause.md +34 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C030-custom-error-classes.md +32 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C033-separate-data-access.md +52 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C035-error-context-logging.md +34 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C041-no-hardcoded-secrets.md +29 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C042-boolean-naming.md +38 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C052-controller-parsing.md +37 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C060-superclass-logic.md +38 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/C067-no-hardcoded-config.md +37 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S003-open-redirect.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S004-no-log-credentials.md +38 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S005-server-authorization.md +37 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S006-default-credentials.md +29 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S007-output-encoding.md +31 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S009-approved-crypto.md +31 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S010-csprng.md +30 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S011-encrypted-client-hello.md +27 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S012-secrets-management.md +28 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S013-tls-connections.md +30 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S016-no-sensitive-query-string.md +37 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S017-parameterized-queries.md +33 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S019-email-input-sanitization.md +31 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S020-eval-code-execution.md +36 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S022-context-escaping.md +36 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S023-dynamic-js-encoding.md +33 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S025-server-validation.md +30 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S026-tls-encryption.md +30 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S027-mtls-validation.md +26 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S028-upload-limits.md +33 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S029-csrf-protection.md +32 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S030-directory-browsing.md +30 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S031-secure-cookie-flag.md +27 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S032-httponly-cookie.md +26 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S033-samesite-cookie.md +29 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S034-host-prefix-cookie.md +30 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S035-app-hostnames.md +28 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S036-internal-file-paths.md +37 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S037-anti-cache-headers.md +31 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S039-tls-certificate-validation.md +29 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S041-logout-invalidation.md +31 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S042-long-lived-sessions.md +27 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S044-critical-changes-reauth.md +34 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S045-brute-force-protection.md +33 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S047-oauth-csrf-protection.md +33 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S048-oauth-redirect-validation.md +29 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S049-auth-code-expiry.md +31 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S050-token-entropy.md +26 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S051-password-length.md +38 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S052-otp-entropy.md +25 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S053-generic-error-messages.md +33 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S054-no-default-admin.md +29 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S055-content-type-validation.md +24 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S056-log-injection.md +28 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S057-synchronized-time.md +18 -0
- package/skill-assets/sunlint-code-quality/rules/ruby/S058-ssrf-protection.md +39 -0
|
@@ -203,6 +203,22 @@ class SunlintRuleAdapter {
|
|
|
203
203
|
return Array.from(this.rulesCache.keys());
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Get rules registry for severity resolution
|
|
208
|
+
* Returns object with rules property compatible with ConfigValidator
|
|
209
|
+
*/
|
|
210
|
+
getRulesRegistry() {
|
|
211
|
+
if (this.registryCache) {
|
|
212
|
+
return { rules: this.registryCache };
|
|
213
|
+
}
|
|
214
|
+
// Convert cache map to registry format
|
|
215
|
+
const rules = {};
|
|
216
|
+
this.rulesCache.forEach((rule, ruleId) => {
|
|
217
|
+
rules[ruleId] = rule;
|
|
218
|
+
});
|
|
219
|
+
return { rules };
|
|
220
|
+
}
|
|
221
|
+
|
|
206
222
|
/**
|
|
207
223
|
* Validate rule ID
|
|
208
224
|
*/
|
|
@@ -406,6 +406,7 @@ class CliActionHandler {
|
|
|
406
406
|
*/
|
|
407
407
|
getPresetName() {
|
|
408
408
|
if (this.options.all) return 'all preset';
|
|
409
|
+
if (this.options.specific) return 'language-specific preset';
|
|
409
410
|
if (this.options.quality) return 'quality preset';
|
|
410
411
|
if (this.options.security) return 'security preset';
|
|
411
412
|
if (this.options.category) return `${this.options.category} preset`;
|
|
@@ -525,6 +526,7 @@ class CliActionHandler {
|
|
|
525
526
|
return this.options.architecture &&
|
|
526
527
|
!this.options.impact &&
|
|
527
528
|
!this.options.all &&
|
|
529
|
+
!this.options.specific &&
|
|
528
530
|
!this.options.rule &&
|
|
529
531
|
!this.options.rules &&
|
|
530
532
|
!this.options.quality &&
|
|
@@ -540,6 +542,7 @@ class CliActionHandler {
|
|
|
540
542
|
return this.options.impact &&
|
|
541
543
|
!this.options.architecture &&
|
|
542
544
|
!this.options.all &&
|
|
545
|
+
!this.options.specific &&
|
|
543
546
|
!this.options.rule &&
|
|
544
547
|
!this.options.rules &&
|
|
545
548
|
!this.options.quality &&
|
package/core/cli-program.js
CHANGED
|
@@ -22,6 +22,7 @@ function createCliProgram() {
|
|
|
22
22
|
.option('--rules <rules>', 'Run multiple rules (comma-separated)')
|
|
23
23
|
.option('-a, --all', 'Run all available rules')
|
|
24
24
|
.option('-c, --category <category>', 'Run rules by category (quality, security, logging, naming)')
|
|
25
|
+
.option('-s, --specific', 'Run language-specific rules (requires --languages)')
|
|
25
26
|
.option('--quality', 'Run all code quality rules')
|
|
26
27
|
.option('--security', 'Run all secure coding rules');
|
|
27
28
|
|
|
@@ -108,6 +109,12 @@ Examples:
|
|
|
108
109
|
sunlint --quality --input=src # Quality rules only
|
|
109
110
|
sunlint --security --input=src # Security rules only
|
|
110
111
|
|
|
112
|
+
Language-Specific:
|
|
113
|
+
sunlint --specific --languages=dart --input=lib # Dart rules only
|
|
114
|
+
sunlint --specific --languages=kotlin --input=app/src # Kotlin rules only
|
|
115
|
+
sunlint --specific --languages=dart,kotlin --input=. # Dart + Kotlin rules
|
|
116
|
+
sunlint --all --languages=dart --input=lib # All rules + Dart rules
|
|
117
|
+
|
|
111
118
|
Git Integration:
|
|
112
119
|
sunlint --all --changed-files --input=. # Changed files only
|
|
113
120
|
sunlint --all --staged-files --input=. # Staged files only
|
|
@@ -10,11 +10,13 @@ const fs = require('fs');
|
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const RuleMappingService = require('./rule-mapping-service');
|
|
12
12
|
const SunlintRuleAdapter = require('./adapters/sunlint-rule-adapter');
|
|
13
|
+
const ConfigValidator = require('./config-validator');
|
|
13
14
|
|
|
14
15
|
class RuleSelectionService {
|
|
15
16
|
constructor() {
|
|
16
17
|
this.ruleAdapter = SunlintRuleAdapter.getInstance();
|
|
17
18
|
this.ruleMappingService = new RuleMappingService();
|
|
19
|
+
this.configValidator = new ConfigValidator();
|
|
18
20
|
this.initialized = false;
|
|
19
21
|
// Path works both in dev (from pages/) and npm package (from config/)
|
|
20
22
|
this.releasedRulesPath = path.join(__dirname, '../config/released-rules.json');
|
|
@@ -60,6 +62,48 @@ class RuleSelectionService {
|
|
|
60
62
|
}
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Collect language-specific rules from ALL versions of released-rules.json
|
|
67
|
+
* Searches across all versions because the latest version may have empty categories
|
|
68
|
+
* @param {string[]} languages - Array of language names (e.g., ['dart', 'kotlin'])
|
|
69
|
+
* @returns {string[]} Array of rule IDs matching the specified languages
|
|
70
|
+
*/
|
|
71
|
+
collectLanguageSpecificRules(languages) {
|
|
72
|
+
const { getLanguageFromRuleId } = require('./constants/rules');
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
if (!fs.existsSync(this.releasedRulesPath)) {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const data = JSON.parse(fs.readFileSync(this.releasedRulesPath, 'utf8'));
|
|
80
|
+
const versions = data.versions || [];
|
|
81
|
+
|
|
82
|
+
if (versions.length === 0) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const targetLanguages = new Set(languages.map(l => l.toLowerCase()));
|
|
87
|
+
const matchingRuleIds = new Set();
|
|
88
|
+
|
|
89
|
+
for (const versionEntry of versions) {
|
|
90
|
+
const rulesByCategory = versionEntry.rulesByCategory || {};
|
|
91
|
+
for (const categoryRules of Object.values(rulesByCategory)) {
|
|
92
|
+
for (const ruleId of categoryRules) {
|
|
93
|
+
const ruleLanguage = getLanguageFromRuleId(ruleId);
|
|
94
|
+
if (ruleLanguage && targetLanguages.has(ruleLanguage)) {
|
|
95
|
+
matchingRuleIds.add(ruleId);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return Array.from(matchingRuleIds);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
63
107
|
async selectRules(config, options) {
|
|
64
108
|
// Ensure adapter is initialized
|
|
65
109
|
await this.initialize();
|
|
@@ -75,6 +119,22 @@ class RuleSelectionService {
|
|
|
75
119
|
selectedRules = [options.rule];
|
|
76
120
|
} else if (options.rules) {
|
|
77
121
|
selectedRules = options.rules.split(',').map(r => r.trim());
|
|
122
|
+
} else if (options.specific) {
|
|
123
|
+
// --specific requires --languages to determine which language rules to load
|
|
124
|
+
if (!options.languages) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
chalk.red('--specific requires --languages to be specified\n') +
|
|
127
|
+
chalk.gray('Example: sunlint --specific --languages=dart --input=lib')
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
const targetLanguages = options.languages.split(',').map(l => l.trim());
|
|
131
|
+
selectedRules = this.collectLanguageSpecificRules(targetLanguages);
|
|
132
|
+
|
|
133
|
+
if (selectedRules.length === 0) {
|
|
134
|
+
console.warn(
|
|
135
|
+
chalk.yellow(`\u26A0\uFE0F No language-specific rules found for: ${targetLanguages.join(', ')}`)
|
|
136
|
+
);
|
|
137
|
+
}
|
|
78
138
|
} else if (options.all) {
|
|
79
139
|
// Load all rules from released-rules.json
|
|
80
140
|
if (releasedRules) {
|
|
@@ -89,6 +149,20 @@ class RuleSelectionService {
|
|
|
89
149
|
// Fallback to preset file
|
|
90
150
|
selectedRules = this.loadPresetRules('all');
|
|
91
151
|
}
|
|
152
|
+
|
|
153
|
+
// Augment with language-specific rules when --languages is specified
|
|
154
|
+
// This handles the case where the latest version has empty categories
|
|
155
|
+
if (options.languages) {
|
|
156
|
+
const targetLanguages = options.languages.split(',').map(l => l.trim());
|
|
157
|
+
const languageRules = this.collectLanguageSpecificRules(targetLanguages);
|
|
158
|
+
const existingRuleSet = new Set(selectedRules);
|
|
159
|
+
|
|
160
|
+
for (const ruleId of languageRules) {
|
|
161
|
+
if (!existingRuleSet.has(ruleId)) {
|
|
162
|
+
selectedRules.push(ruleId);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
92
166
|
} else if (options.quality) {
|
|
93
167
|
// Load Common rules from released-rules.json
|
|
94
168
|
if (releasedRules && releasedRules.Common) {
|
|
@@ -137,14 +211,33 @@ class RuleSelectionService {
|
|
|
137
211
|
selectedRules = selectedRules.filter(ruleId => !disabledRules.has(ruleId));
|
|
138
212
|
}
|
|
139
213
|
|
|
140
|
-
//
|
|
214
|
+
// Get rules registry for severity resolution
|
|
215
|
+
const rulesRegistry = this.ruleAdapter.getRulesRegistry();
|
|
216
|
+
|
|
217
|
+
// Convert to rule objects with proper severity from config
|
|
141
218
|
return selectedRules.map(ruleId => {
|
|
142
219
|
const adapterRule = this.ruleAdapter.getRuleById(ruleId);
|
|
220
|
+
|
|
221
|
+
// Resolve severity from config (config.rules > config.categories > rule default)
|
|
222
|
+
const effectiveConfig = this.configValidator.getEffectiveRuleConfiguration(
|
|
223
|
+
ruleId,
|
|
224
|
+
config,
|
|
225
|
+
rulesRegistry
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
// Map config values to severity (error/warn)
|
|
229
|
+
let severity = 'warning';
|
|
230
|
+
if (effectiveConfig === 'error' || effectiveConfig === 2) {
|
|
231
|
+
severity = 'error';
|
|
232
|
+
} else if (effectiveConfig === 'warn' || effectiveConfig === 'warning' || effectiveConfig === 1) {
|
|
233
|
+
severity = 'warning';
|
|
234
|
+
}
|
|
235
|
+
|
|
143
236
|
return {
|
|
237
|
+
...(adapterRule || {}),
|
|
144
238
|
id: ruleId,
|
|
145
239
|
name: this.getRuleName(ruleId),
|
|
146
|
-
severity
|
|
147
|
-
...(adapterRule || {})
|
|
240
|
+
severity // Must come AFTER spread to override adapter's severity
|
|
148
241
|
};
|
|
149
242
|
}).filter(rule => rule.id);
|
|
150
243
|
}
|
|
@@ -1002,7 +1002,12 @@ class HeuristicEngine extends AnalysisEngineInterface {
|
|
|
1002
1002
|
fileResult = { file: filePath, violations: [] };
|
|
1003
1003
|
results.results.push(fileResult);
|
|
1004
1004
|
}
|
|
1005
|
-
|
|
1005
|
+
// Apply rule severity to each violation (from config)
|
|
1006
|
+
const violationsWithSeverity = violations.map(v => ({
|
|
1007
|
+
...v,
|
|
1008
|
+
severity: rule.severity || v.severity || 'warning'
|
|
1009
|
+
}));
|
|
1010
|
+
fileResult.violations.push(...violationsWithSeverity);
|
|
1006
1011
|
}
|
|
1007
1012
|
}
|
|
1008
1013
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sun-asterisk/sunlint",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.47",
|
|
4
4
|
"description": "☀️ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards",
|
|
5
5
|
"main": "cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"eslint-plugin-react": "^7.37.5",
|
|
65
65
|
"eslint-plugin-react-hooks": "^5.2.0",
|
|
66
66
|
"espree": "^10.3.0",
|
|
67
|
-
"glob": "^
|
|
67
|
+
"glob": "^10.5.0",
|
|
68
68
|
"minimatch": "^10.0.3",
|
|
69
69
|
"node-fetch": "^3.3.2",
|
|
70
70
|
"table": "^6.8.2",
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Function Names Verb-Noun
|
|
3
|
+
impact: LOW
|
|
4
|
+
impactDescription: makes code self-documenting
|
|
5
|
+
tags: naming, functions, readability, conventions, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Function Names Verb-Noun
|
|
9
|
+
|
|
10
|
+
Methods in Ruby should describe actions clearly. Action verbs make the purpose manifest.
|
|
11
|
+
|
|
12
|
+
**Incorrect (vague names):**
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
def user # Noun only
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def user_data # Noun only
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def do_something # Vague
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def handle_stuff # Vague
|
|
25
|
+
end
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Correct (action verbs):**
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
def fetch_user
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def create_user_account
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def validate_email_format?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def calculate_total_price
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def send_confirmation_email
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def convert_currency_to_usd
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Verb categories:**
|
|
51
|
+
|
|
52
|
+
| Category | Verbs |
|
|
53
|
+
|----------|-------|
|
|
54
|
+
| Retrieval | `get`, `fetch`, `find`, `load`, `query` |
|
|
55
|
+
| Creation | `create`, `build`, `make`, `generate` |
|
|
56
|
+
| Modification | `set`, `update`, `modify`, `change` |
|
|
57
|
+
| Deletion | `delete`, `remove`, `destroy`, `clear` |
|
|
58
|
+
| Validation | `validate`, `verify`, `check`, `ensure` |
|
|
59
|
+
| Computation | `calculate`, `compute`, `parse`, `format` |
|
|
60
|
+
| Boolean | `is`, `has`, `can`, `should`, `will` |
|
|
61
|
+
|
|
62
|
+
**Tools:** PR review, RuboCop
|
|
63
|
+
---
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Do Not Use Dead Code
|
|
3
|
+
impact: LOW
|
|
4
|
+
impactDescription: reduces codebase noise and maintenance effort
|
|
5
|
+
tags: dead-code, cleanup, maintenance, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Do Not Use Dead Code
|
|
9
|
+
|
|
10
|
+
Dead code confuses readers and clutters the codebase. Git history preserves deleted code, so there is no need to keep it in the active source files.
|
|
11
|
+
|
|
12
|
+
**Incorrect (keeping dead code):**
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
def process_order(order)
|
|
16
|
+
# Old implementation - keeping for reference
|
|
17
|
+
# total = order.items.inject(0) do |sum, item|
|
|
18
|
+
# sum + item.price * item.quantity
|
|
19
|
+
# end
|
|
20
|
+
|
|
21
|
+
total = calculate_total(order)
|
|
22
|
+
total
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Unused function - someone might need it later
|
|
26
|
+
def legacy_calculation
|
|
27
|
+
end
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Correct (clean code):**
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
def process_order(order)
|
|
34
|
+
calculate_total(order)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Delete unused methods - git history preserves them
|
|
38
|
+
# Delete commented code - git history preserves it
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Types of dead code:**
|
|
42
|
+
- Commented-out code
|
|
43
|
+
- Unused methods/classes
|
|
44
|
+
- Unreachable code
|
|
45
|
+
- Unused variables
|
|
46
|
+
|
|
47
|
+
**Tools:** RuboCop, debride
|
|
48
|
+
---
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use Dependency Injection
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: enables testable business logic and decupling
|
|
5
|
+
tags: dependency-injection, decoupling, testability, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Use Dependency Injection
|
|
9
|
+
|
|
10
|
+
Hardcoding dependencies inside classes makes them difficult to test and tightly coupled. Pass dependencies through the constructor (`initialize`).
|
|
11
|
+
|
|
12
|
+
**Incorrect (tightly coupled):**
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
class OrderService
|
|
16
|
+
def process(order)
|
|
17
|
+
# Hardcoded dependency
|
|
18
|
+
mailer = UserMailer.new
|
|
19
|
+
mailer.send_confirmation(order.user)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Correct (dependency injection):**
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
class OrderService
|
|
28
|
+
def initialize(mailer = UserMailer.new)
|
|
29
|
+
@mailer = mailer
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def process(order)
|
|
33
|
+
@mailer.send_confirmation(order.user)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# In tests:
|
|
38
|
+
# service = OrderService.new(MockMailer.new)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Tools:** PR review
|
|
42
|
+
---
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: No Complex Logic in Constructor
|
|
3
|
+
impact: LOW
|
|
4
|
+
impactDescription: makes objects easy to instantiate and test
|
|
5
|
+
tags: constructor, initialization, testability, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## No Complex Logic in Constructor
|
|
9
|
+
|
|
10
|
+
The `initialize` method should only set up initial state. Avoid performing computations, database queries, or network calls during object construction.
|
|
11
|
+
|
|
12
|
+
**Incorrect (complex initialize):**
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
class UserProfile
|
|
16
|
+
def initialize(user_id)
|
|
17
|
+
@user = User.find(user_id) # DB query in constructor
|
|
18
|
+
@stats = RemoteApiService.fetch_stats(user_id) # Network call
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Correct (simple initialize):**
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
class UserProfile
|
|
27
|
+
def initialize(user, stats)
|
|
28
|
+
@user = user
|
|
29
|
+
@stats = stats
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Use a factory method or dependency injection
|
|
33
|
+
def self.build(user_id)
|
|
34
|
+
user = User.find(user_id)
|
|
35
|
+
stats = RemoteApiService.fetch_stats(user_id)
|
|
36
|
+
new(user, stats)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Tools:** PR review
|
|
42
|
+
---
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Catch Specific Exceptions
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: prevents hiding critical system errors and unexpected behavior
|
|
5
|
+
tags: error-handling, maintenance, safety, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Catch Specific Exceptions
|
|
9
|
+
|
|
10
|
+
Avoid catching the generic `Exception` or `StandardError` classes without a good reason. This can hide bugs and system-level issues like `NoMemoryError` or `SignalException`.
|
|
11
|
+
|
|
12
|
+
**Incorrect (generic rescue):**
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
begin
|
|
16
|
+
do_something
|
|
17
|
+
rescue => e # Rescues StandardError
|
|
18
|
+
log_error(e)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
begin
|
|
22
|
+
do_something
|
|
23
|
+
rescue Exception => e # Even worse, rescues everything
|
|
24
|
+
log_error(e)
|
|
25
|
+
end
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Correct (specific rescue):**
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
begin
|
|
32
|
+
do_something
|
|
33
|
+
rescue ActiveRecord::RecordNotFound => e
|
|
34
|
+
handle_missing_record(e)
|
|
35
|
+
rescue JSON::ParserError => e
|
|
36
|
+
handle_invalid_json(e)
|
|
37
|
+
end
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Tools:** RuboCop (`Style/RescueStandardError`)
|
|
41
|
+
---
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use Appropriate Log Levels
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: facilitates effective monitoring and troubleshooting
|
|
5
|
+
tags: logging, observability, monitoring, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Use Appropriate Log Levels
|
|
9
|
+
|
|
10
|
+
Use the correct log level (`debug`, `info`, `warn`, `error`, `fatal`) to distinguish between operational noise and critical issues.
|
|
11
|
+
|
|
12
|
+
**Incorrect (misused levels):**
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
begin
|
|
16
|
+
process_data
|
|
17
|
+
rescue => e
|
|
18
|
+
Rails.logger.info "Error happened: #{e.message}" # Error logged as Info
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
Rails.logger.error "Variable x is #{x}" # Debugging info logged as Error
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Correct (appropriate levels):**
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
Rails.logger.debug "Processing payload: #{payload}"
|
|
28
|
+
|
|
29
|
+
begin
|
|
30
|
+
process_data
|
|
31
|
+
rescue => e
|
|
32
|
+
Rails.logger.error "Data processing failed: #{e.message}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
if retry_count > 0
|
|
36
|
+
Rails.logger.warn "Retry attempt #{retry_count} for user #{user_id}"
|
|
37
|
+
end
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Tools:** Manual Review
|
|
41
|
+
---
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: No Unused Requires
|
|
3
|
+
impact: LOW
|
|
4
|
+
impactDescription: reduces startup time and avoids unnecessary dependencies
|
|
5
|
+
tags: cleanup, maintenance, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## No Unused Requires
|
|
9
|
+
|
|
10
|
+
Avoid requiring libraries or files that are not used in the current file. This clutters the code and may slightly impact load times.
|
|
11
|
+
|
|
12
|
+
**Incorrect (unused requires):**
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
require 'json' # Not used
|
|
16
|
+
require 'net/http' # Not used
|
|
17
|
+
|
|
18
|
+
class MyService
|
|
19
|
+
def call
|
|
20
|
+
puts "Hello"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Correct (clean requires):**
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
class MyService
|
|
29
|
+
def call
|
|
30
|
+
puts "Hello"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Tools:** RuboCop
|
|
36
|
+
---
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: No Unused Variables
|
|
3
|
+
impact: LOW
|
|
4
|
+
impactDescription: reduces confusion and identifies potential logic errors
|
|
5
|
+
tags: cleanup, quality, maintenance
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## No Unused Variables
|
|
9
|
+
|
|
10
|
+
Variables that are declared but never used often indicate a typo or a forgotten piece of logic.
|
|
11
|
+
|
|
12
|
+
**Incorrect (unused variables):**
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
def calculate(a, b)
|
|
16
|
+
result = a + b
|
|
17
|
+
temp_data = fetch_additional_info # Fetched but never used
|
|
18
|
+
result
|
|
19
|
+
end
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Correct (clean variables):**
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
def calculate(a, b)
|
|
26
|
+
a + b
|
|
27
|
+
end
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Tools:** RuboCop (`Lint/UselessAssignment`)
|
|
31
|
+
---
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Avoid Duplicate Naming
|
|
3
|
+
impact: LOW
|
|
4
|
+
impactDescription: prevents confusion and shadowing
|
|
5
|
+
tags: naming, quality, maintenance
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Avoid Duplicate Naming
|
|
9
|
+
|
|
10
|
+
Avoid using the same name for local variables, method arguments, and instance variables if it causes confusion or shadowing.
|
|
11
|
+
|
|
12
|
+
**Incorrect (shadowing/ambiguity):**
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
class Processor
|
|
16
|
+
attr_reader :data
|
|
17
|
+
|
|
18
|
+
def process(data) # Argument shadows attribute
|
|
19
|
+
data = data.clean # Shadowing
|
|
20
|
+
@data = data
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Correct (distinct names):**
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
class Processor
|
|
29
|
+
attr_reader :data
|
|
30
|
+
|
|
31
|
+
def process(raw_data)
|
|
32
|
+
cleaned_data = raw_data.clean
|
|
33
|
+
@data = cleaned_data
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Tools:** RuboCop (`Lint/ShadowingOuterLocalVariable`)
|
|
39
|
+
---
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Centralize Constants
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: improves maintainability and avoids magic numbers
|
|
5
|
+
tags: constants, maintainability, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Centralize Constants
|
|
9
|
+
|
|
10
|
+
Avoid hardcoding magic strings and numbers throughout your logic. Define them as constants at the top of the class or in a specific module.
|
|
11
|
+
|
|
12
|
+
**Incorrect (magic values):**
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
class User < ApplicationRecord
|
|
16
|
+
def premium?
|
|
17
|
+
self.score > 1000 # What is 1000?
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Correct (constants):**
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
class User < ApplicationRecord
|
|
26
|
+
PREMIUM_THRESHOLD = 1000
|
|
27
|
+
|
|
28
|
+
def premium?
|
|
29
|
+
score > PREMIUM_THRESHOLD
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Tools:** RuboCop
|
|
35
|
+
---
|