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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c5eafef4540bb30839eb54b9de70e43000be89b8984a5494d9c5b1e37c76b968
4
- data.tar.gz: de69c9165352044cdfc64e84a480e4aef098a28d3ecfd285d6d0a2227d417b56
3
+ metadata.gz: 920e953982643da9bb58171ea626a130d27a3cc41f6ccfb3511eabefbb0b41c8
4
+ data.tar.gz: 8c7e5d91900e962efca4d08f242e9582ad9192c1d07401e45ce545df5af68d2a
5
5
  SHA512:
6
- metadata.gz: 71d6ecd16abc63e8b74db4a09f6d5979e168219b16fa27014c848209e230d9674f9393b4087fa97af1c9bd0af84e7f00eaebabd2375fbc788f1d090377b39685
7
- data.tar.gz: 5b590194ecec3ef30621d67c85bcd07a9cd4c7a43374184bd45ac53badab44cd86d83887294624e1e82122a5f85f5d57874b8e46252a92c1d3b3401a3de23be8
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