spoom 1.2.1 → 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/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 +18 -1
- 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 +26 -3
- data/lib/spoom/printer.rb +0 -2
- 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 -1
- data/lib/spoom/version.rb +1 -1
- data/lib/spoom.rb +1 -0
- metadata +40 -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/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
@@ -69,7 +69,24 @@ module Spoom
|
|
69
69
|
allowed_extensions = Spoom::Sorbet::Config::DEFAULT_ALLOWED_EXTENSIONS if allowed_extensions.empty?
|
70
70
|
allowed_extensions -= [".rbi"] unless include_rbis
|
71
71
|
|
72
|
-
excluded_patterns = config.ignore.map
|
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
|
73
90
|
|
74
91
|
collector = FileCollector.new(allow_extensions: allowed_extensions, exclude_patterns: excluded_patterns)
|
75
92
|
collector.visit_paths(config.paths.map { |path| absolute_path_to(path) })
|
@@ -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
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Spoom
|
5
|
+
module Deadcode
|
6
|
+
class Index
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
sig { returns(T::Hash[String, T::Array[Definition]]) }
|
10
|
+
attr_reader :definitions
|
11
|
+
|
12
|
+
sig { returns(T::Hash[String, T::Array[Reference]]) }
|
13
|
+
attr_reader :references
|
14
|
+
|
15
|
+
sig { void }
|
16
|
+
def initialize
|
17
|
+
@definitions = T.let({}, T::Hash[String, T::Array[Definition]])
|
18
|
+
@references = T.let({}, T::Hash[String, T::Array[Reference]])
|
19
|
+
end
|
20
|
+
|
21
|
+
# Indexing
|
22
|
+
|
23
|
+
sig { params(definition: Definition).void }
|
24
|
+
def define(definition)
|
25
|
+
(@definitions[definition.name] ||= []) << definition
|
26
|
+
end
|
27
|
+
|
28
|
+
sig { params(reference: Reference).void }
|
29
|
+
def reference(reference)
|
30
|
+
(@references[reference.name] ||= []) << reference
|
31
|
+
end
|
32
|
+
|
33
|
+
# Mark all definitions having a reference of the same name as `alive`
|
34
|
+
#
|
35
|
+
# To be called once all the files have been indexed and all the definitions and references discovered.
|
36
|
+
sig { void }
|
37
|
+
def finalize!
|
38
|
+
@references.keys.each do |name|
|
39
|
+
definitions_for_name(name).each(&:alive!)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Utils
|
44
|
+
|
45
|
+
sig { params(name: String).returns(T::Array[Definition]) }
|
46
|
+
def definitions_for_name(name)
|
47
|
+
@definitions[name] || []
|
48
|
+
end
|
49
|
+
|
50
|
+
sig { returns(T::Array[Definition]) }
|
51
|
+
def all_definitions
|
52
|
+
@definitions.values.flatten
|
53
|
+
end
|
54
|
+
|
55
|
+
sig { returns(T::Array[Reference]) }
|
56
|
+
def all_references
|
57
|
+
@references.values.flatten
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,394 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Spoom
|
5
|
+
module Deadcode
|
6
|
+
class Indexer < SyntaxTree::Visitor
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
sig { returns(String) }
|
10
|
+
attr_reader :path, :file_name
|
11
|
+
|
12
|
+
sig { returns(Index) }
|
13
|
+
attr_reader :index
|
14
|
+
|
15
|
+
sig { params(path: String, source: String, index: Index).void }
|
16
|
+
def initialize(path, source, index)
|
17
|
+
super()
|
18
|
+
|
19
|
+
@path = path
|
20
|
+
@file_name = T.let(File.basename(path), String)
|
21
|
+
@source = source
|
22
|
+
@index = index
|
23
|
+
@previous_node = T.let(nil, T.nilable(SyntaxTree::Node))
|
24
|
+
@names_nesting = T.let([], T::Array[String])
|
25
|
+
@nodes_nesting = T.let([], T::Array[SyntaxTree::Node])
|
26
|
+
@in_const_field = T.let(false, T::Boolean)
|
27
|
+
@in_opassign = T.let(false, T::Boolean)
|
28
|
+
@in_symbol_literal = T.let(false, T::Boolean)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Visit
|
32
|
+
|
33
|
+
sig { override.params(node: T.nilable(SyntaxTree::Node)).void }
|
34
|
+
def visit(node)
|
35
|
+
return unless node
|
36
|
+
|
37
|
+
@nodes_nesting << node
|
38
|
+
super
|
39
|
+
@nodes_nesting.pop
|
40
|
+
@previous_node = node
|
41
|
+
end
|
42
|
+
|
43
|
+
sig { override.params(node: SyntaxTree::AliasNode).void }
|
44
|
+
def visit_alias(node)
|
45
|
+
reference_method(node_string(node.right), node)
|
46
|
+
end
|
47
|
+
|
48
|
+
sig { override.params(node: SyntaxTree::ARef).void }
|
49
|
+
def visit_aref(node)
|
50
|
+
super
|
51
|
+
|
52
|
+
reference_method("[]", node)
|
53
|
+
end
|
54
|
+
|
55
|
+
sig { override.params(node: SyntaxTree::ARefField).void }
|
56
|
+
def visit_aref_field(node)
|
57
|
+
super
|
58
|
+
|
59
|
+
reference_method("[]=", node)
|
60
|
+
end
|
61
|
+
|
62
|
+
sig { override.params(node: SyntaxTree::ArgBlock).void }
|
63
|
+
def visit_arg_block(node)
|
64
|
+
value = node.value
|
65
|
+
|
66
|
+
case value
|
67
|
+
when SyntaxTree::SymbolLiteral
|
68
|
+
# If the block call is something like `x.select(&:foo)`, we need to reference the `foo` method
|
69
|
+
reference_method(symbol_string(value), node)
|
70
|
+
when SyntaxTree::VCall
|
71
|
+
# If the block call is something like `x.select { ... }`, we need to visit the block
|
72
|
+
super
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
sig { override.params(node: SyntaxTree::Binary).void }
|
77
|
+
def visit_binary(node)
|
78
|
+
super
|
79
|
+
|
80
|
+
op = node.operator
|
81
|
+
|
82
|
+
# Reference the operator itself
|
83
|
+
reference_method(op.to_s, node)
|
84
|
+
|
85
|
+
case op
|
86
|
+
when :<, :>, :<=, :>=
|
87
|
+
# For comparison operators, we also reference the `<=>` method
|
88
|
+
reference_method("<=>", node)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
sig { override.params(node: SyntaxTree::CallNode).void }
|
93
|
+
def visit_call(node)
|
94
|
+
visit_send(
|
95
|
+
Send.new(
|
96
|
+
node: node,
|
97
|
+
name: node_string(node.message),
|
98
|
+
recv: node.receiver,
|
99
|
+
args: call_args(node.arguments),
|
100
|
+
),
|
101
|
+
)
|
102
|
+
end
|
103
|
+
|
104
|
+
sig { override.params(node: SyntaxTree::ClassDeclaration).void }
|
105
|
+
def visit_class(node)
|
106
|
+
const_name = node_string(node.constant)
|
107
|
+
@names_nesting << const_name
|
108
|
+
define_class(T.must(const_name.split("::").last), @names_nesting.join("::"), node)
|
109
|
+
|
110
|
+
# We do not call `super` here because we don't want to visit the `constant` again
|
111
|
+
visit(node.superclass) if node.superclass
|
112
|
+
visit(node.bodystmt)
|
113
|
+
|
114
|
+
@names_nesting.pop
|
115
|
+
end
|
116
|
+
|
117
|
+
sig { override.params(node: SyntaxTree::Command).void }
|
118
|
+
def visit_command(node)
|
119
|
+
visit_send(
|
120
|
+
Send.new(
|
121
|
+
node: node,
|
122
|
+
name: node_string(node.message),
|
123
|
+
args: call_args(node.arguments),
|
124
|
+
block: node.block,
|
125
|
+
),
|
126
|
+
)
|
127
|
+
end
|
128
|
+
|
129
|
+
sig { override.params(node: SyntaxTree::CommandCall).void }
|
130
|
+
def visit_command_call(node)
|
131
|
+
visit_send(
|
132
|
+
Send.new(
|
133
|
+
node: node,
|
134
|
+
name: node_string(node.message),
|
135
|
+
recv: node.receiver,
|
136
|
+
args: call_args(node.arguments),
|
137
|
+
block: node.block,
|
138
|
+
),
|
139
|
+
)
|
140
|
+
end
|
141
|
+
|
142
|
+
sig { override.params(node: SyntaxTree::Const).void }
|
143
|
+
def visit_const(node)
|
144
|
+
reference_constant(node.value, node) unless @in_symbol_literal
|
145
|
+
end
|
146
|
+
|
147
|
+
sig { override.params(node: SyntaxTree::ConstPathField).void }
|
148
|
+
def visit_const_path_field(node)
|
149
|
+
# We do not call `super` here because we don't want to visit the `constant` again
|
150
|
+
visit(node.parent)
|
151
|
+
|
152
|
+
name = node.constant.value
|
153
|
+
full_name = [*@names_nesting, node_string(node.parent), name].join("::")
|
154
|
+
define_constant(name, full_name, node)
|
155
|
+
end
|
156
|
+
|
157
|
+
sig { override.params(node: SyntaxTree::DefNode).void }
|
158
|
+
def visit_def(node)
|
159
|
+
super
|
160
|
+
|
161
|
+
name = node_string(node.name)
|
162
|
+
define_method(name, [*@names_nesting, name].join("::"), node)
|
163
|
+
end
|
164
|
+
|
165
|
+
sig { override.params(node: SyntaxTree::Field).void }
|
166
|
+
def visit_field(node)
|
167
|
+
visit(node.parent)
|
168
|
+
|
169
|
+
name = node.name
|
170
|
+
case name
|
171
|
+
when SyntaxTree::Const
|
172
|
+
name = name.value
|
173
|
+
full_name = [*@names_nesting, node_string(node.parent), name].join("::")
|
174
|
+
define_constant(name, full_name, node)
|
175
|
+
when SyntaxTree::Ident
|
176
|
+
reference_method(name.value, node) if @in_opassign
|
177
|
+
reference_method("#{name.value}=", node)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
sig { override.params(node: SyntaxTree::ModuleDeclaration).void }
|
182
|
+
def visit_module(node)
|
183
|
+
const_name = node_string(node.constant)
|
184
|
+
@names_nesting << const_name
|
185
|
+
define_module(T.must(const_name.split("::").last), @names_nesting.join("::"), node)
|
186
|
+
|
187
|
+
# We do not call `super` here because we don't want to visit the `constant` again
|
188
|
+
visit(node.bodystmt)
|
189
|
+
|
190
|
+
@names_nesting.pop
|
191
|
+
end
|
192
|
+
|
193
|
+
sig { override.params(node: SyntaxTree::OpAssign).void }
|
194
|
+
def visit_opassign(node)
|
195
|
+
# Both `FOO = x` and `FOO += x` yield a VarField node, but the former is a constant definition and the latter is
|
196
|
+
# a constant reference. We need to distinguish between the two cases.
|
197
|
+
@in_opassign = true
|
198
|
+
super
|
199
|
+
@in_opassign = false
|
200
|
+
end
|
201
|
+
|
202
|
+
sig { params(send: Send).void }
|
203
|
+
def visit_send(send)
|
204
|
+
visit(send.recv)
|
205
|
+
|
206
|
+
case send.name
|
207
|
+
when "attr_reader"
|
208
|
+
send.args.each do |arg|
|
209
|
+
next unless arg.is_a?(SyntaxTree::SymbolLiteral)
|
210
|
+
|
211
|
+
name = symbol_string(arg)
|
212
|
+
define_attr_reader(name, [*@names_nesting, name].join("::"), arg)
|
213
|
+
end
|
214
|
+
when "attr_writer"
|
215
|
+
send.args.each do |arg|
|
216
|
+
next unless arg.is_a?(SyntaxTree::SymbolLiteral)
|
217
|
+
|
218
|
+
name = symbol_string(arg)
|
219
|
+
define_attr_writer("#{name}=", "#{[*@names_nesting, name].join("::")}=", arg)
|
220
|
+
end
|
221
|
+
when "attr_accessor"
|
222
|
+
send.args.each do |arg|
|
223
|
+
next unless arg.is_a?(SyntaxTree::SymbolLiteral)
|
224
|
+
|
225
|
+
name = symbol_string(arg)
|
226
|
+
full_name = [*@names_nesting, name].join("::")
|
227
|
+
define_attr_reader(name, full_name, arg)
|
228
|
+
define_attr_writer("#{name}=", "#{full_name}=", arg)
|
229
|
+
end
|
230
|
+
else
|
231
|
+
reference_method(send.name, send.node)
|
232
|
+
visit_all(send.args)
|
233
|
+
visit(send.block)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
sig { override.params(node: SyntaxTree::SymbolLiteral).void }
|
238
|
+
def visit_symbol_literal(node)
|
239
|
+
# Something like `:FOO` will yield a Const node but we do not want to treat it as a constant reference.
|
240
|
+
# So we need to distinguish between the two cases.
|
241
|
+
@in_symbol_literal = true
|
242
|
+
super
|
243
|
+
@in_symbol_literal = false
|
244
|
+
end
|
245
|
+
|
246
|
+
sig { override.params(node: SyntaxTree::TopConstField).void }
|
247
|
+
def visit_top_const_field(node)
|
248
|
+
define_constant(node.constant.value, node.constant.value, node)
|
249
|
+
end
|
250
|
+
|
251
|
+
sig { override.params(node: SyntaxTree::VarField).void }
|
252
|
+
def visit_var_field(node)
|
253
|
+
value = node.value
|
254
|
+
case value
|
255
|
+
when SyntaxTree::Const
|
256
|
+
if @in_opassign
|
257
|
+
reference_constant(value.value, node)
|
258
|
+
else
|
259
|
+
name = value.value
|
260
|
+
define_constant(name, [*@names_nesting, name].join("::"), node)
|
261
|
+
end
|
262
|
+
when SyntaxTree::Ident
|
263
|
+
reference_method(value.value, node) if @in_opassign
|
264
|
+
reference_method("#{value.value}=", node)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
sig { override.params(node: SyntaxTree::VCall).void }
|
269
|
+
def visit_vcall(node)
|
270
|
+
visit_send(Send.new(node: node, name: node_string(node.value)))
|
271
|
+
end
|
272
|
+
|
273
|
+
private
|
274
|
+
|
275
|
+
# Definition indexing
|
276
|
+
|
277
|
+
sig { params(name: String, full_name: String, node: SyntaxTree::Node).void }
|
278
|
+
def define_attr_reader(name, full_name, node)
|
279
|
+
definition = Definition.new(
|
280
|
+
kind: Definition::Kind::AttrReader,
|
281
|
+
name: name,
|
282
|
+
full_name: full_name,
|
283
|
+
location: node_location(node),
|
284
|
+
)
|
285
|
+
@index.define(definition)
|
286
|
+
end
|
287
|
+
|
288
|
+
sig { params(name: String, full_name: String, node: SyntaxTree::Node).void }
|
289
|
+
def define_attr_writer(name, full_name, node)
|
290
|
+
definition = Definition.new(
|
291
|
+
kind: Definition::Kind::AttrWriter,
|
292
|
+
name: name,
|
293
|
+
full_name: full_name,
|
294
|
+
location: node_location(node),
|
295
|
+
)
|
296
|
+
@index.define(definition)
|
297
|
+
end
|
298
|
+
|
299
|
+
sig { params(name: String, full_name: String, node: SyntaxTree::Node).void }
|
300
|
+
def define_class(name, full_name, node)
|
301
|
+
definition = Definition.new(
|
302
|
+
kind: Definition::Kind::Class,
|
303
|
+
name: name,
|
304
|
+
full_name: full_name,
|
305
|
+
location: node_location(node),
|
306
|
+
)
|
307
|
+
@index.define(definition)
|
308
|
+
end
|
309
|
+
|
310
|
+
sig { params(name: String, full_name: String, node: SyntaxTree::Node).void }
|
311
|
+
def define_constant(name, full_name, node)
|
312
|
+
definition = Definition.new(
|
313
|
+
kind: Definition::Kind::Constant,
|
314
|
+
name: name,
|
315
|
+
full_name: full_name,
|
316
|
+
location: node_location(node),
|
317
|
+
)
|
318
|
+
@index.define(definition)
|
319
|
+
end
|
320
|
+
|
321
|
+
sig { params(name: String, full_name: String, node: SyntaxTree::Node).void }
|
322
|
+
def define_method(name, full_name, node)
|
323
|
+
definition = Definition.new(
|
324
|
+
kind: Definition::Kind::Method,
|
325
|
+
name: name,
|
326
|
+
full_name: full_name,
|
327
|
+
location: node_location(node),
|
328
|
+
)
|
329
|
+
@index.define(definition)
|
330
|
+
end
|
331
|
+
|
332
|
+
sig { params(name: String, full_name: String, node: SyntaxTree::Node).void }
|
333
|
+
def define_module(name, full_name, node)
|
334
|
+
definition = Definition.new(
|
335
|
+
kind: Definition::Kind::Module,
|
336
|
+
name: name,
|
337
|
+
full_name: full_name,
|
338
|
+
location: node_location(node),
|
339
|
+
)
|
340
|
+
@index.define(definition)
|
341
|
+
end
|
342
|
+
|
343
|
+
# Reference indexing
|
344
|
+
|
345
|
+
sig { params(name: String, node: SyntaxTree::Node).void }
|
346
|
+
def reference_constant(name, node)
|
347
|
+
@index.reference(Reference.new(name: name, kind: Reference::Kind::Constant, location: node_location(node)))
|
348
|
+
end
|
349
|
+
|
350
|
+
sig { params(name: String, node: SyntaxTree::Node).void }
|
351
|
+
def reference_method(name, node)
|
352
|
+
@index.reference(Reference.new(name: name, kind: Reference::Kind::Method, location: node_location(node)))
|
353
|
+
end
|
354
|
+
|
355
|
+
# Node utils
|
356
|
+
|
357
|
+
sig { params(node: T.any(Symbol, SyntaxTree::Node)).returns(String) }
|
358
|
+
def node_string(node)
|
359
|
+
case node
|
360
|
+
when Symbol
|
361
|
+
node.to_s
|
362
|
+
else
|
363
|
+
T.must(@source[node.location.start_char...node.location.end_char])
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
367
|
+
sig { params(node: SyntaxTree::Node).returns(Location) }
|
368
|
+
def node_location(node)
|
369
|
+
Location.from_syntax_tree(@path, node.location)
|
370
|
+
end
|
371
|
+
|
372
|
+
sig { params(node: SyntaxTree::Node).returns(String) }
|
373
|
+
def symbol_string(node)
|
374
|
+
node_string(node).delete_prefix(":")
|
375
|
+
end
|
376
|
+
|
377
|
+
sig do
|
378
|
+
params(
|
379
|
+
node: T.any(SyntaxTree::Args, SyntaxTree::ArgParen, SyntaxTree::ArgsForward, NilClass),
|
380
|
+
).returns(T::Array[SyntaxTree::Node])
|
381
|
+
end
|
382
|
+
def call_args(node)
|
383
|
+
case node
|
384
|
+
when SyntaxTree::ArgParen
|
385
|
+
call_args(node.arguments)
|
386
|
+
when SyntaxTree::Args
|
387
|
+
node.parts
|
388
|
+
else
|
389
|
+
[]
|
390
|
+
end
|
391
|
+
end
|
392
|
+
end
|
393
|
+
end
|
394
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Spoom
|
5
|
+
module Deadcode
|
6
|
+
class Location
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
include Comparable
|
10
|
+
|
11
|
+
class LocationError < Spoom::Error; end
|
12
|
+
|
13
|
+
class << self
|
14
|
+
extend T::Sig
|
15
|
+
|
16
|
+
sig { params(file: String, location: SyntaxTree::Location).returns(Location) }
|
17
|
+
def from_syntax_tree(file, location)
|
18
|
+
new(file, location.start_line, location.start_column, location.end_line, location.end_column)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
sig { returns(String) }
|
23
|
+
attr_reader :file
|
24
|
+
|
25
|
+
sig { returns(Integer) }
|
26
|
+
attr_reader :start_line, :start_column, :end_line, :end_column
|
27
|
+
|
28
|
+
sig do
|
29
|
+
params(
|
30
|
+
file: String,
|
31
|
+
start_line: Integer,
|
32
|
+
start_column: Integer,
|
33
|
+
end_line: Integer,
|
34
|
+
end_column: Integer,
|
35
|
+
).void
|
36
|
+
end
|
37
|
+
def initialize(file, start_line, start_column, end_line, end_column)
|
38
|
+
@file = file
|
39
|
+
@start_line = start_line
|
40
|
+
@start_column = start_column
|
41
|
+
@end_line = end_line
|
42
|
+
@end_column = end_column
|
43
|
+
end
|
44
|
+
|
45
|
+
sig { override.params(other: BasicObject).returns(T.nilable(Integer)) }
|
46
|
+
def <=>(other)
|
47
|
+
return nil unless Location === other
|
48
|
+
|
49
|
+
to_s <=> other.to_s
|
50
|
+
end
|
51
|
+
|
52
|
+
sig { returns(String) }
|
53
|
+
def to_s
|
54
|
+
"#{@file}:#{@start_line}:#{@start_column}-#{@end_line}:#{@end_column}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Spoom
|
5
|
+
module Deadcode
|
6
|
+
# A reference is a call to a method or a constant
|
7
|
+
class Reference < T::Struct
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
class Kind < T::Enum
|
11
|
+
enums do
|
12
|
+
Constant = new
|
13
|
+
Method = new
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
const :kind, Kind
|
18
|
+
const :name, String
|
19
|
+
const :location, Location
|
20
|
+
|
21
|
+
# Kind
|
22
|
+
|
23
|
+
sig { returns(T::Boolean) }
|
24
|
+
def constant?
|
25
|
+
kind == Kind::Constant
|
26
|
+
end
|
27
|
+
|
28
|
+
sig { returns(T::Boolean) }
|
29
|
+
def method?
|
30
|
+
kind == Kind::Method
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Spoom
|
5
|
+
module Deadcode
|
6
|
+
# An abstraction to simplify handling of SyntaxTree::CallNode, SyntaxTree::Command, SyntaxTree::CommandCall and
|
7
|
+
# SyntaxTree::VCall nodes.
|
8
|
+
class Send < T::Struct
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
const :node, SyntaxTree::Node
|
12
|
+
const :name, String
|
13
|
+
const :recv, T.nilable(SyntaxTree::Node), default: nil
|
14
|
+
const :args, T::Array[SyntaxTree::Node], default: []
|
15
|
+
const :block, T.nilable(SyntaxTree::Node), default: nil
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "erubi"
|
5
|
+
require "syntax_tree"
|
6
|
+
|
7
|
+
require_relative "deadcode/erb"
|
8
|
+
require_relative "deadcode/index"
|
9
|
+
require_relative "deadcode/indexer"
|
10
|
+
|
11
|
+
require_relative "deadcode/location"
|
12
|
+
require_relative "deadcode/definition"
|
13
|
+
require_relative "deadcode/reference"
|
14
|
+
require_relative "deadcode/send"
|
15
|
+
|
16
|
+
module Spoom
|
17
|
+
module Deadcode
|
18
|
+
class Error < Spoom::Error
|
19
|
+
extend T::Sig
|
20
|
+
extend T::Helpers
|
21
|
+
|
22
|
+
abstract!
|
23
|
+
|
24
|
+
sig { params(message: String, parent: Exception).void }
|
25
|
+
def initialize(message, parent:)
|
26
|
+
super(message)
|
27
|
+
set_backtrace(parent.backtrace)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class ParserError < Error; end
|
32
|
+
class IndexerError < Error; end
|
33
|
+
|
34
|
+
class << self
|
35
|
+
extend T::Sig
|
36
|
+
|
37
|
+
sig { params(index: Index, ruby: String, file: String).void }
|
38
|
+
def index_ruby(index, ruby, file:)
|
39
|
+
node = SyntaxTree.parse(ruby)
|
40
|
+
visitor = Spoom::Deadcode::Indexer.new(file, ruby, index)
|
41
|
+
visitor.visit(node)
|
42
|
+
rescue SyntaxTree::Parser::ParseError => e
|
43
|
+
raise ParserError.new("Error while parsing #{file} (#{e.message} at #{e.lineno}:#{e.column})", parent: e)
|
44
|
+
rescue => e
|
45
|
+
raise IndexerError.new("Error while indexing #{file} (#{e.message})", parent: e)
|
46
|
+
end
|
47
|
+
|
48
|
+
sig { params(index: Index, erb: String, file: String).void }
|
49
|
+
def index_erb(index, erb, file:)
|
50
|
+
ruby = ERB.new(erb).src
|
51
|
+
index_ruby(index, ruby, file: file)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/spoom/file_collector.rb
CHANGED
@@ -12,15 +12,21 @@ module Spoom
|
|
12
12
|
#
|
13
13
|
# If `allow_extensions` is empty, all files are collected.
|
14
14
|
# If `allow_extensions` is an array of extensions, only files with one of these extensions are collected.
|
15
|
+
#
|
16
|
+
# If `allow_mime_types` is empty, all files are collected.
|
17
|
+
# If `allow_mime_types` is an array of mimetypes, files without an extension are collected if their mimetype is in
|
18
|
+
# the list.
|
15
19
|
sig do
|
16
20
|
params(
|
17
21
|
allow_extensions: T::Array[String],
|
22
|
+
allow_mime_types: T::Array[String],
|
18
23
|
exclude_patterns: T::Array[String],
|
19
24
|
).void
|
20
25
|
end
|
21
|
-
def initialize(allow_extensions: [], exclude_patterns: [])
|
26
|
+
def initialize(allow_extensions: [], allow_mime_types: [], exclude_patterns: [])
|
22
27
|
@files = T.let([], T::Array[String])
|
23
28
|
@allow_extensions = allow_extensions
|
29
|
+
@allow_mime_types = allow_mime_types
|
24
30
|
@exclude_patterns = exclude_patterns
|
25
31
|
end
|
26
32
|
|
@@ -68,12 +74,29 @@ module Spoom
|
|
68
74
|
return false if @allow_extensions.empty?
|
69
75
|
|
70
76
|
extension = File.extname(path)
|
71
|
-
|
77
|
+
if extension.empty?
|
78
|
+
return true if @allow_mime_types.empty?
|
79
|
+
|
80
|
+
mime = mime_type_for(path)
|
81
|
+
@allow_mime_types.none? { |allowed| mime == allowed }
|
82
|
+
else
|
83
|
+
@allow_extensions.none? { |allowed| extension == allowed }
|
84
|
+
end
|
72
85
|
end
|
73
86
|
|
74
87
|
sig { params(path: String).returns(T::Boolean) }
|
75
88
|
def excluded_path?(path)
|
76
|
-
@exclude_patterns.any?
|
89
|
+
@exclude_patterns.any? do |pattern|
|
90
|
+
# Use `FNM_PATHNAME` so patterns do not match directory separators
|
91
|
+
# Use `FNM_EXTGLOB` to allow file globbing through `{a,b}`
|
92
|
+
File.fnmatch?(pattern, path, File::FNM_PATHNAME | File::FNM_EXTGLOB)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
sig { params(path: String).returns(T.nilable(String)) }
|
97
|
+
def mime_type_for(path)
|
98
|
+
# The `file` command appears to be hanging on MacOS for some files so we timeout after 1s.
|
99
|
+
%x{timeout 1s file --mime-type -b '#{path}'}.split("; ").first&.strip
|
77
100
|
end
|
78
101
|
end
|
79
102
|
end
|
data/lib/spoom/printer.rb
CHANGED
@@ -12,9 +12,6 @@ module Spoom
|
|
12
12
|
class Message
|
13
13
|
extend T::Sig
|
14
14
|
|
15
|
-
sig { returns(String) }
|
16
|
-
attr_reader :jsonrpc
|
17
|
-
|
18
15
|
sig { void }
|
19
16
|
def initialize
|
20
17
|
@jsonrpc = T.let("2.0", String)
|
@@ -43,9 +40,6 @@ module Spoom
|
|
43
40
|
sig { returns(Integer) }
|
44
41
|
attr_reader :id
|
45
42
|
|
46
|
-
sig { returns(String) }
|
47
|
-
attr_reader :method
|
48
|
-
|
49
43
|
sig { returns(T::Hash[T.untyped, T.untyped]) }
|
50
44
|
attr_reader :params
|
51
45
|
|
data/lib/spoom/sorbet/sigils.rb
CHANGED
data/lib/spoom/version.rb
CHANGED
data/lib/spoom.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: spoom
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.2.
|
4
|
+
version: 1.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alexandre Terrasa
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-06-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: 13.0.1
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: erubi
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 1.10.0
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 1.10.0
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
84
|
name: sorbet
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -94,6 +108,20 @@ dependencies:
|
|
94
108
|
- - ">="
|
95
109
|
- !ruby/object:Gem::Version
|
96
110
|
version: 0.5.9204
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: syntax_tree
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 6.1.1
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 6.1.1
|
97
125
|
- !ruby/object:Gem::Dependency
|
98
126
|
name: thor
|
99
127
|
requirement: !ruby/object:Gem::Requirement
|
@@ -143,6 +171,14 @@ files:
|
|
143
171
|
- lib/spoom/coverage/d3/timeline.rb
|
144
172
|
- lib/spoom/coverage/report.rb
|
145
173
|
- lib/spoom/coverage/snapshot.rb
|
174
|
+
- lib/spoom/deadcode.rb
|
175
|
+
- lib/spoom/deadcode/definition.rb
|
176
|
+
- lib/spoom/deadcode/erb.rb
|
177
|
+
- lib/spoom/deadcode/index.rb
|
178
|
+
- lib/spoom/deadcode/indexer.rb
|
179
|
+
- lib/spoom/deadcode/location.rb
|
180
|
+
- lib/spoom/deadcode/reference.rb
|
181
|
+
- lib/spoom/deadcode/send.rb
|
146
182
|
- lib/spoom/file_collector.rb
|
147
183
|
- lib/spoom/file_tree.rb
|
148
184
|
- lib/spoom/printer.rb
|
@@ -173,14 +209,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
173
209
|
requirements:
|
174
210
|
- - ">="
|
175
211
|
- !ruby/object:Gem::Version
|
176
|
-
version:
|
212
|
+
version: 3.0.0
|
177
213
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
178
214
|
requirements:
|
179
215
|
- - ">="
|
180
216
|
- !ruby/object:Gem::Version
|
181
217
|
version: '0'
|
182
218
|
requirements: []
|
183
|
-
rubygems_version: 3.4.
|
219
|
+
rubygems_version: 3.4.14
|
184
220
|
signing_key:
|
185
221
|
specification_version: 4
|
186
222
|
summary: Useful tools for Sorbet enthusiasts.
|