class Parlour::TypeParser
Parses Ruby source to find Sorbet type signatures.
Attributes
@return [Parser::AST::Node] The AST which this type parser should use.
@return [RbiGenerator] The {RbiGenerator} to load the source into.
@return [Boolean] Whether to raise an error if a node of an unknown kind
is encountered.
Public Class Methods
Creates a new {TypeParser} from a source file and its filename.
@param [String] filename A filename. This does not need to be an actual
file; it merely identifies this source.
@param [String] source The Ruby source code. @return [TypeParser]
# File lib/parlour/type_parser.rb, line 108 def self.from_source(filename, source, generator: nil) buffer = Parser::Source::Buffer.new(filename) buffer.source = source # || special case handles parser returning nil on an empty file parsed = Parser::CurrentRuby.new.parse(buffer) || Parser::AST::Node.new(:body) TypeParser.new(parsed, generator: generator) end
Creates a new {TypeParser} from whitequark/parser AST.
@param [Parser::AST::Node] The AST. @param [Boolean] unknown_node_errors
Whether to raise an error if a node
of an unknown kind is encountered. If false, the node is simply ignored; if true, a parse error is raised. Setting this to true is likely to raise errors for lots of non-RBI Ruby code, but setting it to false could miss genuine typed objects if Parlour or your code contains a bug.
# File lib/parlour/type_parser.rb, line 95 def initialize(ast, unknown_node_errors: false, generator: nil) @ast = ast @unknown_node_errors = unknown_node_errors @generator = generator || DetachedRbiGenerator.new end
TODO doc
# File lib/parlour/type_parser.rb, line 728 def self.parse_single_type(str) i = TypeParser.from_source('(none)', str) i.parse_node_to_type(i.ast) end
Public Instance Methods
# File lib/parlour/type_parser.rb, line 134 def parse_all root = generator.root root.children.concat(parse_path_to_object(NodePath.new([]))) root end
Given a path to a method in the AST, finds the associated definition and parses them into methods. Usually this will return one method; the only exception currently is for attributes, where multiple can be declared in one call, e.g. +attr_reader :x, :y, :z+.
@param [NodePath] path The sig to parse. @param [Boolean] is_within_eigenclass Whether the method definition this sig is
associated with appears inside an eigenclass definition. If true, the returned method is made a class method. If the method definition is already a class method, an exception is thrown as the method will be a class method of the eigenclass, which Parlour can't represent.
@return [<RbiGenerator::Method>] The parsed methods.
# File lib/parlour/type_parser.rb, line 636 def parse_method_into_methods(path, is_within_eigenclass: false) # A :def node represents a definition like "def x; end" # A :defs node represents a definition like "def self.x; end" def_node = path.traverse(ast) case def_node.type when :def class_method = false def_names = [def_node.to_a[0].to_s] def_params = def_node.to_a[1].to_a kind = :def when :defs parse_err 'targeted definitions on a non-self target are not supported', def_node \ unless def_node.to_a[0].type == :self class_method = true def_names = [def_node.to_a[1].to_s] def_params = def_node.to_a[2].to_a kind = :def when :send target, method_name, *parameters = *def_node parse_err 'node after a sig must be a method definition', def_node \ unless [:attr_reader, :attr_writer, :attr_accessor].include?(method_name) \ || target != nil parse_err 'typed attribute should have at least one name', def_node if parameters&.length == 0 kind = :attr attr_direction = method_name.to_s.gsub('attr_', '').to_sym def_names = T.must(parameters).map { |param| param.to_a[0].to_s } class_method = false else parse_err 'node after a sig must be a method definition', def_node end if is_within_eigenclass parse_err 'cannot represent multiple levels of eigenclassing', def_node if class_method class_method = true end return_type = "T.untyped" if kind == :def parameters = def_params.map do |def_param| arg_name = def_param.to_a[0] # TODO: anonymous restarg full_name = arg_name.to_s full_name = "*#{arg_name}" if def_param.type == :restarg full_name = "**#{arg_name}" if def_param.type == :kwrestarg full_name = "#{arg_name}:" if def_param.type == :kwarg || def_param.type == :kwoptarg full_name = "&#{arg_name}" if def_param.type == :blockarg default = def_param.to_a[1] ? node_to_s(def_param.to_a[1]) : nil type = nil RbiGenerator::Parameter.new(full_name, type: type, default: default) end # There should only be one ever here, but future-proofing anyway def_names.map do |def_name| RbiGenerator::Method.new( generator, def_name, parameters, return_type, class_method: class_method ) end elsif kind == :attr case attr_direction when :reader, :accessor, :writer attr_type = return_type else raise "unknown attribute direction #{attr_direction}" end def_names.map do |def_name| RbiGenerator::Attribute.new( generator, def_name, attr_direction, attr_type, class_attribute: class_method ) end else raise "unknown definition kind #{kind}" end end
Given an AST node representing an RBI type (such as 'T::Array'), parses it into a generic type.
@param [Parser::AST::Node] node @return [Parlour::Types::Type]
# File lib/parlour/type_parser.rb, line 739 def parse_node_to_type(node) case node.type when :send target, message, *args = *node # Special case: is this a generic type instantiation? if message == :[] names = constant_names(target) known_single_element_collections = [:Array, :Set, :Range, :Enumerator, :Enumerable] if names.length == 2 && names[0] == :T && known_single_element_collections.include?(names[1]) parse_err "no type in T::#{names[1]}[...]", node if args.nil? || args.empty? parse_err "too many types in T::#{names[1]}[...]", node unless args.length == 1 return T.must(Types.const_get(T.must(names[1]))).new(parse_node_to_type(T.must(args.first))) elsif names.length == 2 && names == [:T, :Hash] parse_err "not enough types in T::Hash[...]", node if args.nil? || args.length < 2 parse_err "too many types in T::Hash[...]", node unless args.length == 2 return Types::Hash.new( parse_node_to_type(args[0]), parse_node_to_type(args[1]) ) else type = names.join('::') if args.nil? parse_err( "user defined generic '#{type}' requires at least one type parameter", node ) end return Types::Generic.new( type, args.map { |arg| parse_node_to_type(arg) } ) end end # Special case: is this a proc? # This parsing is pretty simplified, but you'd also have to be doing # something pretty cursed with procs to break this # This checks for (send (send (send (const nil :T) :proc) ...) ...) # That's the right amount of nesting for T.proc.params(...).returns(...) if node.to_a[0].type == :send && node.to_a[0].to_a[0].type == :send && node.to_a[0].to_a[0].to_a[1] == :proc && node.to_a[0].to_a[0].to_a[0].type == :const && node.to_a[0].to_a[0].to_a[0].to_a == [nil, :T] # yuck # Get parameters params_send = node.to_a[0] parse_err "expected 'params' to follow 'T.proc'", node unless params_send.to_a[1] == :params parse_err "expected 'params' to have kwargs", node unless params_send.to_a[2].type == :hash parameters = params_send.to_a[2].to_a.map do |pair| name, value = *pair parse_err "expected 'params' name to be symbol", node unless name.type == :sym name = name.to_a[0].to_s value = parse_node_to_type(value) RbiGenerator::Parameter.new(name, type: value) end # Get return value if node.to_a[1] == :void return_type = nil else _, call, *args = *node parse_err 'expected .returns or .void', node unless call == :returns parse_err 'no argument to .returns', node if args.nil? || args.empty? parse_err 'too many arguments to .returns', node unless args.length == 1 return_type = parse_node_to_type(T.must(args.first)) end return Types::Proc.new(parameters, return_type) end # The other options for a valid call are all "T.something" methods parse_err "unexpected call #{node_to_s(node).inspect} in type", node \ unless target.type == :const && target.to_a == [nil, :T] case message when :nilable parse_err 'no argument to T.nilable', node if args.nil? || args.empty? parse_err 'too many arguments to T.nilable', node unless args.length == 1 Types::Nilable.new(parse_node_to_type(T.must(args.first))) when :any Types::Union.new((args || []).map { |x| parse_node_to_type(T.must(x)) }) when :all Types::Intersection.new((args || []).map { |x| parse_node_to_type(T.must(x)) }) when :let # Not really allowed in a type signature, but handy for generalizing # constant types parse_err 'not enough argument to T.let', node if args.nil? || args.length < 2 parse_err 'too many arguments to T.nilable', node unless args.length == 2 parse_node_to_type(args[1]) when :type_parameter parse_err 'no argument to T.type_parameter', node if args.nil? || args.empty? parse_err 'too many arguments to T.type_parameter', node unless args.length == 1 parse_err 'expected T.type_parameter to be passed a symbol', node unless T.must(args.first).type == :sym Types::Raw.new(T.must(args.first.to_a[0].to_s)) when :class_of parse_err 'no argument to T.class_of', node if args.nil? || args.empty? parse_err 'too many arguments to T.class_of', node unless args.length == 1 Types::Class.new(parse_node_to_type(args[0])) when :untyped parse_err 'T.untyped does not accept arguments', node if !args.nil? && !args.empty? Types::Untyped.new else warning "unknown method T.#{message}, treating as untyped", node Types::Untyped.new end when :const # Special case: T::Boolean if constant_names(node) == [:T, :Boolean] return Types::Boolean.new end # Otherwise, just a plain old constant Types::Raw.new(constant_names(node).join('::')) when :array # Tuple Types::Tuple.new(node.to_a.map { |x| parse_node_to_type(T.must(x)) }) when :hash # Shape/record keys_to_types = node.to_a.map do |pair| key, value = *pair parse_err "all shape keys must be symbols", node unless key.type == :sym [key.to_a[0], parse_node_to_type(value)] end.to_h Types::Record.new(keys_to_types) else parse_err "unable to parse type #{node_to_s(node).inspect}", node end end
# File lib/parlour/type_parser.rb, line 152 def parse_path_to_object(path, is_within_eigenclass: false) node = path.traverse(ast) case node.type when :class parse_err 'cannot declare classes in an eigenclass', node if is_within_eigenclass name, superclass, body = *node final = body_has_modifier?(body, :final!) sealed = body_has_modifier?(body, :sealed!) abstract = body_has_modifier?(body, :abstract!) includes, extends = body ? body_includes_and_extends(body) : [[], []] # Create all classes, if we're given a definition like "class A::B" *parent_names, this_name = constant_names(name) target = T.let(nil, T.nilable(RbiGenerator::Namespace)) top_level = T.let(nil, T.nilable(RbiGenerator::Namespace)) parent_names.each do |n| new_obj = RbiGenerator::Namespace.new( generator, n.to_s, false, false, ) target.children << new_obj if target target = new_obj top_level ||= new_obj end if parent_names # Instantiate the correct kind of class if ['T::Struct', '::T::Struct'].include?(node_to_s(superclass)) # Find all of this struct's props and consts # The body is typically a `begin` element but when there's only # one node there's no wrapping block and instead it would directly # be the node. prop_nodes = body.nil? ? [] : (body.type == :begin ? body.to_a : [body]).select { |x| x.type == :send && [:prop, :const].include?(x.to_a[1]) } props = prop_nodes.map do |prop_node| _, prop_type, name_node, type_node, extras_hash_node = *prop_node # "const" is just "prop ..., immutable: true" extras_hash = extras_hash_node.to_a.map do |pair_node| key_node, value_node = *pair_node parse_err 'prop/const key must be a symbol', prop_node unless key_node.type == :sym key = key_node.to_a.first value = if key == :default T.must(node_to_s(value_node)) else case value_node.type when :true true when :false false when :sym value_node.to_a.first else T.must(node_to_s(value_node)) end end [key, value] end.to_h if prop_type == :const parse_err 'const cannot use immutable key', prop_node unless extras_hash[:immutable].nil? extras_hash[:immutable] = true end # Get prop/const name parse_err 'prop/const name must be a symbol or string', prop_node unless [:sym, :str].include?(name_node.type) name = name_node.to_a.first.to_s RbiGenerator::StructProp.new( name, T.must(node_to_s(type_node)), **T.unsafe(extras_hash) ) end final_obj = RbiGenerator::StructClassNamespace.new( generator, this_name.to_s, final, sealed, props, abstract, ) elsif ['T::Enum', '::T::Enum'].include?(node_to_s(superclass)) # Look for (block (send nil :enums) ...) structure enums_node = body.nil? ? nil : (body.type == :begin ? body.to_a : [body]).find { |x| x.type == :block && x.to_a[0].type == :send && x.to_a[0].to_a[1] == :enums } # Find the constant assigments within this block constant_nodes = enums_node.to_a[2].to_a # Convert this to an array to enums as EnumClassNamespace expects enums = constant_nodes.map do |constant_node| _, name, new_node = *constant_node serialize_value = node_to_s(new_node.to_a[2]) serialize_value ? [name.to_s, serialize_value] : name.to_s end final_obj = RbiGenerator::EnumClassNamespace.new( generator, this_name.to_s, final, sealed, enums, abstract, ) else final_obj = RbiGenerator::ClassNamespace.new( generator, this_name.to_s, final, sealed, node_to_s(superclass), abstract, ) end final_obj.children.concat(parse_path_to_object(path.child(2))) if body final_obj.create_includes(includes) final_obj.create_extends(extends) if target target.children << final_obj [T.must(top_level)] else [final_obj] end when :module parse_err 'cannot declare modules in an eigenclass', node if is_within_eigenclass name, body = *node abstract = body_has_modifier?(body, :abstract!) final = body_has_modifier?(body, :final!) sealed = body_has_modifier?(body, :sealed!) interface = body_has_modifier?(body, :interface!) includes, extends = body ? body_includes_and_extends(body) : [[], []] # Create all modules, if we're given a definition like "module A::B" *parent_names, this_name = constant_names(name) target = T.let(nil, T.nilable(RbiGenerator::Namespace)) top_level = T.let(nil, T.nilable(RbiGenerator::Namespace)) parent_names.each do |n| new_obj = RbiGenerator::Namespace.new( generator, n.to_s, false, false, ) target.children << new_obj if target target = new_obj top_level ||= new_obj end if parent_names final_obj = RbiGenerator::ModuleNamespace.new( generator, this_name.to_s, final, sealed, interface, abstract, ) do |m| m.children.concat(parse_path_to_object(path.child(1))) if body m.create_includes(includes) m.create_extends(extends) end if target target.children << final_obj [T.must(top_level)] else [final_obj] end when :send, :block if sig_node?(node) parse_sig_into_methods(path, is_within_eigenclass: is_within_eigenclass) elsif node.type == :send && [:attr_reader, :attr_writer, :attr_accessor].include?(node.to_a[1]) && !previous_sibling_sig_node?(path) parse_method_into_methods(path, is_within_eigenclass: is_within_eigenclass) else [] end when :def, :defs if previous_sibling_sig_node?(path) [] else parse_method_into_methods(path, is_within_eigenclass: is_within_eigenclass) end when :sclass parse_err 'cannot access eigen of non-self object', node unless node.to_a[0].type == :self parse_path_to_object(path.child(1), is_within_eigenclass: true) when :begin # Just map over all the things node.to_a.length.times.map do |c| parse_path_to_object(path.child(c), is_within_eigenclass: is_within_eigenclass) end.flatten when :casgn _, name, body = *node # Determine whether this is a constant or a type alias # A type alias looks like: # (block (send (const nil :T) :type_alias) (args) (type_to_alias)) if body.type == :block && body.to_a[0].type == :send && body.to_a[0].to_a[0].type == :const && body.to_a[0].to_a[0].to_a == [nil, :T] && body.to_a[0].to_a[1] == :type_alias [Parlour::RbiGenerator::TypeAlias.new( generator, name: T.must(name).to_s, type: T.must(node_to_s(body.to_a[2])), )] else [Parlour::RbiGenerator::Constant.new( generator, name: T.must(name).to_s, value: T.must(node_to_s(body)), )] end else if unknown_node_errors parse_err "don't understand node type #{node.type}", node else [] end end end
Given a path to a sig in the AST, finds the associated definition and parses them into methods. This will raise an exception if the sig is invalid. Usually this will return one method; the only exception currently is for attributes, where multiple can be declared in one call, e.g. +attr_reader :x, :y, :z+.
@param [NodePath] path The sig to parse. @param [Boolean] is_within_eigenclass Whether the method definition this sig is
associated with appears inside an eigenclass definition. If true, the returned method is made a class method. If the method definition is already a class method, an exception is thrown as the method will be a class method of the eigenclass, which Parlour can't represent.
@return [<RbiGenerator::Method>] The parsed methods.
# File lib/parlour/type_parser.rb, line 487 def parse_sig_into_methods(path, is_within_eigenclass: false) sig_block_node = path.traverse(ast) # A :def node represents a definition like "def x; end" # A :defs node represents a definition like "def self.x; end" def_node = path.sibling(1).traverse(ast) case def_node.type when :def class_method = false def_names = [def_node.to_a[0].to_s] def_params = def_node.to_a[1].to_a kind = :def when :defs parse_err 'targeted definitions on a non-self target are not supported', def_node \ unless def_node.to_a[0].type == :self class_method = true def_names = [def_node.to_a[1].to_s] def_params = def_node.to_a[2].to_a kind = :def when :send target, method_name, *parameters = *def_node parse_err 'node after a sig must be a method definition', def_node \ unless [:attr_reader, :attr_writer, :attr_accessor].include?(method_name) \ || target != nil parse_err 'typed attribute should have at least one name', def_node if parameters&.length == 0 kind = :attr attr_direction = method_name.to_s.gsub('attr_', '').to_sym def_names = T.must(parameters).map { |param| param.to_a[0].to_s } class_method = false else parse_err 'node after a sig must be a method definition', def_node end if is_within_eigenclass parse_err 'cannot represent multiple levels of eigenclassing', def_node if class_method class_method = true end this_sig = parse_sig_into_sig(path) params = this_sig.params return_type = this_sig.return_type if kind == :def # Sorbet allows a trailing blockarg that's not in the sig if params && def_params.length == params.length + 1 && def_params[-1].type == :blockarg def_params = def_params[0...-1] end parse_err 'mismatching number of arguments in sig and def', sig_block_node \ if params && def_params.length != params.length # sig_args will look like: # [(pair (sym :x) <type>), (pair (sym :y) <type>), ...] # def_params will look like: # [(arg :x), (arg :y), ...] parameters = params \ ? zip_by(params, ->x{ x.to_a[0].to_a[0] }, def_params, ->x{ x.to_a[0] }) .map do |sig_arg, def_param| arg_name = def_param.to_a[0] # TODO: anonymous restarg full_name = arg_name.to_s full_name = "*#{arg_name}" if def_param.type == :restarg full_name = "**#{arg_name}" if def_param.type == :kwrestarg full_name = "#{arg_name}:" if def_param.type == :kwarg || def_param.type == :kwoptarg full_name = "&#{arg_name}" if def_param.type == :blockarg default = def_param.to_a[1] ? node_to_s(def_param.to_a[1]) : nil type = node_to_s(sig_arg.to_a[1]) RbiGenerator::Parameter.new(full_name, type: type, default: default) end : [] # There should only be one ever here, but future-proofing anyway def_names.map do |def_name| RbiGenerator::Method.new( generator, def_name, parameters, return_type, type_parameters: this_sig.type_parameters, override: this_sig.override, overridable: this_sig.overridable, abstract: this_sig.abstract, final: this_sig.final, class_method: class_method ) end elsif kind == :attr case attr_direction when :reader, :accessor parse_err "attr_#{attr_direction} sig should have no parameters", sig_block_node \ if params && params.length > 0 parse_err "attr_#{attr_direction} sig should have non-void return", sig_block_node \ unless return_type attr_type = return_type when :writer # These are special and can only have one name raise 'typed attr_writer can only have one name' if def_names.length > 1 def_name = def_names[0] parse_err "attr_writer sig should take one argument with the property's name", sig_block_node \ if !params || params.length != 1 || params[0].to_a[0].to_a[0].to_s != def_name parse_err "attr_writer sig should have non-void return", sig_block_node \ if return_type.nil? attr_type = T.must(node_to_s(params[0].to_a[1])) else raise "unknown attribute direction #{attr_direction}" end def_names.map do |def_name| RbiGenerator::Attribute.new( generator, def_name, attr_direction, attr_type, class_attribute: class_method ) end else raise "unknown definition kind #{kind}" end end
Given a path to a sig in the AST, parses that sig into an intermediate sig object. This will raise an exception if the sig is invalid. This is intended to be called by {#parse_sig_into_methods}, and shouldn't be called manually unless you're doing something hacky.
@param [NodePath] path The sig to parse. @return [IntermediateSig] The parsed sig.
# File lib/parlour/type_parser.rb, line 409 def parse_sig_into_sig(path) sig_block_node = path.traverse(ast) # A sig's AST uses lots of nested nodes due to a deep call chain, so let's # flatten it out to make it easier to work with sig_chain = [] current_sig_chain_node = sig_block_node.to_a[2] while current_sig_chain_node name = current_sig_chain_node.to_a[1] arguments = current_sig_chain_node.to_a[2..-1] sig_chain << [name, arguments] current_sig_chain_node = current_sig_chain_node.to_a[0] end # Get basic boolean flags override = !!sig_chain.find { |(n, a)| n == :override && a.empty? } overridable = !!sig_chain.find { |(n, a)| n == :overridable && a.empty? } abstract = !!sig_chain.find { |(n, a)| n == :abstract && a.empty? } # Determine whether this method is final (i.e. sig(:final)) _, _, *sig_arguments = *sig_block_node.to_a[0] final = sig_arguments.any? { |a| a.type == :sym && a.to_a[0] == :final } # Find the return type by looking for a "returns" call return_type = sig_chain .find { |(n, _)| n == :returns } &.then do |(_, a)| parse_err 'wrong number of arguments in "returns" for sig', sig_block_node if a.length != 1 node_to_s(a[0]) end # Find the arguments specified in the "params" call in the sig sig_args = sig_chain .find { |(n, _)| n == :params } &.then do |(_, a)| parse_err 'wrong number of arguments in "params" for sig', sig_block_node if a.length != 1 arg = a[0] parse_err 'argument to "params" should be a hash', arg unless arg.type == :hash arg.to_a end # Find type parameters if they were used type_parameters = sig_chain .find { |(n, _)| n == :type_parameters } &.then do |(_, a)| a.map do |arg| parse_err 'type parameter must be a symbol', arg if arg.type != :sym arg.to_a[0] end end IntermediateSig.new( type_parameters: type_parameters, overridable: overridable, override: override, abstract: abstract, final: final, params: sig_args, return_type: return_type ) end
Protected Instance Methods
Given an AST node and a symbol, determines if that node is a call (or a body containing a call at the top level) to the method represented by the symbol, without any arguments or a block.
This is designed to be used to determine if a namespace body uses a Sorbet modifier such as “abstract!”.
@param [Parser::AST::Node, nil] node The AST node to search in. @param [Symbol] modifier The method name to search for. @return [T::Boolean] True if the call is found, or false otherwise.
# File lib/parlour/type_parser.rb, line 956 def body_has_modifier?(node, modifier) return false unless node (node.type == :send && node.to_a == [nil, modifier]) || (node.type == :begin && node.to_a.any? { |c| c.type == :send && c.to_a == [nil, modifier] }) end
Given an AST node representing the body of a class or module, returns two arrays of the includes and extends contained within the body.
@param [Parser::AST::Node] node The body of the namespace. @return [(Array<String>, Array<String>)] An array of the includes and an
array of the extends.
# File lib/parlour/type_parser.rb, line 971 def body_includes_and_extends(node) result = [[], []] nodes_to_search = node.type == :begin ? node.to_a : [node] nodes_to_search.each do |this_node| next unless this_node.type == :send target, name, *args = *this_node next unless target.nil? && args.length == 1 if name == :include result[0] << node_to_s(args.first) elsif name == :extend result[1] << node_to_s(args.first) end end result end
Given a node representing a simple chain of constants (such as A or A::B::C), converts that node into an array of the constant names which are accessed. For example, A::B::C would become [:A, :B, :C].
@param [Parser::AST::Node, nil] node The node to convert. This must
consist only of nested (:const) nodes.
@return [Array<Symbol>] The chain of constant names.
# File lib/parlour/type_parser.rb, line 896 def constant_names(node) node ? constant_names(node.to_a[0]) + [node.to_a[1]] : [] end
Given an AST node, returns the source code from which it was constructed. If the given AST node is nil, this returns nil.
@param [Parser::AST::Node, nil] node The AST node, or nil. @return [String] The source code string it represents, or nil.
# File lib/parlour/type_parser.rb, line 938 def node_to_s(node) return nil unless node exp = node.loc.expression exp.source_buffer.source[exp.begin_pos...exp.end_pos] end
Raises a parse error on a node. @param [String] desc A description of the error. @param [Parser::AST::Node, NodePath] A node, passed as either a path or a
raw parser node.
# File lib/parlour/type_parser.rb, line 995 def parse_err(desc, node) node = node.traverse(ast) if node.is_a?(NodePath) range = node.loc.expression buffer = range.source_buffer raise ParseError.new(buffer, range), desc end
Given a path, returns a boolean indicating whether the previous sibling represents a call to “sig” with a block.
@param [NodePath] path The path to the namespace definition. @return [Boolean] True if that node represents a “sig” call, false
otherwise.
# File lib/parlour/type_parser.rb, line 921 def previous_sibling_sig_node?(path) previous_sibling = path.sibling(-1) previous_node = previous_sibling.traverse(ast) sig_node?(previous_node) rescue IndexError, ArgumentError, TypeError # `sibling` call could raise IndexError or ArgumentError if reaching into negative indices # `traverse` call could raise TypeError if path doesn't return Parser::AST::Node false end
Given a node, returns a boolean indicating whether that node represents a a call to “sig” with a block. No further semantic checking, such as whether it preceeds a method call, is done.
@param [Parser::AST::Node] node The node to check. @return [Boolean] True if that node represents a “sig” call, false
otherwise.
# File lib/parlour/type_parser.rb, line 908 def sig_node?(node) node.type == :block && node.to_a[0].type == :send && node.to_a[0].to_a[1] == :sig end
# File lib/parlour/type_parser.rb, line 878 def warning(msg, node) return if $VERBOSE.nil? print Rainbow("Parlour warning: ").yellow.dark.bold print Rainbow("Type generalization: ").magenta.bright.bold puts msg print Rainbow(" └ at code: ").blue.bright.bold puts node_to_s(node) end
Given two arrays and functions to get a key for each item in the two arrays, joins the two arrays into one array of pairs by that key.
The arrays should both be the same length, and the key functions should never return duplicate keys for two different items.
@param [Array<A>] a The first array. @param [A -> Any] fa A function to obtain a key for any element in the
first array.
@param [Array<B>] b The second array. @param [B -> Any] fb A function to obtain a key for any element in the
second array.
@return [Array<(A, B)>] An array of pairs, where the left of the pair is
an element from A and the right is the element from B with the corresponding key.
# File lib/parlour/type_parser.rb, line 1028 def zip_by(a, fa, b, fb) raise ArgumentError, "arrays are not the same length" if a.length != b.length a.map do |a_item| a_key = fa.(a_item) b_items = b.select { |b_item| fb.(b_item) == a_key } raise "multiple items for key #{a_key}" if b_items.length > 1 raise "no item in second list corresponding to key #{a_key}" if b_items.length == 0 [a_item, T.must(b_items[0])] end end