class Solargraph::TypeChecker
A static analysis tool for validating data types.
def arg_to_duck arg, par
def arg_to_duck arg, par return false unless par.duck_type? meths = api_map.get_complex_type_methods(arg).map(&:name) par.each do |quack| return false unless meths.include?(quack.to_s[1..-1]) end true end
def check_hash_params arg, params
def check_hash_params arg, params result = [] keys = arg.children.map do |child| child.children[0].children[0].to_s end keys.each do |key| param = params[key] if param # @todo typecheck else # @todo This error might not be valid. If there's a splat in the # method parameters, should the type checker let it pass? result.push Problem.new(nil, "Keyword argument #{key} does not have a @param tag") end end result end
def check_param_tags pin
-
(Array-)
Parameters:
-
pin(Pin::BaseMethod) --
def check_param_tags pin return [] if ParamDef.from(pin).map(&:type).include?(:kwrestarg) result = [] pin.docstring.tags(:param).each do |par| next if pin.parameter_names.include?(par.name) result.push Problem.new(pin.location, "#{pin.name} has unknown @param #{par.name}") end result end
def check_return_type pin
-
(Array-)
Parameters:
-
pin(Pin::BaseMethod) --
def check_return_type pin tagged = pin.typify(api_map) if tagged.undefined? if pin.return_type.undefined? probed = pin.probe(api_map) return [Problem.new(pin.location, "#{pin.name} has undefined @return type", pin: pin, suggestion: probed.to_s)] else return [Problem.new(pin.location, "#{pin.name} has unresolved @return type #{pin.return_type}")] end end [] end
def check_send_args node, skip_send = false
-
(Array-)
Parameters:
-
skip_send(Boolean) -- -
node(Parser::AST::Node) --
def check_send_args node, skip_send = false result = [] if node.type == :send smap = api_map.source_map(filename) locals = smap.locals_at(Solargraph::Location.new(filename, Solargraph::Range.from_node(node))) block = smap.locate_block_pin(node.loc.line, node.loc.column) chain = Solargraph::Source::NodeChainer.chain(node, filename) pins = chain.define(api_map, block, locals).select { |pin| pin.is_a?(Pin::BaseMethod ) } if pins.empty? if !more_signature?(node) base = chain.base.define(api_map, block, locals).first if base.nil? || report_location?(base.location) result.push Problem.new(Solargraph::Location.new(filename, Solargraph::Range.from_node(node)), "Unresolved method signature #{chain.links.map(&:word).join('.')}") end end else pin = pins.first ptypes = ParamDef.from(pin) params = param_tags_from(pin) cursor = 0 curtype = nil node.children[2..-1].each_with_index do |arg, index| if pin.is_a?(Pin::Attribute) curtype = ParamDef.new('value', :arg) else curtype = ptypes[cursor] if curtype.nil? || curtype == :arg end if curtype.nil? if pin.parameters[index].nil? result.push Problem.new(Solargraph::Location.new(filename, Solargraph::Range.from_node(node)), "Not enough arguments sent to #{pin.path}") break end else # @todo This should also detect when the last parameter is a hash if curtype.type == :kwrestarg if arg.type != :hash result.push Problem.new(Solargraph::Location.new(filename, Solargraph::Range.from_node(node)), "Wrong parameter type for #{pin.path}: expected hash or keyword") else result.concat check_hash_params arg, params end # @todo Break here? Not sure about that break end break if curtype.type == :restarg if arg.is_a?(Parser::AST::Node) && arg.type == :hash arg.children.each do |pair| sym = pair.children[0].children[0].to_s partype = params[sym] if partype chain = Solargraph::Source::NodeChainer.chain(pair.children[1], filename) argtype = chain.infer(api_map, block, locals) if argtype.tag != partype.tag && !api_map.super_and_sub?(partype.tag.to_s, argtype.tag.to_s) result.push Problem.new(Solargraph::Location.new(filename, Solargraph::Range.from_node(node)), "Wrong parameter type for #{pin.path}: #{pin.parameter_names[index]} expected #{partype.tag}, received #{argtype.tag}") end end end elsif arg.is_a?(Parser::AST::Node) && arg.type == :splat result.push Problem.new(Solargraph::Location.new(filename, Solargraph::Range.from_node(node)), "Can't handle splat in #{pin.parameter_names[index]} #{pin.path}") break if curtype != :arg && ptypes.map(&:type).include?(:restarg) else if pin.is_a?(Pin::Attribute) partype = pin.return_type else partype = params[pin.parameter_names[index]] end if partype arg = chain.links.last.arguments[index] if arg.nil? result.push Problem.new(Solargraph::Location.new(filename, Solargraph::Range.from_node(node)), "Wrong number of arguments to #{pin.path}") else argtype = arg.infer(api_map, block, locals) if !arg_to_duck(argtype, partype) if argtype.tag != partype.tag && !api_map.super_and_sub?(partype.tag.to_s, argtype.tag.to_s) result.push Problem.new(Solargraph::Location.new(filename, Solargraph::Range.from_node(node)), "Wrong parameter type for #{pin.path}: #{pin.parameter_names[index]} expected #{partype.tag}, received #{argtype.tag}") end end end end end end cursor += 1 if curtype == :arg end end end node.children.each do |child| next unless child.is_a?(Parser::AST::Node) next if child.type == :send && skip_send result.concat check_send_args(child) end result end
def confirm_return_type pin
-
(Array-)
Parameters:
-
pin(Solargraph::Pin::Base) --
def confirm_return_type pin tagged = pin.typify(api_map).self_to(pin.namespace) return [] if tagged.void? || tagged.undefined? || pin.is_a?(Pin::Attribute) probed = pin.probe(api_map) return [] if probed.undefined? if tagged.to_s != probed.to_s if probed.name == 'Array' && probed.subtypes.empty? return [] if tagged.name == 'Array' end if probed.name == 'Hash' && probed.value_types.empty? return [] if tagged.name == 'Hash' end all = true probed.each do |pt| tagged.each do |tt| if pt.name == tt.name && !api_map.super_and_sub?(tt.namespace, pt.namespace) && !tagged.map(&:namespace).include?(pt.namespace) all = false break elsif pt.name == tt.name && ['Array', 'Class', 'Module'].include?(pt.name) if !(tt.subtypes.any? { |ttx| pt.subtypes.any? { |ptx| api_map.super_and_sub?(ttx.to_s, ptx.to_s) } }) all = false break end elsif pt.name == tt.name && pt.name == 'Hash' if !(tt.key_types.empty? && !pt.key_types.empty?) && !(tt.key_types.any? { |ttx| pt.key_types.any? { |ptx| api_map.super_and_sub?(ttx.to_s, ptx.to_s) } }) if !(tt.value_types.empty? && !pt.value_types.empty?) && !(tt.value_types.any? { |ttx| pt.value_types.any? { |ptx| api_map.super_and_sub?(ttx.to_s, ptx.to_s) } }) all = false break end end elsif pt.name != tt.name && !api_map.super_and_sub?(tt.to_s, pt.to_s) && !tagged.map(&:to_s).include?(pt.to_s) all = false break end end end return [] if all return [Problem.new(pin.location, "@return type `#{tagged.to_s}` does not match inferred type `#{probed.to_s}`", pin: pin, suggestion: probed.to_s)] end [] end
def initialize filename, api_map: nil
-
api_map(ApiMap) -- -
filename(String) --
def initialize filename, api_map: nil @filename = filename # @todo Smarter directory resolution @api_map = api_map || Solargraph::ApiMap.load(File.dirname(filename)) end
def load filename
-
(self)-
Parameters:
-
filename(String) --
def load filename source = Solargraph::Source.load(filename) api_map = Solargraph::ApiMap.new api_map.map(source) new(filename, api_map: api_map) end
def load_string code, filename = nil
-
(self)-
Parameters:
-
filename(String, nil) -- -
code(String) --
def load_string code, filename = nil source = Solargraph::Source.load_string(code, filename) api_map = Solargraph::ApiMap.new api_map.map(source) new(filename, api_map: api_map) end
def more_signature? node
def more_signature? node node.children.any? do |child| child.is_a?(Parser::AST::Node) && ( child.type == :send || (child.type == :block && more_signature?(child)) ) end end
def param_tags_from pin
-
(Hash)-
Parameters:
-
pin(Pin::Base) --
def param_tags_from pin # @todo Look for see references # and dig through all the pins return {} if pin.nil? tags = pin.docstring.tags(:param) result = {} tags.each do |tag| result[tag.name] = ComplexType::UNDEFINED result[tag.name] = ComplexType.try_parse(*tag.types).qualify(api_map, pin.context.namespace) end result end
def param_type_problems
-
(Array-)
def param_type_problems result = [] smap = api_map.source_map(filename) smap.pins.select { |pin| pin.is_a?(Pin::Method) }.each do |pin| if pin.parameters.empty? pin.docstring.tags(:param).each do |tag| result.push Problem.new(pin.location, "#{pin.name} has unknown @param #{tag.name}", pin: pin) end end end smap.locals.select { |pin| pin.is_a?(Solargraph::Pin::Parameter) }.each do |par| next unless par.closure.is_a?(Solargraph::Pin::Method) result.concat check_param_tags(par.closure) type = par.typify(api_map) pdefs = ParamDef.from(par.closure) if type.undefined? if par.return_type.undefined? && !pdefs.any? { |pd| pd.name == par.name && [:restarg, :kwrestarg, :blockarg].include?(pd.type) } result.push Problem.new(par.location, "#{par.closure.name} has undefined @param type for #{par.name}") elsif !pdefs.any? { |pd| [:restarg, :kwrestarg, :blockarg].include?(pd.type) } result.push Problem.new(par.location, "#{par.closure.name} has unresolved @param type for #{par.name}") end end end result end
def report_location? location
-
location(Location, nil) --
def report_location? location return false if location.nil? filename == location.filename || api_map.bundled?(location.filename) end
def return_type_problems
-
(Array-)
def return_type_problems result = [] smap = api_map.source_map(filename) pins = smap.pins.select { |pin| pin.is_a?(Solargraph::Pin::BaseMethod) } pins.each { |pin| result.concat check_return_type(pin) } result end
def strict_type_problems
-
(Array-)
def strict_type_problems result = [] smap = api_map.source_map(filename) smap.pins.select { |pin| pin.is_a?(Pin::BaseMethod) }.each do |pin| result.concat confirm_return_type(pin) end return result if smap.source.node.nil? result.concat check_send_args smap.source.node result end