From e3e77688ff466fa602b8a49dadbf493c5eb3ff2b Mon Sep 17 00:00:00 2001 From: Angelika Tyborska Date: Tue, 30 Jan 2024 06:35:42 +0100 Subject: [PATCH] Rewrite file-sniffer analysis (#412) * Update submodule * Add ideal file-sniffer analysis implementation * Add tests for assert_call * Show in test that special forms are local functions * Add one more test case --- elixir | 2 +- lib/elixir_analyzer/constants.ex | 2 +- .../test_suite/file_sniffer.ex | 154 +----------------- .../assert_call/function_head_call_test.exs | 114 +++++++++++++ .../assert_call/kernel_functions_test.exs | 28 +++- .../test_suite/file_sniffer_test.exs | 123 ++++++++++---- .../assert_call/function_head_call.ex | 35 ++++ .../assert_call/kernel_functions.ex | 6 + 8 files changed, 287 insertions(+), 177 deletions(-) create mode 100644 test/elixir_analyzer/exercise_test/assert_call/function_head_call_test.exs create mode 100644 test/support/analyzer_verification/assert_call/function_head_call.ex diff --git a/elixir b/elixir index 8d74999e..f387a440 160000 --- a/elixir +++ b/elixir @@ -1 +1 @@ -Subproject commit 8d74999e643302cd02057e32b51168f6d60e592c +Subproject commit f387a4405e63ea89fb876adbb0b79aee0955734e diff --git a/lib/elixir_analyzer/constants.ex b/lib/elixir_analyzer/constants.ex index 8f2a68f0..19247b35 100644 --- a/lib/elixir_analyzer/constants.ex +++ b/lib/elixir_analyzer/constants.ex @@ -90,7 +90,7 @@ defmodule ElixirAnalyzer.Constants do dna_encoding_use_tail_call_recursion: "elixir.dna-encoding.use_tail_call_recursion", # File Sniffer Comments - file_sniffer_use_pattern_matching: "elixir.file-sniffer.use_pattern_matching", + file_sniffer_use_bitstring: "elixir.file-sniffer.use_bitstring", # Freelancer Rates Comments freelancer_rates_apply_discount_function_reuse: diff --git a/lib/elixir_analyzer/test_suite/file_sniffer.ex b/lib/elixir_analyzer/test_suite/file_sniffer.ex index f74a8fd4..01c1b1c7 100644 --- a/lib/elixir_analyzer/test_suite/file_sniffer.ex +++ b/lib/elixir_analyzer/test_suite/file_sniffer.ex @@ -6,155 +6,17 @@ defmodule ElixirAnalyzer.TestSuite.FileSniffer do alias ElixirAnalyzer.Constants use ElixirAnalyzer.ExerciseTest - feature "use pattern matching for bmp" do + assert_call "use <<>> to pattern match a bitstring in function body" do type :essential - find :any - comment Constants.file_sniffer_use_pattern_matching() - - form do - <<0x42, 0x4D, _ignore>> - end - - form do - <<0x42, 0x4D>> - end - - form do - <<0x42::8, 0x4D::8, _ignore>> - end - - form do - <<0x42::8, 0x4D::8>> - end - - form do - <<0x42::size(8), 0x4D::size(8), _ignore>> - end - - form do - <<0x42::size(8), 0x4D::size(8)>> - end - end - - feature "use pattern matching for png" do - type :essential - find :any - comment Constants.file_sniffer_use_pattern_matching() - - form do - <<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, _ignore>> - end - - form do - <<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A>> - end - - form do - <<0x89::8, 0x50::8, 0x4E::8, 0x47::8, 0x0D::8, 0x0A::8, 0x1A::8, 0x0A::8, _ignore>> - end - - form do - <<0x89::8, 0x50::8, 0x4E::8, 0x47::8, 0x0D::8, 0x0A::8, 0x1A::8, 0x0A::8>> - end - - form do - <<0x89::size(8), 0x50::size(8), 0x4E::size(8), 0x47::size(8), 0x0D::size(8), 0x0A::size(8), - 0x1A::size(8), 0x0A::size(8), _ignore>> - end - - form do - <<0x89::size(8), 0x50::size(8), 0x4E::size(8), 0x47::size(8), 0x0D::size(8), 0x0A::size(8), - 0x1A::size(8), 0x0A::size(8)>> - end + comment Constants.file_sniffer_use_bitstring() + called_fn name: :<<>> + calling_fn module: FileSniffer, name: :type_from_binary end - feature "use pattern matching for jpg" do + assert_call "use :: in bitstrings to specify segment type when pattern matching in function body" do type :essential - find :any - comment Constants.file_sniffer_use_pattern_matching() - - form do - <<0xFF, 0xD8, 0xFF, _ignore>> - end - - form do - <<0xFF, 0xD8, 0xFF>> - end - - form do - <<0xFF::8, 0xD8::8, 0xFF::8, _ignore>> - end - - form do - <<0xFF::8, 0xD8::8, 0xFF::8>> - end - - form do - <<0xFF::size(8), 0xD8::size(8), 0xFF::size(8), _ignore>> - end - - form do - <<0xFF::size(8), 0xD8::size(8), 0xFF::size(8)>> - end - end - - feature "use pattern matching for gif" do - type :essential - find :any - comment Constants.file_sniffer_use_pattern_matching() - - form do - <<0x47, 0x49, 0x46, _ignore>> - end - - form do - <<0x47, 0x49, 0x46>> - end - - form do - <<0x47::8, 0x49::8, 0x46::8, _ignore>> - end - - form do - <<0x47::8, 0x49::8, 0x46::8>> - end - - form do - <<0x47::size(8), 0x49::size(8), 0x46::size(8), _ignore>> - end - - form do - <<0x47::size(8), 0x49::size(8), 0x46::size(8)>> - end - end - - feature "use pattern matching for exe" do - type :essential - find :any - comment Constants.file_sniffer_use_pattern_matching() - - form do - <<0x7F, 0x45, 0x4C, 0x46, _ignore>> - end - - form do - <<0x7F, 0x45, 0x4C, 0x46>> - end - - form do - <<0x7F::8, 0x45::8, 0x4C::8, 0x46::8, _ignore>> - end - - form do - <<0x7F::8, 0x45::8, 0x4C::8, 0x46::8>> - end - - form do - <<0x7F::size(8), 0x45::size(8), 0x4C::size(8), 0x46::size(8), _ignore>> - end - - form do - <<0x7F::size(8), 0x45::size(8), 0x4C::size(8), 0x46::size(8)>> - end + comment Constants.file_sniffer_use_bitstring() + called_fn name: :"::" + calling_fn module: FileSniffer, name: :type_from_binary end end diff --git a/test/elixir_analyzer/exercise_test/assert_call/function_head_call_test.exs b/test/elixir_analyzer/exercise_test/assert_call/function_head_call_test.exs new file mode 100644 index 00000000..cd997d8e --- /dev/null +++ b/test/elixir_analyzer/exercise_test/assert_call/function_head_call_test.exs @@ -0,0 +1,114 @@ +defmodule ElixirAnalyzer.ExerciseTest.AssertCall.FunctionHeadCallTest do + use ElixirAnalyzer.ExerciseTestCase, + exercise_test_module: ElixirAnalyzer.Support.AnalyzerVerification.AssertCall.FunctionHeadCall + + test_exercise_analysis "calls in function head count", comments: [] do + [ + defmodule AssertCallVerification do + def main_function(@jackpot) do + :win + end + + def main_function(x) when x > 100 do + :too_much + end + + def main_function([x | rest]) when is_integer(x) do + main_function(x) + end + end, + defmodule AssertCallVerification do + def main_function(@jackpot) do + :win + end + + def main_function([x | rest]) when is_integer(x) and x > 100 do + :too_much + end + end, + defmodule AssertCallVerification do + def main_function(x) when x == @jackpot do + :win + end + + def main_function(a, b, [x | rest]) when is_integer(x) and x > 100 do + :too_much + end + end, + defmodule AssertCallVerification do + def main_function(x) when x == @jackpot do + :win + end + + def main_function(%{list: [x | rest]}) when is_integer(x) and x > 100 do + :too_much + end + end + ] + end + + test_exercise_analysis "indirect calls in function head count", comments: [] do + [ + defmodule AssertCallVerification do + def main_function(x) do + helper_function(x) + end + + def helper_function(@jackpot) do + :win + end + + def helper_function([x | rest]) when is_integer(x) do + main_function(x) + end + + defp helper_function(x) when x > 100 do + :too_much + end + end + ] + end + + test_exercise_analysis "calls from the wrong function", + comments: [ + "didn't find any call to Kernel.is_integer/1 from main_function/1", + "didn't find any call to |/2 from main_function/1", + "didn't find any call to Kernel.@/1 from main_function/1" + ] do + [ + defmodule AssertCallVerification do + def main_function() do + nil + end + + def wrong_function(@jackpot) do + :win + end + + def wrong_function(x) when x > 100 do + :too_much + end + + def wrong_function([x | rest]) when is_integer(x) do + wrong_function(x) + end + end + ] + end + + test_exercise_analysis "no calls", + comments: [ + "didn't find any call to Kernel.>/2 from anywhere", + "didn't find any call to Kernel.is_integer/1 from main_function/1", + "didn't find any call to |/2 from main_function/1", + "didn't find any call to Kernel.@/1 from main_function/1" + ] do + [ + defmodule AssertCallVerification do + def main_function(x) when x >= 100 do + :too_much + end + end + ] + end +end diff --git a/test/elixir_analyzer/exercise_test/assert_call/kernel_functions_test.exs b/test/elixir_analyzer/exercise_test/assert_call/kernel_functions_test.exs index d12e064d..0e1528ca 100644 --- a/test/elixir_analyzer/exercise_test/assert_call/kernel_functions_test.exs +++ b/test/elixir_analyzer/exercise_test/assert_call/kernel_functions_test.exs @@ -7,7 +7,8 @@ defmodule ElixirAnalyzer.ExerciseTest.AssertCall.KernelTest do "didn't find a call to Kernel.dbg/0", "didn't find a call to dbg/0", "didn't find a call to Kernel.self/0", - "didn't find a call to self/0" + "didn't find a call to self/0", + "didn't find a call to <<>>/1" ] do defmodule AssertCallVerification do def function() do @@ -62,4 +63,29 @@ defmodule ElixirAnalyzer.ExerciseTest.AssertCall.KernelTest do end ] end + + test_exercise_analysis "Calling <<>>/1", + comments_exclude: [ + "didn't find a call to <<>>/1" + ] do + [ + defmodule AssertCallVerification do + def function() do + <> = <<1, 2, 3>> + a + end + end, + defmodule AssertCallVerification do + def function() do + <> = <<1, 2, 3>> + + if b > 2 do + a + c + else + nil + end + end + end + ] + end end diff --git a/test/elixir_analyzer/test_suite/file_sniffer_test.exs b/test/elixir_analyzer/test_suite/file_sniffer_test.exs index 5832cc37..7e19f6dc 100644 --- a/test/elixir_analyzer/test_suite/file_sniffer_test.exs +++ b/test/elixir_analyzer/test_suite/file_sniffer_test.exs @@ -55,31 +55,37 @@ defmodule ElixirAnalyzer.ExerciseTest.FileSnifferTest do def type_from_binary(<>), do: "application/octet-stream" end, - def type_from_binary(file_binary) do - case file_binary do - <<0x7F, 0x45, 0x4C, 0x46, rest::binary>> -> "application/octet-stream" - <<0x42, 0x4D, rest::binary>> -> "image/bmp" - <<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, rest::binary>> -> "image/png" - <<0xFF, 0xD8, 0xFF, rest::binary>> -> "image/jpg" - <<0x47, 0x49, 0x46, rest::binary>> -> "image/gif" + defmodule FileSniffer do + def type_from_binary(file_binary) do + case file_binary do + <<0x7F, 0x45, 0x4C, 0x46, rest::binary>> -> "application/octet-stream" + <<0x42, 0x4D, rest::binary>> -> "image/bmp" + <<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, rest::binary>> -> "image/png" + <<0xFF, 0xD8, 0xFF, rest::binary>> -> "image/jpg" + <<0x47, 0x49, 0x46, rest::binary>> -> "image/gif" + end end end, - def type_from_binary(file_binary) do - case file_binary do - <<0x7F, 0x45, 0x4C, 0x46, rest::bitstring>> -> "application/octet-stream" - <<0x42, 0x4D, rest::bitstring>> -> "image/bmp" - <<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, rest::bitstring>> -> "image/png" - <<0xFF, 0xD8, 0xFF, rest::bitstring>> -> "image/jpg" - <<0x47, 0x49, 0x46, rest::bitstring>> -> "image/gif" + defmodule FileSniffer do + def type_from_binary(file_binary) do + case file_binary do + <<0x7F, 0x45, 0x4C, 0x46, rest::bitstring>> -> "application/octet-stream" + <<0x42, 0x4D, rest::bitstring>> -> "image/bmp" + <<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, rest::bitstring>> -> "image/png" + <<0xFF, 0xD8, 0xFF, rest::bitstring>> -> "image/jpg" + <<0x47, 0x49, 0x46, rest::bitstring>> -> "image/gif" + end end end, - def type_from_binary(file_binary) do - case file_binary do - <<0x7F, 0x45, 0x4C, 0x46, rest::bits>> -> "application/octet-stream" - <<0x42, 0x4D, rest::bits>> -> "image/bmp" - <<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, rest::bits>> -> "image/png" - <<0xFF, 0xD8, 0xFF, rest::bits>> -> "image/jpg" - <<0x47, 0x49, 0x46, rest::bits>> -> "image/gif" + defmodule FileSniffer do + def type_from_binary(file_binary) do + case file_binary do + <<0x7F, 0x45, 0x4C, 0x46, rest::bits>> -> "application/octet-stream" + <<0x42, 0x4D, rest::bits>> -> "image/bmp" + <<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, rest::bits>> -> "image/png" + <<0xFF, 0xD8, 0xFF, rest::bits>> -> "image/jpg" + <<0x47, 0x49, 0x46, rest::bits>> -> "image/gif" + end end end, defmodule FileSniffer do @@ -147,12 +153,62 @@ defmodule ElixirAnalyzer.ExerciseTest.FileSnifferTest do "image/gif" end end + end, + defmodule FileSniffer do + @exe_type "application/octet-stream" + @bmp_type "image/bmp" + @png_type "image/png" + @jpg_type "image/jpg" + @gif_type "image/gif" + + def type_from_binary(<<"\dELF", _rest::binary>>), do: @exe_type + def type_from_binary(<<"BM", _rest::binary>>), do: @bmp_type + def type_from_binary(<<"\x89PNG\r\n\x1A\n", _rest::binary>>), do: @png_type + def type_from_binary(<<"\xFF\xD8\xFF", _rest::binary>>), do: @jpg_type + def type_from_binary(<<"GIF", _rest::binary>>), do: @gif_type + end, + defmodule FileSniffer do + def type_from_binary(<>) + when <<0x42, 0x4D>> == signature, + do: "image/bmp" + + def type_from_binary(<>) + when <<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A>> == signature, + do: "image/png" + + def type_from_binary(<>) + when <<0xFF, 0xD8, 0xFF>> == signature, + do: "image/jpg" + + def type_from_binary(<>) + when <<0x47, 0x49, 0x46>> == signature, + do: "image/gif" + + def type_from_binary(<>) + when <<0x7F, 0x45, 0x4C, 0x46>> == signature, + do: "application/octet-stream" + + def type_from_binary(_binary), do: nil + end, + defmodule FileSniffer do + def type_from_binary(<<66, 77, 30, _::binary>>), do: type_from_extension("bmp") + + def type_from_binary(<<71, 73, 70, 56, 57, 97, _::binary>>), + do: type_from_extension("gif") + + def type_from_binary(<<255, 216, 255, 219, _::binary>>), do: type_from_extension("jpg") + + def type_from_binary(<<137, 80, 78, 71, 13, 10, 26, 10, _::binary>>), + do: type_from_extension("png") + + def type_from_binary(<<127, 69, 76, 70, _::binary>>), do: type_from_extension("exe") + def type_from_binary(file), do: type_from_extension(file) end ] end - test_exercise_analysis "doesn't use pattern matching", - comments: [Constants.file_sniffer_use_pattern_matching()] do + test_exercise_analysis "doesn't use <<>> nor ::", + comments: [Constants.file_sniffer_use_bitstring()] do [ defmodule FileSniffer do def type_from_binary(file) do @@ -160,16 +216,16 @@ defmodule ElixirAnalyzer.ExerciseTest.FileSnifferTest do String.starts_with?(file, "BM") -> "image/bmp" - String.starts_with?(file, <<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A>>) -> + String.starts_with?(file, "\x89PNG\r\n\x1A\n") -> "image/png" - String.starts_with?(file, <<0xFF, 0xD8, 0xFF>>) -> + String.starts_with?(file, "\xFF\xD8\xFF") -> "image/jpg" String.starts_with?(file, "GIF") -> "image/gif" - String.starts_with?(file, <<0x7F, 0x45, 0x4C, 0x46>>) -> + String.starts_with?(file, "\dELF") -> "application/octet-stream" end end @@ -177,12 +233,23 @@ defmodule ElixirAnalyzer.ExerciseTest.FileSnifferTest do defmodule FileSniffer do def type_from_binary("BM" <> _rest), do: "image/bmp" - def type_from_binary(<<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, _::binary>>), + def type_from_binary("\x89PNG\r\n\x1A\n" <> _rest), do: "image/png" - def type_from_binary(<<0xFF, 0xD8, 0xFF, _::binary>>), do: "image/jpg" + def type_from_binary("\xFF\xD8\xFF" <> _rest), do: "image/jpg" def type_from_binary("GIF" <> _rest), do: "image/gif" def type_from_binary("\dELF" <> _rest), do: "application/octet-stream" + end, + defmodule FileSniffer do + def type_from_binary(<<0x42, 0x4D>> <> _rest), do: "image/bmp" + + def type_from_binary(<<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A>> <> _rest), + do: "image/png" + + def type_from_binary(<<0xFF, 0xD8, 0xFF>> <> _rest), do: "image/jpg" + def type_from_binary(<<0x47, 0x49, 0x46>> <> _rest), do: "image/gif" + def type_from_binary(<<0x7F, 0x45, 0x4C, 0x46>> <> _rest), do: "application/octet-stream" + def type_from_binary(_), do: nil end ] end diff --git a/test/support/analyzer_verification/assert_call/function_head_call.ex b/test/support/analyzer_verification/assert_call/function_head_call.ex new file mode 100644 index 00000000..a140dc05 --- /dev/null +++ b/test/support/analyzer_verification/assert_call/function_head_call.ex @@ -0,0 +1,35 @@ +defmodule ElixirAnalyzer.Support.AnalyzerVerification.AssertCall.FunctionHeadCall do + @moduledoc """ + This is an exercise analyzer extension module to test assert_call calling a function from + outside any function/macro bodies. + """ + + use ElixirAnalyzer.ExerciseTest + + assert_call "find a call to Kernel.>/2" do + type :informative + called_fn module: Kernel, name: :> + comment "didn't find any call to Kernel.>/2 from anywhere" + end + + assert_call "find a call to Kernel.is_integer/1 from main_function" do + type :informative + called_fn module: Kernel, name: :is_integer + calling_fn module: AssertCallVerification, name: :main_function + comment "didn't find any call to Kernel.is_integer/1 from main_function/1" + end + + assert_call "find a call to |/2 from main_function" do + type :informative + called_fn name: :| + calling_fn module: AssertCallVerification, name: :main_function + comment "didn't find any call to |/2 from main_function/1" + end + + assert_call "find a call to Kernel.@/1 from main_function" do + type :informative + called_fn module: Kernel, name: :@ + calling_fn module: AssertCallVerification, name: :main_function + comment "didn't find any call to Kernel.@/1 from main_function/1" + end +end diff --git a/test/support/analyzer_verification/assert_call/kernel_functions.ex b/test/support/analyzer_verification/assert_call/kernel_functions.ex index 9379589e..b64244a4 100644 --- a/test/support/analyzer_verification/assert_call/kernel_functions.ex +++ b/test/support/analyzer_verification/assert_call/kernel_functions.ex @@ -29,4 +29,10 @@ defmodule ElixirAnalyzer.Support.AnalyzerVerification.AssertCall.Kernel do called_fn name: :self comment "didn't find a call to self/0" end + + assert_call "finds call to Kernel.SpecialForms.<<>>/1 without module" do + type :informative + called_fn name: :<<>> + comment "didn't find a call to <<>>/1" + end end