spoom 1.6.3 → 1.7.1

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.
@@ -0,0 +1,23 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module Sorbet
6
+ module Translate
7
+ # Deletes all `sig` nodes from the given Ruby code.
8
+ # It doesn't handle type members and class annotations.
9
+ class StripSorbetSigs < Translator
10
+ # @override
11
+ #: (Prism::CallNode node) -> void
12
+ def visit_call_node(node)
13
+ return unless sorbet_sig?(node)
14
+
15
+ @rewriter << Source::Delete.new(
16
+ adjust_to_line_start(node.location.start_offset),
17
+ adjust_to_line_end(node.location.end_offset),
18
+ )
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,71 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module Sorbet
6
+ module Translate
7
+ # @abstract
8
+ class Translator < Spoom::Visitor
9
+ #: (String, file: String) -> void
10
+ def initialize(ruby_contents, file:)
11
+ super()
12
+
13
+ @file = file #: String
14
+
15
+ @original_encoding = ruby_contents.encoding #: Encoding
16
+ @ruby_contents = if @original_encoding == "UTF-8"
17
+ ruby_contents
18
+ else
19
+ ruby_contents.encode("UTF-8")
20
+ end #: String
21
+
22
+ node = Spoom.parse_ruby(ruby_contents, file: file, comments: true)
23
+ @node = node #: Prism::Node
24
+ @ruby_bytes = ruby_contents.bytes #: Array[Integer]
25
+ @rewriter = Spoom::Source::Rewriter.new #: Source::Rewriter
26
+ end
27
+
28
+ #: -> String
29
+ def rewrite
30
+ visit(@node)
31
+ @rewriter.rewrite!(@ruby_bytes)
32
+ @ruby_bytes.pack("C*").force_encoding(@original_encoding)
33
+ end
34
+
35
+ private
36
+
37
+ #: (Prism::CallNode node) -> bool
38
+ def sorbet_sig?(node)
39
+ return false unless node.message == "sig"
40
+
41
+ recv = node.receiver
42
+ return false if recv && !recv.is_a?(Prism::SelfNode) && !recv.slice.match?(/(::)?T::Sig::WithoutRuntime/)
43
+
44
+ true
45
+ end
46
+
47
+ #: (Integer) -> Integer
48
+ def adjust_to_line_start(offset)
49
+ offset -= 1 while offset > 0 && @ruby_bytes[offset - 1] != "\n".ord
50
+ offset
51
+ end
52
+
53
+ #: (Integer) -> Integer
54
+ def adjust_to_line_end(offset)
55
+ offset += 1 while offset < @ruby_bytes.size && @ruby_bytes[offset] != "\n".ord
56
+ offset
57
+ end
58
+
59
+ # Consume the next blank line if any
60
+ #: (Integer) -> Integer
61
+ def adjust_to_new_line(offset)
62
+ if offset + 1 < @ruby_bytes.size && @ruby_bytes[offset + 1] == "\n".ord
63
+ offset += 1
64
+ end
65
+
66
+ offset
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,49 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "rbi"
5
+
6
+ require "spoom/source/rewriter"
7
+ require "spoom/sorbet/translate/translator"
8
+ require "spoom/sorbet/translate/rbs_comments_to_sorbet_sigs"
9
+ require "spoom/sorbet/translate/sorbet_assertions_to_rbs_comments"
10
+ require "spoom/sorbet/translate/sorbet_sigs_to_rbs_comments"
11
+ require "spoom/sorbet/translate/strip_sorbet_sigs"
12
+
13
+ module Spoom
14
+ module Sorbet
15
+ module Translate
16
+ class Error < Spoom::Error; end
17
+
18
+ class << self
19
+ # Deletes all `sig` nodes from the given Ruby code.
20
+ # It doesn't handle type members and class annotations.
21
+ #: (String ruby_contents, file: String) -> String
22
+ def strip_sorbet_sigs(ruby_contents, file:)
23
+ StripSorbetSigs.new(ruby_contents, file: file).rewrite
24
+ end
25
+
26
+ # Converts all `sig` nodes to RBS comments in the given Ruby code.
27
+ # It also handles type members and class annotations.
28
+ #: (String ruby_contents, file: String, ?positional_names: bool) -> String
29
+ def sorbet_sigs_to_rbs_comments(ruby_contents, file:, positional_names: true)
30
+ SorbetSigsToRBSComments.new(ruby_contents, file: file, positional_names: positional_names).rewrite
31
+ end
32
+
33
+ # Converts all the RBS comments in the given Ruby code to `sig` nodes.
34
+ # It also handles type members and class annotations.
35
+ #: (String ruby_contents, file: String) -> String
36
+ def rbs_comments_to_sorbet_sigs(ruby_contents, file:)
37
+ RBSCommentsToSorbetSigs.new(ruby_contents, file: file).rewrite
38
+ end
39
+
40
+ # Converts all `T.let` and `T.cast` nodes to RBS comments in the given Ruby code.
41
+ # It also handles type members and class annotations.
42
+ #: (String ruby_contents, file: String) -> String
43
+ def sorbet_assertions_to_rbs_comments(ruby_contents, file:)
44
+ SorbetAssertionsToRBSComments.new(ruby_contents, file: file).rewrite
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
data/lib/spoom/sorbet.rb CHANGED
@@ -1,12 +1,12 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "spoom/sorbet/assertions"
5
4
  require "spoom/sorbet/config"
6
5
  require "spoom/sorbet/errors"
7
6
  require "spoom/sorbet/lsp"
8
7
  require "spoom/sorbet/metrics"
9
8
  require "spoom/sorbet/sigils"
9
+ require "spoom/sorbet/translate"
10
10
 
11
11
  require "open3"
12
12
 
@@ -0,0 +1,167 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ # This module provides a simple API to rewrite source code.
6
+ #
7
+ # Using a `Rewriter`, you can build a list of changes to apply to a source file
8
+ # and apply them all at once. Edits are applied from bottom to top, so that the
9
+ # line numbers are not remapped after each edit.
10
+ #
11
+ # The source code is represented as an array of bytes, so that it can be
12
+ # manipulated in place. The client is responsible for `string <-> bytes`
13
+ # conversions and encoding handling.
14
+ #
15
+ # ```ruby
16
+ # bytes = "def foo; end".bytes
17
+ #
18
+ # rewriter = Spoom::Source::Rewriter.new
19
+ # rewriter << Spoom::Source::Replace.new(4, 6, "baz")
20
+ # rewriter << Spoom::Source::Insert.new(0, "def bar; end\n")
21
+ # rewriter.rewrite!(bytes)
22
+ #
23
+ # puts bytes.pack("C*") # => "def bar; end\ndef baz; end"
24
+ # ```
25
+ module Source
26
+ class PositionError < Spoom::Error; end
27
+
28
+ # @abstract
29
+ class Edit
30
+ # @abstract
31
+ #: (Array[Integer]) -> void
32
+ def apply(bytes); end
33
+
34
+ # @abstract
35
+ #: -> [Integer, Integer]
36
+ def range; end
37
+ end
38
+
39
+ class Insert < Edit
40
+ #: Integer
41
+ attr_reader :position
42
+
43
+ #: String
44
+ attr_reader :text
45
+
46
+ #: (Integer, String) -> void
47
+ def initialize(position, text)
48
+ super()
49
+
50
+ @position = position
51
+ @text = text
52
+ end
53
+
54
+ # @override
55
+ #: (Array[Integer]) -> void
56
+ def apply(bytes)
57
+ raise PositionError, "Position is out of bounds" if position < 0 || position > bytes.size
58
+
59
+ bytes #: untyped
60
+ .insert(position, *text.bytes)
61
+ end
62
+
63
+ # @override
64
+ #: -> [Integer, Integer]
65
+ def range
66
+ [position, position]
67
+ end
68
+
69
+ # @override
70
+ #: -> String
71
+ def to_s
72
+ "Insert `#{text}` at #{position}"
73
+ end
74
+ end
75
+
76
+ class Replace < Edit
77
+ #: Integer
78
+ attr_reader :from, :to
79
+
80
+ #: String
81
+ attr_reader :text
82
+
83
+ #: (Integer, Integer, String) -> void
84
+ def initialize(from, to, text)
85
+ super()
86
+
87
+ @from = from
88
+ @to = to
89
+ @text = text
90
+ end
91
+
92
+ # @override
93
+ #: (Array[Integer]) -> void
94
+ def apply(bytes)
95
+ raise PositionError, "Position is out of bounds" if from < 0 || to < 0 || from > bytes.size || to > bytes.size || from > to
96
+
97
+ bytes[from..to] = *text.bytes
98
+ end
99
+
100
+ # @override
101
+ #: -> [Integer, Integer]
102
+ def range
103
+ [from, to]
104
+ end
105
+
106
+ # @override
107
+ #: -> String
108
+ def to_s
109
+ "Replace #{from}-#{to} with `#{text}`"
110
+ end
111
+ end
112
+
113
+ class Delete < Edit
114
+ #: Integer
115
+ attr_reader :from, :to
116
+
117
+ #: (Integer, Integer) -> void
118
+ def initialize(from, to)
119
+ super()
120
+
121
+ @from = from
122
+ @to = to
123
+ end
124
+
125
+ # @override
126
+ #: (Array[untyped]) -> void
127
+ def apply(bytes)
128
+ raise PositionError, "Position is out of bounds" if from < 0 || to < 0 || from > bytes.size || to > bytes.size || from > to
129
+
130
+ bytes[from..to] = "".bytes
131
+ end
132
+
133
+ # @override
134
+ #: -> [Integer, Integer]
135
+ def range
136
+ [from, to]
137
+ end
138
+
139
+ # @override
140
+ #: -> String
141
+ def to_s
142
+ "Delete #{from}-#{to}"
143
+ end
144
+ end
145
+
146
+ class Rewriter
147
+ #: -> void
148
+ def initialize
149
+ @edits = [] #: Array[Edit]
150
+ end
151
+
152
+ #: (Edit) -> void
153
+ def <<(other)
154
+ @edits << other
155
+ end
156
+
157
+ #: (Array[Integer]) -> void
158
+ def rewrite!(bytes)
159
+ # To avoid remapping positions after each edit,
160
+ # we sort the changes by position and apply them in reverse order.
161
+ @edits.sort_by(&:range).reverse_each do |edit|
162
+ edit.apply(bytes)
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,4 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "source/rewriter"
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.6.3"
5
+ VERSION = "1.7.1"
6
6
  end
data/lib/spoom.rb CHANGED
@@ -15,7 +15,10 @@ require "spoom/context"
15
15
  require "spoom/colors"
16
16
  require "spoom/poset"
17
17
  require "spoom/model"
18
+ require "spoom/source"
18
19
  require "spoom/deadcode"
20
+ require "spoom/rbs"
21
+ require "spoom/counters"
19
22
  require "spoom/sorbet"
20
23
  require "spoom/cli"
21
24
  require "spoom/version"