Current section
Files
Jump to
Current section
Files
lib/zig/parser/unit.ex
defmodule Zig.Parser.Unit do
@moduledoc """
parses zig code and converts test blocks to test functions
"""
defstruct tests: [],
file: nil,
offset: 0,
test_dirs: ["./"],
context: []
import NimbleParsec
whitespace = ascii_string([?\s, ?\n], min: 1)
blankspace = ascii_string([?\s], min: 1)
@close_paren 41
defcombinatorp(
:parenthetical,
string("(")
|> repeat(
choice([
parsec(:parenthetical),
ascii_char(not: @close_paren)
])
)
|> string(")")
)
init_context =
empty()
|> post_traverse(:init_context)
# file and line number declaration
file_line_decl =
string("// ref: ")
|> ascii_string([not: ?\s], min: 1)
|> string(" line: ")
|> ascii_string([?0..?9], min: 1)
|> string("\n")
|> post_traverse(:parse_file_line_decl)
# test declaration
test_decl =
ignore(
string("test")
|> concat(whitespace)
|> string("\"")
)
|> ascii_string([not: ?\"], min: 1)
|> ignore(string("\""))
|> post_traverse(:parse_test_decl)
# assert statement
assert_parser =
ignore(string("assert("))
|> repeat(
choice([
parsec(:parenthetical),
ascii_char(not: @close_paren)
])
)
|> ignore(string(")"))
|> pre_traverse(:parse_assert)
if Mix.env() == :test do
defparsec(:test_parenthetical, parsec(:parenthetical))
defparsec(:test_decl_parser, concat(init_context, test_decl))
defparsec(:test_assert_parser, concat(init_context, assert_parser))
end
test_line =
optional(blankspace)
|> concat(test_decl)
|> concat(whitespace)
|> string("{")
|> optional(ascii_string([not: ?\n], min: 1))
|> string("\n")
normal_line =
optional(
repeat(
choice([
assert_parser,
ascii_char(not: ?\n)
])
)
)
|> string("\n")
unit_code =
init_context
|> repeat(
choice([
file_line_decl,
test_line,
normal_line
])
)
defparsec(:unit_parser, unit_code)
#############################################################################
## POST-TRAVERSAL IMPLEMENTATIONS
# ninjas in a context module for the context map.
defp init_context(_, _, c, _, _) do
{[], struct(__MODULE__, Map.put_new(c, :line, 1))}
end
defp parse_file_line_decl(
_rest,
l = [_, line_str, _, file | _],
context,
{line, _char},
_offset
) do
this_line = String.to_integer(line_str)
{l, %{context | file: file, offset: this_line - line + 1}}
end
defp parse_test_decl(_rest, [test_name], context, _line, _offset) do
test_nif = new_nif(test_name)
{["pub fn #{test_nif.name}() !void"], %{context | tests: [test_nif | context.tests]}}
end
defp parse_assert(_rest, content, context, {line, _char}, _offset) do
test_content = content |> Enum.reverse() |> IO.iodata_to_binary()
rewritten = "try assert(#{test_content}, \"#{context.file}\", #{line + context.offset})"
{[rewritten], context}
end
alias Zig.Parser.Nif
defp new_nif(name) do
%Nif{
name: String.to_atom("test_#{Zig.Unit.name_to_hash(name)}"),
test: String.to_atom(name),
arity: 0,
args: [],
retval: "!void"
}
end
#############################################################################
## API
def parse(code, info) do
case unit_parser(code, []) do
{:ok, code, _, %{tests: tests}, _, _} ->
{Enum.map(
tests,
&contextualize(&1, info[:context] || [])
), code}
err ->
raise CompileError,
info ++
[description: "error parsing code in #{info[:file]}: #{inspect(err)}"]
end
end
defp contextualize(test, context) do
rename =
case context do
[] ->
test.name
lst ->
lst
|> Kernel.++([test.name])
|> Enum.join(".")
|> String.to_atom()
end
%{test | name: rename}
end
end