diff --git a/config/config.exs b/config/config.exs index 3cbc3c08..f2dc74cc 100644 --- a/config/config.exs +++ b/config/config.exs @@ -63,6 +63,9 @@ config :elixir_analyzer, "german-sysadmin" => %{ analyzer_module: ElixirAnalyzer.TestSuite.GermanSysadmin }, + "gotta-snatch-em-all" => %{ + analyzer_module: ElixirAnalyzer.TestSuite.GottaSnatchEmAll + }, "high-school-sweetheart" => %{ analyzer_module: ElixirAnalyzer.TestSuite.HighSchoolSweetheart }, diff --git a/lib/elixir_analyzer/constants.ex b/lib/elixir_analyzer/constants.ex index 19247b35..6a7b7c30 100644 --- a/lib/elixir_analyzer/constants.ex +++ b/lib/elixir_analyzer/constants.ex @@ -100,6 +100,34 @@ defmodule ElixirAnalyzer.Constants do german_sysadmin_no_string: "elixir.german-sysadmin.no_string", german_sysadmin_use_case: "elixir.german-sysadmin.use_case", + # Gotta Snatch Em All Comments + gotta_snatch_em_all_add_card_use_mapset_member_and_put: + "elixir.gotta-snatch-em-all.add_card_use_mapset_member_and_put", + gotta_snatch_em_all_trade_card_use_mapset_member_put_and_delete: + "elixir.gotta-snatch-em-all.trade_card_use_mapset_member_put_and_delete", + gotta_snatch_em_all_remove_duplicates_use_mapset_new: + "elixir.gotta-snatch-em-all.remove_duplicates_use_mapset_new", + gotta_snatch_em_all_remove_duplicates_use_enum_sort: + "elixir.gotta-snatch-em-all.remove_duplicates_use_enum_sort", + gotta_snatch_em_all_remove_duplicates_do_not_use_enum_uniq: + "elixir.gotta-snatch-em-all.remove_duplicates_do_not_use_enum_uniq", + gotta_snatch_em_all_extra_cards_use_mapset_difference_and_size: + "elixir.gotta-snatch-em-all.extra_cards_use_mapset_difference_and_size", + gotta_snatch_em_all_boring_cards_use_mapset_intersection: + "elixir.gotta-snatch-em-all.boring_cards_use_mapset_intersection", + gotta_snatch_em_all_boring_cards_use_enum_sort: + "elixir.gotta-snatch-em-all.boring_cards_use_enum_sort", + gotta_snatch_em_all_boring_cards_use_enum_reduce: + "elixir.gotta-snatch-em-all.boring_cards_use_enum_reduce", + gotta_snatch_em_all_total_cards_use_mapset_union_and_size: + "elixir.gotta-snatch-em-all.total_cards_use_mapset_union_and_size", + gotta_snatch_em_all_total_cards_use_enum_reduce: + "elixir.gotta-snatch-em-all.total_cards_use_enum_reduce", + gotta_snatch_em_all_split_shiny_cards_use_enum_sort: + "elixir.gotta-snatch-em-all.shiny_cards_use_enum_sort", + gotta_snatch_em_all_split_shiny_cards_use_string_starts_with: + "elixir.gotta-snatch-em-all.shiny_cards_use_string_starts_with", + # Guessing Game Comments guessing_game_use_default_argument: "elixir.guessing-game.use_default_argument", guessing_game_use_multiple_clause_functions: diff --git a/lib/elixir_analyzer/test_suite/gotta-snatch-em-all.ex b/lib/elixir_analyzer/test_suite/gotta-snatch-em-all.ex new file mode 100644 index 00000000..e11a9a65 --- /dev/null +++ b/lib/elixir_analyzer/test_suite/gotta-snatch-em-all.ex @@ -0,0 +1,139 @@ +defmodule ElixirAnalyzer.TestSuite.GottaSnatchEmAll do + @moduledoc """ + This is an exercise analyzer extension module for the concept exercise Gotta Snatch Em All + """ + use ElixirAnalyzer.ExerciseTest + alias ElixirAnalyzer.Constants + + assert_call "add_card uses MapSet.member?" do + type :essential + calling_fn module: GottaSnatchEmAll, name: :add_card + called_fn module: MapSet, name: :member? + comment Constants.gotta_snatch_em_all_add_card_use_mapset_member_and_put() + end + + assert_call "add_card uses MapSet.put" do + type :essential + calling_fn module: GottaSnatchEmAll, name: :add_card + called_fn module: MapSet, name: :put + comment Constants.gotta_snatch_em_all_add_card_use_mapset_member_and_put() + suppress_if "add_card uses MapSet.member?", :fail + end + + assert_call "trade_card uses MapSet.member?" do + type :essential + calling_fn module: GottaSnatchEmAll, name: :trade_card + called_fn module: MapSet, name: :member? + comment Constants.gotta_snatch_em_all_trade_card_use_mapset_member_put_and_delete() + end + + assert_call "trade_card uses MapSet.put" do + type :essential + calling_fn module: GottaSnatchEmAll, name: :trade_card + called_fn module: MapSet, name: :put + comment Constants.gotta_snatch_em_all_trade_card_use_mapset_member_put_and_delete() + suppress_if "trade_card uses MapSet.member?", :fail + end + + assert_call "trade_card uses MapSet.delete" do + type :essential + calling_fn module: GottaSnatchEmAll, name: :trade_card + called_fn module: MapSet, name: :delete + comment Constants.gotta_snatch_em_all_trade_card_use_mapset_member_put_and_delete() + suppress_if "trade_card uses MapSet.member?", :fail + suppress_if "trade_card uses MapSet.put", :fail + end + + assert_call "remove_duplicates uses MapSet.new" do + type :essential + calling_fn module: GottaSnatchEmAll, name: :remove_duplicates + called_fn module: MapSet, name: :new + comment Constants.gotta_snatch_em_all_remove_duplicates_use_mapset_new() + end + + assert_call "remove_duplicates uses Enum.sort" do + type :essential + calling_fn module: GottaSnatchEmAll, name: :remove_duplicates + called_fn module: Enum, name: :sort + comment Constants.gotta_snatch_em_all_remove_duplicates_use_enum_sort() + end + + assert_no_call "remove_duplicates does not use Enum.uniq" do + type :essential + calling_fn module: GottaSnatchEmAll, name: :remove_duplicates + called_fn module: Enum, name: :uniq + comment Constants.gotta_snatch_em_all_remove_duplicates_do_not_use_enum_uniq() + end + + assert_call "extra_cards uses MapSet.difference" do + type :essential + calling_fn module: GottaSnatchEmAll, name: :extra_cards + called_fn module: MapSet, name: :difference + comment Constants.gotta_snatch_em_all_extra_cards_use_mapset_difference_and_size() + end + + assert_call "extra_cards uses MapSet.size" do + type :essential + calling_fn module: GottaSnatchEmAll, name: :extra_cards + called_fn module: MapSet, name: :size + comment Constants.gotta_snatch_em_all_extra_cards_use_mapset_difference_and_size() + suppress_if "extra_cards uses MapSet.difference", :fail + end + + assert_call "boring_cards uses MapSet.intersection" do + type :essential + calling_fn module: GottaSnatchEmAll, name: :boring_cards + called_fn module: MapSet, name: :intersection + comment Constants.gotta_snatch_em_all_boring_cards_use_mapset_intersection() + end + + assert_call "boring_cards uses Enum.sort" do + type :essential + calling_fn module: GottaSnatchEmAll, name: :boring_cards + called_fn module: Enum, name: :sort + comment Constants.gotta_snatch_em_all_boring_cards_use_enum_sort() + end + + assert_call "boring_cards uses Enum.reduce" do + type :actionable + calling_fn module: GottaSnatchEmAll, name: :boring_cards + called_fn module: Enum, name: :reduce + comment Constants.gotta_snatch_em_all_boring_cards_use_enum_reduce() + end + + assert_call "total_cards uses MapSet.union" do + type :essential + calling_fn module: GottaSnatchEmAll, name: :total_cards + called_fn module: MapSet, name: :union + comment Constants.gotta_snatch_em_all_total_cards_use_mapset_union_and_size() + end + + assert_call "total_cards uses MapSet.size" do + type :essential + calling_fn module: GottaSnatchEmAll, name: :total_cards + called_fn module: MapSet, name: :size + comment Constants.gotta_snatch_em_all_total_cards_use_mapset_union_and_size() + suppress_if "total_cards uses MapSet.union", :fail + end + + assert_call "total_cards uses Enum.reduce" do + type :actionable + calling_fn module: GottaSnatchEmAll, name: :total_cards + called_fn module: Enum, name: :reduce + comment Constants.gotta_snatch_em_all_total_cards_use_enum_reduce() + end + + assert_call "split_shiny_cards uses Enum.sort" do + type :essential + calling_fn module: GottaSnatchEmAll, name: :split_shiny_cards + called_fn module: Enum, name: :sort + comment Constants.gotta_snatch_em_all_split_shiny_cards_use_enum_sort() + end + + assert_call "split_shiny_cards uses String.starts_with" do + type :actionable + calling_fn module: GottaSnatchEmAll, name: :split_shiny_cards + called_fn module: String, name: :starts_with? + comment Constants.gotta_snatch_em_all_split_shiny_cards_use_string_starts_with() + end +end diff --git a/test/elixir_analyzer/test_suite/gotta_snatch_em_all_test.exs b/test/elixir_analyzer/test_suite/gotta_snatch_em_all_test.exs new file mode 100644 index 00000000..a83abed2 --- /dev/null +++ b/test/elixir_analyzer/test_suite/gotta_snatch_em_all_test.exs @@ -0,0 +1,392 @@ +defmodule ElixirAnalyzer.ExerciseTest.GottSnatchEmAllTest do + use ElixirAnalyzer.ExerciseTestCase, + exercise_test_module: ElixirAnalyzer.TestSuite.GottaSnatchEmAll + + test_exercise_analysis "example solution", + comments: [ElixirAnalyzer.Constants.solution_same_as_exemplar()] do + defmodule GottaSnatchEmAll do + @type card :: String.t() + @type collection :: MapSet.t(card()) + + @spec new_collection(card()) :: collection() + def new_collection(card) do + MapSet.new([card]) + end + + @spec add_card(card(), collection()) :: {boolean(), collection()} + def add_card(card, collection) do + {MapSet.member?(collection, card), MapSet.put(collection, card)} + end + + @spec trade_card(card(), card(), collection()) :: {boolean(), collection()} + def trade_card(your_card, their_card, collection) do + can_trade? = + MapSet.member?(collection, your_card) and not MapSet.member?(collection, their_card) + + updated_collection = + collection + |> MapSet.delete(your_card) + |> MapSet.put(their_card) + + {can_trade?, updated_collection} + end + + @spec remove_duplicates([card()]) :: [card()] + def remove_duplicates(cards) do + cards + |> MapSet.new() + |> MapSet.to_list() + |> Enum.sort() + end + + @spec extra_cards(collection(), collection()) :: non_neg_integer() + def extra_cards(your_collection, their_collection) do + your_collection + |> MapSet.difference(their_collection) + |> MapSet.size() + end + + @spec boring_cards([collection()]) :: [card()] + def boring_cards(collections) do + case collections do + [] -> + [] + + [collection | rest] -> + Enum.reduce(rest, collection, &MapSet.intersection/2) + |> MapSet.to_list() + |> Enum.sort() + end + end + + @spec total_cards([collection()]) :: non_neg_integer() + def total_cards(collections) do + collections + |> Enum.reduce(MapSet.new(), &MapSet.union/2) + |> MapSet.size() + end + + @spec split_shiny_cards(collection()) :: {[card()], [card()]} + def split_shiny_cards(collection) do + {shiny, not_shiny} = split_with(collection, &String.starts_with?(&1, "Shiny")) + + shiny_list = shiny |> MapSet.to_list() |> Enum.sort() + not_shiny_list = not_shiny |> MapSet.to_list() |> Enum.sort() + + {shiny_list, not_shiny_list} + end + + defp split_with(mapset, predicate) do + init = {MapSet.new(), MapSet.new()} + + Enum.reduce(mapset, init, fn item, {passes, fails} -> + if predicate.(item) do + {MapSet.put(passes, item), fails} + else + {passes, MapSet.put(fails, item)} + end + end) + end + end + end + + test_exercise_analysis "does not use use MapSet.member or MapSet.put in add_card", + comments_include: [ + ElixirAnalyzer.Constants.gotta_snatch_em_all_add_card_use_mapset_member_and_put() + ] do + [ + defmodule GottaSnatchEmAll do + def add_card(card, collection) when is_binary(card) and is_map(collection) do + case MapSet.put(collection, card) do + ^collection -> {true, collection} + new_collection -> {false, new_collection} + end + end + end, + defmodule GottaSnatchEmAll do + def add_card(card, collection) when is_binary(card) and is_map(collection) do + {Enum.member?(collection, card), MapSet.put(collection, card)} + end + end, + defmodule GottaSnatchEmAll do + def add_card(card, collection) when is_binary(card) and is_map(collection) do + {Enum.any?(collection, &(&1 === card)), MapSet.put(collection, card)} + end + end, + defmodule GottaSnatchEmAll do + def add_card(card, collection) when is_binary(card) and is_map(collection) do + {MapSet.member?(collection, card), Enum.into([card], collection)} + end + end + ] + end + + test_exercise_analysis "does not use MapSet.member, MapSet.put or MapSet.delete in trade_card", + comments_include: [ + ElixirAnalyzer.Constants.gotta_snatch_em_all_trade_card_use_mapset_member_put_and_delete() + ] do + [ + defmodule GottaSnatchEmAll do + def trade_card(your_card, their_card, collection) do + can_trade? = + Enum.member?(collection, your_card) and not Enum.member?(collection, their_card) + + updated_collection = + collection + |> MapSet.delete(your_card) + |> MapSet.put(their_card) + + {can_trade?, updated_collection} + end + end, + defmodule GottaSnatchEmAll do + def trade_card(your_card, their_card, collection) do + can_trade? = + MapSet.member?(collection, your_card) and not MapSet.member?(collection, their_card) + + updated_collection = + collection + |> MapSet.reject(&(&1 === your_card)) + |> MapSet.put(their_card) + + {can_trade?, updated_collection} + end + end, + defmodule GottaSnatchEmAll do + def trade_card(your_card, their_card, collection) do + can_trade? = + MapSet.member?(collection, your_card) and not MapSet.member?(collection, their_card) + + updated_collection = + collection + |> MapSet.delete(your_card) + + {can_trade?, Enum.into([their_card], updated_collection)} + end + end + ] + end + + test_exercise_analysis "does not use MapSet.new in remove_duplicates", + comments_include: [ + ElixirAnalyzer.Constants.gotta_snatch_em_all_remove_duplicates_use_mapset_new() + ] do + [ + defmodule GottaSnatchEmAll do + def remove_duplicates(cards) do + cards + |> Enum.into(%{}, &{&1, true}) + |> Map.keys() + |> Enum.sort() + end + end + ] + end + + test_exercise_analysis "detect unsorted output from remove_duplicates", + comments_include: [ + ElixirAnalyzer.Constants.gotta_snatch_em_all_remove_duplicates_use_enum_sort() + ] do + [ + defmodule GottaSnatchEmAll do + def remove_duplicates(cards) do + cards + |> MapSet.new() + |> MapSet.to_list() + end + end + ] + end + + test_exercise_analysis "detect Enum.uniq usage in remove_duplicates", + comments_include: [ + ElixirAnalyzer.Constants.gotta_snatch_em_all_remove_duplicates_use_mapset_new(), + ElixirAnalyzer.Constants.gotta_snatch_em_all_remove_duplicates_do_not_use_enum_uniq() + ] do + [ + defmodule GottaSnatchEmAll do + def remove_duplicates(cards) do + cards + |> Enum.uniq() + |> Enum.sort() + end + end + ] + end + + test_exercise_analysis "does not use MapSet.difference in extra_cards", + comments_include: [ + ElixirAnalyzer.Constants.gotta_snatch_em_all_extra_cards_use_mapset_difference_and_size() + ] do + [ + defmodule GottaSnatchEmAll do + def extra_cards(your_collection, their_collection) do + your_collection + |> MapSet.reject(fn card -> MapSet.member?(their_collection, card) end) + |> MapSet.size() + end + end + ] + end + + test_exercise_analysis "does not use MapSet.size in extra_cards", + comments_include: [ + ElixirAnalyzer.Constants.gotta_snatch_em_all_extra_cards_use_mapset_difference_and_size() + ] do + [ + defmodule GottaSnatchEmAll do + def extra_cards(your_collection, their_collection) do + MapSet.difference(your_collection, their_collection) + |> Enum.count() + end + end, + defmodule GottaSnatchEmAll do + def extra_cards(your_collection, their_collection) do + MapSet.difference(your_collection, their_collection) + |> length + end + end + ] + end + + test_exercise_analysis "does not use MapSet.intersection in boring_cards", + comments_include: [ + ElixirAnalyzer.Constants.gotta_snatch_em_all_boring_cards_use_mapset_intersection() + ] do + [ + defmodule GottaSnatchEmAll do + def boring_cards([]), do: [] + def boring_cards([first]), do: MapSet.to_list(first) |> Enum.sort() + + def boring_cards(collections) do + rest + |> Enum.reduce(first, fn set1, set2 -> + MapSet.filter(set1, fn item -> MapSet.member?(set2, item) end) + end) + |> Enum.sort() + end + end, + defmodule GottaSnatchEmAll do + def boring_cards([]), do: [] + def boring_cards([first]), do: MapSet.to_list(first) |> Enum.sort() + + def boring_cards(cards) do + rest + |> Enum.reduce(first, fn set1, set2 -> for item <- set1, item in set2, do: item end) + |> Enum.sort() + end + end + ] + end + + test_exercise_analysis "detect unsorted output from boring_cards", + comments_include: [ + ElixirAnalyzer.Constants.gotta_snatch_em_all_boring_cards_use_enum_sort() + ] do + [ + def boring_cards(collections) do + case collections do + [] -> + [] + + [collection | rest] -> + Enum.reduce(rest, collection, &MapSet.intersection/2) + |> MapSet.to_list() + end + end + ] + end + + test_exercise_analysis "does not use Enum.reduce in boring_cards", + comments_include: [ + ElixirAnalyzer.Constants.gotta_snatch_em_all_boring_cards_use_enum_reduce() + ] do + [ + defmodule GottaSnatchEmAll do + def boring_cards([]), do: [] + def boring_cards([first]), do: MapSet.to_list(first) |> Enum.sort() + def boring_cards([first | rest]), do: do_boring_cards(rest, first) + + defp do_boring_cards([], acc), do: MapSet.to_list(acc) |> Enum.sort() + + defp do_boring_cards([next | rest], acc), + do: do_boring_cards(rest, MapSet.intersection(acc, next)) + end + ] + end + + test_exercise_analysis "does not use MapSet.union in total_cards", + comments_include: [ + ElixirAnalyzer.Constants.gotta_snatch_em_all_total_cards_use_mapset_union_and_size() + ] do + [ + defmodule GottaSnatchEmAll do + def total_cards(collections) do + collections + |> Enum.reduce([], &Enum.concat/2) + |> Enum.uniq() + |> Enum.count() + end + end, + defmodule GottaSnatchEmAll do + def total_cards(collections) do + collections + |> Enum.reduce(MapSet.new([]), fn set, acc -> + Enum.reduce(set, acc, fn i, acc2 -> MapSet.put(acc2, i) end) + end) + |> MapSet.size() + end + end + ] + end + + test_exercise_analysis "does not use MapSet.size in total_cards", + comments_include: [ + ElixirAnalyzer.Constants.gotta_snatch_em_all_total_cards_use_mapset_union_and_size() + ] do + [ + defmodule GottaSnatchEmAll do + def total_cards(collections) do + collections + |> Enum.reduce(MapSet.new(), &MapSet.union/2) + |> Enum.count() + end + end, + defmodule GottaSnatchEmAll do + def total_cards(collections) do + collections + |> Enum.reduce(MapSet.new(), &MapSet.union/2) + |> length + end + end + ] + end + + test_exercise_analysis "does not use String.starts_with? in split_shiny_cards", + comments_include: [ + ElixirAnalyzer.Constants.gotta_snatch_em_all_split_shiny_cards_use_string_starts_with() + ] do + [ + defmodule GottaSnatchEmAll do + def split_shiny_cards(collections) do + {shiny, others} = MapSet.split_with(collection, &String.match?(&1, ~r/^Shiny/)) + {MapSet.to_list(shiny) |> Enum.sort(), MapSet.to_list(others) |> Enum.sort()} + end + end + ] + end + + test_exercise_analysis "detect unsorted output from split_shiny_cards", + comments_include: [ + ElixirAnalyzer.Constants.gotta_snatch_em_all_split_shiny_cards_use_enum_sort() + ] do + [ + defmodule GottaSnatchEmAll do + def split_shiny_cards(collections) do + {shiny, others} = MapSet.split_with(collection, &String.starts_with?(&1, "Shiny")) + {MapSet.to_list(shiny), MapSet.to_list(others)} + end + end + ] + end +end