From 8c00d0bc3ea33c2014e0af9a15415b5ff6fe119f Mon Sep 17 00:00:00 2001 From: Mathias Fussenegger Date: Fri, 16 Aug 2024 11:55:12 +0200 Subject: [PATCH] Add support for lpeg patterns and match functions to `from_pattern` An alternative and more general approach to https://github.com/mfussenegger/nvim-lint/issues/625 --- README.md | 39 +++++++++++++++++++++++-------- lua/lint/parser.lua | 23 ++++++++++++++++-- tests/parser_spec.lua | 54 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 9bacb587..f24636ad 100644 --- a/README.md +++ b/README.md @@ -277,15 +277,26 @@ The function takes two arguments: `errorformat` and `skeleton` (optional). ### from_pattern +Creates a parser function from a pattern. + ```lua parser = require('lint.parser').from_pattern(pattern, groups, severity_map, defaults, opts) ``` -The function allows to parse the linter's output using a Lua regular expression pattern. +### pattern + +The function allows to parse the linter's output using a pattern which can be either: + +- A Lua pattern. See `:help lua-patterns`. +- A LPEG pattern object. See `:help vim.lpeg`. +- A function (`fun(line: string):string[]`). It takes one parameter - a line + from the linter output and must return a string array with the matches. The + array should be empty if there was no match. + -- pattern: The regular expression pattern applied on each line of the output -- groups: The groups specified by the pattern +### groups +The groups specify the result format of the pattern. Available groups: - `lnum` @@ -305,7 +316,11 @@ local pattern = '[^:]+:(%d+):(%d+):(%w+):(.+)' local groups = { 'lnum', 'col', 'code', 'message' } ``` -- severity: A mapping from severity codes to diagnostic codes +The captures in the pattern correspond to the group at the same position. + +### severity + +A mapping from severity codes to diagnostic codes ``` lua default_severity = { @@ -316,18 +331,22 @@ default_severity = { } ``` -- defaults: The defaults diagnostic values +### defaults + +The defaults diagnostic values ```lua defaults = {["source"] = "mylint-name"} ``` -- opts: Additional options +### opts + +Additional options - - `lnum_offset`: Added to `lnum`. Defaults to 0 - - `end_lnum_offset`: Added to `end_lnum`. Defaults to 0 - - `end_col_offset`: offset added to `end_col`. Defaults to `-1`, assuming - that the end-column position is exclusive. +- `lnum_offset`: Added to `lnum`. Defaults to 0 +- `end_lnum_offset`: Added to `end_lnum`. Defaults to 0 +- `end_col_offset`: offset added to `end_col`. Defaults to `-1`, assuming + that the end-column position is exclusive. ## Customize built-in linters diff --git a/lua/lint/parser.lua b/lua/lint/parser.lua index a918fc64..d9edac6e 100644 --- a/lua/lint/parser.lua +++ b/lua/lint/parser.lua @@ -46,7 +46,7 @@ local normalize = (vim.fs ~= nil and vim.fs.normalize ~= nil) --- Parse a linter's output using a Lua pattern --- ----@param pattern string +---@param pattern string|vim.lpeg.Pattern|fun(line: string):string[] ---@param groups string[] ---@param severity_map? table ---@param defaults? table @@ -55,10 +55,29 @@ function M.from_pattern(pattern, groups, severity_map, defaults, opts) defaults = defaults or {} severity_map = severity_map or {} opts = opts or {} + + local type_ = type(pattern) + local matchline + if type_ == "string" then + matchline = function(line) + return { line:match(pattern) } + end + elseif type_ == "function" then + matchline = pattern + else + matchline = function(line) + return { pattern:match(line) } + end + end + + -- Like vim.diagnostic.match but also checks if a `file` group matches the buffer path -- Some linters produce diagnostics for the full project and this should only produce buffer diagnostics local match = function(linter_cwd, buffer_path, line) - local matches = { line:match(pattern) } + local ok, matches = pcall(matchline, line) + if not ok then + error(string.format("pattern match failed on line: %s with error: %q", line, matches)) + end if not next(matches) then return nil end diff --git a/tests/parser_spec.lua b/tests/parser_spec.lua index b11b7a4c..af88c762 100644 --- a/tests/parser_spec.lua +++ b/tests/parser_spec.lua @@ -73,4 +73,58 @@ bar:209:14 Bigger mistake } assert.are.same(expected, result) end) + + it("supports lpeg pattern", function() + if not vim.re then + return + end + local pattern = vim.re.compile("{[0-9]+} ':' { (.*) }") + local groups = { 'lnum', 'message' } + local parser = require('lint.parser').from_pattern(pattern, groups) + local output = [[ +10:Big mistake +14:Bigger mistake +]] + local result = parser(output, 0) + local expected = { + { + message = 'Big mistake', + lnum = 9, + end_lnum = 9, + col = 0, + end_col = 0, + severity = vim.diagnostic.severity.ERROR, + }, + { + message = 'Bigger mistake', + lnum = 13, + col = 0, + end_lnum = 13, + end_col = 0, + severity = vim.diagnostic.severity.ERROR, + }, + } + assert.are.same(expected, result) + assert.are.same({}, parser("no-match", 0)) + end) + + it("supports match function", function() + local pattern = function(_) + return { 10, "hello" } + end + local groups = { 'lnum', 'message' } + local parser = require('lint.parser').from_pattern(pattern, groups) + local result = parser("foo", 0) + local expected = { + { + message = "hello", + lnum = 9, + col = 0, + end_lnum = 9, + end_col = 0, + severity = vim.diagnostic.severity.ERROR + }, + } + assert.are.same(expected, result) + end) end)