pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
201 lines (200 loc) • 10.4 kB
JavaScript
/**
* Auxiliary-diagnostic-LSP capability.
*
* Some LSP servers aren't a file's *language* server — they're cross-cutting,
* diagnostic-only scanners that attach across many languages and run ALONGSIDE
* the primary (security, spelling, secrets, …). Running them as warm LSP servers
* compiles their rules/dictionaries once per session instead of paying a
* cold-start on every file (see #111 — Opengrep's ~8s CLI-per-file → ~1.3s warm).
*
* This module is the registry that maps such a server to:
* - its enablement gate (default-on with an optional kill-switch flag), and
* - how to turn its raw LSP diagnostics into pi-lens diagnostics (tool name +
* semantic policy + defect class), since the LSP `source` differs from our
* tool id and most auxiliaries should be advisory, not blocking.
*
* Adding a new cross-cutting tool = register an `LSPServerInfo` with
* `role:"auxiliary"` (clients/lsp/server.ts) + one profile entry here.
*/
import { shouldDegradeAuxiliaryLsp } from "../lsp-budget.js";
import { findLocalOpengrepConfig } from "../opengrep-config.js";
import { findLocalTyposConfig } from "../typos-config.js";
import { findLocalZizmorConfig } from "../zizmor-config.js";
import { classifyDefect } from "./diagnostic-taxonomy.js";
/**
* Semgrep/opengrep `# nosemgrep` / `# nosemgrep: <rule-id>[,<rule-id>]` inline
* suppression (#441). A bare `# nosemgrep` drops every finding on its line; the
* `: <ids>` form drops only the listed rule ids. `d.code` is the semgrep rule id.
* Also accepts the `//` comment form.
*
* Matches Semgrep placement: honored on the finding's OWN line (inline or not),
* and on the line ABOVE only when that line is a STANDALONE comment (no code before
* it) — so `a() # nosemgrep` suppresses a finding on `a()` but not the next line.
*/
const NOSEMGREP_RE = /(?:#|\/\/)\s*nosemgrep(?::\s*(.+))?/i;
const NOSEMGREP_STANDALONE_RE = /^\s*(?:#|\/\/)\s*nosemgrep(?::\s*(.+))?\s*$/i;
export function isNosemgrepSuppressed(d, content) {
const startLine = d.range?.start?.line; // 0-based
if (startLine == null)
return false;
const lines = content.split("\n");
const ruleId = String(d.code ?? "");
const checkLine = (text, standaloneOnly) => {
if (!text)
return false;
const m = (standaloneOnly ? NOSEMGREP_STANDALONE_RE : NOSEMGREP_RE).exec(text);
if (!m)
return false;
if (m[1] === undefined)
return true; // bare nosemgrep → suppress the line
return m[1]
.split(",")
.map((s) => s.trim())
.filter(Boolean)
.includes(ruleId);
};
// The finding's own line (inline OK), then the line above (standalone comment only).
return (checkLine(lines[startLine], false) ||
checkLine(lines[startLine - 1], true));
}
// #277 R7: every profile below used this exact same rule (ERROR-severity
// findings block only when the workspace opted into curated/authored rules;
// everything else stays advisory) — shared here instead of copy-pasted per
// profile so a future policy change (e.g. a WARNING-blocking tier) is one edit.
const blockOnErrorWhenAllowed = (d, { blockingAllowed }) => (blockingAllowed && d.severity === 1 ? "blocking" : "warning");
export const AUXILIARY_LSP_PROFILES = [
{
serverId: "opengrep",
tool: "opengrep",
// Opengrep is a Semgrep fork and tags LSP diagnostics `source: "Semgrep"`.
sourceMatch: /opengrep|semgrep/i,
killSwitchFlag: "no-opengrep",
enabledByDefault: true,
// The LSP diagnostic carries severity + rule id but NOT confidence (the
// CLI's metadata.confidence is stripped). Opengrep's login-free `auto`
// Community set is uniformly ERROR/LOW-confidence audit-tier, so blocking on
// it would be a firehose. We honor ERROR→blocking ONLY when the repo
// supplies its own curated rules (the author's deliberate severity); the
// auto set is advisory. Either way, findings surface via lens_diagnostics.
allowBlocking: (cwd) => Boolean(findLocalOpengrepConfig(cwd)),
semantic: blockOnErrorWhenAllowed,
defectClass: (d) => classifyDefect(String(d.code ?? ""), "opengrep", d.message ?? ""),
// Honor the canonical Semgrep suppression the user already knows (#441).
isSuppressed: isNosemgrepSuppressed,
},
{
serverId: "ast-grep",
tool: "ast-grep",
// ast-grep tags its LSP diagnostics `source: "ast-grep"`.
sourceMatch: /ast[-_]?grep/i,
killSwitchFlag: "no-ast-grep",
enabledByDefault: true,
// The ast-grep LSP runs either the repo's own sgconfig (when present) or
// pi-lens's shipped baseline sgconfig. In both cases the rule severity is
// deliberate, so preserve ast-grep's severity semantics: ERROR can block,
// WARNING/INFO stay advisory.
allowBlocking: () => true,
semantic: blockOnErrorWhenAllowed,
defectClass: (d) => classifyDefect(String(d.code ?? ""), "ast-grep", d.message ?? ""),
},
{
serverId: "zizmor",
tool: "zizmor",
// zizmor tags its LSP diagnostics `source: "zizmor"`.
sourceMatch: /zizmor/i,
killSwitchFlag: "no-zizmor",
enabledByDefault: true,
// zizmor's default ("regular") persona is a curated, low-false-positive
// audit set, but as an always-on advisory we only let it BLOCK when the repo
// opts in with its own `zizmor.yml` (the author's deliberate severities /
// ignores). Advisory otherwise — findings still surface via lens_diagnostics.
// zizmor maps High→ERROR(1), Medium/Low→WARNING(2), Informational→INFO(3).
allowBlocking: (cwd) => Boolean(findLocalZizmorConfig(cwd)),
semantic: blockOnErrorWhenAllowed,
defectClass: (d) => classifyDefect(String(d.code ?? ""), "zizmor", d.message ?? ""),
},
{
serverId: "typos",
tool: "typos",
// typos-lsp tags its LSP diagnostics `source: "typos"`.
sourceMatch: /typos/i,
killSwitchFlag: "no-typos",
enabledByDefault: true,
// typos is allow-list based (only KNOWN misspellings with a known
// correction), but as an always-on advisory we only let it BLOCK when the
// repo opts in with its own `typos.toml`/`_typos.toml`/`.typos.toml` (the
// team's curated dictionary + chosen severity). Advisory otherwise —
// findings still surface via lens_diagnostics. Note typos-lsp's default
// severity is WARNING, so even with a config it stays advisory unless the
// repo raises `diagnostic-severity` to Error.
allowBlocking: (cwd) => Boolean(findLocalTyposConfig(cwd)),
semantic: blockOnErrorWhenAllowed,
// A misspelling is a documentation/quality defect — not security or
// correctness. "style" is the closest taxonomy class.
defectClass: () => "style",
},
];
/** The auxiliary server ids enabled for this turn (the lsp runner passes these
* to `touchFile` since it — not the LSP service — owns flag access).
*
* #449 slice 2 (prototype): when this process decided at `session_start` that
* the machine-wide LSP budget is exceeded (`clients/lsp-budget.ts`), auxiliary
* servers are skipped entirely for the rest of the session — the primary
* language server per file is unaffected. This is a per-SESSION degrade, not
* per-file: once over budget, this session never spawns its auxiliary fleet,
* rather than flip-flopping file to file. */
export function enabledAuxiliaryLspServerIds(getFlag) {
if (shouldDegradeAuxiliaryLsp())
return [];
return AUXILIARY_LSP_PROFILES.flatMap((p) => p.enabledByDefault &&
!(p.killSwitchFlag && getFlag(p.killSwitchFlag) === true)
? [p.serverId]
: []);
}
// #277 R7: `findAuxiliaryProfileForSource` is called once per diagnostic, and a
// file's diagnostics are typically dominated by a handful of distinct `source`
// strings (one per tool that fired). Memoizing by exact `source` turns an
// O(profiles) regex scan per diagnostic into one scan per distinct source seen
// — safe because `AUXILIARY_LSP_PROFILES` is a fixed module-level const, never
// mutated at runtime, so a source's matching profile never changes.
const profileForSourceCache = new Map();
/** Find the profile whose server emitted a diagnostic with this `source`. */
export function findAuxiliaryProfileForSource(source) {
if (!source)
return undefined;
const cached = profileForSourceCache.get(source);
if (cached !== undefined || profileForSourceCache.has(source))
return cached;
const found = AUXILIARY_LSP_PROFILES.find((p) => p.sourceMatch.test(source));
profileForSourceCache.set(source, found);
return found;
}
/**
* Single-diagnostic suppression check (#586): look up the diagnostic's
* auxiliary profile by `source` and, if that profile declares an
* `isSuppressed` callback (currently only opengrep's `# nosemgrep`, #441),
* apply it. Returns false for diagnostics with no matching profile or whose
* profile has no native suppression syntax — the common case for plain
* language-server diagnostics.
*
* This is the ONE lookup+apply implementation; every call site that decides
* whether to drop a diagnostic for its tool's own inline suppression comment
* should go through this (or `applyAuxiliarySuppressions` below) rather than
* re-deriving the profile lookup.
*/
export function isAuxiliaryDiagnosticSuppressed(d, content) {
const profile = findAuxiliaryProfileForSource(d.source);
return Boolean(profile?.isSuppressed?.(d, content));
}
/**
* Filter a diagnostic list down to the ones NOT suppressed by their
* auxiliary profile's native inline-comment syntax (#586). This is the
* shared helper `tools/lsp-diagnostics.ts` and `clients/lsp/index.ts`'s
* `runWorkspaceDiagnostics` use so a `// nosemgrep` (or any future profile's
* equivalent) suppresses a finding identically whether it's seen via the
* per-edit dispatch runner or a standalone diagnostics query — previously
* only the former honored it (#586).
*/
export function applyAuxiliarySuppressions(diagnostics, content) {
return diagnostics.filter((d) => !isAuxiliaryDiagnosticSuppressed(d, content));
}