diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..982fb99 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +zig_doc-*.tar + +# Temporary files, for example, from tests. +/tmp/ diff --git a/lib/mix.tasks/zig_doc.ex b/lib/mix.tasks/zig_doc.ex index bf81fe9..4b9c24d 100644 --- a/lib/mix.tasks/zig_doc.ex +++ b/lib/mix.tasks/zig_doc.ex @@ -4,7 +4,7 @@ defmodule Mix.Tasks.ZigDoc do @shortdoc "Generate documentation for the project" @requirements ["compile"] - @spec run([String.t], keyword) :: :ok + @spec run([String.t()], keyword) :: :ok @moduledoc """ see `Mix.Tasks.Docs` for more information diff --git a/lib/zig.doc.ex b/lib/zig.doc.ex index e8cd847..815ff89 100644 --- a/lib/zig.doc.ex +++ b/lib/zig.doc.ex @@ -1,5 +1,4 @@ defmodule Zig.Doc do - alias Zig.Doc.Generator @doc """ @@ -33,7 +32,7 @@ defmodule Zig.Doc do |> add_zig_doc_config(zig_doc_options) docs - |> List.first + |> List.first() |> Map.get(:docs) |> dbg(limit: 25) diff --git a/lib/zig.doc/generator.ex b/lib/zig.doc/generator.ex index 4078108..ee5da4b 100644 --- a/lib/zig.doc/generator.ex +++ b/lib/zig.doc/generator.ex @@ -7,43 +7,52 @@ defmodule Zig.Doc.Generator do with {:ok, file_path} <- Keyword.fetch(options, :file), {{:ok, file}, :read, _} <- {File.read(file_path), :read, file_path}, {{:ok, sema}, :sema, _} <- {sema_module.run_sema(file_path), :sema, file_path} do - parsed_document = Zig.Parser.parse(file) - doc_ast = if moduledoc = parsed_document.doc_comment do - DocAST.parse!(moduledoc, "text/markdown", [file: file_path, line: 1]) - end + doc_ast = + if moduledoc = parsed_document.doc_comment do + DocAST.parse!(moduledoc, "text/markdown", file: file_path, line: 1) + end # TODO: needs source_path and source_url node = %ExDoc.ModuleNode{id: "#{id}", doc_line: 1, doc: doc_ast} Enum.reduce(parsed_document.code, node, &obtain_content(&1, &2, file_path, sema)) - else - :error -> Mix.raise("zig doc config error: configuration for module #{id} requires a `:file` option") - {{:error, reason}, :read, path} -> Mix.raise("zig doc error: file at `#{path}` doesn't exist") - {{:error, reason}, :sema, path} -> Mix.raise("zig doc error: sema failed for `#{path}`") + :error -> + Mix.raise( + "zig doc config error: configuration for module #{id} requires a `:file` option" + ) + + {{:error, reason}, :read, path} -> + Mix.raise("zig doc error: failure reading file at `#{path}` #{reason}") + + {{:error, _reason}, :sema, path} -> + Mix.raise("zig doc error: sema failed for `#{path}`") end end defp obtain_content({:fn, fun = %{pub: true}, fn_parts}, acc, file_path, sema) do - doc_ast = if fndoc = fun.doc_comment do - DocAST.parse!(fndoc, "text/markdown", [file: file_path, line: fun.position.line]) - end + doc_ast = + if fndoc = fun.doc_comment do + DocAST.parse!(fndoc, "text/markdown", file: file_path, line: fun.position.line) + end name = Keyword.fetch!(fn_parts, :name) type = Keyword.fetch!(fn_parts, :type) params = Keyword.fetch!(fn_parts, :params) # find the function in the sema - specs = sema.functions - |> Enum.find(&(&1.name == name)) - |> Spec.function_from_sema - |> List.wrap + specs = + sema.functions + |> Enum.find(&(&1.name == name)) + |> Spec.function_from_sema() + |> List.wrap() - param_string = params - |> Enum.map(fn {var, _, type} -> "#{var}: #{type}" end) - |> Enum.join(", ") + param_string = + params + |> Enum.map(fn {var, _, type} -> "#{var}: #{type}" end) + |> Enum.join(", ") signature = "#{name}(#{param_string}) #{type}" @@ -60,5 +69,52 @@ defmodule Zig.Doc.Generator do %{acc | docs: [node | acc.docs]} end + defp obtain_content({:const, const, {name, _, _}}, acc, file_path, sema) do + doc_ast = + if doc = const.doc_comment do + DocAST.parse!(doc, "text/markdown", file: file_path, line: const.position.line) + end + + # find the function in the sema + cond do + this_func = Enum.find(sema.functions, &(&1.name == name)) -> + specs = + this_func + |> Spec.function_from_sema() + |> List.wrap() + + param_string = Enum.join(this_func.args, ", ") + + signature = "#{name}(#{param_string}) #{this_func.return}" + + # TODO: needs source_path and source_url + node = %ExDoc.FunctionNode{ + id: "#{name}", + name: name, + arity: length(this_func.args), + doc: doc_ast, + signature: signature, + specs: specs + } + + %{acc | docs: [node | acc.docs]} + + this_type = Enum.find(sema.types, &(&1.name == name)) -> + + node = %ExDoc.TypeNode{ + id: "#{name}", + name: name, + signature: "#{name}", + doc: doc_ast, + spec: Spec.type_from_sema(this_type) + } + + %{acc | typespecs: [node | acc.typespecs]} + + true -> + acc + end + end + defp obtain_content(_, acc, _, _), do: acc end diff --git a/lib/zig.doc/sema.ex b/lib/zig.doc/sema.ex index 8698a1b..43b13ba 100644 --- a/lib/zig.doc/sema.ex +++ b/lib/zig.doc/sema.ex @@ -2,21 +2,33 @@ defmodule Zig.Doc.Sema do @type type :: atom @type fun :: %{ - name: atom, - return: type, - args: [type], - } + name: atom, + return: type, + args: [type] + } - @type const :: %{ - name: atom, - type: type - } + @type decls :: %{ + name: atom, + type: type + } + + @type collection :: %{ + vars: [decls], + consts: [decls], + functions: [fun] + } + + @type typedef :: %{ + name: atom, + def: atom | collection + } @type file :: %{ - functions: [function], - consts: [const], - types: [type] - } + functions: [function], + consts: [decls], + vars: [decls], + types: [typedef] + } def new(addin \\ []) do Enum.into(addin, %{functions: [], consts: [], types: []}) diff --git a/lib/zig.doc/spec.ex b/lib/zig.doc/spec.ex index 19bb2e6..eef2327 100644 --- a/lib/zig.doc/spec.ex +++ b/lib/zig.doc/spec.ex @@ -1,7 +1,7 @@ defmodule Zig.Doc.Spec do alias Zig.Doc.Sema - @spec function_from_sema(Sema.fun) :: Macro.t + @spec function_from_sema(Sema.fun()) :: Macro.t() def function_from_sema(fun) do name = fun.name @@ -10,4 +10,8 @@ defmodule Zig.Doc.Spec do {:"::", [], [{name, [], args}, return_type]} end + + def type_from_sema(type) do + {:"::", [], [{type.name, [], Elixir}, {type.def, [], Elixir}]} + end end diff --git a/test/_sources/const_function.zig b/test/_sources/const_function.zig new file mode 100644 index 0000000..1f2ef99 --- /dev/null +++ b/test/_sources/const_function.zig @@ -0,0 +1,6 @@ +fn foo(value: i32) i32 { + return value + 1; +} + +/// this is the function bar +pub const bar = foo; \ No newline at end of file diff --git a/test/_sources/type_basic.zig b/test/_sources/type_basic.zig new file mode 100644 index 0000000..67705e0 --- /dev/null +++ b/test/_sources/type_basic.zig @@ -0,0 +1,2 @@ +/// this is the foo type. +const foo = i32; \ No newline at end of file diff --git a/test/_support/sema.ex b/test/_support/sema.ex index c3bedf6..4ba9c81 100644 --- a/test/_support/sema.ex +++ b/test/_support/sema.ex @@ -1,6 +1,6 @@ defmodule Zig.SemaAPI do - @type json :: nil | boolean | number | String.t | [json] | %{optional(String.t) => json} - @callback run_sema(Path.t) :: {:ok, json} | {:error, String.t} + @type json :: nil | boolean | number | String.t() | [json] | %{optional(String.t()) => json} + @callback run_sema(Path.t()) :: {:ok, json} | {:error, String.t()} end Mox.defmock(Zig.SemaMock, for: Zig.SemaAPI) diff --git a/test/_support/zig_doc_case.ex b/test/_support/zig_doc_case.ex index 3f0598e..6fb1034 100644 --- a/test/_support/zig_doc_case.ex +++ b/test/_support/zig_doc_case.ex @@ -12,8 +12,7 @@ defmodule Zig.Doc.Case do end def get_module(file) do - [module] = - Zig.Doc.add_zig_doc_config([], [module: [file: file]], Zig.SemaMock) + [module] = Zig.Doc.add_zig_doc_config([], [module: [file: file]], Zig.SemaMock) module end @@ -23,13 +22,19 @@ defmodule Zig.Doc.Case do defmacro assert_code(string, data) do quote do - tgt = unquote(string) - |> Code.format_string! - |> IO.iodata_to_binary + tgt = + unquote(string) + |> Code.format_string!() + |> IO.iodata_to_binary() - assert tgt == unquote(data) - |> List.first - |> Macro.to_string() + # we'll never have multiple function heads, but here + # we need to be able to handle arrays and singular macros + # which is what types will give us. + assert tgt == + unquote(data) + |> List.wrap + |> List.first() + |> Macro.to_string() end end end diff --git a/test/documentation/const_function_test.exs b/test/documentation/const_function_test.exs new file mode 100644 index 0000000..d067de3 --- /dev/null +++ b/test/documentation/const_function_test.exs @@ -0,0 +1,25 @@ +defmodule ZigDocTest.Documentation.ConstFunctionTest do + use Zig.Doc.Case, async: true + + alias Zig.Doc.Sema + + test "documentation is generated for consts that are functions" do + expect_sema({:ok, Sema.new(functions: [%{name: :bar, return: :i32, args: [:i32]}])}) + + assert %{docs: [function]} = get_module("test/_sources/const_function.zig") + + assert [{:p, [], [" this is the function bar"], %{}}] = function.doc + + # note that we lose the signature because we won't be digging too hard to find + # the rest. + + assert "bar(i32) i32" = function.signature + + assert_code( + """ + bar(i32) :: i32 + """, + function.specs + ) + end +end diff --git a/test/documentation/function_test.exs b/test/documentation/function_test.exs index ada7049..d0606b9 100644 --- a/test/documentation/function_test.exs +++ b/test/documentation/function_test.exs @@ -6,15 +6,16 @@ defmodule ZigDocTest.Documentation.FunctionTest do test "function-level documentation is generated" do expect_sema({:ok, Sema.new(functions: [%{name: :foo, return: :i32, args: [:i32]}])}) - assert %{docs: [function]} = - get_module("test/_sources/function.zig") + assert %{docs: [function]} = get_module("test/_sources/function.zig") assert [{:p, [], [" this is the function foo"], %{}}] = function.doc assert "foo(value: i32) i32" = function.signature - assert_code(""" - foo(i32) :: i32 - """, - function.specs) + assert_code( + """ + foo(i32) :: i32 + """, + function.specs + ) end end diff --git a/test/documentation/module_test.exs b/test/documentation/module_test.exs index 4d2d32c..d1f67bf 100644 --- a/test/documentation/module_test.exs +++ b/test/documentation/module_test.exs @@ -4,7 +4,6 @@ defmodule ZigDocTest.Documentation.ModuleTest do alias Zig.Doc.Sema test "module-level documentation is generated" do - expect_sema({:ok, Sema.new()}) assert %{doc: [{:p, [], [" tests module-level comment content"], %{}}]} = diff --git a/test/documentation/type_basic_test.exs b/test/documentation/type_basic_test.exs new file mode 100644 index 0000000..0b0ce7b --- /dev/null +++ b/test/documentation/type_basic_test.exs @@ -0,0 +1,21 @@ +defmodule ZigDocTest.Documentation.TypeBasicTest do + use Zig.Doc.Case, async: true + + alias Zig.Doc.Sema + + test "type-level documentation is generated" do + expect_sema({:ok, Sema.new(types: [%{name: :foo, def: :i32}])}) + + assert %{typespecs: [type]} = get_module("test/_sources/type_basic.zig") + + assert [{:p, [], [" this is the foo type."], %{}}] = type.doc + assert "foo" = type.signature + + assert_code( + """ + foo :: i32 + """, + type.spec + ) + end +end