diff --git a/decode.gemspec b/decode.gemspec index 8f2edff..3478319 100644 --- a/decode.gemspec +++ b/decode.gemspec @@ -25,5 +25,5 @@ Gem::Specification.new do |spec| spec.required_ruby_version = ">= 3.2" - spec.add_dependency "parser" + spec.add_dependency "prism" end diff --git a/lib/decode/definition.rb b/lib/decode/definition.rb index 872ca62..32c537d 100644 --- a/lib/decode/definition.rb +++ b/lib/decode/definition.rb @@ -9,20 +9,24 @@ module Decode # A symbol with attached documentation. class Definition # Initialize the symbol. - # @parameter name [Symbol] The name of the definition. + # @parameter path [Symbol | Array(Symbol)] The path of the definition relatve to the parent. # @parameter parent [Symbol] The parent lexical scope. # @parameter language [Language] The language in which the symbol is defined in. # @parameter comments [Array(String)] The comments associated with the definition. - def initialize(name, parent: nil, language: parent.language, comments: nil) - @name = name + # @parameter source [Source] The source file containing this definition. + def initialize(path, parent: nil, language: parent&.language, comments: nil, visibility: :public, source: parent&.source) + @path = Array(path).map(&:to_sym) @parent = parent @language = language + @source = source @comments = comments + @visibility = visibility - @path = nil + @full_path = nil @qualified_name = nil + @nested_name = nil end def inspect @@ -34,7 +38,27 @@ def inspect # The symbol name. # e.g. `:Decode`. # @attribute [Symbol] - attr :name + def name + @path.last + end + + # The path to the definition, relative to the parent. + # @attribute [Array(Symbol)] + attr :path + + # The full path to the definition. + def full_path + @full_path ||= begin + if @parent + @parent.full_path + @path + else + @path + end + end + end + + # @deprecated Use {#path} instead. + alias lexical_path path # The parent definition, defining lexical scope. # @attribute [Definition | Nil] @@ -44,6 +68,10 @@ def inspect # @attribute [Language::Generic] attr :language + # The source file containing this definition. + # @attribute [Source | Nil] + attr :source + # The comment lines which directly preceeded the definition. # @attribute [Array(String)] attr :comments @@ -62,17 +90,17 @@ def public? def qualified_name @qualified_name ||= begin if @parent - @parent.qualified_name + self.nested_name + [@parent.qualified_name, self.nested_name].join("::") else - @name.to_s + self.nested_name end end end - # The name of this definition plus the nesting prefix. + # The name relative to the parent. # @returns [String] def nested_name - "::#{@name}" + @nested_name ||= "#{@path.join("::")}" end # Does the definition name match the specified prefix? @@ -86,28 +114,6 @@ def convert(kind) raise ArgumentError, "Unable to convert #{self} into #{kind}!" end - # The lexical scope as an array of names. - # e.g. `[:Decode, :Definition]` - # @returns [Array] - def path - if @path - # Cached version: - @path - elsif @parent - # Merge with parent: - @path = [*@parent.path, *path_name].freeze - else - # At top: - @path = path_name.freeze - end - end - - def path_name - [@name] - end - - alias lexical_path path - # A short form of the definition. # e.g. `def short_form`. # @@ -173,5 +179,9 @@ def documentation def location nil end + + # The visibility of the definition. + # @attribute [Symbol] :public, :private, :protected + attr_accessor :visibility end end diff --git a/lib/decode/index.rb b/lib/decode/index.rb index 9824822..468860a 100644 --- a/lib/decode/index.rb +++ b/lib/decode/index.rb @@ -57,7 +57,7 @@ def update(paths) # $stderr.puts "Adding #{symbol.qualified_name} to #{symbol.lexical_path.join(' -> ')}" @definitions[symbol.qualified_name] = symbol - @trie.insert(symbol.path, symbol) + @trie.insert(symbol.full_path, symbol) end end end @@ -71,7 +71,7 @@ def lookup(reference, relative_to: nil) if reference.absolute? || relative_to.nil? lexical_path = [] else - lexical_path = relative_to.path.dup + lexical_path = relative_to.full_path.dup end path = reference.path diff --git a/lib/decode/language/ruby/alias.rb b/lib/decode/language/ruby/alias.rb new file mode 100644 index 0000000..3bac697 --- /dev/null +++ b/lib/decode/language/ruby/alias.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2020-2024, by Samuel Williams. + +require_relative "definition" + +module Decode + module Language + module Ruby + # Represents an alias statement, e.g., `alias new_name old_name` or `alias_method :new_name, :old_name` + class Alias < Definition + def initialize(new_name, old_name, **options) + super(new_name, **options) + @old_name = old_name + end + + attr :old_name + + def short_form + "alias #{self.name} #{@old_name}" + end + + def long_form + "alias #{self.name} #{@old_name}" + end + + def to_s + "#{self.class.name} #{self.name} -> #{@old_name}" + end + end + end + end +end diff --git a/lib/decode/language/ruby/attribute.rb b/lib/decode/language/ruby/attribute.rb index 387ef41..8839ec6 100644 --- a/lib/decode/language/ruby/attribute.rb +++ b/lib/decode/language/ruby/attribute.rb @@ -13,17 +13,17 @@ class Attribute < Definition # The short form of the attribute. # e.g. `attr :value`. def short_form - case @node.type - when :block + case @node&.type + when :block_node "#{@name} { ... }" else - @node.location.expression.source + @node&.location&.slice || @name end end def long_form - if @node.location.line == @node.location.last_line - @node.location.expression.source + if @node&.location&.start_line == @node&.location&.end_line + @node.location.slice else short_form end diff --git a/lib/decode/language/ruby/call.rb b/lib/decode/language/ruby/call.rb index ca317e5..6ab6198 100644 --- a/lib/decode/language/ruby/call.rb +++ b/lib/decode/language/ruby/call.rb @@ -12,22 +12,32 @@ module Ruby class Call < Definition # A block can sometimes be a container for other definitions. def container? - false + @node&.block && @node.block.opening == "do" end # The short form of the class. # e.g. `foo`. def short_form - @name.to_s + if @node&.block && @node.block.opening == "{" + "#{name} { ... }" + else + name.to_s + end end # The long form of the class. # e.g. `foo(:bar)`. def long_form - if @node.location.line == @node.location.last_line - @node.location.expression.source + if @node.location.start_line == @node.location.end_line + @node.location.slice else - self.short_form + # For multiline calls, use the actual call name with arguments + if @node.arguments && @node.arguments.arguments.any? + arg_text = @node.arguments.arguments.map { |arg| arg.location.slice }.join(", ") + "#{@node.name}(#{arg_text})" + else + @node.name.to_s + end end end diff --git a/lib/decode/language/ruby/class.rb b/lib/decode/language/ruby/class.rb index d60d41c..904ed16 100644 --- a/lib/decode/language/ruby/class.rb +++ b/lib/decode/language/ruby/class.rb @@ -10,19 +10,23 @@ module Language module Ruby # A Ruby-specific class. class Class < Definition + def initialize(*arguments, super_class: nil, **options) + super(*arguments, **options) + + @super_class = super_class + end + + attr :super_class + # A class is a container for other definitions. def container? true end - def nested_name - "::#{name}" - end - # The short form of the class. # e.g. `class Animal`. def short_form - "class #{path_name.last}" + "class #{self.name}" end # The long form of the class. @@ -35,21 +39,11 @@ def long_form end end - def super_class - if super_node = @node.children[1] - super_node.location.expression.source - end - end - # The fully qualified name of the class. # e.g. `class ::Barnyard::Dog`. def qualified_form "class #{self.qualified_name}" end - - def path_name - @name.to_s.split("::").map(&:to_sym) - end end # A Ruby-specific singleton class. @@ -73,30 +67,14 @@ def nested? # The short form of the class. # e.g. `class << self`. def short_form - "class << #{@name}" + "class << #{self.name}" end # The long form is the same as the short form. alias long_form short_form - - def path_name - [:class] - end - - # The lexical scope as an array of names. - # e.g. `[:Decode, :Definition]` - # @returns [Array] - def path - if @path - # Cached version: - @path - else - @path = [*self.absolute_path, *self.path_name] - end - end - + private - + def absolute_path if @parent @parent.path diff --git a/lib/decode/language/ruby/code.rb b/lib/decode/language/ruby/code.rb index 526363c..8725ec2 100644 --- a/lib/decode/language/ruby/code.rb +++ b/lib/decode/language/ruby/code.rb @@ -6,7 +6,7 @@ require_relative "definition" require_relative "../../syntax/link" -require "parser/current" +require "prism" module Decode module Language @@ -15,7 +15,7 @@ module Ruby class Code def initialize(text, index, relative_to: nil, language: relative_to&.language) @text = text - @root = ::Parser::CurrentRuby.parse(text) + @root = ::Prism.parse(text) @index = index @relative_to = relative_to @language = language @@ -27,7 +27,7 @@ def initialize(text, index, relative_to: nil, language: relative_to&.language) def extract(into = []) if @index - traverse(@root, into) + traverse(@root.value, into) end return into @@ -37,29 +37,34 @@ def extract(into = []) def traverse(node, into) case node&.type - when :send + when :program_node + traverse(node.statements, into) + when :call_node if reference = Reference.from_const(node, @language) if definition = @index.lookup(reference, relative_to: @relative_to) - expression = node.location.selector - range = expression.begin_pos...expression.end_pos + # Use message_loc for the method name, not the entire call + expression = node.message_loc + range = expression.start_offset...expression.end_offset into << Syntax::Link.new(range, definition) end end # Extract constants from arguments: - children = node.children[2..-1].each do |node| - traverse(node, into) + if node.arguments + node.arguments.arguments.each do |arg_node| + traverse(arg_node, into) + end end - when :const + when :constant_read_node if reference = Reference.from_const(node, @language) if definition = @index.lookup(reference, relative_to: @relative_to) - expression = node.location.name - range = expression.begin_pos...expression.end_pos + expression = node.location + range = expression.start_offset...expression.end_offset into << Syntax::Link.new(range, definition) end end - when :begin - node.children.each do |child| + when :statements_node + node.body.each do |child| traverse(child, into) end end diff --git a/lib/decode/language/ruby/constant.rb b/lib/decode/language/ruby/constant.rb index 58279ea..f56b65c 100644 --- a/lib/decode/language/ruby/constant.rb +++ b/lib/decode/language/ruby/constant.rb @@ -13,7 +13,7 @@ class Constant < Definition # The short form of the constant. # e.g. `NAME`. def short_form - @node.location.name.source + @node.name.to_s end def nested_name @@ -23,12 +23,12 @@ def nested_name # The long form of the constant. # e.g. `NAME = "Alice"`. def long_form - if @node.location.line == @node.location.last_line - @node.location.expression.source - elsif @node.children[2].type == :array - "#{@name} = [...]" - elsif @node.children[2].type == :hash - "#{@name} = {...}" + if @node.location.start_line == @node.location.end_line + @node.location.slice + elsif @node.value&.type == :array_node + "#{@node.name} = [...]" + elsif @node.value&.type == :hash_node + "#{@node.name} = {...}" else self.short_form end diff --git a/lib/decode/language/ruby/definition.rb b/lib/decode/language/ruby/definition.rb index e84015f..d6d343d 100644 --- a/lib/decode/language/ruby/definition.rb +++ b/lib/decode/language/ruby/definition.rb @@ -11,15 +11,11 @@ module Ruby # A Ruby-specific definition. class Definition < Decode::Definition # Initialize the definition from the syntax tree node. - def initialize(node, *arguments, visibility: nil, **options) + def initialize(*arguments, visibility: nil, node: nil, **options) super(*arguments, **options) - @node = node @visibility = visibility - end - - def nested_name - "\##{@name}" + @node = node end # @attribute [Parser::AST::Node] The parser syntax tree node. @@ -33,18 +29,18 @@ def public? end def multiline? - @node.location.line != @node.location.last_line + @node.location.start_line != @node.location.end_line end # The source code associated with the definition. # @returns [String] def text - expression = @node.location.expression - lines = expression.source.lines + expression = @node.location + lines = expression.slice.lines if lines.count == 1 return lines.first else - if indentation = expression.source_line[/\A\s+/] + if indentation = expression.slice.lines.first[/\A\s+/] # Remove all the indentation: lines.each{|line| line.sub!(indentation, "")} end @@ -54,9 +50,9 @@ def text end def location - expression = @node.location.expression - - Location.new(expression.source_buffer.name, expression.line) + if @source + Location.new(@source.path, @node.location.start_line) + end end end end diff --git a/lib/decode/language/ruby/generic.rb b/lib/decode/language/ruby/generic.rb index 956f1af..f54165d 100644 --- a/lib/decode/language/ruby/generic.rb +++ b/lib/decode/language/ruby/generic.rb @@ -7,6 +7,8 @@ require_relative "parser" require_relative "code" +require_relative "../generic" + module Decode module Language module Ruby diff --git a/lib/decode/language/ruby/method.rb b/lib/decode/language/ruby/method.rb index fadc3f8..9a6da3b 100644 --- a/lib/decode/language/ruby/method.rb +++ b/lib/decode/language/ruby/method.rb @@ -10,28 +10,45 @@ module Language module Ruby # A Ruby-specific method. class Method < Definition + def initialize(*arguments, receiver: nil, **options) + super(*arguments, **options) + @receiver = receiver + end + + attr :receiver + + def nested_name + if @receiver + ".#{self.name}" + else + "##{self.name}" + end + end + # The short form of the method. - # e.g. `def puts`. + # e.g. `def puts` or `def self.puts`. def short_form - @node.location.keyword.join(@node.location.name).source + if @receiver + "def #{@receiver}.#{@node.name}" + else + "def #{@node.name}" + end end # The node which contains the function arguments. def arguments_node - if node = @node.children[1] - if node.location.expression - return node - end - end + @node.parameters end # The long form of the method. - # e.g. `def puts(*lines, separator: "\n")`. + # e.g. `def puts(*lines, separator: "\n")` or `def self.puts(*lines, separator: "\n")`. def long_form if arguments_node = self.arguments_node - @node.location.keyword.join( - arguments_node.location.expression - ).source + if @receiver + "def #{@receiver}.#{@node.name}(#{arguments_node.location.slice})" + else + "def #{@node.name}(#{arguments_node.location.slice})" + end else self.short_form end @@ -43,6 +60,17 @@ def qualified_form self.qualified_name end + # Override the qualified_name method to handle method name joining correctly + def qualified_name + @qualified_name ||= begin + if @parent + [@parent.qualified_name, self.nested_name].join("") + else + self.nested_name + end + end + end + def convert(kind) case kind when :attribute diff --git a/lib/decode/language/ruby/module.rb b/lib/decode/language/ruby/module.rb index bd3d6c4..b7afbfc 100644 --- a/lib/decode/language/ruby/module.rb +++ b/lib/decode/language/ruby/module.rb @@ -15,29 +15,21 @@ def container? true end - def nested_name - "::#{name}" - end - # The short form of the module. # e.g. `module Barnyard`. def short_form - "module #{path_name.last}" + "module #{self.name}" end def long_form qualified_form end - # The fully qualified name of the class. + # The fully qualified name of the module. # e.g. `module ::Barnyard::Dog`. def qualified_form "module #{self.qualified_name}" end - - def path_name - @name.to_s.split("::").map(&:to_sym) - end end end end diff --git a/lib/decode/language/ruby/parser.rb b/lib/decode/language/ruby/parser.rb index 7369284..a429e2b 100644 --- a/lib/decode/language/ruby/parser.rb +++ b/lib/decode/language/ruby/parser.rb @@ -3,10 +3,11 @@ # Released under the MIT License. # Copyright, 2020-2024, by Samuel Williams. -require "parser/current" +require "prism" require_relative "../../scope" +require_relative "alias" require_relative "attribute" require_relative "block" require_relative "call" @@ -38,23 +39,30 @@ def initialize(language) (@definitions[parent] ||= {})[name] end + private def store_definition(parent, name, definition) + (@definitions[parent] ||= {})[name] = definition + end + # Parse the given source object, can be a string or a Source instance. # @parameter source [String | Source] The source to parse. private def parse_source(source) if source.is_a?(Source) - ::Parser::CurrentRuby.parse_with_comments(source.read, source.relative_path) + Prism.parse(source.read, filepath: source.path) else - ::Parser::CurrentRuby.parse_with_comments(source) + Prism.parse(source) end end # Extract definitions from the given input file. def definitions_for(source, &block) - top, comments = self.parse_source(source) + return enum_for(:definitions_for, source) unless block_given? - if top - walk_definitions(top, comments, &block) - end + result = self.parse_source(source) + result.attach_comments! + + # Pass the source to walk_definitions for location tracking + source = source.is_a?(Source) ? source : nil + walk_definitions(result.value, nil, source, &block) end def extract_comments_for(node, comments) @@ -76,7 +84,8 @@ def extract_comments_for(node, comments) if comment = prefix.last if comment.location.line == (node.location.line - 1) return prefix.map do |comment| - comment.text.sub(/\A\#\s?/, "") + # Remove # and at most one space/tab to preserve indentation + comment.slice.sub(/\A\#[\s\t]?/, "") end end end @@ -91,183 +100,300 @@ def with_visibility(visibility = :public, &block) end # Walk over the syntax tree and extract relevant definitions with their associated comments. - def walk_definitions(node, comments, parent = nil, &block) + def walk_definitions(node, parent = nil, source = nil, &block) + # Check for scope definitions from comments + if node.comments.any? + parent = scope_for(node.comments.map(&:slice), parent, &block) || parent + end + case node.type - when :module - definition = Module.new( - node, nested_name_for(node.children[0]), - comments: extract_comments_for(node, comments), + when :program_node + with_visibility do + node.child_nodes.each do |child| + walk_definitions(child, parent, source, &block) + end + end + when :statements_node + node.child_nodes.each do |child| + walk_definitions(child, parent, source, &block) + end + when :block_node + if node.body + walk_definitions(node.body, parent, source, &block) + end + when :module_node + path = nested_path_for(node.constant_path) + + definition = Module.new(path, + visibility: :public, + comments: node.comments.map(&:slice), parent: parent, - language: @language, visibility: :public + node: node, + language: @language, + source: source, ) - assign_definition(parent, definition) - + store_definition(parent, path.last.to_sym, definition) yield definition - if children = node.children[1] + if body = node.body with_visibility do - walk_definitions(children, comments, definition, &block) + walk_definitions(body, definition, source, &block) end end - when :class - definition = Class.new( - node, nested_name_for(node.children[0]), - comments: extract_comments_for(node, comments), - parent: parent, language: @language, visibility: :public - ) + when :class_node + path = nested_path_for(node.constant_path) + super_class = nested_name_for(node.superclass) - assign_definition(parent, definition) + definition = Class.new(path, + super_class: super_class, + visibility: :public, + comments: node.comments.map(&:slice), + parent: parent, + node: node, + language: @language, + source: source, + ) + store_definition(parent, path.last.to_sym, definition) yield definition - if children = node.children[2] + if body = node.body with_visibility do - walk_definitions(children, comments, definition, &block) + walk_definitions(body, definition, source, &block) end end - when :sclass - if name = singleton_name_for(node.children[0]) - definition = Singleton.new( - node, name, - comments: extract_comments_for(node, comments), - parent: parent, language: @language, visibility: :public + when :singleton_class_node + if name = singleton_name_for(node) + definition = Singleton.new(name, + comments: node.comments.map(&:slice), + parent: parent, language: @language, visibility: :public, source: source ) yield definition - if children = node.children[1] - walk_definitions(children, comments, definition, &block) + if body = node.body + walk_definitions(body, definition, source, &block) end end - when :def - definition = Method.new( - node, node.children[0], - comments: extract_comments_for(node, comments), - parent: parent, language: @language, visibility: @visibility - ) - - yield definition - when :defs - extracted_comments = extract_comments_for(node, comments) + when :def_node + receiver = receiver_for(node.receiver) - definition = Function.new( - node, node.children[1], - comments: extracted_comments, - parent: scope_for(extracted_comments, parent, &block), - language: @language, visibility: @visibility + definition = Method.new(node.name, + visibility: @visibility, + comments: node.comments.map(&:slice), + parent: parent, + node: node, + language: @language, + receiver: receiver, + source: source, ) yield definition - when :casgn - definition = Constant.new( - node, node.children[1], - comments: extract_comments_for(node, comments), - parent: parent, language: @language + when :constant_write_node + definition = Constant.new(node.name, + comments: node.comments.map(&:slice), + parent: parent, + node: node, + language: @language, ) + store_definition(parent, node.name, definition) yield definition - when :send - name = node.children[1] + when :call_node + name = node.name case name when :public, :protected, :private - @visibility = name + # Handle cases like "private def foo" where method definitions are arguments + if node.arguments + has_method_definitions = false + node.arguments.arguments.each do |arg_node| + if arg_node.type == :def_node + has_method_definitions = true + # Process the method definition with the specified visibility + receiver = receiver_for(arg_node.receiver) + + definition = Method.new(arg_node.name, + visibility: name, + comments: arg_node.comments.map(&:slice), + parent: parent, + node: arg_node, + language: @language, + receiver: receiver, + ) + + yield definition + end + end + + # Only set visibility state if this is NOT an inline method definition + unless has_method_definitions + @visibility = name + end + else + # No arguments, so this is a standalone visibility modifier + @visibility = name + end when :private_constant - constant_names_for(node.children[2..]) do |name| - if definition = lookup_definition(parent, name) - definition.visibility = :private + if node.arguments + constant_names_for(node.arguments.arguments) do |name| + if definition = lookup_definition(parent, name) + definition.visibility = :private + end end end when :attr, :attr_reader, :attr_writer, :attr_accessor - definition = Attribute.new( - node, name_for(node.children[2]), - comments: extract_comments_for(node, comments), - parent: parent, language: @language + definition = Attribute.new(attribute_name_for(node), + comments: node.comments.map(&:slice), + parent: parent, language: @language, node: node ) yield definition + when :alias_method + # Handle alias_method :new_name, :old_name syntax + if node.arguments && node.arguments.arguments.size >= 2 + new_name_arg = node.arguments.arguments[0] + old_name_arg = node.arguments.arguments[1] + + # Extract symbol names from the arguments + new_name = symbol_name_for(new_name_arg) + old_name = symbol_name_for(old_name_arg) + + definition = Alias.new(new_name.to_sym, old_name.to_sym, + comments: node.comments.map(&:slice), + parent: parent, + node: node, + language: @language, + visibility: @visibility, + source: source, + ) + + yield definition + end else - extracted_comments = extract_comments_for(node, comments) - if kind = kind_for(node, extracted_comments) + # Check if this call should be treated as a definition + # either because it has a @name comment, @attribute comment, or a block + has_name_comment = node.comments.any? { |comment| comment.slice.match(NAME_ATTRIBUTE) } + has_attribute_comment = kind_for(node, node.comments.map(&:slice)) + has_block = node.block + + if has_name_comment || has_attribute_comment || has_block definition = Call.new( - node, name_for(node, extracted_comments), - comments: extracted_comments, - parent: parent, language: @language + attribute_name_for(node), + comments: node.comments.map(&:slice), + parent: parent, language: @language, node: node ) yield definition + + # Walk into the block body if it exists + if node.block + walk_definitions(node.block, definition, source, &block) + end end end - when :block - extracted_comments = extract_comments_for(node, comments) + when :alias_method_node + # Handle alias new_name old_name syntax + new_name = node.new_name.unescaped + old_name = node.old_name.unescaped - if name = name_for(node, extracted_comments) - definition = Block.new( - node, name, - comments: extracted_comments, - parent: scope_for(extracted_comments, parent, &block), - language: @language - ) - - if kind = kind_for(node, extracted_comments) - definition = definition.convert(kind) - end - - yield definition - - if children = node.children[2] - walk_definitions(children, comments, definition, &block) - end - end + definition = Alias.new(new_name.to_sym, old_name.to_sym, + comments: node.comments.map(&:slice), + parent: parent, + node: node, + language: @language, + visibility: @visibility, + source: source, + ) + + yield definition else - node.children.each do |child| - if child.is_a?(::Parser::AST::Node) - walk_definitions(child, comments, parent, &block) if child - end + if node.respond_to?(:statements) + walk_definitions(node.statements, parent, source, &block) + else + # $stderr.puts "Ignoring #{node.type}" end end end - NAME_ATTRIBUTE = /\A@name\s+(?.*?)\Z/ + NAME_ATTRIBUTE = /\A\#\s*@name\s+(?.*?)\Z/ - def name_for(node, comments = nil) - comments&.each do |comment| - if match = comment.match(NAME_ATTRIBUTE) + def attribute_name_for(node) + node.comments.each do |comment| + text = comment.slice + if match = text.match(NAME_ATTRIBUTE) return match[:value].to_sym end end + if node.arguments && node.arguments.arguments.any? + argument = node.arguments.arguments.first + case argument.type + when :symbol_node + return argument.unescaped.to_sym + when :call_node + return argument.name + when :block_node + return node.name + end + end + + return node.name + end + + def nested_path_for(node, path = []) + return nil if node.nil? + case node.type - when :sym - return node.children[0] - when :send - return node.children[1] - when :block - return node.children[0].children[1] + when :constant_read_node + path << node.name + when :constant_path_node + nested_path_for(node.parent, path) + path << node.name end + + return path.empty? ? nil : path end def nested_name_for(node) - if prefix = node.children[0] - "#{nested_name_for(prefix)}::#{node.children[1]}".to_sym + nested_path_for(node)&.join("::") + end + + def symbol_name_for(node) + case node.type + when :symbol_node + node.unescaped else - node.children[1] + node.slice end end - def singleton_name_for(node) + def receiver_for(node) + return nil unless node + case node.type - when :const + when :self_node + "self" + when :constant_read_node + node.name.to_s + when :constant_path_node nested_name_for(node) - when :self - :'self' end end - + + def singleton_name_for(node) + case node.expression.type + when :self_node + "self" + when :constant_read_node + nested_name_for(node.expression) + end + end + KIND_ATTRIBUTE = /\A - (@(?attribute)\s+(?.*?))| - (@define\s+(?)\s+(?.*?)) + (\#\s*@(?attribute)\s+(?.*?))| + (\#\s*@define\s+(?)\s+(?.*?)) \Z/x def kind_for(node, comments = nil) @@ -281,7 +407,7 @@ def kind_for(node, comments = nil) end SCOPE_ATTRIBUTE = /\A - (@scope\s+(?.*?)) + \#\s*@scope\s+(?.*?) \Z/x def scope_for(comments, parent = nil, &block) @@ -298,59 +424,80 @@ def scope_for(comments, parent = nil, &block) return parent end - def constant_names_for(children) - children.each do |node| - if node.type == :sym - yield node.children[0] + def constant_names_for(child_nodes) + child_nodes.each do |node| + if node.type == :symbol_node + yield node.unescaped.to_sym end end end # Extract segments from the given input file. def segments_for(source, &block) - top, comments = self.parse_source(source) - - # We delete any leading comments: - line = 0 - - while comment = comments.first - if comment.location.line == line - comments.pop - line += 1 - else - break - end + result = self.parse_source(source) + comments = result.comments.reject do |comment| + comment.location.slice.start_with?("#!/") || + comment.location.slice.start_with?("# frozen_string_literal:") || + comment.location.slice.start_with?("# Released under the MIT License.") || + comment.location.slice.start_with?("# Copyright,") end # Now we iterate over the syntax tree and generate segments: - walk_segments(top, comments, &block) + walk_segments(result.value, comments, &block) end def walk_segments(node, comments, &block) case node.type - when :begin - segment = nil + when :program_node + walk_segments(node.statements, comments, &block) + when :statements_node + statements = node.child_nodes + current_segment = nil - node.children.each do |child| - if segment.nil? - segment = Segment.new( - extract_comments_for(child, comments), - @language, child + statements.each_with_index do |stmt, stmt_index| + # Find comments that precede this statement and are not inside previous statements + preceding_comments = [] + last_stmt_end_line = stmt_index > 0 ? statements[stmt_index - 1].location.end_line : 0 + + comments.each do |comment| + comment_line = comment.location.start_line + # Comment must be after the previous statement and before this statement + if comment_line > last_stmt_end_line && comment_line < stmt.location.start_line + preceding_comments << comment + end + end + + # Remove consumed comments + comments -= preceding_comments + + if preceding_comments.any? + # Start a new segment with these comments + yield current_segment if current_segment + current_segment = Segment.new( + preceding_comments.map { |c| c.location.slice.sub(/^#[\s\t]?/, "") }, + @language, + stmt ) - elsif next_comments = extract_comments_for(child, comments) - yield segment if segment - segment = Segment.new(next_comments, @language, child) + elsif current_segment + # Extend current segment with this statement + current_segment.expand(stmt) else - segment.expand(child) + # Start a new segment without comments + current_segment = Segment.new( + [], + @language, + stmt + ) end end - yield segment if segment + yield current_segment if current_segment else # One top level segment: segment = Segment.new( - extract_comments_for(node, comments), - @language, node + [], + @language, + node ) yield segment diff --git a/lib/decode/language/ruby/reference.rb b/lib/decode/language/ruby/reference.rb index 0d2cd2e..81868b7 100644 --- a/lib/decode/language/ruby/reference.rb +++ b/lib/decode/language/ruby/reference.rb @@ -13,29 +13,30 @@ class Reference < Language::Reference def self.from_const(node, language) lexical_path = append_const(node) - return self.new(node.location.expression.source, language, lexical_path) + return self.new(node.location.slice, language, lexical_path) end def self.append_const(node, path = []) - parent, name = node.children - - if parent and parent.type != :cbase - append_const(parent, path) - end - case node.type - when :const - if parent && parent.type != :cbase - path << ["::", name] + when :constant_read_node + path << [nil, node.name.to_s] + when :constant_path_node + if node.parent + append_const(node.parent, path) + path << ["::", node.name.to_s] + else + path << [nil, node.name.to_s] + end + when :call_node + # For call nodes like Tuple(...), treat them as constant references + if node.receiver.nil? + path << [nil, node.name.to_s] else - path << [nil, name] + append_const(node.receiver, path) + path << [".", node.name.to_s] end - when :send - path << ["#", name] - when :cbase - # Ignore. else - raise ArgumentError, "Could not determine reference for #{node}!" + raise ArgumentError, "Could not determine reference for #{node.type}!" end return path diff --git a/lib/decode/language/ruby/segment.rb b/lib/decode/language/ruby/segment.rb index a25dc09..342dd45 100644 --- a/lib/decode/language/ruby/segment.rb +++ b/lib/decode/language/ruby/segment.rb @@ -14,20 +14,20 @@ def initialize(comments, language, node, **options) super(comments, language, **options) @node = node - @expression = node.location.expression + @expression = node.location end # The parser syntax tree node. attr :node def expand(node) - @expression = @expression.join(node.location.expression) + @expression = @expression.join(node.location) end # The source code trailing the comments. # @returns [String | nil] def code - @expression.source + @expression.slice end end end diff --git a/test/decode/language/ruby/.fixtures/aliases.rb b/test/decode/language/ruby/.fixtures/aliases.rb new file mode 100644 index 0000000..a8a3aac --- /dev/null +++ b/test/decode/language/ruby/.fixtures/aliases.rb @@ -0,0 +1,18 @@ +class Test + def original_method + puts "original" + end + + alias new_method original_method + alias_method :another_method, :original_method + + private + + def private_original + puts "private original" + end + + private + alias private_alias private_original + alias_method :private_alias_method, :private_original +end diff --git a/test/decode/language/ruby/.fixtures/attributes.rb b/test/decode/language/ruby/.fixtures/attributes.rb index 379a3ae..6181edd 100644 --- a/test/decode/language/ruby/.fixtures/attributes.rb +++ b/test/decode/language/ruby/.fixtures/attributes.rb @@ -3,6 +3,7 @@ # Released under the MIT License. # Copyright, 2020-2024, by Samuel Williams. +# The first attribute attr :a attr_reader :b attr_writer :c diff --git a/test/decode/language/ruby/.fixtures/inline_visibility.rb b/test/decode/language/ruby/.fixtures/inline_visibility.rb new file mode 100644 index 0000000..f81ae9b --- /dev/null +++ b/test/decode/language/ruby/.fixtures/inline_visibility.rb @@ -0,0 +1,41 @@ +# Released under the MIT License. +# Copyright, 2024, by Samuel Williams. + +class VisibilityTest + def public_method_1 + end + + private def private_method_1 + end + + def public_method_2 + end + + protected def protected_method_1 + end + + def public_method_3 + end + + public def public_method_4 + end + + # Test standalone modifier after inline + private + + def private_method_2 + end + + def private_method_3 + end + + protected + + def protected_method_2 + end + + public + + def public_method_5 + end +end diff --git a/test/decode/language/ruby/aliases.rb b/test/decode/language/ruby/aliases.rb new file mode 100644 index 0000000..8bb2bc9 --- /dev/null +++ b/test/decode/language/ruby/aliases.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2020-2024, by Samuel Williams. + +require "decode/language/ruby" +require "decode/source" + +describe Decode::Language::Ruby do + let(:language) {Decode::Language::Ruby.new} + + with "alias definitions" do + let(:source) {Decode::Source.new("test/decode/language/ruby/.fixtures/aliases.rb", language)} + + it "should extract alias definitions" do + definitions = language.definitions_for(source).to_a + + aliases = definitions.select do |definition| + definition.is_a?(Decode::Language::Ruby::Alias) + end + expect(aliases.size).to be == 4 + + # Check regular alias + new_method_alias = aliases.find do |alias_definition| + alias_definition.name == :new_method + end + expect(new_method_alias).to be_a(Decode::Language::Ruby::Alias) + expect(new_method_alias.old_name).to be == :original_method + expect(new_method_alias.visibility).to be == :public + + # Check alias_method + another_method_alias = aliases.find do |alias_definition| + alias_definition.name == :another_method + end + expect(another_method_alias).to be_a(Decode::Language::Ruby::Alias) + expect(another_method_alias.old_name).to be == :original_method + expect(another_method_alias.visibility).to be == :public + + # Check private aliases + private_alias = aliases.find do |alias_definition| + alias_definition.name == :private_alias + end + expect(private_alias).to be_a(Decode::Language::Ruby::Alias) + expect(private_alias.old_name).to be == :private_original + expect(private_alias.visibility).to be == :private + + private_alias_method = aliases.find do |alias_definition| + alias_definition.name == :private_alias_method + end + expect(private_alias_method).to be_a(Decode::Language::Ruby::Alias) + expect(private_alias_method.old_name).to be == :private_original + expect(private_alias_method.visibility).to be == :private + end + + it "should have correct short and long forms" do + definitions = language.definitions_for(source).to_a + alias_def = definitions.find do |definition| + definition.is_a?(Decode::Language::Ruby::Alias) && definition.name == :new_method + end + + expect(alias_def.short_form).to be == "alias new_method original_method" + expect(alias_def.long_form).to be == "alias new_method original_method" + end + end +end diff --git a/test/decode/language/ruby/comments.rb b/test/decode/language/ruby/comments.rb new file mode 100644 index 0000000..243af36 --- /dev/null +++ b/test/decode/language/ruby/comments.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2020-2024, by Samuel Williams. + +require "decode/language/ruby" +require "decode/source" + +describe Decode::Language::Ruby do + let(:language) {Decode::Language::Ruby.new} + + with "comment extraction" do + it "should preserve comment indentation" do + # Use the existing comments fixture + source = Decode::Source.new("test/decode/language/ruby/.fixtures/comments.rb", language) + definitions = language.definitions_for(source).to_a + # Should have definitions with comments + definitions_with_comments = definitions.select do |definition| + definition.comments.any? + end + expect(definitions_with_comments.size).to be > 0 + end + + it "should handle comments correctly" do + # Use the comments fixture + source = Decode::Source.new("test/decode/language/ruby/.fixtures/comments.rb", language) + definitions = language.definitions_for(source).to_a + + # Should extract comments properly + definitions.each do |definition| + if definition.comments.any? + expect(definition.comments).to be_a(Array) + expect(definition.comments.first).to be_a(String) + end + end + end + end +end diff --git a/test/decode/language/ruby/parser.rb b/test/decode/language/ruby/parser.rb index edca3a4..8bf8d8e 100644 --- a/test/decode/language/ruby/parser.rb +++ b/test/decode/language/ruby/parser.rb @@ -41,6 +41,13 @@ expect(definitions[2].long_form).to be == "class << self" expect(definitions[3].long_form).to be == "class My::Nested::Child" end + + it "should handle singleton classes" do + singleton_classes = definitions.select do |definition| + definition.is_a?(Decode::Language::Ruby::Singleton) + end + expect(singleton_classes.size).to be > 0 + end end with "modules" do @@ -79,7 +86,7 @@ end it "has full path" do - expect(definitions[1].path).to be == [:X, :Y, :Z] + expect(definitions[1].full_path).to be == [:X, :Y, :Z] end it "has long form" do @@ -91,6 +98,14 @@ expect(definitions[0].qualified_form).to be == "module X::Y" expect(definitions[1].qualified_form).to be == "module X::Y::Z" end + + it "should handle complex constant paths" do + # Should have modules with nested paths + modules = definitions.select do |definition| + definition.is_a?(Decode::Language::Ruby::Module) + end + expect(modules.size).to be > 0 + end end with "instance methods" do @@ -127,6 +142,18 @@ expect(definitions[0].long_form).to be == "def self.without_arguments" expect(definitions[1].long_form).to be == "def self.with_arguments(x = 10)" end + + it "should handle method definitions with complex receivers" do + methods = definitions.select do |definition| + definition.is_a?(Decode::Language::Ruby::Method) + end + + # Should have methods with self receivers + class_methods = methods.select do |method| + method.receiver == "self" + end + expect(class_methods.size).to be > 0 + end end with "functions" do @@ -145,7 +172,7 @@ end it "has correct path" do - expect(definitions[1].path).to be == [:Foo, :bar] + expect(definitions[1].full_path).to be == [:Foo, :bar] end end @@ -189,6 +216,14 @@ expect(definitions[2].long_form).to be == "attr_writer :c" expect(definitions[3].long_form).to be == "attr_accessor :d" end + + it "should handle complex attribute definitions" do + attributes = definitions.select do |definition| + definition.is_a?(Decode::Language::Ruby::Attribute) + end + + expect(attributes.size).to be > 0 + end end with "comments" do @@ -277,4 +312,235 @@ expect(definitions[6].visibility).to be == :public end end + + with "inline visibility" do + let(:path) {File.expand_path(".fixtures/inline_visibility.rb", __dir__)} + + it "can extract definitions" do + expect(definitions).not.to be(:empty?) + end + + it "handles inline visibility modifiers correctly" do + expect(definitions.size).to be == 11 + + # First definition is the class itself + expect(definitions[0].name).to be == :VisibilityTest + expect(definitions[0].visibility).to be == :public + + # Test that inline visibility modifiers only affect the specific method + expect(definitions[1].name).to be == :public_method_1 + expect(definitions[1].visibility).to be == :public + + expect(definitions[2].name).to be == :private_method_1 + expect(definitions[2].visibility).to be == :private + + expect(definitions[3].name).to be == :public_method_2 + expect(definitions[3].visibility).to be == :public # Should remain public after inline private + + expect(definitions[4].name).to be == :protected_method_1 + expect(definitions[4].visibility).to be == :protected + + expect(definitions[5].name).to be == :public_method_3 + expect(definitions[5].visibility).to be == :public # Should remain public after inline protected + + expect(definitions[6].name).to be == :public_method_4 + expect(definitions[6].visibility).to be == :public + + # Test that standalone modifiers still work after inline modifiers + expect(definitions[7].name).to be == :private_method_2 + expect(definitions[7].visibility).to be == :private + + expect(definitions[8].name).to be == :private_method_3 + expect(definitions[8].visibility).to be == :private + + expect(definitions[9].name).to be == :protected_method_2 + expect(definitions[9].visibility).to be == :protected + + expect(definitions[10].name).to be == :public_method_5 + expect(definitions[10].visibility).to be == :public + end + end + + with "enumerator functionality" do + let(:path) {File.expand_path(".fixtures/classes.rb", __dir__)} + + it "should return an enumerator when no block is given" do + enumerator = language.definitions_for(source) + expect(enumerator).to be_a(Enumerator) + end + + it "should yield definitions when enumerator is used" do + definitions = language.definitions_for(source).to_a + expect(definitions.size).to be > 0 + + definitions.each do |definition| + expect(definition).to be_a(Decode::Definition) + end + end + + it "should work with enumerator methods" do + definitions = language.definitions_for(source) + + # Test select + classes = definitions.select do |definition| + definition.is_a?(Decode::Language::Ruby::Class) + end + expect(classes.size).to be > 0 + + # Test map + names = definitions.map(&:name) + expect(names.size).to be > 0 + + # Test count + count = definitions.count + expect(count).to be > 0 + end + + it "should provide the same results when called multiple times" do + definitions1 = language.definitions_for(source).to_a + definitions2 = language.definitions_for(source).to_a + + expect(definitions1.size).to be == definitions2.size + expect(definitions1.map(&:name)).to be == definitions2.map(&:name) + end + end + + with "helper methods" do + let(:parser) { language.parser } + + with "#symbol_name_for" do + it "should extract symbol names" do + code = "alias_method :new_name, :old_name" + result = Prism.parse(code) + node = result.value.statements.body.first + new_name_arg = node.arguments.arguments[0] + old_name_arg = node.arguments.arguments[1] + + expect(parser.send(:symbol_name_for, new_name_arg)).to be == "new_name" + expect(parser.send(:symbol_name_for, old_name_arg)).to be == "old_name" + end + + it "should extract string names" do + code = 'alias_method "new_name", "old_name"' + result = Prism.parse(code) + node = result.value.statements.body.first + new_name_arg = node.arguments.arguments[0] + old_name_arg = node.arguments.arguments[1] + + expect(parser.send(:symbol_name_for, new_name_arg)).to be == '"new_name"' + expect(parser.send(:symbol_name_for, old_name_arg)).to be == '"old_name"' + end + end + + with "#receiver_for" do + it "should handle self receiver" do + code = "def self.foo; end" + result = Prism.parse(code) + node = result.value.statements.body.first + + expect(parser.send(:receiver_for, node.receiver)).to be == "self" + end + + it "should handle constant receiver" do + code = "def Test.foo; end" + result = Prism.parse(code) + node = result.value.statements.body.first + + expect(parser.send(:receiver_for, node.receiver)).to be == "Test" + end + + it "should handle constant path receiver" do + code = "def Nested::Class.foo; end" + result = Prism.parse(code) + node = result.value.statements.body.first + + expect(parser.send(:receiver_for, node.receiver)).to be == "Nested" + end + + it "should handle nil receiver" do + code = "def foo; end" + result = Prism.parse(code) + node = result.value.statements.body.first + + expect(parser.send(:receiver_for, node.receiver)).to be == nil + end + end + + with "#nested_name_for" do + it "should handle simple constant" do + code = "class Test; end" + result = Prism.parse(code) + node = result.value.statements.body.first + + expect(parser.send(:nested_name_for, node.constant_path)).to be == "Test" + end + + it "should handle nested constant" do + code = "class Nested::Test; end" + result = Prism.parse(code) + node = result.value.statements.body.first + + expect(parser.send(:nested_name_for, node.constant_path)).to be == "Nested::Test" + end + + it "should handle nil" do + expect(parser.send(:nested_name_for, nil)).to be == nil + end + end + + with "#singleton_name_for" do + it "should handle self singleton" do + code = "class << self; end" + result = Prism.parse(code) + node = result.value.statements.body.first + + expect(parser.send(:singleton_name_for, node)).to be == "self" + end + + it "should handle constant singleton" do + code = "class << Test; end" + result = Prism.parse(code) + node = result.value.statements.body.first + + expect(parser.send(:singleton_name_for, node)).to be == "Test" + end + end + end + + with "edge cases" do + let(:parser) { language.parser } + + it "should handle inline visibility with non-method definitions" do + code = "private :some_method" + + definitions = parser.definitions_for(code).to_a + + # This should not create any definitions but should set visibility state + expect(definitions.size).to be == 0 + end + + it "should handle attribute with call node argument" do + code = " + # @name custom_name + attr_reader some_method_call() + " + + definitions = parser.definitions_for(code).to_a + + expect(definitions.size).to be == 1 + expect(definitions.first.name).to be == :custom_name + end + + it "should handle attribute with block node argument" do + code = " + # @name block_attr + attr_reader { block_content } + " + + definitions = parser.definitions_for(code).to_a + + expect(definitions.size).to be == 1 + expect(definitions.first.name).to be == :block_attr + end + end end diff --git a/test/decode/language/ruby/source.rb b/test/decode/language/ruby/source.rb new file mode 100644 index 0000000..754d3e9 --- /dev/null +++ b/test/decode/language/ruby/source.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2020-2024, by Samuel Williams. + +require "decode/language/ruby" +require "decode/source" + +describe Decode::Language::Ruby do + let(:language) {Decode::Language::Ruby.new} + + with "source tracking" do + let(:source) {Decode::Source.new("test/decode/language/ruby/.fixtures/classes.rb", language)} + + it "should attach source to all definitions" do + definitions = language.definitions_for(source).to_a + + definitions.each do |definition| + expect(definition.source).to be == source + end + end + + it "should provide correct location information" do + definitions = language.definitions_for(source).to_a + + # Find a class definition + class_def = definitions.find do |definition| + definition.is_a?(Decode::Language::Ruby::Class) + end + expect(class_def).not.to be_nil + expect(class_def.location).not.to be_nil + expect(class_def.location.line).to be > 0 + end + + it "should handle nested definitions with correct source" do + # Use the existing nested modules fixture + source = Decode::Source.new("test/decode/language/ruby/.fixtures/nested_modules.rb", language) + definitions = language.definitions_for(source).to_a + + # All definitions should have the same source + definitions.each do |definition| + expect(definition.source).to be == source + end + end + end +end diff --git a/test/decode/language/ruby/visibility.rb b/test/decode/language/ruby/visibility.rb new file mode 100644 index 0000000..c2908bd --- /dev/null +++ b/test/decode/language/ruby/visibility.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2020-2024, by Samuel Williams. + +require "decode/language/ruby" +require "decode/source" + +describe Decode::Language::Ruby do + let(:language) {Decode::Language::Ruby.new} + + with "visibility modifiers" do + let(:source) {Decode::Source.new("test/decode/language/ruby/.fixtures/inline_visibility.rb", language)} + + it "should handle standalone visibility modifiers" do + definitions = language.definitions_for(source).to_a + methods = definitions.select do |definition| + definition.is_a?(Decode::Language::Ruby::Method) + end + + # Test public methods + public_methods = methods.select do |method| + method.visibility == :public + end + public_method_names = public_methods.map(&:name) + expect(public_method_names).to be(:include?, :public_method_1) + expect(public_method_names).to be(:include?, :public_method_2) + expect(public_method_names).to be(:include?, :public_method_3) + expect(public_method_names).to be(:include?, :public_method_4) + expect(public_method_names).to be(:include?, :public_method_5) + + # Test private methods + private_methods = methods.select do |method| + method.visibility == :private + end + private_method_names = private_methods.map(&:name) + expect(private_method_names).to be(:include?, :private_method_1) + expect(private_method_names).to be(:include?, :private_method_2) + expect(private_method_names).to be(:include?, :private_method_3) + + # Test protected methods + protected_methods = methods.select do |method| + method.visibility == :protected + end + protected_method_names = protected_methods.map(&:name) + expect(protected_method_names).to be(:include?, :protected_method_1) + expect(protected_method_names).to be(:include?, :protected_method_2) + end + + it "should handle inline visibility modifiers" do + definitions = language.definitions_for(source).to_a + methods = definitions.select do |definition| + definition.is_a?(Decode::Language::Ruby::Method) + end + + # private def private_method_1 should be private + private_method_1 = methods.find do |method| + method.name == :private_method_1 + end + expect(private_method_1.visibility).to be == :private + + # protected def protected_method_1 should be protected + protected_method_1 = methods.find do |method| + method.name == :protected_method_1 + end + expect(protected_method_1.visibility).to be == :protected + end + + it "should reset visibility correctly after inline definitions" do + definitions = language.definitions_for(source).to_a + methods = definitions.select do |definition| + definition.is_a?(Decode::Language::Ruby::Method) + end + + # Method after inline private should still be public + public_method_2 = methods.find do |method| + method.name == :public_method_2 + end + expect(public_method_2.visibility).to be == :public + + # Method after inline protected should still be public + public_method_3 = methods.find do |method| + method.name == :public_method_3 + end + expect(public_method_3.visibility).to be == :public + end + end + + with "class methods and visibility" do + it "should handle class method visibility correctly" do + # Test that class methods can have visibility modifiers + # This is a simplified test that uses the existing fixtures + source = Decode::Source.new("test/decode/language/ruby/.fixtures/class_methods.rb", language) + definitions = language.definitions_for(source).to_a + methods = definitions.select do |definition| + definition.is_a?(Decode::Language::Ruby::Method) + end + + # All methods in class_methods.rb should have receivers + class_methods = methods.select do |method| + method.receiver + end + expect(class_methods.size).to be > 0 + + class_methods.each do |method| + expect(method.receiver).to be == "self" + end + end + end +end \ No newline at end of file