diff --git a/README.md b/README.md index e2c3571..248d261 100644 --- a/README.md +++ b/README.md @@ -48,11 +48,13 @@ Run `:checkhealth coderabbit` to verify everything is wired up. | `:CodeRabbitReview [type]` | Run a review. Defaults to `all`, or pass `committed`/`uncommitted` | | `:CodeRabbitStop` | Cancel a running review | | `:CodeRabbitClear` | Clear diagnostics | -| `:CodeRabbitShow [id]` | View results (float or buffer). Defaults to the latest review | +| `:CodeRabbitShow [id] [severity]` | View results (float or buffer). Defaults to the latest review; severity can be `critical`, `major`, or `minor` | | `:CodeRabbitRestore [id]` | Reapply diagnostics from a saved review. Defaults to the most recent | -| `:CodeRabbitQuickfix [id]` | Populate quickfix list with findings | +| `:CodeRabbitQuickfix [id] [severity]` | Populate quickfix list with findings | | `:CodeRabbitHistory` | Browse past reviews | +Examples: `:CodeRabbitShow critical` shows only critical findings from the latest review, and `:CodeRabbitShow 1 critical` shows only critical findings from saved review `1`. + For your statusline: ```lua @@ -77,6 +79,7 @@ require("coderabbit").setup({ }, diagnostics = { enabled = true, + severity_filter = nil, -- nil/all, "critical", "major", "minor", or a list severity_map = { critical = vim.diagnostic.severity.ERROR, major = vim.diagnostic.severity.WARN, @@ -88,6 +91,7 @@ require("coderabbit").setup({ }, show = { layout = "float", -- "float" or "buffer" + severity_filter = nil, float = { width = 0.6, height = 0.7, @@ -96,6 +100,7 @@ require("coderabbit").setup({ }, quickfix = { auto = false, -- populate on review complete + severity_filter = nil, }, history = { max_entries = 50, -- keep the most recent saved reviews per repo diff --git a/doc/coderabbit.txt b/doc/coderabbit.txt index bbdec5c..42ba376 100644 --- a/doc/coderabbit.txt +++ b/doc/coderabbit.txt @@ -45,6 +45,7 @@ All options are optional. Defaults: >lua }, diagnostics = { enabled = true, + severity_filter = nil, severity_map = { critical = vim.diagnostic.severity.ERROR, major = vim.diagnostic.severity.WARN, @@ -56,6 +57,7 @@ All options are optional. Defaults: >lua }, show = { layout = "float", + severity_filter = nil, float = { width = 0.6, height = 0.7, @@ -64,6 +66,7 @@ All options are optional. Defaults: >lua }, quickfix = { auto = false, + severity_filter = nil, }, history = { max_entries = 50, @@ -81,6 +84,10 @@ review.base Base branch for comparison. review.base_commit Base commit SHA for comparison. diagnostics.enabled Populate |vim.diagnostic| with findings. +diagnostics.severity_filter + Limit diagnostics to one or more CodeRabbit severities. + Accepts nil, `"all"`, `"critical"`, `"major"`, + `"minor"`, or a list of those strings. diagnostics.severity_map Map CodeRabbit severities to |vim.diagnostic.severity|. diagnostics.virtual_text Show inline virtual text. diagnostics.signs Show sign column indicators. @@ -89,12 +96,16 @@ diagnostics.underline Underline diagnostic ranges. show.layout `"float"` or `"buffer"`. Default: `"float"`. `"float"` opens a centered floating window. `"buffer"` replaces the current buffer (oil.nvim style). +show.severity_filter Default severity filter for |:CodeRabbitShow|. show.float.width Fraction of editor width (0-1). Default: `0.6`. show.float.height Fraction of editor height (0-1). Default: `0.7`. show.float.border Border style for the floating window. Default: `"rounded"`. quickfix.auto Populate the quickfix list automatically when a review completes. Default: `false`. +quickfix.severity_filter + Default severity filter for |:CodeRabbitQuickfix| and + automatic quickfix population. history.max_entries Maximum saved reviews kept per repo. Default: `50`. Set to `0` to keep all saved reviews. @@ -114,9 +125,14 @@ COMMANDS *coderabbit-commands* :CodeRabbitClear *:CodeRabbitClear* Clear all CodeRabbit diagnostics. -:CodeRabbitShow [id] *:CodeRabbitShow* +:CodeRabbitShow [id] [severity] *:CodeRabbitShow* Open review results. Display mode is controlled by `show.layout`. Pass an `id` from `:CodeRabbitHistory` to view a saved review. + Pass a severity (`critical`, `major`, `minor`) to focus the output. + Examples: > + :CodeRabbitShow critical + :CodeRabbitShow 1 critical +< Press `q` to close. :CodeRabbitRestore [id] *:CodeRabbitRestore* @@ -127,10 +143,11 @@ COMMANDS *coderabbit-commands* :CodeRabbitHistory *:CodeRabbitHistory* Browse saved reviews via |vim.ui.select|. -:CodeRabbitQuickfix [id] *:CodeRabbitQuickfix* +:CodeRabbitQuickfix [id] [severity] *:CodeRabbitQuickfix* Populate the quickfix list with findings. Pass an `id` from `:CodeRabbitHistory` to load a saved review. Without an `id`, uses the current review findings. Navigate with |:cnext| and |:cprev|. + Pass a severity (`critical`, `major`, `minor`) to focus the list. ============================================================================== LUA API *coderabbit-api* @@ -147,8 +164,9 @@ require("coderabbit").stop() *coderabbit.stop()* require("coderabbit").clear() *coderabbit.clear()* Clear diagnostics and reset state. -require("coderabbit").show({id}) *coderabbit.show()* +require("coderabbit").show({id}, {opts}) *coderabbit.show()* Open the review buffer. `nil` = current, number = saved. + Optional opts: `{ severity_filter = "critical" }`. require("coderabbit").restore({id}) *coderabbit.restore()* Reapply diagnostics from a saved review. `nil` = most recent. @@ -156,8 +174,9 @@ require("coderabbit").restore({id}) *coderabbit.restore()* require("coderabbit").history() *coderabbit.history()* Open the review history picker. -require("coderabbit").quickfix({id}) *coderabbit.quickfix()* +require("coderabbit").quickfix({id}, {opts}) *coderabbit.quickfix()* Populate the quickfix list with findings. `nil` = current, number = saved. + Optional opts: `{ severity_filter = "critical" }`. require("coderabbit").status() *coderabbit.status()* Returns `"⠋ CodeRabbit (12s)"` while reviewing, `nil` when idle. diff --git a/lua/coderabbit/config.lua b/lua/coderabbit/config.lua index 355bd9e..d3461b0 100644 --- a/lua/coderabbit/config.lua +++ b/lua/coderabbit/config.lua @@ -13,6 +13,7 @@ M.defaults = { }, diagnostics = { enabled = true, + severity_filter = nil, severity_map = { critical = vim.diagnostic.severity.ERROR, major = vim.diagnostic.severity.WARN, @@ -24,6 +25,7 @@ M.defaults = { }, show = { layout = "float", + severity_filter = nil, float = { width = 0.6, height = 0.7, @@ -32,6 +34,7 @@ M.defaults = { }, quickfix = { auto = false, + severity_filter = nil, }, history = { max_entries = 50, diff --git a/lua/coderabbit/init.lua b/lua/coderabbit/init.lua index 450965b..22ce657 100644 --- a/lua/coderabbit/init.lua +++ b/lua/coderabbit/init.lua @@ -24,16 +24,16 @@ function M.restore(id) require("coderabbit.review").restore(id) end -function M.show(id) - require("coderabbit.show").open(id) +function M.show(id, opts) + require("coderabbit.show").open(id, opts) end function M.history() require("coderabbit.history").open() end -function M.quickfix(id) - require("coderabbit.quickfix").populate(id) +function M.quickfix(id, opts) + require("coderabbit.quickfix").populate(id, opts) end function M.status() diff --git a/lua/coderabbit/quickfix.lua b/lua/coderabbit/quickfix.lua index 9a07776..6a40a2e 100644 --- a/lua/coderabbit/quickfix.lua +++ b/lua/coderabbit/quickfix.lua @@ -1,6 +1,7 @@ local M = {} local utils = require("coderabbit.utils") +local severity_filter = require("coderabbit.severity") local severity_types = { [vim.diagnostic.severity.ERROR] = "E", @@ -16,8 +17,12 @@ end --- Convert findings to quickfix items (pure function, no side effects). --- @param findings table[] Array of { diagnostic, filepath } +--- @param opts table|nil { severity_filter = table } --- @return table[] Array of { filename, lnum, col, text, type } for setqflist() -function M.findings_to_qf_items(findings) +function M.findings_to_qf_items(findings, opts) + opts = opts or {} + findings = severity_filter.filter_findings(findings, opts.severity_filter) + local items = {} for _, f in ipairs(findings) do local d = f.diagnostic @@ -37,12 +42,24 @@ end --- Populate the quickfix list from findings and open the window. --- @param findings table[] Array of { diagnostic, filepath } ---- @param opts table|nil { title = string } +--- @param opts table|nil { title = string, severity_filter = table|string|string[] } function M.set(findings, opts) opts = opts or {} - local items = M.findings_to_qf_items(findings) + local filter, err = severity_filter.parse_filter(opts.severity_filter) + if err then + utils.notify("Invalid severity filter: " .. err, vim.log.levels.ERROR) + return + end + + local title = opts.title or "CodeRabbit Review" + local filter_label = severity_filter.describe(filter) + if filter_label then + title = title .. " (" .. filter_label .. ")" + end + + local items = M.findings_to_qf_items(findings, { severity_filter = filter }) vim.fn.setqflist({}, "r", { - title = opts.title or "CodeRabbit Review", + title = title, items = items, }) vim.cmd("copen") @@ -50,7 +67,9 @@ end --- Populate quickfix from current review or a saved review by ID. --- @param id number|nil Review ID (nil = current in-memory findings) -function M.populate(id) +--- @param opts table|nil { severity_filter = table|string|string[] } +function M.populate(id, opts) + opts = opts or {} local findings, title local review = require("coderabbit.review") @@ -72,7 +91,14 @@ function M.populate(id) title = "CodeRabbit Review" end - M.set(findings, { title = title }) + local filter_arg + if opts.severity_filter ~= nil then + filter_arg = opts.severity_filter + else + filter_arg = require("coderabbit.config").get().quickfix.severity_filter + end + + M.set(findings, { title = title, severity_filter = filter_arg }) end return M diff --git a/lua/coderabbit/review.lua b/lua/coderabbit/review.lua index 6631dbf..313509b 100644 --- a/lua/coderabbit/review.lua +++ b/lua/coderabbit/review.lua @@ -5,6 +5,7 @@ local cli = require("coderabbit.cli") local parser = require("coderabbit.parser") local diagnostics = require("coderabbit.diagnostics") local utils = require("coderabbit.utils") +local severity = require("coderabbit.severity") local spinner_frames = { "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" } local FRAME_MS = 80 @@ -114,6 +115,11 @@ function M.run(opts) state.findings = {} state.cwd = vim.fn.getcwd() local cfg = config.get() + local diagnostics_filter, diagnostics_filter_err = severity.parse_filter(cfg.diagnostics.severity_filter) + if diagnostics_filter_err then + utils.notify("Invalid diagnostics severity filter: " .. diagnostics_filter_err, vim.log.levels.ERROR) + return + end local finding_count = 0 local got_error = false @@ -152,8 +158,9 @@ function M.run(opts) finding_count = finding_count + 1 local diag, filepath = parser.finding_to_diagnostic(event, state.cwd, cfg.diagnostics.severity_map) if diag then - table.insert(state.findings, { diagnostic = diag, filepath = filepath }) - if cfg.diagnostics.enabled then + local finding = { diagnostic = diag, filepath = filepath } + table.insert(state.findings, finding) + if cfg.diagnostics.enabled and severity.matches(finding, diagnostics_filter) then diagnostics.set(filepath, { diag }) end end @@ -211,6 +218,7 @@ function M.run(opts) if type(cfg.quickfix) == "table" and cfg.quickfix.auto and #state.findings > 0 then require("coderabbit.quickfix").set(state.findings, { title = "CodeRabbit Review", + severity_filter = cfg.quickfix.severity_filter, }) end @@ -249,14 +257,25 @@ function M.restore(id) return end + local cfg = config.get() + local diagnostics_filter, diagnostics_filter_err = severity.parse_filter(cfg.diagnostics.severity_filter) + if diagnostics_filter_err then + utils.notify("Invalid diagnostics severity filter: " .. diagnostics_filter_err, vim.log.levels.ERROR) + return + end + diagnostics.clear() local findings = type(review.findings) == "table" and review.findings or {} vim.schedule(function() + local restored = 0 for _, finding in ipairs(findings) do - diagnostics.set(finding.filepath, { finding.diagnostic }) + if severity.matches(finding, diagnostics_filter) then + diagnostics.set(finding.filepath, { finding.diagnostic }) + restored = restored + 1 + end end - utils.notify(string.format("Restored %d findings from review #%d", #findings, id)) + utils.notify(string.format("Restored %d findings from review #%d", restored, id)) end) end diff --git a/lua/coderabbit/severity.lua b/lua/coderabbit/severity.lua new file mode 100644 index 0000000..29c5d77 --- /dev/null +++ b/lua/coderabbit/severity.lua @@ -0,0 +1,153 @@ +local M = {} + +M.names = { "critical", "major", "minor" } + +local valid = { + critical = true, + major = true, + minor = true, +} + +local aliases = { + error = "critical", + err = "critical", + warn = "major", + warning = "major", + info = "minor", + information = "minor", +} + +local diagnostic_to_raw = { + [vim.diagnostic.severity.ERROR] = "critical", + [vim.diagnostic.severity.WARN] = "major", + [vim.diagnostic.severity.INFO] = "minor", + [vim.diagnostic.severity.HINT] = "minor", +} + +--- Normalize a user-facing severity name. +--- @param value string +--- @return string|nil +function M.normalize(value) + if type(value) ~= "string" then + return nil + end + value = value:lower() + return valid[value] and value or aliases[value] +end + +local function append_filter_values(values, out) + for _, value in ipairs(values) do + if type(value) ~= "string" then + return "severity filters must be strings" + end + for part in value:gmatch("[^,%s]+") do + local lower = part:lower() + if lower == "all" or lower == "*" then + return nil, true + end + local normalized = M.normalize(lower) + if not normalized then + return "invalid severity: " .. part + end + out[normalized] = true + end + end +end + +--- Parse a severity filter from nil, string, or string array. +--- Returns nil for no filter / "all". +--- @param value string|string[]|table|nil +--- @return table|nil, string|nil +function M.parse_filter(value) + if value == nil or value == false or value == "" then + return nil + end + + local filter = {} + local err, all + if type(value) == "string" then + err, all = append_filter_values({ value }, filter) + elseif type(value) == "table" then + local has_named_keys = false + for _, name in ipairs(M.names) do + if value[name] ~= nil then + has_named_keys = true + if value[name] then + filter[name] = true + end + end + end + if not has_named_keys then + err, all = append_filter_values(value, filter) + end + else + return nil, "severity filter must be a string or list" + end + + if err then + return nil, err + end + if all or not next(filter) then + return nil + end + return filter +end + +--- Human-readable filter label, sorted by CodeRabbit severity order. +--- @param filter table|nil +--- @return string|nil +function M.describe(filter) + if not filter then + return nil + end + local parts = {} + for _, name in ipairs(M.names) do + if filter[name] then + table.insert(parts, name) + end + end + return #parts > 0 and table.concat(parts, ", ") or nil +end + +local function raw_severity(finding) + local diagnostic = finding and finding.diagnostic + local user_data = diagnostic and diagnostic.user_data + local raw = user_data and user_data.severity_raw + local normalized = M.normalize(raw) + if normalized then + return normalized + end + return diagnostic and diagnostic_to_raw[diagnostic.severity] or nil +end + +--- Check whether a finding matches a parsed severity filter. +--- @param finding table +--- @param filter table|nil +--- @return boolean +function M.matches(finding, filter) + if not filter then + return true + end + local raw = raw_severity(finding) + return raw ~= nil and filter[raw] == true +end + +--- Return findings matching a parsed severity filter. +--- @param findings table[] +--- @param filter table|nil +--- @return table[] +function M.filter_findings(findings, filter) + if not filter then + return findings + end + + local filtered = {} + for _, finding in ipairs(findings or {}) do + if M.matches(finding, filter) then + table.insert(filtered, finding) + end + end + return filtered +end + +return M diff --git a/lua/coderabbit/show.lua b/lua/coderabbit/show.lua index 3082f21..8bd6115 100644 --- a/lua/coderabbit/show.lua +++ b/lua/coderabbit/show.lua @@ -1,6 +1,7 @@ local M = {} local utils = require("coderabbit.utils") +local severity = require("coderabbit.severity") local buf_id = nil local severity_labels = vim.diagnostic.severity @@ -79,29 +80,41 @@ end --- @return string[] function M.render(findings, context, opts) opts = opts or {} + local total_findings = #findings + local filter = opts.severity_filter + local filter_label = severity.describe(filter) + findings = severity.filter_findings(findings, filter) + local lines = {} table.insert(lines, "# CodeRabbit Review") table.insert(lines, "") - if context then + if context or filter_label then local parts = {} - if context.review_type then + if context and context.review_type then table.insert(parts, "**Type:** " .. context.review_type) end - if context.current_branch then + if context and context.current_branch then table.insert(parts, "**Branch:** " .. context.current_branch) end - if context.base_branch then + if context and context.base_branch then table.insert(parts, "**Base:** " .. context.base_branch) end - if context.base_commit then + if context and context.base_commit then table.insert(parts, "**Commit:** " .. context.base_commit) end + if filter_label then + table.insert(parts, "**Severity:** " .. filter_label) + end if #parts > 0 then table.insert(lines, table.concat(parts, " | ")) end - table.insert(lines, string.format("**Findings:** %d", #findings)) + if filter_label then + table.insert(lines, string.format("**Findings:** %d of %d", #findings, total_findings)) + else + table.insert(lines, string.format("**Findings:** %d", #findings)) + end table.insert(lines, "") end @@ -109,7 +122,11 @@ function M.render(findings, context, opts) table.insert(lines, "") if #findings == 0 then - table.insert(lines, "*No findings -- your code looks good!*") + if filter_label then + table.insert(lines, "*No findings match severity: " .. filter_label .. ".*") + else + table.insert(lines, "*No findings -- your code looks good!*") + end return lines end @@ -168,7 +185,8 @@ function M.render(findings, context, opts) return lines end -function M.open(id) +function M.open(id, opts) + opts = opts or {} local review = require("coderabbit.review") local findings, context, running @@ -192,15 +210,31 @@ function M.open(id) return end - local content = M.render(findings, context, { cwd = context and context.cwd or vim.fn.getcwd() }) + local filter_arg + if opts.severity_filter ~= nil then + filter_arg = opts.severity_filter + else + filter_arg = require("coderabbit.config").get().show.severity_filter + end + local filter, err = severity.parse_filter(filter_arg) + if err then + utils.notify("Invalid severity filter: " .. err, vim.log.levels.ERROR) + return + end + + local content = M.render(findings, context, { + cwd = context and context.cwd or vim.fn.getcwd(), + severity_filter = filter, + }) if running then + local visible_count = #severity.filter_findings(findings, filter) table.insert(content, 1, "") table.insert( content, 1, string.format( "> **Review in progress...** Showing %s so far. Run `:CodeRabbitShow` again to refresh.", - utils.pluralize(#findings, "finding") + utils.pluralize(visible_count, "finding") ) ) end diff --git a/plugin/coderabbit.lua b/plugin/coderabbit.lua index 3b76eaa..0052669 100644 --- a/plugin/coderabbit.lua +++ b/plugin/coderabbit.lua @@ -9,6 +9,69 @@ local function ensure_setup() end end +local severity_names = require("coderabbit.severity").names + +local function parse_id_and_severity(command, args) + local id = nil + local severity = nil + + if args.fargs[1] then + id = tonumber(args.fargs[1]) + if id then + severity = args.fargs[2] + if args.fargs[3] then + vim.notify(command .. ": too many arguments", vim.log.levels.ERROR) + return nil, nil, false + end + else + severity = args.fargs[1] + if args.fargs[2] then + vim.notify(command .. ": invalid review ID: " .. args.fargs[1], vim.log.levels.ERROR) + return nil, nil, false + end + end + end + + if severity then + local _, err = require("coderabbit.severity").parse_filter(severity) + if err then + vim.notify(command .. ": " .. err, vim.log.levels.ERROR) + return nil, nil, false + end + end + + return id, severity, true +end + +local function severity_completion(arglead) + local completions = vim.list_extend(vim.deepcopy(severity_names), { "all" }) + return vim.tbl_filter(function(item) + return item:find(arglead, 1, true) == 1 + end, completions) +end + +local function id_and_severity_completion(arglead, cmdline, cursorpos) + cursorpos = cursorpos or (#cmdline + 1) + local before_cursor = cmdline:sub(1, cursorpos - 1) + local has_trailing_space = before_cursor:match("%s$") ~= nil + local args = vim.split(before_cursor, "%s+", { trimempty = true }) + local first_arg = args[2] + local ids = require("coderabbit.storage").ids() + + if not first_arg or (first_arg and #args == 2 and not has_trailing_space) then + local completions = vim.list_extend(ids, severity_completion(arglead)) + return vim.tbl_filter(function(item) + return item:find(arglead, 1, true) == 1 + end, completions) + end + + if tonumber(first_arg) and (#args <= 3 or has_trailing_space) then + return severity_completion(arglead) + end + + return {} +end + vim.api.nvim_create_user_command("CodeRabbitReview", function(args) ensure_setup() local opts = {} @@ -38,14 +101,15 @@ end, { vim.api.nvim_create_user_command("CodeRabbitShow", function(args) ensure_setup() - local id = args.fargs[1] and tonumber(args.fargs[1]) or nil - require("coderabbit").show(id) + local id, severity, ok = parse_id_and_severity("CodeRabbitShow", args) + if not ok then + return + end + require("coderabbit").show(id, { severity_filter = severity }) end, { - nargs = "?", - complete = function() - return require("coderabbit.storage").ids() - end, - desc = "Show CodeRabbit review results in a buffer (optional: review ID)", + nargs = "*", + complete = id_and_severity_completion, + desc = "Show CodeRabbit review results in a buffer (optional: review ID and severity)", }) vim.api.nvim_create_user_command("CodeRabbitRestore", function(args) @@ -76,19 +140,13 @@ end, { vim.api.nvim_create_user_command("CodeRabbitQuickfix", function(args) ensure_setup() - local id = nil - if args.fargs[1] then - id = tonumber(args.fargs[1]) - if not id then - vim.notify("CodeRabbitQuickfix: invalid review ID: " .. args.fargs[1], vim.log.levels.ERROR) - return - end + local id, severity, ok = parse_id_and_severity("CodeRabbitQuickfix", args) + if not ok then + return end - require("coderabbit").quickfix(id) + require("coderabbit").quickfix(id, { severity_filter = severity }) end, { - nargs = "?", - complete = function() - return require("coderabbit.storage").ids() - end, - desc = "Populate quickfix list with CodeRabbit findings (optional: review ID)", + nargs = "*", + complete = id_and_severity_completion, + desc = "Populate quickfix list with CodeRabbit findings (optional: review ID and severity)", }) diff --git a/tests/coderabbit/quickfix_spec.lua b/tests/coderabbit/quickfix_spec.lua index 7f6ac35..3de8fec 100644 --- a/tests/coderabbit/quickfix_spec.lua +++ b/tests/coderabbit/quickfix_spec.lua @@ -3,6 +3,12 @@ local h = require("tests.helpers") local test, eq = h.test, h.eq local E, W, I = h.E, h.W, h.I +local function severity_f(path, lnum, sev, raw, msg) + local finding = h.finding(path, lnum, sev, msg) + finding.diagnostic.user_data.severity_raw = raw + return finding +end + -- ────────────────────────────────────────────────────────── -- Tests: severity_to_type -- ────────────────────────────────────────────────────────── @@ -100,6 +106,17 @@ test("findings_to_qf_items: multiple findings produce correct count", function() eq(#items, 3) end) +test("findings_to_qf_items: severity filter limits items", function() + local filter = assert(require("coderabbit.severity").parse_filter("critical")) + local findings = { + severity_f("/tmp/repo/a.lua", 1, E, "critical", "one"), + severity_f("/tmp/repo/b.lua", 2, W, "major", "two"), + } + local items = quickfix.findings_to_qf_items(findings, { severity_filter = filter }) + eq(#items, 1) + eq(items[1].filename, "/tmp/repo/a.lua") +end) + -- ────────────────────────────────────────────────────────── -- Tests: populate -- ────────────────────────────────────────────────────────── @@ -180,6 +197,17 @@ test("set: replaces existing quickfix content", function() eq(#qf.items, 1) end) +test("set: severity filter updates title and items", function() + quickfix.set({ + severity_f("/tmp/repo/a.lua", 0, E, "critical", "critical issue"), + severity_f("/tmp/repo/b.lua", 0, W, "major", "major issue"), + }, { title = "Filtered", severity_filter = "critical" }) + vim.cmd("cclose") + local qf = vim.fn.getqflist({ title = 1, items = 1 }) + eq(qf.title, "Filtered (critical)") + eq(#qf.items, 1) +end) + h.summary() -- Clean up temp dir after all tests complete diff --git a/tests/coderabbit/restore_spec.lua b/tests/coderabbit/restore_spec.lua index d748c1c..f55c303 100644 --- a/tests/coderabbit/restore_spec.lua +++ b/tests/coderabbit/restore_spec.lua @@ -1,4 +1,5 @@ local diagnostics = require("coderabbit.diagnostics") +local config = require("coderabbit.config") local review = require("coderabbit.review") local storage = require("coderabbit.storage") local h = require("tests.helpers") @@ -37,6 +38,17 @@ local function make_findings(n) return findings end +local function make_severity_finding(raw, sev, name) + local tmpdir = vim.fn.tempname() + vim.fn.mkdir(tmpdir, "p") + table.insert(finding_tmpdirs, tmpdir) + local filepath = tmpdir .. "/" .. name .. ".ts" + vim.fn.writefile({ "// mock" }, filepath) + local finding = h.finding(filepath, 1, sev, raw .. " finding") + finding.diagnostic.user_data.severity_raw = raw + return finding +end + -- ────────────────────────────────────────────────────────── -- Tests -- ────────────────────────────────────────────────────────── @@ -110,5 +122,33 @@ test("restore: clears previous diagnostics before applying", function() eq(total, 1) end) +test("restore: honors diagnostics severity filter", function() + cleanup() + local findings = { + make_severity_finding("critical", h.E, "critical"), + make_severity_finding("major", h.W, "major"), + } + storage.save(findings, h.context()) + + local prev = config.get().diagnostics.severity_filter + config.get().diagnostics.severity_filter = "critical" + local ok, err = pcall(function() + review.restore(1) + flush() + + local total = 0 + for _, d in ipairs(vim.diagnostic.get()) do + if d.source == "coderabbit" then + total = total + 1 + end + end + eq(total, 1) + end) + config.get().diagnostics.severity_filter = prev + if not ok then + error(err, 2) + end +end) + cleanup() h.summary() diff --git a/tests/coderabbit/severity_spec.lua b/tests/coderabbit/severity_spec.lua new file mode 100644 index 0000000..7b1c5af --- /dev/null +++ b/tests/coderabbit/severity_spec.lua @@ -0,0 +1,72 @@ +local severity = require("coderabbit.severity") +local h = require("tests.helpers") +local test, eq = h.test, h.eq + +local function finding(raw, diag_severity) + return { + filepath = "/tmp/repo/a.lua", + diagnostic = { + lnum = 0, + col = 0, + severity = diag_severity or h.I, + message = raw or "finding", + user_data = raw and { severity_raw = raw } or nil, + }, + } +end + +test("normalize: accepts CodeRabbit severities", function() + eq(severity.normalize("critical"), "critical") + eq(severity.normalize("major"), "major") + eq(severity.normalize("minor"), "minor") +end) + +test("normalize: accepts diagnostic aliases", function() + eq(severity.normalize("error"), "critical") + eq(severity.normalize("warning"), "major") + eq(severity.normalize("info"), "minor") +end) + +test("parse_filter: parses comma-separated severities", function() + local filter, err = severity.parse_filter("critical,major") + eq(err, nil) + eq(filter.critical, true) + eq(filter.major, true) + eq(filter.minor, nil) +end) + +test("parse_filter: all disables filtering", function() + local filter, err = severity.parse_filter("all") + eq(err, nil) + eq(filter, nil) +end) + +test("parse_filter: rejects unknown severity", function() + local filter, err = severity.parse_filter("blocker") + eq(filter, nil) + assert(err:find("invalid severity", 1, true), "expected invalid severity error") +end) + +test("filter_findings: matches raw CodeRabbit severity", function() + local filter = assert(severity.parse_filter("critical")) + local findings = { + finding("critical", h.E), + finding("major", h.W), + } + local filtered = severity.filter_findings(findings, filter) + eq(#filtered, 1) + eq(filtered[1], findings[1]) +end) + +test("filter_findings: falls back to diagnostic severity", function() + local filter = assert(severity.parse_filter("critical")) + local findings = { + finding(nil, h.E), + finding(nil, h.W), + } + local filtered = severity.filter_findings(findings, filter) + eq(#filtered, 1) + eq(filtered[1], findings[1]) +end) + +h.summary() diff --git a/tests/coderabbit/show_spec.lua b/tests/coderabbit/show_spec.lua index cb72451..32e82e8 100644 --- a/tests/coderabbit/show_spec.lua +++ b/tests/coderabbit/show_spec.lua @@ -7,6 +7,12 @@ local function f(path, lnum, sev, msg, suggestions, end_lnum) return h.finding(path, lnum, sev, msg, suggestions, end_lnum) end +local function severity_f(path, lnum, sev, raw, msg) + local finding = f(path, lnum, sev, msg) + finding.diagnostic.user_data.severity_raw = raw + return finding +end + local function render(findings, ctx, cwd) return show.render(findings, ctx, { cwd = cwd or CWD }) end @@ -119,6 +125,28 @@ test("render: multiple files sorted alphabetically", function() eq(files[2], "## z.ts") end) +test("render: severity filter limits findings and shows count", function() + local filter = assert(require("coderabbit.severity").parse_filter("critical")) + local lines = show.render({ + severity_f(CWD .. "/a.ts", 0, E, "critical", "critical issue"), + severity_f(CWD .. "/b.ts", 0, W, "major", "major issue"), + }, { cwd = CWD, review_type = "all" }, { cwd = CWD, severity_filter = filter }) + + assert(has(lines, "**Severity:** critical"), "expected severity metadata") + assert(has(lines, "**Findings:** 1 of 2"), "expected filtered count") + assert(has(lines, "critical issue"), "expected matching finding") + assert(not has(lines, "major issue"), "unexpected filtered finding") +end) + +test("render: severity filter with no matches shows filtered empty state", function() + local filter = assert(require("coderabbit.severity").parse_filter("critical")) + local lines = show.render({ + severity_f(CWD .. "/b.ts", 0, W, "major", "major issue"), + }, nil, { cwd = CWD, severity_filter = filter }) + + assert(has(lines, "No findings match severity: critical"), "expected filtered empty state") +end) + test("render: multiple suggestions each get a code block", function() local lines = render({ f(CWD .. "/app.ts", 5, W, "pick one", { "A", "B" }) }) eq(count(lines, "^```ts"), 2)