spoom 1.8.1 → 1.8.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0cbd6c18447795a6c80c58456353edf11b7a591fb745a16b69b85076dbdb283b
4
- data.tar.gz: ffb8f40253e1496b180aa6cee5d4d120567f95698b7e61f2e2992a3cfd978da8
3
+ metadata.gz: a2abc815d0eba3a4297d4ee83c8aeb6cfa8e70bcd734785e6db51e109692cb5b
4
+ data.tar.gz: 7c27a7d39af33be7c307cc1055ee8ad1930a3781a32fd6e44de50e936fc84ac2
5
5
  SHA512:
6
- metadata.gz: fab5f10f8233cec7793b019d427a483f8f0f71dcce16f8661cfb595388fbda72dac8d99fa3f068c4dd6aa24c16c4330bc4111108436c79d294d963b99ae88bee
7
- data.tar.gz: f896ed13b17abd4e483a0a3334d0add82e199cc3c68194932b1c6ae04cfd6f6ce0a48dae475e49cfe86a3e9fe1f61a7b17aeb07f3acb040da9585e15d6e155db
6
+ metadata.gz: 063b6a30843d0a49533a0ae4c797370bd685e71f82887e0ed28d471a95944ff88e45602078a9b847c284cc4a447f7578c98c364b9340f4e06318701517aeb93d
7
+ data.tar.gz: fdcdc43a27d19ec879deb78fc9ad5fe817283444a35d8a3bc2ce027d3f3e9a704dd3780ef45138e062b7c6d83fb40f9c5291510a77dc90bf0f440c109091b927
@@ -3,6 +3,8 @@
3
3
 
4
4
  require "find"
5
5
  require "open3"
6
+ require "tempfile"
7
+ require "yaml"
6
8
 
7
9
  module Spoom
8
10
  module Cli
@@ -72,6 +74,11 @@ module Spoom
72
74
  exit(1)
73
75
  end
74
76
 
77
+ unless context.sorbet_config.typed_overrides.empty?
78
+ say_error("Cannot run `spoom bump` on a project that already uses Sorbet's `--typed-override` option")
79
+ exit(1)
80
+ end
81
+
75
82
  say("Checking files...")
76
83
 
77
84
  files_to_bump = context.srb_files_with_strictness(from, include_rbis: false)
@@ -90,18 +97,18 @@ module Spoom
90
97
  exit(0)
91
98
  end
92
99
 
93
- Sorbet::Sigils.change_sigil_in_files(files_to_bump, to)
94
-
95
100
  if force
101
+ Sorbet::Sigils.change_sigil_in_files(files_to_bump, to) unless dry
96
102
  print_changes(files_to_bump, command: cmd, from: from, to: to, dry: dry, path: exec_path)
97
- undo_changes(files_to_bump, from) if dry
98
103
  exit(files_to_bump.empty?)
99
104
  end
100
105
 
101
106
  error_url_base = Spoom::Sorbet::Errors::DEFAULT_ERROR_URL_BASE
107
+ typed_override_file = create_typed_override_file(context, files_to_bump, to)
102
108
  result = begin
103
109
  T.unsafe(context).srb_tc(
104
110
  *options[:sorbet_options].split(" "),
111
+ "--typed-override=#{T.must(typed_override_file.path)}",
105
112
  "--error-url-base=#{error_url_base}",
106
113
  capture_err: true,
107
114
  sorbet_bin: options[:sorbet],
@@ -114,7 +121,6 @@ module Spoom
114
121
  It means one of the file bumped to `typed: #{to}` made Sorbet crash.
115
122
  Run `spoom bump -f` locally followed by `bundle exec srb tc` to investigate the problem.
116
123
  ERR
117
- undo_changes(files_to_bump, from)
118
124
  exit(error.result.exit_code)
119
125
  rescue Spoom::Sorbet::Error::Killed => error
120
126
  say_error(<<~ERR, status: nil)
@@ -123,13 +129,14 @@ module Spoom
123
129
  It means Sorbet was killed while executing. Changes to files have not been applied.
124
130
  Re-run `spoom bump` to try again.
125
131
  ERR
126
- undo_changes(files_to_bump, from)
127
132
  exit(error.result.exit_code)
133
+ ensure
134
+ typed_override_file.close!
128
135
  end
129
136
 
130
137
  if result.status
138
+ Sorbet::Sigils.change_sigil_in_files(files_to_bump, to) unless dry
131
139
  print_changes(files_to_bump, command: cmd, from: from, to: to, dry: dry, path: exec_path)
132
- undo_changes(files_to_bump, from) if dry
133
140
  exit(files_to_bump.empty?)
134
141
  end
135
142
 
@@ -137,7 +144,6 @@ module Spoom
137
144
  # Sorbet will return exit code 100 if there are type checking errors.
138
145
  # If Sorbet returned something else, it means it didn't terminate normally.
139
146
  say_error(result.err, status: nil, nl: false)
140
- undo_changes(files_to_bump, from)
141
147
  exit(1)
142
148
  end
143
149
 
@@ -156,13 +162,11 @@ module Spoom
156
162
  path
157
163
  end.compact.uniq
158
164
 
159
- undo_changes(files_with_errors, from)
160
-
161
165
  say("Found #{errors.length} type checking error#{"s" if errors.length > 1}") if options[:count_errors]
162
166
 
163
167
  files_changed = files_to_bump - files_with_errors
168
+ Sorbet::Sigils.change_sigil_in_files(files_changed, to) unless dry
164
169
  print_changes(files_changed, command: cmd, from: from, to: to, dry: dry, path: exec_path)
165
- undo_changes(files_to_bump, from) if dry
166
170
  exit(files_changed.empty?)
167
171
  end
168
172
 
@@ -189,8 +193,16 @@ module Spoom
189
193
  end
190
194
  end
191
195
 
192
- def undo_changes(files, from_strictness)
193
- Sorbet::Sigils.change_sigil_in_files(files, from_strictness)
196
+ #: (Spoom::Context context, Array[String] files, String strictness) -> Tempfile
197
+ def create_typed_override_file(context, files, strictness)
198
+ relative_paths = files.map do |file|
199
+ relative_path = file.delete_prefix("#{context.absolute_path}/")
200
+ "./#{relative_path}"
201
+ end
202
+ typed_override_file = Tempfile.new(["spoom-typed-override", ".yml"])
203
+ typed_override_file.write(YAML.dump({ strictness => relative_paths }))
204
+ typed_override_file.flush
205
+ typed_override_file
194
206
  end
195
207
  end
196
208
  end
@@ -48,8 +48,10 @@ module Spoom
48
48
 
49
49
  node = @node_context.node
50
50
  case node
51
- when Prism::ClassNode, Prism::ModuleNode, Prism::DefNode
51
+ when Prism::ClassNode, Prism::ModuleNode
52
52
  delete_node_and_comments_and_sigs(@node_context)
53
+ when Prism::DefNode
54
+ delete_node_and_comments_and_sigs(modifier_call_context(node) || @node_context)
53
55
  when Prism::ConstantWriteNode, Prism::ConstantOperatorWriteNode,
54
56
  Prism::ConstantAndWriteNode, Prism::ConstantOrWriteNode,
55
57
  Prism::ConstantPathWriteNode, Prism::ConstantPathOperatorWriteNode,
@@ -65,6 +67,47 @@ module Spoom
65
67
 
66
68
  private
67
69
 
70
+ # When a method is defined as the argument of a modifier call (e.g. `private def foo; end` or
71
+ # `private_class_method def self.foo; end`), the `def` node's parent is the call rather than the
72
+ # enclosing class or module body, so its sigs and comments are siblings of the call. Return a
73
+ # context targeting the outermost such call so they are removed together with the method.
74
+ #
75
+ # Modifiers are matched structurally rather than from a fixed list, so user-defined ones are
76
+ # handled too. A call only counts as a modifier when the `def` is its sole argument and it takes
77
+ # no block and is a standalone statement, so we never remove a call that does more than wrap
78
+ # the method (e.g. `register(:thing, def foo; end)` or `FOO = register(def foo; end)`).
79
+ #: (Prism::DefNode def_node) -> NodeContext?
80
+ def modifier_call_context(def_node)
81
+ wrapped = def_node #: Prism::Node
82
+ nesting = @node_context.nesting.dup
83
+ outer_call = nil #: Prism::CallNode?
84
+ outer_nesting = nil #: Array[Prism::Node]?
85
+
86
+ while (ancestor = nesting.pop)
87
+ case ancestor
88
+ when Prism::ArgumentsNode
89
+ # An arguments node sits between a call and its arguments, keep climbing
90
+ next
91
+ when Prism::CallNode
92
+ # Stop unless the call wraps the node as its sole argument and takes no block, otherwise
93
+ # it does more than wrap the method and we must not remove it
94
+ args = ancestor.arguments&.arguments
95
+ break if ancestor.block
96
+ break unless args && args.size == 1 && args.first.equal?(wrapped)
97
+
98
+ wrapped = ancestor
99
+ outer_call = ancestor
100
+ outer_nesting = nesting.dup
101
+ else
102
+ break
103
+ end
104
+ end
105
+ return unless outer_call && outer_nesting
106
+ return unless outer_nesting.last.is_a?(Prism::StatementsNode)
107
+
108
+ NodeContext.new(@old_source, @node_context.comments, outer_call, outer_nesting)
109
+ end
110
+
68
111
  #: (NodeContext context) -> void
69
112
  def delete_constant_assignment(context)
70
113
  case context.node
data/lib/spoom/rbs.rb CHANGED
@@ -68,7 +68,12 @@ module Spoom
68
68
  end
69
69
  end
70
70
 
71
- class Annotation < Comment; end
71
+ class Annotation < Comment
72
+ #: () -> bool
73
+ def abstract?
74
+ @string == "@abstract"
75
+ end
76
+ end
72
77
 
73
78
  class Signature < Comment
74
79
  # Locations of the `#|` continuation comment lines that make up a multiline signature,
@@ -27,7 +27,7 @@ module Spoom
27
27
  DEFAULT_ALLOWED_EXTENSIONS = [".rb", ".rbi"].freeze #: Array[String]
28
28
 
29
29
  #: Array[String]
30
- attr_accessor :paths, :ignore, :allowed_extensions
30
+ attr_accessor :paths, :ignore, :allowed_extensions, :typed_overrides
31
31
 
32
32
  #: bool
33
33
  attr_accessor :no_stdlib
@@ -37,6 +37,7 @@ module Spoom
37
37
  @paths = [] #: Array[String]
38
38
  @ignore = [] #: Array[String]
39
39
  @allowed_extensions = [] #: Array[String]
40
+ @typed_overrides = [] #: Array[String]
40
41
  @no_stdlib = false #: bool
41
42
  end
42
43
 
@@ -46,6 +47,7 @@ module Spoom
46
47
  @paths = @paths.dup
47
48
  @ignore = @ignore.dup
48
49
  @allowed_extensions = @allowed_extensions.dup
50
+ @typed_overrides = @typed_overrides.dup
49
51
  end
50
52
 
51
53
  # Returns self as a string of options that can be passed to Sorbet
@@ -66,6 +68,7 @@ module Spoom
66
68
  opts.concat(paths.map { |p| "'#{p}'" })
67
69
  opts.concat(ignore.map { |p| "--ignore '#{p}'" })
68
70
  opts.concat(allowed_extensions.map { |ext| "--allowed-extension '#{ext}'" })
71
+ opts.concat(typed_overrides.map { |path| "--typed-override '#{path}'" })
69
72
  opts << "--no-stdlib" if @no_stdlib
70
73
  opts.join(" ")
71
74
  end
@@ -95,6 +98,12 @@ module Spoom
95
98
  when /^--ignore=/
96
99
  config.ignore << parse_option(line)
97
100
  next
101
+ when /^--typed-override$/
102
+ state = :typed_override
103
+ next
104
+ when /^--typed-override=/
105
+ config.typed_overrides << parse_option(line)
106
+ next
98
107
  when /^--file$/
99
108
  next
100
109
  when /^--file=/
@@ -124,6 +133,8 @@ module Spoom
124
133
  config.ignore << line
125
134
  when :extension
126
135
  config.allowed_extensions << line
136
+ when :typed_override
137
+ config.typed_overrides << line
127
138
  when :skip
128
139
  # nothing
129
140
  else
@@ -25,6 +25,7 @@ module Spoom
25
25
  end #: Integer?
26
26
 
27
27
  @overloads_strategy = options.overloads_strategy #: Symbol
28
+ @translate_abstract_methods = options.translate_abstract_methods #: bool
28
29
  @type_translator = RBI::RBS::TypeTranslator.new #: RBI::RBS::TypeTranslator
29
30
  end
30
31
 
@@ -141,6 +142,7 @@ module Spoom
141
142
  def rewrite_def(def_node, comments)
142
143
  return if comments.empty?
143
144
  return if comments.signatures.empty?
145
+ return if !@translate_abstract_methods && comments.method_annotations.any?(&:abstract?)
144
146
 
145
147
  signatures = apply_overloads_strategy(
146
148
  comments.signatures,
@@ -47,13 +47,18 @@ module Spoom
47
47
  #: BaseRBIFormat
48
48
  attr_reader :output_format
49
49
 
50
+ #: bool
51
+ attr_reader :translate_abstract_methods
52
+
50
53
  #: (
51
54
  #| ?overloads_strategy: Symbol,
52
55
  #| ?output_format: BaseRBIFormat,
56
+ #| ?translate_abstract_methods: bool,
53
57
  #| ) -> void
54
58
  def initialize(
55
59
  overloads_strategy: :translate_all,
56
- output_format: HumanReadableRBIFormat.default
60
+ output_format: HumanReadableRBIFormat.default,
61
+ translate_abstract_methods: true
57
62
  )
58
63
  unless ALLOWED_OVERLOAD_STRATEGIES.include?(overloads_strategy)
59
64
  raise ArgumentError, "Unknown overloads_strategy: #{overloads_strategy.inspect}. " \
@@ -62,6 +67,7 @@ module Spoom
62
67
 
63
68
  @overloads_strategy = overloads_strategy
64
69
  @output_format = output_format
70
+ @translate_abstract_methods = translate_abstract_methods
65
71
 
66
72
  freeze
67
73
  end
@@ -103,10 +103,12 @@ module Spoom
103
103
  # Otherwise, replace up to the end of the node
104
104
  end_offset = comment_end_offset || node.location.end_offset
105
105
 
106
+ heredoc_body = heredoc_body_within_range(value, end_offset)
107
+
106
108
  replacement = if node.name == :bind
107
109
  "#{rbs_annotation}#{trailing_comment}"
108
110
  else
109
- "#{dedent_value(node, value)} #{rbs_annotation}#{trailing_comment}"
111
+ "#{dedent_value(node, value)} #{rbs_annotation}#{trailing_comment}#{heredoc_body}"
110
112
  end
111
113
 
112
114
  @rewriter << Source::Replace.new(start_offset, end_offset - 1, replacement)
@@ -212,6 +214,53 @@ module Spoom
212
214
  [" #{range.pack("C*")}", end_offset]
213
215
  end
214
216
 
217
+ #: (Prism::Node, Integer) -> String?
218
+ def heredoc_body_within_range(node, replace_end_offset)
219
+ heredoc_end = heredoc_end_offsets(node)
220
+ .select { |offset| offset <= replace_end_offset }
221
+ .max
222
+ return unless heredoc_end
223
+
224
+ opener_line_end = adjust_to_line_end(node.location.end_offset)
225
+ body_bytes = @ruby_bytes[(opener_line_end + 1)...heredoc_end] #: as !nil
226
+ body = body_bytes.pack("C*")
227
+ body.chomp! if @ruby_bytes[replace_end_offset] == LINE_BREAK
228
+ "\n#{body}"
229
+ end
230
+
231
+ #: (Prism::Node) -> Array[Integer]
232
+ def heredoc_end_offsets(node)
233
+ offsets = [] #: Array[Integer]
234
+
235
+ case node
236
+ when Prism::StringNode, Prism::InterpolatedStringNode, Prism::XStringNode, Prism::InterpolatedXStringNode
237
+ opening = node.opening_loc
238
+ closing = node.closing_loc
239
+ if opening && closing && opening.start_line != closing.start_line && opening.slice.start_with?("<<")
240
+ offsets << closing.end_offset
241
+ end
242
+ end
243
+
244
+ node.child_nodes.each do |child|
245
+ next unless child
246
+
247
+ offsets.concat(heredoc_end_offsets(child))
248
+ end
249
+
250
+ offsets
251
+ end
252
+
253
+ #: (Prism::Node) -> bool
254
+ def string_literal?(node)
255
+ case node
256
+ when Prism::StringNode, Prism::InterpolatedStringNode,
257
+ Prism::XStringNode, Prism::InterpolatedXStringNode
258
+ true
259
+ else
260
+ false
261
+ end
262
+ end
263
+
215
264
  #: (Prism::Node, Prism::Node) -> String
216
265
  def dedent_value(assign, value)
217
266
  if value.location.start_line == assign.location.start_line
@@ -243,7 +292,7 @@ module Spoom
243
292
  # ```
244
293
  indent = value.location.start_line - assign.location.start_line
245
294
  lines = value.slice.lines
246
- if lines.size > 1
295
+ if lines.size > 1 && !string_literal?(value)
247
296
  lines[1..]&.each_with_index do |line, i|
248
297
  lines[i + 1] = line.delete_prefix(" " * indent)
249
298
  end
data/lib/spoom/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Spoom
5
- VERSION = "1.8.1"
5
+ VERSION = "1.8.3"
6
6
  end
data/rbi/spoom.rbi CHANGED
@@ -126,9 +126,11 @@ class Spoom::Cli::Srb::Bump < ::Thor
126
126
  sig { params(directory: ::String).void }
127
127
  def bump(directory = T.unsafe(nil)); end
128
128
 
129
+ sig { params(context: ::Spoom::Context, files: T::Array[::String], strictness: ::String).returns(::Tempfile) }
130
+ def create_typed_override_file(context, files, strictness); end
131
+
129
132
  def help(command = T.unsafe(nil), subcommand = T.unsafe(nil)); end
130
133
  def print_changes(files, command:, from: T.unsafe(nil), to: T.unsafe(nil), dry: T.unsafe(nil), path: T.unsafe(nil)); end
131
- def undo_changes(files, from_strictness); end
132
134
  end
133
135
 
134
136
  class Spoom::Cli::Srb::Coverage < ::Thor
@@ -1519,6 +1521,9 @@ class Spoom::Deadcode::Remover::NodeRemover
1519
1521
  end
1520
1522
  def insert_accessor(node, send_context, was_removed:); end
1521
1523
 
1524
+ sig { params(def_node: ::Prism::DefNode).returns(T.nilable(::Spoom::Deadcode::Remover::NodeContext)) }
1525
+ def modifier_call_context(def_node); end
1526
+
1522
1527
  sig { params(start_char: ::Integer, end_char: ::Integer, replacement: ::String).void }
1523
1528
  def replace_chars(start_char, end_char, replacement); end
1524
1529
 
@@ -2575,7 +2580,11 @@ end
2575
2580
  module Spoom::PrismTypes; end
2576
2581
  Spoom::PrismTypes::AnyScopeNode = T.type_alias { T.any(::Prism::ClassNode, ::Prism::ModuleNode, ::Prism::SingletonClassNode) }
2577
2582
  module Spoom::RBS; end
2578
- class Spoom::RBS::Annotation < ::Spoom::RBS::Comment; end
2583
+
2584
+ class Spoom::RBS::Annotation < ::Spoom::RBS::Comment
2585
+ sig { returns(T::Boolean) }
2586
+ def abstract?; end
2587
+ end
2579
2588
 
2580
2589
  class Spoom::RBS::Comment
2581
2590
  sig { params(string: ::String, location: ::Prism::Location).void }
@@ -2654,6 +2663,8 @@ class Spoom::Sorbet::Config
2654
2663
  def paths; end
2655
2664
 
2656
2665
  def paths=(_arg0); end
2666
+ def typed_overrides; end
2667
+ def typed_overrides=(_arg0); end
2657
2668
 
2658
2669
  private
2659
2670
 
@@ -3206,10 +3217,11 @@ class Spoom::Sorbet::Translate::RBSCommentsToSorbetSigs::Options
3206
3217
  sig do
3207
3218
  params(
3208
3219
  overloads_strategy: ::Symbol,
3209
- output_format: ::Spoom::Sorbet::Translate::RBSCommentsToSorbetSigs::BaseRBIFormat
3220
+ output_format: ::Spoom::Sorbet::Translate::RBSCommentsToSorbetSigs::BaseRBIFormat,
3221
+ translate_abstract_methods: T::Boolean
3210
3222
  ).void
3211
3223
  end
3212
- def initialize(overloads_strategy: T.unsafe(nil), output_format: T.unsafe(nil)); end
3224
+ def initialize(overloads_strategy: T.unsafe(nil), output_format: T.unsafe(nil), translate_abstract_methods: T.unsafe(nil)); end
3213
3225
 
3214
3226
  sig { returns(::Spoom::Sorbet::Translate::RBSCommentsToSorbetSigs::BaseRBIFormat) }
3215
3227
  def output_format; end
@@ -3217,6 +3229,9 @@ class Spoom::Sorbet::Translate::RBSCommentsToSorbetSigs::Options
3217
3229
  sig { returns(::Symbol) }
3218
3230
  def overloads_strategy; end
3219
3231
 
3232
+ sig { returns(T::Boolean) }
3233
+ def translate_abstract_methods; end
3234
+
3220
3235
  class << self
3221
3236
  sig { returns(::Spoom::Sorbet::Translate::RBSCommentsToSorbetSigs::Options) }
3222
3237
  def default; end
@@ -3262,9 +3277,18 @@ class Spoom::Sorbet::Translate::SorbetAssertionsToRBSComments < ::Spoom::Sorbet:
3262
3277
  sig { params(node: ::Prism::Node).returns(T::Boolean) }
3263
3278
  def has_rbs_annotation?(node); end
3264
3279
 
3280
+ sig { params(node: ::Prism::Node, replace_end_offset: ::Integer).returns(T.nilable(::String)) }
3281
+ def heredoc_body_within_range(node, replace_end_offset); end
3282
+
3283
+ sig { params(node: ::Prism::Node).returns(T::Array[::Integer]) }
3284
+ def heredoc_end_offsets(node); end
3285
+
3265
3286
  sig { params(node: ::Prism::Node).returns(T::Boolean) }
3266
3287
  def maybe_translate_assertion(node); end
3267
3288
 
3289
+ sig { params(node: ::Prism::Node).returns(T::Boolean) }
3290
+ def string_literal?(node); end
3291
+
3268
3292
  sig { params(node: T.nilable(::Prism::Node)).returns(T::Boolean) }
3269
3293
  def t?(node); end
3270
3294
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spoom
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.1
4
+ version: 1.8.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexandre Terrasa