spoom 1.2.1 → 1.2.3
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/cli/coverage.rb +1 -1
- data/lib/spoom/context/bundle.rb +16 -7
- data/lib/spoom/context/file_system.rb +17 -0
- data/lib/spoom/context/git.rb +21 -5
- data/lib/spoom/context/sorbet.rb +24 -7
- 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 +403 -0
- data/lib/spoom/deadcode/location.rb +58 -0
- data/lib/spoom/deadcode/plugins/base.rb +201 -0
- data/lib/spoom/deadcode/plugins/ruby.rb +64 -0
- data/lib/spoom/deadcode/plugins.rb +5 -0
- data/lib/spoom/deadcode/reference.rb +34 -0
- data/lib/spoom/deadcode/send.rb +18 -0
- data/lib/spoom/deadcode.rb +56 -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/lsp.rb +2 -2
- data/lib/spoom/sorbet/sigils.rb +2 -2
- data/lib/spoom/sorbet.rb +1 -0
- data/lib/spoom/version.rb +1 -1
- data/lib/spoom.rb +1 -0
- metadata +33 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8e7ef8ebef70bc8827cdcb4b2c0e49ed6c53686e0db409908354f3762ad07c10
|
4
|
+
data.tar.gz: a6adb405f0379cbe040e743f177e758e2bdaadcdea13ee9c7f75d84f764434b1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 86d74bc7d9554d67c1421908116bbab7615d77e9cb096d461f7885429d495f2b02f19d6024117f7083423cfe7c48165b18918c143f29574896bfbb76a4fb651f
|
7
|
+
data.tar.gz: 754d41e8f26425a42288388879fe1658214adea03002421495297486343a827d6dc79a395e895553ed15f67cdc766a16f4a174aab2a3671bd2455e305a8c6f13
|
data/Gemfile
CHANGED
data/lib/spoom/cli/coverage.rb
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
@@ -13,7 +13,7 @@ module Spoom
|
|
13
13
|
sig { params(string: String).returns(T.nilable(Commit)) }
|
14
14
|
def parse_line(string)
|
15
15
|
sha, epoch = string.split(" ", 2)
|
16
|
-
return
|
16
|
+
return unless sha && epoch
|
17
17
|
|
18
18
|
time = Time.strptime(epoch, "%s")
|
19
19
|
Commit.new(sha: sha, time: time)
|
@@ -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
|
|
@@ -78,7 +88,7 @@ module Spoom
|
|
78
88
|
sig { returns(T.nilable(String)) }
|
79
89
|
def git_current_branch
|
80
90
|
res = git("branch --show-current")
|
81
|
-
return
|
91
|
+
return unless res.status
|
82
92
|
|
83
93
|
res.out.strip
|
84
94
|
end
|
@@ -93,10 +103,10 @@ module Spoom
|
|
93
103
|
sig { params(short_sha: T::Boolean).returns(T.nilable(Spoom::Git::Commit)) }
|
94
104
|
def git_last_commit(short_sha: true)
|
95
105
|
res = git_log("HEAD --format='%#{short_sha ? "h" : "H"} %at' -1")
|
96
|
-
return
|
106
|
+
return unless res.status
|
97
107
|
|
98
108
|
out = res.out.strip
|
99
|
-
return
|
109
|
+
return if out.empty?
|
100
110
|
|
101
111
|
Spoom::Git::Commit.parse_line(out)
|
102
112
|
end
|
@@ -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
@@ -52,7 +52,7 @@ module Spoom
|
|
52
52
|
sorbet_bin: sorbet_bin,
|
53
53
|
capture_err: capture_err,
|
54
54
|
)
|
55
|
-
return
|
55
|
+
return unless file?(metrics_file)
|
56
56
|
|
57
57
|
metrics_path = absolute_path_to(metrics_file)
|
58
58
|
metrics = Spoom::Sorbet::MetricsParser.parse_file(metrics_path)
|
@@ -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) })
|
@@ -92,7 +109,7 @@ module Spoom
|
|
92
109
|
sig { params(arg: String, sorbet_bin: T.nilable(String), capture_err: T::Boolean).returns(T.nilable(String)) }
|
93
110
|
def srb_version(*arg, sorbet_bin: nil, capture_err: true)
|
94
111
|
res = T.unsafe(self).srb_tc("--no-config", "--version", *arg, sorbet_bin: sorbet_bin, capture_err: capture_err)
|
95
|
-
return
|
112
|
+
return unless res.status
|
96
113
|
|
97
114
|
res.out.split(" ")[2]
|
98
115
|
end
|
@@ -130,10 +147,10 @@ module Spoom
|
|
130
147
|
sig { returns(T.nilable(Spoom::Git::Commit)) }
|
131
148
|
def sorbet_intro_commit
|
132
149
|
res = git_log("--diff-filter=A --format='%h %at' -1 -- sorbet/config")
|
133
|
-
return
|
150
|
+
return unless res.status
|
134
151
|
|
135
152
|
out = res.out.strip
|
136
|
-
return
|
153
|
+
return if out.empty?
|
137
154
|
|
138
155
|
Spoom::Git::Commit.parse_line(out)
|
139
156
|
end
|
@@ -142,10 +159,10 @@ module Spoom
|
|
142
159
|
sig { returns(T.nilable(Spoom::Git::Commit)) }
|
143
160
|
def sorbet_removal_commit
|
144
161
|
res = git_log("--diff-filter=D --format='%h %at' -1 -- sorbet/config")
|
145
|
-
return
|
162
|
+
return unless res.status
|
146
163
|
|
147
164
|
out = res.out.strip
|
148
|
-
return
|
165
|
+
return if out.empty?
|
149
166
|
|
150
167
|
Spoom::Git::Commit.parse_line(out)
|
151
168
|
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
|
@@ -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
|