spoom 1.5.0 → 1.7.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/README.md +14 -0
- data/lib/spoom/backtrace_filter/minitest.rb +3 -4
- data/lib/spoom/cli/deadcode.rb +1 -2
- data/lib/spoom/cli/helper.rb +41 -31
- data/lib/spoom/cli/srb/assertions.rb +48 -0
- data/lib/spoom/cli/srb/bump.rb +1 -2
- data/lib/spoom/cli/srb/coverage.rb +1 -1
- data/lib/spoom/cli/srb/metrics.rb +68 -0
- data/lib/spoom/cli/srb/sigs.rb +209 -0
- data/lib/spoom/cli/srb/tc.rb +16 -1
- data/lib/spoom/cli/srb.rb +16 -4
- data/lib/spoom/cli.rb +1 -2
- data/lib/spoom/colors.rb +2 -6
- data/lib/spoom/context/bundle.rb +8 -9
- data/lib/spoom/context/exec.rb +3 -6
- data/lib/spoom/context/file_system.rb +12 -19
- data/lib/spoom/context/git.rb +14 -19
- data/lib/spoom/context/sorbet.rb +14 -27
- data/lib/spoom/context.rb +4 -8
- data/lib/spoom/counters.rb +22 -0
- data/lib/spoom/coverage/d3/base.rb +6 -8
- data/lib/spoom/coverage/d3/circle_map.rb +6 -16
- data/lib/spoom/coverage/d3/pie.rb +14 -19
- data/lib/spoom/coverage/d3/timeline.rb +46 -47
- data/lib/spoom/coverage/d3.rb +2 -4
- data/lib/spoom/coverage/report.rb +41 -79
- data/lib/spoom/coverage/snapshot.rb +8 -14
- data/lib/spoom/coverage.rb +3 -5
- data/lib/spoom/deadcode/definition.rb +12 -14
- data/lib/spoom/deadcode/erb.rb +10 -8
- data/lib/spoom/deadcode/index.rb +21 -25
- data/lib/spoom/deadcode/indexer.rb +5 -6
- data/lib/spoom/deadcode/plugins/action_mailer.rb +2 -3
- data/lib/spoom/deadcode/plugins/action_mailer_preview.rb +2 -3
- data/lib/spoom/deadcode/plugins/actionpack.rb +19 -22
- data/lib/spoom/deadcode/plugins/active_model.rb +2 -3
- data/lib/spoom/deadcode/plugins/active_record.rb +62 -53
- data/lib/spoom/deadcode/plugins/active_support.rb +3 -2
- data/lib/spoom/deadcode/plugins/base.rb +29 -32
- data/lib/spoom/deadcode/plugins/graphql.rb +2 -3
- data/lib/spoom/deadcode/plugins/minitest.rb +4 -4
- data/lib/spoom/deadcode/plugins/namespaces.rb +5 -5
- data/lib/spoom/deadcode/plugins/rails.rb +5 -5
- data/lib/spoom/deadcode/plugins/rubocop.rb +5 -5
- data/lib/spoom/deadcode/plugins/ruby.rb +3 -4
- data/lib/spoom/deadcode/plugins/sorbet.rb +12 -6
- data/lib/spoom/deadcode/plugins/thor.rb +2 -3
- data/lib/spoom/deadcode/plugins.rb +23 -31
- data/lib/spoom/deadcode/remover.rb +58 -79
- data/lib/spoom/deadcode/send.rb +2 -8
- data/lib/spoom/file_collector.rb +11 -19
- data/lib/spoom/file_tree.rb +36 -51
- data/lib/spoom/location.rb +9 -20
- data/lib/spoom/model/builder.rb +54 -17
- data/lib/spoom/model/model.rb +71 -74
- data/lib/spoom/model/namespace_visitor.rb +4 -3
- data/lib/spoom/model/reference.rb +4 -8
- data/lib/spoom/model/references_visitor.rb +50 -30
- data/lib/spoom/parse.rb +4 -4
- data/lib/spoom/poset.rb +22 -24
- data/lib/spoom/printer.rb +10 -13
- data/lib/spoom/rbs.rb +77 -0
- data/lib/spoom/sorbet/config.rb +17 -24
- data/lib/spoom/sorbet/errors.rb +87 -45
- data/lib/spoom/sorbet/lsp/base.rb +10 -16
- data/lib/spoom/sorbet/lsp/errors.rb +8 -16
- data/lib/spoom/sorbet/lsp/structures.rb +65 -91
- data/lib/spoom/sorbet/lsp.rb +20 -22
- data/lib/spoom/sorbet/metrics/code_metrics_visitor.rb +236 -0
- data/lib/spoom/sorbet/metrics/metrics_file_parser.rb +34 -0
- data/lib/spoom/sorbet/metrics.rb +2 -32
- data/lib/spoom/sorbet/sigils.rb +16 -23
- data/lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs.rb +242 -0
- data/lib/spoom/sorbet/translate/sorbet_assertions_to_rbs_comments.rb +123 -0
- data/lib/spoom/sorbet/translate/sorbet_sigs_to_rbs_comments.rb +293 -0
- data/lib/spoom/sorbet/translate/strip_sorbet_sigs.rb +23 -0
- data/lib/spoom/sorbet/translate/translator.rb +71 -0
- data/lib/spoom/sorbet/translate.rb +49 -0
- data/lib/spoom/sorbet.rb +6 -12
- data/lib/spoom/source/rewriter.rb +167 -0
- data/lib/spoom/source.rb +4 -0
- data/lib/spoom/timeline.rb +4 -6
- data/lib/spoom/version.rb +1 -1
- data/lib/spoom/visitor.rb +298 -151
- data/lib/spoom.rb +4 -3
- data/rbi/spoom.rbi +3567 -0
- metadata +62 -8
@@ -0,0 +1,236 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Spoom
|
5
|
+
module Sorbet
|
6
|
+
module Metrics
|
7
|
+
class << self
|
8
|
+
#: (Array[String]) -> Spoom::Counters
|
9
|
+
def collect_code_metrics(files)
|
10
|
+
counters = Counters.new
|
11
|
+
|
12
|
+
files.each do |file|
|
13
|
+
counters.increment("files")
|
14
|
+
|
15
|
+
content = File.read(file)
|
16
|
+
node = Spoom.parse_ruby(content, file: file, comments: true)
|
17
|
+
visitor = CodeMetricsVisitor.new(counters)
|
18
|
+
visitor.visit(node)
|
19
|
+
end
|
20
|
+
|
21
|
+
counters
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Collects metrics about how Sorbet is used in the codebase.
|
26
|
+
#
|
27
|
+
# This approach is different from the metrics file we get directly from Sorbet.
|
28
|
+
#
|
29
|
+
# This visitor actually visits the codebase and collects metrics about the amount of signatures, `T.` calls,
|
30
|
+
# and other metrics. It also knows about RBS comments.
|
31
|
+
#
|
32
|
+
# On the other hand, the metrics file is a snapshot of the metrics at type checking time and knows about
|
33
|
+
# is calls are typed, how many assertions are done, etc.
|
34
|
+
class CodeMetricsVisitor < Spoom::Visitor
|
35
|
+
include RBS::ExtractRBSComments
|
36
|
+
|
37
|
+
#: Counters
|
38
|
+
attr_reader :counters
|
39
|
+
|
40
|
+
#: (Spoom::Counters) -> void
|
41
|
+
def initialize(counters)
|
42
|
+
super()
|
43
|
+
|
44
|
+
@counters = counters
|
45
|
+
|
46
|
+
@last_sigs = [] #: Array[Prism::CallNode]
|
47
|
+
@type_params = [] #: Array[Prism::CallNode]
|
48
|
+
end
|
49
|
+
|
50
|
+
# @override
|
51
|
+
#: (Prism::Node?) -> void
|
52
|
+
def visit(node)
|
53
|
+
return if node.nil?
|
54
|
+
|
55
|
+
node.location.trailing_comments.each do |comment|
|
56
|
+
text = comment.slice.strip
|
57
|
+
next unless text.start_with?("#:")
|
58
|
+
|
59
|
+
@counters.increment("rbs_assertions")
|
60
|
+
|
61
|
+
case text
|
62
|
+
when /^#: as !nil/
|
63
|
+
@counters.increment("rbs_must")
|
64
|
+
when /^#: as untyped/
|
65
|
+
@counters.increment("rbs_unsafe")
|
66
|
+
when /^#: as/
|
67
|
+
@counters.increment("rbs_cast")
|
68
|
+
when /^#:/
|
69
|
+
@counters.increment("rbs_let")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
super
|
74
|
+
end
|
75
|
+
|
76
|
+
# @override
|
77
|
+
#: (Prism::ClassNode) -> void
|
78
|
+
def visit_class_node(node)
|
79
|
+
visit_scope(node) do
|
80
|
+
super
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# @override
|
85
|
+
#: (Prism::ModuleNode) -> void
|
86
|
+
def visit_module_node(node)
|
87
|
+
visit_scope(node) do
|
88
|
+
super
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# @override
|
93
|
+
#: (Prism::SingletonClassNode) -> void
|
94
|
+
def visit_singleton_class_node(node)
|
95
|
+
visit_scope(node) do
|
96
|
+
super
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# @override
|
101
|
+
#: (Prism::DefNode) -> void
|
102
|
+
def visit_def_node(node)
|
103
|
+
unless node.name.to_s.start_with?("test_")
|
104
|
+
@counters.increment("methods")
|
105
|
+
|
106
|
+
rbs_sigs = node_rbs_comments(node).signatures
|
107
|
+
srb_sigs = collect_last_srb_sigs
|
108
|
+
|
109
|
+
if rbs_sigs.any?
|
110
|
+
@counters.increment("methods_with_rbs_sig")
|
111
|
+
end
|
112
|
+
|
113
|
+
if srb_sigs.any?
|
114
|
+
@counters.increment("methods_with_srb_sig")
|
115
|
+
end
|
116
|
+
|
117
|
+
if rbs_sigs.empty? && srb_sigs.empty?
|
118
|
+
@counters.increment("methods_without_sig")
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
super
|
123
|
+
end
|
124
|
+
|
125
|
+
# @override
|
126
|
+
#: (Prism::CallNode) -> void
|
127
|
+
def visit_call_node(node)
|
128
|
+
@counters.increment("calls")
|
129
|
+
|
130
|
+
case node.name
|
131
|
+
when :attr_accessor, :attr_reader, :attr_writer
|
132
|
+
visit_attr_accessor(node)
|
133
|
+
return
|
134
|
+
when :sig
|
135
|
+
visit_sig(node)
|
136
|
+
return
|
137
|
+
when :type_member, :type_template
|
138
|
+
visit_type_member(node)
|
139
|
+
return
|
140
|
+
end
|
141
|
+
|
142
|
+
case node.receiver&.slice
|
143
|
+
when /^(::)?T$/
|
144
|
+
@counters.increment("T_calls")
|
145
|
+
@counters.increment("T.#{node.name}")
|
146
|
+
end
|
147
|
+
|
148
|
+
super
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
152
|
+
|
153
|
+
#: (Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode) { -> void } -> void
|
154
|
+
def visit_scope(node, &block)
|
155
|
+
key = node_key(node)
|
156
|
+
@counters.increment(key)
|
157
|
+
@counters.increment("#{key}_with_rbs_type_params") if node_rbs_comments(node).signatures.any?
|
158
|
+
|
159
|
+
old_type_params = @type_params
|
160
|
+
@type_params = []
|
161
|
+
|
162
|
+
yield
|
163
|
+
|
164
|
+
@counters.increment("#{key}_with_srb_type_params") if @type_params.any?
|
165
|
+
|
166
|
+
@type_params = old_type_params
|
167
|
+
end
|
168
|
+
|
169
|
+
#: (Prism::CallNode) -> void
|
170
|
+
def visit_attr_accessor(node)
|
171
|
+
@counters.increment("accessors")
|
172
|
+
|
173
|
+
rbs_sigs = node_rbs_comments(node).signatures
|
174
|
+
srb_sigs = collect_last_srb_sigs
|
175
|
+
|
176
|
+
if rbs_sigs.any?
|
177
|
+
@counters.increment("accessors_with_rbs_sig")
|
178
|
+
end
|
179
|
+
|
180
|
+
if srb_sigs.any?
|
181
|
+
@counters.increment("accessors_with_srb_sig")
|
182
|
+
end
|
183
|
+
|
184
|
+
if rbs_sigs.empty? && srb_sigs.empty?
|
185
|
+
@counters.increment("accessors_without_sig")
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
#: (Prism::CallNode) -> void
|
190
|
+
def visit_sig(node)
|
191
|
+
@last_sigs << node
|
192
|
+
@counters.increment("srb_sigs")
|
193
|
+
|
194
|
+
if node.slice =~ /abstract/
|
195
|
+
@counters.increment("srb_sigs_abstract")
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
#: (Prism::CallNode) -> void
|
200
|
+
def visit_type_member(node)
|
201
|
+
key = case node.name
|
202
|
+
when :type_member
|
203
|
+
"type_members"
|
204
|
+
when :type_template
|
205
|
+
"type_templates"
|
206
|
+
else
|
207
|
+
return
|
208
|
+
end
|
209
|
+
|
210
|
+
@counters.increment(key)
|
211
|
+
|
212
|
+
@type_params << node
|
213
|
+
end
|
214
|
+
|
215
|
+
#: -> Array[Prism::CallNode]
|
216
|
+
def collect_last_srb_sigs
|
217
|
+
sigs = @last_sigs.dup
|
218
|
+
@last_sigs.clear
|
219
|
+
sigs
|
220
|
+
end
|
221
|
+
|
222
|
+
#: (Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode) -> String
|
223
|
+
def node_key(node)
|
224
|
+
case node
|
225
|
+
when Prism::ClassNode
|
226
|
+
"classes"
|
227
|
+
when Prism::ModuleNode
|
228
|
+
"modules"
|
229
|
+
when Prism::SingletonClassNode
|
230
|
+
"singleton_classes"
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "spoom/sorbet/sigils"
|
5
|
+
|
6
|
+
module Spoom
|
7
|
+
module Sorbet
|
8
|
+
module Metrics
|
9
|
+
module MetricsFileParser
|
10
|
+
DEFAULT_PREFIX = "ruby_typer.unknown."
|
11
|
+
|
12
|
+
class << self
|
13
|
+
#: (String path, ?String prefix) -> Hash[String, Integer]
|
14
|
+
def parse_file(path, prefix = DEFAULT_PREFIX)
|
15
|
+
parse_string(File.read(path), prefix)
|
16
|
+
end
|
17
|
+
|
18
|
+
#: (String string, ?String prefix) -> Hash[String, Integer]
|
19
|
+
def parse_string(string, prefix = DEFAULT_PREFIX)
|
20
|
+
parse_hash(JSON.parse(string), prefix)
|
21
|
+
end
|
22
|
+
|
23
|
+
#: (Hash[String, untyped] obj, ?String prefix) -> Counters
|
24
|
+
def parse_hash(obj, prefix = DEFAULT_PREFIX)
|
25
|
+
obj["metrics"].each_with_object(Counters.new) do |metric, metrics|
|
26
|
+
name = metric["name"].sub(prefix, "")
|
27
|
+
metrics[name] = metric["value"] || 0
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/spoom/sorbet/metrics.rb
CHANGED
@@ -1,35 +1,5 @@
|
|
1
1
|
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
module Spoom
|
7
|
-
module Sorbet
|
8
|
-
module MetricsParser
|
9
|
-
DEFAULT_PREFIX = "ruby_typer.unknown."
|
10
|
-
|
11
|
-
class << self
|
12
|
-
extend T::Sig
|
13
|
-
|
14
|
-
sig { params(path: String, prefix: String).returns(T::Hash[String, Integer]) }
|
15
|
-
def parse_file(path, prefix = DEFAULT_PREFIX)
|
16
|
-
parse_string(File.read(path), prefix)
|
17
|
-
end
|
18
|
-
|
19
|
-
sig { params(string: String, prefix: String).returns(T::Hash[String, Integer]) }
|
20
|
-
def parse_string(string, prefix = DEFAULT_PREFIX)
|
21
|
-
parse_hash(JSON.parse(string), prefix)
|
22
|
-
end
|
23
|
-
|
24
|
-
sig { params(obj: T::Hash[String, T.untyped], prefix: String).returns(T::Hash[String, Integer]) }
|
25
|
-
def parse_hash(obj, prefix = DEFAULT_PREFIX)
|
26
|
-
obj["metrics"].each_with_object(Hash.new(0)) do |metric, metrics|
|
27
|
-
name = metric["name"]
|
28
|
-
name = name.sub(prefix, "")
|
29
|
-
metrics[name] = metric["value"] || 0
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
4
|
+
require "spoom/sorbet/metrics/code_metrics_visitor"
|
5
|
+
require "spoom/sorbet/metrics/metrics_file_parser"
|
data/lib/spoom/sorbet/sigils.rb
CHANGED
@@ -7,8 +7,6 @@
|
|
7
7
|
module Spoom
|
8
8
|
module Sorbet
|
9
9
|
module Sigils
|
10
|
-
extend T::Sig
|
11
|
-
|
12
10
|
STRICTNESS_IGNORE = "ignore"
|
13
11
|
STRICTNESS_FALSE = "false"
|
14
12
|
STRICTNESS_TRUE = "true"
|
@@ -16,50 +14,45 @@ module Spoom
|
|
16
14
|
STRICTNESS_STRONG = "strong"
|
17
15
|
STRICTNESS_INTERNAL = "__STDLIB_INTERNAL"
|
18
16
|
|
19
|
-
VALID_STRICTNESS =
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
].freeze,
|
28
|
-
T::Array[String],
|
29
|
-
)
|
17
|
+
VALID_STRICTNESS = [
|
18
|
+
STRICTNESS_IGNORE,
|
19
|
+
STRICTNESS_FALSE,
|
20
|
+
STRICTNESS_TRUE,
|
21
|
+
STRICTNESS_STRICT,
|
22
|
+
STRICTNESS_STRONG,
|
23
|
+
STRICTNESS_INTERNAL,
|
24
|
+
].freeze #: Array[String]
|
30
25
|
|
31
|
-
SIGIL_REGEXP =
|
26
|
+
SIGIL_REGEXP = /^#[[:blank:]]*typed:[[:blank:]]*(\S*)/ #: Regexp
|
32
27
|
|
33
28
|
class << self
|
34
|
-
extend T::Sig
|
35
|
-
|
36
29
|
# returns the full sigil comment string for the passed strictness
|
37
|
-
|
30
|
+
#: (String strictness) -> String
|
38
31
|
def sigil_string(strictness)
|
39
32
|
"# typed: #{strictness}"
|
40
33
|
end
|
41
34
|
|
42
35
|
# returns true if the passed string is a valid strictness (else false)
|
43
|
-
|
36
|
+
#: (String strictness) -> bool
|
44
37
|
def valid_strictness?(strictness)
|
45
38
|
VALID_STRICTNESS.include?(strictness.strip)
|
46
39
|
end
|
47
40
|
|
48
41
|
# returns the strictness of a sigil in the passed file content string (nil if no sigil)
|
49
|
-
|
42
|
+
#: (String content) -> String?
|
50
43
|
def strictness_in_content(content)
|
51
44
|
SIGIL_REGEXP.match(content)&.[](1)
|
52
45
|
end
|
53
46
|
|
54
47
|
# returns a string which is the passed content but with the sigil updated to a new strictness
|
55
|
-
|
48
|
+
#: (String content, String new_strictness) -> String
|
56
49
|
def update_sigil(content, new_strictness)
|
57
50
|
content.sub(SIGIL_REGEXP, sigil_string(new_strictness))
|
58
51
|
end
|
59
52
|
|
60
53
|
# returns a string containing the strictness of a sigil in a file at the passed path
|
61
54
|
# * returns nil if no sigil
|
62
|
-
|
55
|
+
#: ((String | Pathname) path) -> String?
|
63
56
|
def file_strictness(path)
|
64
57
|
return unless File.file?(path)
|
65
58
|
|
@@ -68,7 +61,7 @@ module Spoom
|
|
68
61
|
end
|
69
62
|
|
70
63
|
# changes the sigil in the file at the passed path to the specified new strictness
|
71
|
-
|
64
|
+
#: ((String | Pathname) path, String new_strictness) -> bool
|
72
65
|
def change_sigil_in_file(path, new_strictness)
|
73
66
|
content = File.read(path, encoding: Encoding::ASCII_8BIT)
|
74
67
|
new_content = update_sigil(content, new_strictness)
|
@@ -79,7 +72,7 @@ module Spoom
|
|
79
72
|
end
|
80
73
|
|
81
74
|
# changes the sigil to have a new strictness in a list of files
|
82
|
-
|
75
|
+
#: (Array[String] path_list, String new_strictness) -> Array[String]
|
83
76
|
def change_sigil_in_files(path_list, new_strictness)
|
84
77
|
path_list.filter do |path|
|
85
78
|
change_sigil_in_file(path, new_strictness)
|
@@ -0,0 +1,242 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Spoom
|
5
|
+
module Sorbet
|
6
|
+
module Translate
|
7
|
+
class RBSCommentsToSorbetSigs < Translator
|
8
|
+
include Spoom::RBS::ExtractRBSComments
|
9
|
+
|
10
|
+
# @override
|
11
|
+
#: (Prism::ClassNode node) -> void
|
12
|
+
def visit_class_node(node)
|
13
|
+
apply_class_annotations(node)
|
14
|
+
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
# @override
|
19
|
+
#: (Prism::ModuleNode node) -> void
|
20
|
+
def visit_module_node(node)
|
21
|
+
apply_class_annotations(node)
|
22
|
+
|
23
|
+
super
|
24
|
+
end
|
25
|
+
|
26
|
+
# @override
|
27
|
+
#: (Prism::SingletonClassNode node) -> void
|
28
|
+
def visit_singleton_class_node(node)
|
29
|
+
apply_class_annotations(node)
|
30
|
+
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
# @override
|
35
|
+
#: (Prism::DefNode node) -> void
|
36
|
+
def visit_def_node(node)
|
37
|
+
comments = node_rbs_comments(node)
|
38
|
+
return if comments.empty?
|
39
|
+
|
40
|
+
return if comments.signatures.empty?
|
41
|
+
|
42
|
+
builder = RBI::Parser::TreeBuilder.new(@ruby_contents, comments: [], file: @file)
|
43
|
+
builder.visit(node)
|
44
|
+
rbi_node = builder.tree.nodes.first #: as RBI::Method
|
45
|
+
|
46
|
+
comments.signatures.each do |signature|
|
47
|
+
method_type = ::RBS::Parser.parse_method_type(signature.string)
|
48
|
+
translator = RBI::RBS::MethodTypeTranslator.new(rbi_node)
|
49
|
+
translator.visit(method_type)
|
50
|
+
sig = translator.result
|
51
|
+
apply_member_annotations(comments.annotations, sig)
|
52
|
+
|
53
|
+
@rewriter << Source::Replace.new(
|
54
|
+
signature.location.start_offset,
|
55
|
+
signature.location.end_offset,
|
56
|
+
sig.string,
|
57
|
+
)
|
58
|
+
rescue ::RBS::ParsingError
|
59
|
+
# Ignore signatures with errors
|
60
|
+
next
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# @override
|
65
|
+
#: (Prism::CallNode node) -> void
|
66
|
+
def visit_call_node(node)
|
67
|
+
case node.message
|
68
|
+
when "attr_reader", "attr_writer", "attr_accessor"
|
69
|
+
visit_attr(node)
|
70
|
+
else
|
71
|
+
super
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
#: (Prism::CallNode) -> void
|
78
|
+
def visit_attr(node)
|
79
|
+
comments = node_rbs_comments(node)
|
80
|
+
return if comments.empty?
|
81
|
+
|
82
|
+
return if comments.signatures.empty?
|
83
|
+
|
84
|
+
comments.signatures.each do |signature|
|
85
|
+
attr_type = ::RBS::Parser.parse_type(signature.string)
|
86
|
+
sig = RBI::Sig.new
|
87
|
+
|
88
|
+
if node.message == "attr_writer"
|
89
|
+
if node.arguments&.arguments&.size != 1
|
90
|
+
raise Error, "AttrWriter must have exactly one name"
|
91
|
+
end
|
92
|
+
|
93
|
+
name = node.arguments&.arguments&.first #: as Prism::SymbolNode
|
94
|
+
sig.params << RBI::SigParam.new(
|
95
|
+
name.slice[1..-1], #: as String
|
96
|
+
RBI::RBS::TypeTranslator.translate(attr_type),
|
97
|
+
)
|
98
|
+
end
|
99
|
+
|
100
|
+
sig.return_type = RBI::RBS::TypeTranslator.translate(attr_type)
|
101
|
+
|
102
|
+
apply_member_annotations(comments.annotations, sig)
|
103
|
+
|
104
|
+
@rewriter << Source::Replace.new(
|
105
|
+
signature.location.start_offset,
|
106
|
+
signature.location.end_offset,
|
107
|
+
sig.string,
|
108
|
+
)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
#: (Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode) -> void
|
113
|
+
def apply_class_annotations(node)
|
114
|
+
comments = node_rbs_comments(node)
|
115
|
+
return if comments.empty?
|
116
|
+
|
117
|
+
indent = " " * (node.location.start_column + 2)
|
118
|
+
insert_pos = case node
|
119
|
+
when Prism::ClassNode
|
120
|
+
(node.superclass || node.constant_path).location.end_offset
|
121
|
+
when Prism::ModuleNode
|
122
|
+
node.constant_path.location.end_offset
|
123
|
+
when Prism::SingletonClassNode
|
124
|
+
node.expression.location.end_offset
|
125
|
+
end
|
126
|
+
|
127
|
+
if comments.annotations.any?
|
128
|
+
unless already_extends?(node, /^(::)?T::Helpers$/)
|
129
|
+
@rewriter << Source::Insert.new(insert_pos, "\n#{indent}extend T::Helpers\n")
|
130
|
+
end
|
131
|
+
|
132
|
+
comments.annotations.reverse_each do |annotation|
|
133
|
+
from = adjust_to_line_start(annotation.location.start_offset)
|
134
|
+
to = adjust_to_line_end(annotation.location.end_offset)
|
135
|
+
|
136
|
+
content = case annotation.string
|
137
|
+
when "@abstract"
|
138
|
+
"abstract!"
|
139
|
+
when "@interface"
|
140
|
+
"interface!"
|
141
|
+
when "@sealed"
|
142
|
+
"sealed!"
|
143
|
+
when "@final"
|
144
|
+
"final!"
|
145
|
+
when /^@requires_ancestor: /
|
146
|
+
srb_type = ::RBS::Parser.parse_type(annotation.string.delete_prefix("@requires_ancestor: "))
|
147
|
+
rbs_type = RBI::RBS::TypeTranslator.translate(srb_type)
|
148
|
+
"requires_ancestor { #{rbs_type} }"
|
149
|
+
else
|
150
|
+
next
|
151
|
+
end
|
152
|
+
|
153
|
+
@rewriter << Source::Delete.new(from, to)
|
154
|
+
|
155
|
+
newline = node.body.nil? ? "" : "\n"
|
156
|
+
@rewriter << Source::Insert.new(insert_pos, "\n#{indent}#{content}#{newline}")
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
signatures = comments.signatures
|
161
|
+
if signatures.any?
|
162
|
+
signatures.each do |signature|
|
163
|
+
type_params = ::RBS::Parser.parse_type_params(signature.string)
|
164
|
+
next if type_params.empty?
|
165
|
+
|
166
|
+
from = adjust_to_line_start(signature.location.start_offset)
|
167
|
+
to = adjust_to_line_end(signature.location.end_offset)
|
168
|
+
@rewriter << Source::Delete.new(from, to)
|
169
|
+
|
170
|
+
unless already_extends?(node, /^(::)?T::Generic$/)
|
171
|
+
@rewriter << Source::Insert.new(insert_pos, "\n#{indent}extend T::Generic\n")
|
172
|
+
end
|
173
|
+
|
174
|
+
type_params.each do |type_param|
|
175
|
+
type_member = "#{type_param.name} = type_member"
|
176
|
+
|
177
|
+
case type_param.variance
|
178
|
+
when :covariant
|
179
|
+
type_member = "#{type_member}(:out)"
|
180
|
+
when :contravariant
|
181
|
+
type_member = "#{type_member}(:in)"
|
182
|
+
end
|
183
|
+
|
184
|
+
if type_param.upper_bound || type_param.default_type
|
185
|
+
if type_param.upper_bound
|
186
|
+
rbs_type = RBI::RBS::TypeTranslator.translate(type_param.upper_bound)
|
187
|
+
type_member = "#{type_member} {{ upper: #{rbs_type} }}"
|
188
|
+
end
|
189
|
+
|
190
|
+
if type_param.default_type
|
191
|
+
rbs_type = RBI::RBS::TypeTranslator.translate(type_param.default_type)
|
192
|
+
type_member = "#{type_member} {{ fixed: #{rbs_type} }}"
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
newline = node.body.nil? ? "" : "\n"
|
197
|
+
@rewriter << Source::Insert.new(insert_pos, "\n#{indent}#{type_member}#{newline}")
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
#: (Array[RBS::Annotations], RBI::Sig) -> void
|
204
|
+
def apply_member_annotations(annotations, sig)
|
205
|
+
annotations.each do |annotation|
|
206
|
+
case annotation.string
|
207
|
+
when "@abstract"
|
208
|
+
sig.is_abstract = true
|
209
|
+
when "@final"
|
210
|
+
sig.is_final = true
|
211
|
+
when "@override"
|
212
|
+
sig.is_override = true
|
213
|
+
when "@override(allow_incompatible: true)"
|
214
|
+
sig.is_override = true
|
215
|
+
sig.allow_incompatible_override = true
|
216
|
+
when "@overridable"
|
217
|
+
sig.is_overridable = true
|
218
|
+
when "@without_runtime"
|
219
|
+
sig.without_runtime = true
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
#: (Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode, Regexp) -> bool
|
225
|
+
def already_extends?(node, constant_regex)
|
226
|
+
node.child_nodes.any? do |c|
|
227
|
+
next false unless c.is_a?(Prism::CallNode)
|
228
|
+
next false unless c.message == "extend"
|
229
|
+
next false unless c.receiver.nil? || c.receiver.is_a?(Prism::SelfNode)
|
230
|
+
next false unless c.arguments&.arguments&.size == 1
|
231
|
+
|
232
|
+
arg = c.arguments&.arguments&.first
|
233
|
+
next false unless arg.is_a?(Prism::ConstantPathNode)
|
234
|
+
next false unless arg.slice.match?(constant_regex)
|
235
|
+
|
236
|
+
true
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|