solid_score 0.2.0
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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +427 -0
- data/config/default.yml +36 -0
- data/exe/solid-score +6 -0
- data/lib/rubocop/cop/solid_score/dependency_inversion.rb +22 -0
- data/lib/rubocop/cop/solid_score/helpers.rb +74 -0
- data/lib/rubocop/cop/solid_score/interface_segregation.rb +22 -0
- data/lib/rubocop/cop/solid_score/liskov_substitution.rb +22 -0
- data/lib/rubocop/cop/solid_score/open_closed.rb +22 -0
- data/lib/rubocop/cop/solid_score/principle_base.rb +47 -0
- data/lib/rubocop/cop/solid_score/single_responsibility.rb +22 -0
- data/lib/rubocop/cop/solid_score/total_score.rb +54 -0
- data/lib/rubocop/solid_score/inject.rb +17 -0
- data/lib/rubocop-solid_score.rb +16 -0
- data/lib/solid_score/analyzers/base_analyzer.rb +17 -0
- data/lib/solid_score/analyzers/dip_analyzer.rb +173 -0
- data/lib/solid_score/analyzers/isp_analyzer.rb +131 -0
- data/lib/solid_score/analyzers/lsp_analyzer.rb +54 -0
- data/lib/solid_score/analyzers/mitigations.rb +19 -0
- data/lib/solid_score/analyzers/ocp_analyzer.rb +104 -0
- data/lib/solid_score/analyzers/srp_analyzer.rb +177 -0
- data/lib/solid_score/cli.rb +83 -0
- data/lib/solid_score/configuration.rb +159 -0
- data/lib/solid_score/diff_analyzer.rb +58 -0
- data/lib/solid_score/diff_classifier.rb +49 -0
- data/lib/solid_score/formatters/base_formatter.rb +11 -0
- data/lib/solid_score/formatters/html_formatter.rb +223 -0
- data/lib/solid_score/formatters/json_formatter.rb +45 -0
- data/lib/solid_score/formatters/text_formatter.rb +54 -0
- data/lib/solid_score/models/class_info.rb +150 -0
- data/lib/solid_score/models/method_info.rb +82 -0
- data/lib/solid_score/models/score_result.rb +95 -0
- data/lib/solid_score/parser/ruby_parser.rb +451 -0
- data/lib/solid_score/presets.rb +64 -0
- data/lib/solid_score/runner.rb +82 -0
- data/lib/solid_score/scorer.rb +35 -0
- data/lib/solid_score/version.rb +5 -0
- data/lib/solid_score.rb +28 -0
- metadata +114 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 465bf41b4b60ca3d4f6c9eecc7de89bb6bcf6dae598a0fa8052bca0e34a16dbf
|
|
4
|
+
data.tar.gz: 235fd46777ef84ab2fd31620a0171f269c1f56ff9347bdac4e98731f7a76c5c0
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5522bbc639d6d4b88a89973f10dda2f58d59266d378361441c0bec332b355a0134255297e8d9a4888b486d1566c78b3791ab35aab8e3ef303dd88a6016d718c8
|
|
7
|
+
data.tar.gz: a81c10321ab5f6e75ca1d134caa1b8111112c98e9a0d3108b9f1313a338491be83c1fbad17f4b1fc55d6eaefc143692e0054fb9d39c47557837ce78903c3040a
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 harachan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
# solid-score
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/rb/solid_score)
|
|
4
|
+
[](https://www.ruby-lang.org/)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
A static analysis tool that scores Ruby classes and modules against SOLID principles using AST analysis.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **SOLID Principles Analysis**: Scores each class/module on all five SOLID principles (SRP, OCP, LSP, ISP, DIP)
|
|
12
|
+
- **Multiple Output Formats**: Text (table), JSON, and HTML report output
|
|
13
|
+
- **Git Diff Mode**: Analyze only changed files to prevent score regression
|
|
14
|
+
- **Configurable Thresholds**: Set minimum scores for CI quality gates
|
|
15
|
+
- **Customizable Weights**: Adjust the importance of each principle in the total score
|
|
16
|
+
- **RuboCop Integration**: Use as RuboCop cops for seamless linting workflow
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
Add this line to your application's Gemfile:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
gem 'solid_score'
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
And then execute:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
bundle install
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or install it yourself as:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
gem install solid_score
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Analyze current directory
|
|
42
|
+
solid-score .
|
|
43
|
+
|
|
44
|
+
# Analyze specific directory
|
|
45
|
+
solid-score app/models
|
|
46
|
+
|
|
47
|
+
# Output as JSON
|
|
48
|
+
solid-score app/ --format json
|
|
49
|
+
|
|
50
|
+
# Generate HTML report
|
|
51
|
+
solid-score app/ --format html > report.html
|
|
52
|
+
|
|
53
|
+
# Analyze only changed files (git diff)
|
|
54
|
+
solid-score . --diff origin/main
|
|
55
|
+
|
|
56
|
+
# Use configuration file
|
|
57
|
+
solid-score . --config .solid-score.yml
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Usage
|
|
61
|
+
|
|
62
|
+
### Command Line Options
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
Usage: solid-score [path] [options]
|
|
66
|
+
|
|
67
|
+
Options:
|
|
68
|
+
--format FORMAT Output format: text (default), json, html
|
|
69
|
+
--config FILE Path to configuration file
|
|
70
|
+
--min-score SCORE Minimum total score (exit 1 if below)
|
|
71
|
+
--min-srp SCORE Minimum SRP score
|
|
72
|
+
--min-ocp SCORE Minimum OCP score
|
|
73
|
+
--min-lsp SCORE Minimum LSP score
|
|
74
|
+
--min-isp SCORE Minimum ISP score
|
|
75
|
+
--min-dip SCORE Minimum DIP score
|
|
76
|
+
--diff REF Analyze only files changed since REF (git ref)
|
|
77
|
+
--max-decrease SCORE Maximum allowed score decrease per class
|
|
78
|
+
--exclude PATTERN Exclude patterns (comma-separated)
|
|
79
|
+
--version Show version
|
|
80
|
+
-h, --help Show help
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Output Examples
|
|
84
|
+
|
|
85
|
+
#### Text Format (default)
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
solid-score v0.1.0
|
|
89
|
+
|
|
90
|
+
Analyzed 5 class(es)
|
|
91
|
+
|
|
92
|
+
Class SRP OCP LSP ISP DIP Total
|
|
93
|
+
-------------------------------------------------------------------------------
|
|
94
|
+
OrderService 100.0 100.0 100.0 100.0 100.0 100.0
|
|
95
|
+
UserRepository 80.0 90.0 100.0 95.0 85.0 88.0
|
|
96
|
+
PaymentProcessor 60.0 70.0 100.0 80.0 50.0 68.5
|
|
97
|
+
ShapeCalculator 60.0 35.0 100.0 100.0 100.0 78.2
|
|
98
|
+
GodClass 30.0 100.0 100.0 65.0 100.0 72.0
|
|
99
|
+
-------------------------------------------------------------------------------
|
|
100
|
+
Average 66.0 79.0 100.0 88.0 87.0 81.3
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
#### JSON Format
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"version": "0.1.0",
|
|
108
|
+
"classes": [
|
|
109
|
+
{
|
|
110
|
+
"class_name": "OrderService",
|
|
111
|
+
"file_path": "app/services/order_service.rb",
|
|
112
|
+
"srp": 100,
|
|
113
|
+
"ocp": 100.0,
|
|
114
|
+
"lsp": 100,
|
|
115
|
+
"isp": 100,
|
|
116
|
+
"dip": 100.0,
|
|
117
|
+
"total": 100.0,
|
|
118
|
+
"confidence": {
|
|
119
|
+
"srp": "high",
|
|
120
|
+
"ocp": "low",
|
|
121
|
+
"lsp": "low_medium",
|
|
122
|
+
"isp": "medium_high",
|
|
123
|
+
"dip": "high"
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
],
|
|
127
|
+
"summary": {
|
|
128
|
+
"total_classes": 1,
|
|
129
|
+
"average_score": 100.0
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Configuration
|
|
135
|
+
|
|
136
|
+
Create a `.solid-score.yml` file in your project root:
|
|
137
|
+
|
|
138
|
+
```yaml
|
|
139
|
+
# Target paths to analyze
|
|
140
|
+
paths:
|
|
141
|
+
- app/models
|
|
142
|
+
- app/services
|
|
143
|
+
- lib
|
|
144
|
+
|
|
145
|
+
# Patterns to exclude (glob format)
|
|
146
|
+
exclude:
|
|
147
|
+
- spec/**
|
|
148
|
+
- test/**
|
|
149
|
+
- tmp/**
|
|
150
|
+
- vendor/**
|
|
151
|
+
|
|
152
|
+
# Preset (loads predefined configuration)
|
|
153
|
+
# preset: rails
|
|
154
|
+
|
|
155
|
+
# Output format
|
|
156
|
+
format: text # or json or html
|
|
157
|
+
|
|
158
|
+
# Score thresholds (CI will fail if below)
|
|
159
|
+
thresholds:
|
|
160
|
+
total: 70
|
|
161
|
+
srp: 60
|
|
162
|
+
ocp: 50
|
|
163
|
+
lsp: 50
|
|
164
|
+
isp: 50
|
|
165
|
+
dip: 60
|
|
166
|
+
|
|
167
|
+
# Weight of each principle in total score (must sum to 1.0)
|
|
168
|
+
weights:
|
|
169
|
+
srp: 0.30 # 30%
|
|
170
|
+
ocp: 0.15 # 15%
|
|
171
|
+
lsp: 0.10 # 10%
|
|
172
|
+
isp: 0.20 # 20%
|
|
173
|
+
dip: 0.25 # 25%
|
|
174
|
+
|
|
175
|
+
# Git diff analysis settings
|
|
176
|
+
diff:
|
|
177
|
+
max_decrease: 5 # Max allowed score decrease per class
|
|
178
|
+
new_class_min: 70 # Minimum score for new classes
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## SOLID Principles Scoring
|
|
182
|
+
|
|
183
|
+
### SRP (Single Responsibility Principle)
|
|
184
|
+
|
|
185
|
+
**Metric**: LCOM4 (Lack of Cohesion of Methods 4)
|
|
186
|
+
|
|
187
|
+
Measures class cohesion using graph connectivity analysis. A class with high cohesion (methods working with shared instance variables) scores higher.
|
|
188
|
+
|
|
189
|
+
- **Score 100**: All methods are connected (single responsibility)
|
|
190
|
+
- **Score 0**: Methods are completely disconnected (multiple responsibilities)
|
|
191
|
+
- **Confidence**: High
|
|
192
|
+
|
|
193
|
+
### OCP (Open/Closed Principle)
|
|
194
|
+
|
|
195
|
+
**Metric**: Conditional complexity + type checking patterns
|
|
196
|
+
|
|
197
|
+
Detects patterns that suggest a class needs modification for extension:
|
|
198
|
+
- `if/else` chains with type checking (`is_a?`, `kind_of?`, `instance_of?`)
|
|
199
|
+
- `case/when` statements with multiple branches
|
|
200
|
+
- Conditional density in methods
|
|
201
|
+
|
|
202
|
+
- **Confidence**: Low-Medium (heuristic-based)
|
|
203
|
+
|
|
204
|
+
### LSP (Liskov Substitution Principle)
|
|
205
|
+
|
|
206
|
+
**Metric**: Inheritance contract compliance
|
|
207
|
+
|
|
208
|
+
Detects potential violations:
|
|
209
|
+
- Method signature changes (different parameters than parent)
|
|
210
|
+
- Missing `super` calls in overridden methods
|
|
211
|
+
- Return type inconsistencies
|
|
212
|
+
|
|
213
|
+
- **Confidence**: Low-Medium (requires runtime analysis for full accuracy)
|
|
214
|
+
|
|
215
|
+
### ISP (Interface Segregation Principle)
|
|
216
|
+
|
|
217
|
+
**Metric**: Interface bloat detection
|
|
218
|
+
|
|
219
|
+
Measures:
|
|
220
|
+
- Number of public methods
|
|
221
|
+
- Method parameter count
|
|
222
|
+
- Unused method dependencies
|
|
223
|
+
|
|
224
|
+
- **Confidence**: Medium-High
|
|
225
|
+
|
|
226
|
+
### DIP (Dependency Inversion Principle)
|
|
227
|
+
|
|
228
|
+
**Metric**: Concrete dependency ratio
|
|
229
|
+
|
|
230
|
+
Calculates the ratio of concrete class instantiations to total dependencies:
|
|
231
|
+
- Direct `ClassName.new` calls penalize the score
|
|
232
|
+
- Standard library classes (Array, Hash, String, etc.) are whitelisted
|
|
233
|
+
- Dependency injection patterns score higher
|
|
234
|
+
|
|
235
|
+
- **Confidence**: High
|
|
236
|
+
|
|
237
|
+
## Rails Preset
|
|
238
|
+
|
|
239
|
+
For Rails projects, use the built-in preset to get sensible defaults:
|
|
240
|
+
|
|
241
|
+
```yaml
|
|
242
|
+
# .solid-score.yml
|
|
243
|
+
preset: rails
|
|
244
|
+
thresholds:
|
|
245
|
+
total: 60
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The Rails preset configures:
|
|
249
|
+
- **Paths**: `app/models`, `app/controllers`, `app/services`, `app/jobs`, `app/mailers`, `lib`, etc.
|
|
250
|
+
- **Excludes**: `spec/**`, `test/**`, `vendor/**`, `db/**`, `tmp/**`
|
|
251
|
+
- **Weights**: DIP weighted higher (0.30), SRP slightly lower (0.25)
|
|
252
|
+
- **DIP Whitelist**: Rails, Logger, Net::HTTP, ActionMailer, ActiveJob, etc.
|
|
253
|
+
|
|
254
|
+
Explicit YAML values override the preset.
|
|
255
|
+
|
|
256
|
+
## CI/CD Integration
|
|
257
|
+
|
|
258
|
+
### GitHub Actions (Official Action)
|
|
259
|
+
|
|
260
|
+
```yaml
|
|
261
|
+
name: SOLID Score Check
|
|
262
|
+
|
|
263
|
+
on:
|
|
264
|
+
pull_request:
|
|
265
|
+
branches: [main]
|
|
266
|
+
|
|
267
|
+
jobs:
|
|
268
|
+
solid-score:
|
|
269
|
+
runs-on: ubuntu-latest
|
|
270
|
+
steps:
|
|
271
|
+
- uses: actions/checkout@v4
|
|
272
|
+
with:
|
|
273
|
+
fetch-depth: 0
|
|
274
|
+
|
|
275
|
+
- uses: harakeishi/solid-score@main
|
|
276
|
+
with:
|
|
277
|
+
min-score: "70"
|
|
278
|
+
pr-comment: "true"
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
The action supports these inputs:
|
|
282
|
+
|
|
283
|
+
| Input | Default | Description |
|
|
284
|
+
|-------|---------|-------------|
|
|
285
|
+
| `ruby-version` | `3.2` | Ruby version |
|
|
286
|
+
| `min-score` | `0` | Minimum total score threshold |
|
|
287
|
+
| `format` | `text` | Output format (text/json/html) |
|
|
288
|
+
| `paths` | `.` | Paths to analyze |
|
|
289
|
+
| `config` | `.solid-score.yml` | Config file path |
|
|
290
|
+
| `pr-comment` | `false` | Post score report as PR comment |
|
|
291
|
+
| `diff-ref` | `origin/main` | Base ref for diff comparison |
|
|
292
|
+
|
|
293
|
+
Outputs: `score-json`, `passing`, `average-score`.
|
|
294
|
+
|
|
295
|
+
### Manual Setup
|
|
296
|
+
|
|
297
|
+
```yaml
|
|
298
|
+
jobs:
|
|
299
|
+
solid-score:
|
|
300
|
+
runs-on: ubuntu-latest
|
|
301
|
+
steps:
|
|
302
|
+
- uses: actions/checkout@v4
|
|
303
|
+
with:
|
|
304
|
+
fetch-depth: 0
|
|
305
|
+
|
|
306
|
+
- uses: ruby/setup-ruby@v1
|
|
307
|
+
with:
|
|
308
|
+
ruby-version: '3.3'
|
|
309
|
+
bundler-cache: true
|
|
310
|
+
|
|
311
|
+
- name: Install solid-score
|
|
312
|
+
run: gem install solid_score
|
|
313
|
+
|
|
314
|
+
- name: Check SOLID scores
|
|
315
|
+
run: solid-score app/ lib/ --min-score 70
|
|
316
|
+
|
|
317
|
+
- name: Check score regression (diff mode)
|
|
318
|
+
run: solid-score . --diff origin/main --max-decrease 5
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### RuboCop Integration
|
|
322
|
+
|
|
323
|
+
Add `rubocop-solid_score` to your `.rubocop.yml` to run SOLID score checks as part of your existing RuboCop workflow:
|
|
324
|
+
|
|
325
|
+
```yaml
|
|
326
|
+
# .rubocop.yml
|
|
327
|
+
require:
|
|
328
|
+
- rubocop-solid_score
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
#### Available Cops
|
|
332
|
+
|
|
333
|
+
| Cop | Default | Description |
|
|
334
|
+
|-----|---------|-------------|
|
|
335
|
+
| `SolidScore/TotalScore` | Enabled (70) | Checks weighted total score |
|
|
336
|
+
| `SolidScore/SingleResponsibility` | Disabled (60) | Checks SRP score |
|
|
337
|
+
| `SolidScore/OpenClosed` | Disabled (50) | Checks OCP score |
|
|
338
|
+
| `SolidScore/LiskovSubstitution` | Disabled (60) | Checks LSP score |
|
|
339
|
+
| `SolidScore/InterfaceSegregation` | Disabled (70) | Checks ISP score |
|
|
340
|
+
| `SolidScore/DependencyInversion` | Disabled (65) | Checks DIP score |
|
|
341
|
+
|
|
342
|
+
#### Configuration
|
|
343
|
+
|
|
344
|
+
```yaml
|
|
345
|
+
# .rubocop.yml
|
|
346
|
+
SolidScore/TotalScore:
|
|
347
|
+
Enabled: true
|
|
348
|
+
MinScore: 70
|
|
349
|
+
Weights:
|
|
350
|
+
srp: 0.30
|
|
351
|
+
ocp: 0.15
|
|
352
|
+
lsp: 0.10
|
|
353
|
+
isp: 0.20
|
|
354
|
+
dip: 0.25
|
|
355
|
+
DipWhitelist:
|
|
356
|
+
- Rails
|
|
357
|
+
- Logger
|
|
358
|
+
|
|
359
|
+
# Enable individual principle checks
|
|
360
|
+
SolidScore/SingleResponsibility:
|
|
361
|
+
Enabled: true
|
|
362
|
+
MinScore: 60
|
|
363
|
+
|
|
364
|
+
SolidScore/DependencyInversion:
|
|
365
|
+
Enabled: true
|
|
366
|
+
MinScore: 65
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Exit Codes
|
|
370
|
+
|
|
371
|
+
| Code | Description |
|
|
372
|
+
|------|-------------|
|
|
373
|
+
| 0 | All checks passed |
|
|
374
|
+
| 1 | Score below threshold or regression detected |
|
|
375
|
+
| 2 | Invalid arguments or configuration error |
|
|
376
|
+
|
|
377
|
+
## Development
|
|
378
|
+
|
|
379
|
+
After checking out the repo, run:
|
|
380
|
+
|
|
381
|
+
```bash
|
|
382
|
+
# Install dependencies
|
|
383
|
+
bundle install
|
|
384
|
+
|
|
385
|
+
# Run tests
|
|
386
|
+
bundle exec rspec
|
|
387
|
+
|
|
388
|
+
# Run tests with coverage
|
|
389
|
+
COVERAGE=true bundle exec rspec
|
|
390
|
+
|
|
391
|
+
# Run linter
|
|
392
|
+
bundle exec rubocop
|
|
393
|
+
|
|
394
|
+
# Run the tool locally
|
|
395
|
+
bundle exec solid-score spec/fixtures
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
## Limitations
|
|
399
|
+
|
|
400
|
+
- **Static Analysis Only**: Cannot detect runtime behavior or duck typing patterns
|
|
401
|
+
- **OCP/LSP Accuracy**: These principles are difficult to measure statically; scores are heuristic-based
|
|
402
|
+
- **Module Support**: Module analysis is less accurate than class analysis
|
|
403
|
+
- **Metaprogramming**: Heavy use of `define_method`, `method_missing`, etc. may affect accuracy
|
|
404
|
+
|
|
405
|
+
## Requirements
|
|
406
|
+
|
|
407
|
+
- Ruby >= 3.2.0
|
|
408
|
+
|
|
409
|
+
## Contributing
|
|
410
|
+
|
|
411
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/harakeishi/solid-score.
|
|
412
|
+
|
|
413
|
+
1. Fork the repository
|
|
414
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
415
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
416
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
417
|
+
5. Open a Pull Request
|
|
418
|
+
|
|
419
|
+
## License
|
|
420
|
+
|
|
421
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
422
|
+
|
|
423
|
+
## References
|
|
424
|
+
|
|
425
|
+
- [SOLID Principles](https://en.wikipedia.org/wiki/SOLID) - Wikipedia
|
|
426
|
+
- [LCOM4 Metric](https://www.aivosto.com/project/help/pm-oo-cohesion.html) - Lack of Cohesion of Methods
|
|
427
|
+
- [Robert C. Martin - Clean Architecture](https://www.amazon.com/Clean-Architecture-Craftsmans-Software-Structure/dp/0134494164)
|
data/config/default.yml
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
SolidScore/TotalScore:
|
|
2
|
+
Description: "Checks that each class/module meets a minimum SOLID total score."
|
|
3
|
+
Enabled: true
|
|
4
|
+
MinScore: 70
|
|
5
|
+
Weights:
|
|
6
|
+
srp: 0.30
|
|
7
|
+
ocp: 0.15
|
|
8
|
+
lsp: 0.10
|
|
9
|
+
isp: 0.20
|
|
10
|
+
dip: 0.25
|
|
11
|
+
DipWhitelist: []
|
|
12
|
+
|
|
13
|
+
SolidScore/SingleResponsibility:
|
|
14
|
+
Description: "Checks that each class/module meets a minimum SRP score."
|
|
15
|
+
Enabled: false
|
|
16
|
+
MinScore: 60
|
|
17
|
+
|
|
18
|
+
SolidScore/OpenClosed:
|
|
19
|
+
Description: "Checks that each class/module meets a minimum OCP score."
|
|
20
|
+
Enabled: false
|
|
21
|
+
MinScore: 50
|
|
22
|
+
|
|
23
|
+
SolidScore/LiskovSubstitution:
|
|
24
|
+
Description: "Checks that each class/module meets a minimum LSP score."
|
|
25
|
+
Enabled: false
|
|
26
|
+
MinScore: 60
|
|
27
|
+
|
|
28
|
+
SolidScore/InterfaceSegregation:
|
|
29
|
+
Description: "Checks that each class/module meets a minimum ISP score."
|
|
30
|
+
Enabled: false
|
|
31
|
+
MinScore: 70
|
|
32
|
+
|
|
33
|
+
SolidScore/DependencyInversion:
|
|
34
|
+
Description: "Checks that each class/module meets a minimum DIP score."
|
|
35
|
+
Enabled: false
|
|
36
|
+
MinScore: 65
|
data/exe/solid-score
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module SolidScore
|
|
6
|
+
# Checks that each class/module meets a minimum DIP (Dependency Inversion Principle) score.
|
|
7
|
+
#
|
|
8
|
+
# @example MinScore: 65 (default)
|
|
9
|
+
# SolidScore/DependencyInversion:
|
|
10
|
+
# Enabled: true
|
|
11
|
+
# MinScore: 65
|
|
12
|
+
class DependencyInversion < PrincipleBase
|
|
13
|
+
MSG = "DIP score %.1f is below minimum %g"
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def principle_key = :dip
|
|
18
|
+
def principle_name = "DIP"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module SolidScore
|
|
6
|
+
# Shared helper for all SolidScore cops.
|
|
7
|
+
# Parses the file once per cop run and caches scored results.
|
|
8
|
+
module Helpers
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def score_results_for
|
|
12
|
+
@solid_score_results ||= compute_scores(processed_source.file_path)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def compute_scores(file_path)
|
|
16
|
+
parser = ::SolidScore::Parser::RubyParser.new
|
|
17
|
+
scorer = ::SolidScore::Scorer.new(
|
|
18
|
+
weights: cop_config_weights,
|
|
19
|
+
dip_whitelist: cop_config_dip_whitelist
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
class_infos = parser.parse_file(file_path)
|
|
23
|
+
scorer.score_all(class_infos)
|
|
24
|
+
rescue ::Parser::SyntaxError
|
|
25
|
+
[]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def cop_config_weights
|
|
29
|
+
weights = ::SolidScore::Models::ScoreResult::DEFAULT_WEIGHTS.dup
|
|
30
|
+
config_weights = cop_config["Weights"]
|
|
31
|
+
return weights unless config_weights
|
|
32
|
+
|
|
33
|
+
config_weights.each { |k, v| weights[k.to_sym] = v.to_f }
|
|
34
|
+
weights
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def cop_config_dip_whitelist
|
|
38
|
+
cop_config["DipWhitelist"] || []
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def score_threshold
|
|
42
|
+
cop_config["MinScore"]&.to_f || 70.0
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def find_class_node_for(result)
|
|
46
|
+
find_node_at_line(processed_source.ast, result.class_info&.line_start)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def find_node_at_line(ast, target_line)
|
|
50
|
+
return nil unless ast && target_line
|
|
51
|
+
|
|
52
|
+
if %i[class module].include?(ast.type) && ast.loc.keyword.line == target_line
|
|
53
|
+
return ast
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
return nil unless ast.respond_to?(:children)
|
|
57
|
+
|
|
58
|
+
ast.children.each do |child|
|
|
59
|
+
next unless child.is_a?(::RuboCop::AST::Node)
|
|
60
|
+
|
|
61
|
+
found = find_node_at_line(child, target_line)
|
|
62
|
+
return found if found
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def same_node?(target, current)
|
|
69
|
+
target.loc.keyword.line == current.loc.keyword.line
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module SolidScore
|
|
6
|
+
# Checks that each class/module meets a minimum ISP (Interface Segregation Principle) score.
|
|
7
|
+
#
|
|
8
|
+
# @example MinScore: 70 (default)
|
|
9
|
+
# SolidScore/InterfaceSegregation:
|
|
10
|
+
# Enabled: true
|
|
11
|
+
# MinScore: 70
|
|
12
|
+
class InterfaceSegregation < PrincipleBase
|
|
13
|
+
MSG = "ISP score %.1f is below minimum %g"
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def principle_key = :isp
|
|
18
|
+
def principle_name = "ISP"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module SolidScore
|
|
6
|
+
# Checks that each class/module meets a minimum LSP (Liskov Substitution Principle) score.
|
|
7
|
+
#
|
|
8
|
+
# @example MinScore: 60 (default)
|
|
9
|
+
# SolidScore/LiskovSubstitution:
|
|
10
|
+
# Enabled: true
|
|
11
|
+
# MinScore: 60
|
|
12
|
+
class LiskovSubstitution < PrincipleBase
|
|
13
|
+
MSG = "LSP score %.1f is below minimum %g"
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def principle_key = :lsp
|
|
18
|
+
def principle_name = "LSP"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module SolidScore
|
|
6
|
+
# Checks that each class/module meets a minimum OCP (Open/Closed Principle) score.
|
|
7
|
+
#
|
|
8
|
+
# @example MinScore: 50 (default)
|
|
9
|
+
# SolidScore/OpenClosed:
|
|
10
|
+
# Enabled: true
|
|
11
|
+
# MinScore: 50
|
|
12
|
+
class OpenClosed < PrincipleBase
|
|
13
|
+
MSG = "OCP score %.1f is below minimum %g"
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def principle_key = :ocp
|
|
18
|
+
def principle_name = "OCP"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|