feat(php): show PHPStan errors in the editor on save (#1518)

PHPStan results were only visible in a terminal. Saving a PHP file in a trusted project with vendor/bin/phpstan now runs phpstan analyse --error-format=json on the saved files (debounced, from the project root, on the selected PHP with XDEBUG_MODE=off) and shows its errors at their lines in the editor and Problems, with the PHPStan identifier as the code; saving the fixed code clears them. Settings > Editor > Inspections lists PHPStan under PHP with each reported identifier, so rules can be turned off or re-graded, and Settings > Languages & Frameworks > PHP > PHPStan selects another configuration file. A failing run is reported once as a notification; a newer save cancels a check in progress, and Restricted Mode stops it.

Closes #1212

Co-authored-by: DanielADK <[email protected]>
Цей коміт міститься в:
Daniel AdámekтаDanielADK authored and GitHub committed 2026-11-05 21:50:06 +08:00
1 джерело 324dded5c1
коміт c495c09416
24 files changed
+1163 -17

No files matched your search

+37
Переглянути файл
@@ -62,6 +62,42 @@ describe("plugin inspections", () => {
});
});
describe("PHPStan inspections", () => {
it("list each identifier below PHPStan, from seen codes and stored overrides", () => {
const tree = buildInspectionTree(
[{ name: "PHP", servers: ["intelephense"] }],
[
{
producer: "phpstan",
code: "return.type",
source: "PHPStan",
sample: "Should return int but returns string.",
},
{ producer: "build", code: "E0308", source: "rustc", sample: "" },
],
[{ "builtin:phpstan:constant.notFound": { enabled: false }, "builtin:build:E1": { enabled: false } }],
);
const php = find(tree, "group:PHP")!;
expect(php.children.map((n) => n.id)).toEqual(["lsp:intelephense", "builtin:phpstan"]);
const phpstan = find(tree, "PHP/builtin:phpstan")!;
expect(phpstan.children.map((n) => [n.id, n.name])).toEqual([
["builtin:phpstan:constant.notFound", "constant.notFound"],
["builtin:phpstan:return.type", "return.type"],
]);
expect(phpstan.children[1]!.description).toBe(
"PHPStan problems with code return.type.\n\nExample: Should return int but returns string.",
);
expect(pathToInspection(tree, "builtin:phpstan:return.type")).toEqual([
"group:PHP",
"PHP/builtin:phpstan",
"PHP/builtin:phpstan:return.type",
]);
// Other built-in checks stay single inspections.
expect(find(tree, "General/builtin:build")!.children).toEqual([]);
expect(checkState({ "builtin:phpstan:constant.notFound": { enabled: false } }, phpstan)).toBe("mixed");
});
});
describe("inspections tree", () => {
const tree = buildInspectionTree(languages, seen, [{ "lsp:ruff:E501": { severity: "typo" } }]);
@@ -72,6 +108,7 @@ describe("inspections tree", () => {
"Java",
"JavaScript",
"Other",
"PHP",
"Proofreading",
"Python",
"SQL",
+5
Переглянути файл
@@ -63,7 +63,12 @@ describe("languages settings", () => {
languageLevel: "",
includePaths: [],
stubs: [],
phpstanConfig: "",
});
expect(validateLanguages({ php: { phpstanConfig: " build/phpstan.neon " } }).php.phpstanConfig).toBe(
"build/phpstan.neon",
);
expect(validateLanguages({ php: { phpstanConfig: 3 } }).php.phpstanConfig).toBe("");
const lists = validateLanguages({ php: { includePaths: [" ../platform ", "", 3, "../platform"], stubs: "redis" } });
expect(lists.php.includePaths).toEqual(["../platform"]);
expect(lists.php.stubs).toEqual([]);
+10
Переглянути файл
@@ -162,6 +162,16 @@ export function PhpPage() {
/>
</Row>
</Section>
<Section title="PHPStan">
<PathInput
k="languages.php.phpstanConfig"
label="Configuration file"
placeholder="phpstan.neon, phpstan.neon.dist or phpstan.dist.neon"
hint="The configuration vendor/bin/phpstan checks saved files with. Empty uses phpstan.neon, phpstan.neon.dist or phpstan.dist.neon in the project root. Absolute, or relative to the project."
title="Select PHPStan Configuration File"
file
/>
</Section>
<Section title="CLI Interpreter">
<SdkRow
k="languages.php.interpreter"
+17 -10
Переглянути файл
@@ -60,18 +60,22 @@ function serverNode(
: languages
? tx`Problems the ${server} language server reports in ${languages} files. Severity "As reported" keeps the level the server assigns; a code below can override it.`
: tx`Problems the ${server} language server reports. Severity "As reported" keeps the level the server assigns; a code below can override it.`,
children: [...codes]
.sort((a, b) => a.code.localeCompare(b.code, undefined, { numeric: true }))
.map((c) => ({
key: `${group}/${codeInspectionId(id, c.code)}`,
id: codeInspectionId(id, c.code),
name: c.code,
description: codeDescription(plugin ?? server, c),
children: [],
})),
children: codeNodes(group, id, plugin ?? server, codes),
};
}
function codeNodes(group: string, id: string, owner: string, codes: readonly SeenCode[]): InspectionNode[] {
return [...codes]
.sort((a, b) => a.code.localeCompare(b.code, undefined, { numeric: true }))
.map((c) => ({
key: `${group}/${codeInspectionId(id, c.code)}`,
id: codeInspectionId(id, c.code),
name: c.code,
description: codeDescription(owner, c),
children: [],
}));
}
/**
* The inspection tree: one group per language that has language servers, the
* built-in checks under their category, and servers known only from seen
@@ -98,6 +102,9 @@ export function buildInspectionTree(
const m = /^lsp:(plugin:[^:]+|[^:]+)(?::(.+))?$/.exec(id);
if (m?.[2]) addCode({ producer: m[1]!, code: m[2], source: "", sample: "" });
else if (m && !codes.has(m[1]!)) codes.set(m[1]!, new Map());
const builtin = BUILTIN_INSPECTIONS.find((b) => b.byCode && id.startsWith(`${b.id}:`));
if (builtin)
addCode({ producer: builtin.producer, code: id.slice(builtin.id.length + 1), source: "", sample: "" });
}
}
const servedBy = new Map<string, string[]>();
@@ -125,7 +132,7 @@ export function buildInspectionTree(
id: b.id,
name: b.name,
description: b.description,
children: [],
children: b.byCode ? codeNodes(b.group, b.id, b.name, [...(codes.get(b.producer)?.values() ?? [])]) : [],
});
}
for (const [producer, list] of codes) {
+2
Переглянути файл
@@ -153,6 +153,8 @@ export const LANGUAGE_PAGES: PageSpec[] = [
"php",
"composer",
"phpunit",
"phpstan",
"phpstan.neon",
"xampp",
"herd",
"homebrew",