# frozen_string_literal: true

return if RUBY_VERSION < "3.1"

require_relative "../test_helper"

module Prism
  class WarningsTest < TestCase
    def test_ambiguous_uminus
      assert_warning("a -b", "ambiguous first argument")
    end

    def test_ambiguous_uplus
      assert_warning("a +b", "ambiguous first argument")
    end

    def test_ambiguous_ustar
      assert_warning("a *b", "argument prefix")
    end

    def test_ambiguous_regexp
      assert_warning("a /b/", "wrap regexp in parentheses", compare: false, version: "4.0")
      refute_warning("a /b/", compare: false, version: "4.1")
    end

    def test_ambiguous_ampersand
      assert_warning("a &b", "argument prefix")
      assert_warning("a &:+", "argument prefix")

      refute_warning("a &:b")
      refute_warning("a &:'b'")
      refute_warning("a &:\"b\"")
    end

    def test_binary_operator
      [
        [:**, "argument prefix"],
        [:*, "argument prefix"],
        [:<<, "here document"],
        [:&, "argument prefix"],
        [:+, "unary operator"],
        [:-, "unary operator"],
        [:/, "regexp literal"],
        [:%, "string literal"]
      ].each do |(operator, warning)|
        assert_warning("puts 1 #{operator}0", warning)
        assert_warning("puts :a #{operator}0", warning)
        assert_warning("m = 1; puts m #{operator}0", warning)
      end
    end

    def test_equal_in_conditional
      assert_warning("if a = 1; end; a = a", "should be ==")
    end

    def test_dot_dot_dot_eol
      assert_warning("_ = foo...", "... at EOL")
      assert_warning("def foo(...) = bar ...", "... at EOL")

      assert_warning("_ = foo... #", "... at EOL")
      assert_warning("_ = foo... \t\v\f\n", "... at EOL")

      refute_warning("p foo...bar")
      refute_warning("p foo...      bar")
    end

    def test_END_in_method
      assert_warning("def foo; END {}; end", "END in method")
    end

    def test_duplicated_hash_key
      assert_warning("{ a: 1, a: 2 }", "duplicated and overwritten")
      assert_warning("{ a: 1, **{ a: 2 } }", "duplicated and overwritten")
    end

    def test_duplicated_when_clause
      assert_warning("case 1; when 1, 1; end", "when' clause")
    end

    # Two literals with the same bytes are the same key only when they also end
    # up in the same encoding. An escape that locks the encoding to UTF-8 sets
    # FORCED_UTF8 whether or not the file is already UTF-8, so the flag has to
    # be resolved against the source encoding before literals are compared.
    #
    # In a binary file the raw bytes stay BINARY while the escape resolves to
    # UTF-8, so these are genuinely different keys and must not warn.
    def test_duplicated_literals_distinct_encodings
      binary = "# -*- encoding: ascii-8bit -*-\n"

      refute_warning("#{binary}{\"\xC3\xA9\" => 1, \"\\u00E9\" => 2}".b)
      refute_warning("#{binary}{:\"\xC3\xA9\" => 1, :\"\\u00E9\" => 2}".b)
      refute_warning("#{binary}case foo\nwhen \"\xC3\xA9\"\nwhen \"\\u00E9\"\nend".b)

      # Written as two escapes rather than raw bytes, the pair still resolves to
      # BINARY and UTF-8 respectively, and the hash really does hold two keys.
      refute_warning("#{binary}{\"\\xC3\\xA9\" => 1, \"\\u00E9\" => 2}".b)
      refute_warning("#{binary}{:\"\\xC3\\xA9\" => 1, :\"\\u00E9\" => 2}".b)

      # A US-ASCII source is the only one that emits FORCED_BINARY, and BINARY
      # is not the source encoding there, so the pair stays distinct.
      us_ascii = "# -*- encoding: us-ascii -*-\n"

      refute_warning("#{us_ascii}{\"\\xC3\\xA9\" => 1, \"\\u00E9\" => 2}")
      refute_warning("#{us_ascii}{:\"\\xC3\\xA9\" => 1, :\"\\u00E9\" => 2}")
    end

    # The mirror image: in a UTF-8 file the escape resolves to the very same
    # string, so the pair is a duplicate and still has to be reported.
    def test_duplicated_literals_matching_encodings
      assert_warning("{\"\xC3\xA9\" => 1, \"\\u00E9\" => 2}", "duplicated and overwritten")
      assert_warning("{:\"\xC3\xA9\" => 1, :\"\\u00E9\" => 2}", "duplicated and overwritten")
      assert_warning("case foo\nwhen \"\xC3\xA9\"\nwhen \"\\u00E9\"\nend", "when' clause")

      # Same bytes and same encoding are duplicates in a binary file too.
      binary = "# -*- encoding: ascii-8bit -*-\n"

      assert_warning("#{binary}{\"\xC3\xA9\" => 1, \"\xC3\xA9\" => 2}".b, "duplicated and overwritten")
      assert_warning("#{binary}{\"\\u00E9\" => 1, \"\\u00E9\" => 2}".b, "duplicated and overwritten")
    end

    # Hash patterns raise rather than warn, and resolve encodings the same way.
    def test_duplicated_pattern_keys_resolve_encoding
      binary = "# -*- encoding: ascii-8bit -*-\n"
      distinct = "#{binary}case x\nin {\"\xC3\xA9\": a, \"\\u00E9\": b}\nend".b
      duplicate = "case x\nin {\"\xC3\xA9\": a, \"\\u00E9\": b}\nend"

      assert_empty Prism.parse(distinct).errors.map(&:message)
      assert_equal ["duplicated key name"], Prism.parse(duplicate).errors.map(&:message)
    end

    def test_float_out_of_range
      assert_warning("_ = 1.0e100000", "out of range")
    end

    def test_indentation_mismatch
      assert_warning("if true\n  end", "mismatched indentations at 'end' with 'if'")
      assert_warning("if true\n  elsif true\nend", "mismatched indentations at 'elsif' with 'if'")
      assert_warning("if true\n  else\nend", "mismatched indentations at 'else' with 'if'", "mismatched indentations at 'end' with 'else'")

      assert_warning("unless true\n  end", "mismatched indentations at 'end' with 'unless'")
      assert_warning("unless true\n  else\nend", "mismatched indentations at 'else' with 'unless'", "mismatched indentations at 'end' with 'else'")

      assert_warning("while true\n  end", "mismatched indentations at 'end' with 'while'")
      assert_warning("until true\n  end", "mismatched indentations at 'end' with 'until'")

      assert_warning("begin\n  end", "mismatched indentations at 'end' with 'begin'")
      assert_warning("begin\n  rescue\nend", "mismatched indentations at 'rescue' with 'begin'")
      assert_warning("begin\n  ensure\nend", "mismatched indentations at 'ensure' with 'begin'")
      assert_warning("begin\nrescue\n  else\nend", "mismatched indentations at 'else' with 'begin'", "mismatched indentations at 'end' with 'else'")
      assert_warning("begin\n  rescue\n    ensure\n      end", "mismatched indentations at 'rescue' with 'begin'", "mismatched indentations at 'ensure' with 'begin'", "mismatched indentations at 'end' with 'begin'");

      assert_warning("def foo\n  end", "mismatched indentations at 'end' with 'def'")
      assert_warning("def foo\n  rescue\nend", "mismatched indentations at 'rescue' with 'def'")
      assert_warning("def foo\n  ensure\nend", "mismatched indentations at 'ensure' with 'def'")
      assert_warning("def foo\nrescue\n  else\nend", "mismatched indentations at 'else' with 'def'", "mismatched indentations at 'end' with 'else'")
      assert_warning("def foo\n  rescue\n    ensure\n      end", "mismatched indentations at 'rescue' with 'def'", "mismatched indentations at 'ensure' with 'def'", "mismatched indentations at 'end' with 'def'");

      assert_warning("class Foo\n  end", "mismatched indentations at 'end' with 'class'")
      assert_warning("class Foo\n  rescue\nend", "mismatched indentations at 'rescue' with 'class'")
      assert_warning("class Foo\n  ensure\nend", "mismatched indentations at 'ensure' with 'class'")
      assert_warning("class Foo\nrescue\n  else\nend", "mismatched indentations at 'else' with 'class'", "mismatched indentations at 'end' with 'else'")
      assert_warning("class Foo\n  rescue\n    ensure\n      end", "mismatched indentations at 'rescue' with 'class'", "mismatched indentations at 'ensure' with 'class'", "mismatched indentations at 'end' with 'class'");

      assert_warning("module Foo\n  end", "mismatched indentations at 'end' with 'module'")
      assert_warning("module Foo\n  rescue\nend", "mismatched indentations at 'rescue' with 'module'")
      assert_warning("module Foo\n  ensure\nend", "mismatched indentations at 'ensure' with 'module'")
      assert_warning("module Foo\nrescue\n  else\nend", "mismatched indentations at 'else' with 'module'", "mismatched indentations at 'end' with 'else'")
      assert_warning("module Foo\n  rescue\n    ensure\n      end", "mismatched indentations at 'rescue' with 'module'", "mismatched indentations at 'ensure' with 'module'", "mismatched indentations at 'end' with 'module'");

      assert_warning("class << foo\n  end", "mismatched indentations at 'end' with 'class'")
      assert_warning("class << foo\n  rescue\nend", "mismatched indentations at 'rescue' with 'class'")
      assert_warning("class << foo\n  ensure\nend", "mismatched indentations at 'ensure' with 'class'")
      assert_warning("class << foo\nrescue\n  else\nend", "mismatched indentations at 'else' with 'class'", "mismatched indentations at 'end' with 'else'")
      assert_warning("class << foo\n  rescue\n    ensure\n      end", "mismatched indentations at 'rescue' with 'class'", "mismatched indentations at 'ensure' with 'class'", "mismatched indentations at 'end' with 'class'");

      assert_warning("case 1; when 2\n  end", "mismatched indentations at 'end' with 'case'")
      assert_warning("case 1; in 2\n  end", "mismatched indentations at 'end' with 'case'")

      assert_warning("  case 1\nwhen 2\n  end", "mismatched indentations at 'when' with 'case'")
      refute_warning("case 1\n  when 2\n    when 3\nend") # case/when allows more indentation

      assert_warning("-> {\n  }", "mismatched indentations at '}' with '->'")
      assert_warning("-> do\n  end", "mismatched indentations at 'end' with '->'")
      assert_warning("-> do\n  rescue\nend", "mismatched indentations at 'rescue' with '->'")
      assert_warning("-> do\n  ensure\nend", "mismatched indentations at 'ensure' with '->'")
      assert_warning("-> do\nrescue\n  else\nend", "mismatched indentations at 'else' with '->'", "mismatched indentations at 'end' with 'else'")
      assert_warning("-> do\n  rescue\n    ensure\n      end", "mismatched indentations at 'rescue' with '->'", "mismatched indentations at 'ensure' with '->'", "mismatched indentations at 'end' with '->'");
      assert_warning("foo do\nrescue\n  else\nend", "mismatched indentations at 'end' with 'else'")

      refute_warning("class Foo; end") # same line
      refute_warning("; class Foo\nend") # non whitespace on opening line
      refute_warning("\tclass Foo\n        end") # tab stop matches space
      refute_warning("    \tclass Foo\n        end") # tab stop matches space
    end

    def test_integer_in_flip_flop
      assert_warning("1 if 2..foo", "integer")
    end

    def test_literal_in_conditionals
      sources = [
        "if (a = 2); a; end",
        "if ($a = 2); end",
        "if (@a = 2); end",
        "if a; elsif b = 2; b end",
        "unless (a = 2); a; end",
        "unless ($a = 2); end",
        "unless (@a = 2); end",
        "while (a = 2); a; end",
        "while ($a = 2); end",
        "while (@a = 2); end",
        "until (a = 2); a; end",
        "until ($a = 2); end",
        "until (@a = 2); end",
        "foo if (a, b = 2); [a, b]",
        "foo if a = 2 and a",
        "(@foo = 1) ? a : b",
        "!(a = 2) and a",
        "not a = 2 and a"
      ]

      if RUBY_VERSION >= "3.3"
        sources.push(
          "if (@@a = 2); end",
          "unless (@@a = 2); end",
          "while (@@a = 2); end",
          "until (@@a = 2); end"
        )
      end

      sources.each do |source|
        assert_warning(source, "= literal' in conditional, should be ==")
      end
    end

    def test_keyword_eol
      assert_warning("if\ntrue; end", "end of line")
      assert_warning("if true\nelsif\nfalse; end", "end of line")
    end

    def test_numbered_reference
      assert_warning("_ = _ = $999999999999999999999", "too big for a number variable, always nil")
    end

    def test_shareable_constant_value
      assert_warning("foo # shareable_constant_value: none", "ignored")
      assert_warning("\v  # shareable_constant_value: none", "ignored")

      refute_warning("# shareable_constant_value: none")
      refute_warning("    # shareable_constant_value: none")
      refute_warning("\t\t# shareable_constant_value: none")
    end

    def test_string_in_predicate
      assert_warning("if 'foo'; end", "string")
      assert_warning("if \"\#{foo}\"; end", "string")
      assert_warning("if __FILE__; end", "string")
    end

    def test_symbol_in_predicate
      assert_warning("if :foo; end", "symbol")
      assert_warning("if :\"\#{foo}\"; end", "symbol")
    end

    def test_literal_in_predicate
      assert_warning("if __LINE__; end", "literal")
      assert_warning("if __ENCODING__; end", "literal")
      assert_warning("if 1; end", "literal")
      assert_warning("if 1.0; end", "literal")
      assert_warning("if 1r; end", "literal")
      assert_warning("if 1i; end", "literal")
    end

    def test_regexp_in_predicate
      assert_warning("if /foo/; end", "regex")
      assert_warning("if /foo\#{bar}/; end", "regex")
    end

    def test_unused_local_variables
      assert_warning("foo = 1", "unused")

      refute_warning("foo = 1", compare: false, command_line: "e")
      refute_warning("foo = 1", compare: false, scopes: [[]])

      refute_warning("foo(bar = 1)")

      assert_warning("def foo; bar = 1; end", "unused")
      assert_warning("def foo; bar, = 1; end", "unused")

      refute_warning("def foo; bar &&= 1; end")
      refute_warning("def foo; bar ||= 1; end")
      refute_warning("def foo; bar += 1; end")

      refute_warning("def foo; bar = bar; end")
      refute_warning("def foo; bar = bar = 1; end")
      refute_warning("def foo; bar = (bar = 1); end")
      refute_warning("def foo; bar = begin; bar = 1; end; end")
      refute_warning("def foo; bar = (qux; bar = 1); end")
      refute_warning("def foo; bar, = bar = 1; end")
      refute_warning("def foo; bar, = 1, bar = 1; end")

      refute_warning("def foo(bar); end")
      refute_warning("def foo(bar = 1); end")
      refute_warning("def foo((bar)); end")
      refute_warning("def foo(*bar); end")
      refute_warning("def foo(*, bar); end")
      refute_warning("def foo(*, (bar)); end")
      refute_warning("def foo(bar:); end")
      refute_warning("def foo(**bar); end")
      refute_warning("def foo(&bar); end")
      refute_warning("->(bar) {}")
      refute_warning("->(; bar) {}", compare: false)

      refute_warning("def foo; bar = 1; tap { bar }; end")
      refute_warning("def foo; bar = 1; tap { baz = bar; baz }; end")

      refute_warning("def foo; bar = 1; end", line: -2, compare: false)
    end

    def test_unused_local_variable_or_assign_with_begin_node
      assert_warning(<<~RUBY, "assigned but unused variable - foo", compare: false)
        var ||= begin
          foo = bar
          baz
        end
      RUBY

      assert_warning(<<~RUBY, "assigned but unused variable - foo", compare: false)
        foo = false
        var ||= begin
          foo = true
          bar
        end
      RUBY
    end

    def test_void_statements
      assert_warning("foo = 1; foo", "a variable in void")
      assert_warning("@foo", "a variable in void")
      assert_warning("@@foo", "a variable in void")
      assert_warning("$foo", "a variable in void")
      assert_warning("$+", "a variable in void")
      assert_warning("$1", "a variable in void")

      assert_warning("self", "self in void")
      assert_warning("nil", "nil in void")
      assert_warning("true", "true in void")
      assert_warning("false", "false in void")

      assert_warning("1", "literal in void")
      assert_warning("1.0", "literal in void")
      assert_warning("1r", "literal in void")
      assert_warning("1i", "literal in void")
      assert_warning(":foo", "literal in void")
      assert_warning("\"foo\"", "literal in void")
      assert_warning("\"foo\#{1}\"", "literal in void")
      assert_warning("/foo/", "literal in void")
      assert_warning("/foo\#{1}/", "literal in void")

      assert_warning("Foo", "constant in void")
      assert_warning("::Foo", ":: in void")
      assert_warning("Foo::Bar", ":: in void")

      assert_warning("1..2", ".. in void")
      assert_warning("1..", ".. in void")
      assert_warning("..2", ".. in void")
      assert_warning("1...2", "... in void")
      assert_warning("1...;", "... in void")
      assert_warning("...2", "... in void")

      assert_warning("defined?(foo)", "defined? in void")

      assert_warning("1 + 1", "+ in void")
      assert_warning("1 - 1", "- in void")
      assert_warning("1 * 1", "* in void")
      assert_warning("1 / 1", "/ in void")
      assert_warning("1 % 1", "% in void")
      assert_warning("1 | 1", "| in void")
      assert_warning("1 ^ 1", "^ in void")
      assert_warning("1 & 1", "& in void")
      assert_warning("1 > 1", "> in void")
      assert_warning("1 < 1", "< in void")

      assert_warning("1 ** 1", "** in void")
      assert_warning("1 <= 1", "<= in void")
      assert_warning("1 >= 1", ">= in void")
      assert_warning("1 != 1", "!= in void")
      assert_warning("1 == 1", "== in void")
      assert_warning("1 <=> 1", "<=> in void")

      assert_warning("+foo", "+@ in void")
      assert_warning("-foo", "-@ in void")

      assert_warning("def foo; @bar; @baz; end", "variable in void")
      refute_warning("def foo; @bar; end")
      refute_warning("@foo", compare: false, scopes: [[]])
    end

    def test_unreachable_statement
      assert_warning("begin; rescue; retry; foo; end", "statement not reached")

      assert_warning("return; foo", "statement not reached")

      assert_warning("tap { break; foo }", "statement not reached")
      assert_warning("tap { break 1; foo }", "statement not reached")

      assert_warning("tap { next; foo }", "statement not reached")
      assert_warning("tap { next 1; foo }", "statement not reached")

      assert_warning("tap { redo; foo }", "statement not reached")
    end

    if windows?
      def test_shebang_ending_with_carriage_return
        refute_warning("#!ruby\r\np(123)\n", compare: false)
      end
    else
      def test_shebang_ending_with_carriage_return
        msg = "shebang line ending with \\r may cause problems"

        assert_warning(<<~RUBY, msg, compare: false, main_script: true)
          #!ruby\r
          p(123)
        RUBY

        assert_warning(<<~RUBY, msg, compare: false, main_script: true)
          #!ruby \r
          p(123)
        RUBY

        assert_warning(<<~RUBY, msg, compare: false, main_script: true)
          #!ruby -Eutf-8\r
          p(123)
        RUBY

        # Used with the `-x` object, to ignore the script up until the first
        # shebang that mentioned "ruby".
        assert_warning(<<~SCRIPT, msg, compare: false, main_script: true)
          #!/usr/bin/env bash
          # Some initial shell script or other content
          # that Ruby should ignore
          echo "This is shell script part"
          exit 0

          #! /usr/bin/env ruby -Eutf-8\r
          # Ruby script starts here
          puts "Hello from Ruby!"
        SCRIPT

        refute_warning("#ruby not_a_shebang\r\n", compare: false, main_script: true)

        # CRuby doesn't emit the warning if a malformed file only has `\r` and
        # not `\n`. https://bugs.ruby-lang.org/issues/20700.
        refute_warning("#!ruby\r", compare: false, main_script: true)
      end
    end

    def test_warnings_verbosity
      warning = Prism.parse("def foo; END { }; end").warnings.first
      assert_equal "END in method; use at_exit", warning.message
      assert_equal :default, warning.level

      warning = Prism.parse("foo +1").warnings.first
      assert_equal "ambiguous first argument; put parentheses or a space even after `+` operator", warning.message
      assert_equal :verbose, warning.level
    end

    private

    def assert_warning(source, *messages, compare: true, **options)
      warnings = Prism.parse(source, **options).warnings
      assert_equal messages.length, warnings.length, "Expected #{messages.length} warning(s) in #{source.inspect}, got #{warnings.map(&:message).inspect}"

      warnings.zip(messages).each do |warning, message|
        assert_include warning.message, message
      end

      if compare && defined?(RubyVM::AbstractSyntaxTree)
        stderr = capture_stderr { RubyVM::AbstractSyntaxTree.parse(source) }
        messages.each { |message| assert_include stderr, message }
      end
    end

    def refute_warning(source, compare: true, **options)
      assert_empty Prism.parse(source, **options).warnings

      if compare && defined?(RubyVM::AbstractSyntaxTree)
        assert_empty capture_stderr { RubyVM::AbstractSyntaxTree.parse(source) }
      end
    end

    def capture_stderr
      stderr, $stderr, verbose, $VERBOSE = $stderr, StringIO.new, $VERBOSE, true

      begin
        yield
        $stderr.string
      ensure
        $stderr, $VERBOSE = stderr, verbose
      end
    end
  end
end
