Vendor rubocop-sorbet.

This commit is contained in:
Markus Reiter 2020-09-20 05:45:30 +02:00
parent 90b92301ae
commit f27d7a21d7
30 changed files with 1429 additions and 0 deletions

1
.gitignore vendored
View File

@ -35,6 +35,7 @@
!**/vendor/bundle/ruby/*/gems/*/lib !**/vendor/bundle/ruby/*/gems/*/lib
!**/vendor/bundle/ruby/*/gems/rubocop-performance-*/config !**/vendor/bundle/ruby/*/gems/rubocop-performance-*/config
!**/vendor/bundle/ruby/*/gems/rubocop-rspec-*/config !**/vendor/bundle/ruby/*/gems/rubocop-rspec-*/config
!**/vendor/bundle/ruby/*/gems/rubocop-sorbet-*/config
# Ignore partially included gems where we don't need all files # Ignore partially included gems where we don't need all files
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support.rb **/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support.rb

View File

@ -25,4 +25,5 @@ gem "patchelf"
gem "plist" gem "plist"
gem "rubocop-performance" gem "rubocop-performance"
gem "rubocop-rspec" gem "rubocop-rspec"
gem "rubocop-sorbet"
gem "ruby-macho" gem "ruby-macho"

View File

@ -114,6 +114,8 @@ GEM
rubocop-ast (>= 0.4.0) rubocop-ast (>= 0.4.0)
rubocop-rspec (1.43.2) rubocop-rspec (1.43.2)
rubocop (~> 0.87) rubocop (~> 0.87)
rubocop-sorbet (0.5.1)
rubocop
ruby-macho (2.2.0) ruby-macho (2.2.0)
ruby-progressbar (1.10.1) ruby-progressbar (1.10.1)
simplecov (0.19.0) simplecov (0.19.0)
@ -167,6 +169,7 @@ DEPENDENCIES
rubocop rubocop
rubocop-performance rubocop-performance
rubocop-rspec rubocop-rspec
rubocop-sorbet
ruby-macho ruby-macho
simplecov simplecov
sorbet sorbet

View File

@ -75,6 +75,7 @@ $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/unicode-display_width
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/rubocop-0.92.0/lib" $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/rubocop-0.92.0/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/rubocop-performance-1.8.1/lib" $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/rubocop-performance-1.8.1/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/rubocop-rspec-1.43.2/lib" $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/rubocop-rspec-1.43.2/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/rubocop-sorbet-0.5.1/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/ruby-macho-2.2.0/lib" $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/ruby-macho-2.2.0/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/sorbet-static-0.5.5942-universal-darwin-19/lib" $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/sorbet-static-0.5.5942-universal-darwin-19/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/sorbet-0.5.5942/lib" $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/sorbet-0.5.5942/lib"

View File

@ -0,0 +1,128 @@
inherit_mode:
merge:
- Exclude
Sorbet/AllowIncompatibleOverride:
Description: 'Disallows using `.override(allow_incompatible: true)`.'
Enabled: true
VersionAdded: 0.2.0
Sorbet/BindingConstantWithoutTypeAlias:
Description: >-
Disallows binding the return value of `T.any`, `T.all`, `T.enum`
to a constant directly. To bind the value, one must use `T.type_alias`.
Enabled: true
VersionAdded: 0.2.0
Sorbet/CheckedTrueInSignature:
Description: 'Disallows the usage of `checked(true)` in signatures.'
Enabled: true
VersionAdded: 0.2.0
Sorbet/ConstantsFromStrings:
Description: >-
Forbids constant access through meta-programming.
For example, things like `constantize` or `const_get`
are forbidden.
Enabled: true
VersionAdded: 0.2.0
Sorbet/EnforceSigilOrder:
Description: 'Ensures that Sorbet sigil comes first in a file.'
Enabled: true
VersionAdded: 0.3.4
Sorbet/EnforceSignatures:
Description: 'Ensures all methods have a valid signature.'
Enabled: false
VersionAdded: 0.3.4
Sorbet/FalseSigil:
Description: 'All files must be at least at strictness `false`.'
Enabled: true
VersionAdded: 0.3.3
SuggestedStrictness: true
Include:
- "**/*.rb"
- "**/*.rbi"
- "**/*.rake"
- "**/*.ru"
Exclude:
- bin/**/*
- db/**/*.rb
- script/**/*
Sorbet/ForbidIncludeConstLiteral:
Description: 'Forbids include of non-literal constants.'
Enabled: false
VersionAdded: 0.2.0
VersionChanged: 0.5.0
Sorbet/ForbidSuperclassConstLiteral:
Description: 'Forbid superclasses which are non-literal constants.'
Enabled: false
VersionAdded: 0.2.0
VersionChanged: 0.5.0
Sorbet/ForbidUntypedStructProps:
Description: >-
Disallows use of `T.untyped` or `T.nilable(T.untyped)` as a
prop type for `T::Struct` subclasses.
Enabled: true
VersionAdded: 0.4.0
Sorbet/HasSigil:
Description: 'Makes the Sorbet typed sigil mandatory in all files.'
Enabled: false
VersionAdded: 0.3.3
Sorbet/IgnoreSigil:
Description: 'All files must be at least at strictness `ignore`.'
Enabled: false
VersionAdded: 0.3.3
Sorbet/KeywordArgumentOrdering:
Description: >-
Enforces a compatible keyword arguments with Sorbet.
All keyword arguments must be at the end of the parameters
list, and all keyword arguments with a default value must be
after those without default values.
Enabled: true
VersionAdded: 0.2.0
Sorbet/ParametersOrderingInSignature:
Description: 'Enforces same parameter order between a method and its signature.'
Enabled: true
VersionAdded: 0.2.0
Sorbet/SignatureBuildOrder:
Description: >-
Enforces the order of parts in a signature.
The order is first inheritance related builders,
then params, then return and finally the modifier
such as: `abstract.params(...).returns(...).soft`.'
Enabled: true
VersionAdded: 0.3.0
Sorbet/StrictSigil:
Description: 'All files must be at least at strictness `strict`.'
Enabled: false
VersionAdded: 0.3.3
Sorbet/StrongSigil:
Description: 'All files must be at least at strictness `strong`.'
Enabled: false
VersionAdded: 0.3.3
Sorbet/TrueSigil:
Description: 'All files must be at least at strictness `true`.'
Enabled: false
VersionAdded: 0.3.3
Sorbet/ValidSigil:
Description: 'All files must have a valid sigil.'
Enabled: true
VersionAdded: 0.3.3

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
require 'rubocop'
require_relative 'rubocop/sorbet'
require_relative 'rubocop/sorbet/version'
require_relative 'rubocop/sorbet/inject'
RuboCop::Sorbet::Inject.defaults!
require_relative 'rubocop/cop/sorbet_cops'

View File

@ -0,0 +1,121 @@
# frozen_string_literal: true
require 'rubocop'
module RuboCop
module Cop
module Sorbet
# This cop disallows binding the return value of `T.any`, `T.all`, `T.enum`
# to a constant directly. To bind the value, one must use `T.type_alias`.
#
# @example
#
# # bad
# FooOrBar = T.any(Foo, Bar)
#
# # good
# FooOrBar = T.type_alias { T.any(Foo, Bar) }
class BindingConstantWithoutTypeAlias < RuboCop::Cop::Cop
def_node_matcher(:binding_unaliased_type?, <<-PATTERN)
(casgn _ _ [#not_nil? #not_t_let? #not_dynamic_type_creation_with_block? #not_generic_parameter_decl? #method_needing_aliasing_on_t?])
PATTERN
def_node_matcher(:using_type_alias?, <<-PATTERN)
(block
(send
(const nil? :T) :type_alias)
_
_
)
PATTERN
def_node_matcher(:using_deprecated_type_alias_syntax?, <<-PATTERN)
(
send
(const nil? :T)
:type_alias
_
)
PATTERN
def_node_matcher(:t_let?, <<-PATTERN)
(
send
(const nil? :T)
:let
_
_
)
PATTERN
def_node_matcher(:dynamic_type_creation_with_block?, <<-PATTERN)
(block
(send
const :new ...)
_
_
)
PATTERN
def_node_matcher(:generic_parameter_decl?, <<-PATTERN)
(
send nil? {:type_template :type_member} ...
)
PATTERN
def_node_search(:method_needing_aliasing_on_t?, <<-PATTERN)
(
send
(const nil? :T)
{:any :all :noreturn :class_of :untyped :nilable :self_type :enum :proc}
...
)
PATTERN
def not_t_let?(node)
!t_let?(node)
end
def not_dynamic_type_creation_with_block?(node)
!dynamic_type_creation_with_block?(node)
end
def not_generic_parameter_decl?(node)
!generic_parameter_decl?(node)
end
def not_nil?(node)
!node.nil?
end
def on_casgn(node)
return unless binding_unaliased_type?(node) && !using_type_alias?(node.children[2])
if using_deprecated_type_alias_syntax?(node.children[2])
add_offense(
node.children[2],
message: "It looks like you're using the old `T.type_alias` syntax. " \
'`T.type_alias` now expects a block.' \
'Run Sorbet with the options "--autocorrect --error-white-list=5043" ' \
'to automatically upgrade to the new syntax.'
)
return
end
add_offense(
node.children[2],
message: "It looks like you're trying to bind a type to a constant. " \
'To do this, you must alias the type using `T.type_alias`.'
)
end
def autocorrect(node)
lambda do |corrector|
corrector.replace(
node.source_range,
"T.type_alias { #{node.source} }"
)
end
end
end
end
end
end

View File

@ -0,0 +1,53 @@
# frozen_string_literal: true
require 'rubocop'
module RuboCop
module Cop
module Sorbet
# This cop disallows the calls that are used to get constants fom Strings
# such as +constantize+, +const_get+, and +constants+.
#
# The goal of this cop is to make the code easier to statically analyze,
# more IDE-friendly, and more predictable. It leads to code that clearly
# expresses which values the constant can have.
#
# @example
#
# # bad
# class_name.constantize
#
# # bad
# constants.detect { |c| c.name == "User" }
#
# # bad
# const_get(class_name)
#
# # good
# case class_name
# when "User"
# User
# else
# raise ArgumentError
# end
#
# # good
# { "User" => User }.fetch(class_name)
class ConstantsFromStrings < ::RuboCop::Cop::Cop
def_node_matcher(:constant_from_string?, <<-PATTERN)
(send _ {:constantize :constants :const_get} ...)
PATTERN
def on_send(node)
return unless constant_from_string?(node)
add_offense(
node,
location: :selector,
message: "Don't use `#{node.method_name}`, it makes the code harder to understand, less editor-friendly, " \
"and impossible to analyze. Replace `#{node.method_name}` with a case/when or a hash."
)
end
end
end
end
end

View File

@ -0,0 +1,58 @@
# encoding: utf-8
# frozen_string_literal: true
require 'rubocop'
# Correct `send` expressions in include statements by constant literals.
#
# Sorbet, the static checker, is not (yet) able to support constructs on the
# following form:
#
# ```ruby
# class MyClass
# include send_expr
# end
# ```
#
# Multiple occurences of this can be found in Shopify's code base like:
#
# ```ruby
# include Rails.application.routes.url_helpers
# ```
# or
# ```ruby
# include Polaris::Engine.helpers
# ```
module RuboCop
module Cop
module Sorbet
class ForbidIncludeConstLiteral < RuboCop::Cop::Cop
MSG = 'Includes must only contain constant literals'
attr_accessor :used_names
def_node_matcher :not_lit_const_include?, <<-PATTERN
(send nil? {:include :extend :prepend}
$_
)
PATTERN
def initialize(*)
super
self.used_names = Set.new
end
def on_send(node)
return unless not_lit_const_include?(node) do |send_argument|
![:const, :self].include?(send_argument.type)
end
parent = node.parent
return unless parent
parent = parent.parent if [:begin, :block].include?(parent.type)
return unless [:module, :class, :sclass].include?(parent.type)
add_offense(node)
end
end
end
end
end

View File

@ -0,0 +1,45 @@
# encoding: utf-8
# frozen_string_literal: true
require 'rubocop'
# Correct superclass `send` expressions by constant literals.
#
# Sorbet, the static checker, is not (yet) able to support constructs on the
# following form:
#
# ```ruby
# class Foo < send_expr; end
# ```
#
# Multiple occurences of this can be found in Shopify's code base like:
#
# ```ruby
# class ShopScope < Component::TrustedIdScope[ShopIdentity::ShopId]
# ```
# or
# ```ruby
# class ApiClientEligibility < Struct.new(:api_client, :match_results, :shop)
# ```
module RuboCop
module Cop
module Sorbet
class ForbidSuperclassConstLiteral < RuboCop::Cop::Cop
MSG = 'Superclasses must only contain constant literals'
def_node_matcher :not_lit_const_superclass?, <<-PATTERN
(class
(const ...)
(send ...)
...
)
PATTERN
def on_class(node)
return unless not_lit_const_superclass?(node)
add_offense(node.child_nodes[1])
end
end
end
end
end

View File

@ -0,0 +1,58 @@
# encoding: utf-8
# frozen_string_literal: true
require 'rubocop'
module RuboCop
module Cop
module Sorbet
# This cop disallows use of `T.untyped` or `T.nilable(T.untyped)`
# as a prop type for `T::Struct`.
#
# @example
#
# # bad
# class SomeClass
# const :foo, T.untyped
# prop :bar, T.nilable(T.untyped)
# end
#
# # good
# class SomeClass
# const :foo, Integer
# prop :bar, T.nilable(String)
# end
class ForbidUntypedStructProps < RuboCop::Cop::Cop
MSG = 'Struct props cannot be T.untyped'
def_node_matcher :t_struct, <<~PATTERN
(const (const nil? :T) :Struct)
PATTERN
def_node_matcher :t_untyped, <<~PATTERN
(send (const nil? :T) :untyped)
PATTERN
def_node_matcher :t_nilable_untyped, <<~PATTERN
(send (const nil? :T) :nilable {#t_untyped #t_nilable_untyped})
PATTERN
def_node_matcher :subclass_of_t_struct?, <<~PATTERN
(class (const ...) #t_struct ...)
PATTERN
def_node_search :untyped_props, <<~PATTERN
(send nil? {:prop :const} _ {#t_untyped #t_nilable_untyped} ...)
PATTERN
def on_class(node)
return unless subclass_of_t_struct?(node)
untyped_props(node).each do |untyped_prop|
add_offense(untyped_prop.child_nodes[1])
end
end
end
end
end
end

View File

@ -0,0 +1,115 @@
# frozen_string_literal: true
require 'rubocop'
module RuboCop
module Cop
module Sorbet
# This cop checks that the Sorbet sigil comes as the first magic comment in the file.
#
# The expected order for magic comments is: typed, (en)?coding, warn_indent then frozen_string_literal.
#
# For example, the following bad ordering:
#
# ```ruby
# # frozen_string_literal: true
# # typed: true
# class Foo; end
# ```
#
# Will be corrected as:
#
# ```ruby
# # typed: true
# # frozen_string_literal: true
# class Foo; end
# ```
#
# Only `typed`, `(en)?coding`, `warn_indent` and `frozen_string_literal` magic comments are considered,
# other comments or magic comments are left in the same place.
class EnforceSigilOrder < ValidSigil
include RangeHelp
def investigate(processed_source)
return if processed_source.tokens.empty?
tokens = extract_magic_comments(processed_source)
return if tokens.empty?
check_magic_comments_order(tokens)
end
def autocorrect(_node)
lambda do |corrector|
tokens = extract_magic_comments(processed_source)
# Get the magic comments tokens in their expected order
expected = PREFERRED_ORDER.keys.map do |re|
tokens.select { |token| re.match?(token.text) }
end.flatten
tokens.each_with_index do |token, index|
corrector.replace(token.pos, expected[index].text)
end
# Remove blank lines between the magic comments
lines = tokens.map(&:line).to_set
(lines.min...lines.max).each do |line|
next if lines.include?(line)
next unless processed_source[line - 1].empty?
corrector.remove(source_range(processed_source.buffer, line, 0))
end
end
end
protected
CODING_REGEX = /#\s+(en)?coding:(?:\s+([\w]+))?/
INDENT_REGEX = /#\s+warn_indent:(?:\s+([\w]+))?/
FROZEN_REGEX = /#\s+frozen_string_literal:(?:\s+([\w]+))?/
PREFERRED_ORDER = {
CODING_REGEX => 'encoding',
SIGIL_REGEX => 'typed',
INDENT_REGEX => 'warn_indent',
FROZEN_REGEX => 'frozen_string_literal',
}.freeze
MAGIC_REGEX = Regexp.union(*PREFERRED_ORDER.keys)
# extraction
# Get all the tokens in `processed_source` that match `MAGIC_REGEX`
def extract_magic_comments(processed_source)
processed_source.tokens
.take_while { |token| token.type == :tCOMMENT }
.select { |token| MAGIC_REGEX.match?(token.text) }
end
# checks
def check_magic_comments_order(tokens)
# Get the current magic comments order
order = tokens.map do |token|
PREFERRED_ORDER.keys.find { |re| re.match?(token.text) }
end.compact.uniq
# Get the expected magic comments order based on the one used in the actual source
expected = PREFERRED_ORDER.keys.select do |re|
tokens.any? { |token| re.match?(token.text) }
end.uniq
if order != expected
tokens.each do |token|
add_offense(
token,
location: token.pos,
message: "Magic comments should be in the following order: #{PREFERRED_ORDER.values.join(', ')}."
)
end
end
end
end
end
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
require 'rubocop'
require_relative 'has_sigil'
module RuboCop
module Cop
module Sorbet
# This cop makes the Sorbet `false` sigil mandatory in all files.
class FalseSigil < HasSigil
def minimum_strictness
'false'
end
end
end
end
end

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
require 'rubocop'
require_relative 'valid_sigil'
module RuboCop
module Cop
module Sorbet
# This cop makes the Sorbet typed sigil mandatory in all files.
#
# Options:
#
# * `SuggestedStrictness`: Sorbet strictness level suggested in offense messages (default: 'false')
# * `MinimumStrictness`: If set, make offense if the strictness level in the file is below this one
#
# If a `MinimumStrictness` level is specified, it will be used in offense messages and autocorrect.
class HasSigil < ValidSigil
@registry = Cop.registry # So we can properly subclass this cop
def require_sigil_on_all_files?
true
end
end
end
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
require 'rubocop'
require_relative 'has_sigil'
module RuboCop
module Cop
module Sorbet
# This cop makes the Sorbet `ignore` sigil mandatory in all files.
class IgnoreSigil < HasSigil
def minimum_strictness
'ignore'
end
end
end
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
require 'rubocop'
require_relative 'has_sigil'
module RuboCop
module Cop
module Sorbet
# This cop makes the Sorbet `strict` sigil mandatory in all files.
class StrictSigil < HasSigil
def minimum_strictness
'strict'
end
end
end
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
require 'rubocop'
require_relative 'has_sigil'
module RuboCop
module Cop
module Sorbet
# This cop makes the Sorbet `strong` sigil mandatory in all files.
class StrongSigil < HasSigil
def minimum_strictness
'strong'
end
end
end
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
require 'rubocop'
require_relative 'has_sigil'
module RuboCop
module Cop
module Sorbet
# This cop makes the Sorbet `true` sigil mandatory in all files.
class TrueSigil < HasSigil
def minimum_strictness
'true'
end
end
end
end
end

View File

@ -0,0 +1,160 @@
# frozen_string_literal: true
require 'rubocop'
module RuboCop
module Cop
module Sorbet
# This cop checks that every Ruby file contains a valid Sorbet sigil.
# Adapted from: https://gist.github.com/clarkdave/85aca4e16f33fd52aceb6a0a29936e52
#
# Options:
#
# * `RequireSigilOnAllFiles`: make offense if the Sorbet typed is not found in the file (default: false)
# * `SuggestedStrictness`: Sorbet strictness level suggested in offense messages (default: 'false')
# * `MinimumStrictness`: If set, make offense if the strictness level in the file is below this one
#
# If a `MinimumStrictness` level is specified, it will be used in offense messages and autocorrect.
class ValidSigil < RuboCop::Cop::Cop
@registry = Cop.registry # So we can properly subclass this cop
def investigate(processed_source)
return if processed_source.tokens.empty?
sigil = extract_sigil(processed_source)
return unless check_sigil_present(sigil)
strictness = extract_strictness(sigil)
return unless check_strictness_not_empty(sigil, strictness)
return unless check_strictness_valid(sigil, strictness)
return unless check_strictness_level(sigil, strictness)
end
def autocorrect(_node)
lambda do |corrector|
return unless require_sigil_on_all_files?
return unless extract_sigil(processed_source).nil?
token = processed_source.tokens.first
replace_with = suggested_strictness_level(minimum_strictness, suggested_strictness)
sigil = "# typed: #{replace_with}"
if token.text.start_with?("#!") # shebang line
corrector.insert_after(token.pos, "\n#{sigil}")
else
corrector.insert_before(token.pos, "#{sigil}\n")
end
end
end
protected
STRICTNESS_LEVELS = %w(ignore false true strict strong)
SIGIL_REGEX = /#\s+typed:(?:\s+([\w]+))?/
# extraction
def extract_sigil(processed_source)
processed_source.tokens
.take_while { |token| token.type == :tCOMMENT }
.find { |token| SIGIL_REGEX.match?(token.text) }
end
def extract_strictness(sigil)
sigil.text.match(SIGIL_REGEX)&.captures&.first
end
# checks
def check_sigil_present(sigil)
return true unless sigil.nil?
token = processed_source.tokens.first
if require_sigil_on_all_files?
strictness = suggested_strictness_level(minimum_strictness, suggested_strictness)
add_offense(
token,
location: token.pos,
message: 'No Sorbet sigil found in file. ' \
"Try a `typed: #{strictness}` to start (you can also use `rubocop -a` to automatically add this)."
)
end
false
end
def suggested_strictness_level(minimum_strictness, suggested_strictness)
# if no minimum strictness is set (eg. using Sorbet/HasSigil without config) then
# we always use the suggested strictness which defaults to `false`
return suggested_strictness unless minimum_strictness
# special case: if you're using Sorbet/IgnoreSigil without config, we should recommend `ignore`
return "ignore" if minimum_strictness == "ignore" && cop_config['SuggestedStrictness'].nil?
# if a minimum strictness is set (eg. you're using Sorbet/FalseSigil)
# we want to compare the minimum strictness and suggested strictness. this is because
# the suggested strictness might be higher than the minimum (eg. if you want all new files
# at a higher strictness level, without having to migrate existing files at lower levels).
suggested_level = STRICTNESS_LEVELS.index(suggested_strictness)
minimum_level = STRICTNESS_LEVELS.index(minimum_strictness)
suggested_level > minimum_level ? suggested_strictness : minimum_strictness
end
def check_strictness_not_empty(sigil, strictness)
return true if strictness
add_offense(
sigil,
location: sigil.pos,
message: 'Sorbet sigil should not be empty.'
)
false
end
def check_strictness_valid(sigil, strictness)
return true if STRICTNESS_LEVELS.include?(strictness)
add_offense(
sigil,
location: sigil.pos,
message: "Invalid Sorbet sigil `#{strictness}`."
)
false
end
def check_strictness_level(sigil, strictness)
return true unless minimum_strictness
minimum_level = STRICTNESS_LEVELS.index(minimum_strictness)
current_level = STRICTNESS_LEVELS.index(strictness)
if current_level < minimum_level
add_offense(
sigil,
location: sigil.pos,
message: "Sorbet sigil should be at least `#{minimum_strictness}` got `#{strictness}`."
)
return false
end
true
end
# options
# Default is `false`
def require_sigil_on_all_files?
!!cop_config['RequireSigilOnAllFiles']
end
# Default is `'false'`
def suggested_strictness
STRICTNESS_LEVELS.include?(cop_config['SuggestedStrictness']) ? cop_config['SuggestedStrictness'] : 'false'
end
# Default is `nil`
def minimum_strictness
cop_config['MinimumStrictness'] if STRICTNESS_LEVELS.include?(cop_config['MinimumStrictness'])
end
end
end
end
end

View File

@ -0,0 +1,59 @@
# frozen_string_literal: true
require 'rubocop'
module RuboCop
module Cop
module Sorbet
# This cop disallows using `.override(allow_incompatible: true)`.
# Using `allow_incompatible` suggests a violation of the Liskov
# Substitution Principle, meaning that a subclass is not a valid
# subtype of it's superclass. This Cop prevents these design smells
# from occurring.
#
# @example
#
# # bad
# sig.override(allow_incompatible: true)
#
# # good
# sig.override
class AllowIncompatibleOverride < RuboCop::Cop::Cop
def_node_search(:sig?, <<-PATTERN)
(
send
nil?
:sig
...
)
PATTERN
def not_nil?(node)
!node.nil?
end
def_node_search(:allow_incompatible?, <<-PATTERN)
(pair (sym :allow_incompatible) (true))
PATTERN
def_node_matcher(:allow_incompatible_override?, <<-PATTERN)
(
send
[#not_nil? #sig?]
:override
[#not_nil? #allow_incompatible?]
)
PATTERN
def on_send(node)
return unless allow_incompatible_override?(node)
add_offense(
node.children[2],
message: 'Usage of `allow_incompatible` suggests a violation of the Liskov Substitution Principle. '\
'Instead, strive to write interfaces which respect subtyping principles and remove `allow_incompatible`',
)
end
end
end
end
end

View File

@ -0,0 +1,53 @@
# frozen_string_literal: true
require 'rubocop'
require_relative 'signature_cop'
module RuboCop
module Cop
module Sorbet
# This cop disallows the usage of `checked(true)`. This usage could cause
# confusion; it could lead some people to believe that a method would be checked
# even if runtime checks have not been enabled on the class or globally.
# Additionally, in the event where checks are enabled, `checked(true)` would
# be redundant; only `checked(false)` or `soft` would change the behaviour.
#
# @example
#
# # bad
# sig { void.checked(true) }
#
# # good
# sig { void }
class CheckedTrueInSignature < SignatureCop
include(RuboCop::Cop::RangeHelp)
def_node_search(:offending_node, <<~PATTERN)
(send _ :checked (true))
PATTERN
MESSAGE =
'Using `checked(true)` in a method signature definition is not allowed. ' \
'`checked(true)` is the default behavior for modules/classes with runtime checks enabled. ' \
'To enable typechecking at runtime for this module, regardless of global settings, ' \
'`include(WaffleCone::RuntimeChecks)` to this module and set other methods to `checked(false)`.'
private_constant(:MESSAGE)
def on_signature(node)
error = offending_node(node).first
return unless error
add_offense(
error,
location: source_range(
processed_source.buffer,
error.location.line,
(error.location.selector.begin_pos)..(error.location.end.begin_pos),
),
message: MESSAGE
)
end
end
end
end
end

View File

@ -0,0 +1,135 @@
# frozen_string_literal: true
require 'rubocop'
require 'stringio'
require_relative 'signature_cop'
module RuboCop
module Cop
module Sorbet
# This cop checks that every method definition and attribute accessor has a Sorbet signature.
#
# It also suggest an autocorrect with placeholders so the following code:
#
# ```
# def foo(a, b, c); end
# ```
#
# Will be corrected as:
#
# ```
# sig { params(a: T.untyped, b: T.untyped, c: T.untyped).returns(T.untyped)
# def foo(a, b, c); end
# ```
#
# You can configure the placeholders used by changing the following options:
#
# * `ParameterTypePlaceholder`: placeholders used for parameter types (default: 'T.untyped')
# * `ReturnTypePlaceholder`: placeholders used for return types (default: 'T.untyped')
class EnforceSignatures < SignatureCop
def_node_matcher(:accessor?, <<-PATTERN)
(send nil? {:attr_reader :attr_writer :attr_accessor} ...)
PATTERN
def on_def(node)
check_node(node)
end
def on_defs(node)
check_node(node)
end
def on_send(node)
return unless accessor?(node)
check_node(node)
end
def autocorrect(node)
lambda do |corrector|
suggest = SigSuggestion.new(node.loc.column, param_type_placeholder, return_type_placeholder)
if node.is_a?(RuboCop::AST::DefNode) # def something
node.arguments.each do |arg|
suggest.params << arg.children.first
end
elsif accessor?(node) # attr reader, writer, accessor
method = node.children[1]
symbol = node.children[2]
suggest.params << symbol.value if symbol && (method == :attr_writer || method == :attr_accessor)
suggest.returns = 'void' if method == :attr_writer
end
corrector.insert_before(node.loc.expression, suggest.to_autocorrect)
end
end
private
def check_node(node)
prev = previous_node(node)
unless signature?(prev)
add_offense(
node,
message: "Each method is required to have a signature."
)
end
end
def previous_node(node)
parent = node.parent
return nil unless parent
parent.children[node.sibling_index - 1]
end
def param_type_placeholder
cop_config['ParameterTypePlaceholder'] || 'T.untyped'
end
def return_type_placeholder
cop_config['ReturnTypePlaceholder'] || 'T.untyped'
end
class SigSuggestion
attr_accessor :params, :returns
def initialize(indent, param_placeholder, return_placeholder)
@params = []
@returns = nil
@indent = indent
@param_placeholder = param_placeholder
@return_placeholder = return_placeholder
end
def to_autocorrect
out = StringIO.new
out << 'sig { '
out << generate_params
out << generate_return
out << " }\n"
out << ' ' * @indent # preserve indent for the next line
out.string
end
private
def generate_params
return if @params.empty?
out = StringIO.new
out << 'params('
out << @params.map do |param|
"#{param}: #{@param_placeholder}"
end.join(", ")
out << ').'
out.string
end
def generate_return
return "returns(#{@return_placeholder})" if @returns.nil?
return @returns if @returns == 'void'
"returns(#{@returns})"
end
end
end
end
end
end

View File

@ -0,0 +1,51 @@
# frozen_string_literal: true
require 'rubocop'
require_relative 'signature_cop'
module RuboCop
module Cop
module Sorbet
# This cop checks for the ordering of keyword arguments required by
# sorbet-runtime. The ordering requires that all keyword arguments
# are at the end of the parameters list, and all keyword arguments
# with a default value must be after those without default values.
#
# @example
#
# # bad
# sig { params(a: Integer, b: String).void }
# def foo(a: 1, b:); end
#
# # good
# sig { params(b: String, a: Integer).void }
# def foo(b:, a: 1); end
class KeywordArgumentOrdering < SignatureCop
def on_signature(node)
method_node = node.parent.children[node.sibling_index + 1]
return if method_node.nil?
method_parameters = method_node.arguments
check_order_for_kwoptargs(method_parameters)
end
private
def check_order_for_kwoptargs(parameters)
out_of_kwoptarg = false
parameters.reverse.each do |param|
out_of_kwoptarg = true unless param.type == :kwoptarg || param.type == :blockarg || param.type == :kwrestarg
next unless param.type == :kwoptarg && out_of_kwoptarg
add_offense(
param,
message: 'Optional keyword arguments must be at the end of the parameter list.'
)
end
end
end
end
end
end

View File

@ -0,0 +1,70 @@
# frozen_string_literal: true
require 'rubocop'
require_relative 'signature_cop'
module RuboCop
module Cop
module Sorbet
# This cop checks for inconsistent ordering of parameters between the
# signature and the method definition. The sorbet-runtime gem raises
# when such inconsistency occurs.
#
# @example
#
# # bad
# sig { params(a: Integer, b: String).void }
# def foo(b:, a:); end
#
# # good
# sig { params(a: Integer, b: String).void }
# def foo(a:, b:); end
class ParametersOrderingInSignature < SignatureCop
def_node_search(:signature_params, <<-PATTERN)
(send _ :params ...)
PATTERN
def on_signature(node)
sig_params = signature_params(node).first
sig_params_order = extract_parameters(sig_params)
return if sig_params_order.nil?
method_node = node.parent.children[node.sibling_index + 1]
return if method_node.nil? || method_node.type != :def
method_parameters = method_node.arguments
check_for_inconsistent_param_ordering(sig_params_order, method_parameters)
end
private
def extract_parameters(sig_params)
return [] if sig_params.nil?
arguments = sig_params.arguments.first
return arguments.keys.map(&:value) if RuboCop::AST::HashNode === arguments
add_offense(
sig_params,
message: "Invalid signature."
)
end
def check_for_inconsistent_param_ordering(sig_params_order, parameters)
parameters.each_with_index do |param, index|
param_name = param.children[0]
sig_param_name = sig_params_order[index]
next if param_name == sig_param_name
add_offense(
param,
message: "Inconsistent ordering of arguments at index #{index}. " \
"Expected `#{sig_param_name}` from sig above."
)
end
end
end
end
end
end

View File

@ -0,0 +1,104 @@
# frozen_string_literal: true
require 'rubocop'
require_relative 'signature_cop'
begin
require 'unparser'
rescue LoadError
nil
end
module RuboCop
module Cop
module Sorbet
class SignatureBuildOrder < SignatureCop
ORDER =
[
:abstract,
:override,
:overridable,
:type_parameters,
:params,
:returns,
:void,
:soft,
:checked,
:on_failure,
].each_with_index.to_h.freeze
def_node_search(:root_call, <<~PATTERN)
(send nil? {#{ORDER.keys.map(&:inspect).join(' ')}} ...)
PATTERN
def on_signature(node)
calls = call_chain(node.children[2]).map(&:method_name)
return unless calls.any?
expected_order = calls.sort_by { |call| ORDER[call] }
return if expected_order == calls
message = "Sig builders must be invoked in the following order: #{expected_order.join(', ')}."
unless can_autocorrect?
message += ' For autocorrection, add the `unparser` gem to your project.'
end
add_offense(
node.children[2],
message: message,
)
node
end
def autocorrect(node)
return nil unless can_autocorrect?
lambda do |corrector|
tree = call_chain(node_with_index_sends(node))
.sort_by { |call| ORDER[call.method_name] }
.reduce(nil) do |receiver, caller|
caller.updated(nil, [receiver] + caller.children.drop(1))
end
corrector.replace(
node.source_range,
Unparser.unparse(tree),
)
end
end
private
def node_with_index_sends(node)
# This is really dirty hack to reparse the current node with index send
# emitting enabled, which is necessary to unparse them back as index accessors.
emit_index_value = RuboCop::AST::Builder.emit_index
RuboCop::AST::Builder.emit_index = true
RuboCop::AST::ProcessedSource.new(node.source, target_ruby_version, processed_source.path).ast
ensure
RuboCop::AST::Builder.emit_index = emit_index_value
end
def can_autocorrect?
defined?(::Unparser)
end
def call_chain(sig_child_node)
call_node = root_call(sig_child_node).first
return [] unless call_node
calls = []
while call_node != sig_child_node
calls << call_node
call_node = call_node.parent
end
calls << sig_child_node
calls
end
end
end
end
end

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
require 'rubocop'
module RuboCop
module Cop
module Sorbet
# Abstract cop specific to Sorbet signatures
#
# You can subclass it to use the `on_signature` trigger and the `signature?` node matcher.
class SignatureCop < RuboCop::Cop::Cop
@registry = Cop.registry # So we can properly subclass this cop
def_node_matcher(:signature?, <<~PATTERN)
(block (send nil? :sig) (args) ...)
PATTERN
def on_block(node)
on_signature(node) if signature?(node)
end
def on_signature(_)
# To be defined in subclasses
end
end
end
end
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
require_relative 'sorbet/binding_constants_without_type_alias'
require_relative 'sorbet/constants_from_strings'
require_relative 'sorbet/forbid_superclass_const_literal'
require_relative 'sorbet/forbid_include_const_literal'
require_relative 'sorbet/forbid_untyped_struct_props'
require_relative 'sorbet/signatures/allow_incompatible_override'
require_relative 'sorbet/signatures/checked_true_in_signature'
require_relative 'sorbet/signatures/keyword_argument_ordering'
require_relative 'sorbet/signatures/parameters_ordering_in_signature'
require_relative 'sorbet/signatures/signature_build_order'
require_relative 'sorbet/signatures/enforce_signatures'
require_relative 'sorbet/sigils/valid_sigil'
require_relative 'sorbet/sigils/has_sigil'
require_relative 'sorbet/sigils/ignore_sigil'
require_relative 'sorbet/sigils/false_sigil'
require_relative 'sorbet/sigils/true_sigil'
require_relative 'sorbet/sigils/strict_sigil'
require_relative 'sorbet/sigils/strong_sigil'
require_relative 'sorbet/sigils/enforce_sigil_order'

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
require "rubocop/sorbet/version"
require "yaml"
module RuboCop
module Sorbet
class Error < StandardError; end
PROJECT_ROOT = Pathname.new(__dir__).parent.parent.expand_path.freeze
CONFIG_DEFAULT = PROJECT_ROOT.join('config', 'default.yml').freeze
CONFIG = YAML.safe_load(CONFIG_DEFAULT.read).freeze
private_constant(:CONFIG_DEFAULT, :PROJECT_ROOT)
end
end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
# The original code is from https://github.com/rubocop-hq/rubocop-rspec/blob/master/lib/rubocop/rspec/inject.rb
# See https://github.com/rubocop-hq/rubocop-rspec/blob/master/MIT-LICENSE.md
module RuboCop
module Sorbet
# Because RuboCop doesn't yet support plugins, we have to monkey patch in a
# bit of our configuration.
module Inject
def self.defaults!
path = CONFIG_DEFAULT.to_s
hash = ConfigLoader.send(:load_yaml_configuration, path)
config = Config.new(hash, path).tap(&:make_excludes_absolute)
puts "configuration from #{path}" if ConfigLoader.debug?
config = ConfigLoader.merge_with_default(config, path)
ConfigLoader.instance_variable_set(:@default_configuration, config)
end
end
end
end

View File

@ -0,0 +1,6 @@
# frozen_string_literal: true
module RuboCop
module Sorbet
VERSION = "0.5.1"
end
end