From bea6c6190fda9bb9b9872c72cca02d84015588c6 Mon Sep 17 00:00:00 2001 From: Thibault Leclercq Date: Fri, 7 Apr 2023 10:39:17 +0200 Subject: [PATCH] relative paths, path mappings setting, better diagnostics --- .changeset/spicy-buses-push.md | 5 +++ package.json | 5 +++ src/commands/analyse.ts | 53 +++++++++++++++++++++++---- src/commands/findPHPStanConfigPath.ts | 3 +- src/commands/loadPHPStanConfig.ts | 2 - src/extension.ts | 12 ++++-- src/utils/fs.ts | 33 ++++++++++++++++- src/utils/neon.ts | 3 +- src/utils/path.ts | 9 +---- src/utils/phpstan.ts | 13 +++---- 10 files changed, 106 insertions(+), 32 deletions(-) create mode 100644 .changeset/spicy-buses-push.md diff --git a/.changeset/spicy-buses-push.md b/.changeset/spicy-buses-push.md new file mode 100644 index 0000000..094629e --- /dev/null +++ b/.changeset/spicy-buses-push.md @@ -0,0 +1,5 @@ +--- +"phpstan-vscode": minor +--- + +Better handling of relative paths, new `pathMappings` setting, better diagnostics diff --git a/package.json b/package.json index 224563d..6bcd01a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/commands/analyse.ts b/src/commands/analyse.ts index 0be936e..000dbf4 100644 --- a/src/commands/analyse.ts +++ b/src/commands/analyse.ts @@ -1,3 +1,4 @@ +import { normalize, resolve } from "path"; import { Ext } from "../extension"; import { parsePHPStanAnalyseResult, @@ -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..."; @@ -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); } @@ -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); } } diff --git a/src/commands/findPHPStanConfigPath.ts b/src/commands/findPHPStanConfigPath.ts index 0953872..b10fb2a 100644 --- a/src/commands/findPHPStanConfigPath.ts +++ b/src/commands/findPHPStanConfigPath.ts @@ -1,3 +1,4 @@ +import { relative } from "path"; import { Ext } from "../extension"; import { RelativePattern, workspace } from "vscode"; @@ -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; } diff --git a/src/commands/loadPHPStanConfig.ts b/src/commands/loadPHPStanConfig.ts index 736f2e3..42f033f 100644 --- a/src/commands/loadPHPStanConfig.ts +++ b/src/commands/loadPHPStanConfig.ts @@ -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; diff --git a/src/extension.ts b/src/extension.ts index d243757..5b97e91 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -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"; @@ -34,6 +35,7 @@ export type ExtSettings = { analysedDelay: number; memoryLimit: string; initialAnalysis: boolean; + pathMappings: string; }; export type ExtStore = { @@ -161,6 +163,7 @@ export class Ext< analysedDelay: get("analysedDelay"), memoryLimit: get("memoryLimit"), initialAnalysis: get("initialAnalysis"), + pathMappings: get("pathMappings"), }; } @@ -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); @@ -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}`, diff --git a/src/utils/fs.ts b/src/utils/fs.ts index be76d7b..002d4a4 100644 --- a/src/utils/fs.ts +++ b/src/utils/fs.ts @@ -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 { 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 { + 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); + } + }); +} diff --git a/src/utils/neon.ts b/src/utils/neon.ts index 27e9abd..de098fe 100644 --- a/src/utils/neon.ts +++ b/src/utils/neon.ts @@ -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) { return contents.replace(/(?:%(\w+)%)/g, (_, name) => env[name] ?? ""); @@ -9,7 +10,7 @@ export async function parseNeonFile( path: string, env: Record = {} ): Promise { - 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; } diff --git a/src/utils/path.ts b/src/utils/path.ts index 9e80573..fe3a8fd 100644 --- a/src/utils/path.ts +++ b/src/utils/path.ts @@ -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); -} diff --git a/src/utils/phpstan.ts b/src/utils/phpstan.ts index 7e09506..9f4c74b 100644 --- a/src/utils/phpstan.ts +++ b/src/utils/phpstan.ts @@ -1,5 +1,5 @@ +import { normalize } from "path"; import { parseNeonFile } from "./neon"; -import { resolvePath } from "./path"; export type PHPStanAnalyseResult = { totals: { @@ -29,7 +29,6 @@ export type PHPStanConfig = { }; export type PHPStanConfigEnv = { - rootDir: string; currentWorkingDirectory: string; }; @@ -44,21 +43,19 @@ export async function parsePHPStanConfigFile( env: PHPStanConfigEnv ): Promise { const config = await parseNeonFile(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; }