module Ameba::Rule::Lint
  # A rule that disallows useless assignments.
  #
  # For example, this is considered invalid:
  #
  # ```
  # def method
  #   var = 1
  #   do_something
  # end
  # ```
  #
  # And has to be written as the following:
  #
  # ```
  # def method
  #   var = 1
  #   do_something(var)
  # end
  # ```
  #
  # YAML configuration example:
  #
  # ```
  # Lint/UselessAssign:
  #   Enabled: true
  # ```
  class UselessAssign < Base
    properties do
      since_version "0.6.0"
      description "Disallows useless variable assignments"
    end

    MSG = "Useless assignment to variable `%s`"

    def test(source)
      UselessAssignScopeVisitor.new(self, source)
    end

    def test(source, node, scope : AST::Scope)
      return if scope.lib_def?(check_outer_scopes: true)

      analyzer = AST::LivenessAnalyzer.new(scope)
      dead_stores = analyzer.dead_stores

      dead_stores.each do |assign|
        var = assign.variable

        next if var.special? || var.ignored? || var.used_in_macro? || var.captured_by_block?
        next if referenced_in_inner_scope?(scope, var)

        report_issue(source, assign, var)
      end
    end

    private def referenced_in_inner_scope?(scope, var)
      scope.inner_scopes.any?(&.references?(var))
    end

    private def report_issue(source, assign, var)
      case target_node = assign.target_node
      when Crystal::TypeDeclaration
        issue_for(target_node.var, MSG % var.name)
      else
        issue_for(target_node, MSG % var.name)
      end
    end
  end

  private class UselessAssignScopeVisitor < AST::ScopeVisitor
    getter? in_call_args = false

    private def in_call_args(value = true, &)
      prev_value = @in_call_args
      begin
        @in_call_args = value
        yield
      ensure
        @in_call_args = prev_value
      end
    end

    def visit(node : Crystal::Def)
      return super unless node.name == "->"

      in_call_args(false) do
        node.accept_children(self)
      end
      false
    end

    def visit(node : Crystal::Block)
      super

      in_call_args(false) do
        node.accept_children(self)
      end
      false
    end

    def visit(node : Crystal::Call)
      return false unless super

      node.obj.try(&.accept(self))
      in_call_args do
        node.args.each(&.accept(self))
        node.named_args.try(&.each(&.accept(self)))
      end
      node.block_arg.try(&.accept(self))
      node.block.try(&.accept(self))

      false
    end

    def visit(node : Crystal::TypeDeclaration)
      super unless in_call_args?
    end

    private def on_assign_end(target, node)
      super unless in_call_args?
    end
  end
end
