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.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -0
  3. data/lib/spoom/backtrace_filter/minitest.rb +3 -4
  4. data/lib/spoom/cli/deadcode.rb +1 -2
  5. data/lib/spoom/cli/helper.rb +41 -31
  6. data/lib/spoom/cli/srb/assertions.rb +48 -0
  7. data/lib/spoom/cli/srb/bump.rb +1 -2
  8. data/lib/spoom/cli/srb/coverage.rb +1 -1
  9. data/lib/spoom/cli/srb/metrics.rb +68 -0
  10. data/lib/spoom/cli/srb/sigs.rb +209 -0
  11. data/lib/spoom/cli/srb/tc.rb +16 -1
  12. data/lib/spoom/cli/srb.rb +16 -4
  13. data/lib/spoom/cli.rb +1 -2
  14. data/lib/spoom/colors.rb +2 -6
  15. data/lib/spoom/context/bundle.rb +8 -9
  16. data/lib/spoom/context/exec.rb +3 -6
  17. data/lib/spoom/context/file_system.rb +12 -19
  18. data/lib/spoom/context/git.rb +14 -19
  19. data/lib/spoom/context/sorbet.rb +14 -27
  20. data/lib/spoom/context.rb +4 -8
  21. data/lib/spoom/counters.rb +22 -0
  22. data/lib/spoom/coverage/d3/base.rb +6 -8
  23. data/lib/spoom/coverage/d3/circle_map.rb +6 -16
  24. data/lib/spoom/coverage/d3/pie.rb +14 -19
  25. data/lib/spoom/coverage/d3/timeline.rb +46 -47
  26. data/lib/spoom/coverage/d3.rb +2 -4
  27. data/lib/spoom/coverage/report.rb +41 -79
  28. data/lib/spoom/coverage/snapshot.rb +8 -14
  29. data/lib/spoom/coverage.rb +3 -5
  30. data/lib/spoom/deadcode/definition.rb +12 -14
  31. data/lib/spoom/deadcode/erb.rb +10 -8
  32. data/lib/spoom/deadcode/index.rb +21 -25
  33. data/lib/spoom/deadcode/indexer.rb +5 -6
  34. data/lib/spoom/deadcode/plugins/action_mailer.rb +2 -3
  35. data/lib/spoom/deadcode/plugins/action_mailer_preview.rb +2 -3
  36. data/lib/spoom/deadcode/plugins/actionpack.rb +19 -22
  37. data/lib/spoom/deadcode/plugins/active_model.rb +2 -3
  38. data/lib/spoom/deadcode/plugins/active_record.rb +62 -53
  39. data/lib/spoom/deadcode/plugins/active_support.rb +3 -2
  40. data/lib/spoom/deadcode/plugins/base.rb +29 -32
  41. data/lib/spoom/deadcode/plugins/graphql.rb +2 -3
  42. data/lib/spoom/deadcode/plugins/minitest.rb +4 -4
  43. data/lib/spoom/deadcode/plugins/namespaces.rb +5 -5
  44. data/lib/spoom/deadcode/plugins/rails.rb +5 -5
  45. data/lib/spoom/deadcode/plugins/rubocop.rb +5 -5
  46. data/lib/spoom/deadcode/plugins/ruby.rb +3 -4
  47. data/lib/spoom/deadcode/plugins/sorbet.rb +12 -6
  48. data/lib/spoom/deadcode/plugins/thor.rb +2 -3
  49. data/lib/spoom/deadcode/plugins.rb +23 -31
  50. data/lib/spoom/deadcode/remover.rb +58 -79
  51. data/lib/spoom/deadcode/send.rb +2 -8
  52. data/lib/spoom/file_collector.rb +11 -19
  53. data/lib/spoom/file_tree.rb +36 -51
  54. data/lib/spoom/location.rb +9 -20
  55. data/lib/spoom/model/builder.rb +54 -17
  56. data/lib/spoom/model/model.rb +71 -74
  57. data/lib/spoom/model/namespace_visitor.rb +4 -3
  58. data/lib/spoom/model/reference.rb +4 -8
  59. data/lib/spoom/model/references_visitor.rb +50 -30
  60. data/lib/spoom/parse.rb +4 -4
  61. data/lib/spoom/poset.rb +22 -24
  62. data/lib/spoom/printer.rb +10 -13
  63. data/lib/spoom/rbs.rb +77 -0
  64. data/lib/spoom/sorbet/config.rb +17 -24
  65. data/lib/spoom/sorbet/errors.rb +87 -45
  66. data/lib/spoom/sorbet/lsp/base.rb +10 -16
  67. data/lib/spoom/sorbet/lsp/errors.rb +8 -16
  68. data/lib/spoom/sorbet/lsp/structures.rb +65 -91
  69. data/lib/spoom/sorbet/lsp.rb +20 -22
  70. data/lib/spoom/sorbet/metrics/code_metrics_visitor.rb +236 -0
  71. data/lib/spoom/sorbet/metrics/metrics_file_parser.rb +34 -0
  72. data/lib/spoom/sorbet/metrics.rb +2 -32
  73. data/lib/spoom/sorbet/sigils.rb +16 -23
  74. data/lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs.rb +242 -0
  75. data/lib/spoom/sorbet/translate/sorbet_assertions_to_rbs_comments.rb +123 -0
  76. data/lib/spoom/sorbet/translate/sorbet_sigs_to_rbs_comments.rb +293 -0
  77. data/lib/spoom/sorbet/translate/strip_sorbet_sigs.rb +23 -0
  78. data/lib/spoom/sorbet/translate/translator.rb +71 -0
  79. data/lib/spoom/sorbet/translate.rb +49 -0
  80. data/lib/spoom/sorbet.rb +6 -12
  81. data/lib/spoom/source/rewriter.rb +167 -0
  82. data/lib/spoom/source.rb +4 -0
  83. data/lib/spoom/timeline.rb +4 -6
  84. data/lib/spoom/version.rb +1 -1
  85. data/lib/spoom/visitor.rb +298 -151
  86. data/lib/spoom.rb +4 -3
  87. data/rbi/spoom.rbi +3567 -0
  88. 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
@@ -1,35 +1,5 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require_relative "sigils"
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"
@@ -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 = T.let(
20
- [
21
- STRICTNESS_IGNORE,
22
- STRICTNESS_FALSE,
23
- STRICTNESS_TRUE,
24
- STRICTNESS_STRICT,
25
- STRICTNESS_STRONG,
26
- STRICTNESS_INTERNAL,
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 = T.let(/^#[[:blank:]]*typed:[[:blank:]]*(\S*)/, 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
- sig { params(strictness: String).returns(String) }
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
- sig { params(strictness: String).returns(T::Boolean) }
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
- sig { params(content: String).returns(T.nilable(String)) }
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
- sig { params(content: String, new_strictness: String).returns(String) }
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
- sig { params(path: T.any(String, Pathname)).returns(T.nilable(String)) }
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
- sig { params(path: T.any(String, Pathname), new_strictness: String).returns(T::Boolean) }
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
- sig { params(path_list: T::Array[String], new_strictness: String).returns(T::Array[String]) }
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