yast/rubocop-yast

View on GitHub
lib/rubocop/yast/track_variable_scope.rb

Summary

Maintainability
A
0 mins
Test Coverage
# encoding: utf-8

require "rubocop/yast/niceness"
require "rubocop/yast/variable_scope"

module RuboCop
  module Yast
    # This module tracks variable usage
    module TrackVariableScope
      include Niceness

      def scopes
        @scopes ||= VariableScopeStack.new
      end

      # currently visible scope
      def scope
        scopes.innermost
      end

      def with_new_scope_rescuing_oops(node, &block)
        scopes.with_new do
          block.call if block_given?
        end
      rescue => e
        oops(node, e)
      end

      def on_def(node)
        name, _, _ = *node
        RuboCop::Yast.logger.debug "ONDEF #{name}"
        RuboCop::Yast.logger.debug "CUR SCOPE #{scope.inspect}"
        RuboCop::Yast.backtrace skip_frames: 50 if $DEBUG

        with_new_scope_rescuing_oops(node) { super }
      end

      def on_defs(node)
        with_new_scope_rescuing_oops(node) { super }
      end

      def on_module(node)
        with_new_scope_rescuing_oops(node) { super }
      end

      def on_class(node)
        with_new_scope_rescuing_oops(node) { super }
      end

      def on_sclass(node)
        with_new_scope_rescuing_oops(node) { super }
      end

      def on_if(node)
        cond, then_body, else_body = *node
        process(cond)

        scopes.with_copy do
          process(then_body)
        end

        scopes.with_copy do
          process(else_body)
        end

        # clean slate
        scope.clear

        node
      end

      # def on_unless
      # Does not exist.
      # `unless` is parsed as an `if` with then_body and else_body swapped.
      # Compare with `while` and `until` which cannot do that and thus need
      # distinct node types.
      # end

      def on_case(node)
        expr, *cases = *node
        process(expr)

        cases.each do |case_|
          scopes.with_copy do
            process(case_)
          end
        end

        # clean slate
        scope.clear

        node
      end

      def on_lvasgn(node)
        super
        name, value = * node
        return if value.nil? # and-asgn, or-asgn, resbody do this
        scope[name].nice = nice(value)
        node
      end

      def on_and_asgn(node)
        super
        var, value = *node
        bool_op_asgn(var, value, :and)
        node
      end

      def on_or_asgn(node)
        super
        var, value = *node
        bool_op_asgn(var, value, :or)
        node
      end

      def on_block(node)
        # ignore body, clean slate
        scope.clear
        node
      end
      alias_method :on_for, :on_block

      def on_while(node)
        # ignore both condition and body,
        # with a simplistic scope we cannot handle them

        # clean slate
        scope.clear
        node
      end
      alias_method :on_until, :on_while

      # Exceptions:
      # `raise` is an ordinary :send for the parser

      def on_rescue(node)
        # (:rescue, begin-block, resbody..., else-block-or-nil)
        begin_body, *rescue_bodies, else_body = *node

        if rescue_bodies.any? { |r| descendant?(r, :retry) }
          # do not process if a retry may cause a loop
          return node
        end

        process(begin_body)
        process(else_body)
        rescue_bodies.each do |r|
          process(r)
        end

        node
      end

      def on_resbody(node)
        # How it is parsed:
        # (:resbody, exception-types-or-nil, exception-variable-or-nil, body)
        # exception-types is an :array
        # exception-variable is a (:lvasgn, name), without a value

        # A rescue means that *some* previous code was skipped.
        # We know nothing. We could process the resbodies individually,
        # and join begin-block with else-block, but it is little worth
        # because they will contain few zombies.
        scope.clear
        super
      end

      def on_ensure(node)
        # (:ensure, guarded-code, ensuring-code)
        # guarded-code may be a :rescue or not

        scope.clear
        node
      end

      private

      # does node have a particular type as a descendant?
      def descendant?(node, descendant_type)
        on_node(descendant_type, node) do
          return true           # short circuit, returns from the method
        end
        false
      end

      def oops(node, exception)
        puts "Node exception @ #{node.loc.expression}"
        puts "Offending node: #{node.inspect}"
        raise exception unless exception.is_a?(TooComplexToTranslateError)
      end

      def bool_op_asgn(var, value, op)
        return if var.type != :lvasgn
        name = var.children[0]

        case op
        when :and
          scope[name].nice &&= nice(value)
        when :or
          scope[name].nice ||= nice(value)
        else
          raise "Unknown operator: #{op}"
        end
      end
    end
  end
end