Skip to content

Relative paths, new path mapping setting, better diagnostics #23

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/spicy-buses-push.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"phpstan-vscode": minor
---

Better handling of relative paths, new `pathMappings` setting, better diagnostics
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@
"description": "PHPStan config path",
"default": "{phpstan.neon,phpstan.neon.dist}"
},
"phpstan.pathMappings": {
"type": "string",
"description": "Mappings for paths in PHPStan results, comma separated",
"default": "/srv/app:."
},
"phpstan.analysedDelay": {
"type": "integer",
"description": "Milliseconds delay between file changes before run analyse",
Expand Down
53 changes: 46 additions & 7 deletions src/commands/analyse.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { normalize, resolve } from "path";
import { Ext } from "../extension";
import {
parsePHPStanAnalyseResult,
Expand All @@ -8,6 +9,7 @@ import showOutput from "./showOutput";
import stopAnalyse from "./stopAnalyse";
import { spawn } from "child_process";
import { Diagnostic, DiagnosticSeverity, Range, Uri } from "vscode";
import { getFileLines } from "../utils/fs";

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

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

for (let path in result.files) {
const pathMaps: {
src: string,
dest: string,
}[] = [];

ext.settings.pathMappings.split(',').map(mapping => {
const parts = mapping.split(':').map(p => p.trim()).map(p => p.length > 0 ? p : '.').map(normalize);
if (parts.length === 2 && parts[0] && parts[1]) {
pathMaps.push({
src: parts[0] + '/',
dest: parts[1] + '/',
});
}
});

ext.log('Using path mappings: ' + JSON.stringify(pathMaps));

for (const path in result.files) {
let realPath = path;

const matches = contextRegex.exec(realPath);

if (matches) realPath = realPath.slice(0, matches.index);

realPath = normalize(realPath);

for (const pathMap of pathMaps) {
if (realPath.startsWith(pathMap.src)) {
realPath = resolve(ext.cwd, pathMap.dest + realPath.substring(pathMap.src.length));
break;
}
}

const fileLines: string[] = await getFileLines(resolve(realPath));

const pathItem = result.files[path];
const diagnostics: Diagnostic[] = [];
for (const messageItem of pathItem.messages) {
const line = messageItem.line ? messageItem.line - 1 : 0;
const range = new Range(line, 0, line, 0);
const lineText = messageItem.line ? (fileLines[line] ?? '') : '';

const startCol = Math.max(0, lineText.search(/[^\s]/g));
const endCol = Math.max(0, lineText.search(/\s*$/g));

const range = new Range(line, startCol, line, endCol);
const diagnostic = new Diagnostic(
range,
messageItem.message,
DiagnosticSeverity.Error
);
diagnostic.source = ext.options.name;

diagnostics.push(diagnostic);
}

const matches = contextRegex.exec(path);

if (matches) path = path.slice(0, matches.index);

diagnostic.set(Uri.file(path), diagnostics);
diagnostic.set(Uri.file(realPath), diagnostics);
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/commands/findPHPStanConfigPath.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { relative } from "path";
import { Ext } from "../extension";
import { RelativePattern, workspace } from "vscode";

Expand All @@ -9,7 +10,7 @@ export default async function findPHPStanConfigPath(ext: Ext) {
1
);
if (!configUri) throw new Error(`Config path not found.`);
const configPath = configUri.fsPath;
const configPath = relative(ext.cwd, configUri.path);
ext.log({ tag: "configPath", message: configPath });
return configPath;
}
2 changes: 0 additions & 2 deletions src/commands/loadPHPStanConfig.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { Ext } from "../extension";
import { parsePHPStanConfigFile } from "../utils/phpstan";
import { dirname, join, normalize } from "path";

export default async function loadPHPStanConfig(ext: Ext) {
if (!ext.store.phpstan.configPath) throw new Error("Config path is required");
const config = await parsePHPStanConfigFile(ext.store.phpstan.configPath, {
currentWorkingDirectory: ext.cwd,
rootDir: normalize(dirname(join(ext.cwd, ext.settings.path))),
});
ext.log({ tag: "config", message: JSON.stringify(config, null, 2) });
return config;
Expand Down
12 changes: 9 additions & 3 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { normalize, resolve } from "path";
import findPHPStanConfigPath from "./commands/findPHPStanConfigPath";
import loadPHPStanConfig from "./commands/loadPHPStanConfig";
import { createDelayedTimeout, DelayedTimeout } from "./utils/async";
Expand Down Expand Up @@ -34,6 +35,7 @@ export type ExtSettings = {
analysedDelay: number;
memoryLimit: string;
initialAnalysis: boolean;
pathMappings: string;
};

export type ExtStore = {
Expand Down Expand Up @@ -161,6 +163,7 @@ export class Ext<
analysedDelay: get("analysedDelay"),
memoryLimit: get("memoryLimit"),
initialAnalysis: get("initialAnalysis"),
pathMappings: get("pathMappings"),
};
}

Expand Down Expand Up @@ -229,7 +232,7 @@ export class Ext<

if (this.settings.configFileWatcher)
this.fileWatchers.register(
new RelativePattern(getWorkspacePath(), this.settings.configPath),
new RelativePattern(this.cwd, this.settings.configPath),
(uri, eventName) => {
if (!this.store.fileWatcher.enabled) return;
const path = sanitizeFsPath(uri.fsPath);
Expand All @@ -246,8 +249,11 @@ export class Ext<
const extensions = config.parameters?.fileExtensions ?? ["php"];
this.fileWatchers.register({ extensions }, async (uri, eventName) => {
if (!this.store.fileWatcher.enabled) return;
for (const patternPath of config.parameters?.paths || []) {
const path = sanitizeFsPath(uri.fsPath);
for (let patternPath of config.parameters?.paths || []) {
patternPath = resolve(this.cwd, patternPath);

const path = normalize(uri.fsPath);

if (path.startsWith(patternPath)) {
this.log({
tag: `event:${eventName}`,
Expand Down
33 changes: 31 additions & 2 deletions src/utils/fs.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,39 @@
import { promises as fs } from "fs";
import * as fs from "fs";
import * as readline from "readline";

export async function checkFile(path: string): Promise<boolean> {
try {
return !!(await fs.stat(path));
return !!(await fs.promises.stat(path));
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
throw error;
}
}

export async function getFileLines(path: string): Promise<string[]> {
if (!checkFile(path)) {
return Promise.resolve([]);
}

return new Promise((resolve, reject) => {
try {
const stream = fs.createReadStream(path);

const rl = readline.createInterface({
input: stream,
});

const lines: string[] = [];

rl.on('line', (line) => {
lines.push(line);
});

rl.on('close', () => {
resolve(lines);
});
} catch (err) {
reject(err);
}
});
}
3 changes: 2 additions & 1 deletion src/utils/neon.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { readFile } from "fs/promises";
import { load } from "js-yaml";
import { join, resolve } from "path";

export function resolveNeon(contents: string, env: Record<string, string>) {
return contents.replace(/(?:%(\w+)%)/g, (_, name) => env[name] ?? "");
Expand All @@ -9,7 +10,7 @@ export async function parseNeonFile<T = unknown>(
path: string,
env: Record<string, string> = {}
): Promise<T> {
const contents = (await readFile(path)).toString();
const contents = (await readFile(resolve(join(env.currentWorkingDirectory, path)))).toString();
const yaml = resolveNeon(contents.replace(/\t/g, " "), env);
return load(yaml) as Promise<T>;
}
9 changes: 1 addition & 8 deletions src/utils/path.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
import { isAbsolute, join, normalize } from "path";

/**
* @link https://github.com/microsoft/vscode/blob/84a3473d/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts#L227
*/
export function sanitizeFsPath(path: string) {
if (process.platform === "win32" && path[1] === ":") {
return path[0].toUpperCase() + path.substr(1);
return path[0].toUpperCase() + path.substring(1);
} else {
return path;
}
}

export function resolvePath(path: string, cwd: string): string {
if (!isAbsolute(path)) path = join(cwd, path);
return normalize(path);
}
13 changes: 5 additions & 8 deletions src/utils/phpstan.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { normalize } from "path";
import { parseNeonFile } from "./neon";
import { resolvePath } from "./path";

export type PHPStanAnalyseResult = {
totals: {
Expand Down Expand Up @@ -29,7 +29,6 @@ export type PHPStanConfig = {
};

export type PHPStanConfigEnv = {
rootDir: string;
currentWorkingDirectory: string;
};

Expand All @@ -44,21 +43,19 @@ export async function parsePHPStanConfigFile(
env: PHPStanConfigEnv
): Promise<PHPStanConfig> {
const config = await parseNeonFile<PHPStanConfig>(path, env);
return normalizePHPStanConfig(config, env.currentWorkingDirectory);
return normalizePHPStanConfig(config);
}

export function normalizePHPStanConfig(
config: PHPStanConfig,
cwd: string
): PHPStanConfig {
config = Object.assign({}, config);
config.parameters = Object.assign({}, config.parameters);
const params = config.parameters;
const resolve = (v: string) => resolvePath(v, cwd);

params.paths = params.paths?.map(resolve);
params.excludes_analyse = params.excludes_analyse?.map(resolve);
params.bootstrapFiles = params.bootstrapFiles?.map(resolve);
params.paths = params.paths?.map(normalize);
params.excludes_analyse = params.excludes_analyse?.map(normalize);
params.bootstrapFiles = params.bootstrapFiles?.map(normalize);

return config;
}