From 7545cdc8cdfa46b17040968e5aaa3e49e29fdf3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crist=C3=B3bal=20Tapia?= Date: Sun, 28 Jul 2024 05:09:51 -0700 Subject: [PATCH] feat(julia): Add julia support (#185) Add support for julia function and structs + test cases included. Closes #184 --------- Co-authored-by: josephsdavid Co-authored-by: Daniel Mathiot --- .github/workflows/test.yml | 11 +- README.md | 1 + lua/neogen/configurations/julia.lua | 230 +++++++++++++++++++++ lua/neogen/init.lua | 6 +- lua/neogen/templates/julia.lua | 36 ++++ tests/minimal_init.lua | 16 +- tests/neogen/julia_julia_spec.lua | 308 ++++++++++++++++++++++++++++ 7 files changed, 597 insertions(+), 11 deletions(-) create mode 100644 lua/neogen/configurations/julia.lua create mode 100644 lua/neogen/templates/julia.lua create mode 100644 tests/neogen/julia_julia_spec.lua diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fa60730..4405a7e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,12 +16,19 @@ jobs: with: repository: danymat/neogen path: neogen - - uses: rhysd/action-setup-vim@v1 + + - name: Setup Neovim + uses: rhysd/action-setup-vim@v1 with: neovim: true version: ${{ matrix.neovim }} + + - name: Compile Needed Parsers + working-directory: ./neogen + run: | + nvim -u tests/minimal_init.lua --headless -c "TSInstallSync python lua julia" -c "q" + - name: Run tests working-directory: ./neogen run: | - nvim --version make test diff --git a/README.md b/README.md index 6912d31..7fc5367 100644 --- a/README.md +++ b/README.md @@ -236,6 +236,7 @@ There is a list of supported languages and fields, with their annotation style | java | [Javadoc](https://docs.oracle.com/javase/1.5.0/docs/tooldocs/windows/javadoc.html#documentationcomments) (`"javadoc`) | `func`, `class` | | javascript | [JSDoc](https://jsdoc.app) (`"jsdoc"`) | `func`, `class`, `type`, `file` | | javascriptreact | [JSDoc](https://jsdoc.app) (`"jsdoc"`) | `func`, `class`, `type`, `file` | +| julia | [Julia](https://docs.julialang.org/en/v1/manual/documentation/#Writing-Documentation) (`"julia"`) | `func`, `class` | | kotlin | [KDoc](https://kotlinlang.org/docs/kotlin-doc.html) (`"kdoc"`) | `func`, `class` | | lua | [Emmylua](https://emmylua.github.io/) (`"emmylua"`)
[Ldoc](https://stevedonovan.github.io/ldoc/manual/doc.md.html) (`"ldoc"`) | `func`, `class`, `type`, `file` | | php | [Php-doc](https://docs.phpdoc.org/3.0/guide/references/phpdoc/index.html) (`"phpdoc"`) | `func`, `type`, `class` | diff --git a/lua/neogen/configurations/julia.lua b/lua/neogen/configurations/julia.lua new file mode 100644 index 0000000..6c7eeb0 --- /dev/null +++ b/lua/neogen/configurations/julia.lua @@ -0,0 +1,230 @@ +local nodes_utils = require("neogen.utilities.nodes") +local extractors = require("neogen.utilities.extractors") +local template = require("neogen.template") +local i = require("neogen.types.template").item + +local parent = { + func = { "function_definition" }, + class = { "struct_definition" }, +} + +--- Extract data for typed parameters +---@param nodes +---@param results +---@return +local function process_typed_parameters(nodes, results) + if nodes["typed_expression"] then + results["typed_parameters"] = {} + for _, n in pairs(nodes["typed_expression"]) do + local type_subtree = { + { position = 1, extract = true, as = i.Parameter }, + { position = 2, extract = true, as = i.Type }, + } + local typed_parameters = nodes_utils:matching_nodes_from(n, type_subtree) + typed_parameters = extractors:extract_from_matched(typed_parameters) + table.insert(results["typed_parameters"], typed_parameters) + end + end + return results +end + +return { + -- Search for these nodes + parent = parent, + + -- Traverse down these nodes and extract the information as necessary + data = { + class = { + + ["struct_definition"] = { + ["0"] = { + extract = function(node) + local results = {} + + local tree = { + -- Get the name of the struct + { + retrieve = "first", + node_type = "identifier", + extract = true, + as = "name", + }, + -- Get type parameters + { + retrieve = "first", + recursive = true, + node_type = "type_parameter_list", + subtree = { + { + retrieve = "all", + node_type = "identifier", + extract = true, + as = "type_params", + }, + }, + }, + -- Fields + { + retrieve = "all", + node_type = "typed_expression", + extract = true, + }, + } + local nodes = nodes_utils:matching_nodes_from(node, tree) + results = process_typed_parameters(nodes, results) + local res = extractors:extract_from_matched(nodes) + + local signature = res.name[1] + if res.type_params then + signature = signature .. "{" .. table.concat(res.type_params, ", ") .. "}" + end + + results.signature = { signature } + results[i.HasParameter] = (res.typed_expression or res.identifier) and { true } or nil + results[i.Type] = res.type + results[i.Parameter] = res.identifier or nil + + return results + end, + }, + }, + }, + func = { + + ["function_definition"] = { + + ["0"] = { + extract = function(node) + local results = {} + + local tree = { + { + retrieve = "first", + node_type = "signature", + recursive = true, + subtree = { + { + retrieve = "first", + node_type = "call_expression", + recursive = true, + subtree = { + -- Get the name of the function + { + position = 1, + node_type = "identifier", + extract = true, + as = "f_name", + }, + -- ... this considers cases like 'Base.add(...)' + { + position = 1, + node_type = "field_expression", + extract = true, + as = "f_name", + }, + }, + }, + { + retrieve = "first", + node_type = "argument_list", + recursive = true, + subtree = { + { + retrieve = "all", + as = "param_list", + extract = true, + }, + }, + }, + }, + }, + { + retrieve = "first", + node_type = "argument_list", + recursive = true, + subtree = { + -- Parameters without type definition + { + retrieve = "all", + node_type = "identifier", + extract = true, + }, + -- Typed parameters + { + retrieve = "all", + node_type = "typed_expression", + extract = true, + }, + -- Optional parameters + { + retrieve = "all", + node_type = "named_argument", + subtree = { + { retrieve = "all", node_type = "typed_expression", extract = true }, + }, + }, + { + retrieve = "all", + node_type = "named_argument", + subtree = { { retrieve = "all", node_type = "identifier", extract = true } }, + }, + -- Splat parameters + { + retrieve = "all", + node_type = "splat_expression", + subtree = { { retrieve = "all", node_type = "identifier", extract = true } }, + }, + }, + }, + { + retrieve = "first", + node_type = "block", + subtree = { + { + retrieve = "all", + node_type = "return_statement", + recursive = true, + extract = true, + }, + }, + }, + } + local nodes = nodes_utils:matching_nodes_from(node, tree) + + -- Add table with {params, types} + results = process_typed_parameters(nodes, results) + + local res = extractors:extract_from_matched(nodes) + -- Make the signature + local signature + if res.param_list then + signature = res.f_name[1] .. "(" .. table.concat(res.param_list, ", ") .. ")" + else + signature = res.f_name[1] .. "()" + end + + results[i.HasParameter] = (res.typed_expression or res.identifier) and { true } or nil + results[i.Type] = res.type + results.signature = { signature } + results.params = res.params + results[i.Parameter] = res.identifier + results[i.Return] = res.return_statement + results[i.ReturnTypeHint] = res[i.ReturnTypeHint] + results[i.HasReturn] = (res.return_statement or res.anonymous_return or res[i.ReturnTypeHint]) and + { true } + or nil + + return results + end, + }, + }, + }, + }, + + -- Use default granulator and generator + locator = nil, + granulator = nil, + generator = nil, + + template = template:add_default_annotation("julia"), +} diff --git a/lua/neogen/init.lua b/lua/neogen/init.lua index f404156..401ff39 100644 --- a/lua/neogen/init.lua +++ b/lua/neogen/init.lua @@ -241,6 +241,10 @@ end --- --- Note: We will only document `major` and `minor` versions, not `patch` ones. (only X and Y in X.Y.z) --- +--- ## 2.19.0~ +--- - Add support for julia (`julia`) ! (#185) +--- ## 2.18.0~ +--- - Add tests cases to tests/ for annotation generation with basic support for python (#174) --- ## 2.17.1~ --- - Python raises now supports `raise foo.Bar()` syntax --- ## 2.17.0~ @@ -305,7 +309,7 @@ end --- with multiple annotation conventions. ---@tag neogen-changelog ---@toc_entry Changes in neogen plugin -neogen.version = "2.18.0" +neogen.version = "2.19.0" --minidoc_afterlines_end return neogen diff --git a/lua/neogen/templates/julia.lua b/lua/neogen/templates/julia.lua new file mode 100644 index 0000000..482eb0a --- /dev/null +++ b/lua/neogen/templates/julia.lua @@ -0,0 +1,36 @@ +local i = require("neogen.types.template").item + +return { + { nil, '""" $1 """', { no_results = true, type = { "func" } } }, + { nil, '""" $1 """', { no_results = true, type = { "class" } } }, + { nil, '"""' }, + { "signature", " %s" }, + -- { nil, "" }, + { nil, "" }, + { nil, "$1" }, + { nil, "" }, + { i.HasParameter, "# Arguments", { type = { "func" } } }, + { i.HasParameter, "# Fields", { type = { "class" } } }, + { + i.Parameter, + "- `%s`: $1", + { required = "parameters", type = { "class" } }, + }, + { + { i.Parameter, i.Type }, + "- `%s::%s`: $1", + { required = "typed_parameters", type = { "func" } }, + }, + { + { i.Parameter, i.Type }, + "- `%s::%s`: $1", + { required = "typed_parameters", type = { "class" } }, + }, + { + i.Parameter, + "- `%s`: $1", + { required = "parameters", type = { "func" } }, + }, + + { nil, '"""' }, +} diff --git a/tests/minimal_init.lua b/tests/minimal_init.lua index b736ba5..19dd321 100644 --- a/tests/minimal_init.lua +++ b/tests/minimal_init.lua @@ -21,13 +21,13 @@ require("plenary.busted") vim.cmd("runtime plugin/nvim-treesitter.lua") --- Some tests require the Python parser --- vim.cmd([[TSInstallSync! python]]) -require("nvim-treesitter.configs").setup({ - ensured_installed = { - "python", - "lua" - } -}) +-- -- Some tests require the Python parser +-- -- vim.cmd([[TSInstallSync! python]]) +-- require("nvim-treesitter.configs").setup({ +-- ensured_installed = { +-- "python", +-- "lua" +-- } +-- }) require("neogen").setup({ snippet_engine = "nvim" }) diff --git a/tests/neogen/julia_julia_spec.lua b/tests/neogen/julia_julia_spec.lua new file mode 100644 index 0000000..f56f6a7 --- /dev/null +++ b/tests/neogen/julia_julia_spec.lua @@ -0,0 +1,308 @@ +--- Test cases for julia +--- +--- @module 'tests.neogen.julia_spec' + +local specs = require("tests.utils.specs") + +local function make_julia(source) + return specs.make_docstring(source, "julia", { annotation_convention = { julia = "julia" } }) +end + +describe("julia: julia", function() + describe("func", function() + it("works with an empty function", function() + local source = [[ +function foo() |cursor|end + ]] + + local expected = [[ +""" + foo() + +[TODO:description] + +""" +function foo() end + ]] + + local result = make_julia(source) + + assert.equal(expected, result) + end) + + it("works with arguments", function() + local source = [[ +function foo(a, b) +|cursor| +end + ]] + + local expected = [[ +""" + foo(a, b) + +[TODO:description] + +# Arguments +- `a`: [TODO:parameter] +- `b`: [TODO:parameter] +""" +function foo(a, b) + +end + ]] + + local result = make_julia(source) + + assert.equal(expected, result) + end) + + it("works with arguments in a new line", function() + local source = [[ +function foo( + a, b + ) +|cursor| +end + ]] + + local expected = [[ +""" + foo(a, b) + +[TODO:description] + +# Arguments +- `a`: [TODO:parameter] +- `b`: [TODO:parameter] +""" +function foo( + a, b + ) + +end + ]] + + local result = make_julia(source) + + assert.equal(expected, result) + end) + + it("works with typed arguments", function() + local source = [[ +function foo(a::Int, b::Float64) +|cursor| +end + ]] + + local expected = [[ +""" + foo(a::Int, b::Float64) + +[TODO:description] + +# Arguments +- `a::Int`: [TODO:description] +- `b::Float64`: [TODO:description] +""" +function foo(a::Int, b::Float64) + +end + ]] + + local result = make_julia(source) + + assert.equal(expected, result) + end) + + it("works with typed and untyped arguments", function() + local source = [[ +function foo(a::Int, b) +|cursor| +end + ]] + + local expected = [[ +""" + foo(a::Int, b) + +[TODO:description] + +# Arguments +- `a::Int`: [TODO:description] +- `b`: [TODO:parameter] +""" +function foo(a::Int, b) + +end + ]] + + local result = make_julia(source) + + assert.equal(expected, result) + end) + + it("works with splat arguments", function() + local source = [[ +function foo(x..., b) +|cursor| +end + ]] + + local expected = [[ +""" + foo(x..., b) + +[TODO:description] + +# Arguments +- `b`: [TODO:parameter] +- `x`: [TODO:parameter] +""" +function foo(x..., b) + +end + ]] + + local result = make_julia(source) + + assert.equal(expected, result) + end) + + it("works with optional arguments", function() + local source = [[ +function foo(a, b=1) +|cursor| +end + ]] + + local expected = [[ +""" + foo(a, b=1) + +[TODO:description] + +# Arguments +- `a`: [TODO:parameter] +- `b`: [TODO:parameter] +""" +function foo(a, b=1) + +end + ]] + + local result = make_julia(source) + + assert.equal(expected, result) + end) + + it("works when extinding a methods (e.g. from Base)", function() + local source = [[ +function Base.foo(a, b) +|cursor| +end + ]] + + local expected = [[ +""" + Base.foo(a, b) + +[TODO:description] + +# Arguments +- `a`: [TODO:parameter] +- `b`: [TODO:parameter] +""" +function Base.foo(a, b) + +end + ]] + + local result = make_julia(source) + + assert.equal(expected, result) + end) + end) + + describe("class", function() + it("works with an empty struct", function() + local source = [[ +struct Foo |cursor|end + ]] + + local expected = [[ +""" + Foo + +[TODO:description] + +""" +struct Foo end + ]] + + local result = make_julia(source) + + assert.equal(expected, result) + end) + + it("works with a struct with typed fields", function() + local source = [[ +struct Foo + a::Int|cursor| + b::Float64 +end + ]] + + local expected = [[ +""" + Foo + +[TODO:description] + +# Fields +- `a::Int`: [TODO:description] +- `b::Float64`: [TODO:description] +""" +struct Foo + a::Int + b::Float64 +end + ]] + + local result = make_julia(source) + + assert.equal(expected, result) + end) + + it("works with a parametric struct with typed fields", function() + local source = [[ +struct Foo{T} + a::Vector{T}|cursor| + b::Float64 +end + ]] + + local expected = [[ +""" + Foo{T} + +[TODO:description] + +# Fields +- `a::Vector{T}`: [TODO:description] +- `b::Float64`: [TODO:description] +""" +struct Foo{T} + a::Vector{T} + b::Float64 +end + ]] + + local result = make_julia(source) + + assert.equal(expected, result) + end) + end) +end) + +-- vim: set shiftwidth=4: