1513 lines
38 KiB
Elixir
1513 lines
38 KiB
Elixir
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
|
|
Code.require_file("../test_helper.exs", __DIR__)
|
|
|
|
defmodule Kernel.DiagnosticsTest do
|
|
use ExUnit.Case, async: false
|
|
|
|
import ExUnit.CaptureIO
|
|
|
|
setup_all do
|
|
previous = Application.get_env(:elixir, :ansi_enabled, false)
|
|
Application.put_env(:elixir, :ansi_enabled, false)
|
|
on_exit(fn -> Application.put_env(:elixir, :ansi_enabled, previous) end)
|
|
end
|
|
|
|
describe "mismatched delimiter" do
|
|
test "same line - handles unicode input" do
|
|
output =
|
|
capture_raise(
|
|
"""
|
|
[1, 2, 3, 4, 5, 6) <- 😎
|
|
""",
|
|
MismatchedDelimiterError
|
|
)
|
|
|
|
assert output == """
|
|
** (MismatchedDelimiterError) mismatched delimiter found on nofile:1:18:
|
|
error: unexpected token: )
|
|
│
|
|
1 │ [1, 2, 3, 4, 5, 6) <- 😎
|
|
│ │ └ mismatched closing delimiter (expected "]")
|
|
│ └ unclosed delimiter
|
|
│
|
|
└─ nofile:1:18\
|
|
"""
|
|
end
|
|
|
|
test "same line" do
|
|
output =
|
|
capture_raise(
|
|
"""
|
|
[1, 2, 3, 4, 5, 6)
|
|
""",
|
|
MismatchedDelimiterError
|
|
)
|
|
|
|
assert output == """
|
|
** (MismatchedDelimiterError) mismatched delimiter found on nofile:1:18:
|
|
error: unexpected token: )
|
|
│
|
|
1 │ [1, 2, 3, 4, 5, 6)
|
|
│ │ └ mismatched closing delimiter (expected "]")
|
|
│ └ unclosed delimiter
|
|
│
|
|
└─ nofile:1:18\
|
|
"""
|
|
end
|
|
|
|
test "same line with offset" do
|
|
output =
|
|
capture_raise(
|
|
"""
|
|
[1, 2, 3, 4, 5, 6)
|
|
""",
|
|
MismatchedDelimiterError,
|
|
line: 3
|
|
)
|
|
|
|
assert output == """
|
|
** (MismatchedDelimiterError) mismatched delimiter found on nofile:3:18:
|
|
error: unexpected token: )
|
|
│
|
|
3 │ [1, 2, 3, 4, 5, 6)
|
|
│ │ └ mismatched closing delimiter (expected "]")
|
|
│ └ unclosed delimiter
|
|
│
|
|
└─ nofile:3:18\
|
|
"""
|
|
end
|
|
|
|
test "two-line span" do
|
|
output =
|
|
capture_raise(
|
|
"""
|
|
[a, b, c
|
|
d, f, g}
|
|
""",
|
|
MismatchedDelimiterError
|
|
)
|
|
|
|
assert output == """
|
|
** (MismatchedDelimiterError) mismatched delimiter found on nofile:2:9:
|
|
error: unexpected token: }
|
|
│
|
|
1 │ [a, b, c
|
|
│ └ unclosed delimiter
|
|
2 │ d, f, g}
|
|
│ └ mismatched closing delimiter (expected "]")
|
|
│
|
|
└─ nofile:2:9\
|
|
"""
|
|
end
|
|
|
|
test "two-line span with offset" do
|
|
output =
|
|
capture_raise(
|
|
"""
|
|
[a, b, c
|
|
d, f, g}
|
|
""",
|
|
MismatchedDelimiterError,
|
|
line: 3
|
|
)
|
|
|
|
assert output == """
|
|
** (MismatchedDelimiterError) mismatched delimiter found on nofile:4:9:
|
|
error: unexpected token: }
|
|
│
|
|
3 │ [a, b, c
|
|
│ └ unclosed delimiter
|
|
4 │ d, f, g}
|
|
│ └ mismatched closing delimiter (expected "]")
|
|
│
|
|
└─ nofile:4:9\
|
|
"""
|
|
end
|
|
|
|
test "many-line span" do
|
|
output =
|
|
capture_raise(
|
|
"""
|
|
[ a,
|
|
b,
|
|
c,
|
|
d
|
|
e )
|
|
""",
|
|
MismatchedDelimiterError
|
|
)
|
|
|
|
assert output == """
|
|
** (MismatchedDelimiterError) mismatched delimiter found on nofile:5:5:
|
|
error: unexpected token: )
|
|
│
|
|
1 │ [ a,
|
|
│ └ unclosed delimiter
|
|
2 │ b,
|
|
3 │ c,
|
|
4 │ d
|
|
5 │ e )
|
|
│ └ mismatched closing delimiter (expected "]")
|
|
│
|
|
└─ nofile:5:5\
|
|
"""
|
|
end
|
|
|
|
test "many-line span with offset" do
|
|
output =
|
|
capture_raise(
|
|
"""
|
|
fn always_forget_end ->
|
|
IO.inspect(2 + 2) + 2
|
|
)
|
|
""",
|
|
MismatchedDelimiterError,
|
|
line: 3
|
|
)
|
|
|
|
assert output == """
|
|
** (MismatchedDelimiterError) mismatched delimiter found on nofile:5:1:
|
|
error: unexpected token: )
|
|
│
|
|
3 │ fn always_forget_end ->
|
|
│ └ unclosed delimiter
|
|
4 │ IO.inspect(2 + 2) + 2
|
|
5 │ )
|
|
│ └ mismatched closing delimiter (expected "end")
|
|
│
|
|
└─ nofile:5:1\
|
|
"""
|
|
end
|
|
|
|
test "line range - handles unicode input" do
|
|
output =
|
|
capture_raise(
|
|
"""
|
|
defmodule A do
|
|
IO.inspect(2 + 2)
|
|
) <- 😎
|
|
""",
|
|
MismatchedDelimiterError
|
|
)
|
|
|
|
assert output == """
|
|
** (MismatchedDelimiterError) mismatched delimiter found on nofile:3:1:
|
|
error: unexpected token: )
|
|
│
|
|
1 │ defmodule A do
|
|
│ └ unclosed delimiter
|
|
2 │ IO.inspect(2 + 2)
|
|
3 │ ) <- 😎
|
|
│ └ mismatched closing delimiter (expected "end")
|
|
│
|
|
└─ nofile:3:1\
|
|
"""
|
|
end
|
|
|
|
test "trim in between lines if too many" do
|
|
output =
|
|
capture_raise(
|
|
"""
|
|
[ :a,
|
|
:b,
|
|
:c,
|
|
:d,
|
|
:e,
|
|
:f,
|
|
:g,
|
|
:h
|
|
)
|
|
""",
|
|
MismatchedDelimiterError
|
|
)
|
|
|
|
assert output == """
|
|
** (MismatchedDelimiterError) mismatched delimiter found on nofile:9:1:
|
|
error: unexpected token: )
|
|
│
|
|
1 │ [ :a,
|
|
│ └ unclosed delimiter
|
|
...
|
|
9 │ )
|
|
│ └ mismatched closing delimiter (expected "]")
|
|
│
|
|
└─ nofile:9:1\
|
|
"""
|
|
end
|
|
|
|
test "trimmed line range - handles unicode input" do
|
|
output =
|
|
capture_raise(
|
|
"""
|
|
[ :a,
|
|
:b,
|
|
:c,
|
|
:d,
|
|
:e,
|
|
:f,
|
|
:g,
|
|
:h
|
|
) <- 😎
|
|
""",
|
|
MismatchedDelimiterError
|
|
)
|
|
|
|
assert output == """
|
|
** (MismatchedDelimiterError) mismatched delimiter found on nofile:9:1:
|
|
error: unexpected token: )
|
|
│
|
|
1 │ [ :a,
|
|
│ └ unclosed delimiter
|
|
...
|
|
9 │ ) <- 😎
|
|
│ └ mismatched closing delimiter (expected "]")
|
|
│
|
|
└─ nofile:9:1\
|
|
"""
|
|
end
|
|
|
|
test "pads according to line number digits" do
|
|
output =
|
|
capture_raise(
|
|
"""
|
|
[ a,
|
|
#{String.duplicate("\n", 10)}
|
|
b )
|
|
""",
|
|
MismatchedDelimiterError
|
|
)
|
|
|
|
assert output == """
|
|
** (MismatchedDelimiterError) mismatched delimiter found on nofile:13:5:
|
|
error: unexpected token: )
|
|
│
|
|
1 │ [ a,
|
|
│ └ unclosed delimiter
|
|
...
|
|
13 │ b )
|
|
│ └ mismatched closing delimiter (expected "]")
|
|
│
|
|
└─ nofile:13:5\
|
|
"""
|
|
|
|
output =
|
|
capture_raise(
|
|
"""
|
|
[ a,
|
|
#{String.duplicate("\n", 400)}
|
|
b )
|
|
""",
|
|
MismatchedDelimiterError
|
|
)
|
|
|
|
assert output == """
|
|
** (MismatchedDelimiterError) mismatched delimiter found on nofile:403:5:
|
|
error: unexpected token: )
|
|
│
|
|
1 │ [ a,
|
|
│ └ unclosed delimiter
|
|
...
|
|
403 │ b )
|
|
│ └ mismatched closing delimiter (expected "]")
|
|
│
|
|
└─ nofile:403:5\
|
|
"""
|
|
|
|
output =
|
|
capture_raise(
|
|
"""
|
|
#{String.duplicate("\n", 97)}
|
|
[ a,
|
|
#{String.duplicate("\n", 6)}
|
|
b )
|
|
""",
|
|
MismatchedDelimiterError
|
|
)
|
|
|
|
assert output == """
|
|
** (MismatchedDelimiterError) mismatched delimiter found on nofile:107:5:
|
|
error: unexpected token: )
|
|
│
|
|
99 │ [ a,
|
|
│ └ unclosed delimiter
|
|
...
|
|
107 │ b )
|
|
│ └ mismatched closing delimiter (expected "]")
|
|
│
|
|
└─ nofile:107:5\
|
|
"""
|
|
end
|
|
end
|
|
|
|
describe "token missing error" do
|
|
test "missing parens terminator" do
|
|
output =
|
|
capture_raise(
|
|
"""
|
|
my_numbers = [1, 2, 3, 4, 5, 6
|
|
IO.inspect(my_numbers)
|
|
""",
|
|
TokenMissingError
|
|
)
|
|
|
|
assert output == """
|
|
** (TokenMissingError) token missing on nofile:2:23:
|
|
error: missing terminator: ]
|
|
│
|
|
1 │ my_numbers = [1, 2, 3, 4, 5, 6
|
|
│ └ unclosed delimiter
|
|
2 │ IO.inspect(my_numbers)
|
|
│ └ missing closing delimiter (expected "]")
|
|
│
|
|
└─ nofile:2:23\
|
|
"""
|
|
end
|
|
|
|
test "missing heredoc terminator" do
|
|
output =
|
|
capture_raise(
|
|
"""
|
|
a = \"""
|
|
test string
|
|
|
|
IO.inspect(10 + 20)
|
|
""",
|
|
TokenMissingError
|
|
)
|
|
|
|
assert output == """
|
|
** (TokenMissingError) token missing on nofile:4:20:
|
|
error: missing terminator: \""" (for heredoc starting at line 1)
|
|
│
|
|
1 │ a = \"""
|
|
│ └ unclosed delimiter
|
|
2 │ test string
|
|
3 │\s
|
|
4 │ IO.inspect(10 + 20)
|
|
│ └ missing closing delimiter (expected \""")
|
|
│
|
|
└─ nofile:4:20\
|
|
"""
|
|
end
|
|
|
|
test "missing sigil terminator" do
|
|
output =
|
|
capture_raise("~s<foobar", TokenMissingError)
|
|
|
|
assert output == """
|
|
** (TokenMissingError) token missing on nofile:1:10:
|
|
error: missing terminator: > (for sigil ~s< starting at line 1)
|
|
│
|
|
1 │ ~s<foobar
|
|
│ │ └ missing closing delimiter (expected ">")
|
|
│ └ unclosed delimiter
|
|
│
|
|
└─ nofile:1:10\
|
|
"""
|
|
|
|
output =
|
|
capture_raise("~s|foobar", TokenMissingError)
|
|
|
|
assert output == """
|
|
** (TokenMissingError) token missing on nofile:1:10:
|
|
error: missing terminator: | (for sigil ~s| starting at line 1)
|
|
│
|
|
1 │ ~s|foobar
|
|
│ │ └ missing closing delimiter (expected "|")
|
|
│ └ unclosed delimiter
|
|
│
|
|
└─ nofile:1:10\
|
|
"""
|
|
end
|
|
|
|
test "missing string terminator" do
|
|
output =
|
|
capture_raise("\"foobar", TokenMissingError)
|
|
|
|
assert output == """
|
|
** (TokenMissingError) token missing on nofile:1:8:
|
|
error: missing terminator: " (for string starting at line 1)
|
|
│
|
|
1 │ "foobar
|
|
│ │ └ missing closing delimiter (expected ")
|
|
│ └ unclosed delimiter
|
|
│
|
|
└─ nofile:1:8\
|
|
"""
|
|
end
|
|
|
|
test "missing atom terminator" do
|
|
output =
|
|
capture_raise(":\"foobar", TokenMissingError)
|
|
|
|
assert output == """
|
|
** (TokenMissingError) token missing on nofile:1:9:
|
|
error: missing terminator: " (for atom starting at line 1)
|
|
│
|
|
1 │ :"foobar
|
|
│ │ └ missing closing delimiter (expected ")
|
|
│ └ unclosed delimiter
|
|
│
|
|
└─ nofile:1:9\
|
|
"""
|
|
end
|
|
|
|
test "missing function terminator" do
|
|
output =
|
|
capture_raise("K.\"foobar", TokenMissingError)
|
|
|
|
assert output == """
|
|
** (TokenMissingError) token missing on nofile:1:10:
|
|
error: missing terminator: " (for function name starting at line 1)
|
|
│
|
|
1 │ K."foobar
|
|
│ │ └ missing closing delimiter (expected ")
|
|
│ └ unclosed delimiter
|
|
│
|
|
└─ nofile:1:10\
|
|
"""
|
|
end
|
|
|
|
test "shows in between lines if EOL is not far below" do
|
|
output =
|
|
capture_raise(
|
|
"""
|
|
my_numbers = [1, 2, 3, 4, 5, 6
|
|
my_numbers
|
|
|> Enum.map(&(&1 + 1))
|
|
|> Enum.map(&(&1 * &1))
|
|
|> IO.inspect()
|
|
""",
|
|
TokenMissingError
|
|
)
|
|
|
|
assert output == """
|
|
** (TokenMissingError) token missing on nofile:5:16:
|
|
error: missing terminator: ]
|
|
│
|
|
1 │ my_numbers = [1, 2, 3, 4, 5, 6
|
|
│ └ unclosed delimiter
|
|
2 │ my_numbers
|
|
3 │ |> Enum.map(&(&1 + 1))
|
|
4 │ |> Enum.map(&(&1 * &1))
|
|
5 │ |> IO.inspect()
|
|
│ └ missing closing delimiter (expected "]")
|
|
│
|
|
└─ nofile:5:16\
|
|
"""
|
|
end
|
|
|
|
test "trims lines" do
|
|
output =
|
|
capture_raise(
|
|
"""
|
|
my_numbers = (1, 2, 3, 4, 5, 6
|
|
|
|
|
|
|
|
|
|
|
|
|
|
IO.inspect(my_numbers)
|
|
""",
|
|
TokenMissingError
|
|
)
|
|
|
|
assert output == """
|
|
** (TokenMissingError) token missing on nofile:8:23:
|
|
error: missing terminator: )
|
|
│
|
|
1 │ my_numbers = (1, 2, 3, 4, 5, 6
|
|
│ └ unclosed delimiter
|
|
...
|
|
8 │ IO.inspect(my_numbers)
|
|
│ └ missing closing delimiter (expected ")")
|
|
│
|
|
└─ nofile:8:23\
|
|
"""
|
|
end
|
|
|
|
test "shows the last non-empty line of a file" do
|
|
output =
|
|
capture_raise(
|
|
"""
|
|
my_numbers = {1, 2, 3, 4, 5, 6
|
|
IO.inspect(my_numbers)
|
|
|
|
|
|
|
|
|
|
""",
|
|
TokenMissingError
|
|
)
|
|
|
|
assert output == """
|
|
** (TokenMissingError) token missing on nofile:2:23:
|
|
error: missing terminator: }
|
|
│
|
|
1 │ my_numbers = {1, 2, 3, 4, 5, 6
|
|
│ └ unclosed delimiter
|
|
2 │ IO.inspect(my_numbers)
|
|
│ └ missing closing delimiter (expected "}")
|
|
│
|
|
└─ nofile:2:23\
|
|
"""
|
|
end
|
|
|
|
test "supports unicode" do
|
|
output =
|
|
capture_raise(
|
|
"""
|
|
my_emojis = [1, 2, 3, 4 # ⚗️
|
|
IO.inspect(my_numbers)
|
|
""",
|
|
TokenMissingError
|
|
)
|
|
|
|
assert output == """
|
|
** (TokenMissingError) token missing on nofile:2:23:
|
|
error: missing terminator: ]
|
|
│
|
|
1 │ my_emojis = [1, 2, 3, 4 # ⚗️
|
|
│ └ unclosed delimiter
|
|
2 │ IO.inspect(my_numbers)
|
|
│ └ missing closing delimiter (expected "]")
|
|
│
|
|
└─ nofile:2:23\
|
|
"""
|
|
end
|
|
end
|
|
|
|
describe "compile-time exceptions" do
|
|
test "SyntaxError (snippet)" do
|
|
output =
|
|
capture_raise(
|
|
"""
|
|
[1, 2, 3, 4, 5, *]
|
|
""",
|
|
SyntaxError
|
|
)
|
|
|
|
assert output == """
|
|
** (SyntaxError) invalid syntax found on nofile:1:17:
|
|
error: syntax error before: '*'
|
|
│
|
|
1 │ [1, 2, 3, 4, 5, *]
|
|
│ ^
|
|
│
|
|
└─ nofile:1:17\
|
|
"""
|
|
end
|
|
|
|
test "SyntaxError (snippet) with offset" do
|
|
output =
|
|
capture_raise(
|
|
"""
|
|
[1, 2, 3, 4, 5, *]
|
|
""",
|
|
SyntaxError,
|
|
line: 3
|
|
)
|
|
|
|
assert output == """
|
|
** (SyntaxError) invalid syntax found on nofile:3:17:
|
|
error: syntax error before: '*'
|
|
│
|
|
3 │ [1, 2, 3, 4, 5, *]
|
|
│ ^
|
|
│
|
|
└─ nofile:3:17\
|
|
"""
|
|
end
|
|
|
|
test "TokenMissingError (snippet)" do
|
|
output =
|
|
capture_raise(
|
|
"""
|
|
1 +
|
|
""",
|
|
TokenMissingError
|
|
)
|
|
|
|
assert output == """
|
|
** (TokenMissingError) token missing on nofile:1:4:
|
|
error: syntax error: expression is incomplete
|
|
│
|
|
1 │ 1 +
|
|
│ ^
|
|
│
|
|
└─ nofile:1:4\
|
|
"""
|
|
end
|
|
|
|
test "TokenMissingError (snippet) with offset and column" do
|
|
output =
|
|
capture_raise(
|
|
"""
|
|
1 +
|
|
""",
|
|
TokenMissingError,
|
|
line: 3,
|
|
column: 3
|
|
)
|
|
|
|
assert output == """
|
|
** (TokenMissingError) token missing on nofile:3:6:
|
|
error: syntax error: expression is incomplete
|
|
│
|
|
3 │ 1 +
|
|
│ ^
|
|
│
|
|
└─ nofile:3:6\
|
|
"""
|
|
end
|
|
|
|
test "TokenMissingError (unclosed delimiter)" do
|
|
expected = """
|
|
** (TokenMissingError) token missing on nofile:1:5:
|
|
error: missing terminator: end
|
|
│
|
|
1 │ fn a
|
|
│ │ └ missing closing delimiter (expected "end")
|
|
│ └ unclosed delimiter
|
|
│
|
|
└─ nofile:1:5\
|
|
"""
|
|
|
|
output =
|
|
capture_raise(
|
|
"""
|
|
fn a
|
|
""",
|
|
TokenMissingError
|
|
)
|
|
|
|
assert output == expected
|
|
end
|
|
|
|
test "keeps trailing whitespace if under threshold" do
|
|
expected = """
|
|
** (SyntaxError) invalid syntax found on nofile:1:23:
|
|
error: unexpected token: "😎" (column 23, code point U+****)
|
|
│
|
|
1 │ a + 😎
|
|
│ ^
|
|
│
|
|
└─ nofile:1:23\
|
|
"""
|
|
|
|
output =
|
|
capture_raise(
|
|
"""
|
|
a + 😎
|
|
""",
|
|
SyntaxError
|
|
)
|
|
|
|
assert output == expected
|
|
end
|
|
|
|
test "limits trailing whitespace if too many" do
|
|
expected = """
|
|
** (SyntaxError) invalid syntax found on nofile:1:43:
|
|
error: unexpected token: "😎" (column 43, code point U+****)
|
|
│
|
|
1 │ ... a + 😎
|
|
│ ^
|
|
│
|
|
└─ nofile:1:43\
|
|
"""
|
|
|
|
output =
|
|
capture_raise(
|
|
"""
|
|
a + 😎
|
|
""",
|
|
SyntaxError
|
|
)
|
|
|
|
assert output == expected
|
|
end
|
|
|
|
test "shows stacktrace if present" do
|
|
fake_stacktrace = [
|
|
{:fake, :fun, 3, [file: "nofile", line: 10]},
|
|
{:real, :fun, 2, [file: "nofile", line: 10]}
|
|
]
|
|
|
|
expected = """
|
|
** (TokenMissingError) token missing on nofile:1:4:
|
|
error: syntax error: expression is incomplete
|
|
│
|
|
1 │ 1 -
|
|
│ ^
|
|
│
|
|
└─ nofile:1:4
|
|
nofile:10: :fake.fun/3
|
|
nofile:10: :real.fun/2
|
|
"""
|
|
|
|
output =
|
|
capture_raise(
|
|
"""
|
|
1 -
|
|
""",
|
|
TokenMissingError,
|
|
stacktrace: fake_stacktrace
|
|
)
|
|
|
|
assert output == expected
|
|
end
|
|
|
|
test "2-digit line errors stay aligned 1-digit line errors" do
|
|
fake_stacktrace = [
|
|
{:fake, :fun, 3, [file: "nofile", line: 10]}
|
|
]
|
|
|
|
expected = """
|
|
** (TokenMissingError) token missing on nofile:12:4:
|
|
error: syntax error: expression is incomplete
|
|
│
|
|
12 │ 1 -
|
|
│ ^
|
|
│
|
|
└─ nofile:12:4
|
|
nofile:10: :fake.fun/3
|
|
"""
|
|
|
|
output =
|
|
capture_raise(
|
|
"""
|
|
#{String.duplicate("\n", 10)}
|
|
1 -
|
|
""",
|
|
TokenMissingError,
|
|
stacktrace: fake_stacktrace
|
|
)
|
|
|
|
assert output == expected
|
|
end
|
|
|
|
test "handles unicode" do
|
|
source = """
|
|
defmodule Sample do
|
|
def a do
|
|
10 + 😎
|
|
end
|
|
end
|
|
"""
|
|
|
|
output = capture_raise(source, SyntaxError)
|
|
|
|
assert output =~ "😎"
|
|
after
|
|
purge(Sample)
|
|
end
|
|
end
|
|
|
|
describe "compiler warnings" do
|
|
@tag :tmp_dir
|
|
test "simple warning (line + column + file)", %{tmp_dir: tmp_dir} do
|
|
path = make_relative_tmp(tmp_dir, "long-warning.ex")
|
|
|
|
source = """
|
|
defmodule Sample do
|
|
@file "#{path}"
|
|
defp a, do: Unknown.b()
|
|
end
|
|
"""
|
|
|
|
File.write!(path, source)
|
|
|
|
expected = """
|
|
warning: Unknown.b/0 is undefined (module Unknown is not available or is yet to be defined). Make sure the module name is correct and has been specified in full (or that an alias has been defined)
|
|
│
|
|
3 │ defp a, do: Unknown.b()
|
|
│ ~
|
|
│
|
|
└─ #{path}:3:23: Sample.a/0
|
|
"""
|
|
|
|
assert capture_eval(source) =~ expected
|
|
after
|
|
purge(Sample)
|
|
end
|
|
|
|
@tag :tmp_dir
|
|
test "simple warning (line + file)", %{tmp_dir: tmp_dir} do
|
|
path = make_relative_tmp(tmp_dir, "long-warning.ex")
|
|
|
|
source = """
|
|
defmodule Sample do
|
|
@file "#{path}"
|
|
defp a, do: Unknown.b()
|
|
end
|
|
"""
|
|
|
|
File.write!(path, source)
|
|
|
|
expected = """
|
|
warning: Unknown.b/0 is undefined (module Unknown is not available or is yet to be defined). Make sure the module name is correct and has been specified in full (or that an alias has been defined)
|
|
│
|
|
3 │ defp a, do: Unknown.b()
|
|
│ ~~~~~~~~~~~~~~~~~~~~~~~
|
|
│
|
|
└─ #{path}:3: Sample.a/0
|
|
"""
|
|
|
|
assert capture_eval(source, columns: false) =~ expected
|
|
after
|
|
purge(Sample)
|
|
end
|
|
|
|
@tag :tmp_dir
|
|
test "simple warning with tabs (line + file)", %{tmp_dir: tmp_dir} do
|
|
path = make_relative_tmp(tmp_dir, "long-warning.ex")
|
|
|
|
source = """
|
|
defmodule Sample do
|
|
\t@file "#{path}"
|
|
\tdefp a do
|
|
\t\tUnknown.b()
|
|
\tend
|
|
end
|
|
"""
|
|
|
|
File.write!(path, source)
|
|
|
|
expected = """
|
|
warning: Unknown.b/0 is undefined (module Unknown is not available or is yet to be defined). Make sure the module name is correct and has been specified in full (or that an alias has been defined)
|
|
│
|
|
4 │ \t\tUnknown.b()
|
|
│ ~~~~~~~~~~~
|
|
│
|
|
└─ #{path}:4: Sample.a/0
|
|
"""
|
|
|
|
assert capture_eval(source, columns: false) =~ expected
|
|
after
|
|
purge(Sample)
|
|
end
|
|
|
|
test "simple warning (no file)" do
|
|
source = """
|
|
defmodule Sample do
|
|
defp a, do: Unknown.b()
|
|
end
|
|
"""
|
|
|
|
expected = """
|
|
warning: Unknown.b/0 is undefined (module Unknown is not available or is yet to be defined). Make sure the module name is correct and has been specified in full (or that an alias has been defined)
|
|
└─ nofile:2:23: Sample.a/0
|
|
"""
|
|
|
|
assert capture_eval(source) =~ expected
|
|
after
|
|
purge(Sample)
|
|
end
|
|
|
|
@tag :tmp_dir
|
|
test "IO.warn file+line", %{tmp_dir: tmp_dir} do
|
|
path = make_relative_tmp(tmp_dir, "io-warn-file-line.ex")
|
|
|
|
source = """
|
|
IO.warn("oops\\nmulti\\nline", file: __ENV__.file, line: __ENV__.line)
|
|
"""
|
|
|
|
File.write!(path, source)
|
|
|
|
expected = """
|
|
warning: oops
|
|
multi
|
|
line
|
|
│
|
|
1 │ IO.warn("oops\\nmulti\\nline", file: __ENV__.file, line: __ENV__.line)
|
|
│ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
│
|
|
└─ tmp\
|
|
"""
|
|
|
|
assert capture_io(:stderr, fn -> Code.eval_file(path) end) =~ expected
|
|
end
|
|
|
|
@tag :tmp_dir
|
|
test "IO.warn file+line+column", %{tmp_dir: tmp_dir} do
|
|
path = make_relative_tmp(tmp_dir, "io-warn-file-line-column.ex")
|
|
|
|
source = """
|
|
IO.warn("oops\\nmulti\\nline", file: __ENV__.file, line: __ENV__.line, column: 4)
|
|
"""
|
|
|
|
File.write!(path, source)
|
|
|
|
expected = """
|
|
warning: oops
|
|
multi
|
|
line
|
|
│
|
|
1 │ IO.warn("oops\\nmulti\\nline", file: __ENV__.file, line: __ENV__.line, column: 4)
|
|
│ ~
|
|
│
|
|
└─ tmp\
|
|
"""
|
|
|
|
assert capture_io(:stderr, fn -> Code.eval_file(path) end) =~ expected
|
|
end
|
|
|
|
test "IO.warn with missing data" do
|
|
assert capture_eval("""
|
|
IO.warn("oops-bad", file: #{inspect(__ENV__.file)}, line: 3, column: nil)
|
|
""") =~ "warning: oops-bad"
|
|
|
|
assert capture_eval("""
|
|
IO.warn("oops-bad", file: #{inspect(__ENV__.file)}, line: nil)
|
|
""") =~ "oops-bad"
|
|
|
|
assert capture_eval("""
|
|
IO.warn("oops-bad", file: nil)
|
|
""") =~ "oops-bad"
|
|
end
|
|
|
|
@tag :tmp_dir
|
|
test "trims lines if too many whitespaces", %{tmp_dir: tmp_dir} do
|
|
path = make_relative_tmp(tmp_dir, "trim_warning_line.ex")
|
|
|
|
source = """
|
|
defmodule Sample do
|
|
@file "#{path}"
|
|
|
|
def a do
|
|
Unknown.bar(:test)
|
|
end
|
|
end
|
|
"""
|
|
|
|
File.write!(path, source)
|
|
|
|
expected = """
|
|
warning: Unknown.bar/1 is undefined (module Unknown is not available or is yet to be defined). Make sure the module name is correct and has been specified in full (or that an alias has been defined)
|
|
│
|
|
5 │ ... Unknown.bar(:test)
|
|
│ ~
|
|
│
|
|
└─ #{path}:5:53: Sample.a/0
|
|
|
|
"""
|
|
|
|
assert capture_eval(source) == expected
|
|
after
|
|
purge(Sample)
|
|
end
|
|
|
|
@tag :tmp_dir
|
|
test "handles unicode", %{tmp_dir: tmp_dir} do
|
|
path = make_relative_tmp(tmp_dir, "warning_group_unicode.ex")
|
|
|
|
source = """
|
|
defmodule Sample do
|
|
@file "#{path}"
|
|
|
|
def a do
|
|
Unknown.bar("😎")
|
|
Unknown.bar("😎")
|
|
end
|
|
end
|
|
"""
|
|
|
|
File.write!(path, source)
|
|
|
|
assert capture_eval(source) =~ "😎"
|
|
after
|
|
purge(Sample)
|
|
end
|
|
end
|
|
|
|
describe "warning groups" do
|
|
test "no file" do
|
|
source = """
|
|
defmodule Sample do
|
|
def a do
|
|
Unknown.bar()
|
|
Unknown.bar()
|
|
Unknown.bar()
|
|
Unknown.bar()
|
|
end
|
|
end
|
|
"""
|
|
|
|
expected = """
|
|
warning: Unknown.bar/0 is undefined (module Unknown is not available or is yet to be defined). Make sure the module name is correct and has been specified in full (or that an alias has been defined)
|
|
└─ nofile:3:13: Sample.a/0
|
|
└─ nofile:4:13: Sample.a/0
|
|
└─ nofile:5:13: Sample.a/0
|
|
└─ nofile:6:13: Sample.a/0
|
|
|
|
"""
|
|
|
|
assert capture_eval(source) =~ expected
|
|
after
|
|
purge(Sample)
|
|
end
|
|
|
|
@tag :tmp_dir
|
|
test "file + line + column", %{tmp_dir: tmp_dir} do
|
|
path = make_relative_tmp(tmp_dir, "warning_group_nofile.ex")
|
|
|
|
source = """
|
|
defmodule Sample do
|
|
@file "#{path}"
|
|
|
|
def a do
|
|
Unknown.bar()
|
|
Unknown.bar()
|
|
Unknown.bar()
|
|
Unknown.bar()
|
|
end
|
|
end
|
|
"""
|
|
|
|
File.write!(path, source)
|
|
|
|
expected = """
|
|
warning: Unknown.bar/0 is undefined (module Unknown is not available or is yet to be defined). Make sure the module name is correct and has been specified in full (or that an alias has been defined)
|
|
│
|
|
5 │ Unknown.bar()
|
|
│ ~
|
|
│
|
|
└─ #{path}:5:13: Sample.a/0
|
|
└─ #{path}:6:13: Sample.a/0
|
|
└─ #{path}:7:13: Sample.a/0
|
|
└─ #{path}:8:13: Sample.a/0
|
|
|
|
"""
|
|
|
|
assert capture_eval(source) == expected
|
|
after
|
|
purge(Sample)
|
|
end
|
|
|
|
@tag :tmp_dir
|
|
test "file + line", %{tmp_dir: tmp_dir} do
|
|
path = make_relative_tmp(tmp_dir, "warning_group_nofile.ex")
|
|
|
|
source = """
|
|
defmodule Sample do
|
|
@file "#{path}"
|
|
|
|
def a do
|
|
Unknown.bar()
|
|
Unknown.bar()
|
|
Unknown.bar()
|
|
Unknown.bar()
|
|
end
|
|
end
|
|
"""
|
|
|
|
File.write!(path, source)
|
|
|
|
expected = """
|
|
warning: Unknown.bar/0 is undefined (module Unknown is not available or is yet to be defined). Make sure the module name is correct and has been specified in full (or that an alias has been defined)
|
|
│
|
|
5 │ Unknown.bar()
|
|
│ ~~~~~~~~~~~~~
|
|
│
|
|
└─ #{path}:5: Sample.a/0
|
|
└─ #{path}:6: Sample.a/0
|
|
└─ #{path}:7: Sample.a/0
|
|
└─ #{path}:8: Sample.a/0
|
|
|
|
"""
|
|
|
|
assert capture_eval(source, columns: false) == expected
|
|
after
|
|
purge(Sample)
|
|
end
|
|
end
|
|
|
|
describe "error diagnostics" do
|
|
@tag :tmp_dir
|
|
test "line only", %{tmp_dir: tmp_dir} do
|
|
path = make_relative_tmp(tmp_dir, "error_line_only.ex")
|
|
|
|
source = """
|
|
defmodule Sample do
|
|
@file "#{path}"
|
|
def CamelCase do
|
|
end
|
|
end
|
|
"""
|
|
|
|
File.write!(path, source)
|
|
|
|
expected = """
|
|
error: function names should start with lowercase characters or underscore, invalid name CamelCase
|
|
│
|
|
3 │ def CamelCase do
|
|
│ ^^^^^^^^^^^^^^^^
|
|
│
|
|
└─ #{path}:3
|
|
|
|
"""
|
|
|
|
assert capture_compile(source, columns: false) == expected
|
|
after
|
|
purge(Sample)
|
|
end
|
|
|
|
@tag :tmp_dir
|
|
test "shows span for unused variables", %{tmp_dir: tmp_dir} do
|
|
path = make_relative_tmp(tmp_dir, "error_line_column.ex")
|
|
|
|
source = """
|
|
defmodule Sample do
|
|
@file "#{path}"
|
|
|
|
def foo(unused_param) do
|
|
:constant
|
|
end
|
|
end
|
|
"""
|
|
|
|
File.write!(path, source)
|
|
|
|
expected = """
|
|
warning: variable "unused_param" is unused (if the variable is not meant to be used, prefix it with an underscore)
|
|
│
|
|
4 │ def foo(unused_param) do
|
|
│ ~~~~~~~~~~~~
|
|
│
|
|
└─ #{path}:4:11: Sample.foo/1
|
|
|
|
"""
|
|
|
|
assert capture_eval(source) == expected
|
|
after
|
|
purge(Sample)
|
|
end
|
|
|
|
@tag :tmp_dir
|
|
test "shows span for undefined variables", %{tmp_dir: tmp_dir} do
|
|
path = make_relative_tmp(tmp_dir, "undefined_variable_span.ex")
|
|
|
|
source = """
|
|
defmodule Sample do
|
|
@file "#{path}"
|
|
|
|
def foo(a) do
|
|
a - unknown_var
|
|
end
|
|
end
|
|
"""
|
|
|
|
File.write!(path, source)
|
|
|
|
expected = """
|
|
error: undefined variable "unknown_var"
|
|
│
|
|
5 │ a - unknown_var
|
|
│ ^^^^^^^^^^^
|
|
│
|
|
└─ #{path}:5:9: Sample.foo/1
|
|
|
|
"""
|
|
|
|
assert capture_compile(source) == expected
|
|
after
|
|
purge(Sample)
|
|
end
|
|
|
|
@tag :tmp_dir
|
|
test "shows span for unknown local function calls", %{tmp_dir: tmp_dir} do
|
|
path = make_relative_tmp(tmp_dir, "unknown_local_function_call.ex")
|
|
|
|
source = """
|
|
defmodule Sample do
|
|
@file "#{path}"
|
|
|
|
def foo do
|
|
_result = unknown_func_call!(:hello!)
|
|
end
|
|
end
|
|
"""
|
|
|
|
File.write!(path, source)
|
|
|
|
expected = """
|
|
error: undefined function unknown_func_call!/1 (expected Sample to define such a function or for it to be imported, but none are available)
|
|
│
|
|
5 │ _result = unknown_func_call!(:hello!)
|
|
│ ^^^^^^^^^^^^^^^^^^
|
|
│
|
|
└─ #{path}:5:15: Sample.foo/0
|
|
|
|
"""
|
|
|
|
assert capture_compile(source) == expected
|
|
after
|
|
purge(Sample)
|
|
end
|
|
|
|
@tag :tmp_dir
|
|
test "line + column", %{tmp_dir: tmp_dir} do
|
|
path = make_relative_tmp(tmp_dir, "error_line_column.ex")
|
|
|
|
source = """
|
|
defmodule Sample do
|
|
@file "#{path}"
|
|
|
|
def foo do
|
|
IO.puts(bar)
|
|
end
|
|
end
|
|
"""
|
|
|
|
File.write!(path, source)
|
|
|
|
expected = """
|
|
error: undefined variable "bar"
|
|
│
|
|
5 │ IO.puts(bar)
|
|
│ ^^^
|
|
│
|
|
└─ #{path}:5:13: Sample.foo/0
|
|
|
|
"""
|
|
|
|
assert capture_compile(source) == expected
|
|
after
|
|
purge(Sample)
|
|
end
|
|
|
|
test "no file" do
|
|
expected = """
|
|
error: undefined function module_info/0 (this function is auto-generated by the compiler and must always be called as a remote, as in __MODULE__.module_info/0)
|
|
└─ nofile:2:16: Sample.foo/0
|
|
|
|
"""
|
|
|
|
output =
|
|
capture_compile("""
|
|
defmodule Sample do
|
|
def foo, do: module_info()
|
|
end
|
|
""")
|
|
|
|
assert expected == output
|
|
after
|
|
purge(Sample)
|
|
end
|
|
end
|
|
|
|
describe "warning diagnostics" do
|
|
@tag :tmp_dir
|
|
test "line only", %{tmp_dir: tmp_dir} do
|
|
path = make_relative_tmp(tmp_dir, "warn_line.ex")
|
|
|
|
source = """
|
|
defmodule Sample do
|
|
@file "#{path}"
|
|
def a(unused), do: 1
|
|
end
|
|
"""
|
|
|
|
File.write!(path, source)
|
|
|
|
expected = """
|
|
warning: variable "unused" is unused (if the variable is not meant to be used, prefix it with an underscore)
|
|
│
|
|
3 │ def a(unused), do: 1
|
|
│ ~~~~~~~~~~~~~~~~~~~~
|
|
│
|
|
└─ #{path}:3: Sample.a/1
|
|
|
|
"""
|
|
|
|
assert capture_eval(source, columns: false) == expected
|
|
after
|
|
purge(Sample)
|
|
end
|
|
|
|
@tag :tmp_dir
|
|
test "line + column", %{tmp_dir: tmp_dir} do
|
|
path = make_relative_tmp(tmp_dir, "warn_line_column.ex")
|
|
|
|
source = """
|
|
defmodule Sample do
|
|
@file "#{path}"
|
|
@foo 1
|
|
|
|
def bar do
|
|
@foo
|
|
:ok
|
|
end
|
|
end
|
|
"""
|
|
|
|
File.write!(path, source)
|
|
|
|
expected = """
|
|
warning: module attribute @foo in code block has no effect as it is never returned (remove the attribute or assign it to _ to avoid warnings)
|
|
│
|
|
6 │ @foo
|
|
│ ~
|
|
│
|
|
└─ #{path}:6:5: Sample.bar/0
|
|
|
|
"""
|
|
|
|
assert capture_eval(source) == expected
|
|
after
|
|
purge(Sample)
|
|
end
|
|
|
|
test "no file" do
|
|
expected = """
|
|
warning: unused alias List
|
|
└─ nofile:2:3
|
|
|
|
"""
|
|
|
|
output =
|
|
capture_eval("""
|
|
defmodule Sample do
|
|
alias :lists, as: List
|
|
import MapSet
|
|
new()
|
|
end
|
|
""")
|
|
|
|
assert output == expected
|
|
after
|
|
purge(Sample)
|
|
end
|
|
end
|
|
|
|
describe "Code.print_diagnostic" do
|
|
@tag :tmp_dir
|
|
test "handles diagnostic with span", %{tmp_dir: tmp_dir} do
|
|
path = make_relative_tmp(tmp_dir, "diagnostic_length.ex")
|
|
|
|
diagnostic = %{
|
|
file: path,
|
|
severity: :error,
|
|
message: "Diagnostic span test",
|
|
stacktrace: [],
|
|
position: {4, 7},
|
|
span: {4, 10},
|
|
compiler_name: "Elixir"
|
|
}
|
|
|
|
source = """
|
|
defmodule Sample do
|
|
@file "#{path}"
|
|
|
|
def bar do
|
|
nil
|
|
end
|
|
end
|
|
"""
|
|
|
|
File.write!(path, source)
|
|
|
|
result =
|
|
ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
|
Code.print_diagnostic(diagnostic)
|
|
end)
|
|
|
|
assert result == """
|
|
error: Diagnostic span test
|
|
│
|
|
4 │ def bar do
|
|
│ ^^^
|
|
│
|
|
└─ #{path}:4:7
|
|
|
|
"""
|
|
end
|
|
|
|
@tag :tmp_dir
|
|
test "prints single marker for multiline diagnostic", %{tmp_dir: tmp_dir} do
|
|
path = make_relative_tmp(tmp_dir, "diagnostic_length.ex")
|
|
|
|
diagnostic = %{
|
|
file: path,
|
|
severity: :error,
|
|
message: "Diagnostic span test",
|
|
stacktrace: [],
|
|
position: {4, 7},
|
|
span: {5, 2},
|
|
compiler_name: "Elixir"
|
|
}
|
|
|
|
source = """
|
|
defmodule Sample do
|
|
@file "#{path}"
|
|
|
|
def bar do
|
|
nil
|
|
end
|
|
end
|
|
"""
|
|
|
|
File.write!(path, source)
|
|
|
|
result =
|
|
ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
|
Code.print_diagnostic(diagnostic)
|
|
end)
|
|
|
|
assert result == """
|
|
error: Diagnostic span test
|
|
│
|
|
4 │ def bar do
|
|
│ ^
|
|
│
|
|
└─ #{path}:4:7
|
|
|
|
"""
|
|
end
|
|
end
|
|
|
|
defp make_relative_tmp(tmp_dir, filename) do
|
|
# Compiler outputs relative, so we just grab the tmp dir
|
|
tmp_dir
|
|
|> Path.join(filename)
|
|
|> Path.relative_to_cwd()
|
|
end
|
|
|
|
defp capture_eval(source, opts \\ [columns: true]) do
|
|
capture_io(:stderr, fn ->
|
|
quoted = Code.string_to_quoted!(source, opts)
|
|
Code.eval_quoted(quoted)
|
|
end)
|
|
end
|
|
|
|
defp capture_compile(source, opts \\ [columns: true]) do
|
|
capture_io(:stderr, fn ->
|
|
assert_raise CompileError, fn ->
|
|
ast = Code.string_to_quoted!(source, opts)
|
|
Code.eval_quoted(ast)
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp capture_raise(source, exception, opts \\ []) do
|
|
{stacktrace, opts} = Keyword.pop(opts, :stacktrace, [])
|
|
|
|
e =
|
|
assert_raise exception, fn ->
|
|
ast = Code.string_to_quoted!(source, [columns: true] ++ opts)
|
|
Code.eval_quoted(ast)
|
|
end
|
|
|
|
Exception.format(:error, e, stacktrace)
|
|
end
|
|
|
|
defp purge(module) when is_atom(module) do
|
|
:code.purge(module)
|
|
:code.delete(module)
|
|
end
|
|
end
|