rubocop-sorbet 0.3.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +2 -0
  3. data/.github/probots.yml +1 -1
  4. data/.github/stale.yml +20 -0
  5. data/.gitignore +0 -2
  6. data/.rspec +3 -0
  7. data/.rubocop.yml +2 -12
  8. data/.travis.yml +3 -1
  9. data/Gemfile +6 -7
  10. data/Gemfile.lock +26 -41
  11. data/README.md +64 -6
  12. data/Rakefile +34 -5
  13. data/config/default.yml +124 -1
  14. data/lib/rubocop-sorbet.rb +9 -1
  15. data/lib/rubocop/cop/sorbet/binding_constants_without_type_alias.rb +21 -2
  16. data/lib/rubocop/cop/sorbet/forbid_untyped_struct_props.rb +58 -0
  17. data/lib/rubocop/cop/sorbet/sigils/enforce_sigil_order.rb +105 -0
  18. data/lib/rubocop/cop/sorbet/sigils/valid_sigil.rb +29 -4
  19. data/lib/rubocop/cop/sorbet/{allow_incompatible_override.rb → signatures/allow_incompatible_override.rb} +0 -0
  20. data/lib/rubocop/cop/sorbet/{checked_true_in_signature.rb → signatures/checked_true_in_signature.rb} +3 -8
  21. data/lib/rubocop/cop/sorbet/signatures/enforce_signatures.rb +135 -0
  22. data/lib/rubocop/cop/sorbet/{keyword_argument_ordering.rb → signatures/keyword_argument_ordering.rb} +4 -9
  23. data/lib/rubocop/cop/sorbet/{parameters_ordering_in_signature.rb → signatures/parameters_ordering_in_signature.rb} +17 -14
  24. data/lib/rubocop/cop/sorbet/{signature_build_order.rb → signatures/signature_build_order.rb} +6 -12
  25. data/lib/rubocop/cop/sorbet/signatures/signature_cop.rb +28 -0
  26. data/lib/rubocop/cop/sorbet_cops.rb +22 -0
  27. data/lib/rubocop/sorbet.rb +15 -0
  28. data/lib/rubocop/sorbet/inject.rb +20 -0
  29. data/lib/rubocop/sorbet/version.rb +6 -0
  30. data/manual/cops.md +29 -0
  31. data/manual/cops_sorbet.md +340 -0
  32. data/rubocop-sorbet.gemspec +18 -18
  33. data/tasks/cops_documentation.rake +317 -0
  34. metadata +31 -17
  35. data/lib/rubocop_sorbet.rb +0 -19
@@ -1,3 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rubocop_sorbet'
3
+ require 'rubocop'
4
+
5
+ require_relative 'rubocop/sorbet'
6
+ require_relative 'rubocop/sorbet/version'
7
+ require_relative 'rubocop/sorbet/inject'
8
+
9
+ RuboCop::Sorbet::Inject.defaults!
10
+
11
+ require_relative 'rubocop/cop/sorbet_cops'
@@ -14,13 +14,22 @@ module RuboCop
14
14
  # FooOrBar = T.any(Foo, Bar)
15
15
  #
16
16
  # # good
17
- # FooOrBar = T.type_alias(T.any(Foo, Bar))
17
+ # FooOrBar = T.type_alias { T.any(Foo, Bar) }
18
18
  class BindingConstantWithoutTypeAlias < RuboCop::Cop::Cop
19
19
  def_node_matcher(:binding_unaliased_type?, <<-PATTERN)
20
20
  (casgn _ _ [#not_nil? #not_t_let? #method_needing_aliasing_on_t?])
21
21
  PATTERN
22
22
 
23
23
  def_node_matcher(:using_type_alias?, <<-PATTERN)
24
+ (block
25
+ (send
26
+ (const nil? :T) :type_alias)
27
+ _
28
+ _
29
+ )
30
+ PATTERN
31
+
32
+ def_node_matcher(:using_deprecated_type_alias_syntax?, <<-PATTERN)
24
33
  (
25
34
  send
26
35
  (const nil? :T)
@@ -58,6 +67,16 @@ module RuboCop
58
67
 
59
68
  def on_casgn(node)
60
69
  return unless binding_unaliased_type?(node) && !using_type_alias?(node.children[2])
70
+ if using_deprecated_type_alias_syntax?(node.children[2])
71
+ add_offense(
72
+ node.children[2],
73
+ message: "It looks like you're using the old `T.type_alias` syntax. " \
74
+ '`T.type_alias` now expects a block.' \
75
+ 'Run Sorbet with the options "--autocorrect --error-white-list=5043" ' \
76
+ 'to automatically upgrade to the new syntax.'
77
+ )
78
+ return
79
+ end
61
80
  add_offense(
62
81
  node.children[2],
63
82
  message: "It looks like you're trying to bind a type to a constant. " \
@@ -69,7 +88,7 @@ module RuboCop
69
88
  lambda do |corrector|
70
89
  corrector.replace(
71
90
  node.source_range,
72
- "T.type_alias(#{node.source})"
91
+ "T.type_alias { #{node.source} }"
73
92
  )
74
93
  end
75
94
  end
@@ -0,0 +1,58 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require 'rubocop'
5
+
6
+ module RuboCop
7
+ module Cop
8
+ module Sorbet
9
+ # This cop disallows use of `T.untyped` or `T.nilable(T.untyped)`
10
+ # as a prop type for `T::Struct`.
11
+ #
12
+ # @example
13
+ #
14
+ # # bad
15
+ # class SomeClass
16
+ # const :foo, T.untyped
17
+ # prop :bar, T.nilable(T.untyped)
18
+ # end
19
+ #
20
+ # # good
21
+ # class SomeClass
22
+ # const :foo, Integer
23
+ # prop :bar, T.nilable(String)
24
+ # end
25
+ class ForbidUntypedStructProps < RuboCop::Cop::Cop
26
+ MSG = 'Struct props cannot be T.untyped'
27
+
28
+ def_node_matcher :t_struct, <<~PATTERN
29
+ (const (const nil? :T) :Struct)
30
+ PATTERN
31
+
32
+ def_node_matcher :t_untyped, <<~PATTERN
33
+ (send (const nil? :T) :untyped)
34
+ PATTERN
35
+
36
+ def_node_matcher :t_nilable_untyped, <<~PATTERN
37
+ (send (const nil? :T) :nilable {#t_untyped #t_nilable_untyped})
38
+ PATTERN
39
+
40
+ def_node_matcher :subclass_of_t_struct?, <<~PATTERN
41
+ (class (const ...) #t_struct ...)
42
+ PATTERN
43
+
44
+ def_node_search :untyped_props, <<~PATTERN
45
+ (send nil? {:prop :const} _ {#t_untyped #t_nilable_untyped} ...)
46
+ PATTERN
47
+
48
+ def on_class(node)
49
+ return unless subclass_of_t_struct?(node)
50
+
51
+ untyped_props(node).each do |untyped_prop|
52
+ add_offense(untyped_prop.child_nodes[1])
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Sorbet
8
+ # This cop checks that the Sorbet sigil comes as the first magic comment in the file.
9
+ #
10
+ # The expected order for magic comments is: typed, (en)?coding, warn_indent then frozen_string_literal.
11
+ #
12
+ # For example, the following bad ordering:
13
+ #
14
+ # ```ruby
15
+ # # frozen_string_literal: true
16
+ # # typed: true
17
+ # class Foo; end
18
+ # ```
19
+ #
20
+ # Will be corrected as:
21
+ #
22
+ # ```ruby
23
+ # # typed: true
24
+ # # frozen_string_literal: true
25
+ # class Foo; end
26
+ # ```
27
+ #
28
+ # Only `typed`, `(en)?coding`, `warn_indent` and `frozen_string_literal` magic comments are considered,
29
+ # other comments or magic comments are left in the same place.
30
+ class EnforceSigilOrder < ValidSigil
31
+ def investigate(processed_source)
32
+ return if processed_source.tokens.empty?
33
+
34
+ tokens = extract_magic_comments(processed_source)
35
+ return if tokens.empty?
36
+
37
+ check_magic_comments_order(tokens)
38
+ end
39
+
40
+ def autocorrect(_node)
41
+ lambda do |corrector|
42
+ tokens = extract_magic_comments(processed_source)
43
+
44
+ # Get the magic comments tokens in their expected order
45
+ expected = PREFERRED_ORDER.keys.map do |re|
46
+ tokens.select { |token| re.match?(token.text) }
47
+ end.flatten
48
+
49
+ tokens.each_with_index do |token, index|
50
+ corrector.replace(token.pos, expected[index].text)
51
+ end
52
+ end
53
+ end
54
+
55
+ protected
56
+
57
+ CODING_REGEX = /#\s+(en)?coding:(?:\s+([\w]+))?/
58
+ INDENT_REGEX = /#\s+warn_indent:(?:\s+([\w]+))?/
59
+ FROZEN_REGEX = /#\s+frozen_string_literal:(?:\s+([\w]+))?/
60
+
61
+ PREFERRED_ORDER = {
62
+ CODING_REGEX => 'encoding',
63
+ SIGIL_REGEX => 'typed',
64
+ INDENT_REGEX => 'warn_indent',
65
+ FROZEN_REGEX => 'frozen_string_literal',
66
+ }.freeze
67
+
68
+ MAGIC_REGEX = Regexp.union(*PREFERRED_ORDER.keys)
69
+
70
+ # extraction
71
+
72
+ # Get all the tokens in `processed_source` that match `MAGIC_REGEX`
73
+ def extract_magic_comments(processed_source)
74
+ processed_source.tokens
75
+ .take_while { |token| token.type == :tCOMMENT }
76
+ .select { |token| MAGIC_REGEX.match?(token.text) }
77
+ end
78
+
79
+ # checks
80
+
81
+ def check_magic_comments_order(tokens)
82
+ # Get the current magic comments order
83
+ order = tokens.map do |token|
84
+ PREFERRED_ORDER.keys.find { |re| re.match?(token.text) }
85
+ end.compact.uniq
86
+
87
+ # Get the expected magic comments order based on the one used in the actual source
88
+ expected = PREFERRED_ORDER.keys.select do |re|
89
+ tokens.any? { |token| re.match?(token.text) }
90
+ end.uniq
91
+
92
+ if order != expected
93
+ tokens.each do |token|
94
+ add_offense(
95
+ token,
96
+ location: token.pos,
97
+ message: "Magic comments should be in the following order: #{PREFERRED_ORDER.values.join(', ')}."
98
+ )
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -36,7 +36,13 @@ module RuboCop
36
36
  return unless extract_sigil(processed_source).nil?
37
37
 
38
38
  token = processed_source.tokens.first
39
- corrector.insert_before(token.pos, "# typed: #{minimum_strictness || suggested_strictness}\n")
39
+ replace_with = suggested_strictness_level(minimum_strictness, suggested_strictness)
40
+ sigil = "# typed: #{replace_with}"
41
+ if token.text.start_with?("#!") # shebang line
42
+ corrector.insert_after(token.pos, "\n#{sigil}")
43
+ else
44
+ corrector.insert_before(token.pos, "#{sigil}\n")
45
+ end
40
46
  end
41
47
  end
42
48
 
@@ -64,7 +70,7 @@ module RuboCop
64
70
 
65
71
  token = processed_source.tokens.first
66
72
  if require_sigil_on_all_files?
67
- strictness = minimum_strictness || suggested_strictness
73
+ strictness = suggested_strictness_level(minimum_strictness, suggested_strictness)
68
74
  add_offense(
69
75
  token,
70
76
  location: token.pos,
@@ -75,6 +81,25 @@ module RuboCop
75
81
  false
76
82
  end
77
83
 
84
+ def suggested_strictness_level(minimum_strictness, suggested_strictness)
85
+ # if no minimum strictness is set (eg. using Sorbet/HasSigil without config) then
86
+ # we always use the suggested strictness which defaults to `false`
87
+ return suggested_strictness unless minimum_strictness
88
+
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?
91
+
92
+ # if a minimum strictness is set (eg. you're using Sorbet/FalseSigil)
93
+ # we want to compare the minimum strictness and suggested strictness. this is because
94
+ # the suggested strictness might be higher than the minimum (eg. if you want all new files
95
+ # at a higher strictness level, without having to migrate existing files at lower levels).
96
+
97
+ suggested_level = STRICTNESS_LEVELS.index(suggested_strictness)
98
+ minimum_level = STRICTNESS_LEVELS.index(minimum_strictness)
99
+
100
+ suggested_level > minimum_level ? suggested_strictness : minimum_strictness
101
+ end
102
+
78
103
  def check_strictness_not_empty(sigil, strictness)
79
104
  return true if strictness
80
105
 
@@ -122,12 +147,12 @@ module RuboCop
122
147
 
123
148
  # Default is `'false'`
124
149
  def suggested_strictness
125
- cop_config['SuggestedStrictness'] || 'false'
150
+ STRICTNESS_LEVELS.include?(cop_config['SuggestedStrictness']) ? cop_config['SuggestedStrictness'] : 'false'
126
151
  end
127
152
 
128
153
  # Default is `nil`
129
154
  def minimum_strictness
130
- cop_config['MinimumStrictness']
155
+ cop_config['MinimumStrictness'] if STRICTNESS_LEVELS.include?(cop_config['MinimumStrictness'])
131
156
  end
132
157
  end
133
158
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rubocop'
4
+ require_relative 'signature_cop'
4
5
 
5
6
  module RuboCop
6
7
  module Cop
@@ -18,13 +19,9 @@ module RuboCop
18
19
  #
19
20
  # # good
20
21
  # sig { void }
21
- class CheckedTrueInSignature < RuboCop::Cop::Cop
22
+ class CheckedTrueInSignature < SignatureCop
22
23
  include(RuboCop::Cop::RangeHelp)
23
24
 
24
- def_node_matcher(:signature?, <<~PATTERN)
25
- (block (send nil? :sig) (args) ...)
26
- PATTERN
27
-
28
25
  def_node_search(:offending_node, <<~PATTERN)
29
26
  (send _ :checked (true))
30
27
  PATTERN
@@ -36,9 +33,7 @@ module RuboCop
36
33
  '`include(WaffleCone::RuntimeChecks)` to this module and set other methods to `checked(false)`.'
37
34
  private_constant(:MESSAGE)
38
35
 
39
- def on_block(node)
40
- return unless signature?(node)
41
-
36
+ def on_signature(node)
42
37
  error = offending_node(node).first
43
38
  return unless error
44
39
 
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+ require 'stringio'
5
+ require_relative 'signature_cop'
6
+
7
+ module RuboCop
8
+ module Cop
9
+ module Sorbet
10
+ # This cop checks that every method definition and attribute accessor has a Sorbet signature.
11
+ #
12
+ # It also suggest an autocorrect with placeholders so the following code:
13
+ #
14
+ # ```
15
+ # def foo(a, b, c); end
16
+ # ```
17
+ #
18
+ # Will be corrected as:
19
+ #
20
+ # ```
21
+ # sig { params(a: T.untyped, b: T.untyped, c: T.untyped).returns(T.untyped)
22
+ # def foo(a, b, c); end
23
+ # ```
24
+ #
25
+ # You can configure the placeholders used by changing the following options:
26
+ #
27
+ # * `ParameterTypePlaceholder`: placeholders used for parameter types (default: 'T.untyped')
28
+ # * `ReturnTypePlaceholder`: placeholders used for return types (default: 'T.untyped')
29
+ class EnforceSignatures < SignatureCop
30
+ def_node_matcher(:accessor?, <<-PATTERN)
31
+ (send nil? {:attr_reader :attr_writer :attr_accessor} ...)
32
+ PATTERN
33
+
34
+ def on_def(node)
35
+ check_node(node)
36
+ end
37
+
38
+ def on_defs(node)
39
+ check_node(node)
40
+ end
41
+
42
+ def on_send(node)
43
+ return unless accessor?(node)
44
+ check_node(node)
45
+ end
46
+
47
+ def autocorrect(node)
48
+ lambda do |corrector|
49
+ suggest = SigSuggestion.new(node.loc.column, param_type_placeholder, return_type_placeholder)
50
+
51
+ if node.is_a?(RuboCop::AST::DefNode) # def something
52
+ node.arguments.each do |arg|
53
+ suggest.params << arg.children.first
54
+ end
55
+ elsif accessor?(node) # attr reader, writer, accessor
56
+ method = node.children[1]
57
+ symbol = node.children[2]
58
+ suggest.params << symbol.value if symbol && (method == :attr_writer || method == :attr_accessor)
59
+ suggest.returns = 'void' if method == :attr_writer
60
+ end
61
+
62
+ corrector.insert_before(node.loc.expression, suggest.to_autocorrect)
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def check_node(node)
69
+ prev = previous_node(node)
70
+ unless signature?(prev)
71
+ add_offense(
72
+ node,
73
+ message: "Each method is required to have a signature."
74
+ )
75
+ 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]
82
+ end
83
+
84
+ def param_type_placeholder
85
+ cop_config['ParameterTypePlaceholder'] || 'T.untyped'
86
+ end
87
+
88
+ def return_type_placeholder
89
+ cop_config['ReturnTypePlaceholder'] || 'T.untyped'
90
+ end
91
+
92
+ class SigSuggestion
93
+ attr_accessor :params, :returns
94
+
95
+ def initialize(indent, param_placeholder, return_placeholder)
96
+ @params = []
97
+ @returns = nil
98
+ @indent = indent
99
+ @param_placeholder = param_placeholder
100
+ @return_placeholder = return_placeholder
101
+ end
102
+
103
+ def to_autocorrect
104
+ out = StringIO.new
105
+ out << 'sig { '
106
+ out << generate_params
107
+ out << generate_return
108
+ out << " }\n"
109
+ out << ' ' * @indent # preserve indent for the next line
110
+ out.string
111
+ end
112
+
113
+ private
114
+
115
+ def generate_params
116
+ return if @params.empty?
117
+ out = StringIO.new
118
+ out << 'params('
119
+ out << @params.map do |param|
120
+ "#{param}: #{@param_placeholder}"
121
+ end.join(", ")
122
+ out << ').'
123
+ out.string
124
+ end
125
+
126
+ def generate_return
127
+ return "returns(#{@return_placeholder})" if @returns.nil?
128
+ return @returns if @returns == 'void'
129
+ "returns(#{@returns})"
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end