keela 0.3.0 → 0.4.1

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: 4fcf07cf3f0adee173c2dfaef9e79f256f6a6318c5dd843935b6113fede4dea4
4
- data.tar.gz: 454fa3bc9a9839dcf7d5f40a6d2fa2160daeb3a2d113d2f372da6ce15f9a49b0
3
+ metadata.gz: 1b1c9fad8b959b91c3af1518da72d36f9f82bd649b4c21a6a0cf261246cbbcdb
4
+ data.tar.gz: 24c7b946587cb844ae9c08c921ffecacfb8b262404b2c177f0368d803cc52861
5
5
  SHA512:
6
- metadata.gz: f9105a9cc47b95443d527772bd1dc44b5578d3b88829ec0144109e1ce82cc03d3414be62de9c8e4f8a7ab308b742e3beea343e6aefd2b4e3184f694847bba711
7
- data.tar.gz: b583a1e03b6b460d4393ed687ef8796315c6fc4e8f9e4afde3a4edce26581536ef0b72dbef7215237b97835263b12a52b55879f391d901efe5409007ae407982
6
+ metadata.gz: e0fcd131e331240b852f7d657113aa0e9610aa4a7dd465b78576478c11abdeca5cd4140c9037cbbd378f53e23e4137d6a255eeb8d28950f0f82f6ad0eeabc7fd
7
+ data.tar.gz: 7d71ea4b1a2c601f0aba8901fbe31f95ba055eb784614213afa386321e096bb2364ecd832b60356f08c8cc42ed2e07e9bdcfc4c3a794fe218931834ac7df7c39
data/CHANGELOG.md CHANGED
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.1] - 2026-08-21
11
+
12
+ ### Fixed
13
+
14
+ - Constants used as hash keys (`CONST => value`) or in rescue splats (`rescue *ERRORS => e`) are now correctly detected as used ([#63](https://github.com/kerrizor/keela/pull/63))
15
+ - Methods referenced as literal symbols (`:method_name`) are now detected as used, including Rails callbacks, `send(:method)`, `validate :method`, etc. ([#64](https://github.com/kerrizor/keela/issues/64))
16
+ - I18n pluralization keys (`one`, `other`, `zero`, etc.) are now detected as used when the parent key is called with `t('key', count: n)` ([#69](https://github.com/kerrizor/keela/pull/69))
17
+ - I18n lazy lookup (`t('.title')` in views) now correctly resolves to the full key based on the view path ([#17](https://github.com/kerrizor/keela/issues/17))
18
+
19
+ ## [0.4.0] - 2026-08-14
20
+
21
+ ### Added
22
+
23
+ - **Verbose mode** via `--verbose` flag to show files being scanned, glob patterns, and configuration for debugging ([#53](https://github.com/kerrizor/keela/pull/53))
24
+ - **Source location** via `--source-location` flag to show line numbers in reports for easier navigation ([#55](https://github.com/kerrizor/keela/pull/55))
25
+ - **Exclusion validation** via `--test-exclusions` flag to find stale entries in exclusion files ([#56](https://github.com/kerrizor/keela/pull/56))
26
+ - **TOON output format** via `--format toon` for token-efficient LLM-friendly output ([#58](https://github.com/kerrizor/keela/pull/58))
27
+ - **Configurable definition paths** per strategy via `strategies.<name>.definition_paths` in config file ([#59](https://github.com/kerrizor/keela/pull/59))
28
+
29
+ ### Changed
30
+
31
+ - **`--quiet` now suppresses all output**, not just the progress bar. Use exit code for success/failure in scripts. ([#54](https://github.com/kerrizor/keela/pull/54))
32
+
33
+ ### Fixed
34
+
35
+ - Multi-method delegate declarations now detect all methods, not just the first ([#51](https://github.com/kerrizor/keela/pull/51))
36
+ - Class methods (`def self.foo`) are now correctly detected as unused ([#60](https://github.com/kerrizor/keela/pull/60))
37
+
10
38
  ## [0.3.0] - 2026-08-05
11
39
 
12
40
  ### Added
data/README.md CHANGED
@@ -66,6 +66,27 @@ If a `.keela_baseline.yml` file exists, Keela compares the current scan against
66
66
 
67
67
  This lets you gradually pay down tech debt while preventing new dead code from sneaking in.
68
68
 
69
+ The baseline file is organized by strategy:
70
+
71
+ ```yaml
72
+ # .keela_baseline.yml
73
+ methods:
74
+ app/models/user.rb:
75
+ - legacy_method
76
+ - old_callback
77
+ app/helpers/application_helper.rb:
78
+ - unused_helper
79
+ scopes:
80
+ app/models/user.rb:
81
+ - inactive
82
+ - archived
83
+ constants:
84
+ app/models/user.rb:
85
+ - OLD_STATUS
86
+ ```
87
+
88
+ **Note:** The baseline file only stores names, not line numbers. This prevents false positives when code moves within a file.
89
+
69
90
  ### Report Mode
70
91
 
71
92
  If no baseline exists (or you use `--report`), Keela shows all unused code:
@@ -74,6 +95,32 @@ If no baseline exists (or you use `--report`), Keela shows all unused code:
74
95
  keela --report
75
96
  ```
76
97
 
98
+ ## What "Unused" Means
99
+
100
+ Keela defines "unused" as **unused in production code** — not just unused anywhere.
101
+
102
+ By default, Keela scans `app/`, `lib/`, and `config/` directories. It intentionally
103
+ excludes `spec/` and `test/` directories because:
104
+
105
+ 1. **Tests aren't usage** — Code that only exists to be tested isn't providing
106
+ application value. If you delete the unused code, you delete its tests too.
107
+
108
+ 2. **Tests can cover dead code** — A method with 100% test coverage can still be
109
+ dead if nothing in the application calls it.
110
+
111
+ 3. **Cleaner signal** — Including test files would hide genuinely unused code
112
+ behind "but it has tests!" false negatives.
113
+
114
+ If Keela flags something that's only used in tests, consider whether the code
115
+ (and its tests) can be removed entirely. If the code is intentionally test-only
116
+ (e.g., test helpers defined in `app/`), add it to your exclusion file.
117
+
118
+ To include test directories in usage scanning (not recommended), use `--include`:
119
+
120
+ ```bash
121
+ keela --include 'test/**/*.rb' --include 'spec/**/*.rb'
122
+ ```
123
+
77
124
  ## Command Line Options
78
125
 
79
126
  ```bash
@@ -114,10 +161,19 @@ keela --config path/to/keela.yml
114
161
  # Output as JSON (for CI integrations)
115
162
  keela --format json
116
163
 
117
- # Suppress progress bar (useful for CI and scripting)
164
+ # Suppress all output (useful for CI and scripting, rely on exit code)
118
165
  keela --quiet
119
166
  keela -q
120
167
 
168
+ # Show verbose debugging output (files scanned, patterns used)
169
+ keela --verbose
170
+
171
+ # Show source location (file:line) for each unused item
172
+ keela --source-location
173
+
174
+ # Validate exclusion file entries (find stale exclusions)
175
+ keela --test-exclusions
176
+
121
177
  # Show version
122
178
  keela --version
123
179
  ```
@@ -143,13 +199,67 @@ Run all strategies (default) or target specific ones with `--type`.
143
199
 
144
200
  The `i18n_keys` strategy is **beta** and may produce false positives. It cannot detect:
145
201
 
146
- - **Lazy lookup** - `t('.title')` in views resolves based on the view path
147
202
  - **Dynamic keys** - `t("users.#{action}.title")` with interpolated segments
148
203
  - **Model translations** - `User.human_attribute_name(:email)` and `User.model_name.human`
149
- - **Pluralization siblings** - If `one:` is used, `other:` may appear unused
204
+
205
+ The following patterns ARE now supported:
206
+ - **Lazy lookup** - `t('.title')` in views resolves based on the view path
207
+ - **Pluralization siblings** - `t('key', count: n)` marks all plural forms as used
150
208
 
151
209
  Review results carefully and use the exclusion file for known false positives.
152
210
 
211
+ ## Limitations
212
+
213
+ Keela uses static analysis — it reads your code without executing it. This means some patterns are **fundamentally undetectable**.
214
+
215
+ ### What Keela CAN Detect
216
+
217
+ Keela detects **literal references** to methods, constants, etc.:
218
+
219
+ ```ruby
220
+ # ✅ Direct calls
221
+ user.save
222
+ User.find(1)
223
+
224
+ # ✅ Literal symbol references
225
+ before_save :ensure_token
226
+ validate :check_valid
227
+ send(:process_data)
228
+ respond_to?(:optional_method)
229
+ ```
230
+
231
+ ### What Keela CANNOT Detect
232
+
233
+ **Dynamic dispatch** with interpolation or variables cannot be analyzed statically:
234
+
235
+ ```ruby
236
+ # ❌ Interpolated symbols — what method does this call?
237
+ public_send(:"add_#{role}", user)
238
+
239
+ # ❌ Variable method names — could be anything
240
+ send(method_name)
241
+
242
+ # ❌ Computed method names
243
+ define_method(compute_name) { }
244
+ ```
245
+
246
+ To detect these, Keela would need to trace all possible runtime values — essentially becoming a Ruby interpreter. This is not a bug; it's a fundamental limitation of static analysis.
247
+
248
+ ### Handling False Positives
249
+
250
+ When Keela reports a method as unused but it's actually called dynamically, add it to your exclusion file:
251
+
252
+ ```yaml
253
+ # .keela/excluded.yml
254
+ methods:
255
+ app/models/project_team.rb:
256
+ - add_owner: "Called via public_send(:\"add_\#{role}\")"
257
+ - add_maintainer: "Called via public_send(:\"add_\#{role}\")"
258
+ - add_developer: "Called via public_send(:\"add_\#{role}\")"
259
+ ```
260
+
261
+ **Tip:** If you see `send`, `public_send`, or `define_method` with interpolation in a file, expect some false positives for methods in that file.
262
+
153
263
  ## Configuration File
154
264
 
155
265
  Create a `keela.yml` or `.keela.yml` in your project root:
@@ -170,10 +280,39 @@ include_patterns:
170
280
 
171
281
  excluded_path: ".keela_excluded.yml"
172
282
  baseline_path: ".keela_baseline.yml"
283
+
284
+ # Per-strategy configuration
285
+ strategies:
286
+ methods:
287
+ definition_paths:
288
+ - app/helpers
289
+ - app/models
290
+ - lib/
173
291
  ```
174
292
 
175
293
  Keela automatically loads `keela.yml` or `.keela.yml` from the current directory. Use `--config` to specify a different path.
176
294
 
295
+ ### Customizing Definition Paths Per Strategy
296
+
297
+ By default, each strategy looks for definitions in specific directories (e.g., `methods` looks in `app/helpers` and `app/models`). You can customize this per-strategy:
298
+
299
+ ```yaml
300
+ # keela.yml
301
+ strategies:
302
+ methods:
303
+ definition_paths:
304
+ - app/helpers
305
+ - app/models
306
+ - lib/
307
+ - ee/app/models
308
+ scopes:
309
+ definition_paths:
310
+ - app/models
311
+ - ee/app/models
312
+ ```
313
+
314
+ This is useful when your project has code in non-standard locations (like `lib/` or enterprise edition directories) that you want Keela to check for unused definitions.
315
+
177
316
  ### Customizing Which Files to Scan
178
317
 
179
318
  There are two approaches:
@@ -238,14 +377,17 @@ The workflow:
238
377
  2. **CI runs**: `keela` compares against baseline, fails on new dead code
239
378
  3. **After cleanup**: Run `keela --update-baseline` to update the baseline
240
379
 
241
- ### JSON Output
380
+ ### Structured Output Formats
242
381
 
243
- Use `--format json` for machine-readable output:
382
+ Use `--format` for machine-readable output:
244
383
 
245
384
  ```bash
246
- keela --format json --report
385
+ keela --format json --report # JSON output
386
+ keela --format toon --report # TOON output (token-efficient for LLMs)
247
387
  ```
248
388
 
389
+ #### JSON
390
+
249
391
  ```json
250
392
  {
251
393
  "strategies": ["methods", "scopes"],
@@ -267,7 +409,25 @@ keela --format json --report
267
409
  }
268
410
  ```
269
411
 
270
- This is useful for integrating with other tools, generating reports, or processing results programmatically.
412
+ #### TOON
413
+
414
+ [TOON (Token-Oriented Object Notation)](https://toonformat.dev/) is a compact format optimized for LLM prompts, using ~40-50% fewer tokens than JSON:
415
+
416
+ ```
417
+ strategies[2]: methods,scopes
418
+ unused:
419
+ methods:
420
+ "app/models/user.rb"[2]: unused_method,old_helper
421
+ scopes:
422
+ "app/models/post.rb"[1]: inactive
423
+ summary:
424
+ total: 3
425
+ by_strategy:
426
+ methods: 2
427
+ scopes: 1
428
+ ```
429
+
430
+ These formats are useful for integrating with other tools, generating reports, processing results programmatically, or including in LLM context windows.
271
431
 
272
432
  ## Exclusion File
273
433
 
data/exe/keela CHANGED
@@ -43,7 +43,7 @@ OptionParser.new do |opts|
43
43
  options[:update_baseline] = true
44
44
  end
45
45
 
46
- opts.on("--format FORMAT", %i[text json], "Output format: text (default) or json") do |format|
46
+ opts.on("--format FORMAT", %i[text json toon], "Output format: text (default), json, or toon") do |format|
47
47
  options[:format] = format
48
48
  end
49
49
 
@@ -82,6 +82,18 @@ OptionParser.new do |opts|
82
82
  options[:quiet] = true
83
83
  end
84
84
 
85
+ opts.on("--verbose", "Show detailed debugging output (files scanned, patterns used)") do
86
+ options[:verbose] = true
87
+ end
88
+
89
+ opts.on("--source-location", "Show file:line for each unused item in reports") do
90
+ options[:source_location] = true
91
+ end
92
+
93
+ opts.on("--test-exclusions", "Validate that exclusion file entries match actual definitions") do
94
+ options[:test_exclusions] = true
95
+ end
96
+
85
97
  opts.on("-h", "--help", "Show this help") do
86
98
  puts opts
87
99
  exit
@@ -99,9 +111,30 @@ end
99
111
  # Apply quiet mode if requested
100
112
  Keela.configuration.show_progress = false if options[:quiet]
101
113
 
114
+ # Apply verbose mode if requested
115
+ Keela.configuration.verbose = true if options[:verbose]
116
+
117
+ # Apply source location mode if requested
118
+ Keela.configuration.source_location = true if options[:source_location]
119
+
102
120
  # Set default baseline path if not specified
103
121
  Keela.configuration.baseline_path ||= ".keela_baseline.yml"
104
122
 
123
+ # Handle --test-exclusions mode
124
+ if options[:test_exclusions]
125
+ excluded_path = Keela.configuration.excluded_path ||
126
+ Keela::Scanner::DEFAULT_EXCLUDED_PATHS.find { |p| File.exist?(p) }
127
+
128
+ unless excluded_path
129
+ warn "Error: No exclusion file found. Specify with --excluded PATH"
130
+ exit 1
131
+ end
132
+
133
+ validator = Keela::ExclusionValidator.new(excluded_path)
134
+ success = validator.validate
135
+ exit(success ? 0 : 1)
136
+ end
137
+
105
138
  STRATEGY_MAP = {
106
139
  methods: Keela::Strategies::Methods,
107
140
  scopes: Keela::Strategies::Scopes,
@@ -141,13 +174,46 @@ baseline = Keela::Baseline.new(Keela.configuration.baseline_path)
141
174
  # Load source files once and share across all strategies
142
175
  source_files = Keela::Scanner.load_source_files
143
176
 
177
+ # Show verbose output if requested
178
+ if options[:verbose]
179
+ config = Keela.configuration
180
+ globs = Keela::Scanner.build_file_globs(config)
181
+
182
+ puts Rainbow("=== Verbose Mode ===").magenta.bright
183
+ puts
184
+ puts Rainbow("Configuration:").yellow
185
+ puts " Extensions: #{config.extensions.join(', ')}"
186
+ puts " Directory patterns: #{config.directory_patterns.join(', ')}"
187
+ puts " Include patterns: #{config.include_patterns.empty? ? '(none)' : config.include_patterns.join(', ')}"
188
+ puts " Exclude patterns: #{config.exclude_patterns.empty? ? '(none)' : config.exclude_patterns.join(', ')}"
189
+ puts
190
+ puts Rainbow("Resolved globs:").yellow
191
+ globs.each { |g| puts " #{g}" }
192
+ puts
193
+ sorted_files = source_files.keys.sort
194
+ file_count = sorted_files.size
195
+ max_display = 50
196
+
197
+ puts Rainbow("Files to scan (#{file_count}):").yellow
198
+ # Show all files when piping (not a TTY), truncate for interactive use
199
+ if file_count <= max_display || !$stdout.tty?
200
+ sorted_files.each { |f| puts " #{f}" }
201
+ else
202
+ sorted_files.first(max_display).each { |f| puts " #{f}" }
203
+ puts " ... and #{file_count - max_display} more files"
204
+ puts " (pipe output to see full list: keela --verbose > files.txt)"
205
+ end
206
+ puts
207
+ end
208
+
144
209
  success = true
145
210
  results = {}
146
211
 
147
- json_mode = options[:format] == :json
212
+ structured_output = Keela::Formatters.structured_format?(options[:format])
213
+ silent_output = structured_output || options[:quiet]
148
214
 
149
215
  strategies.each_with_index do |strategy, index|
150
- unless json_mode
216
+ unless silent_output
151
217
  puts Rainbow("=== Sniffing for unused #{strategy.name} ===").cyan.bright if strategies.size > 1
152
218
  end
153
219
 
@@ -155,13 +221,13 @@ strategies.each_with_index do |strategy, index|
155
221
  success &&= scanner.run(
156
222
  force_report: options[:force_report],
157
223
  update_baseline: options[:update_baseline],
158
- silent: json_mode
224
+ silent: silent_output
159
225
  )
160
226
 
161
- # Collect results for JSON output
227
+ # Collect results for structured output formats
162
228
  results[strategy.name] = scanner.unused_collection.transform_values(&:to_a)
163
229
 
164
- unless json_mode
230
+ unless silent_output
165
231
  puts if strategies.size > 1 && index < strategies.size - 1
166
232
  end
167
233
  end
@@ -169,26 +235,14 @@ end
169
235
  # Save baseline after all strategies have run
170
236
  if options[:update_baseline]
171
237
  baseline.save
172
- puts Rainbow("Updated #{baseline.path}").green.bright unless json_mode
238
+ puts Rainbow("Updated #{baseline.path}").green.bright unless silent_output
173
239
  end
174
240
 
175
- # Output JSON if requested
176
- if json_mode
177
- require "json"
178
-
179
- total = results.values.flat_map(&:values).flatten.size
180
- by_strategy = results.transform_values { |files| files.values.flatten.size }
181
-
182
- output = {
183
- strategies: strategies.map(&:name),
184
- unused: results.reject { |_, v| v.empty? },
185
- summary: {
186
- total: total,
187
- by_strategy: by_strategy.reject { |_, v| v.zero? }
188
- }
189
- }
190
-
191
- puts JSON.pretty_generate(output)
241
+ # Output structured format if requested
242
+ if structured_output
243
+ formatter_class = Keela::Formatters.for(options[:format])
244
+ formatter = formatter_class.new(results: results, strategies: strategies)
245
+ puts formatter.format
192
246
  end
193
247
 
194
248
  exit(success ? 0 : 1)
@@ -17,6 +17,7 @@ module Keela
17
17
  # - excluded_path: Path to YAML file of excluded items
18
18
  # - baseline_path: Path to baseline YAML file
19
19
  # - required_directory: Directory that must exist for scanning to proceed
20
+ # - strategies: Per-strategy configuration (see below)
20
21
  #
21
22
  # Example:
22
23
  # # keela.yml
@@ -29,6 +30,15 @@ module Keela
29
30
  # - rb
30
31
  # - haml
31
32
  # - erb
33
+ # strategies:
34
+ # methods:
35
+ # definition_paths:
36
+ # - app/helpers
37
+ # - app/models
38
+ # - lib/
39
+ # scopes:
40
+ # definition_paths:
41
+ # - app/models
32
42
  #
33
43
  module ConfigFile
34
44
  CONFIG_FILENAMES = %w[.keela/config.yml keela.yml .keela.yml].freeze
@@ -74,6 +84,15 @@ module Keela
74
84
  value = config[key]
75
85
  configuration.public_send("#{key}=", value)
76
86
  end
87
+
88
+ apply_strategy_options(config["strategies"]) if config.key?("strategies")
89
+ end
90
+
91
+ def apply_strategy_options(strategies)
92
+ return unless strategies.is_a?(Hash)
93
+
94
+ configuration = Keela.configuration
95
+ configuration.strategy_options = strategies
77
96
  end
78
97
  end
79
98
  end
@@ -26,6 +26,18 @@ module Keela
26
26
  # Additional directory patterns to include (added to directory_patterns)
27
27
  attr_accessor :include_patterns
28
28
 
29
+ # Whether to show verbose debugging output
30
+ attr_accessor :verbose
31
+
32
+ # Whether to show source location (file:line) in reports
33
+ attr_accessor :source_location
34
+
35
+ # Per-strategy configuration options
36
+ # Hash of strategy_name => { option => value }
37
+ # Supported options:
38
+ # - definition_file_pattern: Regex pattern string for files containing definitions
39
+ attr_accessor :strategy_options
40
+
29
41
  def initialize
30
42
  @extensions = %w[rb haml erb].freeze
31
43
  @directory_patterns = %w[
@@ -39,6 +51,14 @@ module Keela
39
51
  @show_progress = true
40
52
  @exclude_patterns = []
41
53
  @include_patterns = []
54
+ @verbose = false
55
+ @source_location = false
56
+ @strategy_options = {}
57
+ end
58
+
59
+ # Get options for a specific strategy
60
+ def options_for(strategy_name)
61
+ strategy_options[strategy_name.to_s] || {}
42
62
  end
43
63
  end
44
64
  end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "rainbow"
5
+
6
+ module Keela
7
+ class ExclusionValidator
8
+ STRATEGY_MAP = {
9
+ methods: Strategies::Methods,
10
+ scopes: Strategies::Scopes,
11
+ constants: Strategies::Constants,
12
+ delegations: Strategies::Delegations,
13
+ attributes: Strategies::Attributes,
14
+ i18n_keys: Strategies::I18nKeys
15
+ }.freeze
16
+
17
+ attr_reader :excluded_path, :results
18
+
19
+ def initialize(excluded_path)
20
+ @excluded_path = excluded_path
21
+ @results = { valid: [], stale: [] }
22
+ end
23
+
24
+ def validate
25
+ unless File.exist?(excluded_path)
26
+ puts Rainbow("Exclusion file not found: #{excluded_path}").red
27
+ return false
28
+ end
29
+
30
+ all_excluded = YAML.load_file(excluded_path, symbolize_names: true) || {}
31
+
32
+ if all_excluded.empty?
33
+ puts Rainbow("Exclusion file is empty: #{excluded_path}").yellow
34
+ return true
35
+ end
36
+
37
+ puts "Validating exclusions in #{excluded_path}...\n\n"
38
+
39
+ # Detect format: strategy-aware or legacy flat
40
+ if strategy_aware_format?(all_excluded)
41
+ validate_strategy_aware(all_excluded)
42
+ else
43
+ validate_legacy_flat(all_excluded)
44
+ end
45
+
46
+ print_results
47
+ results[:stale].empty?
48
+ end
49
+
50
+ private
51
+
52
+ def strategy_aware_format?(data)
53
+ data.keys.any? { |k| STRATEGY_MAP.key?(k) }
54
+ end
55
+
56
+ def validate_strategy_aware(all_excluded)
57
+ all_excluded.each do |strategy_name, files|
58
+ next unless STRATEGY_MAP.key?(strategy_name)
59
+
60
+ strategy = STRATEGY_MAP[strategy_name].new
61
+ validate_files(strategy, strategy_name, files || {})
62
+ end
63
+ end
64
+
65
+ def validate_legacy_flat(all_excluded)
66
+ # Legacy format applies to all strategies, but we'll check against methods
67
+ strategy = Strategies::Methods.new
68
+ validate_files(strategy, :methods, all_excluded)
69
+ end
70
+
71
+ def validate_files(strategy, strategy_name, files)
72
+ files.each do |file_path, entries|
73
+ file_str = file_path.to_s
74
+ entries ||= []
75
+
76
+ entries.each do |entry|
77
+ name = entry.keys.first.to_s
78
+ reason = entry.values.first
79
+
80
+ if !File.exist?(file_str)
81
+ results[:stale] << {
82
+ strategy: strategy_name,
83
+ file: file_str,
84
+ name: name,
85
+ reason: reason,
86
+ error: "file not found"
87
+ }
88
+ elsif !definition_exists?(strategy, file_str, name)
89
+ results[:stale] << {
90
+ strategy: strategy_name,
91
+ file: file_str,
92
+ name: name,
93
+ reason: reason,
94
+ error: "no definition found"
95
+ }
96
+ else
97
+ results[:valid] << {
98
+ strategy: strategy_name,
99
+ file: file_str,
100
+ name: name,
101
+ reason: reason
102
+ }
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ def definition_exists?(strategy, file_path, name)
109
+ return false unless File.exist?(file_path)
110
+
111
+ lines = File.readlines(file_path)
112
+
113
+ # Check custom extraction first (for I18n YAML files)
114
+ custom_defs = strategy.extract_definitions_from_file(file_path, lines)
115
+ if custom_defs
116
+ return custom_defs.any? { |d| d[:name] == name }
117
+ end
118
+
119
+ # Default line-by-line extraction
120
+ lines.any? do |line|
121
+ next if strategy.skip_comments? && line.strip.start_with?("#")
122
+
123
+ result = strategy.extract_definition(line)
124
+ Array(result).compact.include?(name)
125
+ end
126
+ end
127
+
128
+ def print_results
129
+ if results[:valid].any?
130
+ puts Rainbow("✅ Valid exclusions (#{results[:valid].size}):").green.bright
131
+ group_by_strategy(results[:valid]).each do |strategy, entries|
132
+ puts " #{strategy}:"
133
+ entries.each do |e|
134
+ puts " #{e[:file]}:#{e[:name]}"
135
+ end
136
+ end
137
+ puts
138
+ end
139
+
140
+ if results[:stale].any?
141
+ puts Rainbow("⚠️ Stale exclusions found (#{results[:stale].size}):").yellow.bright
142
+ puts
143
+ group_by_strategy(results[:stale]).each do |strategy, entries|
144
+ puts " #{strategy}:"
145
+ entries.each do |e|
146
+ puts " #{e[:file]}:#{e[:name]} — #{e[:error]}"
147
+ end
148
+ end
149
+ puts
150
+ puts "Consider removing stale exclusions from #{excluded_path}"
151
+ else
152
+ puts Rainbow("All exclusions are valid!").green.bright
153
+ end
154
+ end
155
+
156
+ def group_by_strategy(entries)
157
+ entries.group_by { |e| e[:strategy] }
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keela
4
+ module Formatters
5
+ class Base
6
+ attr_reader :results, :strategies
7
+
8
+ def initialize(results:, strategies:)
9
+ @results = results
10
+ @strategies = strategies
11
+ end
12
+
13
+ def format
14
+ raise NotImplementedError, "Subclasses must implement #format"
15
+ end
16
+
17
+ private
18
+
19
+ def total_count
20
+ results.values.flat_map(&:values).flatten.size
21
+ end
22
+
23
+ def by_strategy_counts
24
+ results.transform_values { |files| files.values.flatten.size }
25
+ .reject { |_, v| v.zero? }
26
+ end
27
+
28
+ def non_empty_results
29
+ results.reject { |_, v| v.empty? }
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Keela
6
+ module Formatters
7
+ class Json < Base
8
+ def format
9
+ output = {
10
+ strategies: strategies.map(&:name),
11
+ unused: non_empty_results,
12
+ summary: {
13
+ total: total_count,
14
+ by_strategy: by_strategy_counts
15
+ }
16
+ }
17
+
18
+ JSON.pretty_generate(output)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "toon"
4
+
5
+ module Keela
6
+ module Formatters
7
+ class Toon < Base
8
+ def format
9
+ output = {
10
+ "strategies" => strategies.map(&:name),
11
+ "unused" => non_empty_results.transform_keys(&:to_s),
12
+ "summary" => {
13
+ "total" => total_count,
14
+ "by_strategy" => by_strategy_counts.transform_keys(&:to_s)
15
+ }
16
+ }
17
+
18
+ ::Toon.encode(output)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "formatters/base"
4
+ require_relative "formatters/json"
5
+ require_relative "formatters/toon"
6
+
7
+ module Keela
8
+ module Formatters
9
+ REGISTRY = {
10
+ json: Json,
11
+ toon: Toon
12
+ }.freeze
13
+
14
+ def self.for(format)
15
+ REGISTRY[format]
16
+ end
17
+
18
+ def self.structured_format?(format)
19
+ REGISTRY.key?(format)
20
+ end
21
+ end
22
+ end
@@ -11,12 +11,12 @@ module Keela
11
11
  @strategy_name = strategy_name
12
12
  end
13
13
 
14
- def print_full_report(unused_collection, elapsed_time)
14
+ def print_full_report(unused_collection, elapsed_time, source_locations: {})
15
15
  unused_count = unused_collection.values.flatten.size
16
16
 
17
17
  if unused_count > 0
18
18
  puts "\nFound #{unused_count} unused #{strategy_name}:\n\n"
19
- puts format_yaml(unused_collection)
19
+ puts format_report(unused_collection, source_locations)
20
20
  puts "\n"
21
21
  else
22
22
  puts Rainbow("No unused #{strategy_name} were found.").green.bright
@@ -25,8 +25,8 @@ module Keela
25
25
  puts "Finished in #{elapsed_time.round(2)} seconds."
26
26
  end
27
27
 
28
- def print_diff_report(new_unused, removed, excluded_path:, baseline_path:)
29
- print_new_unused(new_unused, excluded_path) unless new_unused.empty?
28
+ def print_diff_report(new_unused, removed, excluded_path:, baseline_path:, source_locations: {})
29
+ print_new_unused(new_unused, excluded_path, source_locations) unless new_unused.empty?
30
30
 
31
31
  if new_unused.size + removed.size > 0
32
32
  puts Rainbow("~" * 80).white.bright
@@ -36,13 +36,42 @@ module Keela
36
36
  print_removed(removed, baseline_path) unless removed.empty?
37
37
  end
38
38
 
39
+ def format_report(collection, source_locations = {})
40
+ if source_locations.empty?
41
+ format_yaml(collection)
42
+ else
43
+ format_with_locations(collection, source_locations)
44
+ end
45
+ end
46
+
39
47
  def format_yaml(collection)
40
48
  indent_yaml_list_items(collection.sort.to_h.to_yaml)
41
49
  end
42
50
 
43
51
  private
44
52
 
45
- def print_new_unused(new_unused, excluded_path)
53
+ def format_with_locations(collection, source_locations)
54
+ # Calculate max name length for alignment
55
+ all_names = collection.values.flatten
56
+ max_name_len = all_names.map(&:length).max || 0
57
+ padding = max_name_len + 6 # " - " prefix + 2 spaces
58
+
59
+ lines = ["---"]
60
+ collection.sort.each do |file, names|
61
+ lines << "#{file}:"
62
+ names.each do |name|
63
+ line_num = source_locations["#{file}:#{name}"]
64
+ if line_num
65
+ lines << " - #{name}".ljust(padding) + "#{file}:#{line_num}"
66
+ else
67
+ lines << " - #{name}"
68
+ end
69
+ end
70
+ end
71
+ lines.join("\n")
72
+ end
73
+
74
+ def print_new_unused(new_unused, excluded_path, source_locations = {})
46
75
  error = <<~MESSAGE
47
76
  We have detected #{new_unused.size} newly unused #{strategy_name}.
48
77
 
@@ -50,7 +79,7 @@ module Keela
50
79
  MESSAGE
51
80
 
52
81
  puts Rainbow(error).red.bright
53
- puts Rainbow(format_yaml(parse_diff(new_unused))).red.bright
82
+ puts Rainbow(format_report(parse_diff(new_unused), source_locations)).red.bright
54
83
  end
55
84
 
56
85
  def print_removed(removed, baseline_path)
data/lib/keela/scanner.rb CHANGED
@@ -5,7 +5,7 @@ require "yaml"
5
5
 
6
6
  module Keela
7
7
  class Scanner
8
- attr_reader :strategy, :configuration, :baseline, :source_files, :unused_collection, :new_unused, :removed
8
+ attr_reader :strategy, :configuration, :baseline, :source_files, :unused_collection, :source_locations, :new_unused, :removed
9
9
 
10
10
  DEFAULT_EXCLUDED_PATHS = [".keela/excluded.yml", "keela_excluded.yml"].freeze
11
11
  DEFAULT_BASELINE_PATHS = [".keela/baseline.yml", "keela_baseline.yml"].freeze
@@ -48,6 +48,7 @@ module Keela
48
48
  @source_files = source_files || {}
49
49
  @source_files_preloaded = !source_files.nil?
50
50
  @unused_collection = Hash.new { |hash, key| hash[key] = [] }
51
+ @source_locations = {} # { "file:name" => line_number }
51
52
  @new_unused = []
52
53
  @removed = []
53
54
  end
@@ -70,7 +71,7 @@ module Keela
70
71
 
71
72
  if report_mode
72
73
  elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
73
- reporter.print_full_report(unused_collection, elapsed) unless silent
74
+ reporter.print_full_report(unused_collection, elapsed, source_locations: source_locations) unless silent
74
75
  if update_baseline
75
76
  baseline.set(strategy.name, unused_collection)
76
77
  # Note: caller is responsible for calling baseline.save after all strategies run
@@ -85,7 +86,8 @@ module Keela
85
86
  new_unused,
86
87
  removed,
87
88
  excluded_path: resolve_excluded_path || ".keela/excluded.yml",
88
- baseline_path: baseline.path
89
+ baseline_path: baseline.path,
90
+ source_locations: source_locations
89
91
  )
90
92
  end
91
93
 
@@ -155,11 +157,17 @@ module Keela
155
157
  next custom_definitions if custom_definitions
156
158
 
157
159
  # Default: line-by-line parsing
158
- lines.flat_map do |line|
160
+ track_lines = configuration.source_location
161
+ lines.each_with_index.flat_map do |line, index|
159
162
  next [] if strategy.skip_comments? && line.strip.start_with?("#")
160
163
 
161
- name = strategy.extract_definition(line)
162
- name ? [{ name: name, file: filename }] : []
164
+ result = strategy.extract_definition(line)
165
+ # Support both single name (String) and multiple names (Array)
166
+ Array(result).compact.map do |name|
167
+ definition = { name: name, file: filename }
168
+ definition[:line] = index + 1 if track_lines
169
+ definition
170
+ end
163
171
  end
164
172
  end
165
173
  end
@@ -202,15 +210,25 @@ module Keela
202
210
  def find_unused(definitions, show_progress: false)
203
211
  source_code = source_files.values.flatten.join
204
212
 
213
+ # Get additional used names from strategy-specific detection
214
+ # (e.g., I18n lazy lookup)
215
+ additional_used = strategy.additional_used_names(source_files)
216
+
205
217
  progress_label = show_progress ? "Checking #{strategy.name}" : nil
206
218
 
207
219
  unused = Parallel.flat_map(definitions, progress: progress_label) do |definition|
220
+ # Check if marked as used by additional detection
221
+ next [] if additional_used.include?(definition[:name])
222
+
208
223
  regex = strategy.usage_regex(definition[:name])
209
224
  regex.match?(source_code) ? [] : definition
210
225
  end
211
226
 
212
227
  unused.each do |unused_def|
213
228
  @unused_collection[unused_def[:file]] << unused_def[:name]
229
+ if unused_def[:line]
230
+ @source_locations["#{unused_def[:file]}:#{unused_def[:name]}"] = unused_def[:line]
231
+ end
214
232
  end
215
233
  end
216
234
 
@@ -7,7 +7,7 @@ module Keela
7
7
  "attributes"
8
8
  end
9
9
 
10
- def definition_file_pattern
10
+ def default_definition_file_pattern
11
11
  # Match app/ and lib/ directories, but exclude spec/ and test/
12
12
  %r{(?:^|/)(?:ee/)?(?:app|lib)/}
13
13
  end
@@ -7,7 +7,7 @@ module Keela
7
7
  "constants"
8
8
  end
9
9
 
10
- def definition_file_pattern
10
+ def default_definition_file_pattern
11
11
  # Match app/ and lib/ directories, but exclude spec/ and test/
12
12
  %r{(?:^|/)(?:ee/)?(?:app|lib)/}
13
13
  end
@@ -41,8 +41,12 @@ module Keela
41
41
  # uppercase letters/digits/underscores (partial match)
42
42
  # Uses negative lookahead to avoid:
43
43
  # - partial matches (followed by uppercase letters/digits/underscores)
44
- # - definitions (followed by optional whitespace then =, but not ==)
45
- /(?<![A-Z0-9_])#{Regexp.quote(name)}(?![A-Z0-9_])(?!\s*=(?!=))/
44
+ # - definitions (followed by optional whitespace then =, but not == or =>)
45
+ #
46
+ # The pattern (?!\s*=(?![=>])) means:
47
+ # - Don't match if followed by optional whitespace, then =
48
+ # - UNLESS that = is followed by = (comparison) or > (hash rocket)
49
+ /(?<![A-Z0-9_])#{Regexp.quote(name)}(?![A-Z0-9_])(?!\s*=(?![=>]))/
46
50
  end
47
51
 
48
52
  def skip_comments?
@@ -7,7 +7,7 @@ module Keela
7
7
  "delegations"
8
8
  end
9
9
 
10
- def definition_file_pattern
10
+ def default_definition_file_pattern
11
11
  # Match app/models/ directories (including concerns), but exclude spec/test
12
12
  %r{(?:^|/)(?:ee/)?app/models/}
13
13
  end
@@ -44,12 +44,8 @@ module Keela
44
44
  methods = methods.map { |m| "#{prefix}_#{m}" }
45
45
  end
46
46
 
47
- # Return single string for single method (scanner expects this)
48
- # For multiple methods, return first one only
49
- # The scanner will create one definition entry per extract_definition call
50
- # To handle multiple delegations per line, we'd need to change the scanner
51
- # For now, return just the first method
52
- methods.first
47
+ # Return all methods (scanner handles both single string and array)
48
+ methods.length == 1 ? methods.first : methods
53
49
  end
54
50
 
55
51
  def usage_regex(name)
@@ -15,14 +15,21 @@ module Keela
15
15
  # - t(:key)
16
16
  # - .human_attribute_name(:attr)
17
17
  #
18
- # Note: Lazy lookup (t('.title') in views) is not yet supported.
18
+ # Pluralization keys (zero, one, two, few, many, other) are grouped:
19
+ # if t("items.count", count: n) is called, all siblings are considered used.
20
+ #
21
+ # Lazy lookup is supported: t('.title') in app/views/users/show.html.erb
22
+ # resolves to 'users.show.title'.
19
23
  #
20
24
  class I18nKeys < Strategy
25
+ PLURAL_SUFFIXES = %w[zero one two few many other].freeze
26
+ VIEW_PATH_REGEX = %r{(?:ee/)?app/views/(.+)\.html\.(?:erb|haml|slim)$}.freeze
27
+ LAZY_LOOKUP_REGEX = /(?:I18n\.)?t\s*\(\s*['"](\.[^'"]+)['"]/
21
28
  def name
22
29
  "i18n_keys"
23
30
  end
24
31
 
25
- def definition_file_pattern
32
+ def default_definition_file_pattern
26
33
  # Match locale YAML files
27
34
  %r{config/locales/.*\.ya?ml$}
28
35
  end
@@ -32,10 +39,19 @@ module Keela
32
39
  return [] unless File.exist?(filepath)
33
40
 
34
41
  content = YAML.load_file(filepath, permitted_classes: [Symbol]) || {}
35
- flatten_keys(content).map do |key|
42
+ keys = flatten_keys(content).map do |key|
36
43
  # Remove the locale prefix (e.g., "en.users.show" -> "users.show")
37
- key_without_locale = key.sub(/^[a-z]{2}(-[A-Z]{2})?\./, "")
38
- { name: key_without_locale, file: filepath }
44
+ key.sub(/^[a-z]{2}(-[A-Z]{2})?\./, "")
45
+ end
46
+
47
+ # For pluralization keys, also add the parent key so that
48
+ # t("items.count", count: n) marks all siblings as used
49
+ parent_keys = keys.filter_map do |key|
50
+ parent_key_for_pluralization(key)
51
+ end.uniq
52
+
53
+ (keys + parent_keys).uniq.map do |key|
54
+ { name: key, file: filepath }
39
55
  end
40
56
  rescue Psych::SyntaxError => e
41
57
  warn "Warning: Could not parse #{filepath}: #{e.message}"
@@ -55,20 +71,86 @@ module Keela
55
71
  # t('users.show.title')
56
72
  # t(:users_show_title) - symbol form (underscored)
57
73
  #
58
- # Also match partial keys for lazy lookup support:
59
- # t(".title") in a view could match "users.show.title"
74
+ # For pluralization keys, also match the parent key:
75
+ # t("items.count", count: n) should match items.count.one, items.count.other, etc.
60
76
  quoted_name = Regexp.quote(name)
61
77
 
62
- # Build pattern that matches the key in quotes or as a symbol
63
- /(?:I18n\.)?t\s*\(\s*["':]+#{quoted_name}["']?\s*[,)]/
78
+ # Check if this is a pluralization key and build alternate pattern
79
+ parent_key = parent_key_for_pluralization(name)
80
+ if parent_key
81
+ quoted_parent = Regexp.quote(parent_key)
82
+ # Match either the exact key OR the parent key
83
+ /(?:I18n\.)?t\s*\(\s*["':]+(?:#{quoted_name}|#{quoted_parent})["']?\s*[,)]/
84
+ else
85
+ # Build pattern that matches the key in quotes or as a symbol
86
+ /(?:I18n\.)?t\s*\(\s*["':]+#{quoted_name}["']?\s*[,)]/
87
+ end
64
88
  end
65
89
 
66
90
  def skip_comments?
67
91
  true
68
92
  end
69
93
 
94
+ # Convert a view file path to its I18n prefix
95
+ # "app/views/users/show.html.erb" -> "users.show"
96
+ # "app/views/users/_form.html.erb" -> "users.form"
97
+ # "ee/app/views/users/show.html.erb" -> "users.show"
98
+ def view_path_to_prefix(path)
99
+ return nil unless path =~ VIEW_PATH_REGEX
100
+
101
+ view_path = Regexp.last_match(1)
102
+
103
+ # Split into parts and process
104
+ parts = view_path.split("/")
105
+
106
+ # Handle partials: _form -> form
107
+ parts[-1] = parts[-1].sub(/^_/, "")
108
+
109
+ parts.join(".")
110
+ end
111
+
112
+ # Extract lazy lookup keys from view content
113
+ # Returns array of keys like [".title", ".description"]
114
+ def extract_lazy_keys(content)
115
+ content.scan(LAZY_LOOKUP_REGEX).flatten
116
+ end
117
+
118
+ # Build a set of expanded lazy lookup keys from view files
119
+ # Called by the scanner to detect usage via lazy lookup
120
+ def additional_used_names(source_files)
121
+ expanded = Set.new
122
+
123
+ source_files.each do |filepath, lines|
124
+ prefix = view_path_to_prefix(filepath)
125
+ next unless prefix
126
+
127
+ content = lines.join("\n")
128
+ lazy_keys = extract_lazy_keys(content)
129
+
130
+ lazy_keys.each do |lazy_key|
131
+ # .title -> users.show.title
132
+ full_key = "#{prefix}#{lazy_key}"
133
+ expanded << full_key
134
+ end
135
+ end
136
+
137
+ expanded
138
+ end
139
+
70
140
  private
71
141
 
142
+ # Returns the parent key if this is a pluralization key, nil otherwise
143
+ # "items.count.one" -> "items.count"
144
+ # "users.show.title" -> nil
145
+ def parent_key_for_pluralization(key)
146
+ PLURAL_SUFFIXES.each do |suffix|
147
+ if key.end_with?(".#{suffix}")
148
+ return key.sub(/\.#{suffix}$/, "")
149
+ end
150
+ end
151
+ nil
152
+ end
153
+
72
154
  # Flatten nested hash to dot-notation keys
73
155
  # { "en" => { "users" => { "title" => "..." } } }
74
156
  # becomes ["en.users.title"]
@@ -7,7 +7,7 @@ module Keela
7
7
  "methods"
8
8
  end
9
9
 
10
- def definition_file_pattern
10
+ def default_definition_file_pattern
11
11
  %r{app/helpers|app/models}
12
12
  end
13
13
 
@@ -18,12 +18,20 @@ module Keela
18
18
  end
19
19
 
20
20
  def usage_regex(name)
21
+ method_name = Regexp.quote(name.sub(/^self\./, ""))
22
+
21
23
  if name.end_with?("=")
22
24
  # Setter method: match assignment usage
23
- /(?<!def )#{Regexp.quote(name.sub(/^self\./, "").chomp("="))}\W=*/
25
+ /(?<!def |def self\.)#{method_name.chomp("=")}\W=*/
24
26
  else
25
- # Regular method: match calls
26
- /(?<!def )#{Regexp.quote(name.sub(/^self\./, ""))}\W/
27
+ # Regular method: match calls and symbol references
28
+ # Matches:
29
+ # - Direct calls: foo(arg), obj.foo
30
+ # - Symbol references: :foo, :foo! (callbacks, send, etc.)
31
+ # Excludes:
32
+ # - Definitions: def foo, def self.foo
33
+ # - Partial matches: :foobar, before_foo (via word boundary lookbehind)
34
+ /(?<!def |def self\.)(?<![A-Za-z0-9_]):?#{method_name}(?:\W|$)/
27
35
  end
28
36
  end
29
37
 
@@ -7,7 +7,7 @@ module Keela
7
7
  "scopes"
8
8
  end
9
9
 
10
- def definition_file_pattern
10
+ def default_definition_file_pattern
11
11
  %r{app/models}
12
12
  end
13
13
 
@@ -14,8 +14,23 @@ module Keela
14
14
 
15
15
  # Regex pattern to match files that may contain definitions
16
16
  # (e.g., /app\/models/ for scopes)
17
+ #
18
+ # Can be overridden via configuration:
19
+ # strategies:
20
+ # methods:
21
+ # definition_paths:
22
+ # - app/helpers
23
+ # - app/models
24
+ # - lib/
25
+ #
17
26
  def definition_file_pattern
18
- raise NotImplementedError, "#{self.class} must implement #definition_file_pattern"
27
+ configured_pattern || default_definition_file_pattern
28
+ end
29
+
30
+ # Default pattern when no configuration override is provided.
31
+ # Subclasses should implement this instead of definition_file_pattern.
32
+ def default_definition_file_pattern
33
+ raise NotImplementedError, "#{self.class} must implement #default_definition_file_pattern"
19
34
  end
20
35
 
21
36
  # Extract a definition name from a line of code, or nil if no definition found
@@ -39,5 +54,23 @@ module Keela
39
54
  def extract_definitions_from_file(_filepath, _lines)
40
55
  nil
41
56
  end
57
+
58
+ # Override this method for strategies that need to detect usage through
59
+ # patterns that can't be expressed as a simple regex (e.g., I18n lazy lookup).
60
+ # Returns a Set of definition names that are considered "used".
61
+ # Called with the source_files hash { filepath => [lines] }.
62
+ def additional_used_names(_source_files)
63
+ Set.new
64
+ end
65
+
66
+ private
67
+
68
+ def configured_pattern
69
+ paths = Keela.configuration.options_for(name)["definition_paths"]
70
+ return nil unless paths.is_a?(Array) && paths.any?
71
+
72
+ escaped = paths.map { |p| Regexp.escape(p.to_s) }
73
+ Regexp.new(escaped.join("|"))
74
+ end
42
75
  end
43
76
  end
data/lib/keela/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Keela
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.1"
5
5
  end
data/lib/keela.rb CHANGED
@@ -13,6 +13,8 @@ require_relative "keela/strategies/i18n_keys"
13
13
  require_relative "keela/reporter"
14
14
  require_relative "keela/baseline"
15
15
  require_relative "keela/scanner"
16
+ require_relative "keela/exclusion_validator"
17
+ require_relative "keela/formatters"
16
18
 
17
19
  module Keela
18
20
  class Error < StandardError; end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: keela
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kerri Miller
@@ -51,6 +51,20 @@ dependencies:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
53
  version: '1.11'
54
+ - !ruby/object:Gem::Dependency
55
+ name: toon-ruby
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.1'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0.1'
54
68
  description: Like the famous CSI dog who found what others missed, Keela detects unused
55
69
  methods and scopes in your Ruby codebase.
56
70
  email: kerrizor@kerrizor.com
@@ -67,6 +81,11 @@ files:
67
81
  - lib/keela/baseline.rb
68
82
  - lib/keela/config_file.rb
69
83
  - lib/keela/configuration.rb
84
+ - lib/keela/exclusion_validator.rb
85
+ - lib/keela/formatters.rb
86
+ - lib/keela/formatters/base.rb
87
+ - lib/keela/formatters/json.rb
88
+ - lib/keela/formatters/toon.rb
70
89
  - lib/keela/reporter.rb
71
90
  - lib/keela/scanner.rb
72
91
  - lib/keela/strategies/attributes.rb