keela 0.0.2 → 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 +4 -4
- data/CHANGELOG.md +41 -0
- data/LICENSE.txt +21 -0
- data/README.md +346 -0
- data/exe/keela +174 -0
- data/lib/keela/baseline.rb +82 -0
- data/lib/keela/config_file.rb +79 -0
- data/lib/keela/configuration.rb +44 -0
- data/lib/keela/reporter.rb +84 -0
- data/lib/keela/scanner.rb +167 -0
- data/lib/keela/strategies/attributes.rb +44 -0
- data/lib/keela/strategies/constants.rb +53 -0
- data/lib/keela/strategies/delegations.rb +70 -0
- data/lib/keela/strategies/i18n_keys.rb +88 -0
- data/lib/keela/strategies/methods.rb +35 -0
- data/lib/keela/strategies/scopes.rb +29 -0
- data/lib/keela/strategy.rb +43 -0
- data/lib/keela/version.rb +5 -0
- data/lib/keela.rb +32 -1
- metadata +69 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 920e953982643da9bb58171ea626a130d27a3cc41f6ccfb3511eabefbb0b41c8
|
|
4
|
+
data.tar.gz: 8c7e5d91900e962efca4d08f242e9582ad9192c1d07401e45ce545df5af68d2a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 01efcb8620dd29060d5758ef41def0dd450f62d59d46cb6de8873a18b1a69014895bd64bd68aa263731cb6edbc8640dc6b24f3a9281a865ff650b9717693dd1f
|
|
7
|
+
data.tar.gz: d9e8650985b2ca2a618201ecc2ecc8ced5e275564c37e84658207b498ba70b2279d281486d2bd1127cf9696bc278cff5121a411b42ef27287bf94f5b5d157da6
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.2.0] - 2026-07-17
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `Baseline` class for managing multi-strategy baseline files ([#1](https://github.com/kerrizor/keela/pull/1))
|
|
15
|
+
- Single baseline file now supports multiple strategy sections (methods, scopes)
|
|
16
|
+
- **Constants strategy** for detecting unused constant definitions (`SCREAMING_SNAKE_CASE = value`)
|
|
17
|
+
- **Delegations strategy** for detecting unused `delegate :method, to: :target` declarations
|
|
18
|
+
- **Attributes strategy** for detecting unused `attr_accessor`, `attr_reader`, `attr_writer` declarations
|
|
19
|
+
- **Config file support** (`keela.yml` or `.keela.yml`) for project-specific settings
|
|
20
|
+
- **Exclude patterns** via `--exclude` CLI flag or `exclude_patterns` in config file
|
|
21
|
+
- **Include patterns** via `--include` CLI flag or `include_patterns` in config file (adds to default directory patterns)
|
|
22
|
+
- **Configuration validation** - raises `ConfigurationError` if `directory_patterns` is customized while also using `include_patterns` or `exclude_patterns`
|
|
23
|
+
- **I18n keys strategy (beta)** for detecting unused translation keys in locale files (`--type i18n_keys`)
|
|
24
|
+
- **Multiple types** can now be specified with `--type methods,scopes,constants`
|
|
25
|
+
- **JSON output** via `--format json` for machine-readable results (useful for CI integrations)
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- Running strategies separately no longer overwrites previous strategy data ([#1](https://github.com/kerrizor/keela/pull/1))
|
|
30
|
+
|
|
31
|
+
## [0.1.0] - 2026-07-16
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- Initial release
|
|
36
|
+
- `Scanner` class for detecting unused code
|
|
37
|
+
- Built-in strategies for methods and scopes detection
|
|
38
|
+
- CLI tool with `--report` and `--update-baseline` modes
|
|
39
|
+
- Configurable file extensions and directory patterns
|
|
40
|
+
- Baseline comparison for CI integration
|
|
41
|
+
- Exclusion file support for false positives
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kerri Miller
|
|
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,346 @@
|
|
|
1
|
+
# Keela 🐕
|
|
2
|
+
|
|
3
|
+
Like the famous CSI dog who found what others missed, Keela sniffs out unused code in your Ruby codebase.
|
|
4
|
+
|
|
5
|
+
## Why Remove Unused Code?
|
|
6
|
+
|
|
7
|
+
Dead code isn't harmless — it's actively costly:
|
|
8
|
+
|
|
9
|
+
- **Cognitive overhead**: Developers read and try to understand code that doesn't matter, slowing down onboarding and feature work
|
|
10
|
+
- **CI minutes**: Tests for unused methods still run, burning compute time on every pipeline
|
|
11
|
+
- **False confidence**: Test coverage metrics include dead code, masking gaps in the code that actually runs
|
|
12
|
+
- **Refactoring friction**: Unused code creates dependencies that make refactoring harder ("wait, is this called somewhere?")
|
|
13
|
+
- **Security surface**: More code means more potential vulnerabilities, even in paths users never hit
|
|
14
|
+
|
|
15
|
+
Most codebases accumulate dead code gradually — a feature flag that's always on, a method replaced but never deleted, a scope that lost its last caller. Keela helps you find it and clean it up.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
gem install keela
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or add to your Gemfile:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
gem 'keela', group: :development
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# First time: generate a baseline of current unused code
|
|
33
|
+
keela --update-baseline
|
|
34
|
+
|
|
35
|
+
# This creates .keela_baseline.yml in your project root
|
|
36
|
+
|
|
37
|
+
# From now on, just run:
|
|
38
|
+
keela
|
|
39
|
+
|
|
40
|
+
# Keela will fail if:
|
|
41
|
+
# - NEW unused code is detected (someone added dead code)
|
|
42
|
+
# - Previously unused code was REMOVED (time to update the baseline!)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## How It Works
|
|
46
|
+
|
|
47
|
+
Keela operates in two modes:
|
|
48
|
+
|
|
49
|
+
### Baseline Mode (Default)
|
|
50
|
+
|
|
51
|
+
If a `.keela_baseline.yml` file exists, Keela compares the current scan against it:
|
|
52
|
+
|
|
53
|
+
| Scenario | Result |
|
|
54
|
+
|----------|--------|
|
|
55
|
+
| No changes from baseline | ✅ Pass (silent, exit 0) |
|
|
56
|
+
| NEW unused code detected | ❌ Fail (shows new items) |
|
|
57
|
+
| Code REMOVED from baseline | ❌ Fail (prompts to update baseline) |
|
|
58
|
+
|
|
59
|
+
This lets you gradually pay down tech debt while preventing new dead code from sneaking in.
|
|
60
|
+
|
|
61
|
+
### Report Mode
|
|
62
|
+
|
|
63
|
+
If no baseline exists (or you use `--report`), Keela shows all unused code:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
keela --report
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Command Line Options
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Scan for all unused code (methods, scopes, constants, delegations, attributes)
|
|
73
|
+
keela
|
|
74
|
+
|
|
75
|
+
# Scan for specific types
|
|
76
|
+
keela --type methods
|
|
77
|
+
keela --type scopes
|
|
78
|
+
|
|
79
|
+
# Combine multiple types
|
|
80
|
+
keela --type methods,scopes,constants
|
|
81
|
+
|
|
82
|
+
# Force report mode (ignore baseline)
|
|
83
|
+
keela --report
|
|
84
|
+
|
|
85
|
+
# Update the baseline file
|
|
86
|
+
keela --update-baseline
|
|
87
|
+
|
|
88
|
+
# Use a custom baseline path
|
|
89
|
+
keela --baseline config/unused_baseline.yml
|
|
90
|
+
|
|
91
|
+
# Specify excluded items file
|
|
92
|
+
keela --excluded config/keela_excluded.yml
|
|
93
|
+
|
|
94
|
+
# Custom file extensions
|
|
95
|
+
keela --extensions rb,rake,haml
|
|
96
|
+
|
|
97
|
+
# Exclude files matching patterns
|
|
98
|
+
keela --exclude 'vendor/**/*' --exclude 'tmp/**/*'
|
|
99
|
+
|
|
100
|
+
# Include additional directories (adds to defaults)
|
|
101
|
+
keela --include 'engines/**/*.rb' --include 'custom/**/*.rb'
|
|
102
|
+
|
|
103
|
+
# Use a custom config file
|
|
104
|
+
keela --config path/to/keela.yml
|
|
105
|
+
|
|
106
|
+
# Output as JSON (for CI integrations)
|
|
107
|
+
keela --format json
|
|
108
|
+
|
|
109
|
+
# Show version
|
|
110
|
+
keela --version
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Detection Strategies
|
|
114
|
+
|
|
115
|
+
Keela detects several types of unused code:
|
|
116
|
+
|
|
117
|
+
| Strategy | Detects | Example |
|
|
118
|
+
|----------|---------|---------|
|
|
119
|
+
| **methods** | Unused method definitions | `def unused_method` |
|
|
120
|
+
| **scopes** | Unused ActiveRecord scopes | `scope :unused_scope, -> { }` |
|
|
121
|
+
| **constants** | Unused constants | `UNUSED_CONSTANT = 'value'` |
|
|
122
|
+
| **delegations** | Unused delegate declarations | `delegate :unused, to: :target` |
|
|
123
|
+
| **attributes** | Unused attr_* declarations | `attr_accessor :unused_attr` |
|
|
124
|
+
| **i18n_keys** | Unused translation keys | `en.users.unused_key` in locale YAML |
|
|
125
|
+
|
|
126
|
+
Run all strategies (default) or target specific ones with `--type`.
|
|
127
|
+
|
|
128
|
+
**Note:** The `i18n_keys` strategy is not included in `--type all` because it requires scanning YAML locale files. Run it explicitly with `--type i18n_keys`.
|
|
129
|
+
|
|
130
|
+
### I18n Keys (Beta)
|
|
131
|
+
|
|
132
|
+
The `i18n_keys` strategy is **beta** and may produce false positives. It cannot detect:
|
|
133
|
+
|
|
134
|
+
- **Lazy lookup** - `t('.title')` in views resolves based on the view path
|
|
135
|
+
- **Dynamic keys** - `t("users.#{action}.title")` with interpolated segments
|
|
136
|
+
- **Model translations** - `User.human_attribute_name(:email)` and `User.model_name.human`
|
|
137
|
+
- **Pluralization siblings** - If `one:` is used, `other:` may appear unused
|
|
138
|
+
|
|
139
|
+
Review results carefully and use the exclusion file for known false positives.
|
|
140
|
+
|
|
141
|
+
## Configuration File
|
|
142
|
+
|
|
143
|
+
Create a `keela.yml` or `.keela.yml` in your project root:
|
|
144
|
+
|
|
145
|
+
```yaml
|
|
146
|
+
# keela.yml
|
|
147
|
+
extensions:
|
|
148
|
+
- rb
|
|
149
|
+
- haml
|
|
150
|
+
- erb
|
|
151
|
+
|
|
152
|
+
exclude_patterns:
|
|
153
|
+
- "vendor/**/*"
|
|
154
|
+
- "tmp/**/*"
|
|
155
|
+
|
|
156
|
+
include_patterns:
|
|
157
|
+
- "engines/**/*.%<ext>s"
|
|
158
|
+
|
|
159
|
+
excluded_path: ".keela_excluded.yml"
|
|
160
|
+
baseline_path: ".keela_baseline.yml"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Keela automatically loads `keela.yml` or `.keela.yml` from the current directory. Use `--config` to specify a different path.
|
|
164
|
+
|
|
165
|
+
### Customizing Which Files to Scan
|
|
166
|
+
|
|
167
|
+
There are two approaches:
|
|
168
|
+
|
|
169
|
+
**1. Tweak the defaults** with `--include` and `--exclude` (or `include_patterns`/`exclude_patterns` in config):
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
# Add engines/ to the default app/, lib/, config/ directories
|
|
173
|
+
keela --include 'engines/**/*.rb'
|
|
174
|
+
|
|
175
|
+
# Exclude vendor files from scanning
|
|
176
|
+
keela --exclude 'vendor/**/*'
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**2. Full control** with `directory_patterns` - replaces the defaults entirely:
|
|
180
|
+
|
|
181
|
+
```yaml
|
|
182
|
+
# keela.yml - scan ONLY these directories
|
|
183
|
+
directory_patterns:
|
|
184
|
+
- "src/**/*.%<ext>s"
|
|
185
|
+
- "custom/**/*.%<ext>s"
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Use `directory_patterns` when you need complete control. Use `--include`/`--exclude` when you just want to tweak the defaults.
|
|
189
|
+
|
|
190
|
+
**Note:** Mixing both approaches raises a `ConfigurationError`. Choose one or the other.
|
|
191
|
+
|
|
192
|
+
**Available options:**
|
|
193
|
+
|
|
194
|
+
| Key | Description | Default |
|
|
195
|
+
|-----|-------------|---------|
|
|
196
|
+
| `directory_patterns` | Glob patterns for files to scan (replaces defaults) | `app/`, `lib/`, `config/` |
|
|
197
|
+
| `extensions` | File extensions to scan | `rb`, `haml`, `erb` |
|
|
198
|
+
| `include_patterns` | Additional patterns to scan (added to defaults) | `[]` |
|
|
199
|
+
| `exclude_patterns` | Patterns for files to exclude | `[]` |
|
|
200
|
+
| `excluded_path` | Path to YAML file of excluded items | `nil` |
|
|
201
|
+
| `baseline_path` | Path to baseline YAML file | `.keela_baseline.yml` |
|
|
202
|
+
| `required_directory` | Directory that must exist for scanning to proceed | `nil` |
|
|
203
|
+
|
|
204
|
+
## CI Integration
|
|
205
|
+
|
|
206
|
+
Keela is designed for CI pipelines. Add it to catch dead code before it merges:
|
|
207
|
+
|
|
208
|
+
```yaml
|
|
209
|
+
# .gitlab-ci.yml
|
|
210
|
+
unused_code:
|
|
211
|
+
script:
|
|
212
|
+
- bundle exec keela
|
|
213
|
+
rules:
|
|
214
|
+
- if: $CI_MERGE_REQUEST_IID
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
```yaml
|
|
218
|
+
# .github/workflows/ci.yml
|
|
219
|
+
- name: Check for unused code
|
|
220
|
+
run: bundle exec keela
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
The workflow:
|
|
224
|
+
|
|
225
|
+
1. **Initial setup**: Run `keela --update-baseline` and commit `.keela_baseline.yml`
|
|
226
|
+
2. **CI runs**: `keela` compares against baseline, fails on new dead code
|
|
227
|
+
3. **After cleanup**: Run `keela --update-baseline` to update the baseline
|
|
228
|
+
|
|
229
|
+
### JSON Output
|
|
230
|
+
|
|
231
|
+
Use `--format json` for machine-readable output:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
keela --format json --report
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
```json
|
|
238
|
+
{
|
|
239
|
+
"strategies": ["methods", "scopes"],
|
|
240
|
+
"unused": {
|
|
241
|
+
"methods": {
|
|
242
|
+
"app/models/user.rb": ["unused_method", "old_helper"]
|
|
243
|
+
},
|
|
244
|
+
"scopes": {
|
|
245
|
+
"app/models/post.rb": ["inactive"]
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
"summary": {
|
|
249
|
+
"total": 3,
|
|
250
|
+
"by_strategy": {
|
|
251
|
+
"methods": 2,
|
|
252
|
+
"scopes": 1
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
This is useful for integrating with other tools, generating reports, or processing results programmatically.
|
|
259
|
+
|
|
260
|
+
## Exclusion File
|
|
261
|
+
|
|
262
|
+
Some code appears unused but is actually called dynamically. Exclude it:
|
|
263
|
+
|
|
264
|
+
```yaml
|
|
265
|
+
# .keela_excluded.yml
|
|
266
|
+
app/models/user.rb:
|
|
267
|
+
- legacy_method: "Called via metaprogramming"
|
|
268
|
+
- callback_method: "Used as ActiveRecord callback"
|
|
269
|
+
app/helpers/application_helper.rb:
|
|
270
|
+
- helper_method: "Called from views dynamically"
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Then run with:
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
keela --excluded .keela_excluded.yml
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Ruby API
|
|
280
|
+
|
|
281
|
+
```ruby
|
|
282
|
+
require 'keela'
|
|
283
|
+
|
|
284
|
+
# Configure Keela
|
|
285
|
+
Keela.configure do |config|
|
|
286
|
+
config.extensions = %w[rb haml erb]
|
|
287
|
+
config.directory_patterns = %w[
|
|
288
|
+
app/**/*.%<ext>s
|
|
289
|
+
lib/**/*.%<ext>s
|
|
290
|
+
]
|
|
291
|
+
config.include_patterns = %w[engines/**/*.%<ext>s]
|
|
292
|
+
config.exclude_patterns = %w[vendor/**/* tmp/**/*]
|
|
293
|
+
config.excluded_path = '.keela_excluded.yml'
|
|
294
|
+
config.baseline_path = '.keela_baseline.yml'
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Run a scan
|
|
298
|
+
strategy = Keela::Strategies::Methods.new
|
|
299
|
+
scanner = Keela::Scanner.new(strategy: strategy)
|
|
300
|
+
success = scanner.run
|
|
301
|
+
|
|
302
|
+
# Access results
|
|
303
|
+
scanner.unused_collection # Hash of file => [unused_names]
|
|
304
|
+
scanner.new_unused # Items not in baseline
|
|
305
|
+
scanner.removed # Items in baseline but no longer unused
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Custom Strategies
|
|
309
|
+
|
|
310
|
+
Detect other patterns by creating your own strategy:
|
|
311
|
+
|
|
312
|
+
```ruby
|
|
313
|
+
class CallbackStrategy < Keela::Strategy
|
|
314
|
+
def name
|
|
315
|
+
"callbacks"
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def definition_file_pattern
|
|
319
|
+
%r{app/models}
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def extract_definition(line)
|
|
323
|
+
# Match: before_save :do_something
|
|
324
|
+
line =~ /(?:before|after|around)_\w+\s+:(\w+)/ ? Regexp.last_match(1) : nil
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def usage_regex(name)
|
|
328
|
+
/def #{Regexp.quote(name)}\b/
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def skip_comments?
|
|
332
|
+
true
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
scanner = Keela::Scanner.new(strategy: CallbackStrategy.new)
|
|
337
|
+
scanner.run(force_report: true)
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## About the Name
|
|
341
|
+
|
|
342
|
+
Keela was a famous English Springer Spaniel known as the "CSI dog." She could detect microscopic traces of blood that other forensic methods missed, and worked on many high-profile cases. Like her namesake, this gem finds the unused code that other tools miss.
|
|
343
|
+
|
|
344
|
+
## License
|
|
345
|
+
|
|
346
|
+
MIT License. See [LICENSE.txt](LICENSE.txt).
|
data/exe/keela
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "optparse"
|
|
5
|
+
require "keela"
|
|
6
|
+
|
|
7
|
+
options = {
|
|
8
|
+
type: :all,
|
|
9
|
+
force_report: false,
|
|
10
|
+
update_baseline: false,
|
|
11
|
+
config_path: nil,
|
|
12
|
+
format: :text
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
OptionParser.new do |opts|
|
|
16
|
+
opts.banner = "Usage: keela [options]"
|
|
17
|
+
|
|
18
|
+
opts.separator ""
|
|
19
|
+
opts.separator "Keela sniffs out unused code in your Ruby codebase."
|
|
20
|
+
opts.separator ""
|
|
21
|
+
opts.separator "By default, if a baseline file exists, Keela runs in diff mode:"
|
|
22
|
+
opts.separator " - Fails if NEW unused code is detected"
|
|
23
|
+
opts.separator " - Fails if previously unused code was REMOVED (update baseline)"
|
|
24
|
+
opts.separator " - Passes if unchanged from baseline"
|
|
25
|
+
opts.separator ""
|
|
26
|
+
opts.separator "If no baseline exists, shows a full report of all unused code."
|
|
27
|
+
opts.separator ""
|
|
28
|
+
opts.separator "Options:"
|
|
29
|
+
|
|
30
|
+
opts.on("--type TYPES", "Comma-separated types to detect: methods, scopes, constants, delegations, attributes, i18n_keys, all (default: all)") do |types|
|
|
31
|
+
options[:types] = types.split(",").map { |t| t.strip.to_sym }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
opts.on("--report", "-r", "Force report mode: show all unused code (ignore baseline)") do
|
|
35
|
+
options[:force_report] = true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
opts.on("--update-baseline", "-u", "Update the baseline file with current unused code") do
|
|
39
|
+
options[:update_baseline] = true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
opts.on("--format FORMAT", %i[text json], "Output format: text (default) or json") do |format|
|
|
43
|
+
options[:format] = format
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
opts.on("--excluded PATH", "Path to YAML file of excluded items") do |path|
|
|
47
|
+
Keela.configuration.excluded_path = path
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
opts.on("--baseline PATH", "Path to YAML baseline file (default: .keela_baseline.yml)") do |path|
|
|
51
|
+
Keela.configuration.baseline_path = path
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
opts.on("--extensions EXTS", "Comma-separated file extensions to scan (default: rb,haml,erb)") do |exts|
|
|
55
|
+
Keela.configuration.extensions = exts.split(",").map(&:strip)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
opts.on("--exclude PATTERN", "Glob pattern for files to exclude (can be used multiple times)") do |pattern|
|
|
59
|
+
Keela.configuration.exclude_patterns << pattern
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
opts.on("--include PATTERN", "Additional glob pattern to scan (can be used multiple times)") do |pattern|
|
|
63
|
+
Keela.configuration.include_patterns << pattern
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
opts.on("--config PATH", "-c", "Path to config file (default: keela.yml or .keela.yml)") do |path|
|
|
67
|
+
options[:config_path] = path
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
opts.on("--version", "-v", "Show version") do
|
|
71
|
+
puts "keela #{Keela::VERSION}"
|
|
72
|
+
exit
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
opts.on("-h", "--help", "Show this help") do
|
|
76
|
+
puts opts
|
|
77
|
+
exit
|
|
78
|
+
end
|
|
79
|
+
end.parse!
|
|
80
|
+
|
|
81
|
+
# Load config file (keela.yml or .keela.yml) if present
|
|
82
|
+
# CLI options override config file settings
|
|
83
|
+
Keela::ConfigFile.load(path: options[:config_path])
|
|
84
|
+
|
|
85
|
+
# Set default baseline path if not specified
|
|
86
|
+
Keela.configuration.baseline_path ||= ".keela_baseline.yml"
|
|
87
|
+
|
|
88
|
+
STRATEGY_MAP = {
|
|
89
|
+
methods: Keela::Strategies::Methods,
|
|
90
|
+
scopes: Keela::Strategies::Scopes,
|
|
91
|
+
constants: Keela::Strategies::Constants,
|
|
92
|
+
delegations: Keela::Strategies::Delegations,
|
|
93
|
+
attributes: Keela::Strategies::Attributes,
|
|
94
|
+
i18n_keys: Keela::Strategies::I18nKeys
|
|
95
|
+
}.freeze
|
|
96
|
+
|
|
97
|
+
# Default strategies for "all" (excludes i18n_keys which requires different config)
|
|
98
|
+
DEFAULT_STRATEGIES = %i[methods scopes constants delegations attributes].freeze
|
|
99
|
+
|
|
100
|
+
VALID_TYPES = (STRATEGY_MAP.keys + [:all]).freeze
|
|
101
|
+
|
|
102
|
+
def build_strategies(types)
|
|
103
|
+
types = types || [:all]
|
|
104
|
+
|
|
105
|
+
# Validate types
|
|
106
|
+
invalid = types - VALID_TYPES
|
|
107
|
+
unless invalid.empty?
|
|
108
|
+
warn "Error: Invalid type(s): #{invalid.join(', ')}"
|
|
109
|
+
warn "Valid types: #{VALID_TYPES.join(', ')}"
|
|
110
|
+
exit 1
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Expand :all to default strategies
|
|
114
|
+
expanded = types.flat_map { |t| t == :all ? DEFAULT_STRATEGIES : t }.uniq
|
|
115
|
+
|
|
116
|
+
expanded.map { |t| STRATEGY_MAP[t].new }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
strategies = build_strategies(options[:types])
|
|
120
|
+
|
|
121
|
+
# Share a single baseline across all strategies
|
|
122
|
+
baseline = Keela::Baseline.new(Keela.configuration.baseline_path)
|
|
123
|
+
|
|
124
|
+
success = true
|
|
125
|
+
results = {}
|
|
126
|
+
|
|
127
|
+
json_mode = options[:format] == :json
|
|
128
|
+
|
|
129
|
+
strategies.each_with_index do |strategy, index|
|
|
130
|
+
unless json_mode
|
|
131
|
+
puts Rainbow("=== Sniffing for unused #{strategy.name} ===").cyan.bright if strategies.size > 1
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
scanner = Keela::Scanner.new(strategy: strategy, baseline: baseline)
|
|
135
|
+
success &&= scanner.run(
|
|
136
|
+
force_report: options[:force_report],
|
|
137
|
+
update_baseline: options[:update_baseline],
|
|
138
|
+
silent: json_mode
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Collect results for JSON output
|
|
142
|
+
results[strategy.name] = scanner.unused_collection.transform_values(&:to_a)
|
|
143
|
+
|
|
144
|
+
unless json_mode
|
|
145
|
+
puts if strategies.size > 1 && index < strategies.size - 1
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Save baseline after all strategies have run
|
|
150
|
+
if options[:update_baseline]
|
|
151
|
+
baseline.save
|
|
152
|
+
puts Rainbow("Updated #{baseline.path}").green.bright unless json_mode
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Output JSON if requested
|
|
156
|
+
if json_mode
|
|
157
|
+
require "json"
|
|
158
|
+
|
|
159
|
+
total = results.values.flat_map(&:values).flatten.size
|
|
160
|
+
by_strategy = results.transform_values { |files| files.values.flatten.size }
|
|
161
|
+
|
|
162
|
+
output = {
|
|
163
|
+
strategies: strategies.map(&:name),
|
|
164
|
+
unused: results.reject { |_, v| v.empty? },
|
|
165
|
+
summary: {
|
|
166
|
+
total: total,
|
|
167
|
+
by_strategy: by_strategy.reject { |_, v| v.zero? }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
puts JSON.pretty_generate(output)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
exit(success ? 0 : 1)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Keela
|
|
6
|
+
# Handles reading and writing baseline files with multiple strategy sections.
|
|
7
|
+
#
|
|
8
|
+
# File format:
|
|
9
|
+
# methods:
|
|
10
|
+
# app/models/user.rb:
|
|
11
|
+
# - unused_method
|
|
12
|
+
# scopes:
|
|
13
|
+
# app/models/user.rb:
|
|
14
|
+
# - unused_scope
|
|
15
|
+
#
|
|
16
|
+
class Baseline
|
|
17
|
+
attr_reader :path
|
|
18
|
+
|
|
19
|
+
def initialize(path)
|
|
20
|
+
@path = path
|
|
21
|
+
@data = nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def exists?
|
|
25
|
+
path && File.exist?(path)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def load
|
|
29
|
+
return {} unless exists?
|
|
30
|
+
|
|
31
|
+
@data = YAML.load_file(path) || {}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def data
|
|
35
|
+
@data ||= load
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def get(strategy_name)
|
|
39
|
+
data[strategy_name] || {}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def set(strategy_name, unused_collection)
|
|
43
|
+
# Ensure we've loaded existing data before setting
|
|
44
|
+
load if @data.nil? && exists?
|
|
45
|
+
@data ||= {}
|
|
46
|
+
@data[strategy_name] = unused_collection.sort.to_h
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def save
|
|
50
|
+
return unless path
|
|
51
|
+
|
|
52
|
+
header = <<~HEADER
|
|
53
|
+
# Unused code identified by Keela.
|
|
54
|
+
# These are potential targets for removal.
|
|
55
|
+
#
|
|
56
|
+
# If an item listed here is actually in use,
|
|
57
|
+
# remove it from this file and add it to your excluded file.
|
|
58
|
+
#
|
|
59
|
+
HEADER
|
|
60
|
+
|
|
61
|
+
sorted_data = data.sort.to_h
|
|
62
|
+
yaml_content = if sorted_data.empty? || sorted_data.values.all?(&:empty?)
|
|
63
|
+
"#{header}---\n{}\n"
|
|
64
|
+
else
|
|
65
|
+
"#{header}#{format_yaml(sorted_data)}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
File.write(path, yaml_content)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def format_yaml(hash)
|
|
74
|
+
indent_yaml_list_items(hash.to_yaml)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Indents YAML list items that are not already indented.
|
|
78
|
+
def indent_yaml_list_items(yaml_string)
|
|
79
|
+
yaml_string.gsub(/\n-(\s+\S)/, "\n -\\1")
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|