Unfortunately this makes it so the unused capture warnings emit false positives, so this particular warning was removed. Closes #13609.
2268 lines
52 KiB
Elixir
2268 lines
52 KiB
Elixir
Code.require_file("../test_helper.exs", __DIR__)
|
||
|
||
defmodule Kernel.WarningTest do
|
||
use ExUnit.Case
|
||
import ExUnit.CaptureIO
|
||
|
||
defp capture_err(fun) when is_function(fun), do: capture_io(:stderr, fun)
|
||
|
||
defp assert_messages_match(messages, output) when is_list(messages) do
|
||
for message <- messages do
|
||
assert output =~ message
|
||
end
|
||
end
|
||
|
||
defp assert_warn_eval(messages, source) do
|
||
captured =
|
||
capture_err(fn ->
|
||
quoted = Code.string_to_quoted!(source, columns: true)
|
||
Code.eval_quoted(quoted)
|
||
end)
|
||
|
||
assert_messages_match(messages, captured)
|
||
end
|
||
|
||
defp assert_warn_quoted(messages, source) do
|
||
captured =
|
||
capture_err(fn ->
|
||
Code.string_to_quoted!(source, columns: true)
|
||
end)
|
||
|
||
assert_messages_match(messages, captured)
|
||
end
|
||
|
||
defp assert_warn_compile(messages, source) do
|
||
captured =
|
||
capture_err(fn ->
|
||
quoted = Code.string_to_quoted!(source, columns: true)
|
||
Code.compile_quoted(quoted)
|
||
end)
|
||
|
||
assert_messages_match(messages, captured)
|
||
end
|
||
|
||
defp capture_eval(source) do
|
||
capture_err(fn ->
|
||
quoted = Code.string_to_quoted!(source, columns: true)
|
||
Code.eval_quoted(quoted)
|
||
end)
|
||
end
|
||
|
||
defp capture_quoted(source) do
|
||
capture_err(fn ->
|
||
Code.string_to_quoted!(source, columns: true)
|
||
end)
|
||
end
|
||
|
||
defp capture_compile(source) do
|
||
capture_err(fn ->
|
||
quoted = Code.string_to_quoted!(source, columns: true)
|
||
Code.compile_quoted(quoted)
|
||
end)
|
||
end
|
||
|
||
defmacro will_warn do
|
||
quote file: "demo", line: true do
|
||
%{dup: 1, dup: 2}
|
||
end
|
||
end
|
||
|
||
test "warnings from macro" do
|
||
assert_warn_eval(
|
||
["demo:66\n", "key :dup will be overridden in map\n"],
|
||
"""
|
||
import Kernel.WarningTest
|
||
will_warn()
|
||
"""
|
||
)
|
||
end
|
||
|
||
test "outdented heredoc" do
|
||
assert_warn_eval(
|
||
["nofile:2:3", "outdented heredoc line"],
|
||
"""
|
||
'''
|
||
outdented
|
||
'''
|
||
"""
|
||
)
|
||
end
|
||
|
||
test "does not warn on incomplete tokenization" do
|
||
assert {:error, _} = Code.string_to_quoted(~s[:"foobar" do])
|
||
end
|
||
|
||
describe "unicode identifier security" do
|
||
test "prevents Restricted codepoints in identifiers" do
|
||
exception = assert_raise SyntaxError, fn -> Code.string_to_quoted!("_shibㅤ = 1") end
|
||
|
||
assert Exception.message(exception) =~
|
||
"unexpected token: \"ㅤ\" (column 6, code point U+3164)"
|
||
end
|
||
|
||
test "warns on confusables" do
|
||
assert_warn_quoted(
|
||
["nofile:1:6", "confusable identifier: 'a' looks like 'а' on line 1"],
|
||
"а=1; a=1"
|
||
)
|
||
|
||
assert_warn_quoted(
|
||
["nofile:1:12", "confusable identifier: 'a' looks like 'а' on line 1"],
|
||
"[{:а, 1}, {:a, 1}]"
|
||
)
|
||
|
||
assert_warn_quoted(
|
||
["nofile:1:8", "confusable identifier: 'a' looks like 'а' on line 1"],
|
||
"[а: 1, a: 1]"
|
||
)
|
||
|
||
assert_warn_quoted(
|
||
["nofile:1:18", "confusable identifier: 'a' looks like 'а' on line 1"],
|
||
"quote do: [а(1), a(1)]"
|
||
)
|
||
|
||
assert_warn_quoted(
|
||
["nofile:1:6", "confusable identifier: 'カ' looks like '力' on line 1"],
|
||
"力=1; カ=1"
|
||
)
|
||
|
||
# by convention, doesn't warn on ascii-only confusables
|
||
assert capture_eval("x0 = xO = 1") == ""
|
||
assert capture_eval("l1 = ll = 1") == ""
|
||
|
||
# works with a custom atom encoder
|
||
assert capture_err(fn ->
|
||
Code.string_to_quoted("[{:а, 1}, {:a, 1}]",
|
||
static_atoms_encoder: fn token, _ -> {:ok, {:wrapped, token}} end
|
||
)
|
||
end) =~
|
||
"confusable identifier: 'a' looks like 'а' on line 1"
|
||
end
|
||
|
||
test "prevents unsafe script mixing in identifiers" do
|
||
exception =
|
||
assert_raise SyntaxError, fn ->
|
||
Code.string_to_quoted!("if аdmin_, do: :ok, else: :err")
|
||
end
|
||
|
||
assert Exception.message(exception) =~ "nofile:1:9:"
|
||
assert Exception.message(exception) =~ "invalid mixed-script identifier found: аdmin"
|
||
|
||
for s <- [
|
||
"\\u0430 а {Cyrillic}",
|
||
"\\u0064 d {Latin}",
|
||
"\\u006D m {Latin}",
|
||
"\\u0069 i {Latin}",
|
||
"\\u006E n {Latin}",
|
||
"\\u005F _"
|
||
] do
|
||
assert Exception.message(exception) =~ s
|
||
end
|
||
|
||
# includes suggestion about what to change
|
||
assert Exception.message(exception) =~ """
|
||
Hint: You could write the above in a similar way that is accepted by Elixir:
|
||
"""
|
||
|
||
assert Exception.message(exception) =~ """
|
||
"admin_" (code points 0x00061 0x00064 0x0006D 0x00069 0x0006E 0x0005F)
|
||
"""
|
||
|
||
# a is in cyrillic
|
||
assert_raise SyntaxError, ~r/mixed/, fn -> Code.string_to_quoted!("[аdmin: 1]") end
|
||
assert_raise SyntaxError, ~r/mixed/, fn -> Code.string_to_quoted!("[{:аdmin, 1}]") end
|
||
assert_raise SyntaxError, ~r/mixed/, fn -> Code.string_to_quoted!("quote do: аdmin(1)") end
|
||
assert_raise SyntaxError, ~r/mixed/, fn -> Code.string_to_quoted!("рос_api = 1") end
|
||
|
||
# T is in cyrillic
|
||
assert_raise SyntaxError, ~r/mixed/, fn -> Code.string_to_quoted!("[Тシャツ: 1]") end
|
||
end
|
||
|
||
test "allows legitimate script mixing" do
|
||
# writing systems that legitimately mix multiple scripts, and Common chars like _
|
||
assert capture_eval("幻ㄒㄧㄤ = 1") == ""
|
||
assert capture_eval("幻ㄒㄧㄤ1 = 1") == ""
|
||
assert capture_eval("__सवव_1? = 1") == ""
|
||
|
||
# uts39 5.2 allowed 'highly restrictive' script mixing, like 't-shirt' in Jpan:
|
||
assert capture_quoted(":Tシャツ") == ""
|
||
|
||
# elixir's normalizations combine scriptsets of the 'from' and 'to' characters,
|
||
# ex: {Common} MICRO => {Greek} MU == {Common, Greek}; Common intersects w/all
|
||
assert capture_quoted("μs") == ""
|
||
end
|
||
end
|
||
|
||
test "operators formed by many of the same character followed by that character" do
|
||
assert_warn_eval(
|
||
[
|
||
"nofile:1:12",
|
||
"found \"+++\" followed by \"+\", please use a space between \"+++\" and the next \"+\""
|
||
],
|
||
"quote do: 1++++1"
|
||
)
|
||
end
|
||
|
||
test "identifier that ends in ! followed by the = operator without a space in between" do
|
||
assert_warn_eval(
|
||
["nofile:1:1", "found identifier \"foo!\", ending with \"!\""],
|
||
"foo!= 1"
|
||
)
|
||
|
||
assert_warn_eval(
|
||
["nofile:1:1", "found atom \":foo!\", ending with \"!\""],
|
||
":foo!= :foo!"
|
||
)
|
||
end
|
||
|
||
describe "unnecessary quotes" do
|
||
test "does not warn for unnecessary quotes in uppercase atoms/keywords" do
|
||
assert capture_eval(~s/:"Foo"/) == ""
|
||
assert capture_eval(~s/["Foo": :bar]/) == ""
|
||
assert capture_eval(~s/:"Foo"/) == ""
|
||
assert capture_eval(~s/:"foo@bar"/) == ""
|
||
assert capture_eval(~s/:"héllò"/) == ""
|
||
assert capture_eval(~s/:"3L1X1R"/) == ""
|
||
end
|
||
|
||
test "warns for unnecessary quotes" do
|
||
assert_warn_eval(
|
||
["nofile:1:1", "found quoted atom \"foo\" but the quotes are not required"],
|
||
~s/:"foo"/
|
||
)
|
||
|
||
assert_warn_eval(
|
||
["nofile:1:3", "found quoted keyword \"foo\" but the quotes are not required"],
|
||
~s/["foo": :bar]/
|
||
)
|
||
|
||
assert_warn_eval(
|
||
["nofile:1:9", "found quoted call \"length\" but the quotes are not required"],
|
||
~s/[Kernel."length"([])]/
|
||
)
|
||
end
|
||
end
|
||
|
||
test "warns on :: as atom" do
|
||
assert_warn_eval(
|
||
[
|
||
"nofile:1:1",
|
||
"atom ::: must be written between quotes, as in :\"::\", to avoid ambiguity"
|
||
],
|
||
~s/:::/
|
||
)
|
||
end
|
||
|
||
test "unused variable" do
|
||
# Note we use compile_string because eval_string does not emit unused vars warning
|
||
assert_warn_compile(
|
||
[
|
||
"nofile:2:3",
|
||
"variable \"module\" is unused",
|
||
"nofile:3:13",
|
||
"variable \"arg\" is unused",
|
||
"nofile:5:1",
|
||
"variable \"file\" is unused"
|
||
],
|
||
"""
|
||
defmodule Sample do
|
||
module = 1
|
||
def hello(arg), do: nil
|
||
end
|
||
file = 2
|
||
file = 3
|
||
file
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "unused variable that could be pinned" do
|
||
# Note we use compile_string because eval_string does not emit unused vars warning
|
||
assert_warn_compile(
|
||
[
|
||
"nofile:4:12",
|
||
"variable \"compare_local\" is unused (there is a variable with the same name in the context, use the pin operator (^) to match on it or prefix this variable with underscore if it is not meant to be used)",
|
||
"nofile:8:7",
|
||
"variable \"compare_nested\" is unused (there is a variable with the same name in the context, use the pin operator (^) to match on it or prefix this variable with underscore if it is not meant to be used)"
|
||
],
|
||
"""
|
||
defmodule Sample do
|
||
def test do
|
||
compare_local = "hello"
|
||
match?(compare_local, "hello")
|
||
|
||
compare_nested = "hello"
|
||
case "hello" do
|
||
compare_nested -> true
|
||
_other -> false
|
||
end
|
||
end
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "unused compiler variable" do
|
||
output =
|
||
capture_eval("""
|
||
defmodule Sample do
|
||
def hello(__MODULE___), do: :ok
|
||
def world(_R), do: :ok
|
||
end
|
||
""")
|
||
|
||
assert output =~ "unknown compiler variable \"__MODULE___\""
|
||
assert output =~ "nofile:2:13"
|
||
refute output =~ "unknown compiler variable \"_R\""
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "nested unused variable" do
|
||
messages = ["undefined variable \"x\"", "variable \"x\" is unused"]
|
||
|
||
assert_compile_error(
|
||
["nofile:5:1", "nofile:2:11" | messages],
|
||
"""
|
||
case false do
|
||
true -> x = 1
|
||
_ -> 1
|
||
end
|
||
x
|
||
"""
|
||
)
|
||
|
||
assert_compile_error(
|
||
["nofile:1:12", "nofile:2:1" | messages],
|
||
"""
|
||
false and (x = 1)
|
||
x
|
||
"""
|
||
)
|
||
|
||
assert_compile_error(
|
||
["nofile:1:10", "nofile:2:1" | messages],
|
||
"""
|
||
true or (x = 1)
|
||
x
|
||
"""
|
||
)
|
||
|
||
assert_compile_error(
|
||
["nofile:2:3", "nofile:4:1" | messages],
|
||
"""
|
||
if false do
|
||
x = 1
|
||
end
|
||
x
|
||
"""
|
||
)
|
||
|
||
assert_compile_error(
|
||
["nofile:2:12", "nofile:5:1" | messages],
|
||
"""
|
||
cond do
|
||
false -> x = 1
|
||
true -> 1
|
||
end
|
||
x
|
||
"""
|
||
)
|
||
|
||
assert_compile_error(
|
||
["nofile:2:11", "nofile:6:1" | messages],
|
||
"""
|
||
receive do
|
||
:foo -> x = 1
|
||
after
|
||
0 -> 1
|
||
end
|
||
x
|
||
"""
|
||
)
|
||
|
||
assert_compile_error(
|
||
["nofile:1:11", "nofile:2:1" | messages],
|
||
"""
|
||
false && (x = 1)
|
||
x
|
||
"""
|
||
)
|
||
|
||
assert_compile_error(
|
||
["nofile:1:10", "nofile:2:1" | messages],
|
||
"""
|
||
true || (x = 1)
|
||
x
|
||
"""
|
||
)
|
||
|
||
assert_compile_error(
|
||
["nofile:2:3", "nofile:4:1" | messages],
|
||
"""
|
||
with true <- true do
|
||
x = false
|
||
end
|
||
x
|
||
"""
|
||
)
|
||
|
||
assert_compile_error(
|
||
["nofile:2:3", "nofile:4:1" | messages],
|
||
"""
|
||
fn ->
|
||
x = true
|
||
end
|
||
x
|
||
"""
|
||
)
|
||
end
|
||
|
||
test "unused variable in redefined function in different file" do
|
||
output =
|
||
capture_err(fn ->
|
||
Code.eval_string("""
|
||
defmodule Sample do
|
||
defmacro __using__(_) do
|
||
quote location: :keep do
|
||
def function(arg)
|
||
end
|
||
end
|
||
end
|
||
""")
|
||
|
||
code = """
|
||
defmodule RedefineSample do
|
||
use Sample
|
||
def function(var123), do: nil
|
||
end
|
||
"""
|
||
|
||
Code.eval_string(code, [], file: "redefine_sample.ex")
|
||
end)
|
||
|
||
assert output =~ "redefine_sample.ex:3: "
|
||
assert output =~ "variable \"var123\" is unused"
|
||
after
|
||
purge(Sample)
|
||
purge(RedefineSample)
|
||
end
|
||
|
||
test "unused variable because re-declared in a match? pattern" do
|
||
assert_warn_eval(
|
||
[
|
||
"nofile:1:16",
|
||
"variable \"x\" is unused (there is a variable with the same name in the context,",
|
||
"variable \"x\" is unused (if the variable is not meant to be used,"
|
||
],
|
||
"""
|
||
fn x -> match?(x, :value) end
|
||
"""
|
||
)
|
||
end
|
||
|
||
test "useless literal" do
|
||
message = "code block contains unused literal \"oops\""
|
||
|
||
assert_warn_eval(
|
||
["nofile:1\n", message],
|
||
"""
|
||
"oops"
|
||
:ok
|
||
"""
|
||
)
|
||
|
||
assert_warn_eval(
|
||
["nofile:1\n", message],
|
||
"""
|
||
fn ->
|
||
"oops"
|
||
:ok
|
||
end
|
||
"""
|
||
)
|
||
|
||
assert_warn_eval(
|
||
["nofile:1\n", message],
|
||
"""
|
||
try do
|
||
"oops"
|
||
:ok
|
||
after
|
||
:ok
|
||
end
|
||
"""
|
||
)
|
||
end
|
||
|
||
test "useless attr" do
|
||
assert_warn_eval(
|
||
[
|
||
"nofile:4:3",
|
||
"module attribute @foo in code block has no effect as it is never returned ",
|
||
"nofile:7:5",
|
||
"module attribute @bar in code block has no effect as it is never returned "
|
||
],
|
||
"""
|
||
defmodule Sample do
|
||
@foo 1
|
||
@bar 1
|
||
@foo
|
||
|
||
def bar do
|
||
@bar
|
||
:ok
|
||
end
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "useless var" do
|
||
message = "variable foo in code block has no effect as it is never returned "
|
||
|
||
assert_warn_eval(
|
||
["nofile:2:1", message],
|
||
"""
|
||
foo = 1
|
||
foo
|
||
:ok
|
||
"""
|
||
)
|
||
|
||
assert_warn_eval(
|
||
["nofile:3:3", message],
|
||
"""
|
||
fn ->
|
||
foo = 1
|
||
foo
|
||
:ok
|
||
end
|
||
"""
|
||
)
|
||
|
||
assert_warn_eval(
|
||
["nofile:3:3", message],
|
||
"""
|
||
try do
|
||
foo = 1
|
||
foo
|
||
:ok
|
||
after
|
||
:ok
|
||
end
|
||
"""
|
||
)
|
||
|
||
assert capture_eval("""
|
||
node()
|
||
:ok
|
||
""") == ""
|
||
end
|
||
|
||
test "underscored variable on match" do
|
||
assert_warn_eval(
|
||
["nofile:1:8", "the underscored variable \"_arg\" appears more than once in a match"],
|
||
"""
|
||
{_arg, _arg} = {1, 1}
|
||
"""
|
||
)
|
||
end
|
||
|
||
test "underscored variable on use" do
|
||
assert_warn_eval(
|
||
["nofile:1:12", "the underscored variable \"_var\" is used after being set"],
|
||
"""
|
||
fn _var -> _var + 1 end
|
||
"""
|
||
)
|
||
|
||
assert capture_eval("""
|
||
fn var!(_var, Foo) -> var!(_var, Foo) + 1 end
|
||
""") == ""
|
||
end
|
||
|
||
test "unused function" do
|
||
assert_warn_eval(
|
||
["nofile:2:8: ", "function hello/0 is unused\n"],
|
||
"""
|
||
defmodule Sample1 do
|
||
defp hello, do: nil
|
||
end
|
||
"""
|
||
)
|
||
|
||
assert_warn_eval(
|
||
["nofile:2:8: ", "function hello/1 is unused\n"],
|
||
"""
|
||
defmodule Sample2 do
|
||
defp hello(0), do: hello(1)
|
||
defp hello(1), do: :ok
|
||
end
|
||
"""
|
||
)
|
||
|
||
assert_warn_eval(
|
||
["nofile:4:8: ", "function c/2 is unused\n"],
|
||
~S"""
|
||
defmodule Sample3 do
|
||
def a, do: nil
|
||
def b, do: d(10)
|
||
defp c(x, y \\ 1), do: [x, y]
|
||
defp d(x), do: x
|
||
end
|
||
"""
|
||
)
|
||
|
||
assert_warn_eval(
|
||
["nofile:3:8: ", "function b/2 is unused\n"],
|
||
~S"""
|
||
defmodule Sample4 do
|
||
def a, do: nil
|
||
defp b(x \\ 1, y \\ 1)
|
||
defp b(x, y), do: [x, y]
|
||
end
|
||
"""
|
||
)
|
||
|
||
assert_warn_eval(
|
||
["nofile:3:8: ", "function b/0 is unused\n"],
|
||
~S"""
|
||
defmodule Sample5 do
|
||
def a, do: nil
|
||
defp b(), do: unquote(1)
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge([Sample1, Sample2, Sample3, Sample4, Sample5])
|
||
end
|
||
|
||
test "unused cyclic functions" do
|
||
assert_warn_eval(
|
||
[
|
||
"nofile:2:8: ",
|
||
"function a/0 is unused\n",
|
||
"nofile:3:8: ",
|
||
"function b/0 is unused\n"
|
||
],
|
||
"""
|
||
defmodule Sample do
|
||
defp a, do: b()
|
||
defp b, do: a()
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "unused macro" do
|
||
assert_warn_eval(
|
||
["nofile:2:13: ", "macro hello/0 is unused"],
|
||
"""
|
||
defmodule Sample do
|
||
defmacrop hello, do: nil
|
||
end
|
||
"""
|
||
)
|
||
|
||
assert_warn_eval(
|
||
["nofile:2:13: ", "macro hello/0 is unused\n"],
|
||
~S"""
|
||
defmodule Sample2 do
|
||
defmacrop hello do
|
||
quote do: unquote(1)
|
||
end
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge([Sample, Sample2])
|
||
end
|
||
|
||
test "shadowing" do
|
||
assert capture_eval("""
|
||
defmodule Sample do
|
||
def test(x) do
|
||
case x do
|
||
{:file, fid} -> fid
|
||
{:path, _} -> fn(fid) -> fid end
|
||
end
|
||
end
|
||
end
|
||
""") == ""
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "unused default args" do
|
||
assert_warn_eval(
|
||
["nofile:3:8: ", "default values for the optional arguments in b/3 are never used"],
|
||
~S"""
|
||
defmodule Sample1 do
|
||
def a, do: b(1, 2, 3)
|
||
defp b(arg1 \\ 1, arg2 \\ 2, arg3 \\ 3), do: [arg1, arg2, arg3]
|
||
end
|
||
"""
|
||
)
|
||
|
||
assert_warn_eval(
|
||
[
|
||
"nofile:3:8: ",
|
||
"the default value for the last optional argument in b/3 is never used"
|
||
],
|
||
~S"""
|
||
defmodule Sample2 do
|
||
def a, do: b(1, 2)
|
||
defp b(arg1 \\ 1, arg2 \\ 2, arg3 \\ 3), do: [arg1, arg2, arg3]
|
||
end
|
||
"""
|
||
)
|
||
|
||
assert_warn_eval(
|
||
[
|
||
"nofile:3:8: ",
|
||
"the default values for the last 2 optional arguments in b/4 are never used"
|
||
],
|
||
~S"""
|
||
defmodule Sample3 do
|
||
def a, do: b(1, 2)
|
||
defp b(arg1, arg2 \\ 2, arg3 \\ 3, arg4 \\ 4), do: [arg1, arg2, arg3, arg4]
|
||
end
|
||
"""
|
||
)
|
||
|
||
assert capture_eval(~S"""
|
||
defmodule Sample4 do
|
||
def a, do: b(1)
|
||
defp b(arg1 \\ 1, arg2, arg3 \\ 3), do: [arg1, arg2, arg3]
|
||
end
|
||
""") == ""
|
||
|
||
assert_warn_eval(
|
||
["nofile:3:8: ", "the default value for the last optional argument in b/3 is never used"],
|
||
~S"""
|
||
defmodule Sample5 do
|
||
def a, do: b(1, 2)
|
||
defp b(arg1 \\ 1, arg2 \\ 2, arg3 \\ 3)
|
||
|
||
defp b(arg1, arg2, arg3), do: [arg1, arg2, arg3]
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge([Sample1, Sample2, Sample3, Sample4, Sample5])
|
||
end
|
||
|
||
test "unused import" do
|
||
assert_warn_compile(
|
||
["nofile:2:3", "unused import :lists"],
|
||
"""
|
||
defmodule Sample do
|
||
import :lists
|
||
def a, do: nil
|
||
end
|
||
"""
|
||
)
|
||
|
||
assert_warn_compile(
|
||
["nofile:1:1", "unused import :lists"],
|
||
"""
|
||
import :lists
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "unknown import" do
|
||
assert_warn_compile(
|
||
["nofile:1:1", "cannot import Kernel.invalid/1 because it is undefined or private"],
|
||
"""
|
||
import(Kernel, only: [invalid: 1])
|
||
"""
|
||
)
|
||
end
|
||
|
||
test "unused import of one of the functions in :only" do
|
||
assert_warn_compile(
|
||
[
|
||
"nofile:2:3",
|
||
"unused import String.downcase/1",
|
||
"nofile:2:3",
|
||
"unused import String.trim/1"
|
||
],
|
||
"""
|
||
defmodule Sample do
|
||
import String, only: [upcase: 1, downcase: 1, trim: 1]
|
||
def a, do: upcase("hello")
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "unused import of any of the functions in :only" do
|
||
assert_warn_compile(
|
||
["nofile:1:1", "unused import String"],
|
||
"""
|
||
import String, only: [upcase: 1, downcase: 1]
|
||
"""
|
||
)
|
||
end
|
||
|
||
def with(a, b, c), do: [a, b, c]
|
||
|
||
test "import matches special form" do
|
||
assert_warn_compile(
|
||
[
|
||
"nofile:1:1",
|
||
"cannot import Kernel.WarningTest.with/3 because it conflicts with Elixir special forms"
|
||
],
|
||
"""
|
||
import Kernel.WarningTest, only: [with: 3]
|
||
:ok = with true <- true, true <- true, do: :ok
|
||
"""
|
||
)
|
||
end
|
||
|
||
test "duplicated function on import options" do
|
||
assert_warn_compile(
|
||
["nofile:2:3", "invalid :only option for import, wrap/1 is duplicated"],
|
||
"""
|
||
defmodule Kernel.WarningsTest.DuplicatedFunctionOnImportOnly do
|
||
import List, only: [wrap: 1, keyfind: 3, wrap: 1]
|
||
end
|
||
"""
|
||
)
|
||
|
||
assert_warn_compile(
|
||
["nofile:2:3", "invalid :except option for import, wrap/1 is duplicated"],
|
||
"""
|
||
defmodule Kernel.WarningsTest.DuplicatedFunctionOnImportExcept do
|
||
import List, except: [wrap: 1, keyfind: 3, wrap: 1]
|
||
end
|
||
"""
|
||
)
|
||
end
|
||
|
||
test "unused alias" do
|
||
assert_warn_compile(
|
||
["nofile:2:3", "unused alias List"],
|
||
"""
|
||
defmodule Sample do
|
||
alias :lists, as: List
|
||
def a, do: nil
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "unused alias when also import" do
|
||
assert_warn_compile(
|
||
["nofile:2:3", "unused alias List"],
|
||
"""
|
||
defmodule Sample do
|
||
alias :lists, as: List
|
||
import MapSet
|
||
new()
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "unused inside dynamic module" do
|
||
import List, only: [flatten: 1], warn: false
|
||
|
||
assert capture_err(fn ->
|
||
defmodule Sample do
|
||
import String, only: [downcase: 1]
|
||
|
||
def world do
|
||
flatten([1, 2, 3])
|
||
end
|
||
end
|
||
end) =~ "unused import String"
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "duplicate map keys" do
|
||
assert_warn_eval(
|
||
[
|
||
"key :a will be overridden in map",
|
||
"nofile:4:10\n",
|
||
"key :m will be overridden in map",
|
||
"nofile:5:10\n",
|
||
"key 1 will be overridden in map",
|
||
"nofile:6:10\n"
|
||
],
|
||
"""
|
||
defmodule DuplicateMapKeys do
|
||
import ExUnit.Assertions
|
||
|
||
assert %{a: :b, a: :c} == %{a: :c}
|
||
assert %{m: :n, m: :o, m: :p} == %{m: :p}
|
||
assert %{1 => 2, 1 => 3} == %{1 => 3}
|
||
end
|
||
"""
|
||
)
|
||
|
||
assert map_size(%{System.unique_integer() => 1, System.unique_integer() => 2}) == 2
|
||
end
|
||
|
||
test "unused guard" do
|
||
assert_warn_eval(
|
||
["nofile:5\n", "this check/guard will always yield the same result"],
|
||
"""
|
||
defmodule Sample do
|
||
def atom_case do
|
||
v = "bc"
|
||
case v do
|
||
_ when is_atom(v) -> :ok
|
||
_ -> :fail
|
||
end
|
||
end
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "length(list) == 0 in guard" do
|
||
assert_warn_eval(
|
||
[
|
||
"nofile:5:24",
|
||
"do not use \"length(v) == 0\" to check if a list is empty",
|
||
"Prefer to pattern match on an empty list or use \"v == []\" as a guard"
|
||
],
|
||
"""
|
||
defmodule Sample do
|
||
def list_case do
|
||
v = []
|
||
case v do
|
||
_ when length(v) == 0 -> :ok
|
||
_ -> :fail
|
||
end
|
||
end
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "length(list) > 0 in guard" do
|
||
assert_warn_eval(
|
||
[
|
||
"nofile:5:24",
|
||
"do not use \"length(v) > 0\" to check if a list is not empty",
|
||
"Prefer to pattern match on a non-empty list, such as [_ | _], or use \"v != []\" as a guard"
|
||
],
|
||
"""
|
||
defmodule Sample do
|
||
def list_case do
|
||
v = []
|
||
case v do
|
||
_ when length(v) > 0 -> :ok
|
||
_ -> :fail
|
||
end
|
||
end
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "late function heads" do
|
||
assert_warn_eval(
|
||
[
|
||
"nofile:4:7\n",
|
||
"function head for def add/2 must come at the top of its direct implementation"
|
||
],
|
||
"""
|
||
defmodule Sample do
|
||
def add(a, b), do: a + b
|
||
@doc "hello"
|
||
def add(a, b)
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "late function heads do not warn for meta programming" do
|
||
assert capture_eval("""
|
||
defmodule Sample1 do
|
||
defmacro __using__(_) do
|
||
quote do
|
||
def add(a, b), do: a + b
|
||
end
|
||
end
|
||
end
|
||
|
||
defmodule Sample2 do
|
||
use Sample1
|
||
@doc "hello"
|
||
def add(a, b)
|
||
end
|
||
""") == ""
|
||
|
||
assert capture_eval("""
|
||
defmodule Sample3 do
|
||
for fun <- [:foo, :bar] do
|
||
def unquote(fun)(), do: unquote(fun)
|
||
end
|
||
|
||
def foo()
|
||
def bar()
|
||
end
|
||
""") == ""
|
||
after
|
||
purge([Sample1, Sample2, Sample3])
|
||
end
|
||
|
||
test "used import via alias" do
|
||
assert capture_eval("""
|
||
defmodule Sample1 do
|
||
import List, only: [flatten: 1]
|
||
|
||
defmacro generate do
|
||
List.duplicate(quote(do: flatten([1, 2, 3])), 100)
|
||
end
|
||
end
|
||
|
||
defmodule Sample2 do
|
||
import Sample1
|
||
generate()
|
||
end
|
||
""") == ""
|
||
after
|
||
purge([Sample1, Sample2])
|
||
end
|
||
|
||
test "clause not match" do
|
||
assert_warn_eval(
|
||
[
|
||
"nofile:3\n",
|
||
~r"this clause( for hello/0)? cannot match because a previous clause at line 2 always matches"
|
||
],
|
||
"""
|
||
defmodule Sample do
|
||
def hello, do: nil
|
||
def hello, do: nil
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "generated clause not match" do
|
||
assert_warn_eval(
|
||
[
|
||
"nofile:10\n",
|
||
~r"this clause( for hello/0)? cannot match because a previous clause at line 10 always matches"
|
||
],
|
||
"""
|
||
defmodule Sample do
|
||
defmacro __using__(_) do
|
||
quote do
|
||
def hello, do: nil
|
||
def hello, do: nil
|
||
end
|
||
end
|
||
end
|
||
defmodule UseSample do
|
||
use Sample
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
purge(UseSample)
|
||
end
|
||
|
||
test "deprecated closing sigil delimiter" do
|
||
assert_warn_eval(["nofile:1:7", "deprecated"], "~S(foo\\))")
|
||
end
|
||
|
||
test "deprecated not left in right" do
|
||
assert_warn_eval(["nofile:1:7", "deprecated"], "not 1 in [1, 2, 3]")
|
||
end
|
||
|
||
test "clause with defaults should be first" do
|
||
message = "def hello/1 has multiple clauses and also declares default values"
|
||
|
||
assert_warn_eval(
|
||
["nofile:3\n", message],
|
||
~S"""
|
||
defmodule Sample1 do
|
||
def hello(arg), do: arg
|
||
def hello(arg \\ 0), do: arg
|
||
end
|
||
"""
|
||
)
|
||
|
||
assert_warn_eval(
|
||
["nofile:3:7\n", message],
|
||
~S"""
|
||
defmodule Sample2 do
|
||
def hello(_arg)
|
||
def hello(arg \\ 0), do: arg
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge([Sample1, Sample2])
|
||
end
|
||
|
||
test "clauses with default should use header" do
|
||
assert_warn_eval(
|
||
["nofile:3\n", "def hello/1 has multiple clauses and also declares default values"],
|
||
~S"""
|
||
defmodule Sample do
|
||
def hello(arg \\ 0), do: arg
|
||
def hello(arg), do: arg
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "unused with local with overridable" do
|
||
assert_warn_eval(
|
||
["nofile:3:8: ", "function world/0 is unused"],
|
||
"""
|
||
defmodule Sample do
|
||
def hello, do: world()
|
||
defp world, do: :ok
|
||
defoverridable [hello: 0]
|
||
def hello, do: :ok
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "undefined module attribute" do
|
||
assert_warn_eval(
|
||
[
|
||
"nofile:2: ",
|
||
"undefined module attribute @foo, please remove access to @foo or explicitly set it before access"
|
||
],
|
||
"""
|
||
defmodule Sample do
|
||
@foo
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "parens with module attribute" do
|
||
assert_warn_eval(
|
||
[
|
||
"nofile:3: ",
|
||
"the @foo() notation (with parentheses) is deprecated, please use @foo (without parentheses) instead"
|
||
],
|
||
"""
|
||
defmodule Sample do
|
||
@foo 13
|
||
@foo()
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "undefined module attribute in function" do
|
||
assert_warn_eval(
|
||
[
|
||
"nofile:3: ",
|
||
"undefined module attribute @foo, please remove access to @foo or explicitly set it before access"
|
||
],
|
||
"""
|
||
defmodule Sample do
|
||
def hello do
|
||
@foo
|
||
end
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "undefined module attribute with file" do
|
||
assert_warn_eval(
|
||
[
|
||
"nofile:2: ",
|
||
"undefined module attribute @foo, please remove access to @foo or explicitly set it before access"
|
||
],
|
||
"""
|
||
defmodule Sample do
|
||
@foo
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "parse transform" do
|
||
assert_warn_eval(
|
||
["nofile:1: ", "@compile {:parse_transform, :ms_transform} is deprecated"],
|
||
"""
|
||
defmodule Sample do
|
||
@compile {:parse_transform, :ms_transform}
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "@compile inline no warning for unreachable function" do
|
||
refute capture_eval("""
|
||
defmodule Sample do
|
||
@compile {:inline, foo: 1}
|
||
|
||
defp foo(_), do: :ok
|
||
end
|
||
""") =~ "inlined function foo/1 undefined"
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "in guard empty list" do
|
||
assert_warn_eval(
|
||
["nofile:2\n", "this check/guard will always yield the same result"],
|
||
"""
|
||
defmodule Sample do
|
||
def a(x) when x in [], do: x
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "no effect operator" do
|
||
assert_warn_eval(
|
||
["nofile:3\n", "use of operator != has no effect"],
|
||
"""
|
||
defmodule Sample do
|
||
def a(x) do
|
||
x != :foo
|
||
:ok
|
||
end
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "eval failure warning" do
|
||
assert_warn_eval(
|
||
["nofile:2\n", "the call to Atom.to_string/1 will fail with ArgumentError"],
|
||
"""
|
||
defmodule Sample1 do
|
||
def foo, do: Atom.to_string "abc"
|
||
end
|
||
"""
|
||
)
|
||
|
||
assert_warn_eval(
|
||
["nofile:2\n", "the call to +/2 will fail with ArithmeticError"],
|
||
"""
|
||
defmodule Sample2 do
|
||
def foo, do: 1 + nil
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge([Sample1, Sample2])
|
||
end
|
||
|
||
test "undefined function for behaviour" do
|
||
assert_warn_eval(
|
||
[
|
||
"nofile:5: ",
|
||
"function foo/0 required by behaviour Sample1 is not implemented (in module Sample2)"
|
||
],
|
||
"""
|
||
defmodule Sample1 do
|
||
@callback foo :: term
|
||
end
|
||
|
||
defmodule Sample2 do
|
||
@behaviour Sample1
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge([Sample1, Sample2])
|
||
end
|
||
|
||
test "undefined macro for behaviour" do
|
||
assert_warn_eval(
|
||
[
|
||
"nofile:5: ",
|
||
"macro foo/0 required by behaviour Sample1 is not implemented (in module Sample2)"
|
||
],
|
||
"""
|
||
defmodule Sample1 do
|
||
@macrocallback foo :: Macro.t
|
||
end
|
||
|
||
defmodule Sample2 do
|
||
@behaviour Sample1
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge([Sample1, Sample2])
|
||
end
|
||
|
||
test "wrong kind for behaviour" do
|
||
assert_warn_eval(
|
||
[
|
||
"nofile:5: ",
|
||
"function foo/0 required by behaviour Sample1 was implemented as \"defmacro\" but should have been \"def\""
|
||
],
|
||
"""
|
||
defmodule Sample1 do
|
||
@callback foo :: term
|
||
end
|
||
|
||
defmodule Sample2 do
|
||
@behaviour Sample1
|
||
defmacro foo, do: :ok
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge([Sample1, Sample2])
|
||
end
|
||
|
||
test "conflicting behaviour" do
|
||
assert_warn_eval(
|
||
[
|
||
"nofile:9: ",
|
||
"conflicting behaviours found. Callback function foo/0 is defined by both Sample1 and Sample2 (in module Sample3)"
|
||
],
|
||
"""
|
||
defmodule Sample1 do
|
||
@callback foo :: term
|
||
end
|
||
|
||
defmodule Sample2 do
|
||
@callback foo :: term
|
||
end
|
||
|
||
defmodule Sample3 do
|
||
@behaviour Sample1
|
||
@behaviour Sample2
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge([Sample1, Sample2, Sample3])
|
||
end
|
||
|
||
test "conflicting behaviour (but one optional callback)" do
|
||
message =
|
||
capture_compile("""
|
||
defmodule Sample1 do
|
||
@callback foo :: term
|
||
end
|
||
|
||
defmodule Sample2 do
|
||
@callback foo :: term
|
||
@callback bar :: term
|
||
@optional_callbacks foo: 0
|
||
end
|
||
|
||
defmodule Sample3 do
|
||
@behaviour Sample1
|
||
@behaviour Sample2
|
||
|
||
@impl Sample1
|
||
def foo, do: 1
|
||
@impl Sample2
|
||
def bar, do: 2
|
||
end
|
||
""")
|
||
|
||
assert message =~
|
||
"conflicting behaviours found. Callback function foo/0 is defined by both Sample1 and Sample2 (in module Sample3)"
|
||
|
||
refute message =~ "module attribute @impl was not set"
|
||
refute message =~ "this behaviour does not specify such callback"
|
||
after
|
||
purge([Sample1, Sample2, Sample3])
|
||
end
|
||
|
||
test "duplicate behaviour" do
|
||
assert_warn_eval(
|
||
[
|
||
"nofile:5: ",
|
||
"the behaviour Sample1 has been declared twice (conflict in function foo/0 in module Sample2)"
|
||
],
|
||
"""
|
||
defmodule Sample1 do
|
||
@callback foo :: term
|
||
end
|
||
|
||
defmodule Sample2 do
|
||
@behaviour Sample1
|
||
@behaviour Sample1
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge([Sample1, Sample2])
|
||
end
|
||
|
||
test "unknown remote call" do
|
||
assert capture_compile("""
|
||
defmodule Sample do
|
||
def perform(), do: Unknown.call()
|
||
end
|
||
""") =~
|
||
"Unknown.call/0 is undefined (module Unknown is not available or is yet to be defined)"
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "undefined behaviour" do
|
||
assert_warn_eval(
|
||
["nofile:1: ", "@behaviour UndefinedBehaviour does not exist (in module Sample)"],
|
||
"""
|
||
defmodule Sample do
|
||
@behaviour UndefinedBehaviour
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "empty behaviours" do
|
||
assert_warn_eval(
|
||
["nofile:3: ", "module EmptyBehaviour is not a behaviour (in module Sample)"],
|
||
"""
|
||
defmodule EmptyBehaviour do
|
||
end
|
||
defmodule Sample do
|
||
@behaviour EmptyBehaviour
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
purge(EmptyBehaviour)
|
||
end
|
||
|
||
test "undefined function for protocol" do
|
||
assert_warn_eval(
|
||
[
|
||
"nofile:5: ",
|
||
"function foo/1 required by protocol Sample1 is not implemented (in module Sample1.Atom)"
|
||
],
|
||
"""
|
||
defprotocol Sample1 do
|
||
def foo(subject)
|
||
end
|
||
|
||
defimpl Sample1, for: Atom do
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge([Sample1, Sample1.Atom])
|
||
end
|
||
|
||
test "ungrouped def name" do
|
||
assert_warn_eval(
|
||
[
|
||
"nofile:4:7\n",
|
||
"clauses with the same name should be grouped together, \"def foo/2\" was previously defined (nofile:2)"
|
||
],
|
||
"""
|
||
defmodule Sample do
|
||
def foo(x, 1), do: x + 1
|
||
def foo(), do: nil
|
||
def foo(x, 2), do: x * 2
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "ungrouped def name and arity" do
|
||
assert_warn_eval(
|
||
[
|
||
"nofile:4:7\n",
|
||
"clauses with the same name and arity (number of arguments) should be grouped together, \"def foo/2\" was previously defined (nofile:2)"
|
||
],
|
||
"""
|
||
defmodule Sample do
|
||
def foo(x, 1), do: x + 1
|
||
def bar(), do: nil
|
||
def foo(x, 2), do: x * 2
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "ungrouped defs do not warn for meta programming" do
|
||
assert capture_eval("""
|
||
defmodule Sample do
|
||
for atom <- [:foo, :bar] do
|
||
def from_string(unquote(to_string(atom))), do: unquote(atom)
|
||
def to_string(unquote(atom)), do: unquote(to_string(atom))
|
||
end
|
||
end
|
||
""") == ""
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "warning with overridden file" do
|
||
assert_warn_eval(
|
||
["sample:3:11:", "variable \"x\" is unused"],
|
||
"""
|
||
defmodule Sample do
|
||
@file "sample"
|
||
def foo(x), do: :ok
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "warning on unnecessary code point escape" do
|
||
assert capture_eval("?\\n + ?\\\\") == ""
|
||
|
||
assert_warn_eval(
|
||
["nofile:1:1", "unknown escape sequence ?\\w, use ?w instead"],
|
||
"?\\w"
|
||
)
|
||
end
|
||
|
||
test "warning on code point escape" do
|
||
assert_warn_eval(
|
||
["nofile:1:1", "found ? followed by code point 0x20 (space), please use ?\\s instead"],
|
||
"? "
|
||
)
|
||
|
||
assert_warn_eval(
|
||
["nofile:1:1", "found ?\\ followed by code point 0x20 (space), please use ?\\s instead"],
|
||
"?\\ "
|
||
)
|
||
end
|
||
|
||
test "duplicated docs in the same clause" do
|
||
output =
|
||
capture_eval("""
|
||
defmodule Sample do
|
||
@doc "Something"
|
||
@doc "Another"
|
||
def foo, do: :ok
|
||
|
||
Module.eval_quoted(__MODULE__, quote(do: @doc false))
|
||
@doc "Doc"
|
||
def bar, do: :ok
|
||
end
|
||
""")
|
||
|
||
assert output =~ "redefining @doc attribute previously set at line 2"
|
||
assert output =~ "nofile:3: Sample (module)"
|
||
refute output =~ "nofile:7: "
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "duplicate docs across clauses" do
|
||
assert capture_eval("""
|
||
defmodule Sample1 do
|
||
defmacro __using__(_) do
|
||
quote do
|
||
@doc "hello"
|
||
def add(a, 1), do: a + 1
|
||
end
|
||
end
|
||
end
|
||
|
||
defmodule Sample2 do
|
||
use Sample1
|
||
@doc "world"
|
||
def add(a, 2), do: a + 2
|
||
end
|
||
""") == ""
|
||
|
||
assert_warn_eval(
|
||
["nofile:4: ", "redefining @doc attribute previously set at line"],
|
||
"""
|
||
defmodule Sample3 do
|
||
@doc "hello"
|
||
def add(a, 1), do: a + 1
|
||
@doc "world"
|
||
def add(a, 2), do: a + 2
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge([Sample1, Sample2, Sample3])
|
||
end
|
||
|
||
test "reserved doc metadata keys" do
|
||
{output, diagnostics} =
|
||
Code.with_diagnostics([log: true], fn ->
|
||
capture_eval("""
|
||
defmodule Sample do
|
||
@typedoc opaque: false
|
||
@type t :: binary
|
||
|
||
@doc defaults: 3, since: "1.2.3"
|
||
def foo(a), do: a
|
||
end
|
||
""")
|
||
end)
|
||
|
||
assert output =~ "ignoring reserved documentation metadata key: :opaque"
|
||
assert output =~ "nofile:2: "
|
||
assert output =~ "ignoring reserved documentation metadata key: :defaults"
|
||
assert output =~ "nofile:5: "
|
||
refute output =~ ":since"
|
||
|
||
assert [
|
||
%{
|
||
message: "ignoring reserved documentation metadata key: :opaque",
|
||
position: 2,
|
||
file: "nofile",
|
||
severity: :warning
|
||
},
|
||
%{
|
||
message: "ignoring reserved documentation metadata key: :defaults",
|
||
position: 5,
|
||
file: "nofile",
|
||
severity: :warning
|
||
}
|
||
] = diagnostics
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
describe "typespecs" do
|
||
test "unused types" do
|
||
output =
|
||
capture_eval("""
|
||
defmodule Sample do
|
||
@type pub :: any
|
||
@opaque op :: any
|
||
@typep priv :: any
|
||
@typep priv_args(var1, var2) :: {var1, var2}
|
||
@typep priv2 :: any
|
||
@typep priv3 :: priv2 | atom
|
||
|
||
@spec my_fun(priv3) :: pub
|
||
def my_fun(var), do: var
|
||
end
|
||
""")
|
||
|
||
assert output =~ "nofile:4: "
|
||
assert output =~ "type priv/0 is unused"
|
||
assert output =~ "nofile:5: "
|
||
assert output =~ "type priv_args/2 is unused"
|
||
refute output =~ "type pub/0 is unused"
|
||
refute output =~ "type op/0 is unused"
|
||
refute output =~ "type priv2/0 is unused"
|
||
refute output =~ "type priv3/0 is unused"
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "underspecified opaque types" do
|
||
output =
|
||
capture_eval("""
|
||
defmodule Sample do
|
||
@opaque op1 :: term
|
||
@opaque op2 :: any
|
||
@opaque op3 :: atom
|
||
end
|
||
""")
|
||
|
||
assert output =~ "nofile:2: "
|
||
assert output =~ "@opaque type op1/0 is underspecified and therefore meaningless"
|
||
assert output =~ "nofile:3: "
|
||
assert output =~ "@opaque type op2/0 is underspecified and therefore meaningless"
|
||
refute output =~ "nofile:4: "
|
||
refute output =~ "op3"
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "underscored types variables" do
|
||
output =
|
||
capture_eval("""
|
||
defmodule Sample do
|
||
@type in_typespec_vars(_var1, _var1) :: atom
|
||
@type in_typespec(_var2) :: {atom, _var2}
|
||
|
||
@spec in_spec(_var3) :: {atom, _var3} when _var3: var
|
||
def in_spec(a), do: {:ok, a}
|
||
end
|
||
""")
|
||
|
||
assert output =~ "nofile:2: "
|
||
assert output =~ ~r/the underscored type variable "_var1" is used more than once/
|
||
assert output =~ "nofile:3: "
|
||
assert output =~ ~r/the underscored type variable "_var2" is used more than once/
|
||
assert output =~ "nofile:5: "
|
||
assert output =~ ~r/the underscored type variable "_var3" is used more than once/
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "typedoc on typep" do
|
||
assert_warn_eval(
|
||
[
|
||
"nofile:2: ",
|
||
"type priv/0 is private, @typedoc's are always discarded for private types"
|
||
],
|
||
"""
|
||
defmodule Sample do
|
||
@typedoc "Something"
|
||
@typep priv :: any
|
||
@spec foo() :: priv
|
||
def foo(), do: nil
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "discouraged types" do
|
||
string_discouraged =
|
||
"string() type use is discouraged. " <>
|
||
"For character lists, use charlist() type, for strings, String.t()\n"
|
||
|
||
nonempty_string_discouraged =
|
||
"nonempty_string() type use is discouraged. " <>
|
||
"For non-empty character lists, use nonempty_charlist() type, for strings, String.t()\n"
|
||
|
||
assert_warn_eval(
|
||
[
|
||
"nofile:2: ",
|
||
string_discouraged,
|
||
"nofile:3: ",
|
||
nonempty_string_discouraged
|
||
],
|
||
"""
|
||
defmodule Sample do
|
||
@type foo :: string()
|
||
@type bar :: nonempty_string()
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "nested type annotations" do
|
||
message = "invalid type annotation. Type annotations cannot be nested"
|
||
|
||
assert_warn_eval(
|
||
["nofile:2: ", message],
|
||
"""
|
||
defmodule Sample1 do
|
||
@type my_type :: ann_type :: nested_ann_type :: atom
|
||
end
|
||
"""
|
||
)
|
||
|
||
purge(Sample1)
|
||
|
||
assert_warn_eval(
|
||
["nofile:2: ", message],
|
||
"""
|
||
defmodule Sample2 do
|
||
@type my_type :: ann_type :: nested_ann_type :: atom | port
|
||
end
|
||
"""
|
||
)
|
||
|
||
purge(Sample2)
|
||
|
||
assert_warn_eval(
|
||
["nofile:2: ", message],
|
||
"""
|
||
defmodule Sample3 do
|
||
@spec foo :: {pid, ann_type :: nested_ann_type :: atom}
|
||
def foo, do: nil
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge([Sample1, Sample2, Sample3])
|
||
end
|
||
|
||
test "invalid type annotations" do
|
||
assert_warn_eval(
|
||
[
|
||
"nofile:2: ",
|
||
"invalid type annotation. The left side of :: must be a variable, got: pid()"
|
||
],
|
||
"""
|
||
defmodule Sample1 do
|
||
@type my_type :: (pid() :: atom)
|
||
end
|
||
"""
|
||
)
|
||
|
||
message =
|
||
"invalid type annotation. The left side of :: must be a variable, got: pid | ann_type. " <>
|
||
"Note \"left | right :: ann\" is the same as \"(left | right) :: ann\""
|
||
|
||
assert_warn_eval(
|
||
["nofile:2: ", message],
|
||
"""
|
||
defmodule Sample2 do
|
||
@type my_type :: pid | ann_type :: atom
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge([Sample1, Sample2])
|
||
end
|
||
end
|
||
|
||
test "attribute with no use" do
|
||
assert_warn_eval(
|
||
["nofile:2: ", "module attribute @at was set but never used"],
|
||
"""
|
||
defmodule Sample do
|
||
@at "Something"
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "registered attribute with no use" do
|
||
assert_warn_eval(
|
||
["nofile:3: ", "module attribute @at was set but never used"],
|
||
"""
|
||
defmodule Sample do
|
||
Module.register_attribute(__MODULE__, :at, [])
|
||
@at "Something"
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "typedoc with no type" do
|
||
assert_warn_eval(
|
||
["nofile:2: ", "module attribute @typedoc was set but no type follows it"],
|
||
"""
|
||
defmodule Sample do
|
||
@typedoc "Something"
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "doc with no function" do
|
||
assert_warn_eval(
|
||
["nofile:2: ", "module attribute @doc was set but no definition follows it"],
|
||
"""
|
||
defmodule Sample do
|
||
@doc "Something"
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "pipe without explicit parentheses" do
|
||
assert_warn_eval(
|
||
["nofile:2:1", "parentheses are required when piping into a function call"],
|
||
"""
|
||
[5, 6, 7, 3]
|
||
|> Enum.map_join "", &(Integer.to_string(&1))
|
||
|> String.to_integer
|
||
"""
|
||
)
|
||
end
|
||
|
||
test "keywords without explicit parentheses" do
|
||
assert_warn_eval(
|
||
["nofile:2\n", "missing parentheses for expression following \"label:\" keyword. "],
|
||
"""
|
||
quote do
|
||
IO.inspect arg, label: if true, do: "foo", else: "baz"
|
||
end
|
||
"""
|
||
)
|
||
end
|
||
|
||
test "do+end with operator without explicit parentheses" do
|
||
assert_warn_eval(
|
||
["nofile:3\n", "missing parentheses on expression following operator \"||\""],
|
||
"""
|
||
quote do
|
||
case do
|
||
end || raise 1, 2
|
||
end
|
||
"""
|
||
)
|
||
end
|
||
|
||
test "variable is being expanded to function call (on_undefined_variable: warn)" do
|
||
Code.put_compiler_option(:on_undefined_variable, :warn)
|
||
|
||
output =
|
||
capture_eval("""
|
||
self
|
||
defmodule Sample do
|
||
def my_node(), do: node
|
||
end
|
||
""")
|
||
|
||
assert output =~ "variable \"self\" does not exist and is being expanded to \"self()\""
|
||
assert output =~ "nofile:1:1"
|
||
assert output =~ "variable \"node\" does not exist and is being expanded to \"node()\""
|
||
assert output =~ "nofile:3:22"
|
||
after
|
||
Code.put_compiler_option(:on_undefined_variable, :raise)
|
||
purge(Sample)
|
||
end
|
||
|
||
defmodule User do
|
||
defstruct [:name]
|
||
end
|
||
|
||
test ":__struct__ is ignored when using structs" do
|
||
assert capture_err(fn ->
|
||
code = """
|
||
assert %Kernel.WarningTest.User{__struct__: Ignored, name: "joe"} ==
|
||
%Kernel.WarningTest.User{name: "joe"}
|
||
"""
|
||
|
||
Code.eval_string(code, [], __ENV__)
|
||
end) =~ "key :__struct__ is ignored when using structs"
|
||
|
||
assert capture_err(fn ->
|
||
code = """
|
||
user = %Kernel.WarningTest.User{name: "meg"}
|
||
assert %Kernel.WarningTest.User{user | __struct__: Ignored, name: "joe"} ==
|
||
%Kernel.WarningTest.User{__struct__: Kernel.WarningTest.User, name: "joe"}
|
||
"""
|
||
|
||
Code.eval_string(code, [], __ENV__)
|
||
end) =~ "key :__struct__ is ignored when using structs"
|
||
end
|
||
|
||
test "catch comes before rescue in try block" do
|
||
assert_warn_eval(
|
||
["nofile:1:1\n", ~s("catch" should always come after "rescue" in try)],
|
||
"""
|
||
try do
|
||
:trying
|
||
catch
|
||
_ -> :caught
|
||
rescue
|
||
_ -> :error
|
||
end
|
||
"""
|
||
)
|
||
end
|
||
|
||
test "catch comes before rescue in def" do
|
||
assert_warn_eval(
|
||
["nofile:2:7\n", ~s("catch" should always come after "rescue" in def)],
|
||
"""
|
||
defmodule Sample do
|
||
def foo do
|
||
:trying
|
||
catch
|
||
_, _ -> :caught
|
||
rescue
|
||
_ -> :error
|
||
end
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "unused variable in defguard" do
|
||
assert_warn_eval(
|
||
["nofile:2:21", "variable \"baz\" is unused"],
|
||
"""
|
||
defmodule Sample do
|
||
defguard foo(bar, baz) when bar
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "unused import in defguard" do
|
||
assert_warn_eval(
|
||
["nofile:2:3", "unused import Record\n"],
|
||
"""
|
||
defmodule Sample do
|
||
import Record
|
||
defguard is_record(baz) when baz
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "unused private guard" do
|
||
assert_warn_eval(
|
||
["nofile:2:13: ", "macro foo/2 is unused\n"],
|
||
"""
|
||
defmodule Sample do
|
||
defguardp foo(bar, baz) when bar + baz
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "defguard overriding defmacro" do
|
||
assert_warn_eval(
|
||
[
|
||
"nofile:3\n",
|
||
~r"this clause( for foo/1)? cannot match because a previous clause at line 2 always matches"
|
||
],
|
||
"""
|
||
defmodule Sample do
|
||
defmacro foo(bar), do: bar == :bar
|
||
defguard foo(baz) when baz == :baz
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "defmacro overriding defguard" do
|
||
assert_warn_eval(
|
||
[
|
||
"nofile:3\n",
|
||
~r"this clause( for foo/1)? cannot match because a previous clause at line 2 always matches"
|
||
],
|
||
"""
|
||
defmodule Sample do
|
||
defguard foo(baz) when baz == :baz
|
||
defmacro foo(bar), do: bar == :bar
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "deprecated GenServer super on callbacks" do
|
||
assert_warn_eval(
|
||
["nofile:1: ", "calling super for GenServer callback handle_call/3 is deprecated"],
|
||
"""
|
||
defmodule Sample do
|
||
use GenServer
|
||
|
||
def handle_call(a, b, c) do
|
||
super(a, b, c)
|
||
end
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "super is allowed on GenServer.child_spec/1" do
|
||
refute capture_eval("""
|
||
defmodule Sample do
|
||
use GenServer
|
||
|
||
def child_spec(opts) do
|
||
super(opts)
|
||
end
|
||
end
|
||
""") =~ "calling super for GenServer callback child_spec/1 is deprecated"
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "nested comparison operators" do
|
||
message =
|
||
capture_compile("""
|
||
1 < 3 < 5
|
||
""")
|
||
|
||
assert message =~ "nofile:1:8\n"
|
||
assert message =~ "Elixir does not support nested comparisons"
|
||
assert message =~ "1 < 3 < 5"
|
||
|
||
message =
|
||
capture_compile("""
|
||
x = 5
|
||
y = 7
|
||
1 < x < y < 10
|
||
""")
|
||
|
||
assert message =~ "Elixir does not support nested comparisons"
|
||
assert message =~ "1 < x < y < 10"
|
||
assert message =~ "nofile:3:9\n"
|
||
assert message =~ "nofile:3:13\n"
|
||
end
|
||
|
||
test "def warns if only clause is else" do
|
||
assert_warn_compile(
|
||
["nofile:2:7\n", "\"else\" shouldn't be used as the only clause in \"def\""],
|
||
"""
|
||
defmodule Sample do
|
||
def foo do
|
||
:bar
|
||
else
|
||
_other -> :ok
|
||
end
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(Sample)
|
||
end
|
||
|
||
test "try warns if only clause is else" do
|
||
assert_warn_compile(
|
||
["nofile:1:1\n", "\"else\" shouldn't be used as the only clause in \"try\""],
|
||
"""
|
||
try do
|
||
:ok
|
||
else
|
||
other -> other
|
||
end
|
||
"""
|
||
)
|
||
end
|
||
|
||
test "sigil w/W warns on trailing comma at macro expansion time" do
|
||
for sigil <- ~w(w W),
|
||
modifier <- ~w(a s c) do
|
||
output =
|
||
capture_err(fn ->
|
||
{:ok, ast} =
|
||
"~#{sigil}(foo, bar baz)#{modifier}"
|
||
|> Code.string_to_quoted()
|
||
|
||
Macro.expand(ast, __ENV__)
|
||
end)
|
||
|
||
assert output =~ "the sigils ~w/~W do not allow trailing commas"
|
||
end
|
||
end
|
||
|
||
test "warnings on trailing comma on call" do
|
||
assert_warn_eval(
|
||
["nofile:1:25\n", "trailing commas are not allowed inside function/macro call arguments"],
|
||
"Keyword.merge([], foo: 1,)"
|
||
)
|
||
end
|
||
|
||
test "defstruct warns with duplicate keys" do
|
||
assert_warn_eval(
|
||
["nofile:2: TestMod", "duplicate key :foo found in struct"],
|
||
"""
|
||
defmodule TestMod do
|
||
defstruct [:foo, :bar, foo: 1]
|
||
end
|
||
"""
|
||
)
|
||
after
|
||
purge(TestMod)
|
||
end
|
||
|
||
test "deprecate nullary remote zero-arity capture with parens" do
|
||
assert capture_eval("""
|
||
import System, only: [pid: 0]
|
||
&pid/0
|
||
""") == ""
|
||
|
||
assert_warn_eval(
|
||
[
|
||
"nofile:1:1\n",
|
||
"extra parentheses on a remote function capture &System.pid()/0 have been deprecated. Please remove the parentheses: &System.pid/0"
|
||
],
|
||
"""
|
||
&System.pid()/0
|
||
"""
|
||
)
|
||
end
|
||
|
||
defp assert_compile_error(messages, string) do
|
||
captured =
|
||
capture_err(fn ->
|
||
assert_raise CompileError, fn ->
|
||
ast = Code.string_to_quoted!(string, columns: true)
|
||
Code.eval_quoted(ast)
|
||
end
|
||
end)
|
||
|
||
for message <- List.wrap(messages) do
|
||
assert captured =~ message
|
||
end
|
||
end
|
||
|
||
defp purge(list) when is_list(list) do
|
||
Enum.each(list, &purge/1)
|
||
end
|
||
|
||
defp purge(module) when is_atom(module) do
|
||
:code.delete(module)
|
||
:code.purge(module)
|
||
end
|
||
end
|