keela 0.0.2 → 0.1.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: b2532684d9e7c3cca0411cbe55c3d944265236d159c287f8b719a778695872ed
4
+ data.tar.gz: '08a678fa6105ae093817dba3ce3632f25d173cae617db34aa267bd524b77e2ee'
5
5
  SHA512:
6
- metadata.gz: 71d6ecd16abc63e8b74db4a09f6d5979e168219b16fa27014c848209e230d9674f9393b4087fa97af1c9bd0af84e7f00eaebabd2375fbc788f1d090377b39685
7
- data.tar.gz: 5b590194ecec3ef30621d67c85bcd07a9cd4c7a43374184bd45ac53badab44cd86d83887294624e1e82122a5f85f5d57874b8e46252a92c1d3b3401a3de23be8
6
+ metadata.gz: 8b0574c28fcec2f29a78463c08d20ccf58401d81b67c60878571fef4c381dc88bd4beca3b802c72edb5b456ec9c8bbf7b1f46b3bd2c6ed68380994ac04267330
7
+ data.tar.gz: 90e50d394dff39875fad354923381b9490994a9d2a4d3673f4c1a0d6df15be85c21bb54a24fee6a4860553bcea580ac589d4ed57f0e5fa44c8c2e80ba6742765
data/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
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.1.0] - 2026-07-16
11
+
12
+ ### Added
13
+
14
+ - Initial release
15
+ - `Scanner` class for detecting unused code
16
+ - Built-in strategies for methods and scopes detection
17
+ - CLI tool with `--report` and `--update-baseline` modes
18
+ - Configurable file extensions and directory patterns
19
+ - Baseline comparison for CI integration
20
+ - 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,207 @@
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 and scopes)
73
+ keela
74
+
75
+ # Scan for specific types
76
+ keela --type methods
77
+ keela --type scopes
78
+
79
+ # Force report mode (ignore baseline)
80
+ keela --report
81
+
82
+ # Update the baseline file
83
+ keela --update-baseline
84
+
85
+ # Use a custom baseline path
86
+ keela --baseline config/unused_baseline.yml
87
+
88
+ # Specify excluded items file
89
+ keela --excluded config/keela_excluded.yml
90
+
91
+ # Custom file extensions
92
+ keela --extensions rb,rake,haml
93
+
94
+ # Show version
95
+ keela --version
96
+ ```
97
+
98
+ ## CI Integration
99
+
100
+ Keela is designed for CI pipelines. Add it to catch dead code before it merges:
101
+
102
+ ```yaml
103
+ # .gitlab-ci.yml
104
+ unused_code:
105
+ script:
106
+ - bundle exec keela
107
+ rules:
108
+ - if: $CI_MERGE_REQUEST_IID
109
+ ```
110
+
111
+ ```yaml
112
+ # .github/workflows/ci.yml
113
+ - name: Check for unused code
114
+ run: bundle exec keela
115
+ ```
116
+
117
+ The workflow:
118
+
119
+ 1. **Initial setup**: Run `keela --update-baseline` and commit `.keela_baseline.yml`
120
+ 2. **CI runs**: `keela` compares against baseline, fails on new dead code
121
+ 3. **After cleanup**: Run `keela --update-baseline` to update the baseline
122
+
123
+ ## Exclusion File
124
+
125
+ Some code appears unused but is actually called dynamically. Exclude it:
126
+
127
+ ```yaml
128
+ # .keela_excluded.yml
129
+ app/models/user.rb:
130
+ - legacy_method: "Called via metaprogramming"
131
+ - callback_method: "Used as ActiveRecord callback"
132
+ app/helpers/application_helper.rb:
133
+ - helper_method: "Called from views dynamically"
134
+ ```
135
+
136
+ Then run with:
137
+
138
+ ```bash
139
+ keela --excluded .keela_excluded.yml
140
+ ```
141
+
142
+ ## Ruby API
143
+
144
+ ```ruby
145
+ require 'keela'
146
+
147
+ # Configure Keela
148
+ Keela.configure do |config|
149
+ config.extensions = %w[rb haml erb]
150
+ config.directory_patterns = %w[
151
+ app/**/*.%<ext>s
152
+ lib/**/*.%<ext>s
153
+ ]
154
+ config.excluded_path = '.keela_excluded.yml'
155
+ config.baseline_path = '.keela_baseline.yml'
156
+ end
157
+
158
+ # Run a scan
159
+ strategy = Keela::Strategies::Methods.new
160
+ scanner = Keela::Scanner.new(strategy: strategy)
161
+ success = scanner.run
162
+
163
+ # Access results
164
+ scanner.unused_collection # Hash of file => [unused_names]
165
+ scanner.new_unused # Items not in baseline
166
+ scanner.removed # Items in baseline but no longer unused
167
+ ```
168
+
169
+ ## Custom Strategies
170
+
171
+ Detect other patterns by creating your own strategy:
172
+
173
+ ```ruby
174
+ class CallbackStrategy < Keela::Strategy
175
+ def name
176
+ "callbacks"
177
+ end
178
+
179
+ def definition_file_pattern
180
+ %r{app/models}
181
+ end
182
+
183
+ def extract_definition(line)
184
+ # Match: before_save :do_something
185
+ line =~ /(?:before|after|around)_\w+\s+:(\w+)/ ? Regexp.last_match(1) : nil
186
+ end
187
+
188
+ def usage_regex(name)
189
+ /def #{Regexp.quote(name)}\b/
190
+ end
191
+
192
+ def skip_comments?
193
+ true
194
+ end
195
+ end
196
+
197
+ scanner = Keela::Scanner.new(strategy: CallbackStrategy.new)
198
+ scanner.run(force_report: true)
199
+ ```
200
+
201
+ ## About the Name
202
+
203
+ [Keela](https://en.wikipedia.org/wiki/Keela_(dog)) was a famous English Springer Spaniel known as the "CSI dog." She could detect microscopic traces of blood that other methods missed, and worked on many high-profile forensic cases including the Madeleine McCann investigation. Like her namesake, this gem finds the unused code that other tools miss.
204
+
205
+ ## License
206
+
207
+ MIT License. See [LICENSE.txt](LICENSE.txt).
data/exe/keela ADDED
@@ -0,0 +1,86 @@
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
+ }
12
+
13
+ OptionParser.new do |opts|
14
+ opts.banner = "Usage: keela [options]"
15
+
16
+ opts.separator ""
17
+ opts.separator "Keela sniffs out unused code in your Ruby codebase."
18
+ opts.separator ""
19
+ opts.separator "By default, if a baseline file exists, Keela runs in diff mode:"
20
+ opts.separator " - Fails if NEW unused code is detected"
21
+ opts.separator " - Fails if previously unused code was REMOVED (update baseline)"
22
+ opts.separator " - Passes if unchanged from baseline"
23
+ opts.separator ""
24
+ opts.separator "If no baseline exists, shows a full report of all unused code."
25
+ opts.separator ""
26
+ opts.separator "Options:"
27
+
28
+ opts.on("--type TYPE", %i[methods scopes all], "Type to detect: methods, scopes, all (default: all)") do |type|
29
+ options[:type] = type
30
+ end
31
+
32
+ opts.on("--report", "-r", "Force report mode: show all unused code (ignore baseline)") do
33
+ options[:force_report] = true
34
+ end
35
+
36
+ opts.on("--update-baseline", "-u", "Update the baseline file with current unused code") do
37
+ options[:update_baseline] = true
38
+ end
39
+
40
+ opts.on("--excluded PATH", "Path to YAML file of excluded items") do |path|
41
+ Keela.configuration.excluded_path = path
42
+ end
43
+
44
+ opts.on("--baseline PATH", "Path to YAML baseline file (default: .keela_baseline.yml)") do |path|
45
+ Keela.configuration.baseline_path = path
46
+ end
47
+
48
+ opts.on("--extensions EXTS", "Comma-separated file extensions to scan (default: rb,haml,erb)") do |exts|
49
+ Keela.configuration.extensions = exts.split(",").map(&:strip)
50
+ end
51
+
52
+ opts.on("--version", "-v", "Show version") do
53
+ puts "keela #{Keela::VERSION}"
54
+ exit
55
+ end
56
+
57
+ opts.on("-h", "--help", "Show this help") do
58
+ puts opts
59
+ exit
60
+ end
61
+ end.parse!
62
+
63
+ # Set default baseline path if not specified
64
+ Keela.configuration.baseline_path ||= ".keela_baseline.yml"
65
+
66
+ strategies = case options[:type]
67
+ when :all
68
+ [Keela::Strategies::Methods.new, Keela::Strategies::Scopes.new]
69
+ when :methods
70
+ [Keela::Strategies::Methods.new]
71
+ when :scopes
72
+ [Keela::Strategies::Scopes.new]
73
+ end
74
+
75
+ success = true
76
+
77
+ strategies.each_with_index do |strategy, index|
78
+ puts Rainbow("=== Sniffing for unused #{strategy.name} ===").cyan.bright if strategies.size > 1
79
+
80
+ scanner = Keela::Scanner.new(strategy: strategy)
81
+ success &&= scanner.run(force_report: options[:force_report], update_baseline: options[:update_baseline])
82
+
83
+ puts if strategies.size > 1 && index < strategies.size - 1
84
+ end
85
+
86
+ exit(success ? 0 : 1)
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keela
4
+ class Configuration
5
+ # File extensions to scan for code usage
6
+ attr_accessor :extensions
7
+
8
+ # Directory patterns to scan (use %<ext>s as placeholder for extension)
9
+ attr_accessor :directory_patterns
10
+
11
+ # Path to YAML file listing excluded items (won't be flagged as unused)
12
+ attr_accessor :excluded_path
13
+
14
+ # Path to YAML file tracking known unused items (baseline)
15
+ attr_accessor :baseline_path
16
+
17
+ # Optional directory that must exist for scanning to proceed (e.g., "ee" for GitLab)
18
+ attr_accessor :required_directory
19
+
20
+ # Whether to show progress during scanning
21
+ attr_accessor :show_progress
22
+
23
+ def initialize
24
+ @extensions = %w[rb haml erb].freeze
25
+ @directory_patterns = %w[
26
+ app/**/*.%<ext>s
27
+ lib/**/*.%<ext>s
28
+ config/**/*.%<ext>s
29
+ ].freeze
30
+ @excluded_path = nil
31
+ @baseline_path = nil
32
+ @required_directory = nil
33
+ @show_progress = true
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rainbow"
4
+ require "yaml"
5
+
6
+ module Keela
7
+ class Reporter
8
+ attr_reader :strategy_name
9
+
10
+ def initialize(strategy_name)
11
+ @strategy_name = strategy_name
12
+ end
13
+
14
+ def print_full_report(unused_collection, elapsed_time)
15
+ unused_count = unused_collection.values.flatten.size
16
+
17
+ if unused_count > 0
18
+ puts "\nFound #{unused_count} unused #{strategy_name}:\n\n"
19
+ puts format_yaml(unused_collection)
20
+ puts "\n"
21
+ else
22
+ puts Rainbow("No unused #{strategy_name} were found.").green.bright
23
+ end
24
+
25
+ puts "Finished in #{elapsed_time.round(2)} seconds."
26
+ end
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?
30
+
31
+ if new_unused.size + removed.size > 0
32
+ puts Rainbow("~" * 80).white.bright
33
+ puts "\n"
34
+ end
35
+
36
+ print_removed(removed, baseline_path) unless removed.empty?
37
+ end
38
+
39
+ def format_yaml(collection)
40
+ indent_yaml_list_items(collection.sort.to_h.to_yaml)
41
+ end
42
+
43
+ private
44
+
45
+ def print_new_unused(new_unused, excluded_path)
46
+ error = <<~MESSAGE
47
+ We have detected #{new_unused.size} newly unused #{strategy_name}.
48
+
49
+ Please remove these #{strategy_name}, or if in use, add to #{excluded_path}.
50
+ MESSAGE
51
+
52
+ puts Rainbow(error).red.bright
53
+ puts Rainbow(format_yaml(parse_diff(new_unused))).red.bright
54
+ end
55
+
56
+ def print_removed(removed, baseline_path)
57
+ message = <<~MESSAGE
58
+ It appears you have removed unused #{strategy_name}. Thank you!
59
+
60
+ Please update #{File.basename(baseline_path)} and remove entries for these #{strategy_name}.
61
+ MESSAGE
62
+
63
+ puts Rainbow(message).yellow.bright
64
+ puts Rainbow(format_yaml(parse_diff(removed))).yellow.bright
65
+ end
66
+
67
+ def parse_diff(diff_to_parse)
68
+ result = Hash.new { |hash, key| hash[key] = [] }
69
+
70
+ diff_to_parse.each do |file_name, name|
71
+ result[file_name] << name
72
+ end
73
+
74
+ result
75
+ end
76
+
77
+ # Indents YAML list items that are not already indented.
78
+ # Ruby's to_yaml outputs list items without indentation (e.g., "- item"),
79
+ # but we want 2-space indentation (e.g., " - item").
80
+ def indent_yaml_list_items(yaml_string)
81
+ yaml_string.gsub(/\n-(\s+\S)/, "\n -\\1")
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "parallel"
4
+ require "yaml"
5
+
6
+ module Keela
7
+ class Scanner
8
+ attr_reader :strategy, :configuration, :source_files, :unused_collection, :new_unused, :removed
9
+
10
+ def initialize(strategy:, configuration: Keela.configuration)
11
+ @strategy = strategy
12
+ @configuration = configuration
13
+ @source_files = {}
14
+ @unused_collection = Hash.new { |hash, key| hash[key] = [] }
15
+ @new_unused = []
16
+ @removed = []
17
+ end
18
+
19
+ def run(force_report: false, update_baseline: false)
20
+ return true unless should_run?
21
+
22
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
23
+
24
+ load_source_files
25
+ definitions = find_definitions
26
+ definitions = filter_excluded(definitions)
27
+
28
+ # Determine mode: report if forced, updating baseline, or no baseline exists
29
+ report_mode = force_report || update_baseline || !baseline_exists?
30
+
31
+ find_unused(definitions, show_progress: report_mode)
32
+
33
+ if report_mode
34
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
35
+ reporter.print_full_report(unused_collection, elapsed)
36
+ write_baseline_file if update_baseline
37
+ return true
38
+ end
39
+
40
+ # Baseline mode: compare against known unused code
41
+ compare_with_baseline
42
+ reporter.print_diff_report(
43
+ new_unused,
44
+ removed,
45
+ excluded_path: configuration.excluded_path || "excluded.yml",
46
+ baseline_path: configuration.baseline_path
47
+ )
48
+
49
+ new_unused.empty? && removed.empty?
50
+ end
51
+
52
+ def file_globs
53
+ configuration.extensions.flat_map do |ext|
54
+ configuration.directory_patterns.map { |pattern| format(pattern, ext: ext) }
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def baseline_exists?
61
+ configuration.baseline_path && File.exist?(configuration.baseline_path)
62
+ end
63
+
64
+ def should_run?
65
+ return true unless configuration.required_directory
66
+
67
+ Dir.exist?(configuration.required_directory)
68
+ end
69
+
70
+ def reporter
71
+ @reporter ||= Reporter.new(strategy.name)
72
+ end
73
+
74
+ def load_source_files
75
+ Dir.glob(file_globs).each do |filename|
76
+ @source_files[filename] = File.readlines(filename)
77
+ end
78
+ end
79
+
80
+ def find_definitions
81
+ source_files.keys.grep(strategy.definition_file_pattern).flat_map do |filename|
82
+ source_files[filename].flat_map do |line|
83
+ next [] if strategy.skip_comments? && line.strip.start_with?("#")
84
+
85
+ name = strategy.extract_definition(line)
86
+ name ? [{ name: name, file: filename }] : []
87
+ end
88
+ end
89
+ end
90
+
91
+ def filter_excluded(definitions)
92
+ return definitions unless configuration.excluded_path
93
+ return definitions unless File.exist?(configuration.excluded_path)
94
+
95
+ excluded = YAML.load_file(configuration.excluded_path, symbolize_names: true) || {}
96
+
97
+ definitions.reject do |h|
98
+ excluded_for_file = excluded[h[:file].to_sym]
99
+ excluded_for_file&.flat_map(&:keys)&.include?(h[:name].to_sym)
100
+ end
101
+ end
102
+
103
+ def find_unused(definitions, show_progress: false)
104
+ source_code = source_files.values.flatten.join
105
+
106
+ progress_label = show_progress ? "Checking #{strategy.name}" : nil
107
+
108
+ unused = Parallel.flat_map(definitions, progress: progress_label) do |definition|
109
+ regex = strategy.usage_regex(definition[:name])
110
+ regex.match?(source_code) ? [] : definition
111
+ end
112
+
113
+ unused.each do |unused_def|
114
+ @unused_collection[unused_def[:file]] << unused_def[:name]
115
+ end
116
+ end
117
+
118
+ def compare_with_baseline
119
+ baseline = YAML.load_file(configuration.baseline_path) || {}
120
+ baseline_items = baseline.flat_map { |f, names| [f].product(names) }
121
+
122
+ current_items = unused_collection.flat_map { |f, names| [f].product(names) }
123
+
124
+ @new_unused = current_items - baseline_items
125
+ @removed = baseline_items - current_items
126
+ end
127
+
128
+ def write_baseline_file
129
+ return unless configuration.baseline_path
130
+
131
+ header = <<~HEADER
132
+ # The #{strategy.name} listed here have been identified as "unused" by Keela,
133
+ # and are potential targets for future removal.
134
+ #
135
+ # If a #{strategy.name.chomp('s')} listed here is actually in use,
136
+ # remove it from this file and add it to your excluded file.
137
+ #
138
+ HEADER
139
+
140
+ yaml_content = if unused_collection.empty?
141
+ "#{header}---\n{}\n"
142
+ else
143
+ sorted_collection = unused_collection.sort.to_h
144
+ "#{header}#{reporter.format_yaml(sorted_collection)}"
145
+ end
146
+
147
+ File.write(configuration.baseline_path, yaml_content)
148
+ puts Rainbow("Updated #{configuration.baseline_path}").green.bright
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keela
4
+ module Strategies
5
+ class Methods < Strategy
6
+ def name
7
+ "methods"
8
+ end
9
+
10
+ def definition_file_pattern
11
+ %r{app/helpers|app/models}
12
+ end
13
+
14
+ def extract_definition(line)
15
+ return nil unless line =~ /def ([^(;\s]+)/
16
+
17
+ Regexp.last_match(1).chomp
18
+ end
19
+
20
+ def usage_regex(name)
21
+ if name.end_with?("=")
22
+ # Setter method: match assignment usage
23
+ /(?<!def )#{Regexp.quote(name.sub(/^self\./, "").chomp("="))}\W=*/
24
+ else
25
+ # Regular method: match calls
26
+ /(?<!def )#{Regexp.quote(name.sub(/^self\./, ""))}\W/
27
+ end
28
+ end
29
+
30
+ def skip_comments?
31
+ false
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keela
4
+ module Strategies
5
+ class Scopes < Strategy
6
+ def name
7
+ "scopes"
8
+ end
9
+
10
+ def definition_file_pattern
11
+ %r{app/models}
12
+ end
13
+
14
+ def extract_definition(line)
15
+ return nil unless line =~ /\bscope\s+:(\w+)/
16
+
17
+ Regexp.last_match(1)
18
+ end
19
+
20
+ def usage_regex(name)
21
+ /(?<!scope :)(?<!def )#{Regexp.quote(name)}\W/
22
+ end
23
+
24
+ def skip_comments?
25
+ true
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keela
4
+ # Base class for detection strategies.
5
+ #
6
+ # Subclasses define how to find definitions and detect usage for
7
+ # different types of code (methods, scopes, etc.)
8
+ #
9
+ class Strategy
10
+ # Human-readable name for this strategy (e.g., "methods", "scopes")
11
+ def name
12
+ raise NotImplementedError, "#{self.class} must implement #name"
13
+ end
14
+
15
+ # Regex pattern to match files that may contain definitions
16
+ # (e.g., /app\/models/ for scopes)
17
+ def definition_file_pattern
18
+ raise NotImplementedError, "#{self.class} must implement #definition_file_pattern"
19
+ end
20
+
21
+ # Extract a definition name from a line of code, or nil if no definition found
22
+ def extract_definition(line)
23
+ raise NotImplementedError, "#{self.class} must implement #extract_definition"
24
+ end
25
+
26
+ # Build a regex to detect usage of the given definition name
27
+ def usage_regex(name)
28
+ raise NotImplementedError, "#{self.class} must implement #usage_regex"
29
+ end
30
+
31
+ # Whether to skip lines that start with # (comments)
32
+ def skip_comments?
33
+ false
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keela
4
+ VERSION = "0.1.0"
5
+ end
data/lib/keela.rb CHANGED
@@ -1,5 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "keela/version"
4
+ require_relative "keela/configuration"
5
+ require_relative "keela/strategy"
6
+ require_relative "keela/strategies/methods"
7
+ require_relative "keela/strategies/scopes"
8
+ require_relative "keela/reporter"
9
+ require_relative "keela/scanner"
10
+
3
11
  module Keela
4
- VERSION = '0.0.2'
12
+ class << self
13
+ attr_writer :configuration
14
+
15
+ def configuration
16
+ @configuration ||= Configuration.new
17
+ end
18
+
19
+ def configure
20
+ yield(configuration)
21
+ end
22
+
23
+ def reset_configuration!
24
+ @configuration = Configuration.new
25
+ end
26
+ end
5
27
  end
metadata CHANGED
@@ -1,26 +1,84 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: keela
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kerri Miller
8
- bindir: bin
8
+ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies: []
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: parallel
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.20'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.20'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rainbow
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: ruby-progressbar
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.11'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.11'
12
54
  description: Like the famous CSI dog who found what others missed, Keela detects unused
13
55
  methods and scopes in your Ruby codebase.
14
56
  email: kerrizor@kerrizor.com
15
- executables: []
57
+ executables:
58
+ - keela
16
59
  extensions: []
17
60
  extra_rdoc_files: []
18
61
  files:
62
+ - CHANGELOG.md
63
+ - LICENSE.txt
64
+ - README.md
65
+ - exe/keela
19
66
  - lib/keela.rb
67
+ - lib/keela/configuration.rb
68
+ - lib/keela/reporter.rb
69
+ - lib/keela/scanner.rb
70
+ - lib/keela/strategies/methods.rb
71
+ - lib/keela/strategies/scopes.rb
72
+ - lib/keela/strategy.rb
73
+ - lib/keela/version.rb
20
74
  homepage: https://github.com/kerrizor/keela
21
75
  licenses:
22
76
  - MIT
23
- metadata: {}
77
+ metadata:
78
+ homepage_uri: https://github.com/kerrizor/keela
79
+ source_code_uri: https://github.com/kerrizor/keela
80
+ changelog_uri: https://github.com/kerrizor/keela/blob/main/CHANGELOG.md
81
+ bug_tracker_uri: https://github.com/kerrizor/keela/issues
24
82
  rdoc_options: []
25
83
  require_paths:
26
84
  - lib
@@ -35,7 +93,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
35
93
  - !ruby/object:Gem::Version
36
94
  version: '0'
37
95
  requirements: []
38
- rubygems_version: 4.0.10
96
+ rubygems_version: 4.0.15
39
97
  specification_version: 4
40
98
  summary: Sniff out unused code in your Ruby codebase
41
99
  test_files: []