From 002231b034bde3199399146ae9dded5823f936e3 Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Mon, 23 Sep 2024 20:15:55 +0200 Subject: [PATCH 1/6] feat: add api-linter https://github.com/googleapis/api-linter --- lua/lint/linters/api-linter.lua | 77 +++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 lua/lint/linters/api-linter.lua diff --git a/lua/lint/linters/api-linter.lua b/lua/lint/linters/api-linter.lua new file mode 100644 index 00000000..9234482b --- /dev/null +++ b/lua/lint/linters/api-linter.lua @@ -0,0 +1,77 @@ +local descriptor_filepath = os.tmpname() +local cleanup_descriptor = function() + os.remove(descriptor_filepath) +end + +--- Function to set the `--descriptor-set-in` argument. +--- This requires the buf CLI, which will first build the descriptor file. +local function descriptor_set_in() + if vim.fn.executable("buf") == 0 then + error("buf CLI not found") + end + + -- search for the buf config, searching upwards from the current buffer's directory until $HOME. + local buffer_parent_dir = vim.fn.expand("%:p:h") -- the path to the folder of the opened .proto file. + local buf_config_filepaths = vim.fs.find( + { "buf.yaml", "buf.yml" }, + { path = buffer_parent_dir, upward = true, stop = vim.fs.normalize("~"), type = "file", limit = 1 } + ) + if #buf_config_filepaths == 0 then + error("Buf config file not found") + end + local buf_config_filepath = buf_config_filepaths[1] + + -- build the descriptor file. + local buf_config_folderpath = vim.fn.fnamemodify(buf_config_filepath, ":h") + local buf_cmd = { "buf", "build", "-o", descriptor_filepath } + local buf_cmd_opts = { cwd = buf_config_folderpath } + local obj = vim.system(buf_cmd, buf_cmd_opts):wait() + if obj.code ~= 0 then + error("Command failed: " .. vim.inspect(buf_cmd) .. "\n" .. obj.stderr) + end + + -- return the argument to be passed to the linter. + return "--descriptor-set-in=" .. descriptor_filepath +end + +return { + cmd = "api-linter", + stdin = false, + append_fname = true, + args = { + "--output-format=json", + "--disable-rule=core::0191::java-multiple-files", + "--disable-rule=core::0191::java-package", + "--disable-rule=core::0191::java-outer-classname", + descriptor_set_in, + }, + stream = "stdout", + ignore_exitcode = true, + env = nil, + parser = function(output) + if output == "" then + return {} + end + local json_output = vim.json.decode(output) + local diagnostics = {} + if json_output == nil then + return diagnostics + end + for _, item in ipairs(json_output) do + for _, problem in ipairs(item.problems) do + table.insert(diagnostics, { + message = problem.message, + file = item.file, + code = problem.rule_id .. " " .. problem.rule_doc_uri, + severity = vim.diagnostic.severity.WARN, + lnum = problem.location.start_position.line_number - 1, + col = problem.location.start_position.column_number - 1, + end_lnum = problem.location.end_position.line_number - 1, + end_col = problem.location.end_position.column_number - 1, + }) + end + end + cleanup_descriptor() + return diagnostics + end, +} From efaaae722da5a7e439667d83f3f6fbe85c06641e Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Thu, 3 Oct 2024 11:13:18 +0200 Subject: [PATCH 2/6] fix: rename to buf_api_linter --- lua/lint/linters/{api-linter.lua => buf_api_linter.lua} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lua/lint/linters/{api-linter.lua => buf_api_linter.lua} (100%) diff --git a/lua/lint/linters/api-linter.lua b/lua/lint/linters/buf_api_linter.lua similarity index 100% rename from lua/lint/linters/api-linter.lua rename to lua/lint/linters/buf_api_linter.lua From 4961f2f875fbc54558802d13881a2566abd3f2bf Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Thu, 3 Oct 2024 11:13:24 +0200 Subject: [PATCH 3/6] docs: add buf_api_linter --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 258ea0eb..2fe2481e 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ Other dedicated linters that are built-in are: | [biomejs][biomejs] | `biomejs` | | [blocklint][blocklint] | `blocklint` | | [buf_lint][buf_lint] | `buf_lint` | +| [buf_api_linter][buf_api_linter] | `buf_api_linter` | | [buildifier][buildifier] | `buildifier` | | [cfn-lint][cfn-lint] | `cfn_lint` | | [cfn_nag][cfn_nag] | `cfn_nag` | @@ -525,6 +526,7 @@ busted tests/ [verilator]: https://verilator.org/guide/latest/ [actionlint]: https://github.com/rhysd/actionlint [buf_lint]: https://github.com/bufbuild/buf +[buf_api_linter]: https://github.com/googleapis/api-linter [erb-lint]: https://github.com/shopify/erb-lint [tfsec]: https://github.com/aquasecurity/tfsec [tlint]: https://github.com/tighten/tlint From b3223fe7d94c21aafd1cbd057ec1e3bd5aa958b6 Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Thu, 3 Oct 2024 23:09:10 +0200 Subject: [PATCH 4/6] fix: replace vim.fs.find with custom function --- lua/lint/linters/buf_api_linter.lua | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/lua/lint/linters/buf_api_linter.lua b/lua/lint/linters/buf_api_linter.lua index 9234482b..569fc957 100644 --- a/lua/lint/linters/buf_api_linter.lua +++ b/lua/lint/linters/buf_api_linter.lua @@ -10,16 +10,26 @@ local function descriptor_set_in() error("buf CLI not found") end - -- search for the buf config, searching upwards from the current buffer's directory until $HOME. - local buffer_parent_dir = vim.fn.expand("%:p:h") -- the path to the folder of the opened .proto file. - local buf_config_filepaths = vim.fs.find( - { "buf.yaml", "buf.yml" }, - { path = buffer_parent_dir, upward = true, stop = vim.fs.normalize("~"), type = "file", limit = 1 } - ) - if #buf_config_filepaths == 0 then + -- Custom function to find file upwards + local function find_file_upwards(filename, start_dir) + local current_dir = start_dir + while current_dir ~= "/" do + local file_path = current_dir .. "/" .. filename + if vim.fn.filereadable(file_path) == 1 then + return file_path + end + current_dir = vim.fn.fnamemodify(current_dir, ":h") + end + return nil + end + + local buffer_parent_dir = vim.fn.expand("%:p:h") + local buf_config_filepath = find_file_upwards("buf.yaml", buffer_parent_dir) + or find_file_upwards("buf.yml", buffer_parent_dir) + + if not buf_config_filepath then error("Buf config file not found") end - local buf_config_filepath = buf_config_filepaths[1] -- build the descriptor file. local buf_config_folderpath = vim.fn.fnamemodify(buf_config_filepath, ":h") From e5101d70b557793dc53d7d18e89e487be984b0a0 Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Thu, 3 Oct 2024 23:12:46 +0200 Subject: [PATCH 5/6] fix: replace vim.system with vim.fn.system --- lua/lint/linters/buf_api_linter.lua | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/lua/lint/linters/buf_api_linter.lua b/lua/lint/linters/buf_api_linter.lua index 569fc957..3e78934c 100644 --- a/lua/lint/linters/buf_api_linter.lua +++ b/lua/lint/linters/buf_api_linter.lua @@ -33,11 +33,16 @@ local function descriptor_set_in() -- build the descriptor file. local buf_config_folderpath = vim.fn.fnamemodify(buf_config_filepath, ":h") - local buf_cmd = { "buf", "build", "-o", descriptor_filepath } - local buf_cmd_opts = { cwd = buf_config_folderpath } - local obj = vim.system(buf_cmd, buf_cmd_opts):wait() - if obj.code ~= 0 then - error("Command failed: " .. vim.inspect(buf_cmd) .. "\n" .. obj.stderr) + local buf_cmd = string.format( + "cd %s && buf build -o %s", + vim.fn.shellescape(buf_config_folderpath), + vim.fn.shellescape(descriptor_filepath) + ) + local output = vim.fn.system(buf_cmd) + local exit_code = vim.v.shell_error + + if exit_code ~= 0 then + error("Command failed: " .. buf_cmd .. "\n" .. output) end -- return the argument to be passed to the linter. From 4bf509458832ee7838d6f32741ff506c956e18ed Mon Sep 17 00:00:00 2001 From: Fredrik Averpil Date: Fri, 8 Nov 2024 08:42:04 +0100 Subject: [PATCH 6/6] refactor: break apart into two linters --- README.md | 6 +- lua/lint/linters/api_linter.lua | 41 ++++++++ lua/lint/linters/api_linter_buf.lua | 153 ++++++++++++++++++++++++++++ lua/lint/linters/buf_api_linter.lua | 92 ----------------- 4 files changed, 198 insertions(+), 94 deletions(-) create mode 100644 lua/lint/linters/api_linter.lua create mode 100644 lua/lint/linters/api_linter_buf.lua delete mode 100644 lua/lint/linters/buf_api_linter.lua diff --git a/README.md b/README.md index 2fe2481e..826476f6 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,8 @@ Other dedicated linters that are built-in are: | [biomejs][biomejs] | `biomejs` | | [blocklint][blocklint] | `blocklint` | | [buf_lint][buf_lint] | `buf_lint` | -| [buf_api_linter][buf_api_linter] | `buf_api_linter` | +| [api_linter][api_linter] | `api_linter` | +| [api_linter_buf][api_linter_buf] | `api_linter_buf` | | [buildifier][buildifier] | `buildifier` | | [cfn-lint][cfn-lint] | `cfn_lint` | | [cfn_nag][cfn_nag] | `cfn_nag` | @@ -526,7 +527,8 @@ busted tests/ [verilator]: https://verilator.org/guide/latest/ [actionlint]: https://github.com/rhysd/actionlint [buf_lint]: https://github.com/bufbuild/buf -[buf_api_linter]: https://github.com/googleapis/api-linter +[api_linter]: https://github.com/googleapis/api-linter +[api_linter_buf]: https://github.com/googleapis/api-linter [erb-lint]: https://github.com/shopify/erb-lint [tfsec]: https://github.com/aquasecurity/tfsec [tlint]: https://github.com/tighten/tlint diff --git a/lua/lint/linters/api_linter.lua b/lua/lint/linters/api_linter.lua new file mode 100644 index 00000000..76b835ec --- /dev/null +++ b/lua/lint/linters/api_linter.lua @@ -0,0 +1,41 @@ +-- NOTE: see require("lint.linters.api_linter_buf") for a real-world implementation of api-linter, leveraging the buf CLI. + +return { + cmd = "api-linter", + stdin = false, + append_fname = true, + args = { + "--output-format=json", + "--disable-rule=core::0191::java-multiple-files", + "--disable-rule=core::0191::java-package", + "--disable-rule=core::0191::java-outer-classname", + }, + stream = "stdout", + ignore_exitcode = true, + env = nil, + parser = function(output) + if output == "" then + return {} + end + local json_output = vim.json.decode(output) + local diagnostics = {} + if json_output == nil then + return diagnostics + end + for _, item in ipairs(json_output) do + for _, problem in ipairs(item.problems) do + table.insert(diagnostics, { + message = problem.message, + file = item.file, + code = problem.rule_id .. " " .. problem.rule_doc_uri, + severity = vim.diagnostic.severity.WARN, + lnum = problem.location.start_position.line_number - 1, + col = problem.location.start_position.column_number - 1, + end_lnum = problem.location.end_position.line_number - 1, + end_col = problem.location.end_position.column_number - 1, + }) + end + end + return diagnostics + end, +} diff --git a/lua/lint/linters/api_linter_buf.lua b/lua/lint/linters/api_linter_buf.lua new file mode 100644 index 00000000..fd63c263 --- /dev/null +++ b/lua/lint/linters/api_linter_buf.lua @@ -0,0 +1,153 @@ +--- Extension of api-linter leveraging the buf CLI. +--- +--- This is a real-world implementation of api-linter, extending the nvim-lint +--- api_linter.lua linter with additional capabilities, so to be able to follow all +--- proto imports. You can read more about the history of how this api_linter_buf +--- came to be here: https://github.com/mfussenegger/nvim-lint/pull/665 +--- +--- The api-linter needs a descriptor file, containing all protos in the project as +--- well as third-party proto imports. +--- This adaptation of the api_linter.lua, will build the descriptor file for you, +--- using the buf CLI, as this is the most common way to go about this problem as +--- of writing this. Please see the custom argument function `descriptor_set_in` in +--- this file for more details on how it's done. +--- +--- The api-linter needs to be run in the same directory as the buf.yaml. Therefore, +--- the 'cwd' must be set. Unfortunately, you cannot provide the cwd as a function +--- to a linter, which means you will either have to statically set the path or have +--- a function run (which will likely run on nvim-lint load in your Neovim setup, +--- which is undesirable). To work around this, you can instead load api_linter_buf +--- with an autocmd, in which you provide a function for the cwd. +--- I have bundled an example of this in the api_linter_buf `register_autocmd` +--- function, which you can use to register the autocmd: +--- `require("lint.linters.api_linter_buf").register_autocmd()` +--- Please note that this replaces other ways to load this api_linter_buf linter. + +local api_linter = require("lint.linters.api_linter") + +-- --------------------- api_linter_buf helper functions --------------------- + +--- Cached filepath to buf.yaml, so to avoid searching multiple times for it. +local cached_buf_config_filepath = nil + +--- Find a file by searching upwards through parent directories. +local function find_file_upwards(names, start_path, stop_path) + -- Normalize paths + start_path = vim.fn.fnamemodify(start_path, ":p") + stop_path = vim.fn.fnamemodify(stop_path, ":p") + + local current_dir = start_path + while current_dir >= stop_path do + for _, name in ipairs(names) do + local file_path = current_dir .. "/" .. name + if vim.fn.filereadable(file_path) == 1 then + return file_path + end + end + -- Go up one directory + local parent_dir = vim.fn.fnamemodify(current_dir, ":h") + if parent_dir == current_dir then + break + end + current_dir = parent_dir + end + return nil +end + +-- --------------------- Descriptor file helper functions --------------------- + +local descriptor_filepath = os.tmpname() + +--- Function to set the `--descriptor-set-in` argument. +--- This requires the buf CLI, which will first build the descriptor file. +local function descriptor_set_in() + if vim.fn.executable("buf") == 0 then + error("buf CLI not found") + end + + local buffer_parent_dir = vim.fn.expand("%:p:h") + local buf_config_filepath = find_file_upwards({ "buf.yaml", "buf.yml" }, buffer_parent_dir, vim.fn.expand("~")) + or find_file_upwards("buf.yml", buffer_parent_dir) + + if not buf_config_filepath then + error("Buf config file not found") + end + + -- build the descriptor file. + local buf_config_folderpath = vim.fn.fnamemodify(buf_config_filepath, ":h") + local buf_cmd = string.format( + "cd %s && buf build -o %s", + vim.fn.shellescape(buf_config_folderpath), + vim.fn.shellescape(descriptor_filepath) + ) + local output = vim.fn.system(buf_cmd) + local exit_code = vim.v.shell_error + + if exit_code ~= 0 then + error("Command failed: " .. buf_cmd .. "\n" .. output) + end + + -- return the argument to be passed to the linter. + return "--descriptor-set-in=" .. descriptor_filepath +end + +local cleanup_descriptor = function() + os.remove(descriptor_filepath) +end + +-- --------------------- Autocmd example --------------------- + +--- Autocmd which will load the linter, due to no cwd-as-function support. +--- HACK: more info: https://github.com/mfussenegger/nvim-lint/pull/674 +local function autocmd(cwd) + --- Find buf.yaml. + local function get_buf_config_filepath() + if cached_buf_config_filepath ~= nil then + return cached_buf_config_filepath + end + local buffer_parent_dir = vim.fn.expand("%:p:h") + local buf_config_filepath = find_file_upwards({ "buf.yaml", "buf.yml" }, buffer_parent_dir, vim.fn.expand("~")) + if not buf_config_filepath then + error("Buf config file not found") + end + cached_buf_config_filepath = buf_config_filepath + vim.notify("buf config file found: " .. cached_buf_config_filepath) + return cached_buf_config_filepath + end + + local function buf_lint_cwd() + return vim.fn.fnamemodify(get_buf_config_filepath(), ":h") + end + + if cwd == nil then + cwd = buf_lint_cwd + end + + vim.api.nvim_create_autocmd({ "BufEnter", "BufWritePost" }, { + pattern = { "*.proto" }, + callback = function() + require("lint").try_lint("api_linter", { + cwd = cwd(), + }) + end, + }) +end + +-- --------------------- api_linter_buf configuration --------------------- + +return { + cmd = api_linter.cmd, + stdin = api_linter.stdin, + append_fname = api_linter.append_fname, + args = table.insert(api_linter.args, descriptor_set_in), + stream = api_linter.stream, + ignore_exitcode = api_linter.ignore_exitcode, + env = api_linter.env, + parser = function(output) + local diagnostics = api_linter.parser(output) + cleanup_descriptor() + return diagnostics + end, + + register_autocmd = autocmd, +} diff --git a/lua/lint/linters/buf_api_linter.lua b/lua/lint/linters/buf_api_linter.lua deleted file mode 100644 index 3e78934c..00000000 --- a/lua/lint/linters/buf_api_linter.lua +++ /dev/null @@ -1,92 +0,0 @@ -local descriptor_filepath = os.tmpname() -local cleanup_descriptor = function() - os.remove(descriptor_filepath) -end - ---- Function to set the `--descriptor-set-in` argument. ---- This requires the buf CLI, which will first build the descriptor file. -local function descriptor_set_in() - if vim.fn.executable("buf") == 0 then - error("buf CLI not found") - end - - -- Custom function to find file upwards - local function find_file_upwards(filename, start_dir) - local current_dir = start_dir - while current_dir ~= "/" do - local file_path = current_dir .. "/" .. filename - if vim.fn.filereadable(file_path) == 1 then - return file_path - end - current_dir = vim.fn.fnamemodify(current_dir, ":h") - end - return nil - end - - local buffer_parent_dir = vim.fn.expand("%:p:h") - local buf_config_filepath = find_file_upwards("buf.yaml", buffer_parent_dir) - or find_file_upwards("buf.yml", buffer_parent_dir) - - if not buf_config_filepath then - error("Buf config file not found") - end - - -- build the descriptor file. - local buf_config_folderpath = vim.fn.fnamemodify(buf_config_filepath, ":h") - local buf_cmd = string.format( - "cd %s && buf build -o %s", - vim.fn.shellescape(buf_config_folderpath), - vim.fn.shellescape(descriptor_filepath) - ) - local output = vim.fn.system(buf_cmd) - local exit_code = vim.v.shell_error - - if exit_code ~= 0 then - error("Command failed: " .. buf_cmd .. "\n" .. output) - end - - -- return the argument to be passed to the linter. - return "--descriptor-set-in=" .. descriptor_filepath -end - -return { - cmd = "api-linter", - stdin = false, - append_fname = true, - args = { - "--output-format=json", - "--disable-rule=core::0191::java-multiple-files", - "--disable-rule=core::0191::java-package", - "--disable-rule=core::0191::java-outer-classname", - descriptor_set_in, - }, - stream = "stdout", - ignore_exitcode = true, - env = nil, - parser = function(output) - if output == "" then - return {} - end - local json_output = vim.json.decode(output) - local diagnostics = {} - if json_output == nil then - return diagnostics - end - for _, item in ipairs(json_output) do - for _, problem in ipairs(item.problems) do - table.insert(diagnostics, { - message = problem.message, - file = item.file, - code = problem.rule_id .. " " .. problem.rule_doc_uri, - severity = vim.diagnostic.severity.WARN, - lnum = problem.location.start_position.line_number - 1, - col = problem.location.start_position.column_number - 1, - end_lnum = problem.location.end_position.line_number - 1, - end_col = problem.location.end_position.column_number - 1, - }) - end - end - cleanup_descriptor() - return diagnostics - end, -}