spoom 1.2.1 → 1.2.2
Sign up to get free protection for your applications and to get access to all the features.
- 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.
|