Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Support attr_x methods #53

Merged
merged 1 commit into from
Sep 21, 2022
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
14 changes: 13 additions & 1 deletion lib/rucoa/nodes/class_node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@

module Rucoa
module Nodes
class ClassNode < ModuleNode
class ClassNode < Base
include NodeConcerns::NameFullyQualifiable

# @return [String]
def name
const_node.name
end

# @return [String, nil]
# @example returns nil for class for `class Foo`
# node = Rucoa::Source.new(
Expand Down Expand Up @@ -48,6 +55,11 @@ def super_class_chained_name

private

# @return [Rucoa::Nodes::ConstNode]
def const_node
children[0]
end

# @return [Rucoa::Nodes::Base, nil]
def super_class_node
children[1]
Expand Down
329 changes: 283 additions & 46 deletions lib/rucoa/yard/definitions_loader.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require 'logger'
require 'set'
require 'yard'

module Rucoa
Expand All @@ -18,17 +19,6 @@ def call(
root_node: root_node
).call
end

# @param comment [String]
# @return [YARD::DocstringParser]
def parse_yard_comment(comment)
::YARD::Logger.instance.enter_level(::Logger::FATAL) do
::YARD::Docstring.parser.parse(
comment,
::YARD::CodeObjects::Base.new(:root, 'stub')
)
end
end
end

# @param associations [Hash]
Expand All @@ -46,41 +36,17 @@ def call
[
@root_node,
*@root_node.descendants
].filter_map do |node|
comment = comment_for(node)
case node
when Nodes::ClassNode
Definitions::ClassDefinition.new(
fully_qualified_name: node.fully_qualified_name,
module_nesting: node.module_nesting,
source_path: @root_node.location.expression.source_buffer.name,
super_class_chained_name: node.super_class_chained_name
)
when Nodes::ModuleNode
Definitions::ModuleDefinition.new(
fully_qualified_name: node.fully_qualified_name,
source_path: @root_node.location.expression.source_buffer.name
)
when Nodes::DefNode, Nodes::DefsNode
docstring_parser = self.class.parse_yard_comment(comment)
return_types = docstring_parser.tags.select do |tag|
tag.tag_name == 'return'
end.flat_map(&:types).compact.map do |yard_type|
Type.new(yard_type).to_rucoa_type
end
return_types = ['Object'] if return_types.empty?
Definitions::MethodDefinition.new(
description: docstring_parser.to_docstring.to_s,
kind: node.singleton? ? :singleton : :instance,
method_name: node.name,
namespace: node.namespace,
source_path: @root_node.location.expression.source_buffer.name,
types: return_types.map do |type|
Types::MethodType.new(
parameters_string: '', # TODO
return_type: type
)
end
].flat_map do |node|
[
DefinitionGenerators::ClassDefinitionGenerator,
DefinitionGenerators::MethodDefinitionGenerator,
DefinitionGenerators::ModuleDefinitionGenerator,
DefinitionGenerators::AttributeReaderDefinitionGenerator,
DefinitionGenerators::AttributeWriterDefinitionGenerator
].flat_map do |generator|
generator.call(
comment: comment_for(node),
node: node
)
end
end
Expand All @@ -92,6 +58,277 @@ def comment_for(node)
parser_comment.text.gsub(/^#\s*/m, '')
end.join("\n")
end

module DefinitionGenerators
class Base
class << self
# @param comment [String]
# @param node [Rucoa::Nodes::Base]
# @return [Array<Rucoa::Definitions::Base>]
def call(
comment:,
node:
)
new(
comment: comment,
node: node
).call
end
end

# @param comment [String]
# @param node [Rucoa::Nodes::Base]
def initialize(
comment:,
node:
)
@comment = comment
@node = node
end

# @return [Array<Rucoa::Definitions::Base>]
def call
raise ::NotImplementedError
end

private

# @return [YARD::DocstringParser]
def docstring_parser
@docstring_parser ||= ::YARD::Logger.instance.enter_level(::Logger::FATAL) do
::YARD::Docstring.parser.parse(
@comment,
::YARD::CodeObjects::Base.new(:root, 'stub')
)
end
end

# @return [Array<String>]
# @example returns annotated return types if return tag is provided
# definitions = Rucoa::Source.new(
# content: <<~RUBY,
# # @return [String]
# def foo
# end
# RUBY
# uri: '/path/to/foo.rb'
# ).definitions
# expect(definitions[0].return_types).to eq(%w[String])
# @example returns Object if no return tag is provided
# definitions = Rucoa::Source.new(
# content: <<~RUBY,
# def foo
# end
# RUBY
# uri: '/path/to/foo.rb'
# ).definitions
# expect(definitions[0].return_types).to eq(%w[Object])
def return_types
types = docstring_parser.tags.select do |tag|
tag.tag_name == 'return'
end.flat_map(&:types).compact.map do |yard_type|
Type.new(yard_type).to_rucoa_type
end
if types.empty?
%w[Object]
else
types
end
end
end

class ClassDefinitionGenerator < Base
# @example returns class definition for class node
# definitions = Rucoa::Source.new(
# content: <<~RUBY,
# class Foo
# end
# RUBY
# uri: '/path/to/foo.rb'
# ).definitions
# expect(definitions[0]).to be_a(Rucoa::Definitions::ClassDefinition)
def call
return [] unless @node.is_a?(Nodes::ClassNode)

[
Definitions::ClassDefinition.new(
fully_qualified_name: @node.fully_qualified_name,
module_nesting: @node.module_nesting,
source_path: @node.location.expression.source_buffer.name,
super_class_chained_name: @node.super_class_chained_name
)
]
end
end

class ModuleDefinitionGenerator < Base
# @example returns module definition for module node
# definitions = Rucoa::Source.new(
# content: <<~RUBY,
# module Foo
# end
# RUBY
# uri: '/path/to/foo.rb'
# ).definitions
# expect(definitions[0]).to be_a(Rucoa::Definitions::ModuleDefinition)
def call
return [] unless @node.is_a?(Nodes::ModuleNode)

[
Definitions::ModuleDefinition.new(
fully_qualified_name: @node.fully_qualified_name,
source_path: @node.location.expression.source_buffer.name
)
]
end
end

class MethodDefinitionGenerator < Base
# @example returns method definition for def node
# definitions = Rucoa::Source.new(
# content: <<~RUBY,
# def foo
# end
# RUBY
# uri: '/path/to/foo.rb'
# ).definitions
# expect(definitions[0]).to be_a(Rucoa::Definitions::MethodDefinition)
# @example returns method definition for defs node
# definitions = Rucoa::Source.new(
# content: <<~RUBY,
# def self.foo
# end
# RUBY
# uri: '/path/to/foo.rb'
# ).definitions
# expect(definitions[0]).to be_a(Rucoa::Definitions::MethodDefinition)
def call
return [] unless @node.is_a?(Nodes::DefNode) || @node.is_a?(Nodes::DefsNode)

[
Definitions::MethodDefinition.new(
description: docstring_parser.to_docstring.to_s,
kind: @node.singleton? ? :singleton : :instance,
method_name: @node.name,
namespace: @node.namespace,
source_path: @node.location.expression.source_buffer.name,
types: return_types.map do |type|
Types::MethodType.new(
parameters_string: '', # TODO
return_type: type
)
end
)
]
end
end

class AttributeReaderDefinitionGenerator < Base
READER_METHOD_NAMES = ::Set[
'attr_accessor',
'attr_reader'
].freeze

# @example returns method definition for attr_reader node
# definitions = Rucoa::Source.new(
# content: <<~RUBY,
# class Foo
# attr_reader :bar
# end
# RUBY
# uri: '/path/to/foo.rb'
# ).definitions
# expect(definitions[1]).to be_a(Rucoa::Definitions::MethodDefinition)
# @example returns method definition for attr_accessor node
# definitions = Rucoa::Source.new(
# content: <<~RUBY,
# class Foo
# attr_accessor :bar
# end
# RUBY
# uri: '/path/to/foo.rb'
# ).definitions
# expect(definitions.map(&:fully_qualified_name)).to eq(
# %w[
# Foo
# Foo#bar
# Foo#bar=
# ]
# )
def call
return [] unless @node.is_a?(Nodes::SendNode) && READER_METHOD_NAMES.include?(@node.name)

@node.arguments.map do |argument|
Definitions::MethodDefinition.new(
description: docstring_parser.to_docstring.to_s,
kind: :instance,
method_name: argument.value.to_s,
namespace: @node.namespace,
source_path: @node.location.expression.source_buffer.name,
types: return_types.map do |type|
Types::MethodType.new(
parameters_string: '', # TODO
return_type: type
)
end
)
end
end
end

class AttributeWriterDefinitionGenerator < Base
WRITER_METHOD_NAMES = ::Set[
'attr_accessor',
'attr_writer'
].freeze

# @example returns method definition for attr_writer node
# definitions = Rucoa::Source.new(
# content: <<~RUBY,
# class Foo
# attr_writer :bar
# end
# RUBY
# uri: '/path/to/foo.rb'
# ).definitions
# expect(definitions[1]).to be_a(Rucoa::Definitions::MethodDefinition)
# @example returns method definition for attr_accessor node
# definitions = Rucoa::Source.new(
# content: <<~RUBY,
# class Foo
# attr_accessor :bar
# end
# RUBY
# uri: '/path/to/foo.rb'
# ).definitions
# expect(definitions.map(&:fully_qualified_name)).to eq(
# %w[
# Foo
# Foo#bar
# Foo#bar=
# ]
# )
def call
return [] unless @node.is_a?(Nodes::SendNode) && WRITER_METHOD_NAMES.include?(@node.name)

@node.arguments.map do |argument|
Definitions::MethodDefinition.new(
description: docstring_parser.to_docstring.to_s,
kind: :instance,
method_name: "#{argument.value}=",
namespace: @node.namespace,
source_path: @node.location.expression.source_buffer.name,
types: return_types.map do |type|
Types::MethodType.new(
parameters_string: 'value', # TODO
return_type: type
)
end
)
end
end
end
end
end
end
end