Skip to content
This repository has been archived by the owner on May 19, 2024. It is now read-only.

Handle hashes and rescue blocks properly #8

Merged
merged 7 commits into from
Nov 24, 2017
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
11 changes: 5 additions & 6 deletions lib/analist/annotations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,10 @@ def initialize(receiver_type, args_types, return_type, hint: nil)
def ==(other)
return false unless other.is_a?(self.class)

if args_types == [Analist::AnyArgs]
return [receiver_type, return_type] == [other.receiver_type, other.return_type]
attrs = %i[receiver_type args_types return_type]
attrs.all? do |attr|
send(attr) == other.send(attr)
end

[receiver_type, args_types, return_type] ==
[other.receiver_type, other.args_types, other.return_type]
end

def to_s
Expand Down Expand Up @@ -82,10 +80,11 @@ def send_annotations # rubocop:disable Metrics/MethodLength

def primitive_annotations
{
const: ->(node) { Annotation.new(nil, [], type: node.children.last, on: :collection) },
dstr: ->(_) { Annotation.new(nil, [], String) },
int: ->(_) { Annotation.new(nil, [], Integer) },
str: ->(_) { Annotation.new(nil, [], String) },
const: ->(node) { Annotation.new(nil, [], type: node.children.last, on: :collection) }
sym: ->(_) { Annotation.new(nil, [], Symbol) }
}
end
end
Expand Down
45 changes: 20 additions & 25 deletions lib/analist/annotator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,61 +17,50 @@ def annotate(node, resources = {}) # rubocop:disable Metrics/CyclomaticComplexit
return node unless node.respond_to?(:type)

case node.type
when :args
annotate_args(node, resources)
when :array
annotate_array(node, resources)
when :begin
annotate_begin(node, resources)
when :block
annotate_block(node, resources)
when :class
annotate_class(node, resources)
when :module
annotate_module(node, resources)
when :def
annotate_def(node, resources)
when :defs
annotate_defs(node, resources)
when :if
annotate_if(node, resources)
when :dstr
annotate_dstr(node, resources)
when :module
annotate_module(node, resources)
when :send
annotate_send(node, resources)
when :lvasgn
annotate_local_variable_assignment(node, resources)
when :lvar
annotate_local_variable(node, resources)
when :int, :str, :const, :dstr
when :int, :str, :sym, :const
annotate_primitive(node)
else
if ENV['ANALIST_DEBUG']
raise NotImplementedError, "Node type `#{node.type}` cannot be annotated"
end
AnnotatedNode.new(node, node.children, UNKNOWN_ANNOTATION_TYPE)
annotate_children(node, resources)
end
end

def annotate_args(node, resources)
annotate_begin(node, resources)
end

def annotate_array(node, resources)
AnnotatedNode.new(node, node.children.map { |n| annotate(n, resources) },
Analist::Annotation.new(nil, [], Array))
end

def annotate_begin(node, resources)
AnnotatedNode.new(node, node.children.map { |n| annotate(n, resources) },
Analist::Annotation.new(nil, [], Analist::AnnotationTypeUnknown))
end

def annotate_block(node, resources, name = nil)
resources[:symbol_table].enter_scope(name)
block = annotate_begin(node, resources)
block = annotate_children(node, resources)
resources[:symbol_table].exit_scope
block
end

def annotate_children(node, resources)
AnnotatedNode.new(node, node.children.map { |n| annotate(n, resources) },
Analist::Annotation.new(nil, [], Analist::AnnotationTypeUnknown))
end

def annotate_class(node, resources)
annotate_block(node, resources, node.children.first.children[1])
end
Expand All @@ -84,14 +73,20 @@ def annotate_defs(node, resources)
annotate_block(node, resources, :"self.#{node.children[1]}")
end

def annotate_if(node, resources)
annotate_block(node, resources)
def annotate_dstr(node, resources)
AnnotatedNode.new(node, node.children.map { |n| annotate(n, resources) },
Analist::Annotation.new(nil, [], String))
end

def annotate_local_variable_assignment(node, resources)
annotated_children = node.children.map { |n| annotate(n, resources) }
variable, value = annotated_children

unless value
return AnnotatedNode.new(node, annotated_children,
Analist::Annotation.new(nil, [], AnnotationTypeUnknown))
end

resources[:symbol_table].store(variable, value.annotation)

AnnotatedNode.new(node, annotated_children,
Expand Down
85 changes: 27 additions & 58 deletions lib/analist/checker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,88 +7,39 @@ module Analist
module Checker
module_function

def check(node) # rubocop:disable Metrics/CyclomaticComplexity
return [] unless node.respond_to?(:type)
def check(node)
return [] unless node.is_a?(Analist::AnnotatedNode)

case node.type
when :begin
check_begin(node)
when :block
check_block(node)
when :class
check_class(node)
when :def
check_def(node)
when :defs
check_defs(node)
when :if
check_if(node)
when :module
check_module(node)
when :send
check_send(node)
when :array
check_array(node)
when :int, :str, :const
return
else
if ENV['ANALIST_DEBUG']
raise NotImplementedError, "Node type `#{node.type}` cannot be checked"
end
[]
check_children(node)
end
end

def check_array(node)
check_children(node)
end

def check_begin(node)
check_children(node)
end

def check_block(node)
check_children(node)
end

def check_children(node)
node.children.flat_map { |n| check(n) }.compact
end

def check_class(node)
check_children(node)
end

def check_def(node)
check_children(node)
end

def check_defs(node)
check_children(node)
end

def check_if(node)
check_children(node)
end

def check_module(node)
check_children(node)
end

def check_send(node)
return [Analist::DecorateWarning.new(node)] if node.annotation.hint ==
Analist::ResolveLookup::Hint::Decorate
return [] if node.annotation.return_type[:type] == Analist::AnnotationTypeUnknown

receiver, _method_name, *args = node.children
expected_annotation = node.annotation
expected_args = node.annotation.args_types.flat_map do |a|
a.respond_to?(:annotation) ? a.annotation.return_type[:type] : a
end
expected_annotation = Analist::Annotation.new(node.annotation.receiver_type,
expected_args, node.annotation.return_type)

actual_annotation = Analist::Annotation.new(
receiver&.annotation&.return_type, args.flat_map { |a| a.annotation.return_type[:type] },
node.annotation.return_type
)

if expected_annotation != actual_annotation
if significant_difference?(expected_annotation, actual_annotation)
error = if expected_annotation.args_types.count != actual_annotation.args_types.count
Analist::ArgumentError.new(node, expected_number_of_args:
expected_annotation.args_types.count,
Expand All @@ -103,5 +54,23 @@ def check_send(node)

[error, check(receiver), args.flat_map { |a| check(a) }.compact].compact.flatten
end

# rubocop:disable Metrics/LineLength
def significant_difference?(annotation, other_annotation) # rubocop:disable Metrics/CyclomaticComplexity
attrs = %i[receiver_type args_types return_type]
attrs.delete(:args_types) if annotation.args_types.any? { |t| t == Analist::AnnotationTypeUnknown } ||
annotation.args_types == [Analist::AnyArgs] ||
other_annotation.args_types.any? { |t| t == Analist::AnnotationTypeUnknown } ||
other_annotation.args_types == [Analist::AnyArgs]
%i[receiver_type return_type].each do |field|
attrs.delete(field) if annotation.send(field)[:type] == Analist::AnnotationTypeUnknown ||
other_annotation.send(field)[:type] == Analist::AnnotationTypeUnknown
end

attrs.any? do |attr|
annotation.send(attr) != other_annotation.send(attr)
end
end
# rubocop:enable Metrics/LineLength
end
end
8 changes: 7 additions & 1 deletion lib/analist/resolve_lookup.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ def return_type
return unless @headers
return unless last_statement

Analist::Annotator.annotate(last_statement, @resources).annotation.return_type
lookup_chain = @resources.fetch(:lookup_chain, [])
return if lookup_chain.include?(@method)

@resources[:lookup_chain] = lookup_chain + [@method]
return_type = Analist::Annotator.annotate(last_statement, @resources).annotation.return_type
@resources.delete(:lookup_chain)
return_type
end

def klass_method?
Expand Down
Loading