laerad 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9868751b6212ee096d1ca903704d4c4deb477ebd962fbf9afd4ad555851b8e56
4
+ data.tar.gz: e4d1b263fd9e4b5f441b2663dfb9880906cc18b7373c72d038187f60948a512a
5
+ SHA512:
6
+ metadata.gz: 8d99e9f2eb8ef8e8d08323072137f6f1b0e5e06381fe7f249279a0cb6228e52a18ebad07318643abfee0af6648c90d9d59fae5fbed061715d9896c300cd033b9
7
+ data.tar.gz: 976b8023bed99446cf2117bbd85f92e6ab05cf701d718c7d6ba2c5b52d6ceecc6aff0645eb7318e43ebe5dccc1535415eab7b6a885df4be5f391f1f5272a3fef
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Giles
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,103 @@
1
+ # Laerad: Eliminate Single-Use Variables
2
+
3
+ A static analyzer that detects single-use variables in Ruby code.
4
+
5
+ ## Usage
6
+
7
+ Scan a file or directory for single-use variables:
8
+
9
+ ```bash
10
+ bundle exec bin/laerad scan path/to/file.rb
11
+ bundle exec bin/laerad scan path/to/directory
12
+ ```
13
+
14
+ ## How It Works
15
+
16
+ Laerad uses [SyntaxTree](https://github.com/ruby-syntax-tree/syntax_tree) to
17
+ parse Ruby source files into an abstract syntax tree (AST). It then walks this
18
+ tree, tracking every variable definition along with its references.
19
+
20
+ ### Detection
21
+
22
+ Laerad flags variables that are used only once or not at all.
23
+
24
+ ### Scoping
25
+
26
+ Variables are tracked per lexical scope. Each method body, block, or lambda
27
+ creates a new scope. A variable defined inside a block is separate from a
28
+ variable with the same name outside that block.
29
+
30
+ ## Architecture
31
+
32
+ ```
33
+ CLI (Thor)
34
+ └─> Runner
35
+ └─> FileAnalyzer (per file)
36
+ ├─> SyntaxTree.parse
37
+ ├─> AST visitor
38
+ ├─> Scope stack (tracks variables)
39
+ └─> Result (violations)
40
+ ```
41
+
42
+ - **CLI** (`lib/laerad/cli.rb`) - Thor-based command interface with `scan` and
43
+ `version` commands
44
+ - **Runner** (`lib/laerad/runner.rb`) - Expands directories into file lists
45
+ and orchestrates analysis
46
+ - **FileAnalyzer** (`lib/laerad/file_analyzer.rb`) - Parses Ruby, walks the
47
+ AST, maintains a scope stack
48
+ - **Scope** (`lib/laerad/scope.rb`) - Tracks definitions and references with
49
+ usage counts
50
+ - **Result** (`lib/laerad/result.rb`) - Collects violations and formats output
51
+
52
+ ### Options
53
+
54
+ Short output (file:line only):
55
+
56
+ ```bash
57
+ bundle exec bin/laerad scan --short path/to/file.rb
58
+ bundle exec bin/laerad scan -s path/to/file.rb
59
+ ```
60
+
61
+ Print version:
62
+
63
+ ```bash
64
+ bundle exec bin/laerad version
65
+ ```
66
+
67
+ ## Development
68
+
69
+ Install dependencies:
70
+
71
+ ```bash
72
+ bundle install
73
+ ```
74
+
75
+ ## Example Output
76
+
77
+ ```
78
+ ❯ bundle exec bin/laerad scan test/fixtures/unused_variable.rb
79
+ +--------------------------------------+------+----------+------+
80
+ | File | Line | Variable | Uses |
81
+ +--------------------------------------+------+----------+------+
82
+ | test/fixtures/unused_variable.rb | 2 | x | 1 |
83
+ +--------------------------------------+------+----------+------+
84
+
85
+ ❯ bundle exec bin/laerad scan -s test/fixtures/unused_variable.rb
86
+ test/fixtures/unused_variable.rb:2
87
+ ```
88
+
89
+ ## Tests
90
+
91
+ ```bash
92
+ bundle exec rake test
93
+ ```
94
+
95
+ ### What's in a name?
96
+
97
+ This gem combines Thor with SyntaxTree. Combining Thor with trees made me think
98
+ of Yggdrasil, the world tree of Norse mythology, but there's already a gem by
99
+ that name. Laerad is an Anglicization of another Norse mythology tree name. It's
100
+ [unclear](https://en.wikipedia.org/wiki/L%C3%A6ra%C3%B0r#Theories) how distinct
101
+ this tree is from Yggdrasil — could be another name for the same tree, could
102
+ be a separate but related tree — but that's usually how things are with
103
+ mythologies.
data/bin/laerad ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/laerad"
5
+
6
+ Laerad::CLI.start(ARGV)
data/lib/laerad/cli.rb ADDED
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ module Laerad
6
+ class CLI < Thor
7
+ def self.exit_on_failure?
8
+ true
9
+ end
10
+
11
+ desc "scan PATH", "Scan Ruby files for single-use variables"
12
+ method_option :short, type: :boolean, aliases: "-s", desc: "Output only file:line"
13
+ def scan(path = ".")
14
+ result = Runner.new(path, options).run
15
+
16
+ if result.violations?
17
+ puts result.format_output(short: options[:short])
18
+ exit 1
19
+ else
20
+ puts "No violations found."
21
+ exit 0
22
+ end
23
+ end
24
+
25
+ desc "version", "Print version"
26
+ def version
27
+ puts "laerad #{VERSION}"
28
+ end
29
+
30
+ default_task :scan
31
+ end
32
+ end
@@ -0,0 +1,362 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "syntax_tree"
4
+
5
+ module Laerad
6
+ class FileAnalyzer
7
+ def self.analyze(path, options = {})
8
+ new(path, options).analyze
9
+ end
10
+
11
+ def initialize(path, options = {})
12
+ @path = path
13
+ @source = File.read(path)
14
+ @scope_stack = [Scope.new]
15
+ @result = Result.new(file: path)
16
+ @options = options
17
+ end
18
+
19
+ def analyze
20
+ ast = SyntaxTree.parse(@source)
21
+ visit(ast)
22
+ finalize_scope(@scope_stack.last)
23
+ @result
24
+ rescue SyntaxTree::Parser::ParseError
25
+ @result
26
+ end
27
+
28
+ private
29
+
30
+ def current_scope
31
+ @scope_stack.last
32
+ end
33
+
34
+ def push_scope
35
+ @scope_stack.push(Scope.new)
36
+ end
37
+
38
+ def pop_scope
39
+ scope = @scope_stack.pop
40
+ finalize_scope(scope)
41
+ scope
42
+ end
43
+
44
+ def finalize_scope(scope)
45
+ scope.single_use_variables.each do |name|
46
+ line = scope.variable_definition_line(name)
47
+ @result.add_variable_violation(
48
+ name: name,
49
+ line: line,
50
+ count: scope.variable_count(name)
51
+ )
52
+ end
53
+ end
54
+
55
+ def visit(node)
56
+ return unless node
57
+
58
+ case node
59
+ when SyntaxTree::Program
60
+ visit(node.statements)
61
+
62
+ when SyntaxTree::Statements
63
+ node.body.each { |stmt| visit(stmt) }
64
+
65
+ when SyntaxTree::VarField
66
+ name = extract_var_name(node)
67
+ if name
68
+ line = node.location.start_line
69
+ current_scope.register_variable_def(name, line)
70
+ end
71
+
72
+ when SyntaxTree::VarRef
73
+ name = extract_var_name(node)
74
+ current_scope.register_variable_ref(name) if name
75
+
76
+ when SyntaxTree::Assign
77
+ visit(node.target)
78
+ visit(node.value)
79
+
80
+ when SyntaxTree::OpAssign
81
+ visit(node.target)
82
+ visit(node.value)
83
+
84
+ when SyntaxTree::DefNode
85
+ push_scope
86
+ visit_params(node.params)
87
+ visit(node.bodystmt)
88
+ pop_scope
89
+
90
+ when SyntaxTree::BodyStmt
91
+ visit(node.statements)
92
+ node.rescue_clause&.then { |r| visit(r) }
93
+ node.else_clause&.then { |e| visit(e) }
94
+ node.ensure_clause&.then { |en| visit(en) }
95
+
96
+ when SyntaxTree::Rescue
97
+ if node.exception
98
+ visit_rescue_exception(node.exception)
99
+ end
100
+ visit(node.statements)
101
+ visit(node.consequent) if node.consequent
102
+
103
+ when SyntaxTree::RescueEx
104
+ if node.variable
105
+ visit(node.variable)
106
+ end
107
+
108
+ when SyntaxTree::MethodAddBlock
109
+ visit(node.call)
110
+ visit(node.block)
111
+
112
+ when SyntaxTree::CallNode
113
+ visit(node.receiver) if node.receiver
114
+ visit(node.arguments) if node.arguments
115
+
116
+ when SyntaxTree::Command
117
+ visit(node.arguments)
118
+
119
+ when SyntaxTree::CommandCall
120
+ visit(node.receiver) if node.receiver
121
+ visit(node.arguments) if node.arguments
122
+
123
+ when SyntaxTree::BlockNode
124
+ push_scope
125
+ visit_block_params(node.block_var) if node.block_var
126
+ visit(node.bodystmt)
127
+ pop_scope
128
+
129
+ when SyntaxTree::Lambda
130
+ push_scope
131
+ visit_lambda_params(node.params)
132
+ visit(node.statements)
133
+ pop_scope
134
+
135
+ when SyntaxTree::ClassDeclaration
136
+ visit(node.bodystmt)
137
+
138
+ when SyntaxTree::ModuleDeclaration
139
+ visit(node.bodystmt)
140
+
141
+ when SyntaxTree::Binary
142
+ visit(node.left)
143
+ visit(node.right)
144
+
145
+ when SyntaxTree::Unary
146
+ visit(node.statement)
147
+
148
+ when SyntaxTree::Paren
149
+ visit(node.contents)
150
+
151
+ when SyntaxTree::IfNode
152
+ visit(node.predicate)
153
+ visit(node.statements)
154
+ visit(node.consequent) if node.consequent
155
+
156
+ when SyntaxTree::UnlessNode
157
+ visit(node.predicate)
158
+ visit(node.statements)
159
+ visit(node.consequent) if node.consequent
160
+
161
+ when SyntaxTree::Elsif
162
+ visit(node.predicate)
163
+ visit(node.statements)
164
+ visit(node.consequent) if node.consequent
165
+
166
+ when SyntaxTree::Else
167
+ visit(node.statements)
168
+
169
+ when SyntaxTree::WhileNode
170
+ visit(node.predicate)
171
+ visit(node.statements)
172
+
173
+ when SyntaxTree::UntilNode
174
+ visit(node.predicate)
175
+ visit(node.statements)
176
+
177
+ when SyntaxTree::For
178
+ visit(node.index)
179
+ visit(node.collection)
180
+ visit(node.statements)
181
+
182
+ when SyntaxTree::Case
183
+ visit(node.value) if node.value
184
+ visit(node.consequent)
185
+
186
+ when SyntaxTree::When
187
+ node.arguments.parts.each { |arg| visit(arg) }
188
+ visit(node.statements)
189
+ visit(node.consequent) if node.consequent
190
+
191
+ when SyntaxTree::In
192
+ visit(node.pattern)
193
+ visit(node.statements)
194
+ visit(node.consequent) if node.consequent
195
+
196
+ when SyntaxTree::Begin
197
+ visit(node.bodystmt)
198
+
199
+ when SyntaxTree::Ensure
200
+ visit(node.statements)
201
+
202
+ when SyntaxTree::ReturnNode
203
+ visit(node.arguments) if node.arguments
204
+
205
+ when SyntaxTree::YieldNode
206
+ visit(node.arguments) if node.arguments
207
+
208
+ when SyntaxTree::Args
209
+ node.parts.each { |part| visit(part) }
210
+
211
+ when SyntaxTree::ArgParen
212
+ visit(node.arguments)
213
+
214
+ when SyntaxTree::ArrayLiteral
215
+ visit(node.contents) if node.contents
216
+
217
+ when SyntaxTree::HashLiteral
218
+ node.assocs.each { |assoc| visit(assoc) } if node.assocs.is_a?(Array)
219
+ visit(node.assocs) if node.assocs && !node.assocs.is_a?(Array)
220
+
221
+ when SyntaxTree::Assoc
222
+ visit(node.key)
223
+ visit(node.value)
224
+
225
+ when SyntaxTree::AssocSplat
226
+ visit(node.value)
227
+
228
+ when SyntaxTree::RangeNode
229
+ visit(node.left) if node.left
230
+ visit(node.right) if node.right
231
+
232
+ when SyntaxTree::Not
233
+ visit(node.statement)
234
+
235
+ when SyntaxTree::Defined
236
+ visit(node.value)
237
+
238
+ when SyntaxTree::ARef
239
+ visit(node.collection)
240
+ visit(node.index)
241
+
242
+ when SyntaxTree::ARefField
243
+ visit(node.collection)
244
+ visit(node.index)
245
+
246
+ when SyntaxTree::StringConcat
247
+ visit(node.left)
248
+ visit(node.right)
249
+
250
+ when SyntaxTree::StringEmbExpr
251
+ visit(node.statements)
252
+
253
+ when SyntaxTree::StringLiteral
254
+ node.parts.each { |part| visit(part) }
255
+
256
+ when SyntaxTree::DynaSymbol
257
+ node.parts.each { |part| visit(part) }
258
+
259
+ when SyntaxTree::XStringLiteral
260
+ node.parts.each { |part| visit(part) }
261
+
262
+ when SyntaxTree::RegexpLiteral
263
+ node.parts.each { |part| visit(part) }
264
+
265
+ when SyntaxTree::Heredoc
266
+ node.parts.each { |part| visit(part) }
267
+
268
+ when SyntaxTree::MAssign
269
+ visit(node.target)
270
+ visit(node.value)
271
+
272
+ when SyntaxTree::MLHS
273
+ node.parts.each { |part| visit(part) }
274
+
275
+ when SyntaxTree::MLHSParen
276
+ visit(node.contents)
277
+
278
+ when SyntaxTree::Next, SyntaxTree::Break, SyntaxTree::Redo, SyntaxTree::Retry
279
+ # control flow, no-op
280
+
281
+ when SyntaxTree::VoidStmt
282
+ # empty statement, no-op
283
+ end
284
+ end
285
+
286
+ def visit_params(params)
287
+ return unless params
288
+
289
+ case params
290
+ when SyntaxTree::Params
291
+ params.requireds.each { |p| register_param(p) }
292
+ params.optionals.each do |opt|
293
+ register_param(opt[0])
294
+ visit(opt[1])
295
+ end
296
+ register_param(params.rest) if params.rest && params.rest != :nil
297
+ params.posts.each { |p| register_param(p) }
298
+ params.keywords.each do |kw|
299
+ register_param(kw[0])
300
+ visit(kw[1]) if kw[1]
301
+ end
302
+ register_param(params.keyword_rest) if params.keyword_rest
303
+ register_param(params.block) if params.block
304
+ when SyntaxTree::Paren
305
+ visit_params(params.contents)
306
+ end
307
+ end
308
+
309
+ def visit_block_params(block_var)
310
+ return unless block_var
311
+
312
+ visit_params(block_var.params)
313
+ block_var.locals.each { |local| register_param(local) }
314
+ end
315
+
316
+ def visit_lambda_params(params)
317
+ case params
318
+ when SyntaxTree::LambdaVar
319
+ visit_params(params.params)
320
+ params.locals.each { |local| register_param(local) }
321
+ when SyntaxTree::Paren
322
+ visit_lambda_params(params.contents)
323
+ when SyntaxTree::Params
324
+ visit_params(params)
325
+ end
326
+ end
327
+
328
+ def register_param(param)
329
+ return unless param
330
+
331
+ name = case param
332
+ when SyntaxTree::Ident
333
+ param.value
334
+ when SyntaxTree::RestParam
335
+ param.name&.value
336
+ when SyntaxTree::KeywordRestParam
337
+ param.name&.value
338
+ when SyntaxTree::BlockArg
339
+ param.name&.value
340
+ when SyntaxTree::ArgsForward
341
+ nil
342
+ end
343
+
344
+ if name
345
+ current_scope.register_variable_def(name, param.location.start_line)
346
+ end
347
+ end
348
+
349
+ def visit_rescue_exception(exception)
350
+ if exception.variable
351
+ visit(exception.variable)
352
+ end
353
+ end
354
+
355
+ def extract_var_name(node)
356
+ case node.value
357
+ when SyntaxTree::Ident
358
+ node.value.value
359
+ end
360
+ end
361
+ end
362
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "terminal-table"
4
+
5
+ module Laerad
6
+ class Result
7
+ attr_reader :file, :variable_violations
8
+
9
+ def initialize(file: nil, variable_violations: [])
10
+ @file = file
11
+ @variable_violations = variable_violations
12
+ end
13
+
14
+ def add_variable_violation(name:, line:, count:)
15
+ @variable_violations << {name: name, line: line, count: count}
16
+ end
17
+
18
+ def violations?
19
+ @variable_violations.any?
20
+ end
21
+
22
+ def self.merge(*results)
23
+ merged_variable_violations = []
24
+
25
+ results.each do |result|
26
+ result.variable_violations.each do |v|
27
+ merged_variable_violations << v.merge(file: result.file)
28
+ end
29
+ end
30
+
31
+ new(variable_violations: merged_variable_violations)
32
+ end
33
+
34
+ def format_output(short: false)
35
+ return "" if @variable_violations.empty?
36
+
37
+ if short
38
+ @variable_violations.map do |v|
39
+ file_path = v[:file] || @file
40
+ "#{file_path}:#{v[:line]}"
41
+ end.join("\n")
42
+ else
43
+ rows = @variable_violations.map do |v|
44
+ file_path = v[:file] || @file
45
+ [file_path, v[:line], v[:name], v[:count]]
46
+ end
47
+
48
+ table = Terminal::Table.new(
49
+ headings: ["File", "Line", "Variable", "Uses"],
50
+ rows: rows
51
+ )
52
+
53
+ table.to_s
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Laerad
4
+ class Runner
5
+ def initialize(paths, options = {})
6
+ @paths = Array(paths)
7
+ @options = options
8
+ end
9
+
10
+ def run
11
+ files = expand_paths
12
+ results = files.map { |file| FileAnalyzer.analyze(file, @options) }
13
+ Result.merge(*results)
14
+ end
15
+
16
+ private
17
+
18
+ def expand_paths
19
+ @paths.flat_map do |path|
20
+ if File.directory?(path)
21
+ Dir.glob(File.join(path, "**", "*.rb"))
22
+ elsif File.file?(path) && path.end_with?(".rb")
23
+ [path]
24
+ else
25
+ []
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Laerad
4
+ class Scope
5
+ attr_reader :variables, :variable_def_lines
6
+
7
+ def initialize
8
+ @variables = Hash.new(0)
9
+ @variable_def_lines = Hash.new { |h, k| h[k] = [] }
10
+ end
11
+
12
+ def register_variable_def(name, line)
13
+ @variables[name] += 1
14
+ @variable_def_lines[name] << line
15
+ end
16
+
17
+ def register_variable_ref(name)
18
+ @variables[name] += 1
19
+ end
20
+
21
+ def single_use_variables
22
+ @variables.select { |_, count| count <= 2 }.keys
23
+ end
24
+
25
+ def variable_definition_line(name)
26
+ @variable_def_lines[name].first
27
+ end
28
+
29
+ def variable_count(name)
30
+ @variables[name]
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Laerad
4
+ VERSION = "0.1.0"
5
+ end
data/lib/laerad.rb ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "laerad/version"
4
+ require_relative "laerad/scope"
5
+ require_relative "laerad/result"
6
+ require_relative "laerad/file_analyzer"
7
+ require_relative "laerad/runner"
8
+ require_relative "laerad/cli"
9
+
10
+ module Laerad
11
+ end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: laerad
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Giles Bowkett
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: syntax_tree
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '6.3'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '6.3'
26
+ - !ruby/object:Gem::Dependency
27
+ name: terminal-table
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '4.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '4.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: thor
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.4'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.4'
54
+ executables:
55
+ - laerad
56
+ extensions: []
57
+ extra_rdoc_files: []
58
+ files:
59
+ - LICENSE
60
+ - README.md
61
+ - bin/laerad
62
+ - lib/laerad.rb
63
+ - lib/laerad/cli.rb
64
+ - lib/laerad/file_analyzer.rb
65
+ - lib/laerad/result.rb
66
+ - lib/laerad/runner.rb
67
+ - lib/laerad/scope.rb
68
+ - lib/laerad/version.rb
69
+ homepage: https://github.com/gilesbowkett/laerad
70
+ licenses:
71
+ - MIT
72
+ metadata: {}
73
+ rdoc_options: []
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: 3.4.7
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ requirements: []
87
+ rubygems_version: 3.6.9
88
+ specification_version: 4
89
+ summary: Static analyzer to detect single-use variables in Ruby code
90
+ test_files: []