lib/reek/source/source_code.rb
# frozen_string_literal: true
require_relative '../cli/silencer'
# Silence Parser's warnings about Ruby micro version differences
Reek::CLI::Silencer.silently { require 'parser/current' }
require_relative '../tree_dresser'
require_relative '../ast/node'
require_relative '../ast/builder'
# Opt in to new way of representing lambdas
Reek::AST::Builder.emit_lambda = true
module Reek
module Source
#
# A +SourceCode+ object represents a chunk of Ruby source code.
#
class SourceCode
IO_IDENTIFIER = 'STDIN'
STRING_IDENTIFIER = 'string'
# Initializer.
#
# @param source [File|Pathname|IO|String] Ruby source code
# @param origin [String] Origin of the source code. Will be determined
# automatically if left blank.
# @param parser the parser to use for generating AST's out of the given code
def initialize(source:, origin: nil, parser: self.class.default_parser)
@origin = origin
@parser = parser
@source = source
end
# Initializes an instance of SourceCode given a source.
# This source can come via several different ways:
# - from Files or Pathnames a la `reek lib/reek/`
# - from IO (STDIN) a la `echo "class Foo; end" | reek`
# - from String via our rspec matchers a la `expect("class Foo; end").to reek`
# - from an existing SourceCode object. This is passed through unchanged
#
# @param source [SourceCode|File|Pathname|IO|String] the given source
# @param origin [String|nil]
#
# @return an instance of SourceCode
def self.from(source, origin: nil)
case source
when self then source
else new(source: source, origin: origin)
end
end
def syntax_tree
@syntax_tree ||= parse
end
def self.default_parser
Parser::CurrentRuby.new(AST::Builder.new).tap do |parser|
diagnostics = parser.diagnostics
diagnostics.all_errors_are_fatal = true
diagnostics.ignore_warnings = true
end
end
def origin
@origin ||=
case source
when File then source.path
when IO then IO_IDENTIFIER
when Pathname then source.to_s
when String then STRING_IDENTIFIER
end
end
private
def code
@code ||=
case source
when File, Pathname then source.read
when IO then source.readlines.join
when String then source
end.force_encoding(Encoding::UTF_8)
end
attr_reader :parser, :source
# Parses the given code into an AST and associates the source code comments with it.
# This AST is then traversed by a TreeDresser which adorns the nodes in the AST
# with our SexpExtensions.
# Finally this AST is returned where each node is an anonymous subclass of Reek::AST::Node
#
# Given this @code:
#
# # comment about C
# class C
# def m
# puts 'nada'
# end
# end
#
# this method would return something that looks like
#
# (class
# (const nil :C) nil
# (def :m
# (args)
# (send nil :puts
# (str "nada"))))
#
# where each node is possibly adorned with our SexpExtensions (see ast/ast_node_class_map
# and ast/sexp_extensions for details).
#
# @return Reek::AST::Node the AST presentation for the given code
def parse
buffer = Parser::Source::Buffer.new(origin, 1)
buffer.source = code
ast, comments = parser.parse_with_comments(buffer)
# See https://whitequark.github.io/parser/Parser/Source/Comment/Associator.html
comment_map = Parser::Source::Comment.associate(ast, comments)
TreeDresser.new.dress(ast, comment_map) || AST::Node.new(:empty)
end
end
end
end