Files
openidea/src/features/code-insight/problemsStore.test.ts
T
l0ng-ai aa5821bb46 fix(spelling): Rename identifiers on Typo fixes, add inspection controls, cut false positives (#51)
Root causes: Typo "Change to" replaced one occurrence even for declared names (breaking references); inspections had no Disable / Edit settings controls anywhere; en_US Hunspell does not derive agent nouns like "checkpointer" and compound identifier words were flagged; local inspections (Typo, file references, SQL) never reached Problems.
Fix (IDEA): "Typo: Rename to…" renames via LSP rename, falling back to syntax-based local rename, never a silent single replace, and lists identifier-safe words only; intention submenu (→) with Edit inspection profile setting / Disable inspection for every inspection; Problems and Inspection Results get Disable Inspection, Change Severity, Edit Inspection Profile Setting; Typo options Process code / literals / comments; -er/-ers derivation and identifier compounds accepted, common misspellings still flagged.
Siblings covered: all LSP and built-in inspections' intentions, file reference and SQL inspections in Problems, severity counter.
Verified: ide-spell tests (issue examples + ~50 real misspellings), vitest (286), in-app on macOS: only 'mesage' flagged, submenu, Rename to → 'message' updated both occurrences.

Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
2026-10-27 10:38:53 +08:00

106 lines
4.4 KiB
TypeScript

import { beforeEach, describe, expect, it } from "vitest";
import type { Diagnostic } from "@/bindings";
import {
applyDiagnosticsEvent,
clearProblems,
countSeverities,
fileDiagnostics,
mergeDiagnostics,
profiledProblems,
projectCounts,
useProblems,
worstSeverity,
} from "./problemsStore";
import { problemDecorations } from "./decorations";
const d = (line: number, severity: Diagnostic["severity"] = "error", source: string | null = null): Diagnostic => ({
range: { start: { line, col: 0 }, end: { line, col: 1 } },
severity,
message: `line ${line}`,
source,
code: null,
});
describe("mergeDiagnostics", () => {
it("replaces per (file, source) and keeps other sources", () => {
let map = mergeDiagnostics(new Map(), "/a", "ra", [d(3), d(1)]);
map = mergeDiagnostics(map, "/a", "clippy", [d(2, "warning")]);
expect(fileDiagnostics(map, "/a").map((x) => [x.range.start.line, x.source])).toEqual([
[1, "ra"],
[2, "clippy"],
[3, "ra"],
]);
map = mergeDiagnostics(map, "/a", "ra", [d(5, "info")]);
expect(fileDiagnostics(map, "/a").map((x) => x.range.start.line)).toEqual([2, 5]);
});
it("drops files whose sources are all empty and is a no-op for unknown clears", () => {
const a = mergeDiagnostics(new Map(), "/a", "ra", [d(1)]);
expect(mergeDiagnostics(a, "/a", "other", [])).toBe(a);
expect(mergeDiagnostics(a, "/b", "ra", [])).toBe(a);
const cleared = mergeDiagnostics(a, "/a", "ra", []);
expect(cleared.has("/a")).toBe(false);
});
it("keeps an item's own source", () => {
const map = mergeDiagnostics(new Map(), "/a", "eslint", [d(1, "error", "eslint/no-undef")]);
expect(fileDiagnostics(map, "/a")[0]!.source).toBe("eslint/no-undef");
});
});
describe("counts", () => {
it("counts per severity", () => {
let map = mergeDiagnostics(new Map(), "/a", "ra", [d(1), d(2, "warning")]);
map = mergeDiagnostics(map, "/b", "ra", [d(1, "warning"), d(2, "hint")]);
expect(projectCounts(map)).toEqual({ error: 1, warning: 2, info: 0, hint: 1 });
expect(countSeverities(fileDiagnostics(map, "/b"))).toEqual({ error: 0, warning: 1, info: 0, hint: 1 });
expect(worstSeverity(fileDiagnostics(map, "/b"))).toBe("warning");
expect(worstSeverity([])).toBeNull();
});
});
describe("store + decorations", () => {
beforeEach(() => clearProblems());
it("applies events and decorates only files with errors", () => {
applyDiagnosticsEvent("/p/src/a.rs", "ra", [d(1), d(2, "warning")]);
applyDiagnosticsEvent("/p/src/b.rs", "ra", [d(1, "warning")]);
const deco = problemDecorations(useProblems.getState().map);
expect([...deco.keys()]).toEqual(["/p/src/a.rs"]);
expect(deco.get("/p/src/a.rs")).toEqual({ error: true, propagate: true, tooltip: "1 error, 1 warning" });
});
});
describe("profiledProblems", () => {
it("applies the inspection profile per producer", () => {
let raw = mergeDiagnostics(new Map(), "/a", "ra", [d(1), d(2, "warning")]);
raw = mergeDiagnostics(raw, "/b", "eslint", [d(3)]);
const out = profiledProblems(raw, { "lsp:eslint": { enabled: false }, "lsp:ra": { severity: "weakWarning" } });
expect([...out.keys()]).toEqual(["/a"]);
expect(fileDiagnostics(out, "/a").map((x) => x.severity)).toEqual(["info", "info"]);
expect(fileDiagnostics(profiledProblems(raw, {}), "/b")).toHaveLength(1);
});
});
describe("local inspections (issue #42)", () => {
it("reach Problems and clear there too, with their inspection known", async () => {
const { installProblemsEvents, producerFor } = await import("./problemsStore");
const { setFileDiagnostics } = await import("@/editor/lsp/diagnostics");
const { diagnosticInspection } = await import("@/platform/inspections");
clearProblems();
const off = installProblemsEvents();
try {
setFileDiagnostics("/p/a.py", "spelling", [{ ...d(0, "info"), message: "Typo: In word 'mesage'", source: "Spelling" }]);
const [typo] = fileDiagnostics(useProblems.getState().map, "/p/a.py");
expect(typo).toMatchObject({ message: "Typo: In word 'mesage'", typo: true });
const producer = producerFor(useProblems.getState().map, "/p/a.py", typo!);
expect(producer).toBe("spelling");
expect(diagnosticInspection(producer!, typo!)?.id).toBe("builtin:spelling");
setFileDiagnostics("/p/a.py", "spelling", []);
expect(useProblems.getState().map.size).toBe(0);
} finally {
off();
}
});
});