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 +7 -0
- data/LICENSE +21 -0
- data/README.md +103 -0
- data/bin/laerad +6 -0
- data/lib/laerad/cli.rb +32 -0
- data/lib/laerad/file_analyzer.rb +362 -0
- data/lib/laerad/result.rb +57 -0
- data/lib/laerad/runner.rb +30 -0
- data/lib/laerad/scope.rb +33 -0
- data/lib/laerad/version.rb +5 -0
- data/lib/laerad.rb +11 -0
- metadata +90 -0
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
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
|
data/lib/laerad/scope.rb
ADDED
|
@@ -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
|
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: []
|