Skip to content

Commit bea6c61

Browse files
committed
relative paths, path mappings setting, better diagnostics
1 parent 269d18b commit bea6c61

File tree

10 files changed

+106
-32
lines changed

10 files changed

+106
-32
lines changed

.changeset/spicy-buses-push.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"phpstan-vscode": minor
3+
---
4+
5+
Better handling of relative paths, new `pathMappings` setting, better diagnostics

package.json

+5
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@
118118
"description": "PHPStan config path",
119119
"default": "{phpstan.neon,phpstan.neon.dist}"
120120
},
121+
"phpstan.pathMappings": {
122+
"type": "string",
123+
"description": "Mappings for paths in PHPStan results, comma separated",
124+
"default": "/srv/app:."
125+
},
121126
"phpstan.analysedDelay": {
122127
"type": "integer",
123128
"description": "Milliseconds delay between file changes before run analyse",

src/commands/analyse.ts

+46-7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { normalize, resolve } from "path";
12
import { Ext } from "../extension";
23
import {
34
parsePHPStanAnalyseResult,
@@ -8,6 +9,7 @@ import showOutput from "./showOutput";
89
import stopAnalyse from "./stopAnalyse";
910
import { spawn } from "child_process";
1011
import { Diagnostic, DiagnosticSeverity, Range, Uri } from "vscode";
12+
import { getFileLines } from "../utils/fs";
1113

1214
function setStatusBarProgress(ext: Ext, progress?: number) {
1315
let text = "$(sync~spin) PHPStan analysing...";
@@ -25,6 +27,7 @@ async function refreshDiagnostics(ext: Ext, result: PHPStanAnalyseResult) {
2527
for (const error of result.errors) {
2628
const range = new Range(0, 0, 0, 0);
2729
const diagnostic = new Diagnostic(range, error, DiagnosticSeverity.Error);
30+
diagnostic.source = ext.options.name;
2831
globalDiagnostics.push(diagnostic);
2932
}
3033

@@ -38,26 +41,62 @@ async function refreshDiagnostics(ext: Ext, result: PHPStanAnalyseResult) {
3841
// https://github.com/phpstan/phpstan-src/blob/6d228a53/src/Analyser/MutatingScope.php#L289
3942
const contextRegex = / \(in context of .+\)$/;
4043

41-
for (let path in result.files) {
44+
const pathMaps: {
45+
src: string,
46+
dest: string,
47+
}[] = [];
48+
49+
ext.settings.pathMappings.split(',').map(mapping => {
50+
const parts = mapping.split(':').map(p => p.trim()).map(p => p.length > 0 ? p : '.').map(normalize);
51+
if (parts.length === 2 && parts[0] && parts[1]) {
52+
pathMaps.push({
53+
src: parts[0] + '/',
54+
dest: parts[1] + '/',
55+
});
56+
}
57+
});
58+
59+
ext.log('Using path mappings: ' + JSON.stringify(pathMaps));
60+
61+
for (const path in result.files) {
62+
let realPath = path;
63+
64+
const matches = contextRegex.exec(realPath);
65+
66+
if (matches) realPath = realPath.slice(0, matches.index);
67+
68+
realPath = normalize(realPath);
69+
70+
for (const pathMap of pathMaps) {
71+
if (realPath.startsWith(pathMap.src)) {
72+
realPath = resolve(ext.cwd, pathMap.dest + realPath.substring(pathMap.src.length));
73+
break;
74+
}
75+
}
76+
77+
const fileLines: string[] = await getFileLines(resolve(realPath));
78+
4279
const pathItem = result.files[path];
4380
const diagnostics: Diagnostic[] = [];
4481
for (const messageItem of pathItem.messages) {
4582
const line = messageItem.line ? messageItem.line - 1 : 0;
46-
const range = new Range(line, 0, line, 0);
83+
const lineText = messageItem.line ? (fileLines[line] ?? '') : '';
84+
85+
const startCol = Math.max(0, lineText.search(/[^\s]/g));
86+
const endCol = Math.max(0, lineText.search(/\s*$/g));
87+
88+
const range = new Range(line, startCol, line, endCol);
4789
const diagnostic = new Diagnostic(
4890
range,
4991
messageItem.message,
5092
DiagnosticSeverity.Error
5193
);
94+
diagnostic.source = ext.options.name;
5295

5396
diagnostics.push(diagnostic);
5497
}
5598

56-
const matches = contextRegex.exec(path);
57-
58-
if (matches) path = path.slice(0, matches.index);
59-
60-
diagnostic.set(Uri.file(path), diagnostics);
99+
diagnostic.set(Uri.file(realPath), diagnostics);
61100
}
62101
}
63102

src/commands/findPHPStanConfigPath.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { relative } from "path";
12
import { Ext } from "../extension";
23
import { RelativePattern, workspace } from "vscode";
34

@@ -9,7 +10,7 @@ export default async function findPHPStanConfigPath(ext: Ext) {
910
1
1011
);
1112
if (!configUri) throw new Error(`Config path not found.`);
12-
const configPath = configUri.fsPath;
13+
const configPath = relative(ext.cwd, configUri.path);
1314
ext.log({ tag: "configPath", message: configPath });
1415
return configPath;
1516
}

src/commands/loadPHPStanConfig.ts

-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import { Ext } from "../extension";
22
import { parsePHPStanConfigFile } from "../utils/phpstan";
3-
import { dirname, join, normalize } from "path";
43

54
export default async function loadPHPStanConfig(ext: Ext) {
65
if (!ext.store.phpstan.configPath) throw new Error("Config path is required");
76
const config = await parsePHPStanConfigFile(ext.store.phpstan.configPath, {
87
currentWorkingDirectory: ext.cwd,
9-
rootDir: normalize(dirname(join(ext.cwd, ext.settings.path))),
108
});
119
ext.log({ tag: "config", message: JSON.stringify(config, null, 2) });
1210
return config;

src/extension.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { normalize, resolve } from "path";
12
import findPHPStanConfigPath from "./commands/findPHPStanConfigPath";
23
import loadPHPStanConfig from "./commands/loadPHPStanConfig";
34
import { createDelayedTimeout, DelayedTimeout } from "./utils/async";
@@ -34,6 +35,7 @@ export type ExtSettings = {
3435
analysedDelay: number;
3536
memoryLimit: string;
3637
initialAnalysis: boolean;
38+
pathMappings: string;
3739
};
3840

3941
export type ExtStore = {
@@ -161,6 +163,7 @@ export class Ext<
161163
analysedDelay: get("analysedDelay"),
162164
memoryLimit: get("memoryLimit"),
163165
initialAnalysis: get("initialAnalysis"),
166+
pathMappings: get("pathMappings"),
164167
};
165168
}
166169

@@ -229,7 +232,7 @@ export class Ext<
229232

230233
if (this.settings.configFileWatcher)
231234
this.fileWatchers.register(
232-
new RelativePattern(getWorkspacePath(), this.settings.configPath),
235+
new RelativePattern(this.cwd, this.settings.configPath),
233236
(uri, eventName) => {
234237
if (!this.store.fileWatcher.enabled) return;
235238
const path = sanitizeFsPath(uri.fsPath);
@@ -246,8 +249,11 @@ export class Ext<
246249
const extensions = config.parameters?.fileExtensions ?? ["php"];
247250
this.fileWatchers.register({ extensions }, async (uri, eventName) => {
248251
if (!this.store.fileWatcher.enabled) return;
249-
for (const patternPath of config.parameters?.paths || []) {
250-
const path = sanitizeFsPath(uri.fsPath);
252+
for (let patternPath of config.parameters?.paths || []) {
253+
patternPath = resolve(this.cwd, patternPath);
254+
255+
const path = normalize(uri.fsPath);
256+
251257
if (path.startsWith(patternPath)) {
252258
this.log({
253259
tag: `event:${eventName}`,

src/utils/fs.ts

+31-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,39 @@
1-
import { promises as fs } from "fs";
1+
import * as fs from "fs";
2+
import * as readline from "readline";
23

34
export async function checkFile(path: string): Promise<boolean> {
45
try {
5-
return !!(await fs.stat(path));
6+
return !!(await fs.promises.stat(path));
67
} catch (error) {
78
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
89
throw error;
910
}
1011
}
12+
13+
export async function getFileLines(path: string): Promise<string[]> {
14+
if (!checkFile(path)) {
15+
return Promise.resolve([]);
16+
}
17+
18+
return new Promise((resolve, reject) => {
19+
try {
20+
const stream = fs.createReadStream(path);
21+
22+
const rl = readline.createInterface({
23+
input: stream,
24+
});
25+
26+
const lines: string[] = [];
27+
28+
rl.on('line', (line) => {
29+
lines.push(line);
30+
});
31+
32+
rl.on('close', () => {
33+
resolve(lines);
34+
});
35+
} catch (err) {
36+
reject(err);
37+
}
38+
});
39+
}

src/utils/neon.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { readFile } from "fs/promises";
22
import { load } from "js-yaml";
3+
import { join, resolve } from "path";
34

45
export function resolveNeon(contents: string, env: Record<string, string>) {
56
return contents.replace(/(?:%(\w+)%)/g, (_, name) => env[name] ?? "");
@@ -9,7 +10,7 @@ export async function parseNeonFile<T = unknown>(
910
path: string,
1011
env: Record<string, string> = {}
1112
): Promise<T> {
12-
const contents = (await readFile(path)).toString();
13+
const contents = (await readFile(resolve(join(env.currentWorkingDirectory, path)))).toString();
1314
const yaml = resolveNeon(contents.replace(/\t/g, " "), env);
1415
return load(yaml) as Promise<T>;
1516
}

src/utils/path.ts

+1-8
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,10 @@
1-
import { isAbsolute, join, normalize } from "path";
2-
31
/**
42
* @link https://github.com/microsoft/vscode/blob/84a3473d/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts#L227
53
*/
64
export function sanitizeFsPath(path: string) {
75
if (process.platform === "win32" && path[1] === ":") {
8-
return path[0].toUpperCase() + path.substr(1);
6+
return path[0].toUpperCase() + path.substring(1);
97
} else {
108
return path;
119
}
1210
}
13-
14-
export function resolvePath(path: string, cwd: string): string {
15-
if (!isAbsolute(path)) path = join(cwd, path);
16-
return normalize(path);
17-
}

src/utils/phpstan.ts

+5-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import { normalize } from "path";
12
import { parseNeonFile } from "./neon";
2-
import { resolvePath } from "./path";
33

44
export type PHPStanAnalyseResult = {
55
totals: {
@@ -29,7 +29,6 @@ export type PHPStanConfig = {
2929
};
3030

3131
export type PHPStanConfigEnv = {
32-
rootDir: string;
3332
currentWorkingDirectory: string;
3433
};
3534

@@ -44,21 +43,19 @@ export async function parsePHPStanConfigFile(
4443
env: PHPStanConfigEnv
4544
): Promise<PHPStanConfig> {
4645
const config = await parseNeonFile<PHPStanConfig>(path, env);
47-
return normalizePHPStanConfig(config, env.currentWorkingDirectory);
46+
return normalizePHPStanConfig(config);
4847
}
4948

5049
export function normalizePHPStanConfig(
5150
config: PHPStanConfig,
52-
cwd: string
5351
): PHPStanConfig {
5452
config = Object.assign({}, config);
5553
config.parameters = Object.assign({}, config.parameters);
5654
const params = config.parameters;
57-
const resolve = (v: string) => resolvePath(v, cwd);
5855

59-
params.paths = params.paths?.map(resolve);
60-
params.excludes_analyse = params.excludes_analyse?.map(resolve);
61-
params.bootstrapFiles = params.bootstrapFiles?.map(resolve);
56+
params.paths = params.paths?.map(normalize);
57+
params.excludes_analyse = params.excludes_analyse?.map(normalize);
58+
params.bootstrapFiles = params.bootstrapFiles?.map(normalize);
6259

6360
return config;
6461
}

0 commit comments

Comments
 (0)