rubocop-sorbet 0.4.1 → 0.6.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +26 -0
- data/.gitignore +1 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +5 -3
- data/README.md +7 -0
- data/bin/rubocop +29 -0
- data/config/default.yml +53 -3
- data/dev.yml +1 -1
- data/lib/rubocop/cop/sorbet/binding_constants_without_type_alias.rb +14 -1
- data/lib/rubocop/cop/sorbet/callback_conditionals_binding.rb +138 -0
- data/lib/rubocop/cop/sorbet/forbid_include_const_literal.rb +0 -40
- data/lib/rubocop/cop/sorbet/forbid_superclass_const_literal.rb +0 -17
- data/lib/rubocop/cop/sorbet/forbid_t_unsafe.rb +26 -0
- data/lib/rubocop/cop/sorbet/one_ancestor_per_line.rb +75 -0
- data/lib/rubocop/cop/sorbet/rbi/forbid_extend_t_sig_helpers_in_shims.rb +53 -0
- data/lib/rubocop/cop/sorbet/rbi/forbid_rbi_outside_of_sorbet_dir.rb +31 -0
- data/lib/rubocop/cop/sorbet/rbi/single_line_rbi_class_module_definitions.rb +46 -0
- data/lib/rubocop/cop/sorbet/sigils/enforce_sigil_order.rb +10 -0
- data/lib/rubocop/cop/sorbet/sigils/valid_sigil.rb +4 -2
- data/lib/rubocop/cop/sorbet/signatures/enforce_signatures.rb +19 -10
- data/lib/rubocop/cop/sorbet/signatures/signature_build_order.rb +18 -9
- data/lib/rubocop/cop/sorbet_cops.rb +7 -0
- data/lib/rubocop/sorbet/version.rb +1 -1
- data/manual/cops.md +6 -0
- data/manual/cops_sorbet.md +208 -4
- data/service.yml +1 -4
- metadata +11 -5
- data/.shopify-build/VERSION +0 -1
- data/.shopify-build/rubocop-sorbet.yml +0 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 309687789d8de94ff0cc8c0d64361cd227ab332ffdd16ab0248d588639dfcd80
|
4
|
+
data.tar.gz: ca39d50843f2e577ed0a0328c9707e21af175daacd8c92b9b4403bc2195c4cff
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f774385eec35af24755af1cc5e7f5de554bedf0f6a475e3235879aee61a2c68a818ea87ca4663daa17bb807c862d911264284d1f3270e852166c3e84cde32e7d
|
7
|
+
data.tar.gz: d6d91114e9c70bc64fa5adf6679a689c12263caa352f7f38b6d96d36609a81e90bc18f369e42b1605259262626114aac6fd5d50f7c91bced3e2f24e2f606fbc4
|
@@ -0,0 +1,26 @@
|
|
1
|
+
name: CI
|
2
|
+
|
3
|
+
on: [push, pull_request]
|
4
|
+
|
5
|
+
env:
|
6
|
+
SRB_SKIP_GEM_RBIS: true
|
7
|
+
|
8
|
+
jobs:
|
9
|
+
build:
|
10
|
+
runs-on: ubuntu-latest
|
11
|
+
strategy:
|
12
|
+
fail-fast: false
|
13
|
+
matrix:
|
14
|
+
ruby: [ 2.5, 2.6, 2.7, 3.0 ]
|
15
|
+
name: Test Ruby ${{ matrix.ruby }}
|
16
|
+
steps:
|
17
|
+
- uses: actions/checkout@v2
|
18
|
+
- name: Set up Ruby
|
19
|
+
uses: ruby/setup-ruby@v1
|
20
|
+
with:
|
21
|
+
ruby-version: ${{ matrix.ruby }}
|
22
|
+
bundler-cache: true
|
23
|
+
- name: Lint Ruby files
|
24
|
+
run: bin/rubocop
|
25
|
+
- name: Run tests
|
26
|
+
run: bin/rspec
|
data/.gitignore
CHANGED
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
rubocop-sorbet (0.
|
4
|
+
rubocop-sorbet (0.6.2)
|
5
5
|
rubocop
|
6
6
|
|
7
7
|
GEM
|
@@ -12,6 +12,7 @@ GEM
|
|
12
12
|
ice_nine (~> 0.11.0)
|
13
13
|
memoizable (~> 0.4.0)
|
14
14
|
ast (2.4.0)
|
15
|
+
byebug (11.1.3)
|
15
16
|
concord (0.1.5)
|
16
17
|
adamantium (~> 0.2.0)
|
17
18
|
equalizer (~> 0.0.9)
|
@@ -27,7 +28,7 @@ GEM
|
|
27
28
|
rainbow (3.0.0)
|
28
29
|
rake (13.0.1)
|
29
30
|
regexp_parser (1.7.0)
|
30
|
-
rexml (3.2.
|
31
|
+
rexml (3.2.5)
|
31
32
|
rspec (3.8.0)
|
32
33
|
rspec-core (~> 3.8.0)
|
33
34
|
rspec-expectations (~> 3.8.0)
|
@@ -71,6 +72,7 @@ PLATFORMS
|
|
71
72
|
ruby
|
72
73
|
|
73
74
|
DEPENDENCIES
|
75
|
+
byebug
|
74
76
|
rake (>= 12.3.3)
|
75
77
|
rspec
|
76
78
|
rubocop-shopify
|
@@ -79,4 +81,4 @@ DEPENDENCIES
|
|
79
81
|
yard (~> 0.9)
|
80
82
|
|
81
83
|
BUNDLED WITH
|
82
|
-
|
84
|
+
2.2.16
|
data/README.md
CHANGED
@@ -17,6 +17,13 @@ or, if you use `Bundler`, add this line your application's `Gemfile`:
|
|
17
17
|
gem 'rubocop-sorbet', require: false
|
18
18
|
```
|
19
19
|
|
20
|
+
Note: in order to use the [Sorbet/SignatureBuildOrder](https://github.com/Shopify/rubocop-sorbet/blob/master/manual/cops_sorbet.md#sorbetsignaturebuildorder) cop autocorrect feature, it is necessary
|
21
|
+
to install `unparser` in addition to `rubocop-sorbet`.
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
gem "unparser", require: false
|
25
|
+
```
|
26
|
+
|
20
27
|
## Usage
|
21
28
|
|
22
29
|
You need to tell RuboCop to load the Sorbet extension. There are three ways to do this:
|
data/bin/rubocop
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'rubocop' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
require "bundler/setup"
|
28
|
+
|
29
|
+
load(Gem.bin_path("rubocop", "rubocop"))
|
data/config/default.yml
CHANGED
@@ -14,6 +14,11 @@ Sorbet/BindingConstantWithoutTypeAlias:
|
|
14
14
|
Enabled: true
|
15
15
|
VersionAdded: 0.2.0
|
16
16
|
|
17
|
+
Sorbet/CallbackConditionalsBinding:
|
18
|
+
Description: 'Ensures callback conditionals are bound to the right type.'
|
19
|
+
Enabled: true
|
20
|
+
VersionAdded: 0.7.0
|
21
|
+
|
17
22
|
Sorbet/CheckedTrueInSignature:
|
18
23
|
Description: 'Disallows the usage of `checked(true)` in signatures.'
|
19
24
|
Enabled: true
|
@@ -42,7 +47,7 @@ Sorbet/FalseSigil:
|
|
42
47
|
Description: 'All files must be at least at strictness `false`.'
|
43
48
|
Enabled: true
|
44
49
|
VersionAdded: 0.3.3
|
45
|
-
SuggestedStrictness:
|
50
|
+
SuggestedStrictness: "false"
|
46
51
|
Include:
|
47
52
|
- "**/*.rb"
|
48
53
|
- "**/*.rbi"
|
@@ -53,15 +58,39 @@ Sorbet/FalseSigil:
|
|
53
58
|
- db/**/*.rb
|
54
59
|
- script/**/*
|
55
60
|
|
61
|
+
Sorbet/ForbidExtendTSigHelpersInShims:
|
62
|
+
Description: 'Forbid the use of `extend T::Sig` and `extend T::Helpers` in RBI shims'
|
63
|
+
Enabled: true
|
64
|
+
VersionAdded: '0.6.0'
|
65
|
+
Include:
|
66
|
+
- "**/*.rbi"
|
67
|
+
|
68
|
+
Sorbet/ForbidRBIOutsideOfSorbetDir:
|
69
|
+
Description: 'Forbids RBI files outside of the sorbet/ directory.'
|
70
|
+
Enabled: true
|
71
|
+
VersionAdded: 0.6.1
|
72
|
+
Include:
|
73
|
+
- "**/*.rbi"
|
74
|
+
|
56
75
|
Sorbet/ForbidIncludeConstLiteral:
|
57
76
|
Description: 'Forbids include of non-literal constants.'
|
58
|
-
Enabled:
|
77
|
+
Enabled: false
|
59
78
|
VersionAdded: 0.2.0
|
79
|
+
VersionChanged: 0.5.0
|
60
80
|
|
61
81
|
Sorbet/ForbidSuperclassConstLiteral:
|
62
82
|
Description: 'Forbid superclasses which are non-literal constants.'
|
63
|
-
Enabled:
|
83
|
+
Enabled: false
|
64
84
|
VersionAdded: 0.2.0
|
85
|
+
VersionChanged: 0.6.1
|
86
|
+
Exclude:
|
87
|
+
- db/migrate/*.rb
|
88
|
+
|
89
|
+
Sorbet/ForbidTUnsafe:
|
90
|
+
Description: 'Forbid usage of T.unsafe.'
|
91
|
+
Enabled: false
|
92
|
+
VersionAdded: 0.7.0
|
93
|
+
VersionChanged: 0.7.0
|
65
94
|
|
66
95
|
Sorbet/ForbidUntypedStructProps:
|
67
96
|
Description: >-
|
@@ -73,11 +102,14 @@ Sorbet/ForbidUntypedStructProps:
|
|
73
102
|
Sorbet/HasSigil:
|
74
103
|
Description: 'Makes the Sorbet typed sigil mandatory in all files.'
|
75
104
|
Enabled: false
|
105
|
+
SuggestedStrictness: "false"
|
106
|
+
MinimumStrictness: "false"
|
76
107
|
VersionAdded: 0.3.3
|
77
108
|
|
78
109
|
Sorbet/IgnoreSigil:
|
79
110
|
Description: 'All files must be at least at strictness `ignore`.'
|
80
111
|
Enabled: false
|
112
|
+
SuggestedStrictness: "ignore"
|
81
113
|
VersionAdded: 0.3.3
|
82
114
|
|
83
115
|
Sorbet/KeywordArgumentOrdering:
|
@@ -90,6 +122,11 @@ Sorbet/KeywordArgumentOrdering:
|
|
90
122
|
Enabled: true
|
91
123
|
VersionAdded: 0.2.0
|
92
124
|
|
125
|
+
Sorbet/OneAncestorPerLine:
|
126
|
+
Description: 'Enforces one ancestor per call to requires_ancestor'
|
127
|
+
Enabled: false
|
128
|
+
VersionAdded: '0.6.0'
|
129
|
+
|
93
130
|
Sorbet/ParametersOrderingInSignature:
|
94
131
|
Description: 'Enforces same parameter order between a method and its signature.'
|
95
132
|
Enabled: true
|
@@ -105,22 +142,35 @@ Sorbet/SignatureBuildOrder:
|
|
105
142
|
Enabled: true
|
106
143
|
VersionAdded: 0.3.0
|
107
144
|
|
145
|
+
Sorbet/SingleLineRbiClassModuleDefinitions:
|
146
|
+
Description: 'Empty class and module definitions in RBI must be on a single line.'
|
147
|
+
Enabled: false
|
148
|
+
VersionAdded: '0.6.0'
|
149
|
+
Include:
|
150
|
+
- "**/*.rbi"
|
151
|
+
|
108
152
|
Sorbet/StrictSigil:
|
109
153
|
Description: 'All files must be at least at strictness `strict`.'
|
110
154
|
Enabled: false
|
155
|
+
SuggestedStrictness: "strict"
|
111
156
|
VersionAdded: 0.3.3
|
112
157
|
|
113
158
|
Sorbet/StrongSigil:
|
114
159
|
Description: 'All files must be at least at strictness `strong`.'
|
115
160
|
Enabled: false
|
161
|
+
SuggestedStrictness: "strong"
|
116
162
|
VersionAdded: 0.3.3
|
117
163
|
|
118
164
|
Sorbet/TrueSigil:
|
119
165
|
Description: 'All files must be at least at strictness `true`.'
|
120
166
|
Enabled: false
|
167
|
+
SuggestedStrictness: "true"
|
121
168
|
VersionAdded: 0.3.3
|
122
169
|
|
123
170
|
Sorbet/ValidSigil:
|
124
171
|
Description: 'All files must have a valid sigil.'
|
125
172
|
Enabled: true
|
173
|
+
RequireSigilOnAllFiles: false
|
174
|
+
SuggestedStrictness: "false"
|
175
|
+
MinimumStrictness: "false"
|
126
176
|
VersionAdded: 0.3.3
|
data/dev.yml
CHANGED
@@ -17,7 +17,7 @@ module RuboCop
|
|
17
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
|
-
(casgn _ _ [#not_nil? #not_t_let? #not_generic_parameter_decl? #method_needing_aliasing_on_t?])
|
20
|
+
(casgn _ _ [#not_nil? #not_t_let? #not_dynamic_type_creation_with_block? #not_generic_parameter_decl? #method_needing_aliasing_on_t?])
|
21
21
|
PATTERN
|
22
22
|
|
23
23
|
def_node_matcher(:using_type_alias?, <<-PATTERN)
|
@@ -48,6 +48,15 @@ module RuboCop
|
|
48
48
|
)
|
49
49
|
PATTERN
|
50
50
|
|
51
|
+
def_node_matcher(:dynamic_type_creation_with_block?, <<-PATTERN)
|
52
|
+
(block
|
53
|
+
(send
|
54
|
+
const :new ...)
|
55
|
+
_
|
56
|
+
_
|
57
|
+
)
|
58
|
+
PATTERN
|
59
|
+
|
51
60
|
def_node_matcher(:generic_parameter_decl?, <<-PATTERN)
|
52
61
|
(
|
53
62
|
send nil? {:type_template :type_member} ...
|
@@ -67,6 +76,10 @@ module RuboCop
|
|
67
76
|
!t_let?(node)
|
68
77
|
end
|
69
78
|
|
79
|
+
def not_dynamic_type_creation_with_block?(node)
|
80
|
+
!dynamic_type_creation_with_block?(node)
|
81
|
+
end
|
82
|
+
|
70
83
|
def not_generic_parameter_decl?(node)
|
71
84
|
!generic_parameter_decl?(node)
|
72
85
|
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Sorbet
|
6
|
+
# This cop ensures that callback conditionals are bound to the right type
|
7
|
+
# so that they are type checked properly.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
#
|
11
|
+
# # bad
|
12
|
+
# class Post < ApplicationRecord
|
13
|
+
# before_create :do_it, if: -> { should_do_it? }
|
14
|
+
#
|
15
|
+
# def should_do_it?
|
16
|
+
# true
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# # good
|
21
|
+
# class Post < ApplicationRecord
|
22
|
+
# before_create :do_it, if: -> { T.bind(self, Post).should_do_it? }
|
23
|
+
#
|
24
|
+
# def should_do_it?
|
25
|
+
# true
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
class CallbackConditionalsBinding < RuboCop::Cop::Cop
|
29
|
+
CALLBACKS = %i(
|
30
|
+
validate
|
31
|
+
validates
|
32
|
+
validates_with
|
33
|
+
before_validation
|
34
|
+
around_validation
|
35
|
+
|
36
|
+
before_create
|
37
|
+
before_save
|
38
|
+
before_destroy
|
39
|
+
before_update
|
40
|
+
|
41
|
+
after_create
|
42
|
+
after_save
|
43
|
+
after_destroy
|
44
|
+
after_update
|
45
|
+
after_touch
|
46
|
+
after_initialize
|
47
|
+
after_find
|
48
|
+
|
49
|
+
around_create
|
50
|
+
around_save
|
51
|
+
around_destroy
|
52
|
+
around_update
|
53
|
+
|
54
|
+
before_commit
|
55
|
+
|
56
|
+
after_commit
|
57
|
+
after_create_commit
|
58
|
+
after_destroy_commit
|
59
|
+
after_rollback
|
60
|
+
after_save_commit
|
61
|
+
after_update_commit
|
62
|
+
|
63
|
+
before_action
|
64
|
+
prepend_before_action
|
65
|
+
append_before_action
|
66
|
+
|
67
|
+
around_action
|
68
|
+
prepend_around_action
|
69
|
+
append_around_action
|
70
|
+
|
71
|
+
after_action
|
72
|
+
prepend_after_action
|
73
|
+
append_after_action
|
74
|
+
).freeze
|
75
|
+
|
76
|
+
def autocorrect(node)
|
77
|
+
lambda do |corrector|
|
78
|
+
options = node.each_child_node.find(&:hash_type?)
|
79
|
+
|
80
|
+
conditional = nil
|
81
|
+
options.each_pair do |keyword, block|
|
82
|
+
if keyword.value == :if || keyword.value == :unless
|
83
|
+
conditional = block
|
84
|
+
break
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
_, _, block = conditional.child_nodes
|
89
|
+
expected_class = node.parent_module_name
|
90
|
+
|
91
|
+
bind = if block.begin_type?
|
92
|
+
indentation = " " * block.child_nodes.first.loc.column
|
93
|
+
"T.bind(self, #{expected_class})\n#{indentation}"
|
94
|
+
elsif block.child_nodes.empty? && !block.ivar_type?
|
95
|
+
"T.bind(self, #{expected_class})."
|
96
|
+
else
|
97
|
+
"T.bind(self, #{expected_class}); "
|
98
|
+
end
|
99
|
+
|
100
|
+
corrector.insert_before(block, bind)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def on_send(node)
|
105
|
+
return unless CALLBACKS.include?(node.method_name)
|
106
|
+
|
107
|
+
options = node.each_child_node.find(&:hash_type?)
|
108
|
+
return if options.nil?
|
109
|
+
|
110
|
+
conditional = nil
|
111
|
+
options.each_pair do |keyword, block|
|
112
|
+
next unless keyword.sym_type?
|
113
|
+
|
114
|
+
if keyword.value == :if || keyword.value == :unless
|
115
|
+
conditional = block
|
116
|
+
break
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
return if conditional.nil? || conditional.child_nodes.empty?
|
121
|
+
|
122
|
+
type, _, block = conditional.child_nodes
|
123
|
+
return unless type.lambda_or_proc?
|
124
|
+
|
125
|
+
expected_class = node.parent_module_name
|
126
|
+
return if expected_class.nil?
|
127
|
+
|
128
|
+
unless block.source.include?("T.bind(self, #{expected_class})")
|
129
|
+
add_offense(
|
130
|
+
node,
|
131
|
+
message: "Callback conditionals should be bound to the right type. Use T.bind(self, #{expected_class})"
|
132
|
+
)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -14,15 +14,6 @@ require 'rubocop'
|
|
14
14
|
# end
|
15
15
|
# ```
|
16
16
|
#
|
17
|
-
# This cop replaces them by:
|
18
|
-
#
|
19
|
-
# ```ruby
|
20
|
-
# class MyClass
|
21
|
-
# MyClassInclude = send_expr
|
22
|
-
# include MyClassInclude
|
23
|
-
# end
|
24
|
-
# ```
|
25
|
-
#
|
26
17
|
# Multiple occurences of this can be found in Shopify's code base like:
|
27
18
|
#
|
28
19
|
# ```ruby
|
@@ -61,37 +52,6 @@ module RuboCop
|
|
61
52
|
return unless [:module, :class, :sclass].include?(parent.type)
|
62
53
|
add_offense(node)
|
63
54
|
end
|
64
|
-
|
65
|
-
def autocorrect(node)
|
66
|
-
lambda do |corrector|
|
67
|
-
# Find parent class node
|
68
|
-
parent = node.parent
|
69
|
-
parent = parent.parent if parent.type == :begin
|
70
|
-
|
71
|
-
# Build include variable name
|
72
|
-
class_name = (parent.child_nodes.first.const_name || 'Anon').split('::').last
|
73
|
-
include_name = find_free_name("#{class_name}Include")
|
74
|
-
used_names << include_name
|
75
|
-
|
76
|
-
# Apply fix
|
77
|
-
indent = ' ' * node.loc.column
|
78
|
-
fix = "#{include_name} = #{node.child_nodes.first.source}\n#{indent}"
|
79
|
-
corrector.insert_before(node.loc.expression, fix)
|
80
|
-
corrector.replace(node.child_nodes.first.loc.expression, include_name)
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
# Find a free local variable name
|
85
|
-
#
|
86
|
-
# Since each include uses its own local variable to store the send result,
|
87
|
-
# we need to ensure that we don't use the same name twice in the same
|
88
|
-
# module.
|
89
|
-
def find_free_name(base_name)
|
90
|
-
return base_name unless used_names.include?(base_name)
|
91
|
-
i = 2
|
92
|
-
i += 1 while used_names.include?("#{base_name}#{i}")
|
93
|
-
"#{base_name}#{i}"
|
94
|
-
end
|
95
55
|
end
|
96
56
|
end
|
97
57
|
end
|