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.
- checksums.yaml +4 -4
- data/.github/CODEOWNERS +2 -0
- data/.github/probots.yml +1 -1
- data/.github/stale.yml +20 -0
- data/.gitignore +0 -2
- data/.rspec +3 -0
- data/.rubocop.yml +2 -12
- data/.travis.yml +3 -1
- data/Gemfile +6 -7
- data/Gemfile.lock +26 -41
- data/README.md +64 -6
- data/Rakefile +34 -5
- data/config/default.yml +124 -1
- data/lib/rubocop-sorbet.rb +9 -1
- data/lib/rubocop/cop/sorbet/binding_constants_without_type_alias.rb +21 -2
- data/lib/rubocop/cop/sorbet/forbid_untyped_struct_props.rb +58 -0
- data/lib/rubocop/cop/sorbet/sigils/enforce_sigil_order.rb +105 -0
- data/lib/rubocop/cop/sorbet/sigils/valid_sigil.rb +29 -4
- data/lib/rubocop/cop/sorbet/{allow_incompatible_override.rb → signatures/allow_incompatible_override.rb} +0 -0
- data/lib/rubocop/cop/sorbet/{checked_true_in_signature.rb → signatures/checked_true_in_signature.rb} +3 -8
- data/lib/rubocop/cop/sorbet/signatures/enforce_signatures.rb +135 -0
- data/lib/rubocop/cop/sorbet/{keyword_argument_ordering.rb → signatures/keyword_argument_ordering.rb} +4 -9
- data/lib/rubocop/cop/sorbet/{parameters_ordering_in_signature.rb → signatures/parameters_ordering_in_signature.rb} +17 -14
- data/lib/rubocop/cop/sorbet/{signature_build_order.rb → signatures/signature_build_order.rb} +6 -12
- data/lib/rubocop/cop/sorbet/signatures/signature_cop.rb +28 -0
- data/lib/rubocop/cop/sorbet_cops.rb +22 -0
- data/lib/rubocop/sorbet.rb +15 -0
- data/lib/rubocop/sorbet/inject.rb +20 -0
- data/lib/rubocop/sorbet/version.rb +6 -0
- data/manual/cops.md +29 -0
- data/manual/cops_sorbet.md +340 -0
- data/rubocop-sorbet.gemspec +18 -18
- data/tasks/cops_documentation.rake +317 -0
- metadata +31 -17
- data/lib/rubocop_sorbet.rb +0 -19
data/lib/rubocop-sorbet.rb
CHANGED
@@ -1,3 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
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
|
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
|
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
|
-
|
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
|
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']
|
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
|
File without changes
|
data/lib/rubocop/cop/sorbet/{checked_true_in_signature.rb → signatures/checked_true_in_signature.rb}
RENAMED
@@ -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 <
|
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
|
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
|