From 728c1527526023f7f8da8a18eff92cc814318ce0 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 20 Jan 2026 22:39:33 +0100 Subject: [PATCH 1/2] Add operator precedence test --- test/systematic_operators_test.exs | 2259 ++++++++++++++++++++++++++++ 1 file changed, 2259 insertions(+) create mode 100644 test/systematic_operators_test.exs diff --git a/test/systematic_operators_test.exs b/test/systematic_operators_test.exs new file mode 100644 index 0000000..9ac6105 --- /dev/null +++ b/test/systematic_operators_test.exs @@ -0,0 +1,2259 @@ +defmodule Spitfire.SystematicOperatorsTest do + use ExUnit.Case, async: true + + # ============================================================================= + # Operator Classifications + # ============================================================================= + # + # Based on Elixir operator precedence table (highest to lowest): + # + # Operator | Associativity + # ---------------------------------------------- | ------------- + # `@` | Unary + # `.` | Left + # `+` `-` `!` `^` `not` `~~~` | Unary + # `**` | Left + # `*` `/` | Left + # `+` `-` | Left + # `++` `--` `+++` `---` `..` `<>` | Right + # `^^^` | Left + # `in` `not in` | Left + # `|>` `<<<` `>>>` `<<~` `~>>` `<~` `~>` `<~>` | Left + # `<` `>` `<=` `>=` | Left + # `==` `!=` `=~` `===` `!==` | Left + # `&&` `&&&` `and` | Left + # `||` `|||` `or` | Left + # `=` | Right + # `&`, `...` | Unary + # `=>` (valid only inside `%{}`) | Right + # `|` | Right + # `::` | Right + # `when` | Right + # `<-` `\\` | Left + # ============================================================================= + + # Unary operators (note: @ and & have special syntax requirements) + @unary_ops ~w(@ + - ! ^ not & ... ~~~)a + + # Unary operators that can be freely combined with binary operators + @simple_unary_ops ~w(+ - ! ^ not ~~~)a + + # Binary operators organized by precedence level + @binary_ops [ + :., + :**, + :*, + :/, + :+, + :-, + :++, + :--, + :+++, + :---, + :.., + :<>, + :"^^^", + :in, + :"not in", + :|>, + :<<<, + :>>>, + :<<~, + :~>>, + :<~, + :~>, + :<~>, + :<, + :>, + :<=, + :>=, + :==, + :!=, + :=~, + :===, + :!==, + :&&, + :&&&, + :and, + :||, + :|||, + :or, + :=, + :|, + :"::", + :when, + :<-, + :\\ + ] + + # Binary operators that work with simple variable operands + @simple_binary_ops [ + :**, + :*, + :/, + :+, + :-, + :++, + :--, + :+++, + :---, + :.., + :<>, + :"^^^", + :in, + :"not in", + :|>, + :<<<, + :>>>, + :<<~, + :~>>, + :<~, + :~>, + :<~>, + :<, + :>, + :<=, + :>=, + :==, + :!=, + :=~, + :===, + :!==, + :&&, + :&&&, + :and, + :||, + :|||, + :or, + :=, + :|, + :"::", + :when, + :<-, + :\\ + ] + + # Right-associative binary operators + @right_assoc_ops ~w(++ -- +++ --- .. <> = | :: when)a + + # Left-associative binary operators + @left_assoc_ops ~w(** * / + - ^^^ in |> <<< >>> <<~ ~>> <~ ~> <~> < > <= >= == != =~ === !== && &&& and || ||| or <- \\)a + + # 3 expression kinds: matched_expr, unmatched_expr and no_parens_expr + @expressions [ + :matched, + :unmatched, + :no_parens + ] + + defp gen_expr(:matched, name) do + "#{String.capitalize(name)}Matched.#{name}_matched(#{name}_matched_arg)" + end + + defp gen_expr(:unmatched, name) do + "#{String.capitalize(name)}Unmatched.#{name}_unmatched do #{name}_unmatched_some end" + end + + defp gen_expr(:no_parens, name) do + "#{String.capitalize(name)}NoParens.#{name}_no_parens #{name}_no_parens_arg1, #{name}_no_parens_arg2" + end + + # Helper to convert atom to string representation + defp op_to_string(:"not in"), do: "not in" + defp op_to_string(op), do: Atom.to_string(op) + + defp unary_op_to_string(:not), do: "not " + defp unary_op_to_string(op), do: Atom.to_string(op) + + defp s2q(code) do + Code.string_to_quoted(code, columns: true, token_metadata: true, emit_warnings: false) + rescue + _e -> {:error, :reference_parser_crash} + end + + describe "systematic operator combinations" do + test "binary - binary combinations (a op1 b op2 c)" do + failures = + for op1 <- @binary_ops, + op2 <- @binary_ops, + expr_a <- @expressions -- [:no_parens], + expr_b <- @expressions -- [:no_parens], + expr_c <- @expressions do + s_op1 = op_to_string(op1) + s_op2 = op_to_string(op2) + + code = + "#{gen_expr(expr_a, "a")} #{s_op1} #{gen_expr(expr_b, "b")} #{s_op2} #{gen_expr(expr_c, "c")}" + + check(code) + end + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures |> Enum.take(5), pretty: true, limit: :infinity)}" + end + + test "unary - binary combinations (op1 a op2 b)" do + failures = + for op1 <- @unary_ops, + op2 <- @binary_ops, + expr_a <- @expressions -- [:no_parens], + expr_b <- @expressions do + s_op1 = unary_op_to_string(op1) + s_op2 = op_to_string(op2) + + code = "#{s_op1}#{gen_expr(expr_a, "a")} #{s_op2} #{gen_expr(expr_b, "b")}" + + check(code) + end + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "binary - unary combinations (a op1 op2 b)" do + failures = + for op1 <- @binary_ops, + op2 <- @unary_ops, + expr_a <- @expressions -- [:no_parens], + expr_b <- @expressions do + s_op1 = op_to_string(op1) + s_op2 = unary_op_to_string(op2) + + code = "#{gen_expr(expr_a, "a")} #{s_op1} #{s_op2}#{gen_expr(expr_b, "b")}" + + check(code) + end + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures |> Enum.take(5), pretty: true, limit: :infinity)}" + end + + test "ternary range (a..b//c) combinations" do + failures = + for op <- @binary_ops, + expr_a <- @expressions -- [:no_parens], + expr_b <- @expressions -- [:no_parens], + expr_c <- @expressions -- [:no_parens], + expr_d <- @expressions -- [] do + s_op = op_to_string(op) + + code1 = + "#{gen_expr(expr_a, "a")} #{s_op} #{gen_expr(expr_b, "b")}..#{gen_expr(expr_c, "c")}//#{gen_expr(expr_d, "d")}" + + code2 = + "#{gen_expr(expr_a, "a")}..#{gen_expr(expr_b, "b")} #{s_op} #{gen_expr(expr_c, "c")}//#{gen_expr(expr_d, "d")}" + + code3 = + "#{gen_expr(expr_a, "a")}..#{gen_expr(expr_b, "b")}//#{gen_expr(expr_c, "c")} #{s_op} #{gen_expr(expr_d, "d")}" + + [ + check(code1), + check(code2), + check(code3) + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "ternary range with unary operators" do + failures = + for op <- @unary_ops, + expr_a <- @expressions -- [:no_parens], + expr_b <- @expressions -- [:no_parens], + expr_c <- @expressions -- [] do + s_op = unary_op_to_string(op) + + [ + check( + "#{s_op}#{gen_expr(expr_a, "a")}..#{gen_expr(expr_b, "b")}//#{gen_expr(expr_c, "c")}" + ), + check( + "#{gen_expr(expr_a, "a")}..#{s_op}#{gen_expr(expr_b, "b")}//#{gen_expr(expr_c, "c")}" + ), + check( + "#{gen_expr(expr_a, "a")}..#{gen_expr(expr_b, "b")}//#{s_op}#{gen_expr(expr_c, "c")}" + ) + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "list update binary (a | b op c) combinations" do + failures = + for op <- @binary_ops, + expr_a <- @expressions -- [], + expr_b <- @expressions -- [], + expr_c <- @expressions -- [] do + s_op = op_to_string(op) + + # Inside tl + code1 = + "[#{gen_expr(expr_a, "a")} | #{gen_expr(expr_b, "b")} #{s_op} #{gen_expr(expr_c, "c")}]" + + # Inside hd + code2 = + "[#{gen_expr(expr_a, "a")} #{s_op} #{gen_expr(expr_b, "b")} | #{gen_expr(expr_c, "c")}]" + + [ + check(code1), + check(code2) + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + all_failures = failures + + assert all_failures == [], + "Failed combinations: #{inspect(all_failures, pretty: true, limit: :infinity)}" + end + + test "list update unary (a | op b) combinations" do + failures = + for op <- @unary_ops, + expr_a <- @expressions -- [], + expr_b <- @expressions -- [] do + s_op = unary_op_to_string(op) + + # Inside tl + code1 = + "[#{gen_expr(expr_a, "a")} | #{s_op}#{gen_expr(expr_b, "b")}]" + + # Inside hd + code2 = + "[#{s_op}#{gen_expr(expr_a, "a")} | #{gen_expr(expr_b, "b")}]" + + [ + check(code1), + check(code2) + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + all_failures = failures + + assert all_failures == [], + "Failed combinations: #{inspect(all_failures, pretty: true, limit: :infinity)}" + end + + test "stab binary (a -> b op c) combinations" do + failures = + for op <- @binary_ops, + expr_a <- @expressions -- [], + expr_b <- @expressions -- [], + expr_c <- @expressions -- [] do + s_op = op_to_string(op) + + # Inside tl + code1 = + "(#{gen_expr(expr_a, "a")} -> #{gen_expr(expr_b, "b")} #{s_op} #{gen_expr(expr_c, "c")})" + + # Inside hd + code2 = + "(#{gen_expr(expr_a, "a")} #{s_op} #{gen_expr(expr_b, "b")} -> #{gen_expr(expr_c, "c")})" + + [ + check(code1), + check(code2) + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + all_failures = failures + + assert all_failures == [], + "Failed combinations: #{inspect(all_failures, pretty: true, limit: :infinity)}" + end + + test "stab unary (a -> op b) combinations" do + failures = + for op <- @unary_ops, + expr_a <- @expressions -- [], + expr_b <- @expressions -- [] do + s_op = unary_op_to_string(op) + + # Inside tl + code1 = + "(#{gen_expr(expr_a, "a")} -> #{s_op}#{gen_expr(expr_b, "b")})" + + # Inside hd + code2 = + "(#{s_op}#{gen_expr(expr_a, "a")} -> #{gen_expr(expr_b, "b")})" + + [ + check(code1), + check(code2) + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + all_failures = failures + + assert all_failures == [], + "Failed combinations: #{inspect(all_failures, pretty: true, limit: :infinity)}" + end + + test "map update (a | b => c op d) combinations" do + failures = + for op <- @binary_ops, + expr_a <- @expressions -- [], + expr_b <- @expressions -- [], + expr_c <- @expressions -- [], + expr_d <- @expressions -- [] do + s_op = op_to_string(op) + + # Inside values + code1 = + "%{#{gen_expr(expr_a, "a")} | #{gen_expr(expr_b, "b")} => #{gen_expr(expr_c, "c")} #{s_op} #{gen_expr(expr_d, "d")}}" + + # Inside keys + code2 = + "%{#{gen_expr(expr_a, "a")} | #{gen_expr(expr_b, "b")} #{s_op} #{gen_expr(expr_c, "c")} => #{gen_expr(expr_d, "d")}}" + + # Inside struct + code3 = + "%{#{gen_expr(expr_a, "a")} #{s_op} #{gen_expr(expr_b, "b")} | #{gen_expr(expr_c, "c")} => #{gen_expr(expr_d, "d")}}" + + # Inside assoc + code4 = + "%{#{gen_expr(expr_a, "a")} | #{gen_expr(expr_b, "b")} #{s_op} #{gen_expr(expr_c, "c")}}" + + [ + check(code1), + check(code2), + check(code3), + check(code4) + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + all_failures = failures + + assert all_failures == [], + "Failed combinations: #{inspect(all_failures, pretty: true, limit: :infinity)}" + end + + test "map update unary (a | b => op c) combinations" do + failures = + for op <- @unary_ops, + expr_a <- @expressions -- [], + expr_b <- @expressions -- [], + expr_c <- @expressions -- [] do + s_op = unary_op_to_string(op) + + # Inside values + code1 = + "%{#{gen_expr(expr_a, "a")} | #{gen_expr(expr_b, "b")} => #{s_op}#{gen_expr(expr_c, "c")}}" + + # Inside keys + code2 = + "%{#{gen_expr(expr_a, "a")} | #{s_op}#{gen_expr(expr_b, "b")} => #{gen_expr(expr_c, "c")}}" + + # Inside struct + code3 = + "%{#{s_op}#{gen_expr(expr_a, "a")} | #{gen_expr(expr_b, "b")} => #{gen_expr(expr_c, "c")}}" + + # Inside assoc + code4 = + "%{#{gen_expr(expr_a, "a")} | #{s_op}#{gen_expr(expr_b, "b")}}" + + [ + check(code1), + check(code2), + check(code3), + check(code4) + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + all_failures = failures + + assert all_failures == [], + "Failed combinations: #{inspect(all_failures, pretty: true, limit: :infinity)}" + end + + test "ternary range between two binary operators (a op1 b..c//d op2 e)" do + failures = + for op1 <- @binary_ops, + op2 <- @binary_ops, + expr_a <- @expressions -- [:no_parens], + expr_b <- @expressions -- [:no_parens], + expr_c <- @expressions -- [:no_parens], + expr_d <- @expressions -- [:no_parens], + expr_e <- @expressions -- [] do + s1 = op_to_string(op1) + s2 = op_to_string(op2) + + check( + "#{gen_expr(expr_a, "a")} #{s1} #{gen_expr(expr_b, "b")}..#{gen_expr(expr_c, "c")}//#{gen_expr(expr_d, "d")} #{s2} #{gen_expr(expr_e, "e")}" + ) + end + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "map update between two binary operators (a op1 %{m | k => v} op2 b)" do + failures = + for op1 <- @binary_ops, op2 <- @binary_ops do + s1 = op_to_string(op1) + s2 = op_to_string(op2) + + check("a #{s1} %{m | k => v} #{s2} b") + end + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "map update with unary operators" do + failures = + for op <- @simple_unary_ops, + expr_a <- @expressions -- [], + expr_b <- @expressions -- [], + expr_c <- @expressions -- [] do + s_op = unary_op_to_string(op) + + [ + check( + "%{#{s_op}#{gen_expr(expr_a, "a")} | #{gen_expr(expr_b, "b")} => #{gen_expr(expr_c, "c")}}" + ), + check( + "%{#{gen_expr(expr_a, "a")} | #{s_op}#{gen_expr(expr_b, "b")} => #{gen_expr(expr_c, "c")}}" + ), + check( + "%{#{gen_expr(expr_a, "a")} | #{gen_expr(expr_b, "b")} => #{s_op}#{gen_expr(expr_c, "c")}}" + ) + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + defp check(code) do + case s2q(code) do + {:ok, expected} -> + case Spitfire.parse(code) do + {:ok, actual} -> + if actual != expected, do: {code, expected, actual}, else: nil + + {:error, _} -> + {code, expected, :error} + end + + {:error, _} -> + nil + end + end + + # ============================================================================= + # Operators with Literals + # ============================================================================= + # The repro 34 case "not \"\" <> \"\"" shows that operators with literal + # operands can behave differently than with variables. + + describe "operators with string literals" do + test "unary operators with string binary operators" do + failures = + for op1 <- @simple_unary_ops, op2 <- [:++, :--, :<>, :..] do + s_op1 = op_to_string(op1) + s_op2 = op_to_string(op2) + + [ + check(~s'#{s_op1} "" #{s_op2} ""'), + check(~s'#{s_op1} "foo" #{s_op2} "bar"'), + check(~s'#{s_op1} a #{s_op2} "bar"'), + check(~s'#{s_op1} "foo" #{s_op2} b') + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "binary operators with mixed literals and variables" do + literals = [~s'""', ~s'"foo"', "1", "1.0", ":atom", "'c'", "[]", "{}"] + + failures = + for lit <- literals, op <- @simple_binary_ops do + s_op = op_to_string(op) + + [ + check("a #{s_op} #{lit}"), + check("#{lit} #{s_op} a"), + check("#{lit} #{s_op} #{lit}") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Operators with do/end Blocks + # ============================================================================= + # Many repro cases involve case/try/with/quote/fn expressions + + describe "operators with do/end blocks" do + @do_blocks [ + "case a do\n _ -> b\nend", + "try do\n a\nrescue\n _ -> b\nend", + "with a <- b, do: c", + "for x <- xs, do: x", + "quote do\n a\nend", + "fn -> a end", + "fn x -> x end", + "if a, do: b, else: c", + "cond do\n true -> a\nend", + "receive do\n _ -> a\nend" + ] + + test "unary operators with do/end blocks followed by binary operator" do + failures = + for unary <- @simple_unary_ops, block <- @do_blocks, binary <- @simple_binary_ops do + s_unary = unary_op_to_string(unary) + s_binary = op_to_string(binary) + + code = "#{s_unary}#{block} #{s_binary} a" + check(code) + end + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "do/end blocks followed by binary operators" do + failures = + for block <- @do_blocks, op <- @simple_binary_ops do + s_op = op_to_string(op) + + [ + check("#{block} #{s_op} a"), + check("a #{s_op} #{block}") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "do/end blocks with operators inside" do + failures = + for op <- @simple_binary_ops do + s_op = op_to_string(op) + + [ + check("case a #{s_op} b do\n _ -> c\nend"), + check("case a do\n x #{s_op} y -> z\nend"), + check("fn x -> x #{s_op} y end"), + check("quote do\n a #{s_op} b\nend"), + check("for x <- a #{s_op} b, do: x") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Triple Operator Combinations (4 operands) + # ============================================================================= + # Tests precedence when three binary operators interact + + describe "triple binary operator combinations" do + # Use a representative subset to avoid combinatorial explosion + @precedence_representatives [ + # highest among simple binary + :**, + # mult/div level + :*, + # add/sub level + :+, + # right-associative list ops + :++, + # binary concat + :<>, + # pipe + :|>, + # comparison + :<, + # equality + :==, + # logical and + :&&, + # logical or + :||, + # match + := + ] + + test "a op1 b op2 c op3 d" do + failures = + for op1 <- @precedence_representatives, + op2 <- @precedence_representatives, + op3 <- @precedence_representatives do + s1 = op_to_string(op1) + s2 = op_to_string(op2) + s3 = op_to_string(op3) + + check("a #{s1} b #{s2} c #{s3} d") + end + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "unary op1 a op2 b op3 c" do + failures = + for unary <- @simple_unary_ops, + op1 <- @precedence_representatives, + op2 <- @precedence_representatives do + s_unary = unary_op_to_string(unary) + s1 = op_to_string(op1) + s2 = op_to_string(op2) + + check("#{s_unary}a #{s1} b #{s2} c") + end + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Chained Unary Operators + # ============================================================================= + + describe "chained unary operators" do + test "double unary operators" do + failures = + for op1 <- @simple_unary_ops, op2 <- @simple_unary_ops do + s1 = unary_op_to_string(op1) + s2 = unary_op_to_string(op2) + + [ + check("#{s1}#{s2}a"), + check("#{s1}#{s2}a + b") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "triple unary operators" do + failures = + for op1 <- @simple_unary_ops, op2 <- @simple_unary_ops, op3 <- @simple_unary_ops do + s1 = unary_op_to_string(op1) + s2 = unary_op_to_string(op2) + s3 = unary_op_to_string(op3) + + check("#{s1}#{s2}#{s3}a") + end + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Associativity Tests + # ============================================================================= + # Verify that chained same-operator expressions parse correctly + + describe "associativity verification" do + test "right-associative operators chain correctly" do + failures = + for op <- @right_assoc_ops do + s_op = op_to_string(op) + + [ + check("a #{s_op} b #{s_op} c"), + check("a #{s_op} b #{s_op} c #{s_op} d") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "left-associative operators chain correctly" do + failures = + for op <- @left_assoc_ops, op not in [:"not in"] do + s_op = op_to_string(op) + + [ + check("a #{s_op} b #{s_op} c"), + check("a #{s_op} b #{s_op} c #{s_op} d") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # String Interpolation with Operators + # ============================================================================= + + describe "string interpolation with operators" do + test "operators inside interpolation" do + failures = + for op <- @simple_binary_ops do + s_op = op_to_string(op) + + [ + check(~s'"foo\#{a #{s_op} b}bar"'), + check(~s'"foo\#{a}bar" #{s_op} "baz"'), + check(~s'a #{s_op} "foo\#{b}bar"') + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "heredocs with operators" do + failures = + for op <- @simple_binary_ops do + s_op = op_to_string(op) + + [ + check(~s'"""\nfoo \#{a #{s_op} b}\n"""'), + check(~s'a #{s_op} """\nfoo\n"""') + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "charlists with interpolation and operators" do + failures = + for op <- @simple_binary_ops do + s_op = op_to_string(op) + + [ + check(~s|'foo\#{a #{s_op} b}bar'|), + check(~s|'foo' #{s_op} a|) + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Data Structures with Operators + # ============================================================================= + + describe "operators in data structures" do + test "operators in lists" do + failures = + for op <- @simple_binary_ops do + s_op = op_to_string(op) + + [ + check("[a #{s_op} b]"), + check("[a #{s_op} b, c #{s_op} d]"), + check("[a | b #{s_op} c]"), + check("[a #{s_op} b | c]") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "operators in tuples" do + failures = + for op <- @simple_binary_ops do + s_op = op_to_string(op) + + [ + check("{a #{s_op} b}"), + check("{a #{s_op} b, c #{s_op} d}"), + check("{a, b #{s_op} c}") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "operators in maps" do + failures = + for op <- @simple_binary_ops do + s_op = op_to_string(op) + + [ + check("%{a: b #{s_op} c}"), + check("%{a #{s_op} b => c}"), + check("%{a => b #{s_op} c}"), + check("%{a #{s_op} b => c #{s_op} d}") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "operators in structs" do + failures = + for op <- @simple_binary_ops do + s_op = op_to_string(op) + + [ + check("%Foo{a: b #{s_op} c}"), + check("%Foo{s | a: b #{s_op} c}"), + check("%Foo{a #{s_op} b | c: d}") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "operators in keyword lists" do + failures = + for op <- @simple_binary_ops do + s_op = op_to_string(op) + + [ + check("[a: b #{s_op} c]"), + check("[a: b #{s_op} c, d: e #{s_op} f]"), + check("foo(a: b #{s_op} c)") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Function Capture with Operators + # ============================================================================= + + describe "function capture with operators" do + test "capture with operator expressions" do + failures = + for op <- @simple_binary_ops do + s_op = op_to_string(op) + + [ + check("&(&1 #{s_op} a)"), + check("&(&1 #{s_op} &2)"), + check("&(a #{s_op} &1)"), + check("f = &(&1 #{s_op} 1)") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "capture followed by binary operator" do + failures = + for op <- @simple_binary_ops do + s_op = op_to_string(op) + + [ + check("&foo/1 #{s_op} a"), + check("a #{s_op} &foo/1"), + check("&Foo.bar/2 #{s_op} a") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Module Attribute with Operators + # ============================================================================= + + describe "module attribute with operators" do + test "@attr with binary operators" do + failures = + for op <- @simple_binary_ops do + s_op = op_to_string(op) + + [ + check("@foo #{s_op} a"), + check("a #{s_op} @foo"), + check("@foo a #{s_op} b") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "@attr with do/end blocks" do + failures = + for block <- @do_blocks do + [ + check("@foo #{block}"), + check("@foo #{block}..1"), + check("@foo #{block}..1//2") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Bitstring with Type Operators + # ============================================================================= + + describe "bitstring with type operators" do + test "basic bitstring type specs" do + failures = + [ + check("<>"), + check("<>"), + check("<>"), + check("<>"), + check("<>"), + check("<>"), + check("<>") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "bitstring with operators inside" do + failures = + for op <- @simple_binary_ops do + s_op = op_to_string(op) + + [ + check("<>"), + check("<>"), + check("<>") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "bitstring with operators outside" do + failures = + for op <- @simple_binary_ops do + s_op = op_to_string(op) + + [ + check("<> #{s_op} b"), + check("a #{s_op} <>") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Guard Expressions (when) + # ============================================================================= + + describe "guard expressions" do + test "when with binary operators" do + failures = + for op <- @simple_binary_ops do + s_op = op_to_string(op) + + [ + check("def foo(a) when a #{s_op} b, do: a"), + check("fn a when a #{s_op} b -> a end"), + check("case a do\n x when x #{s_op} y -> x\nend") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "when with multiple guards" do + failures = + for op1 <- [:and, :or, :&&, :||], op2 <- [:and, :or, :&&, :||] do + s1 = op_to_string(op1) + s2 = op_to_string(op2) + + [ + check("def foo(a) when a > 0 #{s1} a < 10 #{s2} a != 5, do: a"), + check("fn a when a > 0 #{s1} a < 10 -> a end") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Comprehension Generators + # ============================================================================= + + describe "comprehension generators" do + test "<- with binary operators" do + failures = + for op <- @simple_binary_ops do + s_op = op_to_string(op) + + [ + check("for x <- a #{s_op} b, do: x"), + check("for x <- xs, x #{s_op} y, do: x"), + check("with {:ok, x} <- a #{s_op} b, do: x") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Default Arguments + # ============================================================================= + + describe "default arguments" do + test "\\\\ with binary operators" do + failures = + for op <- @simple_binary_ops do + s_op = op_to_string(op) + + [ + check("def foo(a \\\\ b #{s_op} c), do: a"), + check("def foo(a \\\\ 1 #{s_op} 2 #{s_op} 3), do: a") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Sigils with Operators + # ============================================================================= + + describe "sigils with operators" do + test "sigils followed by binary operators" do + failures = + for op <- @simple_binary_ops do + s_op = op_to_string(op) + + [ + check("~r/foo/ #{s_op} a"), + check("a #{s_op} ~r/bar/"), + check("~s(foo) #{s_op} ~s(bar)"), + check("~w(a b c) #{s_op} list") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "sigils with interpolation and operators" do + failures = + for op <- @simple_binary_ops do + s_op = op_to_string(op) + + [ + check(~s|~s"foo\#{a #{s_op} b}bar"|), + check(~s|~s"foo" #{s_op} a|) + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Dot Operator Combinations + # ============================================================================= + + describe "dot operator combinations" do + test "dot with binary operators" do + failures = + for op <- @simple_binary_ops do + s_op = op_to_string(op) + + [ + check("a.b #{s_op} c"), + check("a #{s_op} b.c"), + check("a.b #{s_op} c.d"), + check("Foo.bar #{s_op} Baz.qux"), + check("Foo.bar() #{s_op} a"), + check("a #{s_op} Foo.bar()"), + check("foo.bar(a #{s_op} b)") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "quoted function names with operators" do + failures = + for op <- @simple_binary_ops do + s_op = op_to_string(op) + + [ + check(~s|Foo."bar"() #{s_op} a|), + check(~s|a #{s_op} Foo."bar"()|), + check(~s|Foo."bar"(a #{s_op} b)|) + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Access Syntax with Operators + # ============================================================================= + + describe "access syntax with operators" do + test "access with binary operators" do + failures = + for op <- @simple_binary_ops do + s_op = op_to_string(op) + + [ + check("a[b] #{s_op} c"), + check("a #{s_op} b[c]"), + check("a[b #{s_op} c]"), + check("a[b] #{s_op} c[d]") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Parenthesized Expressions with Operators + # ============================================================================= + + describe "parenthesized expressions with operators" do + test "parentheses override precedence" do + failures = + for op1 <- @precedence_representatives, op2 <- @precedence_representatives do + s1 = op_to_string(op1) + s2 = op_to_string(op2) + + [ + check("(a #{s1} b) #{s2} c"), + check("a #{s1} (b #{s2} c)"), + check("(a #{s1} b) #{s2} (c #{s1} d)") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Range with Two Ternary Operators + # ============================================================================= + + describe "two ternary operators (range..//step and map update)" do + test "range inside map" do + failures = + [ + check("%{a => 1..10//2}"), + check("%{1..10//2 => a}"), + check("%{m | a => 1..10//2}"), + check("%{m | a: 1..10//2}") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "map inside range" do + failures = + [ + check("%{a: 1}..%{b: 2}"), + check("%{a: 1}..%{b: 2}//1"), + check("1..%{a: 2}//3") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "complex range and map combinations" do + failures = + for op <- @precedence_representatives do + s_op = op_to_string(op) + + [ + check("1..10//2 #{s_op} %{a: b}"), + check("%{a: b} #{s_op} 1..10//2"), + check("%{1..2 => 3..4//5}"), + check("a..b//c #{s_op} %{d | e => f}") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "binary operators between two ternaries" do + failures = + for op <- @binary_ops do + s_op = op_to_string(op) + + [ + check("a..b//c #{s_op} d..e//f"), + check("a..b//c #{s_op} %{m | k => v}"), + check("%{m | k => v} #{s_op} a..b//c"), + check("%{m | k => v} #{s_op} %{p | q => r}") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Special Cases from Repro Tests + # ============================================================================= + + describe "special cases from repro tests" do + test "unary with right-associative operators" do + failures = + for unary <- @simple_unary_ops, op <- [:++, :--, :<>, :..] do + s_unary = unary_op_to_string(unary) + s_op = op_to_string(op) + + [ + check("#{s_unary}a #{s_op} b"), + check("#{s_unary}a #{s_op} b #{s_op} c"), + check("a #{s_op} #{s_unary}b"), + check("a #{s_op} #{s_unary}b #{s_op} c") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "unary with pipe operators" do + failures = + for unary <- @simple_unary_ops do + s_unary = unary_op_to_string(unary) + + [ + check("#{s_unary}a |> b"), + check("#{s_unary}a |> b |> c"), + check("a |> #{s_unary}b"), + check("#{s_unary}a |> b < c"), + check("#{s_unary}a |> b == c") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "unary with comparison and logical operators" do + failures = + for unary <- @simple_unary_ops, + comp <- [:<, :>, :<=, :>=, :==, :!=, :===, :!==], + logical <- [:&&, :||, :and, :or] do + s_unary = unary_op_to_string(unary) + s_comp = op_to_string(comp) + s_logical = op_to_string(logical) + + [ + check("#{s_unary}a #{s_comp} b"), + check("#{s_unary}a #{s_comp} b #{s_logical} c"), + check("a #{s_comp} #{s_unary}b #{s_logical} c") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "range with step and other operators" do + failures = + for op <- @simple_binary_ops do + s_op = op_to_string(op) + + [ + check("a #{s_op} b..c//d"), + check("a..b//c #{s_op} d"), + check("a #{s_op} b..c #{s_op} d//e"), + check("a..b #{s_op} c//d") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "capture with do/end blocks" do + failures = + [ + check("&case a do\n _ -> b\nend"), + check("f = &case a do\n _ -> b\nend"), + check("&fn -> a end"), + check(""e do\n a\nend") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "@ with call and do/end blocks" do + failures = + [ + check("@foo Foo.bar(case a do\n _ -> b\nend)"), + check("@foo Foo.bar(try do\n a\nend)"), + check("@foo Foo.bar(quote do\n a\nend)"), + check("@foo case a do\n _ -> b\nend..c"), + check("@foo try do\n a\nend..b//c") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Pipe into Data Structures + # ============================================================================= + + describe "pipe into data structures" do + test "|> with data structure destinations" do + failures = + [ + check("a |> {b, c}"), + check("a |> [b, c]"), + check("a |> %{b: c}"), + check("a |> {b..c}"), + check("a |> {b, c..d//e}"), + check("Foo.bar() |> {a..b, c}") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Complex Nested Expressions + # ============================================================================= + + describe "complex nested expressions" do + test "multiple levels of nesting" do + failures = + [ + check("[[a + b, c * d], e | f]"), + check("{[a: b + c], {d, e * f}}"), + check("%{a: [b | c], d: {e, f + g}}"), + check("case a + b do\n x when x > 0 -> [y | z]\nend"), + check("for x <- a..b//c, y <- d..e, do: {x + y, x * y}") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "deeply nested operators" do + failures = + [ + check("(a + (b * (c ** d)))"), + check("((a + b) * (c + d))"), + check("a || (b && (c || d))"), + check("a = b = c = d + e"), + check("[a | [b | [c | d]]]") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Capture Operator followed by Binary Operators + # ============================================================================= + + describe "capture followed by binary operators" do + test "&(expr) followed by all binary operators" do + failures = + for op <- @simple_binary_ops do + s_op = op_to_string(op) + + [ + check("&(a + 1) #{s_op} b"), + check("&foo/1 #{s_op} b"), + check("&Foo.bar/2 #{s_op} b"), + check("b #{s_op} &(a + 1)"), + check("b #{s_op} &foo/1") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "&(expr) followed by range operators" do + failures = + [ + check("&(a + 1)..b"), + check("&(a + 1)..b//c"), + check("a..&(b + 1)"), + check("a..&(b + 1)//c"), + check("&foo/1..b..c"), + check("&foo/1..b//c") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "&(expr) with keyword list followed by binary operators" do + failures = + for op <- [:++, :--, :|>, :in, :<>, :==, :&&, :||] do + s_op = op_to_string(op) + + [ + check("&([a: 1] + 1) #{s_op} b"), + check("&(['one': :ok] + 1) #{s_op} b") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "&(expr) with struct/map followed by binary operators" do + failures = + for op <- [:++, :--, :|>, :in, :<>, :==, :&&, :||] do + s_op = op_to_string(op) + + [ + check("&(%{a: 1} + 1) #{s_op} b"), + check("&(%Foo{a: 1} + 1) #{s_op} b"), + check("&({a, b} + 1) #{s_op} c") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Nested Captures + # ============================================================================= + + describe "nested captures" do + test "double nested capture" do + failures = + [ + check("&(&(a + 1) + 1)"), + check("&(&(0 + 1) + 1)"), + check("&(&1 + &(&2 + 1))"), + check("&(&(&1 + 1) + 1)") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "nested capture with struct" do + failures = + [ + check("&(&(%Foo{a: b} + 1) + 1)"), + check("&(&(%{a: 1} + 1) + 1)"), + check("&(&([a: 1] + 1) + 1)") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "nested capture inside interpolation" do + failures = + [ + check(~s'"foo\#{&(&(0 + 1) + 1)}bar"'), + check(~s'~s"""\\nfoo \#{&(&(0 + 1) + 1)} bar\\n"""') + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Capture with Char Literals + # ============================================================================= + + describe "capture with char literals" do + test "&(?x + n) patterns" do + failures = + for op <- @simple_binary_ops do + s_op = op_to_string(op) + + [ + check("&(?a + 1)"), + check("&(?a + 1) #{s_op} b"), + check("b #{s_op} &(?a + 1)"), + check("&(?a #{s_op} ?b)") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "capture with char literal in map" do + failures = + [ + check("%{a => &(?h + 1)}"), + check("%{&(?h + 1) => a}"), + check("%{a => &(?h + 1) !== b}"), + check("foo(%{a => &(?h + 1) !== b})") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # fn Expressions followed by Binary Operators + # ============================================================================= + + describe "fn expressions followed by binary operators" do + test "fn -> expr end followed by all binary operators" do + failures = + for op <- @simple_binary_ops do + s_op = op_to_string(op) + + [ + check("fn -> a end #{s_op} b"), + check("fn x -> x end #{s_op} b"), + check("fn x, y -> x + y end #{s_op} b"), + check("a #{s_op} fn -> b end") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "fn followed by &&& and then capture with pipe" do + failures = + [ + check("fn a -> a end &&& b"), + check("fn a -> a end &&& &(a + 1)"), + check("fn a -> a end &&& &(a + 1) |> b"), + check("fn a -> a end &&& &(a + 1) |> b |> c") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Unary + do/end Blocks followed by Range with Step + # ============================================================================= + + describe "unary with do/end blocks followed by range" do + test "unary op do/end block followed by range" do + failures = + for unary <- @simple_unary_ops do + s_unary = unary_op_to_string(unary) + + [ + check("#{s_unary}case a do\n _ -> b\nend..c"), + check("#{s_unary}case a do\n _ -> b\nend..c//d"), + check("#{s_unary}try do\n a\nend..b"), + check("#{s_unary}try do\n a\nend..b//c"), + check("#{s_unary}with a <- b, do: c end..d//e"), + check("#{s_unary}quote do\n a\nend..b//c") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "unary + case followed by pipe and power operators" do + failures = + [ + check("+case a do\n _ -> b\nend |> c"), + check("+case a do\n _ -> b\nend |> c ** d"), + check("+case a do\n _ -> b\nend |> c ** d >>> e"), + check("-case a do\n _ -> b\nend |> c ** d") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Char Literals Piped to Data Structures + # ============================================================================= + + describe "char literals with pipe to data structures" do + test "char literal piped to struct/map" do + failures = + [ + check("?a |> %Foo{}"), + check("?a |> %Foo{b: c}"), + check("?a |> %{b: c}"), + check("'M' |> %Foo{}"), + check("'M' |> %Foo{a: b}") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "charlist piped to struct in function call" do + failures = + [ + check("foo('M' |> %Baz{})"), + check("Foo.bar('M' |> %Baz{a: b})"), + check("Foo.bar('M' |> %Baz{a: b, c: d})") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Range inside Bitstring + # ============================================================================= + + describe "range inside bitstring" do + test "char literal range in bitstring" do + failures = + [ + check("<>"), + check("<>"), + check("<>"), + check("<>"), + check("<>") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "complex expressions in bitstring" do + failures = + [ + check("<>"), + check("<>"), + check("< b>>"), + check("<>") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Unary Operators inside Bitstring + # ============================================================================= + + describe "unary operators inside bitstring" do + test "unary with do/end block inside bitstring" do + failures = + for unary <- @simple_unary_ops do + s_unary = unary_op_to_string(unary) + + [ + check("<<#{s_unary}a>>"), + check("<<#{s_unary}a, b::8>>"), + check("<>") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "unary + do/end blocks inside bitstring" do + failures = + [ + check("<<+quote do\n a\nend>>"), + check("<<-case a do\n _ -> b\nend>>"), + check("<<+quote do\n a\nend, b::8>>"), + check("{:ok, <<+quote do\n a\nend>>}") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Complex Interpolation with Capture and Pipe + # ============================================================================= + + describe "complex interpolation with capture and pipe" do + test "capture with pipe inside interpolation" do + failures = + [ + check(~s'"foo\#{&(a |> b)}bar"'), + check(~s'"foo\#{&(a |> b + 1)}bar"'), + check(~s'"foo\#{&(a |> b + 1) |> c}bar"'), + check("'foo\#{&(a |> b + 1)}bar'"), + check("'foo\#{&(0 |> Foo + 1) |> %{a: b}}bar'") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "capture followed by in and fn with pipe" do + failures = + [ + check("&(a + 1) in b"), + check("&(a + 1) in fn -> b end"), + check("&(a + 1) in fn -> b end |> c"), + check("&(a + 1) in fn -> b end |> quote do\n c\nend") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Heredoc Charlists with Complex Interpolation + # ============================================================================= + + describe "heredoc charlists with operators" do + test "capture with heredoc charlist followed by range" do + failures = + [ + check("&('''\nfoo\n''' + 1)"), + check("&('''\nfoo\n''' + 1)..b"), + check("&('''\nfoo\n''' + 1)..b//c"), + check("a..&('''\nfoo\n''' + 1)") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "heredoc charlist with interpolation containing operators" do + failures = + [ + check("'''\nfoo \#{a + b}\n'''"), + check("'''\nfoo \#{a |> b}\n'''"), + check("'''\nfoo \#{&(a + 1)}\n'''"), + check("'''\nfoo \#{%{a: b}}\n'''") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Combination of Multiple Complex Patterns + # ============================================================================= + + describe "combination of multiple complex patterns" do + test "fn in capture with operators" do + failures = + [ + check("&({a, b} + 1) in fn -> c end"), + check("fn a -> &(b + 1) end"), + check("fn a -> &(b + 1) in c end") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "atoms as quoted keys with operators" do + failures = + [ + check("%{:'ok' => a + b}"), + check("%{:'ok' + a => b}"), + check("<<:'ok' + a>>"), + check("[:'ok': a + b]") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "with expression inside capture" do + failures = + [ + check("&(with a <- b, do: c)"), + check("&(with a <- b, do: c + d)"), + check("&(with a <- b, do: c) |> d") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Range with Keyword List as Operand + # ============================================================================= + + describe "range with keyword list operand" do + test "range with keyword list as right operand" do + failures = + [ + check("a..['key': b]"), + check("a..b..['key': c]"), + check("a..['key': b]//c"), + check("a..['do': b, 'else': c]"), + check("\"\" <> \"foo\"..['do': bar]") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "range with keyword list in case expression" do + failures = + [ + check("case a..['key': b] do\n _ -> c\nend"), + check("case \"\" <> \"foo\"..['do': bar] do\n _ -> c\nend"), + check("case a..b..['key': c]//d do\n _ -> e\nend") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "range with heredoc in keyword list" do + failures = + [ + check("a..['key': \"\"\"\nfoo\n\"\"\"]"), + check("case a..['do': \"\"\"\nfoo\n\"\"\"] do\n _ -> b\nend") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Pipe Inside Function Call Arguments + # ============================================================================= + + describe "pipe inside function call arguments" do + test "charlist piped to struct/map in function call" do + failures = + [ + check("foo('M' |> a)"), + check("foo('M' |> %{a: b})"), + check("foo('M' |> %Foo{a: b})"), + check("foo('abc' |> %{\"ok\": :err})"), + check("Foo.bar('M' |> %{a: b})") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "pipe with heredoc in struct inside function call" do + failures = + [ + check("foo(a |> %{b: \"\"\"\nfoo\n\"\"\"})"), + check("foo('M' |> %Baz{\"ok\": \"\"\"\nfoo\n\"\"\"})"), + check("Foo.bar('M' |> %{a: \"\"\"\nfoo \#{b} bar\n\"\"\"})") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "multiple pipes in function call arguments" do + failures = + [ + check("foo(a |> b |> c)"), + check("foo(a |> b, c |> d)"), + check("foo('M' |> a |> b)"), + check("Foo.bar(a |> %{b: c}, d |> e)") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "pipe with operators in function call" do + failures = + for op <- @simple_binary_ops do + s_op = op_to_string(op) + + [ + check("foo(a |> b #{s_op} c)"), + check("foo(a #{s_op} b |> c)") + ] + end + |> List.flatten() + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Struct/Map with Quoted String Keys and Heredoc Values + # ============================================================================= + + describe "struct/map with quoted string keys" do + test "struct with quoted string key and heredoc value" do + failures = + [ + check("%Foo{\"ok\": a}"), + check("%Foo{\"ok\": \"\"\"\nfoo\n\"\"\"}"), + check("%{\"ok\": \"\"\"\nfoo \#{a} bar\n\"\"\"}"), + check("foo(%Baz{\"ok\": \"\"\"\nfoo\n\"\"\"})") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "pipe into struct with heredoc" do + failures = + [ + check("a |> %Foo{b: \"\"\"\nfoo\n\"\"\"}"), + check("a |> %{\"ok\": \"\"\"\nfoo\n\"\"\"}"), + check("'M' |> %Baz{\"ok\": \"\"\"\nfoo \#{a} bar\n\"\"\"})") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end + + # ============================================================================= + # Case Expression with Complex First Argument + # ============================================================================= + + describe "case with complex first argument" do + test "case with binary operator expression" do + failures = + for op <- @simple_binary_ops do + s_op = op_to_string(op) + + check("case a #{s_op} b do\n _ -> c\nend") + end + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "case with string concatenation and range" do + failures = + [ + check("case \"\" <> \"foo\" do\n _ -> a\nend"), + check("case a..b do\n _ -> c\nend"), + check("case \"\" <> \"foo\"..a do\n _ -> b\nend"), + check("case a..b//c do\n _ -> d\nend") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + + test "case with range and keyword list" do + failures = + [ + check("case a..[b: c] do\n _ -> d\nend"), + check("case \"\" <> \"foo\"..[do: a] do\n _ -> b\nend"), + check("case a..['key': \"\"\"\nfoo\n\"\"\"] do\n _ -> b\nend") + ] + |> Enum.reject(&is_nil/1) + + assert failures == [], + "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" + end + end +end From f93d806061b5e9db0746a1b24615dfa04539ba8a Mon Sep 17 00:00:00 2001 From: doorgan Date: Tue, 10 Feb 2026 13:15:16 -0300 Subject: [PATCH 2/2] fix: align operator parsing with elixir grammar --- lib/spitfire.ex | 302 ++++++++-- test/systematic_operators_test.exs | 906 +++++++++++++++++------------ 2 files changed, 800 insertions(+), 408 deletions(-) diff --git a/lib/spitfire.ex b/lib/spitfire.ex index 55d665d..c561f75 100644 --- a/lib/spitfire.ex +++ b/lib/spitfire.ex @@ -130,6 +130,7 @@ defmodule Spitfire do # Operators that indicate an identifier should be treated as a lone identifier (not a call) @no_parens_stop_operators [ + :|, :"=>", :->, :+, @@ -317,6 +318,8 @@ defmodule Spitfire do defp do_parse_expression(parser, {associativity, precedence}, is_list, is_map, is_top) do stop_before_stab_op? = Map.get(parser, :stop_before_stab_op?, false) + stop_before_map_op? = Map.get(parser, :stop_before_map_op?, false) + inside_map_update_pairs? = Map.get(parser, :inside_map_update_pairs, false) prefix = case current_token_type(parser) do @@ -381,6 +384,9 @@ defmodule Spitfire do if is_valid do while (not stab_state_set?(parser) and not MapSet.member?(terminals, peek_token(parser))) && (current_token(parser) != :do and peek_token(parser) != :eol) && + (not stop_before_map_op? or + (peek_token_type(parser) != :assoc_op and + peek_token(parser) != :"=>")) && calc_prec(parser, associativity, precedence) <- {left, parser} do parser = consume_fuel(parser) peek_token_type = peek_token_type(parser) @@ -392,7 +398,7 @@ defmodule Spitfire do :when_op -> parse_infix_expression(next_token(parser), left) - :pipe_op when is_map -> + :pipe_op when is_map and not inside_map_update_pairs? -> parse_pipe_op_in_map(next_token(parser), left) :pipe_op -> @@ -886,23 +892,22 @@ defmodule Spitfire do end parser = parser |> next_token() |> eat_eoe() + {value, parser} = parse_expression(parser, @lowest, false, false, false) - {value, parser} = - with_context(parser, %{in_map: false}, fn parser -> - parse_expression(parser, @assoc_op, false, false, false) - end) + {:pair, pair} = normalize_assoc_key(key, value, assoc_meta) + {pair, parser} + end + end - key = - case key do - {f, meta, args} -> - {f, [{:assoc, assoc_meta} | meta], args} + defp add_assoc_meta({f, meta, args}, assoc_meta) when is_list(meta) and (is_list(args) or is_nil(args)) do + {f, [{:assoc, assoc_meta} | meta], args} + end - _ -> - key - end + defp add_assoc_meta(other, _assoc_meta), do: other - {{key, value}, parser} - end + defp normalize_assoc_key(key, value, assoc_meta) do + key = add_assoc_meta(key, assoc_meta) + {:pair, {key, value}} end defp(parse_comma_list(parser, precedence \\ @list_comma, is_list \\ false, is_map \\ false)) @@ -994,7 +999,13 @@ defmodule Spitfire do {rhs, parser} = if unparenthesized_do_end_block?(rhs) do - parse_expression(rhs_parser, @lowest, false, false, false) + if Map.get(parser, :in_map, false) do + with_context(rhs_parser, %{stop_before_map_op?: true}, fn parser -> + parse_expression(parser, @lowest, false, false, false) + end) + else + parse_expression(rhs_parser, @lowest, false, false, false) + end else {rhs, parser} end @@ -1016,6 +1027,16 @@ defmodule Spitfire do end end + # An expression is "unmatched" if it contains an unparenthesized do-end block + # anywhere in its AST. Binary operators with an unmatched operand produce + # unmatched expressions. + defp unmatched_expr?({_, meta, args} = ast) when is_list(meta) do + unparenthesized_do_end_block?(ast) or + (is_list(args) and Enum.any?(args, &unmatched_expr?/1)) + end + + defp unmatched_expr?(_), do: false + defp parse_prefix_lone_identifer(parser) do trace "parse_prefix_lone_identifer", trace_meta(parser) do token = current_token(parser) @@ -1316,6 +1337,16 @@ defmodule Spitfire do token = current_token(parser) meta = current_meta(parser) precedence = current_precedence(parser) + + effective_precedence = + if Map.get(parser, :in_map, false) do + {_, prec} = precedence + {_, assoc_prec} = @assoc_op + if prec > assoc_prec, do: precedence, else: {:left, assoc_prec} + else + precedence + end + # we save this in case the next expression is an error pre_parser = parser @@ -1333,13 +1364,13 @@ defmodule Spitfire do if token == :when do {rhs, parser} = with_context(parser, %{stop_before_stab_op?: true}, fn parser -> - parse_expression(parser, precedence, false, false, false) + parse_expression(parser, effective_precedence, false, false, false) end) parser = Map.delete(parser, :stab_state) {rhs, parser} else - parse_expression(parser, precedence, false, false, false) + parse_expression(parser, effective_precedence, false, false, false) end {rhs, parser} = @@ -1416,7 +1447,11 @@ defmodule Spitfire do rhs_parser = parser |> next_token() |> eat_eoe() - if ambiguous_map_pipe_assoc?(lhs, rhs_parser) do + if rhs_has_binding_op?(rhs_parser) or + (unmatched_expr?(lhs) and rhs_has_bare_comma?(rhs_parser)) do + # When the RHS of `|` has low-precedence operators (::, when, <-, \\) or + # the LHS is an unmatched_expr (do-end) and the RHS has no-parens commas, + # treat `|` as a regular pipe operator (matching Elixir's LALR grammar). parse_infix_expression(parser, lhs) else {pairs, pairs_parser} = parse_map_update_pairs(rhs_parser) @@ -1426,49 +1461,173 @@ defmodule Spitfire do end end - defp ambiguous_map_pipe_assoc?(lhs, rhs_parser) do - unparenthesized_do_end_block?(lhs) and ambiguous_map_pipe_assoc_rhs?(rhs_parser) + # Operators with precedence between assoc_op (18) and pipe_op (22) that + # should NOT be consumed inside parse_map_update_pairs. + @low_prec_map_op_types MapSet.new([:type_op, :when_op, :in_match_op]) + + defp rhs_has_binding_op?(parser) do + scan_binding_op(eat_eoe(parser), 0) end - defp ambiguous_map_pipe_assoc_rhs?(rhs_parser) do - case {current_token_type(rhs_parser), peek_token_type(rhs_parser)} do - {:identifier, :identifier} -> - parser = rhs_parser |> next_token() |> eat_eoe() + defp scan_binding_op(parser, nesting) do + token = peek_token(parser) + token_type = peek_token_type(parser) - if peek_token(parser) == :"," do - parser = parser |> next_token() |> next_token() |> eat_eoe() - peek_token_type(parser) == :assoc_op - else - false - end + cond do + MapSet.member?(@low_prec_map_op_types, token_type) and nesting == 0 -> + true - _ -> + token_type == :assoc_op and nesting == 0 -> + false + + token_type in [:kw_identifier, :kw_identifier_safe, :kw_identifier_unsafe] and nesting == 0 -> + false + + token == :"}" and nesting == 0 -> + false + + token == :"," and nesting == 0 -> + false + + token == :eof -> + false + + token in [:"(", :"[", :"{", :"<<"] -> + scan_binding_op(next_token(parser), nesting + 1) + + token in [:")", :"]", :"}", :">>"] -> + scan_binding_op(next_token(parser), max(nesting - 1, 0)) + + token == :do -> + skip_do_end_for_binding_op(next_token(parser), 1, nesting) + + true -> + scan_binding_op(next_token(parser), nesting) + end + end + + defp skip_do_end_for_binding_op(parser, 0, nesting) do + scan_binding_op(parser, nesting) + end + + defp skip_do_end_for_binding_op(parser, depth, nesting) do + case peek_token(parser) do + :end -> skip_do_end_for_binding_op(next_token(parser), depth - 1, nesting) + :do -> skip_do_end_for_binding_op(next_token(parser), depth + 1, nesting) + :eof -> false + _ -> skip_do_end_for_binding_op(next_token(parser), depth, nesting) + end + end + + defp rhs_has_bare_comma?(parser) do + rhs_scan_comma_before_assoc(eat_eoe(parser), 0, false) + end + + defp rhs_scan_comma_before_assoc(parser, nesting, saw_do_end) do + token = peek_token(parser) + token_type = peek_token_type(parser) + + cond do + token == :"," and nesting == 0 -> + not saw_do_end and not rhs_has_do_before_assoc?(next_token(parser), 0) + + token == :"}" and nesting == 0 -> + false + + token == :eof -> + false + + token_type == :assoc_op and nesting == 0 -> + false + + token_type in [:kw_identifier, :kw_identifier_safe, :kw_identifier_unsafe] and nesting == 0 -> false + + token in [:"(", :"[", :"{", :"<<"] -> + rhs_scan_comma_before_assoc(next_token(parser), nesting + 1, saw_do_end) + + token in [:")", :"]", :"}", :">>"] -> + rhs_scan_comma_before_assoc(next_token(parser), max(nesting - 1, 0), saw_do_end) + + token == :do -> + rhs_skip_do_end(next_token(parser), 1, nesting, true) + + true -> + rhs_scan_comma_before_assoc(next_token(parser), nesting, saw_do_end) end end + defp rhs_has_do_before_assoc?(parser, nesting) do + token = peek_token(parser) + token_type = peek_token_type(parser) + + cond do + token == :do and nesting == 0 -> + true + + token_type == :assoc_op and nesting == 0 -> + false + + token_type in [:kw_identifier, :kw_identifier_safe, :kw_identifier_unsafe] and nesting == 0 -> + false + + token == :"}" and nesting == 0 -> + false + + token == :eof -> + false + + token in [:"(", :"[", :"{", :"<<"] -> + rhs_has_do_before_assoc?(next_token(parser), nesting + 1) + + token in [:")", :"]", :"}", :">>"] -> + rhs_has_do_before_assoc?(next_token(parser), max(nesting - 1, 0)) + + true -> + rhs_has_do_before_assoc?(next_token(parser), nesting) + end + end + + defp rhs_skip_do_end(parser, 0, nesting, saw_do_end) do + rhs_scan_comma_before_assoc(parser, nesting, saw_do_end) + end + + defp rhs_skip_do_end(parser, depth, nesting, saw_do_end) do + case peek_token(parser) do + :end -> rhs_skip_do_end(next_token(parser), depth - 1, nesting, saw_do_end) + :do -> rhs_skip_do_end(next_token(parser), depth + 1, nesting, saw_do_end) + :eof -> false + _ -> rhs_skip_do_end(next_token(parser), depth, nesting, saw_do_end) + end + end + + # Parses the RHS of a map update (after `|`). Inside here, `|` is treated + # as a regular pipe operator (not a nested map update), matching the elixir + # grammar where `assoc_update` only appears at the top level of `map_args`. defp parse_map_update_pairs(parser) do - {first, parser} = parse_expression(parser, @list_comma, false, true, false) + with_context(parser, %{inside_map_update_pairs: true}, fn parser -> + {first, parser} = parse_expression(parser, @list_comma, false, true, false) - {items, parser} = - while2 peek_token(parser) == :"," <- parser do - parser = next_token(parser) + {items, parser} = + while2 peek_token(parser) == :"," <- parser do + parser = next_token(parser) - case peek_token(parser) do - delimiter when delimiter in [:"}", :"]", :")", :">>"] -> - {:filter, {nil, parser}} + case peek_token(parser) do + delimiter when delimiter in [:"}", :"]", :")", :">>"] -> + {:filter, {nil, parser}} - _ -> - parser = parser |> next_token() |> eat_eoe() - {item, parser} = parse_expression(parser, @list_comma, false, true, false) - {item, parser} + _ -> + parser = parser |> next_token() |> eat_eoe() + {item, parser} = parse_expression(parser, @list_comma, false, true, false) + {item, parser} + end end - end - pairs = [first | items] - pairs = Enum.reject(pairs, &is_nil/1) + pairs = [first | items] + pairs = Enum.reject(pairs, &is_nil/1) - {pairs, parser} + {pairs, parser} + end) end defp parse_access_expression(parser, lhs) do @@ -1834,13 +1993,28 @@ defmodule Spitfire do # No-parens call with args parser = next_token(parser) parser = push_nesting(parser) - rest_precedence = if Map.get(parser, :in_map, false), do: {:left, 18}, else: @lowest - {first_arg, parser} = parse_expression(parser, rest_precedence, false, false, false) + in_map = Map.get(parser, :in_map, false) + + {first_arg, parser} = + if in_map do + with_context(parser, %{stop_before_map_op?: true}, fn parser -> + parse_expression(parser, @lowest, false, false, false) + end) + else + parse_expression(parser, @lowest, false, false, false) + end {rest_args, parser} = while2 peek_token(parser) == :"," <- parser do parser = parser |> next_token() |> next_token() - parse_expression(parser, rest_precedence, false, false, false) + + if in_map do + with_context(parser, %{stop_before_map_op?: true}, fn parser -> + parse_expression(parser, @lowest, false, false, false) + end) + else + parse_expression(parser, @lowest, false, false, false) + end end args = [first_arg | rest_args] @@ -2447,7 +2621,12 @@ defmodule Spitfire do {{:%{}, meta, []}, parser} true -> - {pairs, parser} = parse_comma_list(parser, @list_comma, false, true) + # Clear inside_map_update_pairs so nested maps (e.g., %{outer | key: %{inner | k => v}}) + # treat their own `|` as a map update, not as a regular pipe from the outer context. + {pairs, parser} = + with_context(parser, %{inside_map_update_pairs: false}, fn parser -> + parse_comma_list(parser, @list_comma, false, true) + end) parser = eat_eol_at(parser, 1) @@ -2580,7 +2759,22 @@ defmodule Spitfire do else meta = current_meta(parser) parser = next_token(parser) - {rhs, parser} = parse_expression(parser, @lowest, false, false, false) + rhs_parser = parser + {rhs, parser} = parse_expression(parser, @capture_op, false, false, false) + + {rhs, parser} = + if unparenthesized_do_end_block?(rhs) do + if Map.get(parser, :in_map, false) do + with_context(rhs_parser, %{stop_before_map_op?: true}, fn parser -> + parse_expression(parser, @lowest, false, false, false) + end) + else + parse_expression(rhs_parser, @lowest, false, false, false) + end + else + {rhs, parser} + end + {{:..., meta, [rhs]}, parser} end end @@ -2652,7 +2846,11 @@ defmodule Spitfire do parser = Map.put(parser, :nesting, old_nesting) {ast, parser} else - {pairs, parser} = parse_comma_list(parser, @list_comma, false, true) + {pairs, parser} = + with_context(parser, %{inside_map_update_pairs: false}, fn parser -> + parse_comma_list(parser, @list_comma, false, true) + end) + parser = eat_eol_at(parser, 1) parser = diff --git a/test/systematic_operators_test.exs b/test/systematic_operators_test.exs index 9ac6105..68a9bae 100644 --- a/test/systematic_operators_test.exs +++ b/test/systematic_operators_test.exs @@ -173,7 +173,7 @@ defmodule Spitfire.SystematicOperatorsTest do describe "systematic operator combinations" do test "binary - binary combinations (a op1 b op2 c)" do - failures = + for_result = for op1 <- @binary_ops, op2 <- @binary_ops, expr_a <- @expressions -- [:no_parens], @@ -187,14 +187,16 @@ defmodule Spitfire.SystematicOperatorsTest do check(code) end - |> Enum.reject(&is_nil/1) + + failures = + Enum.reject(for_result, &is_nil/1) assert failures == [], - "Failed combinations: #{inspect(failures |> Enum.take(5), pretty: true, limit: :infinity)}" + "Failed combinations: #{inspect(Enum.take(failures, 5), pretty: true, limit: :infinity)}" end test "unary - binary combinations (op1 a op2 b)" do - failures = + for_result = for op1 <- @unary_ops, op2 <- @binary_ops, expr_a <- @expressions -- [:no_parens], @@ -206,14 +208,16 @@ defmodule Spitfire.SystematicOperatorsTest do check(code) end - |> Enum.reject(&is_nil/1) + + failures = + Enum.reject(for_result, &is_nil/1) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" end test "binary - unary combinations (a op1 op2 b)" do - failures = + for_result = for op1 <- @binary_ops, op2 <- @unary_ops, expr_a <- @expressions -- [:no_parens], @@ -225,14 +229,16 @@ defmodule Spitfire.SystematicOperatorsTest do check(code) end - |> Enum.reject(&is_nil/1) + + failures = + Enum.reject(for_result, &is_nil/1) assert failures == [], - "Failed combinations: #{inspect(failures |> Enum.take(5), pretty: true, limit: :infinity)}" + "Failed combinations: #{inspect(Enum.take(failures, 5), pretty: true, limit: :infinity)}" end test "ternary range (a..b//c) combinations" do - failures = + for_result = for op <- @binary_ops, expr_a <- @expressions -- [:no_parens], expr_b <- @expressions -- [:no_parens], @@ -255,6 +261,9 @@ defmodule Spitfire.SystematicOperatorsTest do check(code3) ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -263,7 +272,7 @@ defmodule Spitfire.SystematicOperatorsTest do end test "ternary range with unary operators" do - failures = + for_result = for op <- @unary_ops, expr_a <- @expressions -- [:no_parens], expr_b <- @expressions -- [:no_parens], @@ -271,17 +280,14 @@ defmodule Spitfire.SystematicOperatorsTest do s_op = unary_op_to_string(op) [ - check( - "#{s_op}#{gen_expr(expr_a, "a")}..#{gen_expr(expr_b, "b")}//#{gen_expr(expr_c, "c")}" - ), - check( - "#{gen_expr(expr_a, "a")}..#{s_op}#{gen_expr(expr_b, "b")}//#{gen_expr(expr_c, "c")}" - ), - check( - "#{gen_expr(expr_a, "a")}..#{gen_expr(expr_b, "b")}//#{s_op}#{gen_expr(expr_c, "c")}" - ) + check("#{s_op}#{gen_expr(expr_a, "a")}..#{gen_expr(expr_b, "b")}//#{gen_expr(expr_c, "c")}"), + check("#{gen_expr(expr_a, "a")}..#{s_op}#{gen_expr(expr_b, "b")}//#{gen_expr(expr_c, "c")}"), + check("#{gen_expr(expr_a, "a")}..#{gen_expr(expr_b, "b")}//#{s_op}#{gen_expr(expr_c, "c")}") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -290,7 +296,7 @@ defmodule Spitfire.SystematicOperatorsTest do end test "list update binary (a | b op c) combinations" do - failures = + for_result = for op <- @binary_ops, expr_a <- @expressions -- [], expr_b <- @expressions -- [], @@ -310,6 +316,9 @@ defmodule Spitfire.SystematicOperatorsTest do check(code2) ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -320,7 +329,7 @@ defmodule Spitfire.SystematicOperatorsTest do end test "list update unary (a | op b) combinations" do - failures = + for_result = for op <- @unary_ops, expr_a <- @expressions -- [], expr_b <- @expressions -- [] do @@ -339,6 +348,9 @@ defmodule Spitfire.SystematicOperatorsTest do check(code2) ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -349,7 +361,7 @@ defmodule Spitfire.SystematicOperatorsTest do end test "stab binary (a -> b op c) combinations" do - failures = + for_result = for op <- @binary_ops, expr_a <- @expressions -- [], expr_b <- @expressions -- [], @@ -369,6 +381,9 @@ defmodule Spitfire.SystematicOperatorsTest do check(code2) ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -379,7 +394,7 @@ defmodule Spitfire.SystematicOperatorsTest do end test "stab unary (a -> op b) combinations" do - failures = + for_result = for op <- @unary_ops, expr_a <- @expressions -- [], expr_b <- @expressions -- [] do @@ -398,6 +413,9 @@ defmodule Spitfire.SystematicOperatorsTest do check(code2) ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -408,7 +426,7 @@ defmodule Spitfire.SystematicOperatorsTest do end test "map update (a | b => c op d) combinations" do - failures = + for_result = for op <- @binary_ops, expr_a <- @expressions -- [], expr_b <- @expressions -- [], @@ -439,6 +457,9 @@ defmodule Spitfire.SystematicOperatorsTest do check(code4) ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -449,7 +470,7 @@ defmodule Spitfire.SystematicOperatorsTest do end test "map update unary (a | b => op c) combinations" do - failures = + for_result = for op <- @unary_ops, expr_a <- @expressions -- [], expr_b <- @expressions -- [], @@ -479,6 +500,9 @@ defmodule Spitfire.SystematicOperatorsTest do check(code4) ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -489,7 +513,7 @@ defmodule Spitfire.SystematicOperatorsTest do end test "ternary range between two binary operators (a op1 b..c//d op2 e)" do - failures = + for_result = for op1 <- @binary_ops, op2 <- @binary_ops, expr_a <- @expressions -- [:no_parens], @@ -504,28 +528,32 @@ defmodule Spitfire.SystematicOperatorsTest do "#{gen_expr(expr_a, "a")} #{s1} #{gen_expr(expr_b, "b")}..#{gen_expr(expr_c, "c")}//#{gen_expr(expr_d, "d")} #{s2} #{gen_expr(expr_e, "e")}" ) end - |> Enum.reject(&is_nil/1) + + failures = + Enum.reject(for_result, &is_nil/1) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" end test "map update between two binary operators (a op1 %{m | k => v} op2 b)" do - failures = + for_result = for op1 <- @binary_ops, op2 <- @binary_ops do s1 = op_to_string(op1) s2 = op_to_string(op2) check("a #{s1} %{m | k => v} #{s2} b") end - |> Enum.reject(&is_nil/1) + + failures = + Enum.reject(for_result, &is_nil/1) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" end test "map update with unary operators" do - failures = + for_result = for op <- @simple_unary_ops, expr_a <- @expressions -- [], expr_b <- @expressions -- [], @@ -533,17 +561,14 @@ defmodule Spitfire.SystematicOperatorsTest do s_op = unary_op_to_string(op) [ - check( - "%{#{s_op}#{gen_expr(expr_a, "a")} | #{gen_expr(expr_b, "b")} => #{gen_expr(expr_c, "c")}}" - ), - check( - "%{#{gen_expr(expr_a, "a")} | #{s_op}#{gen_expr(expr_b, "b")} => #{gen_expr(expr_c, "c")}}" - ), - check( - "%{#{gen_expr(expr_a, "a")} | #{gen_expr(expr_b, "b")} => #{s_op}#{gen_expr(expr_c, "c")}}" - ) + check("%{#{s_op}#{gen_expr(expr_a, "a")} | #{gen_expr(expr_b, "b")} => #{gen_expr(expr_c, "c")}}"), + check("%{#{gen_expr(expr_a, "a")} | #{s_op}#{gen_expr(expr_b, "b")} => #{gen_expr(expr_c, "c")}}"), + check("%{#{gen_expr(expr_a, "a")} | #{gen_expr(expr_b, "b")} => #{s_op}#{gen_expr(expr_c, "c")}}") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -557,7 +582,7 @@ defmodule Spitfire.SystematicOperatorsTest do {:ok, expected} -> case Spitfire.parse(code) do {:ok, actual} -> - if actual != expected, do: {code, expected, actual}, else: nil + if actual != expected, do: {code, expected, actual} {:error, _} -> {code, expected, :error} @@ -576,7 +601,7 @@ defmodule Spitfire.SystematicOperatorsTest do describe "operators with string literals" do test "unary operators with string binary operators" do - failures = + for_result = for op1 <- @simple_unary_ops, op2 <- [:++, :--, :<>, :..] do s_op1 = op_to_string(op1) s_op2 = op_to_string(op2) @@ -588,6 +613,9 @@ defmodule Spitfire.SystematicOperatorsTest do check(~s'#{s_op1} "foo" #{s_op2} b') ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -598,7 +626,7 @@ defmodule Spitfire.SystematicOperatorsTest do test "binary operators with mixed literals and variables" do literals = [~s'""', ~s'"foo"', "1", "1.0", ":atom", "'c'", "[]", "{}"] - failures = + for_result = for lit <- literals, op <- @simple_binary_ops do s_op = op_to_string(op) @@ -608,6 +636,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("#{lit} #{s_op} #{lit}") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -636,7 +667,7 @@ defmodule Spitfire.SystematicOperatorsTest do ] test "unary operators with do/end blocks followed by binary operator" do - failures = + for_result = for unary <- @simple_unary_ops, block <- @do_blocks, binary <- @simple_binary_ops do s_unary = unary_op_to_string(unary) s_binary = op_to_string(binary) @@ -644,14 +675,16 @@ defmodule Spitfire.SystematicOperatorsTest do code = "#{s_unary}#{block} #{s_binary} a" check(code) end - |> Enum.reject(&is_nil/1) + + failures = + Enum.reject(for_result, &is_nil/1) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" end test "do/end blocks followed by binary operators" do - failures = + for_result = for block <- @do_blocks, op <- @simple_binary_ops do s_op = op_to_string(op) @@ -660,6 +693,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("a #{s_op} #{block}") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -668,7 +704,7 @@ defmodule Spitfire.SystematicOperatorsTest do end test "do/end blocks with operators inside" do - failures = + for_result = for op <- @simple_binary_ops do s_op = op_to_string(op) @@ -680,6 +716,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("for x <- a #{s_op} b, do: x") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -721,7 +760,7 @@ defmodule Spitfire.SystematicOperatorsTest do ] test "a op1 b op2 c op3 d" do - failures = + for_result = for op1 <- @precedence_representatives, op2 <- @precedence_representatives, op3 <- @precedence_representatives do @@ -731,14 +770,16 @@ defmodule Spitfire.SystematicOperatorsTest do check("a #{s1} b #{s2} c #{s3} d") end - |> Enum.reject(&is_nil/1) + + failures = + Enum.reject(for_result, &is_nil/1) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" end test "unary op1 a op2 b op3 c" do - failures = + for_result = for unary <- @simple_unary_ops, op1 <- @precedence_representatives, op2 <- @precedence_representatives do @@ -748,7 +789,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("#{s_unary}a #{s1} b #{s2} c") end - |> Enum.reject(&is_nil/1) + + failures = + Enum.reject(for_result, &is_nil/1) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -761,7 +804,7 @@ defmodule Spitfire.SystematicOperatorsTest do describe "chained unary operators" do test "double unary operators" do - failures = + for_result = for op1 <- @simple_unary_ops, op2 <- @simple_unary_ops do s1 = unary_op_to_string(op1) s2 = unary_op_to_string(op2) @@ -771,6 +814,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("#{s1}#{s2}a + b") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -779,7 +825,7 @@ defmodule Spitfire.SystematicOperatorsTest do end test "triple unary operators" do - failures = + for_result = for op1 <- @simple_unary_ops, op2 <- @simple_unary_ops, op3 <- @simple_unary_ops do s1 = unary_op_to_string(op1) s2 = unary_op_to_string(op2) @@ -787,7 +833,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("#{s1}#{s2}#{s3}a") end - |> Enum.reject(&is_nil/1) + + failures = + Enum.reject(for_result, &is_nil/1) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -801,7 +849,7 @@ defmodule Spitfire.SystematicOperatorsTest do describe "associativity verification" do test "right-associative operators chain correctly" do - failures = + for_result = for op <- @right_assoc_ops do s_op = op_to_string(op) @@ -810,6 +858,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("a #{s_op} b #{s_op} c #{s_op} d") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -818,7 +869,7 @@ defmodule Spitfire.SystematicOperatorsTest do end test "left-associative operators chain correctly" do - failures = + for_result = for op <- @left_assoc_ops, op not in [:"not in"] do s_op = op_to_string(op) @@ -827,6 +878,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("a #{s_op} b #{s_op} c #{s_op} d") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -841,7 +895,7 @@ defmodule Spitfire.SystematicOperatorsTest do describe "string interpolation with operators" do test "operators inside interpolation" do - failures = + for_result = for op <- @simple_binary_ops do s_op = op_to_string(op) @@ -851,6 +905,9 @@ defmodule Spitfire.SystematicOperatorsTest do check(~s'a #{s_op} "foo\#{b}bar"') ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -859,7 +916,7 @@ defmodule Spitfire.SystematicOperatorsTest do end test "heredocs with operators" do - failures = + for_result = for op <- @simple_binary_ops do s_op = op_to_string(op) @@ -868,6 +925,9 @@ defmodule Spitfire.SystematicOperatorsTest do check(~s'a #{s_op} """\nfoo\n"""') ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -876,7 +936,7 @@ defmodule Spitfire.SystematicOperatorsTest do end test "charlists with interpolation and operators" do - failures = + for_result = for op <- @simple_binary_ops do s_op = op_to_string(op) @@ -885,6 +945,9 @@ defmodule Spitfire.SystematicOperatorsTest do check(~s|'foo' #{s_op} a|) ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -899,7 +962,7 @@ defmodule Spitfire.SystematicOperatorsTest do describe "operators in data structures" do test "operators in lists" do - failures = + for_result = for op <- @simple_binary_ops do s_op = op_to_string(op) @@ -910,6 +973,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("[a #{s_op} b | c]") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -918,7 +984,7 @@ defmodule Spitfire.SystematicOperatorsTest do end test "operators in tuples" do - failures = + for_result = for op <- @simple_binary_ops do s_op = op_to_string(op) @@ -928,6 +994,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("{a, b #{s_op} c}") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -936,7 +1005,7 @@ defmodule Spitfire.SystematicOperatorsTest do end test "operators in maps" do - failures = + for_result = for op <- @simple_binary_ops do s_op = op_to_string(op) @@ -947,6 +1016,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("%{a #{s_op} b => c #{s_op} d}") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -955,7 +1027,7 @@ defmodule Spitfire.SystematicOperatorsTest do end test "operators in structs" do - failures = + for_result = for op <- @simple_binary_ops do s_op = op_to_string(op) @@ -965,6 +1037,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("%Foo{a #{s_op} b | c: d}") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -973,7 +1048,7 @@ defmodule Spitfire.SystematicOperatorsTest do end test "operators in keyword lists" do - failures = + for_result = for op <- @simple_binary_ops do s_op = op_to_string(op) @@ -983,6 +1058,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("foo(a: b #{s_op} c)") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -997,7 +1075,7 @@ defmodule Spitfire.SystematicOperatorsTest do describe "function capture with operators" do test "capture with operator expressions" do - failures = + for_result = for op <- @simple_binary_ops do s_op = op_to_string(op) @@ -1008,6 +1086,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("f = &(&1 #{s_op} 1)") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -1016,7 +1097,7 @@ defmodule Spitfire.SystematicOperatorsTest do end test "capture followed by binary operator" do - failures = + for_result = for op <- @simple_binary_ops do s_op = op_to_string(op) @@ -1026,6 +1107,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("&Foo.bar/2 #{s_op} a") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -1040,7 +1124,7 @@ defmodule Spitfire.SystematicOperatorsTest do describe "module attribute with operators" do test "@attr with binary operators" do - failures = + for_result = for op <- @simple_binary_ops do s_op = op_to_string(op) @@ -1050,6 +1134,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("@foo a #{s_op} b") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -1058,7 +1145,7 @@ defmodule Spitfire.SystematicOperatorsTest do end test "@attr with do/end blocks" do - failures = + for_result = for block <- @do_blocks do [ check("@foo #{block}"), @@ -1066,6 +1153,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("@foo #{block}..1//2") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -1081,23 +1171,25 @@ defmodule Spitfire.SystematicOperatorsTest do describe "bitstring with type operators" do test "basic bitstring type specs" do failures = - [ - check("<>"), - check("<>"), - check("<>"), - check("<>"), - check("<>"), - check("<>"), - check("<>") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [ + check("<>"), + check("<>"), + check("<>"), + check("<>"), + check("<>"), + check("<>"), + check("<>") + ], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" end test "bitstring with operators inside" do - failures = + for_result = for op <- @simple_binary_ops do s_op = op_to_string(op) @@ -1107,6 +1199,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("<>") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -1115,7 +1210,7 @@ defmodule Spitfire.SystematicOperatorsTest do end test "bitstring with operators outside" do - failures = + for_result = for op <- @simple_binary_ops do s_op = op_to_string(op) @@ -1124,6 +1219,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("a #{s_op} <>") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -1138,7 +1236,7 @@ defmodule Spitfire.SystematicOperatorsTest do describe "guard expressions" do test "when with binary operators" do - failures = + for_result = for op <- @simple_binary_ops do s_op = op_to_string(op) @@ -1148,6 +1246,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("case a do\n x when x #{s_op} y -> x\nend") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -1156,7 +1257,7 @@ defmodule Spitfire.SystematicOperatorsTest do end test "when with multiple guards" do - failures = + for_result = for op1 <- [:and, :or, :&&, :||], op2 <- [:and, :or, :&&, :||] do s1 = op_to_string(op1) s2 = op_to_string(op2) @@ -1166,6 +1267,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("fn a when a > 0 #{s1} a < 10 -> a end") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -1180,7 +1284,7 @@ defmodule Spitfire.SystematicOperatorsTest do describe "comprehension generators" do test "<- with binary operators" do - failures = + for_result = for op <- @simple_binary_ops do s_op = op_to_string(op) @@ -1190,6 +1294,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("with {:ok, x} <- a #{s_op} b, do: x") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -1204,7 +1311,7 @@ defmodule Spitfire.SystematicOperatorsTest do describe "default arguments" do test "\\\\ with binary operators" do - failures = + for_result = for op <- @simple_binary_ops do s_op = op_to_string(op) @@ -1213,6 +1320,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("def foo(a \\\\ 1 #{s_op} 2 #{s_op} 3), do: a") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -1227,7 +1337,7 @@ defmodule Spitfire.SystematicOperatorsTest do describe "sigils with operators" do test "sigils followed by binary operators" do - failures = + for_result = for op <- @simple_binary_ops do s_op = op_to_string(op) @@ -1238,6 +1348,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("~w(a b c) #{s_op} list") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -1246,7 +1359,7 @@ defmodule Spitfire.SystematicOperatorsTest do end test "sigils with interpolation and operators" do - failures = + for_result = for op <- @simple_binary_ops do s_op = op_to_string(op) @@ -1255,6 +1368,9 @@ defmodule Spitfire.SystematicOperatorsTest do check(~s|~s"foo" #{s_op} a|) ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -1269,7 +1385,7 @@ defmodule Spitfire.SystematicOperatorsTest do describe "dot operator combinations" do test "dot with binary operators" do - failures = + for_result = for op <- @simple_binary_ops do s_op = op_to_string(op) @@ -1283,6 +1399,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("foo.bar(a #{s_op} b)") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -1291,7 +1410,7 @@ defmodule Spitfire.SystematicOperatorsTest do end test "quoted function names with operators" do - failures = + for_result = for op <- @simple_binary_ops do s_op = op_to_string(op) @@ -1301,6 +1420,9 @@ defmodule Spitfire.SystematicOperatorsTest do check(~s|Foo."bar"(a #{s_op} b)|) ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -1315,7 +1437,7 @@ defmodule Spitfire.SystematicOperatorsTest do describe "access syntax with operators" do test "access with binary operators" do - failures = + for_result = for op <- @simple_binary_ops do s_op = op_to_string(op) @@ -1326,6 +1448,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("a[b] #{s_op} c[d]") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -1340,7 +1465,7 @@ defmodule Spitfire.SystematicOperatorsTest do describe "parenthesized expressions with operators" do test "parentheses override precedence" do - failures = + for_result = for op1 <- @precedence_representatives, op2 <- @precedence_representatives do s1 = op_to_string(op1) s2 = op_to_string(op2) @@ -1351,6 +1476,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("(a #{s1} b) #{s2} (c #{s1} d)") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -1366,13 +1494,15 @@ defmodule Spitfire.SystematicOperatorsTest do describe "two ternary operators (range..//step and map update)" do test "range inside map" do failures = - [ - check("%{a => 1..10//2}"), - check("%{1..10//2 => a}"), - check("%{m | a => 1..10//2}"), - check("%{m | a: 1..10//2}") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [ + check("%{a => 1..10//2}"), + check("%{1..10//2 => a}"), + check("%{m | a => 1..10//2}"), + check("%{m | a: 1..10//2}") + ], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -1380,19 +1510,14 @@ defmodule Spitfire.SystematicOperatorsTest do test "map inside range" do failures = - [ - check("%{a: 1}..%{b: 2}"), - check("%{a: 1}..%{b: 2}//1"), - check("1..%{a: 2}//3") - ] - |> Enum.reject(&is_nil/1) + Enum.reject([check("%{a: 1}..%{b: 2}"), check("%{a: 1}..%{b: 2}//1"), check("1..%{a: 2}//3")], &is_nil/1) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" end test "complex range and map combinations" do - failures = + for_result = for op <- @precedence_representatives do s_op = op_to_string(op) @@ -1403,6 +1528,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("a..b//c #{s_op} %{d | e => f}") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -1411,7 +1539,7 @@ defmodule Spitfire.SystematicOperatorsTest do end test "binary operators between two ternaries" do - failures = + for_result = for op <- @binary_ops do s_op = op_to_string(op) @@ -1422,6 +1550,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("%{m | k => v} #{s_op} %{p | q => r}") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -1436,7 +1567,7 @@ defmodule Spitfire.SystematicOperatorsTest do describe "special cases from repro tests" do test "unary with right-associative operators" do - failures = + for_result = for unary <- @simple_unary_ops, op <- [:++, :--, :<>, :..] do s_unary = unary_op_to_string(unary) s_op = op_to_string(op) @@ -1448,6 +1579,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("a #{s_op} #{s_unary}b #{s_op} c") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -1456,7 +1590,7 @@ defmodule Spitfire.SystematicOperatorsTest do end test "unary with pipe operators" do - failures = + for_result = for unary <- @simple_unary_ops do s_unary = unary_op_to_string(unary) @@ -1468,6 +1602,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("#{s_unary}a |> b == c") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -1476,7 +1613,7 @@ defmodule Spitfire.SystematicOperatorsTest do end test "unary with comparison and logical operators" do - failures = + for_result = for unary <- @simple_unary_ops, comp <- [:<, :>, :<=, :>=, :==, :!=, :===, :!==], logical <- [:&&, :||, :and, :or] do @@ -1490,6 +1627,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("a #{s_comp} #{s_unary}b #{s_logical} c") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -1498,7 +1638,7 @@ defmodule Spitfire.SystematicOperatorsTest do end test "range with step and other operators" do - failures = + for_result = for op <- @simple_binary_ops do s_op = op_to_string(op) @@ -1509,6 +1649,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("a..b #{s_op} c//d") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -1518,13 +1661,15 @@ defmodule Spitfire.SystematicOperatorsTest do test "capture with do/end blocks" do failures = - [ - check("&case a do\n _ -> b\nend"), - check("f = &case a do\n _ -> b\nend"), - check("&fn -> a end"), - check(""e do\n a\nend") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [ + check("&case a do\n _ -> b\nend"), + check("f = &case a do\n _ -> b\nend"), + check("&fn -> a end"), + check(""e do\n a\nend") + ], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -1532,14 +1677,16 @@ defmodule Spitfire.SystematicOperatorsTest do test "@ with call and do/end blocks" do failures = - [ - check("@foo Foo.bar(case a do\n _ -> b\nend)"), - check("@foo Foo.bar(try do\n a\nend)"), - check("@foo Foo.bar(quote do\n a\nend)"), - check("@foo case a do\n _ -> b\nend..c"), - check("@foo try do\n a\nend..b//c") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [ + check("@foo Foo.bar(case a do\n _ -> b\nend)"), + check("@foo Foo.bar(try do\n a\nend)"), + check("@foo Foo.bar(quote do\n a\nend)"), + check("@foo case a do\n _ -> b\nend..c"), + check("@foo try do\n a\nend..b//c") + ], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -1553,15 +1700,17 @@ defmodule Spitfire.SystematicOperatorsTest do describe "pipe into data structures" do test "|> with data structure destinations" do failures = - [ - check("a |> {b, c}"), - check("a |> [b, c]"), - check("a |> %{b: c}"), - check("a |> {b..c}"), - check("a |> {b, c..d//e}"), - check("Foo.bar() |> {a..b, c}") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [ + check("a |> {b, c}"), + check("a |> [b, c]"), + check("a |> %{b: c}"), + check("a |> {b..c}"), + check("a |> {b, c..d//e}"), + check("Foo.bar() |> {a..b, c}") + ], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -1575,14 +1724,16 @@ defmodule Spitfire.SystematicOperatorsTest do describe "complex nested expressions" do test "multiple levels of nesting" do failures = - [ - check("[[a + b, c * d], e | f]"), - check("{[a: b + c], {d, e * f}}"), - check("%{a: [b | c], d: {e, f + g}}"), - check("case a + b do\n x when x > 0 -> [y | z]\nend"), - check("for x <- a..b//c, y <- d..e, do: {x + y, x * y}") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [ + check("[[a + b, c * d], e | f]"), + check("{[a: b + c], {d, e * f}}"), + check("%{a: [b | c], d: {e, f + g}}"), + check("case a + b do\n x when x > 0 -> [y | z]\nend"), + check("for x <- a..b//c, y <- d..e, do: {x + y, x * y}") + ], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -1590,14 +1741,16 @@ defmodule Spitfire.SystematicOperatorsTest do test "deeply nested operators" do failures = - [ - check("(a + (b * (c ** d)))"), - check("((a + b) * (c + d))"), - check("a || (b && (c || d))"), - check("a = b = c = d + e"), - check("[a | [b | [c | d]]]") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [ + check("(a + (b * (c ** d)))"), + check("((a + b) * (c + d))"), + check("a || (b && (c || d))"), + check("a = b = c = d + e"), + check("[a | [b | [c | d]]]") + ], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -1610,7 +1763,7 @@ defmodule Spitfire.SystematicOperatorsTest do describe "capture followed by binary operators" do test "&(expr) followed by all binary operators" do - failures = + for_result = for op <- @simple_binary_ops do s_op = op_to_string(op) @@ -1622,6 +1775,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("b #{s_op} &foo/1") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -1631,22 +1787,24 @@ defmodule Spitfire.SystematicOperatorsTest do test "&(expr) followed by range operators" do failures = - [ - check("&(a + 1)..b"), - check("&(a + 1)..b//c"), - check("a..&(b + 1)"), - check("a..&(b + 1)//c"), - check("&foo/1..b..c"), - check("&foo/1..b//c") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [ + check("&(a + 1)..b"), + check("&(a + 1)..b//c"), + check("a..&(b + 1)"), + check("a..&(b + 1)//c"), + check("&foo/1..b..c"), + check("&foo/1..b//c") + ], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" end test "&(expr) with keyword list followed by binary operators" do - failures = + for_result = for op <- [:++, :--, :|>, :in, :<>, :==, :&&, :||] do s_op = op_to_string(op) @@ -1655,6 +1813,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("&(['one': :ok] + 1) #{s_op} b") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -1663,7 +1824,7 @@ defmodule Spitfire.SystematicOperatorsTest do end test "&(expr) with struct/map followed by binary operators" do - failures = + for_result = for op <- [:++, :--, :|>, :in, :<>, :==, :&&, :||] do s_op = op_to_string(op) @@ -1673,6 +1834,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("&({a, b} + 1) #{s_op} c") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -1688,13 +1852,10 @@ defmodule Spitfire.SystematicOperatorsTest do describe "nested captures" do test "double nested capture" do failures = - [ - check("&(&(a + 1) + 1)"), - check("&(&(0 + 1) + 1)"), - check("&(&1 + &(&2 + 1))"), - check("&(&(&1 + 1) + 1)") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [check("&(&(a + 1) + 1)"), check("&(&(0 + 1) + 1)"), check("&(&1 + &(&2 + 1))"), check("&(&(&1 + 1) + 1)")], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -1702,12 +1863,10 @@ defmodule Spitfire.SystematicOperatorsTest do test "nested capture with struct" do failures = - [ - check("&(&(%Foo{a: b} + 1) + 1)"), - check("&(&(%{a: 1} + 1) + 1)"), - check("&(&([a: 1] + 1) + 1)") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [check("&(&(%Foo{a: b} + 1) + 1)"), check("&(&(%{a: 1} + 1) + 1)"), check("&(&([a: 1] + 1) + 1)")], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -1715,11 +1874,10 @@ defmodule Spitfire.SystematicOperatorsTest do test "nested capture inside interpolation" do failures = - [ - check(~s'"foo\#{&(&(0 + 1) + 1)}bar"'), - check(~s'~s"""\\nfoo \#{&(&(0 + 1) + 1)} bar\\n"""') - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [check(~s'"foo\#{&(&(0 + 1) + 1)}bar"'), check(~s'~s"""\\nfoo \#{&(&(0 + 1) + 1)} bar\\n"""')], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -1732,7 +1890,7 @@ defmodule Spitfire.SystematicOperatorsTest do describe "capture with char literals" do test "&(?x + n) patterns" do - failures = + for_result = for op <- @simple_binary_ops do s_op = op_to_string(op) @@ -1743,6 +1901,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("&(?a #{s_op} ?b)") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -1752,13 +1913,15 @@ defmodule Spitfire.SystematicOperatorsTest do test "capture with char literal in map" do failures = - [ - check("%{a => &(?h + 1)}"), - check("%{&(?h + 1) => a}"), - check("%{a => &(?h + 1) !== b}"), - check("foo(%{a => &(?h + 1) !== b})") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [ + check("%{a => &(?h + 1)}"), + check("%{&(?h + 1) => a}"), + check("%{a => &(?h + 1) !== b}"), + check("foo(%{a => &(?h + 1) !== b})") + ], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -1771,7 +1934,7 @@ defmodule Spitfire.SystematicOperatorsTest do describe "fn expressions followed by binary operators" do test "fn -> expr end followed by all binary operators" do - failures = + for_result = for op <- @simple_binary_ops do s_op = op_to_string(op) @@ -1782,6 +1945,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("a #{s_op} fn -> b end") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -1791,13 +1957,15 @@ defmodule Spitfire.SystematicOperatorsTest do test "fn followed by &&& and then capture with pipe" do failures = - [ - check("fn a -> a end &&& b"), - check("fn a -> a end &&& &(a + 1)"), - check("fn a -> a end &&& &(a + 1) |> b"), - check("fn a -> a end &&& &(a + 1) |> b |> c") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [ + check("fn a -> a end &&& b"), + check("fn a -> a end &&& &(a + 1)"), + check("fn a -> a end &&& &(a + 1) |> b"), + check("fn a -> a end &&& &(a + 1) |> b |> c") + ], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -1810,7 +1978,7 @@ defmodule Spitfire.SystematicOperatorsTest do describe "unary with do/end blocks followed by range" do test "unary op do/end block followed by range" do - failures = + for_result = for unary <- @simple_unary_ops do s_unary = unary_op_to_string(unary) @@ -1823,6 +1991,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("#{s_unary}quote do\n a\nend..b//c") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -1832,13 +2003,15 @@ defmodule Spitfire.SystematicOperatorsTest do test "unary + case followed by pipe and power operators" do failures = - [ - check("+case a do\n _ -> b\nend |> c"), - check("+case a do\n _ -> b\nend |> c ** d"), - check("+case a do\n _ -> b\nend |> c ** d >>> e"), - check("-case a do\n _ -> b\nend |> c ** d") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [ + check("+case a do\n _ -> b\nend |> c"), + check("+case a do\n _ -> b\nend |> c ** d"), + check("+case a do\n _ -> b\nend |> c ** d >>> e"), + check("-case a do\n _ -> b\nend |> c ** d") + ], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -1852,14 +2025,16 @@ defmodule Spitfire.SystematicOperatorsTest do describe "char literals with pipe to data structures" do test "char literal piped to struct/map" do failures = - [ - check("?a |> %Foo{}"), - check("?a |> %Foo{b: c}"), - check("?a |> %{b: c}"), - check("'M' |> %Foo{}"), - check("'M' |> %Foo{a: b}") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [ + check("?a |> %Foo{}"), + check("?a |> %Foo{b: c}"), + check("?a |> %{b: c}"), + check("'M' |> %Foo{}"), + check("'M' |> %Foo{a: b}") + ], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -1867,12 +2042,10 @@ defmodule Spitfire.SystematicOperatorsTest do test "charlist piped to struct in function call" do failures = - [ - check("foo('M' |> %Baz{})"), - check("Foo.bar('M' |> %Baz{a: b})"), - check("Foo.bar('M' |> %Baz{a: b, c: d})") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [check("foo('M' |> %Baz{})"), check("Foo.bar('M' |> %Baz{a: b})"), check("Foo.bar('M' |> %Baz{a: b, c: d})")], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -1886,14 +2059,10 @@ defmodule Spitfire.SystematicOperatorsTest do describe "range inside bitstring" do test "char literal range in bitstring" do failures = - [ - check("<>"), - check("<>"), - check("<>"), - check("<>"), - check("<>") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [check("<>"), check("<>"), check("<>"), check("<>"), check("<>")], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -1901,13 +2070,10 @@ defmodule Spitfire.SystematicOperatorsTest do test "complex expressions in bitstring" do failures = - [ - check("<>"), - check("<>"), - check("< b>>"), - check("<>") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [check("<>"), check("<>"), check("< b>>"), check("<>")], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -1920,7 +2086,7 @@ defmodule Spitfire.SystematicOperatorsTest do describe "unary operators inside bitstring" do test "unary with do/end block inside bitstring" do - failures = + for_result = for unary <- @simple_unary_ops do s_unary = unary_op_to_string(unary) @@ -1930,6 +2096,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("<>") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -1939,13 +2108,15 @@ defmodule Spitfire.SystematicOperatorsTest do test "unary + do/end blocks inside bitstring" do failures = - [ - check("<<+quote do\n a\nend>>"), - check("<<-case a do\n _ -> b\nend>>"), - check("<<+quote do\n a\nend, b::8>>"), - check("{:ok, <<+quote do\n a\nend>>}") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [ + check("<<+quote do\n a\nend>>"), + check("<<-case a do\n _ -> b\nend>>"), + check("<<+quote do\n a\nend, b::8>>"), + check("{:ok, <<+quote do\n a\nend>>}") + ], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -1959,14 +2130,16 @@ defmodule Spitfire.SystematicOperatorsTest do describe "complex interpolation with capture and pipe" do test "capture with pipe inside interpolation" do failures = - [ - check(~s'"foo\#{&(a |> b)}bar"'), - check(~s'"foo\#{&(a |> b + 1)}bar"'), - check(~s'"foo\#{&(a |> b + 1) |> c}bar"'), - check("'foo\#{&(a |> b + 1)}bar'"), - check("'foo\#{&(0 |> Foo + 1) |> %{a: b}}bar'") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [ + check(~s'"foo\#{&(a |> b)}bar"'), + check(~s'"foo\#{&(a |> b + 1)}bar"'), + check(~s'"foo\#{&(a |> b + 1) |> c}bar"'), + check("'foo\#{&(a |> b + 1)}bar'"), + check("'foo\#{&(0 |> Foo + 1) |> %{a: b}}bar'") + ], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -1974,13 +2147,15 @@ defmodule Spitfire.SystematicOperatorsTest do test "capture followed by in and fn with pipe" do failures = - [ - check("&(a + 1) in b"), - check("&(a + 1) in fn -> b end"), - check("&(a + 1) in fn -> b end |> c"), - check("&(a + 1) in fn -> b end |> quote do\n c\nend") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [ + check("&(a + 1) in b"), + check("&(a + 1) in fn -> b end"), + check("&(a + 1) in fn -> b end |> c"), + check("&(a + 1) in fn -> b end |> quote do\n c\nend") + ], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -1994,13 +2169,15 @@ defmodule Spitfire.SystematicOperatorsTest do describe "heredoc charlists with operators" do test "capture with heredoc charlist followed by range" do failures = - [ - check("&('''\nfoo\n''' + 1)"), - check("&('''\nfoo\n''' + 1)..b"), - check("&('''\nfoo\n''' + 1)..b//c"), - check("a..&('''\nfoo\n''' + 1)") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [ + check("&('''\nfoo\n''' + 1)"), + check("&('''\nfoo\n''' + 1)..b"), + check("&('''\nfoo\n''' + 1)..b//c"), + check("a..&('''\nfoo\n''' + 1)") + ], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -2008,13 +2185,15 @@ defmodule Spitfire.SystematicOperatorsTest do test "heredoc charlist with interpolation containing operators" do failures = - [ - check("'''\nfoo \#{a + b}\n'''"), - check("'''\nfoo \#{a |> b}\n'''"), - check("'''\nfoo \#{&(a + 1)}\n'''"), - check("'''\nfoo \#{%{a: b}}\n'''") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [ + check("'''\nfoo \#{a + b}\n'''"), + check("'''\nfoo \#{a |> b}\n'''"), + check("'''\nfoo \#{&(a + 1)}\n'''"), + check("'''\nfoo \#{%{a: b}}\n'''") + ], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -2028,12 +2207,10 @@ defmodule Spitfire.SystematicOperatorsTest do describe "combination of multiple complex patterns" do test "fn in capture with operators" do failures = - [ - check("&({a, b} + 1) in fn -> c end"), - check("fn a -> &(b + 1) end"), - check("fn a -> &(b + 1) in c end") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [check("&({a, b} + 1) in fn -> c end"), check("fn a -> &(b + 1) end"), check("fn a -> &(b + 1) in c end")], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -2041,13 +2218,10 @@ defmodule Spitfire.SystematicOperatorsTest do test "atoms as quoted keys with operators" do failures = - [ - check("%{:'ok' => a + b}"), - check("%{:'ok' + a => b}"), - check("<<:'ok' + a>>"), - check("[:'ok': a + b]") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [check("%{:'ok' => a + b}"), check("%{:'ok' + a => b}"), check("<<:'ok' + a>>"), check("[:'ok': a + b]")], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -2055,12 +2229,10 @@ defmodule Spitfire.SystematicOperatorsTest do test "with expression inside capture" do failures = - [ - check("&(with a <- b, do: c)"), - check("&(with a <- b, do: c + d)"), - check("&(with a <- b, do: c) |> d") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [check("&(with a <- b, do: c)"), check("&(with a <- b, do: c + d)"), check("&(with a <- b, do: c) |> d")], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -2074,14 +2246,16 @@ defmodule Spitfire.SystematicOperatorsTest do describe "range with keyword list operand" do test "range with keyword list as right operand" do failures = - [ - check("a..['key': b]"), - check("a..b..['key': c]"), - check("a..['key': b]//c"), - check("a..['do': b, 'else': c]"), - check("\"\" <> \"foo\"..['do': bar]") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [ + check("a..['key': b]"), + check("a..b..['key': c]"), + check("a..['key': b]//c"), + check("a..['do': b, 'else': c]"), + check("\"\" <> \"foo\"..['do': bar]") + ], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -2089,12 +2263,14 @@ defmodule Spitfire.SystematicOperatorsTest do test "range with keyword list in case expression" do failures = - [ - check("case a..['key': b] do\n _ -> c\nend"), - check("case \"\" <> \"foo\"..['do': bar] do\n _ -> c\nend"), - check("case a..b..['key': c]//d do\n _ -> e\nend") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [ + check("case a..['key': b] do\n _ -> c\nend"), + check("case \"\" <> \"foo\"..['do': bar] do\n _ -> c\nend"), + check("case a..b..['key': c]//d do\n _ -> e\nend") + ], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -2102,11 +2278,10 @@ defmodule Spitfire.SystematicOperatorsTest do test "range with heredoc in keyword list" do failures = - [ - check("a..['key': \"\"\"\nfoo\n\"\"\"]"), - check("case a..['do': \"\"\"\nfoo\n\"\"\"] do\n _ -> b\nend") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [check("a..['key': \"\"\"\nfoo\n\"\"\"]"), check("case a..['do': \"\"\"\nfoo\n\"\"\"] do\n _ -> b\nend")], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -2120,14 +2295,16 @@ defmodule Spitfire.SystematicOperatorsTest do describe "pipe inside function call arguments" do test "charlist piped to struct/map in function call" do failures = - [ - check("foo('M' |> a)"), - check("foo('M' |> %{a: b})"), - check("foo('M' |> %Foo{a: b})"), - check("foo('abc' |> %{\"ok\": :err})"), - check("Foo.bar('M' |> %{a: b})") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [ + check("foo('M' |> a)"), + check("foo('M' |> %{a: b})"), + check("foo('M' |> %Foo{a: b})"), + check("foo('abc' |> %{\"ok\": :err})"), + check("Foo.bar('M' |> %{a: b})") + ], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -2135,12 +2312,14 @@ defmodule Spitfire.SystematicOperatorsTest do test "pipe with heredoc in struct inside function call" do failures = - [ - check("foo(a |> %{b: \"\"\"\nfoo\n\"\"\"})"), - check("foo('M' |> %Baz{\"ok\": \"\"\"\nfoo\n\"\"\"})"), - check("Foo.bar('M' |> %{a: \"\"\"\nfoo \#{b} bar\n\"\"\"})") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [ + check("foo(a |> %{b: \"\"\"\nfoo\n\"\"\"})"), + check("foo('M' |> %Baz{\"ok\": \"\"\"\nfoo\n\"\"\"})"), + check("Foo.bar('M' |> %{a: \"\"\"\nfoo \#{b} bar\n\"\"\"})") + ], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -2148,20 +2327,22 @@ defmodule Spitfire.SystematicOperatorsTest do test "multiple pipes in function call arguments" do failures = - [ - check("foo(a |> b |> c)"), - check("foo(a |> b, c |> d)"), - check("foo('M' |> a |> b)"), - check("Foo.bar(a |> %{b: c}, d |> e)") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [ + check("foo(a |> b |> c)"), + check("foo(a |> b, c |> d)"), + check("foo('M' |> a |> b)"), + check("Foo.bar(a |> %{b: c}, d |> e)") + ], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" end test "pipe with operators in function call" do - failures = + for_result = for op <- @simple_binary_ops do s_op = op_to_string(op) @@ -2170,6 +2351,9 @@ defmodule Spitfire.SystematicOperatorsTest do check("foo(a #{s_op} b |> c)") ] end + + failures = + for_result |> List.flatten() |> Enum.reject(&is_nil/1) @@ -2185,13 +2369,15 @@ defmodule Spitfire.SystematicOperatorsTest do describe "struct/map with quoted string keys" do test "struct with quoted string key and heredoc value" do failures = - [ - check("%Foo{\"ok\": a}"), - check("%Foo{\"ok\": \"\"\"\nfoo\n\"\"\"}"), - check("%{\"ok\": \"\"\"\nfoo \#{a} bar\n\"\"\"}"), - check("foo(%Baz{\"ok\": \"\"\"\nfoo\n\"\"\"})") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [ + check("%Foo{\"ok\": a}"), + check("%Foo{\"ok\": \"\"\"\nfoo\n\"\"\"}"), + check("%{\"ok\": \"\"\"\nfoo \#{a} bar\n\"\"\"}"), + check("foo(%Baz{\"ok\": \"\"\"\nfoo\n\"\"\"})") + ], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -2199,12 +2385,14 @@ defmodule Spitfire.SystematicOperatorsTest do test "pipe into struct with heredoc" do failures = - [ - check("a |> %Foo{b: \"\"\"\nfoo\n\"\"\"}"), - check("a |> %{\"ok\": \"\"\"\nfoo\n\"\"\"}"), - check("'M' |> %Baz{\"ok\": \"\"\"\nfoo \#{a} bar\n\"\"\"})") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [ + check("a |> %Foo{b: \"\"\"\nfoo\n\"\"\"}"), + check("a |> %{\"ok\": \"\"\"\nfoo\n\"\"\"}"), + check("'M' |> %Baz{\"ok\": \"\"\"\nfoo \#{a} bar\n\"\"\"})") + ], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -2217,13 +2405,15 @@ defmodule Spitfire.SystematicOperatorsTest do describe "case with complex first argument" do test "case with binary operator expression" do - failures = + for_result = for op <- @simple_binary_ops do s_op = op_to_string(op) check("case a #{s_op} b do\n _ -> c\nend") end - |> Enum.reject(&is_nil/1) + + failures = + Enum.reject(for_result, &is_nil/1) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -2231,13 +2421,15 @@ defmodule Spitfire.SystematicOperatorsTest do test "case with string concatenation and range" do failures = - [ - check("case \"\" <> \"foo\" do\n _ -> a\nend"), - check("case a..b do\n _ -> c\nend"), - check("case \"\" <> \"foo\"..a do\n _ -> b\nend"), - check("case a..b//c do\n _ -> d\nend") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [ + check("case \"\" <> \"foo\" do\n _ -> a\nend"), + check("case a..b do\n _ -> c\nend"), + check("case \"\" <> \"foo\"..a do\n _ -> b\nend"), + check("case a..b//c do\n _ -> d\nend") + ], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}" @@ -2245,12 +2437,14 @@ defmodule Spitfire.SystematicOperatorsTest do test "case with range and keyword list" do failures = - [ - check("case a..[b: c] do\n _ -> d\nend"), - check("case \"\" <> \"foo\"..[do: a] do\n _ -> b\nend"), - check("case a..['key': \"\"\"\nfoo\n\"\"\"] do\n _ -> b\nend") - ] - |> Enum.reject(&is_nil/1) + Enum.reject( + [ + check("case a..[b: c] do\n _ -> d\nend"), + check("case \"\" <> \"foo\"..[do: a] do\n _ -> b\nend"), + check("case a..['key': \"\"\"\nfoo\n\"\"\"] do\n _ -> b\nend") + ], + &is_nil/1 + ) assert failures == [], "Failed combinations: #{inspect(failures, pretty: true, limit: :infinity)}"