rubocop-sorbet 0.7.3 → 0.7.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a0c73cc4c4d3b4982d210699da94d0dca321a93bf95e98579dc32521714e2921
4
- data.tar.gz: b9aa475dea9aa87511868680e92fb1ad5dff83a5b8186439179c17371eaf35f5
3
+ metadata.gz: 9b2451806224baad5c72fb848ae38e6510037b2e26415afc236c3639f8504655
4
+ data.tar.gz: e55c4a39d87a4fe345a6257e14483de155738ea5d39c25ef1e7822a2174321cb
5
5
  SHA512:
6
- metadata.gz: 03a722e469cf1796ba965b044475dabbbe2d99a6b0b79efc083d605bd8dd0973ed1d320dbe511241dd6bfad3c9df7acb6354217ded268db5939ad7f8bf7684aa
7
- data.tar.gz: 9fd995f27b0881e5ec939da2b0b1a87372bc910faa4f27cbaf0de43c367d877e1c5b077397f80130ec58f1caa7b6047a7c51ea1eb0872f194f118ce27dbfa10d
6
+ metadata.gz: d05d0627ce52223c931f3461e221f6cb105838c21eeae6d68a7270934e6e292fcd2a7390453583c95d721bc1544125cb74f2d65ef6161c4d8fe823c3ee8217e6
7
+ data.tar.gz: adf840bad5a49581286937604e83b3ee864f45031efb115db9faed9032da04d4178d7022b3d2040e08e8b10dc5f7a95bc8bf8a0ee1ac06ba74780b135d947b26
@@ -6,12 +6,12 @@ env:
6
6
  SRB_SKIP_GEM_RBIS: true
7
7
 
8
8
  jobs:
9
- build:
9
+ test:
10
10
  runs-on: ubuntu-latest
11
11
  strategy:
12
12
  fail-fast: false
13
13
  matrix:
14
- ruby: [ '2.7', '3.0', '3.1', '3.2' ]
14
+ ruby: ["2.7", "3.0", "3.1", "3.2"]
15
15
  name: Test Ruby ${{ matrix.ruby }}
16
16
  steps:
17
17
  - uses: actions/checkout@v2
@@ -20,9 +20,22 @@ jobs:
20
20
  with:
21
21
  ruby-version: ${{ matrix.ruby }}
22
22
  bundler-cache: true
23
+ - name: Run tests
24
+ run: bin/rspec
25
+
26
+ lint-and-docs:
27
+ runs-on: ubuntu-latest
28
+ strategy:
29
+ fail-fast: false
30
+ name: Lint & Docs
31
+ steps:
32
+ - uses: actions/checkout@v2
33
+ - name: Set up Ruby
34
+ uses: ruby/setup-ruby@v1
35
+ with:
36
+ ruby-version: 3.2
37
+ bundler-cache: true
23
38
  - name: Lint Ruby files
24
39
  run: bin/rubocop
25
40
  - name: Verify documentation is up to date
26
41
  run: bundle exec rake generate_cops_documentation
27
- - name: Run tests
28
- run: bin/rspec
data/.rubocop.yml CHANGED
@@ -22,3 +22,6 @@ Naming/FileName:
22
22
 
23
23
  Layout/LineLength:
24
24
  IgnoreCopDirectives: true
25
+
26
+ InternalAffairs/UndefinedConfig:
27
+ Enabled: false # Bug in implementation fails to find our configs
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rubocop-sorbet (0.7.3)
4
+ rubocop-sorbet (0.7.5)
5
5
  rubocop (>= 0.90.0)
6
6
 
7
7
  GEM
data/config/default.yml CHANGED
@@ -97,6 +97,13 @@ Sorbet/ForbidSuperclassConstLiteral:
97
97
  Exclude:
98
98
  - db/migrate/*.rb
99
99
 
100
+ Sorbet/ForbidTStruct:
101
+ Description: 'Forbid usage of T::Struct.'
102
+ Enabled: false
103
+ VersionAdded: <<next>>
104
+ VersionChanged: <<next>>
105
+ Safe: false
106
+
100
107
  Sorbet/ForbidTUnsafe:
101
108
  Description: 'Forbid usage of T.unsafe.'
102
109
  Enabled: false
@@ -171,6 +178,23 @@ Sorbet/ObsoleteStrictMemoization:
171
178
  Safe: true
172
179
  SafeAutoCorrect: true
173
180
 
181
+ Sorbet/BuggyObsoleteStrictMemoization:
182
+ Description: >-
183
+ Checks for the a mistaken variant of the "obsolete memoization pattern" that used to be required
184
+ for older Sorbet versions in `#typed: strict` files. The mistaken variant would overwrite the ivar with `nil`
185
+ on every call, causing the memoized value to be discarded and recomputed on every call.
186
+
187
+ This cop will correct it to read from the ivar instead of `nil`, which will memoize it correctly.
188
+
189
+ The result of this correction will be the "obsolete memoization pattern", which can further be corrected by
190
+ the `Sorbet/ObsoleteStrictMemoization` cop.
191
+
192
+ See `Sorbet/ObsoleteStrictMemoization` for more details.
193
+ Enabled: true
194
+ VersionAdded: '0.7.3'
195
+ Safe: true
196
+ SafeAutoCorrect: false
197
+
174
198
  Sorbet/OneAncestorPerLine:
175
199
  Description: 'Enforces one ancestor per call to requires_ancestor'
176
200
  Enabled: false
data/config/rbi.yml CHANGED
@@ -242,6 +242,9 @@ Style/ClassAndModuleChildren:
242
242
  Style/DefWithParentheses:
243
243
  Enabled: true
244
244
 
245
+ Sorbet/EmptyLineAfterSig:
246
+ Enabled: true
247
+
245
248
  Style/EmptyMethod:
246
249
  Enabled: true
247
250
  EnforcedStyle: compact
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Sorbet
8
+ # Checks for the a mistaken variant of the "obsolete memoization pattern" that used to be required
9
+ # for older Sorbet versions in `#typed: strict` files. The mistaken variant would overwrite the ivar with `nil`
10
+ # on every call, causing the memoized value to be discarded and recomputed on every call.
11
+ #
12
+ # This cop will correct it to read from the ivar instead of `nil`, which will memoize it correctly.
13
+ #
14
+ # The result of this correction will be the "obsolete memoization pattern", which can further be corrected by
15
+ # the `Sorbet/ObsoleteStrictMemoization` cop.
16
+ #
17
+ # See `Sorbet/ObsoleteStrictMemoization` for more details.
18
+ #
19
+ # @safety
20
+ # If the computation being memoized had side effects, calling it only once (instead of once on every call
21
+ # to the affected method) can be observed, and might be a breaking change.
22
+ #
23
+ # @example
24
+ # # bad
25
+ # sig { returns(Foo) }
26
+ # def foo
27
+ # # This `nil` is likely a mistake, causing the memoized value to be discarded and recomputed on every call.
28
+ # @foo = T.let(nil, T.nilable(Foo))
29
+ # @foo ||= some_computation
30
+ # end
31
+ #
32
+ # # good
33
+ # sig { returns(Foo) }
34
+ # def foo
35
+ # # This will now memoize the value as was likely intended, so `some_computation` is only ever called once.
36
+ # # ⚠️If `some_computation` has side effects, this might be a breaking change!
37
+ # @foo = T.let(@foo, T.nilable(Foo))
38
+ # @foo ||= some_computation
39
+ # end
40
+ #
41
+ # @see Sorbet/ObsoleteStrictMemoization
42
+ class BuggyObsoleteStrictMemoization < RuboCop::Cop::Base
43
+ include RuboCop::Cop::MatchRange
44
+ include RuboCop::Cop::Alignment
45
+ include RuboCop::Cop::LineLengthHelp
46
+ include RuboCop::Cop::RangeHelp
47
+ extend AutoCorrector
48
+
49
+ include TargetSorbetVersion
50
+
51
+ MSG = "This might be a mistaken variant of the two-stage workaround that used to be needed for memoization in "\
52
+ "`#typed: strict` files. See https://sorbet.org/docs/type-assertions#put-type-assertions-behind-memoization."
53
+
54
+ # @!method buggy_legacy_memoization_pattern?(node)
55
+ def_node_matcher :buggy_legacy_memoization_pattern?, <<~PATTERN
56
+ (begin
57
+ ... # Ignore any other lines that come first.
58
+ (ivasgn $_ivar # First line: @_ivar = ...
59
+ (send # T.let(_ivar, T.nilable(_ivar_type))
60
+ (const {nil? cbase} :T) :let
61
+ $nil
62
+ (send (const {nil? cbase} :T) :nilable _ivar_type))) # T.nilable(_ivar_type)
63
+ (or-asgn (ivasgn _ivar) _initialization_expr)) # Second line: @_ivar ||= _initialization_expr
64
+ PATTERN
65
+
66
+ def on_begin(node)
67
+ buggy_legacy_memoization_pattern?(node) do |ivar, nil_node|
68
+ add_offense(nil_node) do |corrector|
69
+ corrector.replace(
70
+ range_between(nil_node.source_range.begin_pos, nil_node.source_range.end_pos),
71
+ ivar,
72
+ )
73
+ end
74
+ end
75
+ end
76
+
77
+ def relevant_file?(file)
78
+ super && sorbet_enabled?
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -26,44 +26,38 @@ module RuboCop
26
26
  # ```ruby
27
27
  # include Polaris::Engine.helpers
28
28
  # ```
29
- class ForbidIncludeConstLiteral < RuboCop::Cop::Cop # rubocop:todo InternalAffairs/InheritDeprecatedCopClass
30
- MSG = "Includes must only contain constant literals"
29
+ class ForbidIncludeConstLiteral < RuboCop::Cop::Base
30
+ extend AutoCorrector
31
31
 
32
- attr_accessor :used_names
32
+ MSG = "`%<inclusion_method>s` must only be used with constant literals as arguments"
33
+ RESTRICT_ON_SEND = [:include, :extend, :prepend].freeze
33
34
 
34
- # @!method not_lit_const_include?(node)
35
- def_node_matcher :not_lit_const_include?, <<-PATTERN
36
- (send nil? {:include :extend :prepend}
37
- $_
38
- )
35
+ # @!method dynamic_inclusion?(node)
36
+ def_node_matcher :dynamic_inclusion?, <<~PATTERN
37
+ (send nil? ${:include :extend :prepend} $#neither_const_nor_self?)
39
38
  PATTERN
40
39
 
41
- def initialize(*)
42
- super
43
- self.used_names = Set.new
44
- end
45
-
46
40
  def on_send(node)
47
- return unless not_lit_const_include?(node) do |send_argument|
48
- ![:const, :self].include?(send_argument.type)
49
- end
41
+ dynamic_inclusion?(node) do |inclusion_method, included|
42
+ return unless within_onymous_module?(node)
50
43
 
51
- parent = node.parent
52
- return unless parent
44
+ add_offense(node, message: format(MSG, inclusion_method: inclusion_method)) do |corrector|
45
+ corrector.replace(node, "T.unsafe(self).#{inclusion_method} #{included.source}")
46
+ end
47
+ end
48
+ end
53
49
 
54
- parent = parent.parent if [:begin, :block].include?(parent.type)
55
- return unless [:module, :class, :sclass].include?(parent.type)
50
+ private
56
51
 
57
- add_offense(node)
52
+ def neither_const_nor_self?(node)
53
+ !node.const_type? && !node.self_type?
58
54
  end
59
55
 
60
- def autocorrect(node)
61
- lambda do |corrector|
62
- corrector.replace(
63
- node,
64
- "T.unsafe(self).#{node.source}",
65
- )
66
- end
56
+ # Returns true if the node is within a module declaration that is not anonymous.
57
+ def within_onymous_module?(node)
58
+ parent = node.parent
59
+ parent = parent.parent while parent&.begin_type? || parent&.block_type?
60
+ parent && (parent.module_type? || parent.class_type? || parent.sclass_type?)
67
61
  end
68
62
  end
69
63
  end
@@ -0,0 +1,232 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Sorbet
8
+ # Disallow using `T::Struct` and `T::Props`.
9
+ #
10
+ # @example
11
+ #
12
+ # # bad
13
+ # class MyStruct < T::Struct
14
+ # const :foo, String
15
+ # prop :bar, Integer, default: 0
16
+ #
17
+ # def some_method; end
18
+ # end
19
+ #
20
+ # # good
21
+ # class MyStruct
22
+ # extend T::Sig
23
+ #
24
+ # sig { returns(String) }
25
+ # attr_reader :foo
26
+ #
27
+ # sig { returns(Integer) }
28
+ # attr_accessor :bar
29
+ #
30
+ # sig { params(foo: String, bar: Integer) }
31
+ # def initialize(foo:, bar: 0)
32
+ # @foo = foo
33
+ # @bar = bar
34
+ # end
35
+ #
36
+ # def some_method; end
37
+ # end
38
+ class ForbidTStruct < RuboCop::Cop::Base
39
+ include Alignment
40
+ include RangeHelp
41
+ include CommentsHelp
42
+ extend AutoCorrector
43
+
44
+ RESTRICT_ON_SEND = [:include, :prepend, :extend].freeze
45
+
46
+ MSG_STRUCT = "Using `T::Struct` or its variants is deprecated."
47
+ MSG_PROPS = "Using `T::Props` or its variants is deprecated."
48
+
49
+ # This class walks down the class body of a T::Struct and collects all the properties that will need to be
50
+ # translated into `attr_reader` and `attr_accessor` methods.
51
+ class TStructWalker
52
+ include AST::Traversal
53
+ extend AST::NodePattern::Macros
54
+
55
+ attr_reader :props, :has_extend_t_sig
56
+
57
+ def initialize
58
+ @props = []
59
+ @has_extend_t_sig = false
60
+ end
61
+
62
+ # @!method extend_t_sig?(node)
63
+ def_node_matcher :extend_t_sig?, <<~PATTERN
64
+ (send _ :extend (const (const {nil? | cbase} :T) :Sig))
65
+ PATTERN
66
+
67
+ # @!method t_struct_prop?(node)
68
+ def_node_matcher(:t_struct_prop?, <<~PATTERN)
69
+ (send nil? {:const :prop} ...)
70
+ PATTERN
71
+
72
+ def on_send(node)
73
+ if extend_t_sig?(node)
74
+ # So we know we won't need to generate again a `extend T::Sig` line in the new class body
75
+ @has_extend_t_sig = true
76
+ return
77
+ end
78
+
79
+ return unless t_struct_prop?(node)
80
+
81
+ kind = node.method?(:const) ? :attr_reader : :attr_accessor
82
+ name = node.arguments[0].source.delete_prefix(":")
83
+ type = node.arguments[1].source
84
+ default = nil
85
+ factory = nil
86
+
87
+ node.arguments[2..-1].each do |arg|
88
+ next unless arg.hash_type?
89
+
90
+ arg.each_pair do |key, value|
91
+ case key.source
92
+ when "default"
93
+ default = value.source
94
+ when "factory"
95
+ factory = value.source
96
+ end
97
+ end
98
+ end
99
+
100
+ @props << Property.new(node, kind, name, type, default: default, factory: factory)
101
+ end
102
+ end
103
+
104
+ class Property
105
+ attr_reader :node, :kind, :name, :type, :default, :factory
106
+
107
+ def initialize(node, kind, name, type, default:, factory:)
108
+ @node = node
109
+ @kind = kind
110
+ @name = name
111
+ @type = type
112
+ @default = default
113
+ @factory = factory
114
+
115
+ # A T::Struct should have both a default and a factory, if we find one let's raise an error
116
+ raise if @default && @factory
117
+ end
118
+
119
+ def attr_sig
120
+ "sig { returns(#{type}) }"
121
+ end
122
+
123
+ def attr_accessor
124
+ "#{kind} :#{name}"
125
+ end
126
+
127
+ def initialize_sig_param
128
+ "#{name}: #{type}"
129
+ end
130
+
131
+ def initialize_param
132
+ rb = String.new
133
+ rb << "#{name}:"
134
+ if default
135
+ rb << " #{default}"
136
+ elsif factory
137
+ rb << " #{factory}"
138
+ elsif nilable?
139
+ rb << " nil"
140
+ end
141
+ rb
142
+ end
143
+
144
+ def initialize_assign
145
+ rb = String.new
146
+ rb << "@#{name} = #{name}"
147
+ rb << ".call" if factory
148
+ rb
149
+ end
150
+
151
+ def nilable?
152
+ type.start_with?("T.nilable(")
153
+ end
154
+ end
155
+
156
+ # @!method t_struct?(node)
157
+ def_node_matcher(:t_struct?, <<~PATTERN)
158
+ (const (const {nil? cbase} :T) {:Struct :ImmutableStruct :InexactStruct})
159
+ PATTERN
160
+
161
+ # @!method t_props?(node)
162
+ def_node_matcher(:t_props?, "(send nil? {:include :prepend :extend} `(const (const {nil? cbase} :T) :Props))")
163
+
164
+ def on_class(node)
165
+ return unless t_struct?(node.parent_class)
166
+
167
+ add_offense(node, message: MSG_STRUCT) do |corrector|
168
+ walker = TStructWalker.new
169
+ walker.walk(node.body)
170
+
171
+ range = range_between(node.identifier.source_range.end_pos, node.parent_class.source_range.end_pos)
172
+ corrector.remove(range)
173
+ next if node.single_line?
174
+
175
+ unless walker.has_extend_t_sig
176
+ indent = offset(node)
177
+ corrector.insert_after(node.identifier, "\n#{indent} extend T::Sig\n")
178
+ end
179
+
180
+ first_prop = walker.props.first
181
+ walker.props.each do |prop|
182
+ node = prop.node
183
+ indent = offset(node)
184
+ line_range = range_by_whole_lines(prop.node.source_range)
185
+ new_line = prop != first_prop && !previous_line_blank?(node)
186
+ trailing_comments = processed_source.each_comment_in_lines(line_range.line..line_range.line)
187
+
188
+ corrector.replace(
189
+ line_range,
190
+ "#{new_line ? "\n" : ""}" \
191
+ "#{trailing_comments.map { |comment| "#{indent}#{comment.text}\n" }.join}" \
192
+ "#{indent}#{prop.attr_sig}\n#{indent}#{prop.attr_accessor}",
193
+ )
194
+ end
195
+
196
+ last_prop = walker.props.last
197
+ if last_prop
198
+ indent = offset(last_prop.node)
199
+ line_range = range_by_whole_lines(last_prop.node.source_range, include_final_newline: true)
200
+ corrector.insert_after(line_range, initialize_method(indent, walker.props))
201
+ end
202
+ end
203
+ end
204
+
205
+ def on_send(node)
206
+ return unless t_props?(node)
207
+
208
+ add_offense(node, message: MSG_PROPS)
209
+ end
210
+
211
+ private
212
+
213
+ def initialize_method(indent, props)
214
+ # We sort optional keyword arguments after required ones
215
+ sorted_props = props.sort_by { |prop| prop.default || prop.factory || prop.nilable? ? 1 : 0 }
216
+
217
+ string = +"\n"
218
+ string << "#{indent}sig { params(#{sorted_props.map(&:initialize_sig_param).join(", ")}).void }\n"
219
+ string << "#{indent}def initialize(#{sorted_props.map(&:initialize_param).join(", ")})\n"
220
+ props.each do |prop|
221
+ string << "#{indent} #{prop.initialize_assign}\n"
222
+ end
223
+ string << "#{indent}end\n"
224
+ end
225
+
226
+ def previous_line_blank?(node)
227
+ processed_source.buffer.source_line(node.source_range.line - 1).blank?
228
+ end
229
+ end
230
+ end
231
+ end
232
+ end
@@ -1,15 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rubocop"
4
-
5
3
  module RuboCop
6
4
  module Cop
7
5
  module Sorbet
8
- # Abstract cop specific to Sorbet signatures
9
- #
10
- # You can subclass it to use the `on_signature` trigger and the `signature?` node matcher.
11
- class SignatureCop < RuboCop::Cop::Cop # rubocop:todo InternalAffairs/InheritDeprecatedCopClass
12
- @registry = Cop.registry # So we can properly subclass this cop
6
+ # Mixin for writing cops for signatures, providing a `signature?` node matcher and an `on_signature` trigger.
7
+ module SignatureHelp
8
+ extend RuboCop::NodePattern::Macros
13
9
 
14
10
  # @!method signature?(node)
15
11
  def_node_matcher(:signature?, <<~PATTERN)
@@ -22,12 +18,12 @@ module RuboCop
22
18
 
23
19
  # @!method with_runtime?(node)
24
20
  def_node_matcher(:with_runtime?, <<~PATTERN)
25
- (const (const nil? :T) :Sig)
21
+ (const (const {nil? cbase} :T) :Sig)
26
22
  PATTERN
27
23
 
28
24
  # @!method without_runtime?(node)
29
25
  def_node_matcher(:without_runtime?, <<~PATTERN)
30
- (const (const (const nil? :T) :Sig) :WithoutRuntime)
26
+ (const (const (const {nil? cbase} :T) :Sig) :WithoutRuntime)
31
27
  PATTERN
32
28
 
33
29
  def on_block(node)
@@ -36,8 +32,8 @@ module RuboCop
36
32
 
37
33
  alias_method :on_numblock, :on_block
38
34
 
39
- def on_signature(_)
40
- # To be defined in subclasses
35
+ def on_signature(_node)
36
+ # To be defined by cop class as needed
41
37
  end
42
38
  end
43
39
  end
@@ -11,21 +11,25 @@ module RuboCop
11
11
  end
12
12
 
13
13
  module ClassMethods
14
- # The version of the Sorbet static type checker required by this cop
14
+ # Sets the version of the Sorbet static type checker required by this cop
15
15
  def minimum_target_sorbet_static_version(version)
16
16
  @minimum_target_sorbet_static_version = Gem::Version.new(version)
17
17
  end
18
18
 
19
- def support_target_sorbet_static_version?(version)
19
+ def supports_target_sorbet_static_version?(version)
20
20
  @minimum_target_sorbet_static_version <= Gem::Version.new(version)
21
21
  end
22
22
  end
23
23
 
24
+ def sorbet_enabled?
25
+ !target_sorbet_static_version_from_bundler_lock_file.nil?
26
+ end
27
+
24
28
  def enabled_for_sorbet_static_version?
25
29
  sorbet_static_version = target_sorbet_static_version_from_bundler_lock_file
26
30
  return false unless sorbet_static_version
27
31
 
28
- self.class.support_target_sorbet_static_version?(sorbet_static_version)
32
+ self.class.supports_target_sorbet_static_version?(sorbet_static_version)
29
33
  end
30
34
 
31
35
  def target_sorbet_static_version_from_bundler_lock_file
@@ -54,7 +54,7 @@ module RuboCop
54
54
  $(ivasgn $_ivar # First line: @_ivar = ...
55
55
  (send # T.let(_ivar, T.nilable(_ivar_type))
56
56
  $(const {nil? cbase} :T) :let
57
- {(ivar _ivar) nil}
57
+ (ivar _ivar)
58
58
  (send (const {nil? cbase} :T) :nilable $_ivar_type))) # T.nilable(_ivar_type)
59
59
  $(or-asgn (ivasgn _ivar) $_initialization_expr)) # Second line: @_ivar ||= _initialization_expr
60
60
  PATTERN
@@ -56,7 +56,7 @@ module RuboCop
56
56
  private
57
57
 
58
58
  def allowed_paths
59
- paths = cop_config["AllowedPaths"] # rubocop:todo InternalAffairs/UndefinedConfig
59
+ paths = cop_config["AllowedPaths"]
60
60
  return unless paths.is_a?(Array)
61
61
 
62
62
  paths.compact
@@ -26,6 +26,7 @@ module RuboCop
26
26
  # end
27
27
  #
28
28
  class RedundantExtendTSig < RuboCop::Cop::Base
29
+ include RangeHelp
29
30
  extend AutoCorrector
30
31
 
31
32
  MSG = "Do not redundantly `extend T::Sig` when it is already included in all modules."
@@ -40,7 +41,7 @@ module RuboCop
40
41
  return unless extend_t_sig?(node)
41
42
 
42
43
  add_offense(node) do |corrector|
43
- corrector.remove(node)
44
+ corrector.remove(range_by_whole_lines(node.source_range, include_final_newline: true))
44
45
  end
45
46
  end
46
47
  end
@@ -90,7 +90,7 @@ module RuboCop
90
90
  return suggested_strictness unless minimum_strictness
91
91
 
92
92
  # special case: if you're using Sorbet/IgnoreSigil without config, we should recommend `ignore`
93
- return "ignore" if minimum_strictness == "ignore" && cop_config["SuggestedStrictness"].nil? # rubocop:todo InternalAffairs/UndefinedConfig
93
+ return "ignore" if minimum_strictness == "ignore" && cop_config["SuggestedStrictness"].nil?
94
94
 
95
95
  # if a minimum strictness is set (eg. you're using Sorbet/FalseSigil)
96
96
  # we want to compare the minimum strictness and suggested strictness. this is because
@@ -161,24 +161,24 @@ module RuboCop
161
161
 
162
162
  # Default is `false`
163
163
  def require_sigil_on_all_files?
164
- !!cop_config["RequireSigilOnAllFiles"] # rubocop:todo InternalAffairs/UndefinedConfig
164
+ !!cop_config["RequireSigilOnAllFiles"]
165
165
  end
166
166
 
167
167
  # Default is `'false'`
168
168
  def suggested_strictness
169
- config = cop_config["SuggestedStrictness"].to_s # rubocop:todo InternalAffairs/UndefinedConfig
169
+ config = cop_config["SuggestedStrictness"].to_s
170
170
  STRICTNESS_LEVELS.include?(config) ? config : "false"
171
171
  end
172
172
 
173
173
  # Default is `nil`
174
174
  def minimum_strictness
175
- config = cop_config["MinimumStrictness"].to_s # rubocop:todo InternalAffairs/UndefinedConfig
175
+ config = cop_config["MinimumStrictness"].to_s
176
176
  config if STRICTNESS_LEVELS.include?(config)
177
177
  end
178
178
 
179
179
  # Default is `nil`
180
180
  def exact_strictness
181
- config = cop_config["ExactStrictness"].to_s # rubocop:todo InternalAffairs/UndefinedConfig
181
+ config = cop_config["ExactStrictness"].to_s
182
182
  config if STRICTNESS_LEVELS.include?(config)
183
183
  end
184
184
  end
@@ -56,7 +56,7 @@ module RuboCop
56
56
  return unless sig?(node.send_node)
57
57
 
58
58
  block = node.children.last
59
- return unless block.send_type?
59
+ return unless block&.send_type?
60
60
 
61
61
  receiver = block.receiver
62
62
  while receiver
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rubocop"
4
- require_relative "signature_cop"
5
-
6
3
  module RuboCop
7
4
  module Cop
8
5
  module Sorbet
@@ -19,8 +16,9 @@ module RuboCop
19
16
  #
20
17
  # # good
21
18
  # sig { void }
22
- class CheckedTrueInSignature < SignatureCop
19
+ class CheckedTrueInSignature < ::RuboCop::Cop::Cop # rubocop:todo InternalAffairs/InheritDeprecatedCopClass
23
20
  include(RuboCop::Cop::RangeHelp)
21
+ include(RuboCop::Cop::Sorbet::SignatureHelp)
24
22
 
25
23
  # @!method offending_node(node)
26
24
  def_node_search(:offending_node, <<~PATTERN)
@@ -1,16 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "signature_cop"
4
-
5
3
  module RuboCop
6
4
  module Cop
7
5
  module Sorbet
8
6
  # Checks for blank lines after signatures.
9
7
  #
10
- # It also suggests an autocorrect
11
- #
12
8
  # @example
13
- #
14
9
  # # bad
15
10
  # sig { void }
16
11
  #
@@ -19,37 +14,60 @@ module RuboCop
19
14
  # # good
20
15
  # sig { void }
21
16
  # def foo; end
22
- #
23
- class EmptyLineAfterSig < SignatureCop
17
+ class EmptyLineAfterSig < ::RuboCop::Cop::Base
18
+ extend AutoCorrector
24
19
  include RangeHelp
20
+ include SignatureHelp
25
21
 
26
- def on_signature(node)
27
- if (next_method(node).line - node.last_line) > 1
28
- location = source_range(processed_source.buffer, next_method(node).line - 1, 0)
29
- add_offense(node, location: location, message: "Extra empty line or comment detected")
30
- end
31
- end
22
+ MSG = "Extra empty line or comment detected"
23
+
24
+ # @!method sig_or_signable_method_definition?(node)
25
+ def_node_matcher :sig_or_signable_method_definition?, <<~PATTERN
26
+ ${
27
+ def
28
+ defs
29
+ (send nil? {:attr_reader :attr_writer :attr_accessor} ...)
30
+ #signature?
31
+ }
32
+ PATTERN
32
33
 
33
- def autocorrect(node)
34
- ->(corrector) do
35
- offending_range = node.source_range.with(
36
- begin_pos: node.source_range.end_pos + 1,
37
- end_pos: processed_source.buffer.line_range(next_method(node).line).begin_pos,
38
- )
39
- corrector.remove(offending_range)
40
- clean_range = offending_range.source.split("\n").reject(&:empty?).join("\n")
41
- offending_line = processed_source.buffer.line_range(node.source_range.first_line)
42
- corrector.insert_before(offending_line, "#{clean_range}\n") unless clean_range.empty?
34
+ def on_signature(sig)
35
+ sig_or_signable_method_definition?(next_sibling(sig)) do |definition|
36
+ range = lines_between(sig, definition)
37
+ next if range.empty? || range.single_line?
38
+
39
+ add_offense(range) do |corrector|
40
+ corrector.insert_before(
41
+ range_by_whole_lines(sig.source_range),
42
+ range.source
43
+ .sub(/\A\n+/, "") # remove initial newline(s)
44
+ .gsub(/\n{2,}/, "\n"), # remove empty line(s)
45
+ )
46
+ corrector.remove(range)
47
+ end
43
48
  end
44
49
  end
45
50
 
46
51
  private
47
52
 
48
- def next_method(node)
49
- processed_source.tokens.find do |t|
50
- t.line >= node.last_line &&
51
- (t.type == :kDEF || t.text.start_with?("attr_"))
52
- end
53
+ def next_sibling(node)
54
+ node.parent&.children&.at(node.sibling_index + 1)
55
+ end
56
+
57
+ def lines_between(node1, node2, buffer: processed_source.buffer)
58
+ end_of_node1_pos = node1.source_range.end_pos
59
+ start_of_node2_pos = node2.source_range.begin_pos
60
+
61
+ string_in_between = buffer.slice(end_of_node1_pos...start_of_node2_pos)
62
+ # Fallbacks handle same line edge case
63
+ begin_offset = string_in_between.index("\n") || 0
64
+ end_offset = string_in_between.rindex("\n") || string_in_between.length - 1
65
+
66
+ Parser::Source::Range.new(
67
+ buffer,
68
+ end_of_node1_pos + begin_offset + 1, # +1 to exclude post-node1 newline
69
+ end_of_node1_pos + end_offset + 1, # +1 to include pre-node2 newline
70
+ )
53
71
  end
54
72
  end
55
73
  end
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rubocop"
4
3
  require "stringio"
5
- require_relative "signature_cop"
6
4
 
7
5
  module RuboCop
8
6
  module Cop
@@ -26,7 +24,9 @@ module RuboCop
26
24
  #
27
25
  # * `ParameterTypePlaceholder`: placeholders used for parameter types (default: 'T.untyped')
28
26
  # * `ReturnTypePlaceholder`: placeholders used for return types (default: 'T.untyped')
29
- class EnforceSignatures < SignatureCop
27
+ class EnforceSignatures < ::RuboCop::Cop::Cop # rubocop:todo InternalAffairs/InheritDeprecatedCopClass
28
+ include SignatureHelp
29
+
30
30
  def initialize(config = nil, options = nil)
31
31
  super(config, options)
32
32
  @last_sig_for_scope = {}
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rubocop"
4
- require_relative "signature_cop"
5
-
6
3
  module RuboCop
7
4
  module Cop
8
5
  module Sorbet
@@ -20,7 +17,9 @@ module RuboCop
20
17
  # # good
21
18
  # sig { params(b: String, a: Integer).void }
22
19
  # def foo(b:, a: 1); end
23
- class KeywordArgumentOrdering < SignatureCop
20
+ class KeywordArgumentOrdering < ::RuboCop::Cop::Cop # rubocop:todo InternalAffairs/InheritDeprecatedCopClass
21
+ include SignatureHelp
22
+
24
23
  def on_signature(node)
25
24
  method_node = node.parent.children[node.sibling_index + 1]
26
25
  return if method_node.nil?
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rubocop"
4
- require_relative "signature_cop"
5
-
6
3
  begin
7
4
  require "unparser"
8
5
  rescue LoadError
@@ -12,7 +9,28 @@ end
12
9
  module RuboCop
13
10
  module Cop
14
11
  module Sorbet
15
- class SignatureBuildOrder < SignatureCop
12
+ # Checks for the correct order of sig builder methods:
13
+ # - abstract, override, or overridable
14
+ # - type_parameters
15
+ # - params
16
+ # - returns, or void
17
+ # - soft, checked, or on_failure
18
+ #
19
+ # @example
20
+ # # bad
21
+ # sig { void.abstract }
22
+ #
23
+ # # good
24
+ # sig { abstract.void }
25
+ #
26
+ # # bad
27
+ # sig { returns(Integer).params(x: Integer) }
28
+ #
29
+ # # good
30
+ # sig { params(x: Integer).returns(Integer) }
31
+ class SignatureBuildOrder < ::RuboCop::Cop::Cop # rubocop:todo InternalAffairs/InheritDeprecatedCopClass
32
+ include SignatureHelp
33
+
16
34
  ORDER =
17
35
  [
18
36
  :abstract,
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "sorbet/mixin/target_sorbet_version.rb"
4
+ require_relative "sorbet/mixin/signature_help.rb"
4
5
 
5
6
  require_relative "sorbet/binding_constant_without_type_alias"
6
7
  require_relative "sorbet/constants_from_strings"
@@ -10,11 +11,13 @@ require_relative "sorbet/forbid_untyped_struct_props"
10
11
  require_relative "sorbet/implicit_conversion_method"
11
12
  require_relative "sorbet/one_ancestor_per_line"
12
13
  require_relative "sorbet/callback_conditionals_binding"
14
+ require_relative "sorbet/forbid_t_struct"
13
15
  require_relative "sorbet/forbid_t_unsafe"
14
16
  require_relative "sorbet/forbid_t_untyped"
15
17
  require_relative "sorbet/redundant_extend_t_sig"
16
18
  require_relative "sorbet/type_alias_name"
17
19
  require_relative "sorbet/obsolete_strict_memoization"
20
+ require_relative "sorbet/buggy_obsolete_strict_memoization"
18
21
 
19
22
  require_relative "sorbet/rbi/forbid_extend_t_sig_helpers_in_shims"
20
23
  require_relative "sorbet/rbi/forbid_rbi_outside_of_allowed_paths"
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Sorbet
5
- VERSION = "0.7.3"
5
+ VERSION = "0.7.5"
6
6
  end
7
7
  end
data/manual/cops.md CHANGED
@@ -7,6 +7,7 @@ In the following section you find all available cops:
7
7
 
8
8
  * [Sorbet/AllowIncompatibleOverride](cops_sorbet.md#sorbetallowincompatibleoverride)
9
9
  * [Sorbet/BindingConstantWithoutTypeAlias](cops_sorbet.md#sorbetbindingconstantwithouttypealias)
10
+ * [Sorbet/BuggyObsoleteStrictMemoization](cops_sorbet.md#sorbetbuggyobsoletestrictmemoization)
10
11
  * [Sorbet/CallbackConditionalsBinding](cops_sorbet.md#sorbetcallbackconditionalsbinding)
11
12
  * [Sorbet/CheckedTrueInSignature](cops_sorbet.md#sorbetcheckedtrueinsignature)
12
13
  * [Sorbet/ConstantsFromStrings](cops_sorbet.md#sorbetconstantsfromstrings)
@@ -19,6 +20,7 @@ In the following section you find all available cops:
19
20
  * [Sorbet/ForbidIncludeConstLiteral](cops_sorbet.md#sorbetforbidincludeconstliteral)
20
21
  * [Sorbet/ForbidRBIOutsideOfAllowedPaths](cops_sorbet.md#sorbetforbidrbioutsideofallowedpaths)
21
22
  * [Sorbet/ForbidSuperclassConstLiteral](cops_sorbet.md#sorbetforbidsuperclassconstliteral)
23
+ * [Sorbet/ForbidTStruct](cops_sorbet.md#sorbetforbidtstruct)
22
24
  * [Sorbet/ForbidTUnsafe](cops_sorbet.md#sorbetforbidtunsafe)
23
25
  * [Sorbet/ForbidTUntyped](cops_sorbet.md#sorbetforbidtuntyped)
24
26
  * [Sorbet/ForbidUntypedStructProps](cops_sorbet.md#sorbetforbiduntypedstructprops)
@@ -30,7 +32,6 @@ In the following section you find all available cops:
30
32
  * [Sorbet/OneAncestorPerLine](cops_sorbet.md#sorbetoneancestorperline)
31
33
  * [Sorbet/RedundantExtendTSig](cops_sorbet.md#sorbetredundantextendtsig)
32
34
  * [Sorbet/SignatureBuildOrder](cops_sorbet.md#sorbetsignaturebuildorder)
33
- * [Sorbet/SignatureCop](cops_sorbet.md#sorbetsignaturecop)
34
35
  * [Sorbet/SingleLineRbiClassModuleDefinitions](cops_sorbet.md#sorbetsinglelinerbiclassmoduledefinitions)
35
36
  * [Sorbet/StrictSigil](cops_sorbet.md#sorbetstrictsigil)
36
37
  * [Sorbet/StrongSigil](cops_sorbet.md#sorbetstrongsigil)
@@ -41,6 +41,44 @@ FooOrBar = T.any(Foo, Bar)
41
41
  FooOrBar = T.type_alias { T.any(Foo, Bar) }
42
42
  ```
43
43
 
44
+ ## Sorbet/BuggyObsoleteStrictMemoization
45
+
46
+ Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged
47
+ --- | --- | --- | --- | ---
48
+ Enabled | Yes | Yes (Unsafe) | 0.7.3 | -
49
+
50
+ Checks for the a mistaken variant of the "obsolete memoization pattern" that used to be required
51
+ for older Sorbet versions in `#typed: strict` files. The mistaken variant would overwrite the ivar with `nil`
52
+ on every call, causing the memoized value to be discarded and recomputed on every call.
53
+
54
+ This cop will correct it to read from the ivar instead of `nil`, which will memoize it correctly.
55
+
56
+ The result of this correction will be the "obsolete memoization pattern", which can further be corrected by
57
+ the `Sorbet/ObsoleteStrictMemoization` cop.
58
+
59
+ See `Sorbet/ObsoleteStrictMemoization` for more details.
60
+
61
+ ### Examples
62
+
63
+ ```ruby
64
+ # bad
65
+ sig { returns(Foo) }
66
+ def foo
67
+ # This `nil` is likely a mistake, causing the memoized value to be discarded and recomputed on every call.
68
+ @foo = T.let(nil, T.nilable(Foo))
69
+ @foo ||= some_computation
70
+ end
71
+
72
+ # good
73
+ sig { returns(Foo) }
74
+ def foo
75
+ # This will now memoize the value as was likely intended, so `some_computation` is only ever called once.
76
+ # ⚠️If `some_computation` has side effects, this might be a breaking change!
77
+ @foo = T.let(@foo, T.nilable(Foo))
78
+ @foo ||= some_computation
79
+ end
80
+ ```
81
+
44
82
  ## Sorbet/CallbackConditionalsBinding
45
83
 
46
84
  Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged
@@ -146,8 +184,6 @@ Enabled | Yes | Yes | 0.7.0 | -
146
184
 
147
185
  Checks for blank lines after signatures.
148
186
 
149
- It also suggests an autocorrect
150
-
151
187
  ### Examples
152
188
 
153
189
  ```ruby
@@ -383,6 +419,45 @@ Name | Default value | Configurable values
383
419
  --- | --- | ---
384
420
  Exclude | `db/migrate/*.rb` | Array
385
421
 
422
+ ## Sorbet/ForbidTStruct
423
+
424
+ Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged
425
+ --- | --- | --- | --- | ---
426
+ Disabled | No | Yes | <<next>> | <<next>>
427
+
428
+ Disallow using `T::Struct` and `T::Props`.
429
+
430
+ ### Examples
431
+
432
+ ```ruby
433
+ # bad
434
+ class MyStruct < T::Struct
435
+ const :foo, String
436
+ prop :bar, Integer, default: 0
437
+
438
+ def some_method; end
439
+ end
440
+
441
+ # good
442
+ class MyStruct
443
+ extend T::Sig
444
+
445
+ sig { returns(String) }
446
+ attr_reader :foo
447
+
448
+ sig { returns(Integer) }
449
+ attr_accessor :bar
450
+
451
+ sig { params(foo: String, bar: Integer) }
452
+ def initialize(foo:, bar: 0)
453
+ @foo = foo
454
+ @bar = bar
455
+ end
456
+
457
+ def some_method; end
458
+ end
459
+ ```
460
+
386
461
  ## Sorbet/ForbidTUnsafe
387
462
 
388
463
  Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged
@@ -633,17 +708,28 @@ Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChan
633
708
  --- | --- | --- | --- | ---
634
709
  Enabled | Yes | Yes | 0.3.0 | -
635
710
 
636
- No documentation
711
+ Checks for the correct order of sig builder methods:
712
+ - abstract, override, or overridable
713
+ - type_parameters
714
+ - params
715
+ - returns, or void
716
+ - soft, checked, or on_failure
637
717
 
638
- ## Sorbet/SignatureCop
718
+ # bad
719
+ sig { returns(Integer).params(x: Integer) }
639
720
 
640
- Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged
641
- --- | --- | --- | --- | ---
642
- Enabled | Yes | No | - | -
721
+ # good
722
+ sig { params(x: Integer).returns(Integer) }
643
723
 
644
- Abstract cop specific to Sorbet signatures
724
+ ### Examples
645
725
 
646
- You can subclass it to use the `on_signature` trigger and the `signature?` node matcher.
726
+ ```ruby
727
+ # bad
728
+ sig { void.abstract }
729
+
730
+ # good
731
+ sig { abstract.void }
732
+ ```
647
733
 
648
734
  ## Sorbet/SingleLineRbiClassModuleDefinitions
649
735
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-sorbet
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.3
4
+ version: 0.7.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ufuk Kayserilioglu
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: exe
13
13
  cert_chain: []
14
- date: 2023-08-16 00:00:00.000000000 Z
14
+ date: 2023-10-31 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: rspec
@@ -87,14 +87,17 @@ files:
87
87
  - dev.yml
88
88
  - lib/rubocop-sorbet.rb
89
89
  - lib/rubocop/cop/sorbet/binding_constant_without_type_alias.rb
90
+ - lib/rubocop/cop/sorbet/buggy_obsolete_strict_memoization.rb
90
91
  - lib/rubocop/cop/sorbet/callback_conditionals_binding.rb
91
92
  - lib/rubocop/cop/sorbet/constants_from_strings.rb
92
93
  - lib/rubocop/cop/sorbet/forbid_include_const_literal.rb
93
94
  - lib/rubocop/cop/sorbet/forbid_superclass_const_literal.rb
95
+ - lib/rubocop/cop/sorbet/forbid_t_struct.rb
94
96
  - lib/rubocop/cop/sorbet/forbid_t_unsafe.rb
95
97
  - lib/rubocop/cop/sorbet/forbid_t_untyped.rb
96
98
  - lib/rubocop/cop/sorbet/forbid_untyped_struct_props.rb
97
99
  - lib/rubocop/cop/sorbet/implicit_conversion_method.rb
100
+ - lib/rubocop/cop/sorbet/mixin/signature_help.rb
98
101
  - lib/rubocop/cop/sorbet/mixin/target_sorbet_version.rb
99
102
  - lib/rubocop/cop/sorbet/mutable_constant_sorbet_aware_behaviour.rb
100
103
  - lib/rubocop/cop/sorbet/obsolete_strict_memoization.rb
@@ -118,7 +121,6 @@ files:
118
121
  - lib/rubocop/cop/sorbet/signatures/enforce_signatures.rb
119
122
  - lib/rubocop/cop/sorbet/signatures/keyword_argument_ordering.rb
120
123
  - lib/rubocop/cop/sorbet/signatures/signature_build_order.rb
121
- - lib/rubocop/cop/sorbet/signatures/signature_cop.rb
122
124
  - lib/rubocop/cop/sorbet/type_alias_name.rb
123
125
  - lib/rubocop/cop/sorbet_cops.rb
124
126
  - lib/rubocop/sorbet.rb
@@ -152,7 +154,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
152
154
  - !ruby/object:Gem::Version
153
155
  version: '0'
154
156
  requirements: []
155
- rubygems_version: 3.4.18
157
+ rubygems_version: 3.4.21
156
158
  signing_key:
157
159
  specification_version: 4
158
160
  summary: Automatic Sorbet code style checking tool.