Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Strict type all non-test code #16

Merged
merged 10 commits into from
Jan 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,9 @@ RSpec/ExampleLength:
Enabled: false
RSpec/MultipleExpectations:
Enabled: false
Sorbet/EnforceSignatures:
Enabled: false
Sorbet/StrictSigil:
Enabled: false
Sorbet/StrongSigil:
Enabled: false
Sorbet/TrueSigil:
Enabled: true
Exclude:
- lib/yard-sorbet/struct_handler.rb
- spec/**/*
Style/AccessModifierDeclarations:
EnforcedStyle: inline
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## master (unreleased)

### Changes

* [#16](https://github.com/dduugg/yard-sorbet/pull/16): Enforce strict typing in non-spec code.

## 0.1.0 (2020-12-01)

### New features
Expand Down
2 changes: 2 additions & 0 deletions lib/yard-sorbet.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
require 'sorbet-runtime'
require 'yard'

T::Configuration.default_checked_level = :tests

# top-level namespace
module YARDSorbet; end

Expand Down
6 changes: 5 additions & 1 deletion lib/yard-sorbet/directives.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# typed: true
# typed: strict
# frozen_string_literal: true

# Extract & re-add directives to a docstring
module YARDSorbet::Directives
extend T::Sig

sig { params(docstring: T.nilable(String)).returns([YARD::Docstring, T::Array[String]]) }
def self.extract_directives(docstring)
parser = YARD::DocstringParser.new.parse(docstring)
# Directives are already parsed at this point, and there doesn't
Expand All @@ -16,6 +19,7 @@ def self.extract_directives(docstring)
[parser.to_docstring, directives]
end

sig { params(docstring: String, directives: T::Array[String]).void }
def self.add_directives(docstring, directives)
directives.each do |directive|
docstring.concat("\n#{directive}")
Expand Down
51 changes: 41 additions & 10 deletions lib/yard-sorbet/sig_handler.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# typed: true
# typed: strict
# frozen_string_literal: true

# A YARD Handler for Sorbet type declarations
class YARDSorbet::SigHandler < YARD::Handlers::Ruby::Base
extend T::Sig
handles :class, :module, :singleton_class?

SIG_NODE_TYPES = %i[call fcall vcall].freeze
SIG_NODE_TYPES = T.let(%i[call fcall vcall].freeze, T::Array[Symbol])
private_constant :SIG_NODE_TYPES

sig { returns(String).checked(:never) }
sig { void }
def process
# Find the list of declarations inside the class
class_def = statement.children.find { |c| c.type == :list }
Expand All @@ -18,6 +18,7 @@ def process
process_class_contents(class_contents)
end

sig { params(class_contents: T::Array[YARD::Parser::Ruby::MethodCallNode]).void }
private def process_class_contents(class_contents)
class_contents.each_with_index do |child, i|
if child.type == :sclass && child.children.size == 2 && child.children[1].type == :list
Expand All @@ -29,14 +30,21 @@ def process
next_statement = class_contents[i + 1]
next unless processable_method?(next_statement)

process_method_definition(next_statement, child)
process_method_definition(T.must(next_statement), child)
end
end

sig { params(next_statement: T.nilable(YARD::Parser::Ruby::AstNode)).returns(T::Boolean) }
private def processable_method?(next_statement)
%i[def defs command].include?(next_statement&.type) && !next_statement.docstring
%i[def defs command].include?(next_statement&.type) && !T.must(next_statement).docstring
end

sig do
params(
method_node: YARD::Parser::Ruby::AstNode,
sig_node: YARD::Parser::Ruby::MethodCallNode
).void
end
private def process_method_definition(method_node, sig_node)
# Swap the method definition docstring and the sig docstring.
# Parse relevant parts of the `sig` and include them as well.
Expand All @@ -54,6 +62,7 @@ def process
sig_node.docstring = nil
end

sig { params(docstring: YARD::Docstring, name: String, types: T::Array[String]).void }
private def enhance_param(docstring, name, types)
tag = docstring.tags.find { |t| t.tag_name == 'param' && t.name == name }
if tag
Expand All @@ -65,6 +74,7 @@ def process
docstring.add_tag(tag)
end

sig { params(docstring: YARD::Docstring, type: Symbol, parsed_sig: T::Hash[Symbol, Object]).void }
private def enhance_tag(docstring, type, parsed_sig)
return if !parsed_sig[type]

Expand All @@ -80,10 +90,22 @@ def process
docstring.add_tag(tag)
end

sig do
params(sig_node: YARD::Parser::Ruby::MethodCallNode)
.returns(
{
abstract: T::Boolean,
params: T::Hash[String, T::Array[String]],
return: T.nilable(T::Array[String])
}
)
end
private def parse_sig(sig_node)
parsed = {}
parsed[:abstract] = false
parsed[:params] = {}
parsed = {
abstract: false,
params: {},
return: nil
}
found_params = T.let(false, T::Boolean)
found_return = T.let(false, T::Boolean)
bfs_traverse(sig_node, exclude: %i[array hash]) do |n|
Expand All @@ -110,16 +132,18 @@ def process
end

# Returns true if the given node is part of a type signature.
sig { params(node: T.nilable(YARD::Parser::Ruby::AstNode)).returns(T::Boolean) }
private def type_signature?(node)
loop do
return false if node.nil?
return false unless SIG_NODE_TYPES.include?(node.type)
return true if T.unsafe(node).method_name(true) == :sig

node = node.children.first
node = T.let(node.children.first, T.nilable(YARD::Parser::Ruby::AstNode))
end
end

sig { params(node: YARD::Parser::Ruby::AstNode).returns(T.nilable(YARD::Parser::Ruby::AstNode)) }
private def sibling_node(node)
found_sibling = T.let(false, T::Boolean)
node.parent.children.each do |n|
Expand All @@ -135,7 +159,14 @@ def process
end

# @yield [YARD::Parser::Ruby::AstNode]
private def bfs_traverse(node, exclude: [])
sig do
params(
node: YARD::Parser::Ruby::AstNode,
exclude: T::Array[Symbol],
_blk: T.proc.params(n: YARD::Parser::Ruby::AstNode).void
).void
end
private def bfs_traverse(node, exclude: [], &_blk)
queue = [node]
while !queue.empty?
n = T.must(queue.shift)
Expand Down
9 changes: 7 additions & 2 deletions lib/yard-sorbet/sig_to_yard.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
# typed: true
# typed: strict
# frozen_string_literal: true

# Translate sig type syntax to YARD type syntax.
module YARDSorbet::SigToYARD
IS_LEGACY_RUBY_VERSION = RUBY_VERSION.start_with?('2.5.')
extend T::Sig

IS_LEGACY_RUBY_VERSION = T.let(RUBY_VERSION.start_with?('2.5.'), T::Boolean)

# @see https://yardoc.org/types.html
sig { params(node: YARD::Parser::Ruby::AstNode).returns(T::Array[String]) }
def self.convert(node)
types = convert_type(node)
# scrub newlines, as they break the YARD parser
types.map { |type| type.gsub(/\n\s*/, ' ') }
end

sig { params(node: YARD::Parser::Ruby::AstNode).returns(T::Array[String]) }
def self.convert_type(node)
children = node.children
case node.type
Expand Down Expand Up @@ -93,6 +97,7 @@ def self.convert_type(node)
end
end

sig { params(node: YARD::Parser::Ruby::AstNode).returns(String) }
def self.build_generic_type(node)
return node.source if node.children.empty? || node.type != :aref

Expand Down
18 changes: 13 additions & 5 deletions lib/yard-sorbet/struct_handler.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
# typed: false
# typed: strict
# frozen_string_literal: true

# Handle all `const` calls, creating accessor methods, and compiles them for later usage at the class level
# in creating a constructor
class YARDSorbet::StructHandler < YARD::Handlers::Ruby::Base
extend T::Sig

handles method_call(:const)
namespace_only

sig { void }
def process
# Store the property for use in the constructor definition
name = statement.parameters[0].jump(:ident).source
Expand Down Expand Up @@ -44,14 +47,19 @@ def process
# Class-level handler that folds all `const` declarations into the constructor documentation
# this needs to be injected as a module otherwise the default Class handler will overwrite documentation
module YARDSorbet::StructClassHandler
extend T::Sig

sig { void }
def process
ret = super

return ret if extra_state.prop_docs.nil?
return ret if T.unsafe(self).extra_state.prop_docs.nil?

# lookup the full YARD path for the current class
class_ns = YARD::CodeObjects::ClassObject.new(namespace, statement[0].source.gsub(/\s/, ''))
props = extra_state.prop_docs[class_ns]
class_ns = YARD::CodeObjects::ClassObject.new(
T.unsafe(self).namespace, T.unsafe(self).statement[0].source.gsub(/\s/, '')
)
props = T.unsafe(self).extra_state.prop_docs[class_ns]

return ret if props.empty?

Expand Down Expand Up @@ -80,7 +88,7 @@ def process
object.source ||= props.map { |p| p[:source] }.join("\n")
object.explicit ||= false # not strictly necessary

register(object)
T.unsafe(self).register(object)

object.docstring = docstring.to_raw

Expand Down
21 changes: 11 additions & 10 deletions sorbet/rbi/gems/regexp_parser.rbi
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
#
# https://github.com/sorbet/sorbet-typed/new/master?filename=lib/regexp_parser/all/regexp_parser.rbi
#
# regexp_parser-2.0.1
# regexp_parser-2.0.3

class Regexp
end
Expand Down Expand Up @@ -212,7 +212,7 @@ module Regexp::Syntax::Token::Literal
end
module Regexp::Syntax::Token::FreeSpace
end
class Regexp::Syntax::NotImplementedError < SyntaxError
class Regexp::Syntax::NotImplementedError < Regexp::Syntax::SyntaxError
def initialize(syntax, type, token); end
end
class Regexp::Syntax::Base
Expand All @@ -232,14 +232,14 @@ class Regexp::Syntax::Base
include Regexp::Syntax::Token
end
class Regexp::Syntax::Any < Regexp::Syntax::Base
def implements!(type, token); end
def implements?(type, token); end
def implements!(_type, _token); end
def implements?(_type, _token); end
def initialize; end
end
class Regexp::Syntax::InvalidVersionNameError < SyntaxError
class Regexp::Syntax::InvalidVersionNameError < Regexp::Syntax::SyntaxError
def initialize(name); end
end
class Regexp::Syntax::UnknownSyntaxNameError < SyntaxError
class Regexp::Syntax::UnknownSyntaxNameError < Regexp::Syntax::SyntaxError
def initialize(name); end
end
class Regexp::Syntax::V1_8_6 < Regexp::Syntax::Base
Expand Down Expand Up @@ -338,12 +338,12 @@ class Regexp::Expression::Subexpression < Regexp::Expression::Base
def at(*args, &block); end
def dig(*indices); end
def each(*args, &block); end
def each_expression(include_self = nil, &block); end
def each_expression(include_self = nil); end
def empty?(*args, &block); end
def expressions; end
def expressions=(arg0); end
def fetch(*args, &block); end
def flat_map(include_self = nil, &block); end
def flat_map(include_self = nil); end
def index(*args, &block); end
def initialize(token, options = nil); end
def initialize_clone(orig); end
Expand Down Expand Up @@ -524,7 +524,7 @@ class Regexp::Expression::EscapeSequence::MetaControl < Regexp::Expression::Esca
end
class Regexp::Expression::FreeSpace < Regexp::Expression::Base
def match_length; end
def quantify(token, text, min = nil, max = nil, mode = nil); end
def quantify(_token, _text, _min = nil, _max = nil, _mode = nil); end
end
class Regexp::Expression::Comment < Regexp::Expression::FreeSpace
end
Expand All @@ -541,6 +541,7 @@ end
class Regexp::Expression::Group::Passive < Regexp::Expression::Group::Base
def implicit=(arg0); end
def implicit?; end
def initialize(*arg0); end
def to_s(format = nil); end
end
class Regexp::Expression::Group::Absence < Regexp::Expression::Group::Base
Expand Down Expand Up @@ -888,7 +889,7 @@ class Regexp::MatchLength
def base_min; end
def base_min=(arg0); end
def each(opts = nil); end
def endless_each(&block); end
def endless_each; end
def exp_class; end
def exp_class=(arg0); end
def fixed?; end
Expand Down
2 changes: 1 addition & 1 deletion sorbet/rbi/gems/rspec-core.rbi
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
#
# https://github.com/sorbet/sorbet-typed/new/master?filename=lib/rspec-core/all/rspec-core.rbi
#
# rspec-core-3.10.0
# rspec-core-3.10.1

module RSpec
def self.clear_examples; end
Expand Down
2 changes: 1 addition & 1 deletion sorbet/rbi/gems/rspec-expectations.rbi
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
#
# https://github.com/sorbet/sorbet-typed/new/master?filename=lib/rspec-expectations/all/rspec-expectations.rbi
#
# rspec-expectations-3.10.0
# rspec-expectations-3.10.1

module RSpec
end
Expand Down
3 changes: 2 additions & 1 deletion sorbet/rbi/gems/rspec-mocks.rbi
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
#
# https://github.com/sorbet/sorbet-typed/new/master?filename=lib/rspec-mocks/all/rspec-mocks.rbi
#
# rspec-mocks-3.10.0
# rspec-mocks-3.10.1

module RSpec
end
Expand Down Expand Up @@ -200,6 +200,7 @@ class RSpec::Mocks::Proxy
def as_null_object; end
def build_expectation(method_name); end
def check_for_unexpected_arguments(expectation); end
def ensure_can_be_proxied!(object); end
def ensure_implemented(*_args); end
def find_almost_matching_expectation(method_name, *args); end
def find_almost_matching_stub(method_name, *args); end
Expand Down
2 changes: 1 addition & 1 deletion sorbet/rbi/gems/rspec-support.rbi
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
#
# https://github.com/sorbet/sorbet-typed/new/master?filename=lib/rspec-support/all/rspec-support.rbi
#
# rspec-support-3.10.0
# rspec-support-3.10.1

module RSpec
extend RSpec::Support::Warnings
Expand Down
Loading