rubocop-sorbet 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/check-version-placeholders.yml +1 -1
- data/.github/workflows/ci.yml +2 -2
- data/.github/workflows/dependabot_automerge.yml +2 -2
- data/.github/workflows/release.yml +52 -0
- data/.github/workflows/stale.yml +1 -1
- data/Gemfile.lock +1 -1
- data/VERSION +1 -1
- data/config/default.yml +20 -0
- data/lib/rubocop/cop/sorbet/mixin/constant_scope.rb +23 -0
- data/lib/rubocop/cop/sorbet/mixin/t_let_correction.rb +65 -0
- data/lib/rubocop/cop/sorbet/mutable_constant_sorbet_aware_behaviour.rb +15 -0
- data/lib/rubocop/cop/sorbet/redundant_t_let.rb +205 -0
- data/lib/rubocop/cop/sorbet/redundant_t_let_for_literal.rb +218 -0
- data/lib/rubocop/cop/sorbet/signatures/enforce_signatures.rb +12 -1
- data/lib/rubocop/cop/sorbet_cops.rb +4 -0
- data/manual/cops.md +2 -0
- data/manual/cops_sorbet.md +123 -1
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b5dcbac6e0a2af2d4bed5b33ef9bd2d9cfad4608a400dc2f81e0c8c338543615
|
|
4
|
+
data.tar.gz: 9960e81847d7cda7ffb4b94d9dff22ac65f3c2b9c74664c931684b492d7da14e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7a0e44a9cc6fd67dd8fb564015f9dfee1e76c510d2b5e3dd853cffb72ee4f6b30948593f8d96f008749fdcbb5a70b02b11342396787a8e48f1fd9b11a6f308cd
|
|
7
|
+
data.tar.gz: 647911f5d12c0a8459e92ee71b0cd3bc01b7d4e8b15195555e82194499f4b57423d2307854f080f00132bf64f86d2356cefc6b22c545cda00b24427cbbb4fd9c
|
|
@@ -8,7 +8,7 @@ jobs:
|
|
|
8
8
|
check-placeholders:
|
|
9
9
|
runs-on: ubuntu-latest
|
|
10
10
|
steps:
|
|
11
|
-
- uses: actions/checkout@
|
|
11
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
12
12
|
- uses: ruby/setup-ruby@v1
|
|
13
13
|
with:
|
|
14
14
|
bundler-cache: true
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -14,7 +14,7 @@ jobs:
|
|
|
14
14
|
ruby: ["3.1", "3.2", "3.3", "3.4"]
|
|
15
15
|
name: Test Ruby ${{ matrix.ruby }}
|
|
16
16
|
steps:
|
|
17
|
-
- uses: actions/checkout@
|
|
17
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
18
18
|
- name: Set up Ruby
|
|
19
19
|
uses: ruby/setup-ruby@a4effe49ee8ee5b8b5091268c473a4628afb5651 # v1.245.0
|
|
20
20
|
with:
|
|
@@ -29,7 +29,7 @@ jobs:
|
|
|
29
29
|
fail-fast: false
|
|
30
30
|
name: Lint & Docs
|
|
31
31
|
steps:
|
|
32
|
-
- uses: actions/checkout@
|
|
32
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
33
33
|
- name: Set up Ruby
|
|
34
34
|
uses: ruby/setup-ruby@a4effe49ee8ee5b8b5091268c473a4628afb5651 # v1.245.0
|
|
35
35
|
with:
|
|
@@ -12,12 +12,12 @@ jobs:
|
|
|
12
12
|
steps:
|
|
13
13
|
- name: Dependabot metadata
|
|
14
14
|
id: metadata
|
|
15
|
-
uses: dependabot/fetch-metadata@
|
|
15
|
+
uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0
|
|
16
16
|
with:
|
|
17
17
|
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
|
18
18
|
- name: Enable auto-merge for Dependabot PRs
|
|
19
19
|
if: ${{ steps.metadata.outputs.update-type != 'version-update:semver-major' }}
|
|
20
|
-
uses: actions/github-script@
|
|
20
|
+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
21
21
|
with:
|
|
22
22
|
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
|
23
23
|
script: |
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: Release gem on RubyGems.org and create GitHub release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v[0-9]+.[0-9]+.[0-9]+'
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
release_gem:
|
|
10
|
+
name: Release gem on RubyGems.org
|
|
11
|
+
if: github.repository == 'Shopify/rubocop-sorbet'
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
contents: write
|
|
16
|
+
id-token: write
|
|
17
|
+
|
|
18
|
+
environment: release
|
|
19
|
+
|
|
20
|
+
steps:
|
|
21
|
+
- name: Checkout repository
|
|
22
|
+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
23
|
+
with:
|
|
24
|
+
persist-credentials: false
|
|
25
|
+
|
|
26
|
+
- name: Set up Ruby
|
|
27
|
+
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0
|
|
28
|
+
with:
|
|
29
|
+
bundler-cache: true
|
|
30
|
+
|
|
31
|
+
- name: Release gem on RubyGems.org
|
|
32
|
+
uses: rubygems/release-gem@052cc82692552de3ef2b81fd670e41d13cba8092 # v1.4.0
|
|
33
|
+
|
|
34
|
+
release_github:
|
|
35
|
+
name: Create GitHub release
|
|
36
|
+
if: github.repository == 'Shopify/rubocop-sorbet'
|
|
37
|
+
needs: release_gem
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
|
|
40
|
+
permissions:
|
|
41
|
+
contents: write
|
|
42
|
+
|
|
43
|
+
steps:
|
|
44
|
+
- name: Checkout repository
|
|
45
|
+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
46
|
+
|
|
47
|
+
- name: Create GitHub release
|
|
48
|
+
run: |
|
|
49
|
+
tag_name="$(git describe --tags --abbrev=0)"
|
|
50
|
+
gh release create "${tag_name}" --verify-tag --generate-notes
|
|
51
|
+
env:
|
|
52
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
data/.github/workflows/stale.yml
CHANGED
|
@@ -12,7 +12,7 @@ jobs:
|
|
|
12
12
|
stale:
|
|
13
13
|
runs-on: ubuntu-latest
|
|
14
14
|
steps:
|
|
15
|
-
- uses: actions/stale@
|
|
15
|
+
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
|
|
16
16
|
with:
|
|
17
17
|
stale-pr-message: >
|
|
18
18
|
This PR has been automatically marked as stale because it has not had
|
data/Gemfile.lock
CHANGED
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.13.0
|
data/config/default.yml
CHANGED
|
@@ -8,6 +8,15 @@ Sorbet/ForbidTSig:
|
|
|
8
8
|
Enabled: false
|
|
9
9
|
VersionAdded: '0.11.0'
|
|
10
10
|
|
|
11
|
+
Sorbet/RedundantTLet:
|
|
12
|
+
Description: >-
|
|
13
|
+
Prevents redundant use of `T.let` where Sorbet infers types automatically:
|
|
14
|
+
instance variables assigned from signature parameters in `initialize`, and
|
|
15
|
+
constants assigned constructor calls.
|
|
16
|
+
Enabled: true
|
|
17
|
+
SafeAutoCorrect: false
|
|
18
|
+
VersionAdded: '0.13.0'
|
|
19
|
+
|
|
11
20
|
inherit_mode:
|
|
12
21
|
merge:
|
|
13
22
|
- Exclude
|
|
@@ -97,7 +106,9 @@ Sorbet/EnforceSignatures:
|
|
|
97
106
|
Description: 'Ensures all methods have a valid signature.'
|
|
98
107
|
Enabled: false
|
|
99
108
|
Style: sig
|
|
109
|
+
AutocorrectStyle: sig
|
|
100
110
|
VersionAdded: 0.3.4
|
|
111
|
+
VersionChanged: '0.13.0'
|
|
101
112
|
|
|
102
113
|
Sorbet/EnforceSingleSigil:
|
|
103
114
|
Description: 'Ensures that there is only one Sorbet sigil in a file.'
|
|
@@ -316,6 +327,15 @@ Sorbet/RedundantExtendTSig:
|
|
|
316
327
|
Safe: false
|
|
317
328
|
VersionAdded: 0.7.0
|
|
318
329
|
|
|
330
|
+
Sorbet/RedundantTLetForLiteral:
|
|
331
|
+
Description: >-
|
|
332
|
+
Checks for redundant `T.let` declarations on constants where the value
|
|
333
|
+
is a simple literal whose type Sorbet can infer automatically.
|
|
334
|
+
Enabled: pending
|
|
335
|
+
Safe: true
|
|
336
|
+
SafeAutoCorrect: false
|
|
337
|
+
VersionAdded: '0.13.0'
|
|
338
|
+
|
|
319
339
|
Sorbet/Refinement:
|
|
320
340
|
Description: >-
|
|
321
341
|
Checks for the use of Ruby Refinements library. Refinements add
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sorbet
|
|
6
|
+
# Shared helper for cops that unwrap a `T.let` on a constant assignment.
|
|
7
|
+
module ConstantScope
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
# True when the constant is assigned directly in a class, module, or
|
|
11
|
+
# top-level body. In `typed: strict` files Sorbet requires a `T.let` on
|
|
12
|
+
# constants assigned elsewhere (inside a conditional, loop, block, or
|
|
13
|
+
# method — error 7027), so removing the annotation there would break
|
|
14
|
+
# typechecking.
|
|
15
|
+
def statically_scoped?(node)
|
|
16
|
+
ancestor = node.parent
|
|
17
|
+
ancestor = ancestor.parent if ancestor&.begin_type?
|
|
18
|
+
ancestor.nil? || ancestor.class_type? || ancestor.module_type? || ancestor.sclass_type?
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sorbet
|
|
6
|
+
# Shared autocorrection helper for cops that unwrap a redundant
|
|
7
|
+
# `T.let(value, Type)` down to `value`, preserving heredoc bodies that a
|
|
8
|
+
# naive `corrector.replace(t_let_node, value_node.source)` would drop
|
|
9
|
+
# (`value_node.source` stops at the marker line, so the body and its
|
|
10
|
+
# terminator sit outside it).
|
|
11
|
+
module TLetCorrection
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
# Replaces `T.let(value, Type)` with `value`.
|
|
15
|
+
def replace_t_let(corrector, t_let_node, value_node)
|
|
16
|
+
heredocs = value_node.each_node(:any_str).select(&:heredoc?)
|
|
17
|
+
return corrector.replace(t_let_node, value_node.source) if heredocs.empty?
|
|
18
|
+
|
|
19
|
+
replace_t_let_preserving_heredocs(corrector, t_let_node, value_node, heredocs)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Replaces `T.let(value, Type)` with `value` without losing any heredocs
|
|
23
|
+
# contained in the value. For example, `T.let(<<~SQL, String)` becomes
|
|
24
|
+
# `<<~SQL` followed by its original body and terminator.
|
|
25
|
+
def replace_t_let_preserving_heredocs(corrector, t_let_node, value_node, heredocs)
|
|
26
|
+
corrector.replace(
|
|
27
|
+
range_including_heredocs(t_let_node, heredocs),
|
|
28
|
+
source_including_heredocs(value_node, heredocs),
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Expands the correction range to include every heredoc body and terminator.
|
|
33
|
+
# For example, the range for `T.let(<<~SQL, String)` extends through the
|
|
34
|
+
# final `SQL` terminator rather than ending at the closing parenthesis.
|
|
35
|
+
def range_including_heredocs(t_let_node, heredocs)
|
|
36
|
+
end_positions = [t_let_node.source_range.end_pos]
|
|
37
|
+
end_positions.concat(heredocs.map { |node| node.loc.heredoc_end.end_pos })
|
|
38
|
+
t_let_node.source_range.with(end_pos: end_positions.max)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Reconstructs the value with its marker-line comment and heredoc bodies.
|
|
42
|
+
# For example, `T.let(<<~SQL, String) # query` becomes a source string
|
|
43
|
+
# containing `<<~SQL # query`, followed by its body and `SQL` terminator.
|
|
44
|
+
def source_including_heredocs(value_node, heredocs)
|
|
45
|
+
marker_line = "#{value_node.source}#{marker_line_comment(value_node)}"
|
|
46
|
+
heredoc_bodies = heredocs
|
|
47
|
+
.sort_by { |node| node.loc.heredoc_body.begin_pos }
|
|
48
|
+
.map { |node| "#{node.loc.heredoc_body.source}#{node.loc.heredoc_end.source}" }
|
|
49
|
+
|
|
50
|
+
([marker_line] + heredoc_bodies).join("\n")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Returns the comment trailing the heredoc marker, including its leading space.
|
|
54
|
+
# For example, the marker line `<<~SQL, String) # query` returns ` # query`.
|
|
55
|
+
def marker_line_comment(value_node)
|
|
56
|
+
buffer = value_node.source_range.source_buffer.source
|
|
57
|
+
value_end = value_node.source_range.end_pos
|
|
58
|
+
line_end = buffer.index("\n", value_end) || buffer.length
|
|
59
|
+
comment = buffer[value_end...line_end][/#.*/]
|
|
60
|
+
comment ? " #{comment}" : ""
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -12,10 +12,25 @@ module RuboCop
|
|
|
12
12
|
base.def_node_matcher(:t_let, <<~PATTERN)
|
|
13
13
|
(send (const nil? :T) :let $_constant _type)
|
|
14
14
|
PATTERN
|
|
15
|
+
|
|
16
|
+
# @!method sorbet_type_declaration?(node)
|
|
17
|
+
base.def_node_matcher(:sorbet_type_declaration?, <<~PATTERN)
|
|
18
|
+
{
|
|
19
|
+
(send nil? {:type_member :type_template} ...)
|
|
20
|
+
(block (send nil? {:type_member :type_template} ...) ...)
|
|
21
|
+
(send (const nil? :T) :type_alias ...)
|
|
22
|
+
(block (send (const nil? :T) :type_alias ...) ...)
|
|
23
|
+
}
|
|
24
|
+
PATTERN
|
|
15
25
|
end
|
|
16
26
|
end
|
|
17
27
|
|
|
18
28
|
def on_assignment(value)
|
|
29
|
+
# Sorbet type declarations such as `type_member`, `type_template`,
|
|
30
|
+
# and `T.type_alias` produce type objects that cannot be frozen
|
|
31
|
+
# (calling `.freeze` on them breaks Sorbet), so they are exempt.
|
|
32
|
+
return if sorbet_type_declaration?(value)
|
|
33
|
+
|
|
19
34
|
t_let(value) do |constant|
|
|
20
35
|
value = constant
|
|
21
36
|
end
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sorbet
|
|
6
|
+
# Prevents unnecessary `T.let` where Sorbet infers the type automatically.
|
|
7
|
+
#
|
|
8
|
+
# When a signature parameter is assigned to an instance variable in
|
|
9
|
+
# `initialize`, the type is inferred from the signature.
|
|
10
|
+
#
|
|
11
|
+
# When a constant is assigned a constructor call (`.new`), optionally
|
|
12
|
+
# followed by `.freeze` (Sorbet 0.6.13304+), the type is inferred from
|
|
13
|
+
# the class being instantiated. Generic classes (e.g. `Set`) are
|
|
14
|
+
# excluded: Sorbet infers their constructor calls as applied types like
|
|
15
|
+
# `T::Set[T.untyped]`, so an annotation is still required.
|
|
16
|
+
#
|
|
17
|
+
# @example
|
|
18
|
+
#
|
|
19
|
+
# # bad
|
|
20
|
+
# sig { params(a: Integer) }
|
|
21
|
+
# def initialize(a)
|
|
22
|
+
# @a = T.let(a, Integer)
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# # good
|
|
26
|
+
# sig { params(a: Integer) }
|
|
27
|
+
# def initialize(a)
|
|
28
|
+
# @a = a
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# # good
|
|
32
|
+
# sig { params(a: Integer) }
|
|
33
|
+
# def initialize(a)
|
|
34
|
+
# @a = T.let(a, T.any(Integer, String))
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
37
|
+
# # bad
|
|
38
|
+
# DEFAULT_PATH = T.let(Pathname.new("/usr/local").freeze, Pathname)
|
|
39
|
+
#
|
|
40
|
+
# # good
|
|
41
|
+
# DEFAULT_PATH = Pathname.new("/usr/local").freeze
|
|
42
|
+
#
|
|
43
|
+
# # good — generic classes are not inferred, so T.let is required
|
|
44
|
+
# LICENSES = T.let(Set.new(["mit"]).freeze, T::Set[String])
|
|
45
|
+
#
|
|
46
|
+
# # good — instance variables are only inferred from signature parameters
|
|
47
|
+
# @path = T.let(Pathname.new("/usr/local"), Pathname)
|
|
48
|
+
class RedundantTLet < RuboCop::Cop::Base
|
|
49
|
+
include SignatureHelp
|
|
50
|
+
include ConstantScope
|
|
51
|
+
include TargetSorbetVersion
|
|
52
|
+
include TLetCorrection
|
|
53
|
+
extend AutoCorrector
|
|
54
|
+
|
|
55
|
+
# The constructor-constant inference these cops rely on (`X = A.new`,
|
|
56
|
+
# `X = A.new.freeze`) was finalized by Sorbet's freeze-transparent
|
|
57
|
+
# inference in 0.6.13304. Flagging it against an older Sorbet could
|
|
58
|
+
# remove a `T.let` that Sorbet still requires, so the constant path
|
|
59
|
+
# (`on_casgn`) disables itself below that version. The signature-based
|
|
60
|
+
# instance-variable path (`on_def`) predates this and is not gated.
|
|
61
|
+
minimum_target_sorbet_static_version "0.6.13304"
|
|
62
|
+
|
|
63
|
+
MSG = "Unnecessary T.let. The instance variable type is inferred from the signature."
|
|
64
|
+
MSG_CONSTRUCTOR = "Unnecessary T.let. The constant type is inferred from the constructor."
|
|
65
|
+
|
|
66
|
+
# Classes whose constructor calls Sorbet infers as applied generic
|
|
67
|
+
# types (e.g. `T::Set[T.untyped]`) rather than the bare class, so
|
|
68
|
+
# constants assigned them still need an explicit annotation.
|
|
69
|
+
GENERIC_CLASSES = ["Array", "Class", "Enumerator", "Hash", "Module", "Range", "Set"].freeze
|
|
70
|
+
|
|
71
|
+
# @!method t_let(node)
|
|
72
|
+
def_node_matcher :t_let, "(ivasgn _ $(send (const {nil? cbase} :T) :let (lvar $_) $_))"
|
|
73
|
+
|
|
74
|
+
# @!method t_let_casgn(node)
|
|
75
|
+
def_node_matcher :t_let_casgn, "(casgn _ _ $(send (const {nil? cbase} :T) :let $_ $_))"
|
|
76
|
+
|
|
77
|
+
# @!method sig_params(node)
|
|
78
|
+
def_node_matcher :sig_params, "`(send nil? :params (hash $...))"
|
|
79
|
+
|
|
80
|
+
def on_def(node)
|
|
81
|
+
return unless node.method?(:initialize)
|
|
82
|
+
|
|
83
|
+
method_args = named_argument_types(node)
|
|
84
|
+
return if method_args.none?
|
|
85
|
+
|
|
86
|
+
sig_node = find_sig_node(node)
|
|
87
|
+
return unless sig_node
|
|
88
|
+
|
|
89
|
+
sig_params = sig_params(sig_node)&.to_h { |pair| [pair.key.value, pair.value] }
|
|
90
|
+
return unless sig_params&.any?
|
|
91
|
+
|
|
92
|
+
ivar_assignments(node).each do |ivasgn_node|
|
|
93
|
+
t_let(ivasgn_node) do |tlet_node, tlet_key, tlet_value|
|
|
94
|
+
find_redundant_t_let(tlet_node, tlet_key, tlet_value, sig_params, method_args)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def on_casgn(node)
|
|
100
|
+
return unless enabled_for_sorbet_static_version?
|
|
101
|
+
return unless statically_scoped?(node)
|
|
102
|
+
|
|
103
|
+
t_let_casgn(node) do |tlet_node, value_node, type_node|
|
|
104
|
+
next unless type_node.const_type?
|
|
105
|
+
|
|
106
|
+
constructor = constructor_call(value_node)
|
|
107
|
+
next unless constructor
|
|
108
|
+
|
|
109
|
+
class_path = constructor.receiver.source.delete_prefix("::")
|
|
110
|
+
next if GENERIC_CLASSES.include?(class_path)
|
|
111
|
+
next unless class_path == type_node.source.delete_prefix("::")
|
|
112
|
+
|
|
113
|
+
add_offense(tlet_node, message: MSG_CONSTRUCTOR) do |corrector|
|
|
114
|
+
replace_t_let(corrector, tlet_node, value_node)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
# Maps named parameters to their AST types, omitting destructured
|
|
122
|
+
# parameters because they have no name and cannot appear in a signature.
|
|
123
|
+
# For example, `def initialize(a, (left, right), *rest)` returns
|
|
124
|
+
# `{ a: :arg, rest: :restarg }`, omitting `(left, right)`.
|
|
125
|
+
def named_argument_types(method_node)
|
|
126
|
+
method_node.arguments.filter_map do |argument|
|
|
127
|
+
[argument.name, argument.type] if argument.respond_to?(:name)
|
|
128
|
+
end.to_h
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Returns the `.new` call for a constant constructor, looking through
|
|
132
|
+
# an optional block and trailing `.freeze`. For example,
|
|
133
|
+
# `Foo.new { _1.enabled = true }.freeze` returns the `Foo.new` call,
|
|
134
|
+
# while `foo.new` and `Foo.build` return `nil`.
|
|
135
|
+
def constructor_call(node)
|
|
136
|
+
node = node.receiver if node.send_type? && node.method?(:freeze)
|
|
137
|
+
node = unwrap_block(node)
|
|
138
|
+
return unless node&.send_type? && node.method?(:new)
|
|
139
|
+
return unless node.receiver&.const_type?
|
|
140
|
+
|
|
141
|
+
node
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Returns the method send wrapped by a block, or the original node when
|
|
145
|
+
# no block is present. For example, `Foo.new { _1.enabled = true }`
|
|
146
|
+
# returns the `Foo.new` send node.
|
|
147
|
+
def unwrap_block(node)
|
|
148
|
+
node&.any_block_type? ? node.send_node : node
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def find_sig_node(method_node)
|
|
152
|
+
# When the def is wrapped by a method modifier (`private def initialize`),
|
|
153
|
+
# Sorbet's initializer rewriter does not process the ivar assignments,
|
|
154
|
+
# so T.let annotations remain required. Skip by returning nil.
|
|
155
|
+
return if method_node.parent&.send_type?
|
|
156
|
+
|
|
157
|
+
method_node.left_sibling.then { |s| signature?(s) ? s : nil }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def normalize_whitespace(source)
|
|
161
|
+
source
|
|
162
|
+
.gsub(/\s+/, " ") # collapse all whitespace to single spaces
|
|
163
|
+
.gsub(/,\s*([)\]\}])/, "\\1") # remove trailing commas before closing delimiters
|
|
164
|
+
.gsub(/,\s*/, ", ") # normalize to a single space after commas
|
|
165
|
+
.gsub(/\(\s*/, "(") # remove space after (
|
|
166
|
+
.gsub(/\s*\)/, ")") # remove space before )
|
|
167
|
+
.gsub(/\[\s*/, "[") # remove space after [
|
|
168
|
+
.gsub(/\s*\]/, "]") # remove space before ]
|
|
169
|
+
.strip
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def ivar_assignments(node)
|
|
173
|
+
return [] unless node.body
|
|
174
|
+
return node.body.each_child_node(:ivasgn) if node.body.begin_type?
|
|
175
|
+
return [node.body] if node.body.ivasgn_type?
|
|
176
|
+
|
|
177
|
+
[]
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def find_redundant_t_let(node, tlet_key, tlet_value, sig_params, method_args)
|
|
181
|
+
sig_type = sig_params[tlet_key]
|
|
182
|
+
return unless sig_type
|
|
183
|
+
|
|
184
|
+
method_arg_kind = method_args[tlet_key]
|
|
185
|
+
return unless method_arg_kind
|
|
186
|
+
|
|
187
|
+
arg_type = expected_type(normalize_whitespace(sig_type.source), method_arg_kind)
|
|
188
|
+
return unless normalize_whitespace(tlet_value.source) == arg_type
|
|
189
|
+
|
|
190
|
+
add_offense(node) do |corrector|
|
|
191
|
+
corrector.replace(node, tlet_key.to_s)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def expected_type(sig_type, arg_kind)
|
|
196
|
+
case arg_kind
|
|
197
|
+
when :restarg then "T::Array[#{sig_type}]"
|
|
198
|
+
when :kwrestarg then "T::Hash[Symbol, #{sig_type}]"
|
|
199
|
+
else sig_type
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rubocop"
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Sorbet
|
|
8
|
+
# Checks for redundant `T.let` declarations where the first argument
|
|
9
|
+
# is a literal whose type Sorbet can infer automatically, so wrapping
|
|
10
|
+
# it in `T.let` is redundant.
|
|
11
|
+
#
|
|
12
|
+
# Simple literals (strings, symbols, integers, floats, regexps) infer as
|
|
13
|
+
# their own class. Regexp literals are the only simple literals whose
|
|
14
|
+
# inference survives a `.freeze` call (Sorbet 0.6.13304+), so
|
|
15
|
+
# `T.let(/foo/.freeze, Regexp)` is also redundant; other frozen simple
|
|
16
|
+
# literals (e.g. `"hello".freeze`) are not inferred and still need `T.let`.
|
|
17
|
+
#
|
|
18
|
+
# Array literals of simple literals are also inferred:
|
|
19
|
+
#
|
|
20
|
+
# * A frozen array (`[...].freeze`) infers as a fixed-size tuple, which is
|
|
21
|
+
# a subtype of the annotated `T::Array`, so the annotation is redundant.
|
|
22
|
+
# * An unfrozen array infers as `T::Array[<element type>]`. It is only
|
|
23
|
+
# flagged when that inferred type matches the annotation exactly, to
|
|
24
|
+
# avoid silently widening (e.g. `["a", nil]` infers a nilable element).
|
|
25
|
+
#
|
|
26
|
+
# Hashes are excluded: Sorbet infers hash literals as `T.untyped`, so the
|
|
27
|
+
# annotation is required.
|
|
28
|
+
#
|
|
29
|
+
# @example
|
|
30
|
+
# # bad
|
|
31
|
+
# MAX_RETRIES = T.let(3, Integer)
|
|
32
|
+
# GREETING = T.let("hello", String)
|
|
33
|
+
# RATE = T.let(1.5, Float)
|
|
34
|
+
# PATTERN = T.let(/foo/, Regexp)
|
|
35
|
+
# FROZEN_PATTERN = T.let(/foo/.freeze, Regexp)
|
|
36
|
+
# STATUS = T.let(:active, Symbol)
|
|
37
|
+
# SHELLS = T.let([:bash, :zsh].freeze, T::Array[Symbol])
|
|
38
|
+
# NAMES = T.let(["alice", "bob"], T::Array[String])
|
|
39
|
+
#
|
|
40
|
+
# # good
|
|
41
|
+
# MAX_RETRIES = 3
|
|
42
|
+
# GREETING = "hello"
|
|
43
|
+
# RATE = 1.5
|
|
44
|
+
# PATTERN = /foo/
|
|
45
|
+
# FROZEN_PATTERN = /foo/.freeze
|
|
46
|
+
# STATUS = :active
|
|
47
|
+
# SHELLS = [:bash, :zsh].freeze
|
|
48
|
+
# NAMES = ["alice", "bob"]
|
|
49
|
+
#
|
|
50
|
+
# # good — non-regexp frozen simple literals are not inferred
|
|
51
|
+
# GREETING = T.let("hello".freeze, String)
|
|
52
|
+
#
|
|
53
|
+
# # good — hash literals are inferred as T.untyped
|
|
54
|
+
# OPTIONS = T.let({ verbose: true }, T::Hash[Symbol, T::Boolean])
|
|
55
|
+
#
|
|
56
|
+
# # good — unfrozen array whose annotation is wider than the inferred type
|
|
57
|
+
# NAMES = T.let(["alice", "bob"], T::Array[T.nilable(String)])
|
|
58
|
+
#
|
|
59
|
+
# # good — type is not the literal's own class
|
|
60
|
+
# value = T.let("hello", T.nilable(String))
|
|
61
|
+
#
|
|
62
|
+
# # good — instance variables need T.let for Sorbet to track their type
|
|
63
|
+
# @max_retries = T.let(3, Integer)
|
|
64
|
+
#
|
|
65
|
+
# # good — local variables may need T.let so Sorbet allows reassignment
|
|
66
|
+
# count = T.let(0, Integer)
|
|
67
|
+
class RedundantTLetForLiteral < Base
|
|
68
|
+
include ConstantScope
|
|
69
|
+
include TargetSorbetVersion
|
|
70
|
+
include TLetCorrection
|
|
71
|
+
extend AutoCorrector
|
|
72
|
+
|
|
73
|
+
# The frozen-literal and literal-array inference the newer cases rely on
|
|
74
|
+
# was added up to Sorbet 0.6.13304 (frozen regexp via freeze-transparent
|
|
75
|
+
# inference; literal arrays via constant tuple inference). Flagging them
|
|
76
|
+
# against an older Sorbet could remove a `T.let` it still requires, so
|
|
77
|
+
# those paths disable themselves below that version. Bare simple
|
|
78
|
+
# literals (`T.let(3, Integer)`) predate this and are not gated.
|
|
79
|
+
minimum_target_sorbet_static_version "0.6.13304"
|
|
80
|
+
|
|
81
|
+
MSG = "Redundant `T.let` for %{type} literal. Sorbet can infer this type automatically."
|
|
82
|
+
|
|
83
|
+
# Simple literal node types Sorbet infers, mapped to the class name.
|
|
84
|
+
# Interpolated strings/symbols (`dstr`/`dsym`) infer as `String`/`Symbol`.
|
|
85
|
+
LITERAL_TYPE_TO_CLASS = {
|
|
86
|
+
dstr: :String,
|
|
87
|
+
dsym: :Symbol,
|
|
88
|
+
float: :Float,
|
|
89
|
+
int: :Integer,
|
|
90
|
+
regexp: :Regexp,
|
|
91
|
+
str: :String,
|
|
92
|
+
sym: :Symbol,
|
|
93
|
+
}.freeze
|
|
94
|
+
|
|
95
|
+
# Element node types allowed inside an inferable array literal: the
|
|
96
|
+
# literals whose class Sorbet reflects into the array's element type.
|
|
97
|
+
# Regexps and ranges, by contrast, degrade the array to `T.untyped`, so
|
|
98
|
+
# they are excluded. Interpolated strings/symbols (`dstr`/`dsym`) infer
|
|
99
|
+
# as `String`/`Symbol`, the same as their plain literal forms.
|
|
100
|
+
ARRAY_ELEMENT_TYPES = [:dstr, :dsym, :str, :sym, :int, :float, :true, :false, :nil].freeze
|
|
101
|
+
|
|
102
|
+
# Element node types whose inferred class is unambiguous, used to derive
|
|
103
|
+
# the element type of an unfrozen array literal.
|
|
104
|
+
ELEMENT_TYPE_TO_CLASS = {
|
|
105
|
+
dstr: "String",
|
|
106
|
+
dsym: "Symbol",
|
|
107
|
+
float: "Float",
|
|
108
|
+
int: "Integer",
|
|
109
|
+
str: "String",
|
|
110
|
+
sym: "Symbol",
|
|
111
|
+
}.freeze
|
|
112
|
+
|
|
113
|
+
# @!method t_let_with_literal_and_class?(node)
|
|
114
|
+
def_node_matcher :t_let_with_literal_and_class?, <<~PATTERN
|
|
115
|
+
(casgn _ _ (send (const {nil? cbase} :T) :let ${literal? (send (regexp ...) :freeze)} (const nil? $_)))
|
|
116
|
+
PATTERN
|
|
117
|
+
|
|
118
|
+
# @!method t_let_with_array?(node)
|
|
119
|
+
def_node_matcher :t_let_with_array?, <<~PATTERN
|
|
120
|
+
(casgn _ _ (send (const {nil? cbase} :T) :let ${array (send array :freeze)} $_))
|
|
121
|
+
PATTERN
|
|
122
|
+
|
|
123
|
+
def on_casgn(node)
|
|
124
|
+
return unless statically_scoped?(node)
|
|
125
|
+
|
|
126
|
+
t_let_with_literal_and_class?(node) do |value_node, class_name|
|
|
127
|
+
literal_node = inferable_literal_node(value_node)
|
|
128
|
+
next unless literal_node
|
|
129
|
+
next unless LITERAL_TYPE_TO_CLASS[literal_node.type] == class_name
|
|
130
|
+
|
|
131
|
+
register_offense(node, value_node, class_name)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
return unless enabled_for_sorbet_static_version?
|
|
135
|
+
|
|
136
|
+
t_let_with_array?(node) do |value_node, type_node|
|
|
137
|
+
frozen = value_node.send_type?
|
|
138
|
+
array_node = frozen ? value_node.receiver : value_node
|
|
139
|
+
next unless inferable_array?(array_node)
|
|
140
|
+
next unless redundant_array_annotation?(array_node, type_node, frozen: frozen)
|
|
141
|
+
|
|
142
|
+
register_offense(node, value_node, :Array)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
private
|
|
147
|
+
|
|
148
|
+
# Returns the underlying literal when its inference is supported by the
|
|
149
|
+
# target Sorbet version. Bare literals are always supported; for example,
|
|
150
|
+
# `3` returns its integer node. Frozen literals are version-gated, so
|
|
151
|
+
# `/foo/.freeze` returns its regexp node only for supported targets.
|
|
152
|
+
def inferable_literal_node(value_node)
|
|
153
|
+
return value_node unless value_node.send_type?
|
|
154
|
+
return unless enabled_for_sorbet_static_version?
|
|
155
|
+
|
|
156
|
+
value_node.receiver
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def register_offense(node, value_node, type)
|
|
160
|
+
t_let_node = node.children[2]
|
|
161
|
+
add_offense(t_let_node, message: format(MSG, type: type)) do |corrector|
|
|
162
|
+
replace_t_let(corrector, t_let_node, value_node)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# An array literal is inferable only when every element is one of the
|
|
167
|
+
# simple literals Sorbet reflects into the element type (or a nested
|
|
168
|
+
# inferable array). Empty arrays are excluded: they infer as
|
|
169
|
+
# `T::Array[T.untyped]` and so still need the annotation.
|
|
170
|
+
def inferable_array?(node)
|
|
171
|
+
return false unless node.array_type?
|
|
172
|
+
return false if node.children.empty?
|
|
173
|
+
|
|
174
|
+
node.children.all? do |child|
|
|
175
|
+
if child.array_type?
|
|
176
|
+
inferable_array?(child)
|
|
177
|
+
else
|
|
178
|
+
ARRAY_ELEMENT_TYPES.include?(child.type)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Returns whether Sorbet's inferred array type makes the annotation
|
|
184
|
+
# redundant. A frozen `[:a].freeze` infers as a tuple compatible with
|
|
185
|
+
# any `T::Array[...]`; an unfrozen `[:a]` must infer exactly the
|
|
186
|
+
# annotated type, such as `T::Array[Symbol]`.
|
|
187
|
+
def redundant_array_annotation?(array_node, type_node, frozen:)
|
|
188
|
+
return t_array_type?(type_node) if frozen
|
|
189
|
+
|
|
190
|
+
inferred_array_type(array_node) == normalize(type_node.source).delete_prefix("::")
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# `T::Array[...]` (with or without a leading `::`)
|
|
194
|
+
def t_array_type?(node)
|
|
195
|
+
node.send_type? && node.method?(:[]) &&
|
|
196
|
+
node.receiver&.source&.delete_prefix("::") == "T::Array"
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# The type Sorbet infers for an unfrozen array literal, or nil when the
|
|
200
|
+
# element types are not uniform enough to render deterministically
|
|
201
|
+
# (mixed classes, booleans, nils and nested arrays are left alone).
|
|
202
|
+
def inferred_array_type(node)
|
|
203
|
+
classes = node.children.map { |child| ELEMENT_TYPE_TO_CLASS[child.type] }
|
|
204
|
+
return unless classes.all? && classes.uniq.size == 1
|
|
205
|
+
|
|
206
|
+
"T::Array[#{classes.first}]"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Strips whitespace and any trailing comma before a closing delimiter,
|
|
210
|
+
# so a multi-line annotation (`T::Array[\n String,\n]`) still compares
|
|
211
|
+
# equal to the rendered inferred type.
|
|
212
|
+
def normalize(source)
|
|
213
|
+
source.gsub(/\s+/, "").gsub(/,([)\]}])/, '\1')
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
@@ -23,11 +23,13 @@ module RuboCop
|
|
|
23
23
|
# * `ParameterTypePlaceholder`: placeholders used for parameter types (default: 'T.untyped')
|
|
24
24
|
# * `ReturnTypePlaceholder`: placeholders used for return types (default: 'T.untyped')
|
|
25
25
|
# * `Style`: signature style to enforce - 'sig' for sig blocks, 'rbs' for RBS comments, 'both' to allow either (default: 'sig')
|
|
26
|
+
# * `AutocorrectStyle`: signature style to use when autocorrecting - 'sig' for sig blocks, 'rbs' for RBS comments (default: 'sig'). Only used when `Style` is 'both'.
|
|
26
27
|
class EnforceSignatures < ::RuboCop::Cop::Base
|
|
27
28
|
extend AutoCorrector
|
|
28
29
|
include SignatureHelp
|
|
29
30
|
|
|
30
31
|
VALID_STYLES = ["sig", "rbs", "both"].freeze
|
|
32
|
+
VALID_AUTOCORRECT_STYLES = ["sig", "rbs"].freeze
|
|
31
33
|
|
|
32
34
|
# @!method accessor?(node)
|
|
33
35
|
def_node_matcher(:accessor?, <<-PATTERN)
|
|
@@ -87,7 +89,7 @@ module RuboCop
|
|
|
87
89
|
# Both styles allowed - require at least one
|
|
88
90
|
unless sig_node || rbs_node
|
|
89
91
|
add_offense(node, message: "Each method is required to have a signature.") do |corrector|
|
|
90
|
-
autocorrect_with_signature_type(corrector, node,
|
|
92
|
+
autocorrect_with_signature_type(corrector, node, autocorrect_style)
|
|
91
93
|
end
|
|
92
94
|
end
|
|
93
95
|
else # "sig" (default)
|
|
@@ -192,6 +194,15 @@ module RuboCop
|
|
|
192
194
|
"sig"
|
|
193
195
|
end
|
|
194
196
|
|
|
197
|
+
def autocorrect_style
|
|
198
|
+
config_value = cop_config["AutocorrectStyle"] || "sig"
|
|
199
|
+
unless VALID_AUTOCORRECT_STYLES.include?(config_value)
|
|
200
|
+
raise ArgumentError, "Invalid AutocorrectStyle option: '#{config_value}'. Valid options are: #{VALID_AUTOCORRECT_STYLES.join(", ")}"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
config_value
|
|
204
|
+
end
|
|
205
|
+
|
|
195
206
|
class SignatureChecker
|
|
196
207
|
def initialize(processed_source)
|
|
197
208
|
@processed_source = processed_source
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require_relative "sorbet/mixin/target_sorbet_version"
|
|
4
4
|
require_relative "sorbet/mixin/t_enum"
|
|
5
5
|
require_relative "sorbet/mixin/signature_help"
|
|
6
|
+
require_relative "sorbet/mixin/constant_scope"
|
|
7
|
+
require_relative "sorbet/mixin/t_let_correction"
|
|
6
8
|
|
|
7
9
|
require_relative "sorbet/binding_constant_without_type_alias"
|
|
8
10
|
require_relative "sorbet/block_method_definition"
|
|
@@ -27,7 +29,9 @@ require_relative "sorbet/forbid_t_struct"
|
|
|
27
29
|
require_relative "sorbet/forbid_t_type_alias"
|
|
28
30
|
require_relative "sorbet/forbid_t_unsafe"
|
|
29
31
|
require_relative "sorbet/forbid_t_untyped"
|
|
32
|
+
require_relative "sorbet/redundant_t_let"
|
|
30
33
|
require_relative "sorbet/redundant_extend_t_sig"
|
|
34
|
+
require_relative "sorbet/redundant_t_let_for_literal"
|
|
31
35
|
require_relative "sorbet/refinement"
|
|
32
36
|
require_relative "sorbet/select_by_is_a"
|
|
33
37
|
require_relative "sorbet/type_alias_name"
|
data/manual/cops.md
CHANGED
|
@@ -49,6 +49,8 @@ In the following section you find all available cops:
|
|
|
49
49
|
* [Sorbet/MultipleTEnumValues](cops_sorbet.md#sorbetmultipletenumvalues)
|
|
50
50
|
* [Sorbet/ObsoleteStrictMemoization](cops_sorbet.md#sorbetobsoletestrictmemoization)
|
|
51
51
|
* [Sorbet/RedundantExtendTSig](cops_sorbet.md#sorbetredundantextendtsig)
|
|
52
|
+
* [Sorbet/RedundantTLet](cops_sorbet.md#sorbetredundanttlet)
|
|
53
|
+
* [Sorbet/RedundantTLetForLiteral](cops_sorbet.md#sorbetredundanttletforliteral)
|
|
52
54
|
* [Sorbet/Refinement](cops_sorbet.md#sorbetrefinement)
|
|
53
55
|
* [Sorbet/RuntimeOnFailureDependsOnChecked](cops_sorbet.md#sorbetruntimeonfailuredependsonchecked)
|
|
54
56
|
* [Sorbet/SelectByIsA](cops_sorbet.md#sorbetselectbyisa)
|
data/manual/cops_sorbet.md
CHANGED
|
@@ -310,7 +310,7 @@ other comments or magic comments are left in the same place.
|
|
|
310
310
|
|
|
311
311
|
Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged
|
|
312
312
|
--- | --- | --- | --- | ---
|
|
313
|
-
Disabled | Yes | Yes | 0.3.4 |
|
|
313
|
+
Disabled | Yes | Yes | 0.3.4 | 0.13.0
|
|
314
314
|
|
|
315
315
|
Checks that every method definition and attribute accessor has a Sorbet signature.
|
|
316
316
|
|
|
@@ -332,12 +332,14 @@ You can configure the placeholders used by changing the following options:
|
|
|
332
332
|
* `ParameterTypePlaceholder`: placeholders used for parameter types (default: 'T.untyped')
|
|
333
333
|
* `ReturnTypePlaceholder`: placeholders used for return types (default: 'T.untyped')
|
|
334
334
|
* `Style`: signature style to enforce - 'sig' for sig blocks, 'rbs' for RBS comments, 'both' to allow either (default: 'sig')
|
|
335
|
+
* `AutocorrectStyle`: signature style to use when autocorrecting - 'sig' for sig blocks, 'rbs' for RBS comments (default: 'sig'). Only used when `Style` is 'both'.
|
|
335
336
|
|
|
336
337
|
### Configurable attributes
|
|
337
338
|
|
|
338
339
|
Name | Default value | Configurable values
|
|
339
340
|
--- | --- | ---
|
|
340
341
|
Style | `sig` | String
|
|
342
|
+
AutocorrectStyle | `sig` | String
|
|
341
343
|
|
|
342
344
|
## Sorbet/EnforceSingleSigil
|
|
343
345
|
|
|
@@ -1160,6 +1162,126 @@ class Example
|
|
|
1160
1162
|
end
|
|
1161
1163
|
```
|
|
1162
1164
|
|
|
1165
|
+
## Sorbet/RedundantTLet
|
|
1166
|
+
|
|
1167
|
+
Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged
|
|
1168
|
+
--- | --- | --- | --- | ---
|
|
1169
|
+
Enabled | Yes | Yes (Unsafe) | 0.13.0 | -
|
|
1170
|
+
|
|
1171
|
+
Prevents unnecessary `T.let` where Sorbet infers the type automatically.
|
|
1172
|
+
|
|
1173
|
+
When a signature parameter is assigned to an instance variable in
|
|
1174
|
+
`initialize`, the type is inferred from the signature.
|
|
1175
|
+
|
|
1176
|
+
When a constant is assigned a constructor call (`.new`), optionally
|
|
1177
|
+
followed by `.freeze` (Sorbet 0.6.13304+), the type is inferred from
|
|
1178
|
+
the class being instantiated. Generic classes (e.g. `Set`) are
|
|
1179
|
+
excluded: Sorbet infers their constructor calls as applied types like
|
|
1180
|
+
`T::Set[T.untyped]`, so an annotation is still required.
|
|
1181
|
+
|
|
1182
|
+
### Examples
|
|
1183
|
+
|
|
1184
|
+
```ruby
|
|
1185
|
+
# bad
|
|
1186
|
+
sig { params(a: Integer) }
|
|
1187
|
+
def initialize(a)
|
|
1188
|
+
@a = T.let(a, Integer)
|
|
1189
|
+
end
|
|
1190
|
+
|
|
1191
|
+
# good
|
|
1192
|
+
sig { params(a: Integer) }
|
|
1193
|
+
def initialize(a)
|
|
1194
|
+
@a = a
|
|
1195
|
+
end
|
|
1196
|
+
|
|
1197
|
+
# good
|
|
1198
|
+
sig { params(a: Integer) }
|
|
1199
|
+
def initialize(a)
|
|
1200
|
+
@a = T.let(a, T.any(Integer, String))
|
|
1201
|
+
end
|
|
1202
|
+
|
|
1203
|
+
# bad
|
|
1204
|
+
DEFAULT_PATH = T.let(Pathname.new("/usr/local").freeze, Pathname)
|
|
1205
|
+
|
|
1206
|
+
# good
|
|
1207
|
+
DEFAULT_PATH = Pathname.new("/usr/local").freeze
|
|
1208
|
+
|
|
1209
|
+
# good — generic classes are not inferred, so T.let is required
|
|
1210
|
+
LICENSES = T.let(Set.new(["mit"]).freeze, T::Set[String])
|
|
1211
|
+
|
|
1212
|
+
# good — instance variables are only inferred from signature parameters
|
|
1213
|
+
@path = T.let(Pathname.new("/usr/local"), Pathname)
|
|
1214
|
+
```
|
|
1215
|
+
|
|
1216
|
+
## Sorbet/RedundantTLetForLiteral
|
|
1217
|
+
|
|
1218
|
+
Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged
|
|
1219
|
+
--- | --- | --- | --- | ---
|
|
1220
|
+
Enabled | Yes | Yes (Unsafe) | 0.13.0 | -
|
|
1221
|
+
|
|
1222
|
+
Checks for redundant `T.let` declarations where the first argument
|
|
1223
|
+
is a literal whose type Sorbet can infer automatically, so wrapping
|
|
1224
|
+
it in `T.let` is redundant.
|
|
1225
|
+
|
|
1226
|
+
Simple literals (strings, symbols, integers, floats, regexps) infer as
|
|
1227
|
+
their own class. Regexp literals are the only simple literals whose
|
|
1228
|
+
inference survives a `.freeze` call (Sorbet 0.6.13304+), so
|
|
1229
|
+
`T.let(/foo/.freeze, Regexp)` is also redundant; other frozen simple
|
|
1230
|
+
literals (e.g. `"hello".freeze`) are not inferred and still need `T.let`.
|
|
1231
|
+
|
|
1232
|
+
Array literals of simple literals are also inferred:
|
|
1233
|
+
|
|
1234
|
+
* A frozen array (`[...].freeze`) infers as a fixed-size tuple, which is
|
|
1235
|
+
a subtype of the annotated `T::Array`, so the annotation is redundant.
|
|
1236
|
+
* An unfrozen array infers as `T::Array[<element type>]`. It is only
|
|
1237
|
+
flagged when that inferred type matches the annotation exactly, to
|
|
1238
|
+
avoid silently widening (e.g. `["a", nil]` infers a nilable element).
|
|
1239
|
+
|
|
1240
|
+
Hashes are excluded: Sorbet infers hash literals as `T.untyped`, so the
|
|
1241
|
+
annotation is required.
|
|
1242
|
+
|
|
1243
|
+
### Examples
|
|
1244
|
+
|
|
1245
|
+
```ruby
|
|
1246
|
+
# bad
|
|
1247
|
+
MAX_RETRIES = T.let(3, Integer)
|
|
1248
|
+
GREETING = T.let("hello", String)
|
|
1249
|
+
RATE = T.let(1.5, Float)
|
|
1250
|
+
PATTERN = T.let(/foo/, Regexp)
|
|
1251
|
+
FROZEN_PATTERN = T.let(/foo/.freeze, Regexp)
|
|
1252
|
+
STATUS = T.let(:active, Symbol)
|
|
1253
|
+
SHELLS = T.let([:bash, :zsh].freeze, T::Array[Symbol])
|
|
1254
|
+
NAMES = T.let(["alice", "bob"], T::Array[String])
|
|
1255
|
+
|
|
1256
|
+
# good
|
|
1257
|
+
MAX_RETRIES = 3
|
|
1258
|
+
GREETING = "hello"
|
|
1259
|
+
RATE = 1.5
|
|
1260
|
+
PATTERN = /foo/
|
|
1261
|
+
FROZEN_PATTERN = /foo/.freeze
|
|
1262
|
+
STATUS = :active
|
|
1263
|
+
SHELLS = [:bash, :zsh].freeze
|
|
1264
|
+
NAMES = ["alice", "bob"]
|
|
1265
|
+
|
|
1266
|
+
# good — non-regexp frozen simple literals are not inferred
|
|
1267
|
+
GREETING = T.let("hello".freeze, String)
|
|
1268
|
+
|
|
1269
|
+
# good — hash literals are inferred as T.untyped
|
|
1270
|
+
OPTIONS = T.let({ verbose: true }, T::Hash[Symbol, T::Boolean])
|
|
1271
|
+
|
|
1272
|
+
# good — unfrozen array whose annotation is wider than the inferred type
|
|
1273
|
+
NAMES = T.let(["alice", "bob"], T::Array[T.nilable(String)])
|
|
1274
|
+
|
|
1275
|
+
# good — type is not the literal's own class
|
|
1276
|
+
value = T.let("hello", T.nilable(String))
|
|
1277
|
+
|
|
1278
|
+
# good — instance variables need T.let for Sorbet to track their type
|
|
1279
|
+
@max_retries = T.let(3, Integer)
|
|
1280
|
+
|
|
1281
|
+
# good — local variables may need T.let so Sorbet allows reassignment
|
|
1282
|
+
count = T.let(0, Integer)
|
|
1283
|
+
```
|
|
1284
|
+
|
|
1163
1285
|
## Sorbet/Refinement
|
|
1164
1286
|
|
|
1165
1287
|
Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged
|
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.
|
|
4
|
+
version: 0.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ufuk Kayserilioglu
|
|
@@ -54,6 +54,7 @@ files:
|
|
|
54
54
|
- ".github/workflows/ci.yml"
|
|
55
55
|
- ".github/workflows/cla.yml"
|
|
56
56
|
- ".github/workflows/dependabot_automerge.yml"
|
|
57
|
+
- ".github/workflows/release.yml"
|
|
57
58
|
- ".github/workflows/stale.yml"
|
|
58
59
|
- ".gitignore"
|
|
59
60
|
- ".rspec"
|
|
@@ -101,8 +102,10 @@ files:
|
|
|
101
102
|
- lib/rubocop/cop/sorbet/forbid_type_aliased_shapes.rb
|
|
102
103
|
- lib/rubocop/cop/sorbet/forbid_untyped_struct_props.rb
|
|
103
104
|
- lib/rubocop/cop/sorbet/implicit_conversion_method.rb
|
|
105
|
+
- lib/rubocop/cop/sorbet/mixin/constant_scope.rb
|
|
104
106
|
- lib/rubocop/cop/sorbet/mixin/signature_help.rb
|
|
105
107
|
- lib/rubocop/cop/sorbet/mixin/t_enum.rb
|
|
108
|
+
- lib/rubocop/cop/sorbet/mixin/t_let_correction.rb
|
|
106
109
|
- lib/rubocop/cop/sorbet/mixin/target_sorbet_version.rb
|
|
107
110
|
- lib/rubocop/cop/sorbet/mutable_constant_sorbet_aware_behaviour.rb
|
|
108
111
|
- lib/rubocop/cop/sorbet/obsolete_strict_memoization.rb
|
|
@@ -112,6 +115,8 @@ files:
|
|
|
112
115
|
- lib/rubocop/cop/sorbet/rbi_versioning/gem_version_annotation_helper.rb
|
|
113
116
|
- lib/rubocop/cop/sorbet/rbi_versioning/valid_gem_version_annotations.rb
|
|
114
117
|
- lib/rubocop/cop/sorbet/redundant_extend_t_sig.rb
|
|
118
|
+
- lib/rubocop/cop/sorbet/redundant_t_let.rb
|
|
119
|
+
- lib/rubocop/cop/sorbet/redundant_t_let_for_literal.rb
|
|
115
120
|
- lib/rubocop/cop/sorbet/refinement.rb
|
|
116
121
|
- lib/rubocop/cop/sorbet/select_by_is_a.rb
|
|
117
122
|
- lib/rubocop/cop/sorbet/sigils/enforce_sigil_order.rb
|
|
@@ -170,7 +175,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
170
175
|
- !ruby/object:Gem::Version
|
|
171
176
|
version: '0'
|
|
172
177
|
requirements: []
|
|
173
|
-
rubygems_version:
|
|
178
|
+
rubygems_version: 3.6.7
|
|
174
179
|
specification_version: 4
|
|
175
180
|
summary: Automatic Sorbet code style checking tool.
|
|
176
181
|
test_files: []
|