Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add api-linter (powered by buf CLI) #665

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ Other dedicated linters that are built-in are:
| [biomejs][biomejs] | `biomejs` |
| [blocklint][blocklint] | `blocklint` |
| [buf_lint][buf_lint] | `buf_lint` |
| [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` |
Expand Down Expand Up @@ -525,6 +527,8 @@ busted tests/
[verilator]: https://verilator.org/guide/latest/
[actionlint]: https://github.com/rhysd/actionlint
[buf_lint]: https://github.com/bufbuild/buf
[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
Expand Down
41 changes: 41 additions & 0 deletions lua/lint/linters/api_linter.lua
Original file line number Diff line number Diff line change
@@ -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,
}
153 changes: 153 additions & 0 deletions lua/lint/linters/api_linter_buf.lua
Original file line number Diff line number Diff line change
@@ -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,
}
Loading