Skip to content

Commit

Permalink
Refactor SpecWalker - extract sub-classes into own files
Browse files Browse the repository at this point in the history
  • Loading branch information
lekemula committed May 19, 2024
1 parent daf2cd6 commit f33e74e
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 144 deletions.
147 changes: 3 additions & 144 deletions lib/solargraph/rspec/spec_walker.rb
Original file line number Diff line number Diff line change
@@ -1,154 +1,13 @@
# frozen_string_literal: true

require_relative 'walker'
require_relative 'spec_walker/node_types'
require_relative 'spec_walker/full_constant_name'
require_relative 'spec_walker/rspec_context_namespace'

module Solargraph
module Rspec
class SpecWalker
class NodeTypes
# @param ast [RubyVM::AbstractSyntaxTree::Node]
# @return [Boolean]
def self.a_block?(ast)
return false unless ast.is_a?(RubyVM::AbstractSyntaxTree::Node)

%i[ITER LAMBDA].include?(ast.type)
end

# @param ast [RubyVM::AbstractSyntaxTree::Node]
# @return [Boolean]
def self.a_context_block?(block_ast)
Solargraph::Rspec::CONTEXT_METHODS.include?(method_with_block_name(block_ast))
end

# @param ast [RubyVM::AbstractSyntaxTree::Node]
# @return [Boolean]
def self.a_subject_block?(block_ast)
Solargraph::Rspec::SUBJECT_METHODS.include?(method_with_block_name(block_ast))
end

# @param ast [RubyVM::AbstractSyntaxTree::Node]
# @return [Boolean]
def self.a_example_block?(block_ast)
Solargraph::Rspec::EXAMPLE_METHODS.include?(method_with_block_name(block_ast))
end

# @param ast [RubyVM::AbstractSyntaxTree::Node]
# @param config [Config]
# @return [Boolean]
def self.a_let_block?(block_ast, config)
config.let_methods.map(&:to_s).include?(method_with_block_name(block_ast))
end

# @param ast [RubyVM::AbstractSyntaxTree::Node]
# @return [Boolean]
def self.a_hook_block?(block_ast)
Solargraph::Rspec::HOOK_METHODS.include?(method_with_block_name(block_ast))
end

def self.a_constant?(ast)
%i[CONST COLON2].include?(ast.type)
end

# @param block_ast [RubyVM::AbstractSyntaxTree::Node]
# @return [String, nil]
def self.method_with_block_name(block_ast)
return nil unless a_block?(block_ast)

method_call = %i[CALL FCALL].include?(block_ast.children[0].type)
return nil unless method_call

block_ast.children[0].children.select { |child| child.is_a?(Symbol) }.first&.to_s
end

# @param block_ast [RubyVM::AbstractSyntaxTree::Node]
# @return [RubyVM::AbstractSyntaxTree::Node]
def self.context_description_node(block_ast)
case block_ast.children[0].type
when :CALL # RSpec.describe "something" do end
block_ast.children[0].children[2].children[0]
when :FCALL # describe "something" do end
block_ast.children[0].children[1].children[0]
end
end

# @param block_ast [RubyVM::AbstractSyntaxTree::Node]
# @return [String]
def self.let_method_name(block_ast)
block_ast.children[0].children[1]&.children&.[](0)&.children&.[](0)&.to_s
end
end

class FullConstantName
class << self
# @param ast [RubyVM::AbstractSyntaxTree::Node]
# @return [String]
def from_ast(ast)
raise 'Node is not a constant' unless NodeTypes.a_constant?(ast)

if ast.type == :CONST
ast.children[0].to_s
elsif ast.type == :COLON2
name = ast.children[1].to_s
"#{from_ast(ast.children[0])}::#{name}"
end
end

def from_context_block_ast(block_ast)
ast = NodeTypes.context_description_node(block_ast)
from_ast(ast)
end
end
end

class RspecContextNamespace
class << self
# @param block_ast [RubyVM::AbstractSyntaxTree::Node]
# @return [String, nil]
def from_block_ast(block_ast)
return unless block_ast.is_a?(RubyVM::AbstractSyntaxTree::Node)

ast = NodeTypes.context_description_node(block_ast)
if ast.type == :STR
string_to_const_name(ast)
elsif NodeTypes.a_constant?(ast)
FullConstantName.from_ast(ast).gsub('::', '')
else
Solargraph.logger.warn "[RSpec] Unexpected AST type #{ast.type}"
nil
end
end

private

# @see https://github.com/rspec/rspec-core/blob/1eeadce5aa7137ead054783c31ff35cbfe9d07cc/lib/rspec/core/example_group.rb#L862
# @param ast [Parser::AST::Node]
# @return [String]
def string_to_const_name(string_ast)
return unless string_ast.type == :STR

name = string_ast.children[0]
return 'Anonymous'.dup if name.empty?

# Convert to CamelCase.
name = +" #{name}"
name.gsub!(/[^0-9a-zA-Z]+([0-9a-zA-Z])/) do
match = ::Regexp.last_match[1]
match.upcase!
match
end

name.lstrip! # Remove leading whitespace
name.gsub!(/\W/, '') # JRuby, RBX and others don't like non-ascii in const names

# Ruby requires first const letter to be A-Z. Use `Nested`
# as necessary to enforce that.
name.gsub!(/\A([^A-Z]|\z)/, 'Nested\1')

name
end
end
end

# @param source_map [SourceMap]
# @param config [Config]
def initialize(source_map:, config:)
Expand Down
29 changes: 29 additions & 0 deletions lib/solargraph/rspec/spec_walker/full_constant_name.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# frozen_string_literal: true

module Solargraph
module Rspec
class SpecWalker
class FullConstantName
class << self
# @param ast [RubyVM::AbstractSyntaxTree::Node]
# @return [String]
def from_ast(ast)
raise 'Node is not a constant' unless NodeTypes.a_constant?(ast)

if ast.type == :CONST
ast.children[0].to_s
elsif ast.type == :COLON2
name = ast.children[1].to_s
"#{from_ast(ast.children[0])}::#{name}"
end
end

def from_context_block_ast(block_ast)
ast = NodeTypes.context_description_node(block_ast)
from_ast(ast)
end
end
end
end
end
end
80 changes: 80 additions & 0 deletions lib/solargraph/rspec/spec_walker/node_types.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# frozen_string_literal: true

module Solargraph
module Rspec
class SpecWalker
class NodeTypes
# @param ast [RubyVM::AbstractSyntaxTree::Node]
# @return [Boolean]
def self.a_block?(ast)
return false unless ast.is_a?(RubyVM::AbstractSyntaxTree::Node)

%i[ITER LAMBDA].include?(ast.type)
end

# @param ast [RubyVM::AbstractSyntaxTree::Node]
# @return [Boolean]
def self.a_context_block?(block_ast)
Solargraph::Rspec::CONTEXT_METHODS.include?(method_with_block_name(block_ast))
end

# @param ast [RubyVM::AbstractSyntaxTree::Node]
# @return [Boolean]
def self.a_subject_block?(block_ast)
Solargraph::Rspec::SUBJECT_METHODS.include?(method_with_block_name(block_ast))
end

# @param ast [RubyVM::AbstractSyntaxTree::Node]
# @return [Boolean]
def self.a_example_block?(block_ast)
Solargraph::Rspec::EXAMPLE_METHODS.include?(method_with_block_name(block_ast))
end

# @param ast [RubyVM::AbstractSyntaxTree::Node]
# @param config [Config]
# @return [Boolean]
def self.a_let_block?(block_ast, config)
config.let_methods.map(&:to_s).include?(method_with_block_name(block_ast))
end

# @param ast [RubyVM::AbstractSyntaxTree::Node]
# @return [Boolean]
def self.a_hook_block?(block_ast)
Solargraph::Rspec::HOOK_METHODS.include?(method_with_block_name(block_ast))
end

def self.a_constant?(ast)
%i[CONST COLON2].include?(ast.type)
end

# @param block_ast [RubyVM::AbstractSyntaxTree::Node]
# @return [String, nil]
def self.method_with_block_name(block_ast)
return nil unless a_block?(block_ast)

method_call = %i[CALL FCALL].include?(block_ast.children[0].type)
return nil unless method_call

block_ast.children[0].children.select { |child| child.is_a?(Symbol) }.first&.to_s
end

# @param block_ast [RubyVM::AbstractSyntaxTree::Node]
# @return [RubyVM::AbstractSyntaxTree::Node]
def self.context_description_node(block_ast)
case block_ast.children[0].type
when :CALL # RSpec.describe "something" do end
block_ast.children[0].children[2].children[0]
when :FCALL # describe "something" do end
block_ast.children[0].children[1].children[0]
end
end

# @param block_ast [RubyVM::AbstractSyntaxTree::Node]
# @return [String]
def self.let_method_name(block_ast)
block_ast.children[0].children[1]&.children&.[](0)&.children&.[](0)&.to_s
end
end
end
end
end
56 changes: 56 additions & 0 deletions lib/solargraph/rspec/spec_walker/rspec_context_namespace.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# frozen_string_literal: true

module Solargraph
module Rspec
class SpecWalker
class RspecContextNamespace
class << self
# @param block_ast [RubyVM::AbstractSyntaxTree::Node]
# @return [String, nil]
def from_block_ast(block_ast)
return unless block_ast.is_a?(RubyVM::AbstractSyntaxTree::Node)

ast = NodeTypes.context_description_node(block_ast)
if ast.type == :STR
string_to_const_name(ast)
elsif NodeTypes.a_constant?(ast)
FullConstantName.from_ast(ast).gsub('::', '')
else
Solargraph.logger.warn "[RSpec] Unexpected AST type #{ast.type}"
nil
end
end

private

# @see https://github.com/rspec/rspec-core/blob/1eeadce5aa7137ead054783c31ff35cbfe9d07cc/lib/rspec/core/example_group.rb#L862
# @param ast [Parser::AST::Node]
# @return [String]
def string_to_const_name(string_ast)
return unless string_ast.type == :STR

name = string_ast.children[0]
return 'Anonymous'.dup if name.empty?

# Convert to CamelCase.
name = +" #{name}"
name.gsub!(/[^0-9a-zA-Z]+([0-9a-zA-Z])/) do
match = ::Regexp.last_match[1]
match.upcase!
match
end

name.lstrip! # Remove leading whitespace
name.gsub!(/\W/, '') # JRuby, RBX and others don't like non-ascii in const names

# Ruby requires first const letter to be A-Z. Use `Nested`
# as necessary to enforce that.
name.gsub!(/\A([^A-Z]|\z)/, 'Nested\1')

name
end
end
end
end
end
end

0 comments on commit f33e74e

Please sign in to comment.