troessner/reek

View on GitHub
lib/reek/source/source_code.rb

Summary

Maintainability
A
0 mins
Test Coverage
# 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