keela 0.2.3 → 0.3.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: 47038e1bf08a377dba99ae8fe91134d51726c3ee20f12a24083ee4f6ecd421db
4
- data.tar.gz: 429275504d8993d585671dde3cb8b09f374aa315080667d3272958656a38fff3
3
+ metadata.gz: 4fcf07cf3f0adee173c2dfaef9e79f256f6a6318c5dd843935b6113fede4dea4
4
+ data.tar.gz: 454fa3bc9a9839dcf7d5f40a6d2fa2160daeb3a2d113d2f372da6ce15f9a49b0
5
5
  SHA512:
6
- metadata.gz: 146b50c0ec50be23c122a63481ba8a6185234dc9234577530178753346f1da8f30bccbecfc4d414a014bb056bf1d2adbcbb80531391ebb7f1185a266497b9ffc
7
- data.tar.gz: d3250b7c810ae53389e0a18551d1c408f7218b1437f5047b76bc36e36e4fdf069d1166cd6e0ac64356366278ed73b986d0e511ad1cbf531814bdab3093fbf233
6
+ metadata.gz: f9105a9cc47b95443d527772bd1dc44b5578d3b88829ec0144109e1ce82cc03d3414be62de9c8e4f8a7ab308b742e3beea343e6aefd2b4e3184f694847bba711
7
+ data.tar.gz: b583a1e03b6b460d4393ed687ef8796315c6fc4e8f9e4afde3a4edce26581536ef0b72dbef7215237b97835263b12a52b55879f391d901efe5409007ae407982
data/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.0] - 2026-08-05
11
+
12
+ ### Added
13
+
14
+ - **Strategy-aware exclusion file format** - Exclusion files can now be organized by strategy (methods, scopes, etc.) to match the baseline file format. This allows excluding the same name in one strategy while flagging it in another. The legacy flat format is still supported for backward compatibility. ([#35](https://github.com/kerrizor/keela/pull/35))
15
+
16
+ ### Changed
17
+
18
+ - **Performance: Share source files across strategies** - When scanning multiple strategies, source files are now loaded once and shared, reducing scan time by ~33% on large codebases. ([#37](https://github.com/kerrizor/keela/pull/37))
19
+
20
+ ### Fixed
21
+
22
+ - CLI options now correctly override config file settings ([#36](https://github.com/kerrizor/keela/pull/36))
23
+
10
24
  ## [0.2.3] - 2026-07-31
11
25
 
12
26
  ### Fixed
data/README.md CHANGED
@@ -271,21 +271,46 @@ This is useful for integrating with other tools, generating reports, or processi
271
271
 
272
272
  ## Exclusion File
273
273
 
274
- Some code appears unused but is actually called dynamically. Exclude it:
274
+ Some code appears unused but is actually called dynamically. Exclude it using the **strategy-aware format** (recommended):
275
275
 
276
276
  ```yaml
277
- # .keela_excluded.yml
277
+ # .keela/excluded.yml (strategy-aware format)
278
+ methods:
279
+ app/models/user.rb:
280
+ - legacy_method: "Called via metaprogramming"
281
+ - callback_method: "Used as ActiveRecord callback"
282
+ app/helpers/application_helper.rb:
283
+ - helper_method: "Called from views dynamically"
284
+ scopes:
285
+ app/models/user.rb:
286
+ - active: "Called dynamically via send()"
287
+ ```
288
+
289
+ This format mirrors the baseline file structure and allows you to exclude the same name in one strategy while flagging it in another. For example, if you have both `scope :active` and `def active` in the same file, you can exclude them independently.
290
+
291
+ ### Legacy Flat Format
292
+
293
+ The flat format is still supported for backward compatibility:
294
+
295
+ ```yaml
296
+ # .keela_excluded.yml (legacy flat format)
278
297
  app/models/user.rb:
279
298
  - legacy_method: "Called via metaprogramming"
280
299
  - callback_method: "Used as ActiveRecord callback"
281
- app/helpers/application_helper.rb:
282
- - helper_method: "Called from views dynamically"
283
300
  ```
284
301
 
285
- Then run with:
302
+ **Note:** With the flat format, an exclusion applies to ALL strategies. If you exclude `active`, both the method and scope named `active` will be excluded.
303
+
304
+ ### Usage
286
305
 
287
306
  ```bash
288
- keela --excluded .keela_excluded.yml
307
+ keela --excluded .keela/excluded.yml
308
+ ```
309
+
310
+ Or configure in `keela.yml`:
311
+
312
+ ```yaml
313
+ excluded_path: ".keela/excluded.yml"
289
314
  ```
290
315
 
291
316
  ## Ruby API
data/exe/keela CHANGED
@@ -13,6 +13,9 @@ options = {
13
13
  quiet: false
14
14
  }
15
15
 
16
+ # Store CLI overrides separately so they can be applied after config file loads
17
+ cli_overrides = {}
18
+
16
19
  OptionParser.new do |opts|
17
20
  opts.banner = "Usage: keela [options]"
18
21
 
@@ -45,23 +48,25 @@ OptionParser.new do |opts|
45
48
  end
46
49
 
47
50
  opts.on("--excluded PATH", "Path to YAML file of excluded items") do |path|
48
- Keela.configuration.excluded_path = path
51
+ cli_overrides[:excluded_path] = path
49
52
  end
50
53
 
51
54
  opts.on("--baseline PATH", "Path to YAML baseline file (default: .keela_baseline.yml)") do |path|
52
- Keela.configuration.baseline_path = path
55
+ cli_overrides[:baseline_path] = path
53
56
  end
54
57
 
55
58
  opts.on("--extensions EXTS", "Comma-separated file extensions to scan (default: rb,haml,erb)") do |exts|
56
- Keela.configuration.extensions = exts.split(",").map(&:strip)
59
+ cli_overrides[:extensions] = exts.split(",").map(&:strip)
57
60
  end
58
61
 
59
62
  opts.on("--exclude PATTERN", "Glob pattern for files to exclude (can be used multiple times)") do |pattern|
60
- Keela.configuration.exclude_patterns << pattern
63
+ cli_overrides[:exclude_patterns] ||= []
64
+ cli_overrides[:exclude_patterns] << pattern
61
65
  end
62
66
 
63
67
  opts.on("--include PATTERN", "Additional glob pattern to scan (can be used multiple times)") do |pattern|
64
- Keela.configuration.include_patterns << pattern
68
+ cli_overrides[:include_patterns] ||= []
69
+ cli_overrides[:include_patterns] << pattern
65
70
  end
66
71
 
67
72
  opts.on("--config PATH", "-c", "Path to config file (default: keela.yml or .keela.yml)") do |path|
@@ -84,9 +89,13 @@ OptionParser.new do |opts|
84
89
  end.parse!
85
90
 
86
91
  # Load config file (keela.yml or .keela.yml) if present
87
- # CLI options override config file settings
88
92
  Keela::ConfigFile.load(path: options[:config_path])
89
93
 
94
+ # Apply CLI overrides (these take precedence over config file)
95
+ cli_overrides.each do |key, value|
96
+ Keela.configuration.public_send("#{key}=", value)
97
+ end
98
+
90
99
  # Apply quiet mode if requested
91
100
  Keela.configuration.show_progress = false if options[:quiet]
92
101
 
@@ -129,6 +138,9 @@ strategies = build_strategies(options[:types])
129
138
  # Share a single baseline across all strategies
130
139
  baseline = Keela::Baseline.new(Keela.configuration.baseline_path)
131
140
 
141
+ # Load source files once and share across all strategies
142
+ source_files = Keela::Scanner.load_source_files
143
+
132
144
  success = true
133
145
  results = {}
134
146
 
@@ -139,7 +151,7 @@ strategies.each_with_index do |strategy, index|
139
151
  puts Rainbow("=== Sniffing for unused #{strategy.name} ===").cyan.bright if strategies.size > 1
140
152
  end
141
153
 
142
- scanner = Keela::Scanner.new(strategy: strategy, baseline: baseline)
154
+ scanner = Keela::Scanner.new(strategy: strategy, baseline: baseline, source_files: source_files)
143
155
  success &&= scanner.run(
144
156
  force_report: options[:force_report],
145
157
  update_baseline: options[:update_baseline],
data/lib/keela/scanner.rb CHANGED
@@ -10,11 +10,43 @@ module Keela
10
10
  DEFAULT_EXCLUDED_PATHS = [".keela/excluded.yml", "keela_excluded.yml"].freeze
11
11
  DEFAULT_BASELINE_PATHS = [".keela/baseline.yml", "keela_baseline.yml"].freeze
12
12
 
13
- def initialize(strategy:, configuration: Keela.configuration, baseline: nil)
13
+ # Load source files once for sharing across multiple Scanner instances.
14
+ # Returns a hash of { filename => [lines] }.
15
+ def self.load_source_files(configuration: Keela.configuration)
16
+ source_files = {}
17
+ file_globs = build_file_globs(configuration)
18
+
19
+ Dir.glob(file_globs).each do |filename|
20
+ next if excluded_file?(filename, configuration)
21
+
22
+ source_files[filename] = File.readlines(filename)
23
+ end
24
+
25
+ source_files
26
+ end
27
+
28
+ def self.build_file_globs(configuration)
29
+ all_patterns = configuration.directory_patterns + configuration.include_patterns
30
+
31
+ configuration.extensions.flat_map do |ext|
32
+ all_patterns.map { |pattern| format(pattern, ext: ext) }
33
+ end
34
+ end
35
+
36
+ def self.excluded_file?(filename, configuration)
37
+ return false if configuration.exclude_patterns.empty?
38
+
39
+ configuration.exclude_patterns.any? do |pattern|
40
+ File.fnmatch?(pattern, filename, File::FNM_PATHNAME | File::FNM_EXTGLOB)
41
+ end
42
+ end
43
+
44
+ def initialize(strategy:, configuration: Keela.configuration, baseline: nil, source_files: nil)
14
45
  @strategy = strategy
15
46
  @configuration = configuration
16
47
  @baseline = baseline || Baseline.new(resolve_baseline_path)
17
- @source_files = {}
48
+ @source_files = source_files || {}
49
+ @source_files_preloaded = !source_files.nil?
18
50
  @unused_collection = Hash.new { |hash, key| hash[key] = [] }
19
51
  @new_unused = []
20
52
  @removed = []
@@ -97,6 +129,8 @@ module Keela
97
129
  end
98
130
 
99
131
  def load_source_files
132
+ return if @source_files_preloaded
133
+
100
134
  Dir.glob(file_globs).each do |filename|
101
135
  next if excluded_file?(filename)
102
136
 
@@ -134,7 +168,16 @@ module Keela
134
168
  path = resolve_excluded_path
135
169
  return definitions unless path
136
170
 
137
- excluded = YAML.load_file(path, symbolize_names: true) || {}
171
+ all_excluded = YAML.load_file(path, symbolize_names: true) || {}
172
+
173
+ # Support both formats:
174
+ # 1. Strategy-aware (new): { methods: { "file.rb": [{ name: "reason" }] } }
175
+ # 2. Flat (legacy): { "file.rb": [{ name: "reason" }] }
176
+ excluded = if all_excluded.key?(strategy.name.to_sym)
177
+ all_excluded[strategy.name.to_sym] || {}
178
+ else
179
+ all_excluded # Legacy flat format
180
+ end
138
181
 
139
182
  definitions.reject do |h|
140
183
  excluded_for_file = excluded[h[:file].to_sym]
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.2.3"
4
+ VERSION = "0.3.0"
5
5
  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.2.3
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kerri Miller