spoom 1.2.0 → 1.2.2
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 +4 -4
- data/Gemfile +0 -1
- data/lib/spoom/cli/bump.rb +4 -5
- data/lib/spoom/cli.rb +4 -8
- data/lib/spoom/context/bundle.rb +16 -7
- data/lib/spoom/context/file_system.rb +17 -0
- data/lib/spoom/context/git.rb +17 -1
- data/lib/spoom/context/sorbet.rb +42 -7
- data/lib/spoom/coverage/d3/circle_map.rb +22 -36
- data/lib/spoom/coverage/report.rb +45 -32
- data/lib/spoom/coverage.rb +12 -12
- data/lib/spoom/deadcode/definition.rb +98 -0
- data/lib/spoom/deadcode/erb.rb +103 -0
- data/lib/spoom/deadcode/index.rb +61 -0
- data/lib/spoom/deadcode/indexer.rb +394 -0
- data/lib/spoom/deadcode/location.rb +58 -0
- data/lib/spoom/deadcode/reference.rb +34 -0
- data/lib/spoom/deadcode/send.rb +18 -0
- data/lib/spoom/deadcode.rb +55 -0
- data/lib/spoom/file_collector.rb +102 -0
- data/lib/spoom/file_tree.rb +168 -83
- data/lib/spoom/printer.rb +0 -2
- data/lib/spoom/sorbet/config.rb +3 -1
- data/lib/spoom/sorbet/lsp/base.rb +0 -6
- data/lib/spoom/sorbet/lsp/structures.rb +1 -1
- data/lib/spoom/sorbet/sigils.rb +1 -16
- data/lib/spoom/version.rb +1 -1
- data/lib/spoom.rb +2 -0
- metadata +41 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5154e37a81129c70e93925cc2a545a0d7861fd645a3ec2422e6549efc0cb76b5
|
4
|
+
data.tar.gz: 30c9ecdc3599a37309180fea890a7888d53b430c8701e0970f503bf5731979f9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a3246a42fa965e3ef75d0178ffe958a8cc5723812ecb86799b5e2e406d5da7487ffd738696ccb3f6d76ae140de281dab73dba3ec3d8af9fdef696820654ed869
|
7
|
+
data.tar.gz: ba4c8ab2d87ff4451f420e2d062c7cebdd19202bc91226fd131ff9ab9f838e7494fa43560ffa317fc0565d023ecceef184c02f762e215ba6451d3ec164e3c75a
|
data/Gemfile
CHANGED
data/lib/spoom/cli/bump.rb
CHANGED
@@ -54,6 +54,7 @@ module Spoom
|
|
54
54
|
dry = options[:dry]
|
55
55
|
only = options[:only]
|
56
56
|
cmd = options[:suggest_bump_command]
|
57
|
+
directory = File.expand_path(directory)
|
57
58
|
exec_path = File.expand_path(self.exec_path)
|
58
59
|
|
59
60
|
unless Sorbet::Sigils.valid_strictness?(from)
|
@@ -73,11 +74,9 @@ module Spoom
|
|
73
74
|
|
74
75
|
say("Checking files...")
|
75
76
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
files_from_config = context.srb_files.map { |file| File.expand_path(file) }
|
80
|
-
files_to_bump.select! { |file| files_from_config.include?(file) }
|
77
|
+
files_to_bump = context.srb_files_with_strictness(from, include_rbis: false)
|
78
|
+
.map { |file| File.expand_path(file, context.absolute_path) }
|
79
|
+
.select { |file| file.start_with?(directory) }
|
81
80
|
|
82
81
|
if only
|
83
82
|
list = File.read(only).lines.map { |file| File.expand_path(file.strip) }
|
data/lib/spoom/cli.rb
CHANGED
@@ -39,23 +39,19 @@ module Spoom
|
|
39
39
|
|
40
40
|
desc "files", "List all the files typechecked by Sorbet"
|
41
41
|
option :tree, type: :boolean, default: true, desc: "Display list as an indented tree"
|
42
|
-
option :rbi, type: :boolean, default:
|
42
|
+
option :rbi, type: :boolean, default: false, desc: "Show RBI files"
|
43
43
|
def files
|
44
44
|
context = context_requiring_sorbet!
|
45
|
-
files = context.srb_files
|
46
|
-
|
47
|
-
unless options[:rbi]
|
48
|
-
files = files.reject { |file| file.end_with?(".rbi") }
|
49
|
-
end
|
50
45
|
|
46
|
+
files = context.srb_files(include_rbis: options[:rbi])
|
51
47
|
if files.empty?
|
52
48
|
say_error("No file matching `#{Sorbet::CONFIG_PATH}`")
|
53
49
|
exit(1)
|
54
50
|
end
|
55
51
|
|
56
52
|
if options[:tree]
|
57
|
-
tree = FileTree.new(files
|
58
|
-
tree.
|
53
|
+
tree = FileTree.new(files)
|
54
|
+
tree.print_with_strictnesses(context, colors: options[:color])
|
59
55
|
else
|
60
56
|
puts files
|
61
57
|
end
|
data/lib/spoom/context/bundle.rb
CHANGED
@@ -10,12 +10,18 @@ module Spoom
|
|
10
10
|
|
11
11
|
requires_ancestor { Context }
|
12
12
|
|
13
|
-
# Read the
|
13
|
+
# Read the contents of the Gemfile in this context directory
|
14
14
|
sig { returns(T.nilable(String)) }
|
15
15
|
def read_gemfile
|
16
16
|
read("Gemfile")
|
17
17
|
end
|
18
18
|
|
19
|
+
# Read the contents of the Gemfile.lock in this context directory
|
20
|
+
sig { returns(T.nilable(String)) }
|
21
|
+
def read_gemfile_lock
|
22
|
+
read("Gemfile.lock")
|
23
|
+
end
|
24
|
+
|
19
25
|
# Set the `contents` of the Gemfile in this context directory
|
20
26
|
sig { params(contents: String, append: T::Boolean).void }
|
21
27
|
def write_gemfile!(contents, append: false)
|
@@ -41,17 +47,20 @@ module Spoom
|
|
41
47
|
bundle("exec #{command}", version: version, capture_err: capture_err)
|
42
48
|
end
|
43
49
|
|
50
|
+
sig { returns(T::Hash[String, Bundler::LazySpecification]) }
|
51
|
+
def gemfile_lock_specs
|
52
|
+
return {} unless file?("Gemfile.lock")
|
53
|
+
|
54
|
+
parser = Bundler::LockfileParser.new(read_gemfile_lock)
|
55
|
+
parser.specs.map { |spec| [spec.name, spec] }.to_h
|
56
|
+
end
|
57
|
+
|
44
58
|
# Get `gem` version from the `Gemfile.lock` content
|
45
59
|
#
|
46
60
|
# Returns `nil` if `gem` cannot be found in the Gemfile.
|
47
61
|
sig { params(gem: String).returns(T.nilable(String)) }
|
48
62
|
def gem_version_from_gemfile_lock(gem)
|
49
|
-
|
50
|
-
|
51
|
-
content = read("Gemfile.lock").match(/^ #{gem} \(.*(\d+\.\d+\.\d+).*\)/)
|
52
|
-
return nil unless content
|
53
|
-
|
54
|
-
content[1]
|
63
|
+
gemfile_lock_specs[gem]&.version&.to_s
|
55
64
|
end
|
56
65
|
end
|
57
66
|
end
|
@@ -43,6 +43,23 @@ module Spoom
|
|
43
43
|
glob("*")
|
44
44
|
end
|
45
45
|
|
46
|
+
sig do
|
47
|
+
params(
|
48
|
+
allow_extensions: T::Array[String],
|
49
|
+
allow_mime_types: T::Array[String],
|
50
|
+
exclude_patterns: T::Array[String],
|
51
|
+
).returns(T::Array[String])
|
52
|
+
end
|
53
|
+
def collect_files(allow_extensions: [], allow_mime_types: [], exclude_patterns: [])
|
54
|
+
collector = FileCollector.new(
|
55
|
+
allow_extensions: allow_extensions,
|
56
|
+
allow_mime_types: allow_mime_types,
|
57
|
+
exclude_patterns: exclude_patterns,
|
58
|
+
)
|
59
|
+
collector.visit_path(absolute_path)
|
60
|
+
collector.files.map { |file| file.delete_prefix("#{absolute_path}/") }
|
61
|
+
end
|
62
|
+
|
46
63
|
# Does `relative_path` point to an existing file in this context directory?
|
47
64
|
sig { params(relative_path: String).returns(T::Boolean) }
|
48
65
|
def file?(relative_path)
|
data/lib/spoom/context/git.rb
CHANGED
@@ -63,8 +63,18 @@ module Spoom
|
|
63
63
|
git("checkout #{ref}")
|
64
64
|
end
|
65
65
|
|
66
|
+
# Run `git checkout -b <branch-name> <ref>` in this context directory
|
67
|
+
sig { params(branch_name: String, ref: T.nilable(String)).returns(ExecResult) }
|
68
|
+
def git_checkout_new_branch!(branch_name, ref: nil)
|
69
|
+
if ref
|
70
|
+
git("checkout -b #{branch_name} #{ref}")
|
71
|
+
else
|
72
|
+
git("checkout -b #{branch_name}")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
66
76
|
# Run `git add . && git commit` in this context directory
|
67
|
-
sig { params(message: String, time: Time, allow_empty: T::Boolean).
|
77
|
+
sig { params(message: String, time: Time, allow_empty: T::Boolean).returns(ExecResult) }
|
68
78
|
def git_commit!(message: "message", time: Time.now.utc, allow_empty: false)
|
69
79
|
git("add --all")
|
70
80
|
|
@@ -106,6 +116,12 @@ module Spoom
|
|
106
116
|
git("log #{arg.join(" ")}")
|
107
117
|
end
|
108
118
|
|
119
|
+
# Run `git push <remote> <ref>` in this context directory
|
120
|
+
sig { params(remote: String, ref: String, force: T::Boolean).returns(ExecResult) }
|
121
|
+
def git_push!(remote, ref, force: false)
|
122
|
+
git("push #{force ? "-f" : ""} #{remote} #{ref}")
|
123
|
+
end
|
124
|
+
|
109
125
|
sig { params(arg: String).returns(ExecResult) }
|
110
126
|
def git_show(*arg)
|
111
127
|
git("show #{arg.join(" ")}")
|
data/lib/spoom/context/sorbet.rb
CHANGED
@@ -61,14 +61,49 @@ module Spoom
|
|
61
61
|
end
|
62
62
|
|
63
63
|
# List all files typechecked by Sorbet from its `config`
|
64
|
-
sig { params(with_config: T.nilable(Spoom::Sorbet::Config)).returns(T::Array[String]) }
|
65
|
-
def srb_files(with_config: nil)
|
64
|
+
sig { params(with_config: T.nilable(Spoom::Sorbet::Config), include_rbis: T::Boolean).returns(T::Array[String]) }
|
65
|
+
def srb_files(with_config: nil, include_rbis: true)
|
66
66
|
config = with_config || sorbet_config
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
67
|
+
|
68
|
+
allowed_extensions = config.allowed_extensions
|
69
|
+
allowed_extensions = Spoom::Sorbet::Config::DEFAULT_ALLOWED_EXTENSIONS if allowed_extensions.empty?
|
70
|
+
allowed_extensions -= [".rbi"] unless include_rbis
|
71
|
+
|
72
|
+
excluded_patterns = config.ignore.map do |string|
|
73
|
+
# We need to simulate the behavior of Sorbet's `--ignore` flag.
|
74
|
+
#
|
75
|
+
# From Sorbet docs on `--ignore`:
|
76
|
+
# > Ignores input files that contain the given string in their paths (relative to the input path passed to
|
77
|
+
# > Sorbet). Strings beginning with / match against the prefix of these relative paths; others are substring
|
78
|
+
# > matchs. Matches must be against whole folder and file names, so `foo` matches `/foo/bar.rb` and
|
79
|
+
# > `/bar/foo/baz.rb` but not `/foo.rb` or `/foo2/bar.rb`.
|
80
|
+
string = if string.start_with?("/")
|
81
|
+
# Strings beginning with / match against the prefix of these relative paths
|
82
|
+
File.join(absolute_path, string)
|
83
|
+
else
|
84
|
+
# Others are substring matchs
|
85
|
+
File.join(absolute_path, "**", string)
|
86
|
+
end
|
87
|
+
# Matches must be against whole folder and file names
|
88
|
+
"#{string.delete_suffix("/")}{,/**}"
|
89
|
+
end
|
90
|
+
|
91
|
+
collector = FileCollector.new(allow_extensions: allowed_extensions, exclude_patterns: excluded_patterns)
|
92
|
+
collector.visit_paths(config.paths.map { |path| absolute_path_to(path) })
|
93
|
+
collector.files.map { |file| file.delete_prefix("#{absolute_path}/") }.sort
|
94
|
+
end
|
95
|
+
|
96
|
+
# List all files typechecked by Sorbet from its `config` that matches `strictness`
|
97
|
+
sig do
|
98
|
+
params(
|
99
|
+
strictness: String,
|
100
|
+
with_config: T.nilable(Spoom::Sorbet::Config),
|
101
|
+
include_rbis: T::Boolean,
|
102
|
+
).returns(T::Array[String])
|
103
|
+
end
|
104
|
+
def srb_files_with_strictness(strictness, with_config: nil, include_rbis: true)
|
105
|
+
srb_files(with_config: with_config, include_rbis: include_rbis)
|
106
|
+
.select { |file| read_file_strictness(file) == strictness }
|
72
107
|
end
|
73
108
|
|
74
109
|
sig { params(arg: String, sorbet_bin: T.nilable(String), capture_err: T::Boolean).returns(T.nilable(String)) }
|
@@ -148,48 +148,34 @@ module Spoom
|
|
148
148
|
class Sigils < CircleMap
|
149
149
|
extend T::Sig
|
150
150
|
|
151
|
-
sig
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
151
|
+
sig do
|
152
|
+
params(
|
153
|
+
id: String,
|
154
|
+
file_tree: FileTree,
|
155
|
+
nodes_strictnesses: T::Hash[FileTree::Node, T.nilable(String)],
|
156
|
+
nodes_scores: T::Hash[FileTree::Node, Float],
|
157
|
+
).void
|
158
|
+
end
|
159
|
+
def initialize(id, file_tree, nodes_strictnesses, nodes_scores)
|
160
|
+
@nodes_strictnesses = nodes_strictnesses
|
161
|
+
@nodes_scores = nodes_scores
|
162
|
+
super(id, file_tree.roots.map { |r| tree_node_to_json(r) })
|
157
163
|
end
|
158
164
|
|
159
165
|
sig { params(node: FileTree::Node).returns(T::Hash[Symbol, T.untyped]) }
|
160
166
|
def tree_node_to_json(node)
|
161
167
|
if node.children.empty?
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
sig { params(node: FileTree::Node).returns(T.nilable(String)) }
|
173
|
-
def tree_node_strictness(node)
|
174
|
-
prefix = @sigils_tree.strip_prefix
|
175
|
-
path = node.path
|
176
|
-
path = "#{prefix}/#{path}" if prefix
|
177
|
-
@strictnesses[node] ||= Spoom::Sorbet::Sigils.file_strictness(path)
|
178
|
-
end
|
179
|
-
|
180
|
-
sig { params(node: FileTree::Node).returns(Float) }
|
181
|
-
def tree_node_score(node)
|
182
|
-
unless @scores.key?(node)
|
183
|
-
if node.name =~ /\.rbi?$/
|
184
|
-
case tree_node_strictness(node)
|
185
|
-
when "true", "strict", "strong"
|
186
|
-
@scores[node] = 1.0
|
187
|
-
end
|
188
|
-
elsif !node.children.empty?
|
189
|
-
@scores[node] = node.children.values.sum { |n| tree_node_score(n) } / node.children.size.to_f
|
190
|
-
end
|
168
|
+
{
|
169
|
+
name: node.name,
|
170
|
+
strictness: @nodes_strictnesses.fetch(node, "false"),
|
171
|
+
}
|
172
|
+
else
|
173
|
+
{
|
174
|
+
name: node.name,
|
175
|
+
children: node.children.values.map { |n| tree_node_to_json(n) },
|
176
|
+
score: @nodes_scores.fetch(node, 0.0),
|
177
|
+
}
|
191
178
|
end
|
192
|
-
@scores[node] || 0.0
|
193
179
|
end
|
194
180
|
end
|
195
181
|
end
|
@@ -153,9 +153,24 @@ module Spoom
|
|
153
153
|
class Map < Card
|
154
154
|
extend T::Sig
|
155
155
|
|
156
|
-
sig
|
157
|
-
|
158
|
-
|
156
|
+
sig do
|
157
|
+
params(
|
158
|
+
file_tree: FileTree,
|
159
|
+
nodes_strictnesses: T::Hash[FileTree::Node, T.nilable(String)],
|
160
|
+
nodes_strictness_scores: T::Hash[FileTree::Node, Float],
|
161
|
+
title: String,
|
162
|
+
).void
|
163
|
+
end
|
164
|
+
def initialize(file_tree:, nodes_strictnesses:, nodes_strictness_scores:, title: "Strictness Map")
|
165
|
+
super(
|
166
|
+
title: title,
|
167
|
+
body: D3::CircleMap::Sigils.new(
|
168
|
+
"map_sigils",
|
169
|
+
file_tree,
|
170
|
+
nodes_strictnesses,
|
171
|
+
nodes_strictness_scores,
|
172
|
+
).html
|
173
|
+
)
|
159
174
|
end
|
160
175
|
end
|
161
176
|
|
@@ -246,27 +261,14 @@ module Spoom
|
|
246
261
|
class Report < Page
|
247
262
|
extend T::Sig
|
248
263
|
|
249
|
-
sig { returns(String) }
|
250
|
-
attr_reader :project_name
|
251
|
-
|
252
|
-
sig { returns(T.nilable(String)) }
|
253
|
-
attr_reader :sorbet_intro_commit
|
254
|
-
|
255
|
-
sig { returns(T.nilable(Time)) }
|
256
|
-
attr_reader :sorbet_intro_date
|
257
|
-
|
258
|
-
sig { returns(T::Array[Snapshot]) }
|
259
|
-
attr_reader :snapshots
|
260
|
-
|
261
|
-
sig { returns(FileTree) }
|
262
|
-
attr_reader :sigils_tree
|
263
|
-
|
264
264
|
sig do
|
265
265
|
params(
|
266
266
|
project_name: String,
|
267
267
|
palette: D3::ColorPalette,
|
268
268
|
snapshots: T::Array[Snapshot],
|
269
|
-
|
269
|
+
file_tree: FileTree,
|
270
|
+
nodes_strictnesses: T::Hash[FileTree::Node, T.nilable(String)],
|
271
|
+
nodes_strictness_scores: T::Hash[FileTree::Node, Float],
|
270
272
|
sorbet_intro_commit: T.nilable(String),
|
271
273
|
sorbet_intro_date: T.nilable(Time),
|
272
274
|
).void
|
@@ -275,24 +277,28 @@ module Spoom
|
|
275
277
|
project_name:,
|
276
278
|
palette:,
|
277
279
|
snapshots:,
|
278
|
-
|
280
|
+
file_tree:,
|
281
|
+
nodes_strictnesses:,
|
282
|
+
nodes_strictness_scores:,
|
279
283
|
sorbet_intro_commit: nil,
|
280
284
|
sorbet_intro_date: nil
|
281
285
|
)
|
282
286
|
super(title: project_name, palette: palette)
|
283
287
|
@project_name = project_name
|
284
288
|
@snapshots = snapshots
|
285
|
-
@
|
289
|
+
@file_tree = file_tree
|
290
|
+
@nodes_strictnesses = nodes_strictnesses
|
291
|
+
@nodes_strictness_scores = nodes_strictness_scores
|
286
292
|
@sorbet_intro_commit = sorbet_intro_commit
|
287
293
|
@sorbet_intro_date = sorbet_intro_date
|
288
294
|
end
|
289
295
|
|
290
296
|
sig { override.returns(String) }
|
291
297
|
def header_html
|
292
|
-
last = T.must(snapshots.last)
|
298
|
+
last = T.must(@snapshots.last)
|
293
299
|
<<~ERB
|
294
300
|
<h1 class="display-3">
|
295
|
-
#{project_name}
|
301
|
+
#{@project_name}
|
296
302
|
<span class="badge badge-pill badge-dark" style="font-size: 20%;">#{last.commit_sha}</span>
|
297
303
|
</h1>
|
298
304
|
ERB
|
@@ -300,17 +306,24 @@ module Spoom
|
|
300
306
|
|
301
307
|
sig { override.returns(T::Array[Cards::Card]) }
|
302
308
|
def cards
|
303
|
-
last = T.must(snapshots.last)
|
309
|
+
last = T.must(@snapshots.last)
|
304
310
|
cards = []
|
305
311
|
cards << Cards::Snapshot.new(snapshot: last)
|
306
|
-
cards << Cards::Map.new(
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
cards << Cards::Timeline::
|
312
|
-
cards << Cards::Timeline::
|
313
|
-
cards << Cards::
|
312
|
+
cards << Cards::Map.new(
|
313
|
+
file_tree: @file_tree,
|
314
|
+
nodes_strictnesses: @nodes_strictnesses,
|
315
|
+
nodes_strictness_scores: @nodes_strictness_scores,
|
316
|
+
)
|
317
|
+
cards << Cards::Timeline::Sigils.new(snapshots: @snapshots)
|
318
|
+
cards << Cards::Timeline::Calls.new(snapshots: @snapshots)
|
319
|
+
cards << Cards::Timeline::Sigs.new(snapshots: @snapshots)
|
320
|
+
cards << Cards::Timeline::RBIs.new(snapshots: @snapshots)
|
321
|
+
cards << Cards::Timeline::Versions.new(snapshots: @snapshots)
|
322
|
+
cards << Cards::Timeline::Runtimes.new(snapshots: @snapshots)
|
323
|
+
cards << Cards::SorbetIntro.new(
|
324
|
+
sorbet_intro_commit: @sorbet_intro_commit,
|
325
|
+
sorbet_intro_date: @sorbet_intro_date,
|
326
|
+
)
|
314
327
|
cards
|
315
328
|
end
|
316
329
|
end
|
data/lib/spoom/coverage.rb
CHANGED
@@ -83,29 +83,29 @@ module Spoom
|
|
83
83
|
def report(context, snapshots, palette:)
|
84
84
|
intro_commit = context.sorbet_intro_commit
|
85
85
|
|
86
|
+
file_tree = file_tree(context)
|
87
|
+
v = FileTree::CollectScores.new(context)
|
88
|
+
v.visit_tree(file_tree)
|
89
|
+
|
86
90
|
Report.new(
|
87
91
|
project_name: File.basename(context.absolute_path),
|
88
92
|
palette: palette,
|
89
93
|
snapshots: snapshots,
|
90
|
-
|
94
|
+
file_tree: file_tree,
|
95
|
+
nodes_strictnesses: v.strictnesses,
|
96
|
+
nodes_strictness_scores: v.scores,
|
91
97
|
sorbet_intro_commit: intro_commit&.sha,
|
92
98
|
sorbet_intro_date: intro_commit&.time,
|
93
99
|
)
|
94
100
|
end
|
95
101
|
|
96
102
|
sig { params(context: Context).returns(FileTree) }
|
97
|
-
def
|
98
|
-
|
99
|
-
|
100
|
-
extensions = context.sorbet_config.allowed_extensions
|
101
|
-
extensions = [".rb"] if extensions.empty?
|
102
|
-
extensions -= [".rbi"]
|
103
|
-
|
104
|
-
pattern = /\.(#{Regexp.union(extensions.map { |ext| ext[1..-1] })})$/
|
105
|
-
files.select! { |file| file =~ pattern }
|
106
|
-
files.reject! { |file| file =~ %r{/test/} }
|
103
|
+
def file_tree(context)
|
104
|
+
config = context.sorbet_config
|
105
|
+
config.ignore += ["test"]
|
107
106
|
|
108
|
-
|
107
|
+
files = context.srb_files(with_config: config, include_rbis: false)
|
108
|
+
FileTree.new(files)
|
109
109
|
end
|
110
110
|
end
|
111
111
|
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Spoom
|
5
|
+
module Deadcode
|
6
|
+
# A definition is a class, module, method, constant, etc. being defined in the code
|
7
|
+
class Definition < T::Struct
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
class Kind < T::Enum
|
11
|
+
enums do
|
12
|
+
AttrReader = new("attr_reader")
|
13
|
+
AttrWriter = new("attr_writer")
|
14
|
+
Class = new("class")
|
15
|
+
Constant = new("constant")
|
16
|
+
Method = new("method")
|
17
|
+
Module = new("module")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Status < T::Enum
|
22
|
+
enums do
|
23
|
+
# A definition is marked as `ALIVE` if it has at least one reference with the same name
|
24
|
+
ALIVE = new
|
25
|
+
# A definition is marked as `DEAD` if it has no reference with the same name
|
26
|
+
DEAD = new
|
27
|
+
# A definition can be marked as `IGNORED` if it is not relevant for the analysis
|
28
|
+
IGNORED = new
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
const :kind, Kind
|
33
|
+
const :name, String
|
34
|
+
const :full_name, String
|
35
|
+
const :location, Location
|
36
|
+
const :status, Status, default: Status::DEAD
|
37
|
+
|
38
|
+
# Kind
|
39
|
+
|
40
|
+
sig { returns(T::Boolean) }
|
41
|
+
def attr_reader?
|
42
|
+
kind == Kind::AttrReader
|
43
|
+
end
|
44
|
+
|
45
|
+
sig { returns(T::Boolean) }
|
46
|
+
def attr_writer?
|
47
|
+
kind == Kind::AttrWriter
|
48
|
+
end
|
49
|
+
|
50
|
+
sig { returns(T::Boolean) }
|
51
|
+
def class?
|
52
|
+
kind == Kind::Class
|
53
|
+
end
|
54
|
+
|
55
|
+
sig { returns(T::Boolean) }
|
56
|
+
def constant?
|
57
|
+
kind == Kind::Constant
|
58
|
+
end
|
59
|
+
|
60
|
+
sig { returns(T::Boolean) }
|
61
|
+
def method?
|
62
|
+
kind == Kind::Method
|
63
|
+
end
|
64
|
+
|
65
|
+
sig { returns(T::Boolean) }
|
66
|
+
def module?
|
67
|
+
kind == Kind::Module
|
68
|
+
end
|
69
|
+
|
70
|
+
# Status
|
71
|
+
|
72
|
+
sig { returns(T::Boolean) }
|
73
|
+
def alive?
|
74
|
+
status == Status::ALIVE
|
75
|
+
end
|
76
|
+
|
77
|
+
sig { void }
|
78
|
+
def alive!
|
79
|
+
@status = Status::ALIVE
|
80
|
+
end
|
81
|
+
|
82
|
+
sig { returns(T::Boolean) }
|
83
|
+
def dead?
|
84
|
+
status == Status::DEAD
|
85
|
+
end
|
86
|
+
|
87
|
+
sig { returns(T::Boolean) }
|
88
|
+
def ignored?
|
89
|
+
status == Status::IGNORED
|
90
|
+
end
|
91
|
+
|
92
|
+
sig { void }
|
93
|
+
def ignored!
|
94
|
+
@status = Status::IGNORED
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Copied from https://github.com/rails/rails/blob/main/actionview/lib/action_view/template/handlers/erb/erubi.rb.
|
5
|
+
#
|
6
|
+
# Copyright (c) David Heinemeier Hansson
|
7
|
+
#
|
8
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
9
|
+
# a copy of this software and associated documentation files (the
|
10
|
+
# "Software"), to deal in the Software without restriction, including
|
11
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
12
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
13
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
14
|
+
# the following conditions:
|
15
|
+
#
|
16
|
+
# The above copyright notice and this permission notice shall be
|
17
|
+
# included in all copies or substantial portions of the Software.
|
18
|
+
#
|
19
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
20
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
21
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
22
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
23
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
24
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
25
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
26
|
+
module Spoom
|
27
|
+
module Deadcode
|
28
|
+
# Custom engine to handle ERB templates as used by Rails
|
29
|
+
class ERB < ::Erubi::Engine
|
30
|
+
extend T::Sig
|
31
|
+
|
32
|
+
sig { params(input: T.untyped, properties: T.untyped).void }
|
33
|
+
def initialize(input, properties = {})
|
34
|
+
@newline_pending = 0
|
35
|
+
|
36
|
+
properties = Hash[properties]
|
37
|
+
properties[:bufvar] ||= "@output_buffer"
|
38
|
+
properties[:preamble] ||= ""
|
39
|
+
properties[:postamble] ||= "#{properties[:bufvar]}.to_s"
|
40
|
+
properties[:escapefunc] = ""
|
41
|
+
|
42
|
+
super
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
sig { params(text: T.untyped).void }
|
48
|
+
def add_text(text)
|
49
|
+
return if text.empty?
|
50
|
+
|
51
|
+
if text == "\n"
|
52
|
+
@newline_pending += 1
|
53
|
+
else
|
54
|
+
src << bufvar << ".safe_append='"
|
55
|
+
src << "\n" * @newline_pending if @newline_pending > 0
|
56
|
+
src << text.gsub(/['\\]/, '\\\\\&')
|
57
|
+
src << "'.freeze;"
|
58
|
+
|
59
|
+
@newline_pending = 0
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
BLOCK_EXPR = /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/
|
64
|
+
|
65
|
+
sig { params(indicator: T.untyped, code: T.untyped).void }
|
66
|
+
def add_expression(indicator, code)
|
67
|
+
flush_newline_if_pending(src)
|
68
|
+
|
69
|
+
src << bufvar << if (indicator == "==") || @escape
|
70
|
+
".safe_expr_append="
|
71
|
+
else
|
72
|
+
".append="
|
73
|
+
end
|
74
|
+
|
75
|
+
if BLOCK_EXPR.match?(code)
|
76
|
+
src << " " << code
|
77
|
+
else
|
78
|
+
src << "(" << code << ");"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
sig { params(code: T.untyped).void }
|
83
|
+
def add_code(code)
|
84
|
+
flush_newline_if_pending(src)
|
85
|
+
super
|
86
|
+
end
|
87
|
+
|
88
|
+
sig { params(_: T.untyped).void }
|
89
|
+
def add_postamble(_)
|
90
|
+
flush_newline_if_pending(src)
|
91
|
+
super
|
92
|
+
end
|
93
|
+
|
94
|
+
sig { params(src: T.untyped).void }
|
95
|
+
def flush_newline_if_pending(src)
|
96
|
+
if @newline_pending > 0
|
97
|
+
src << bufvar << ".safe_append='#{"\n" * @newline_pending}'.freeze;"
|
98
|
+
@newline_pending = 0
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|