rubocop-sorbet 0.5.1 → 0.6.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.github/release.yml +23 -0
  3. data/.github/workflows/ci.yml +26 -0
  4. data/.gitignore +1 -0
  5. data/Gemfile +1 -0
  6. data/Gemfile.lock +25 -41
  7. data/README.md +22 -0
  8. data/Rakefile +12 -16
  9. data/bin/console +3 -3
  10. data/bin/rspec +6 -6
  11. data/bin/rubocop +29 -0
  12. data/config/default.yml +94 -10
  13. data/config/rbi.yml +262 -0
  14. data/dev.yml +1 -1
  15. data/lib/rubocop/cop/sorbet/binding_constants_without_type_alias.rb +4 -4
  16. data/lib/rubocop/cop/sorbet/callback_conditionals_binding.rb +142 -0
  17. data/lib/rubocop/cop/sorbet/constants_from_strings.rb +1 -1
  18. data/lib/rubocop/cop/sorbet/forbid_include_const_literal.rb +11 -2
  19. data/lib/rubocop/cop/sorbet/forbid_superclass_const_literal.rb +2 -2
  20. data/lib/rubocop/cop/sorbet/forbid_t_unsafe.rb +26 -0
  21. data/lib/rubocop/cop/sorbet/forbid_untyped_struct_props.rb +2 -2
  22. data/lib/rubocop/cop/sorbet/one_ancestor_per_line.rb +75 -0
  23. data/lib/rubocop/cop/sorbet/rbi/forbid_extend_t_sig_helpers_in_shims.rb +53 -0
  24. data/lib/rubocop/cop/sorbet/rbi/forbid_rbi_outside_of_allowed_paths.rb +47 -0
  25. data/lib/rubocop/cop/sorbet/rbi/single_line_rbi_class_module_definitions.rb +46 -0
  26. data/lib/rubocop/cop/sorbet/sigils/enforce_sigil_order.rb +6 -6
  27. data/lib/rubocop/cop/sorbet/sigils/enforce_single_sigil.rb +63 -0
  28. data/lib/rubocop/cop/sorbet/sigils/false_sigil.rb +3 -3
  29. data/lib/rubocop/cop/sorbet/sigils/has_sigil.rb +2 -2
  30. data/lib/rubocop/cop/sorbet/sigils/ignore_sigil.rb +3 -3
  31. data/lib/rubocop/cop/sorbet/sigils/strict_sigil.rb +3 -3
  32. data/lib/rubocop/cop/sorbet/sigils/strong_sigil.rb +3 -3
  33. data/lib/rubocop/cop/sorbet/sigils/true_sigil.rb +3 -3
  34. data/lib/rubocop/cop/sorbet/sigils/valid_sigil.rb +10 -8
  35. data/lib/rubocop/cop/sorbet/signatures/allow_incompatible_override.rb +3 -3
  36. data/lib/rubocop/cop/sorbet/signatures/checked_true_in_signature.rb +6 -6
  37. data/lib/rubocop/cop/sorbet/signatures/enforce_signatures.rb +30 -21
  38. data/lib/rubocop/cop/sorbet/signatures/keyword_argument_ordering.rb +3 -3
  39. data/lib/rubocop/cop/sorbet/signatures/signature_build_order.rb +24 -15
  40. data/lib/rubocop/cop/sorbet/signatures/signature_cop.rb +17 -2
  41. data/lib/rubocop/cop/sorbet_cops.rb +26 -19
  42. data/lib/rubocop/sorbet/version.rb +1 -1
  43. data/lib/rubocop/sorbet.rb +1 -1
  44. data/lib/rubocop-sorbet.rb +5 -5
  45. data/manual/cops.md +7 -1
  46. data/manual/cops_sorbet.md +247 -14
  47. data/rubocop-sorbet.gemspec +2 -2
  48. data/service.yml +0 -5
  49. data/tasks/cops_documentation.rake +60 -62
  50. metadata +20 -12
  51. data/.shopify-build/VERSION +0 -1
  52. data/.shopify-build/rubocop-sorbet.yml +0 -16
  53. data/lib/rubocop/cop/sorbet/signatures/parameters_ordering_in_signature.rb +0 -70
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Sorbet
6
+ # This cop makes sure that RBI files are always located under the defined allowed paths.
7
+ #
8
+ # Options:
9
+ #
10
+ # * `AllowedPaths`: A list of the paths where RBI files are allowed (default: ["sorbet/rbi/**"])
11
+ #
12
+ # @example
13
+ # # bad
14
+ # # lib/some_file.rbi
15
+ # # other_file.rbi
16
+ #
17
+ # # good
18
+ # # sorbet/rbi/some_file.rbi
19
+ # # sorbet/rbi/any/path/for/file.rbi
20
+ class ForbidRBIOutsideOfAllowedPaths < RuboCop::Cop::Cop
21
+ include RangeHelp
22
+
23
+ def investigate(processed_source)
24
+ add_offense(
25
+ nil,
26
+ location: source_range(processed_source.buffer, 1, 0),
27
+ message: message
28
+ ) if allowed_paths.none? { |pattern| File.fnmatch(pattern, processed_source.file_path) }
29
+ end
30
+
31
+ private
32
+
33
+ def allowed_paths
34
+ cop_config["AllowedPaths"]&.compact || []
35
+ end
36
+
37
+ def message
38
+ if allowed_paths.empty?
39
+ "RBI files should be located in an allowed path, but AllowedPaths is empty or nil"
40
+ else
41
+ "RBI file path should match one of: #{allowed_paths.join(", ")}"
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Sorbet
6
+ # This cop ensures empty class/module definitions in RBI files are
7
+ # done on a single line rather than being split across multiple lines.
8
+ #
9
+ # @example
10
+ #
11
+ # # bad
12
+ # module SomeModule
13
+ # end
14
+ #
15
+ # # good
16
+ # module SomeModule; end
17
+ class SingleLineRbiClassModuleDefinitions < RuboCop::Cop::Cop
18
+ MSG = "Empty class/module definitions in RBI files should be on a single line."
19
+
20
+ def on_module(node)
21
+ process_node(node)
22
+ end
23
+
24
+ def on_class(node)
25
+ process_node(node)
26
+ end
27
+
28
+ def autocorrect(node)
29
+ -> (corrector) { corrector.replace(node, convert_newlines(node.source)) }
30
+ end
31
+
32
+ protected
33
+
34
+ def convert_newlines(source)
35
+ source.sub(/[\r\n]+\s*[\r\n]*/, "; ")
36
+ end
37
+
38
+ def process_node(node)
39
+ return if node.body
40
+ return if node.single_line?
41
+ add_offense(node)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rubocop'
3
+ require "rubocop"
4
4
 
5
5
  module RuboCop
6
6
  module Cop
@@ -69,10 +69,10 @@ module RuboCop
69
69
  FROZEN_REGEX = /#\s+frozen_string_literal:(?:\s+([\w]+))?/
70
70
 
71
71
  PREFERRED_ORDER = {
72
- CODING_REGEX => 'encoding',
73
- SIGIL_REGEX => 'typed',
74
- INDENT_REGEX => 'warn_indent',
75
- FROZEN_REGEX => 'frozen_string_literal',
72
+ CODING_REGEX => "encoding",
73
+ SIGIL_REGEX => "typed",
74
+ INDENT_REGEX => "warn_indent",
75
+ FROZEN_REGEX => "frozen_string_literal",
76
76
  }.freeze
77
77
 
78
78
  MAGIC_REGEX = Regexp.union(*PREFERRED_ORDER.keys)
@@ -104,7 +104,7 @@ module RuboCop
104
104
  add_offense(
105
105
  token,
106
106
  location: token.pos,
107
- message: "Magic comments should be in the following order: #{PREFERRED_ORDER.values.join(', ')}."
107
+ message: "Magic comments should be in the following order: #{PREFERRED_ORDER.values.join(", ")}."
108
108
  )
109
109
  end
110
110
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Sorbet
6
+ # This cop checks that there is only one Sorbet sigil in a given file
7
+ #
8
+ # For example, the following class with two sigils
9
+ #
10
+ # ```ruby
11
+ # # typed: true
12
+ # # typed: true
13
+ # # frozen_string_literal: true
14
+ # class Foo; end
15
+ # ```
16
+ #
17
+ # Will be corrected as:
18
+ #
19
+ # ```ruby
20
+ # # typed: true
21
+ # # frozen_string_literal: true
22
+ # class Foo; end
23
+ # ```
24
+ #
25
+ # Other comments or magic comments are left in place.
26
+ class EnforceSingleSigil < ValidSigil
27
+ include RangeHelp
28
+
29
+ def investigate(processed_source)
30
+ return if processed_source.tokens.empty?
31
+ sigils = extract_all_sigils(processed_source)
32
+ return unless sigils.size > 1
33
+
34
+ sigils[1..sigils.size].each do |token|
35
+ add_offense(token, location: token.pos, message: "Files must only contain one sigil")
36
+ end
37
+ end
38
+
39
+ def autocorrect(_node)
40
+ -> (corrector) do
41
+ sigils = extract_all_sigils(processed_source)
42
+ return unless sigils.size > 1
43
+
44
+ # The first sigil encountered represents the "real" strictness so remove any below
45
+ sigils[1..sigils.size].each do |token|
46
+ corrector.remove(
47
+ source_range(processed_source.buffer, token.line, (0..token.pos.last_column))
48
+ )
49
+ end
50
+ end
51
+ end
52
+
53
+ protected
54
+
55
+ def extract_all_sigils(processed_source)
56
+ processed_source.tokens
57
+ .take_while { |token| token.type == :tCOMMENT }
58
+ .find_all { |token| SIGIL_REGEX.match?(token.text) }
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rubocop'
4
- require_relative 'has_sigil'
3
+ require "rubocop"
4
+ require_relative "has_sigil"
5
5
 
6
6
  module RuboCop
7
7
  module Cop
@@ -9,7 +9,7 @@ module RuboCop
9
9
  # This cop makes the Sorbet `false` sigil mandatory in all files.
10
10
  class FalseSigil < HasSigil
11
11
  def minimum_strictness
12
- 'false'
12
+ "false"
13
13
  end
14
14
  end
15
15
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rubocop'
4
- require_relative 'valid_sigil'
3
+ require "rubocop"
4
+ require_relative "valid_sigil"
5
5
 
6
6
  module RuboCop
7
7
  module Cop
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rubocop'
4
- require_relative 'has_sigil'
3
+ require "rubocop"
4
+ require_relative "has_sigil"
5
5
 
6
6
  module RuboCop
7
7
  module Cop
@@ -9,7 +9,7 @@ module RuboCop
9
9
  # This cop makes the Sorbet `ignore` sigil mandatory in all files.
10
10
  class IgnoreSigil < HasSigil
11
11
  def minimum_strictness
12
- 'ignore'
12
+ "ignore"
13
13
  end
14
14
  end
15
15
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rubocop'
4
- require_relative 'has_sigil'
3
+ require "rubocop"
4
+ require_relative "has_sigil"
5
5
 
6
6
  module RuboCop
7
7
  module Cop
@@ -9,7 +9,7 @@ module RuboCop
9
9
  # This cop makes the Sorbet `strict` sigil mandatory in all files.
10
10
  class StrictSigil < HasSigil
11
11
  def minimum_strictness
12
- 'strict'
12
+ "strict"
13
13
  end
14
14
  end
15
15
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rubocop'
4
- require_relative 'has_sigil'
3
+ require "rubocop"
4
+ require_relative "has_sigil"
5
5
 
6
6
  module RuboCop
7
7
  module Cop
@@ -9,7 +9,7 @@ module RuboCop
9
9
  # This cop makes the Sorbet `strong` sigil mandatory in all files.
10
10
  class StrongSigil < HasSigil
11
11
  def minimum_strictness
12
- 'strong'
12
+ "strong"
13
13
  end
14
14
  end
15
15
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rubocop'
4
- require_relative 'has_sigil'
3
+ require "rubocop"
4
+ require_relative "has_sigil"
5
5
 
6
6
  module RuboCop
7
7
  module Cop
@@ -9,7 +9,7 @@ module RuboCop
9
9
  # This cop makes the Sorbet `true` sigil mandatory in all files.
10
10
  class TrueSigil < HasSigil
11
11
  def minimum_strictness
12
- 'true'
12
+ "true"
13
13
  end
14
14
  end
15
15
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rubocop'
3
+ require "rubocop"
4
4
 
5
5
  module RuboCop
6
6
  module Cop
@@ -48,7 +48,7 @@ module RuboCop
48
48
 
49
49
  protected
50
50
 
51
- STRICTNESS_LEVELS = %w(ignore false true strict strong)
51
+ STRICTNESS_LEVELS = ["ignore", "false", "true", "strict", "strong"]
52
52
  SIGIL_REGEX = /#\s+typed:(?:\s+([\w]+))?/
53
53
 
54
54
  # extraction
@@ -74,7 +74,7 @@ module RuboCop
74
74
  add_offense(
75
75
  token,
76
76
  location: token.pos,
77
- message: 'No Sorbet sigil found in file. ' \
77
+ message: "No Sorbet sigil found in file. " \
78
78
  "Try a `typed: #{strictness}` to start (you can also use `rubocop -a` to automatically add this)."
79
79
  )
80
80
  end
@@ -87,7 +87,7 @@ module RuboCop
87
87
  return suggested_strictness unless minimum_strictness
88
88
 
89
89
  # special case: if you're using Sorbet/IgnoreSigil without config, we should recommend `ignore`
90
- return "ignore" if minimum_strictness == "ignore" && cop_config['SuggestedStrictness'].nil?
90
+ return "ignore" if minimum_strictness == "ignore" && cop_config["SuggestedStrictness"].nil?
91
91
 
92
92
  # if a minimum strictness is set (eg. you're using Sorbet/FalseSigil)
93
93
  # we want to compare the minimum strictness and suggested strictness. this is because
@@ -106,7 +106,7 @@ module RuboCop
106
106
  add_offense(
107
107
  sigil,
108
108
  location: sigil.pos,
109
- message: 'Sorbet sigil should not be empty.'
109
+ message: "Sorbet sigil should not be empty."
110
110
  )
111
111
  false
112
112
  end
@@ -142,17 +142,19 @@ module RuboCop
142
142
 
143
143
  # Default is `false`
144
144
  def require_sigil_on_all_files?
145
- !!cop_config['RequireSigilOnAllFiles']
145
+ !!cop_config["RequireSigilOnAllFiles"]
146
146
  end
147
147
 
148
148
  # Default is `'false'`
149
149
  def suggested_strictness
150
- STRICTNESS_LEVELS.include?(cop_config['SuggestedStrictness']) ? cop_config['SuggestedStrictness'] : 'false'
150
+ config = cop_config["SuggestedStrictness"].to_s
151
+ STRICTNESS_LEVELS.include?(config) ? config : "false"
151
152
  end
152
153
 
153
154
  # Default is `nil`
154
155
  def minimum_strictness
155
- cop_config['MinimumStrictness'] if STRICTNESS_LEVELS.include?(cop_config['MinimumStrictness'])
156
+ config = cop_config["MinimumStrictness"].to_s
157
+ config if STRICTNESS_LEVELS.include?(config)
156
158
  end
157
159
  end
158
160
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rubocop'
3
+ require "rubocop"
4
4
 
5
5
  module RuboCop
6
6
  module Cop
@@ -49,8 +49,8 @@ module RuboCop
49
49
  return unless allow_incompatible_override?(node)
50
50
  add_offense(
51
51
  node.children[2],
52
- message: 'Usage of `allow_incompatible` suggests a violation of the Liskov Substitution Principle. '\
53
- 'Instead, strive to write interfaces which respect subtyping principles and remove `allow_incompatible`',
52
+ message: "Usage of `allow_incompatible` suggests a violation of the Liskov Substitution Principle. "\
53
+ "Instead, strive to write interfaces which respect subtyping principles and remove `allow_incompatible`",
54
54
  )
55
55
  end
56
56
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rubocop'
4
- require_relative 'signature_cop'
3
+ require "rubocop"
4
+ require_relative "signature_cop"
5
5
 
6
6
  module RuboCop
7
7
  module Cop
@@ -27,10 +27,10 @@ module RuboCop
27
27
  PATTERN
28
28
 
29
29
  MESSAGE =
30
- 'Using `checked(true)` in a method signature definition is not allowed. ' \
31
- '`checked(true)` is the default behavior for modules/classes with runtime checks enabled. ' \
32
- 'To enable typechecking at runtime for this module, regardless of global settings, ' \
33
- '`include(WaffleCone::RuntimeChecks)` to this module and set other methods to `checked(false)`.'
30
+ "Using `checked(true)` in a method signature definition is not allowed. " \
31
+ "`checked(true)` is the default behavior for modules/classes with runtime checks enabled. " \
32
+ "To enable typechecking at runtime for this module, regardless of global settings, " \
33
+ "`include(WaffleCone::RuntimeChecks)` to this module and set other methods to `checked(false)`."
34
34
  private_constant(:MESSAGE)
35
35
 
36
36
  def on_signature(node)
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rubocop'
4
- require 'stringio'
5
- require_relative 'signature_cop'
3
+ require "rubocop"
4
+ require "stringio"
5
+ require_relative "signature_cop"
6
6
 
7
7
  module RuboCop
8
8
  module Cop
@@ -27,6 +27,11 @@ module RuboCop
27
27
  # * `ParameterTypePlaceholder`: placeholders used for parameter types (default: 'T.untyped')
28
28
  # * `ReturnTypePlaceholder`: placeholders used for return types (default: 'T.untyped')
29
29
  class EnforceSignatures < SignatureCop
30
+ def initialize(config = nil, options = nil)
31
+ super(config, options)
32
+ @last_sig_for_scope = {}
33
+ end
34
+
30
35
  def_node_matcher(:accessor?, <<-PATTERN)
31
36
  (send nil? {:attr_reader :attr_writer :attr_accessor} ...)
32
37
  PATTERN
@@ -40,8 +45,11 @@ module RuboCop
40
45
  end
41
46
 
42
47
  def on_send(node)
43
- return unless accessor?(node)
44
- check_node(node)
48
+ check_node(node) if accessor?(node)
49
+ end
50
+
51
+ def on_signature(node)
52
+ @last_sig_for_scope[scope(node)] = node
45
53
  end
46
54
 
47
55
  def autocorrect(node)
@@ -56,37 +64,38 @@ module RuboCop
56
64
  method = node.children[1]
57
65
  symbol = node.children[2]
58
66
  suggest.params << symbol.value if symbol && (method == :attr_writer || method == :attr_accessor)
59
- suggest.returns = 'void' if method == :attr_writer
67
+ suggest.returns = "void" if method == :attr_writer
60
68
  end
61
69
 
62
70
  corrector.insert_before(node.loc.expression, suggest.to_autocorrect)
63
71
  end
64
72
  end
65
73
 
74
+ def scope(node)
75
+ return nil unless node.parent
76
+ return node.parent if [:begin, :block, :class, :module].include?(node.parent.type)
77
+ scope(node.parent)
78
+ end
79
+
66
80
  private
67
81
 
68
82
  def check_node(node)
69
- prev = previous_node(node)
70
- unless signature?(prev)
83
+ scope = self.scope(node)
84
+ unless @last_sig_for_scope[scope]
71
85
  add_offense(
72
86
  node,
73
87
  message: "Each method is required to have a signature."
74
88
  )
75
89
  end
76
- end
77
-
78
- def previous_node(node)
79
- parent = node.parent
80
- return nil unless parent
81
- parent.children[node.sibling_index - 1]
90
+ @last_sig_for_scope[scope] = nil
82
91
  end
83
92
 
84
93
  def param_type_placeholder
85
- cop_config['ParameterTypePlaceholder'] || 'T.untyped'
94
+ cop_config["ParameterTypePlaceholder"] || "T.untyped"
86
95
  end
87
96
 
88
97
  def return_type_placeholder
89
- cop_config['ReturnTypePlaceholder'] || 'T.untyped'
98
+ cop_config["ReturnTypePlaceholder"] || "T.untyped"
90
99
  end
91
100
 
92
101
  class SigSuggestion
@@ -102,11 +111,11 @@ module RuboCop
102
111
 
103
112
  def to_autocorrect
104
113
  out = StringIO.new
105
- out << 'sig { '
114
+ out << "sig { "
106
115
  out << generate_params
107
116
  out << generate_return
108
117
  out << " }\n"
109
- out << ' ' * @indent # preserve indent for the next line
118
+ out << " " * @indent # preserve indent for the next line
110
119
  out.string
111
120
  end
112
121
 
@@ -115,17 +124,17 @@ module RuboCop
115
124
  def generate_params
116
125
  return if @params.empty?
117
126
  out = StringIO.new
118
- out << 'params('
127
+ out << "params("
119
128
  out << @params.map do |param|
120
129
  "#{param}: #{@param_placeholder}"
121
130
  end.join(", ")
122
- out << ').'
131
+ out << ")."
123
132
  out.string
124
133
  end
125
134
 
126
135
  def generate_return
127
136
  return "returns(#{@return_placeholder})" if @returns.nil?
128
- return @returns if @returns == 'void'
137
+ return @returns if @returns == "void"
129
138
  "returns(#{@returns})"
130
139
  end
131
140
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rubocop'
4
- require_relative 'signature_cop'
3
+ require "rubocop"
4
+ require_relative "signature_cop"
5
5
 
6
6
  module RuboCop
7
7
  module Cop
@@ -41,7 +41,7 @@ module RuboCop
41
41
 
42
42
  add_offense(
43
43
  param,
44
- message: 'Optional keyword arguments must be at the end of the parameter list.'
44
+ message: "Optional keyword arguments must be at the end of the parameter list."
45
45
  )
46
46
  end
47
47
  end
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rubocop'
4
- require_relative 'signature_cop'
3
+ require "rubocop"
4
+ require_relative "signature_cop"
5
5
 
6
6
  begin
7
- require 'unparser'
7
+ require "unparser"
8
8
  rescue LoadError
9
9
  nil
10
10
  end
@@ -28,7 +28,7 @@ module RuboCop
28
28
  ].each_with_index.to_h.freeze
29
29
 
30
30
  def_node_search(:root_call, <<~PATTERN)
31
- (send nil? {#{ORDER.keys.map(&:inspect).join(' ')}} ...)
31
+ (send nil? {#{ORDER.keys.map(&:inspect).join(" ")}} ...)
32
32
  PATTERN
33
33
 
34
34
  def on_signature(node)
@@ -38,10 +38,10 @@ module RuboCop
38
38
  expected_order = calls.sort_by { |call| ORDER[call] }
39
39
  return if expected_order == calls
40
40
 
41
- message = "Sig builders must be invoked in the following order: #{expected_order.join(', ')}."
41
+ message = "Sig builders must be invoked in the following order: #{expected_order.join(", ")}."
42
42
 
43
43
  unless can_autocorrect?
44
- message += ' For autocorrection, add the `unparser` gem to your project.'
44
+ message += " For autocorrection, add the `unparser` gem to your project."
45
45
  end
46
46
 
47
47
  add_offense(
@@ -55,7 +55,7 @@ module RuboCop
55
55
  return nil unless can_autocorrect?
56
56
 
57
57
  lambda do |corrector|
58
- tree = call_chain(node_with_index_sends(node))
58
+ tree = call_chain(node_reparsed_with_modern_features(node))
59
59
  .sort_by { |call| ORDER[call.method_name] }
60
60
  .reduce(nil) do |receiver, caller|
61
61
  caller.updated(nil, [receiver] + caller.children.drop(1))
@@ -68,16 +68,25 @@ module RuboCop
68
68
  end
69
69
  end
70
70
 
71
+ # Create a subclass of AST Builder that has modern features turned on
72
+ class ModernBuilder < RuboCop::AST::Builder
73
+ modernize
74
+ end
75
+ private_constant :ModernBuilder
76
+
71
77
  private
72
78
 
73
- def node_with_index_sends(node)
74
- # This is really dirty hack to reparse the current node with index send
75
- # emitting enabled, which is necessary to unparse them back as index accessors.
76
- emit_index_value = RuboCop::AST::Builder.emit_index
77
- RuboCop::AST::Builder.emit_index = true
78
- RuboCop::AST::ProcessedSource.new(node.source, target_ruby_version, processed_source.path).ast
79
- ensure
80
- RuboCop::AST::Builder.emit_index = emit_index_value
79
+ # This method exists to reparse the current node with modern features enabled.
80
+ # Modern features include "index send" emitting, which is necessary to unparse
81
+ # "index sends" (i.e. `[]` calls) back to index accessors (i.e. as `foo[bar]``).
82
+ # Otherwise, we would get the unparsed node as `foo.[](bar)`.
83
+ def node_reparsed_with_modern_features(node)
84
+ # Create a new parser with a modern builder class instance
85
+ parser = Parser::CurrentRuby.new(ModernBuilder.new)
86
+ # Create a new source buffer with the node source
87
+ buffer = Parser::Source::Buffer.new(processed_source.path, source: node.source)
88
+ # Re-parse the buffer
89
+ parser.parse(buffer)
81
90
  end
82
91
 
83
92
  def can_autocorrect?
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rubocop'
3
+ require "rubocop"
4
4
 
5
5
  module RuboCop
6
6
  module Cop
@@ -12,9 +12,24 @@ module RuboCop
12
12
  @registry = Cop.registry # So we can properly subclass this cop
13
13
 
14
14
  def_node_matcher(:signature?, <<~PATTERN)
15
- (block (send nil? :sig) (args) ...)
15
+ (block (send #allowed_recv :sig) (args) ...)
16
16
  PATTERN
17
17
 
18
+ def_node_matcher(:with_runtime?, <<~PATTERN)
19
+ (const (const nil? :T) :Sig)
20
+ PATTERN
21
+
22
+ def_node_matcher(:without_runtime?, <<~PATTERN)
23
+ (const (const (const nil? :T) :Sig) :WithoutRuntime)
24
+ PATTERN
25
+
26
+ def allowed_recv(recv)
27
+ return true unless recv
28
+ return true if with_runtime?(recv)
29
+ return true if without_runtime?(recv)
30
+ false
31
+ end
32
+
18
33
  def on_block(node)
19
34
  on_signature(node) if signature?(node)
20
35
  end