Skip to content

Commit

Permalink
feat: phpstan extension
Browse files Browse the repository at this point in the history
  • Loading branch information
juanrgm committed Nov 2, 2020
1 parent 53c16e7 commit 3ecd4d1
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 0 deletions.
Binary file added assets/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
172 changes: 172 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import * as vscode from "vscode"
import { ChildProcessWithoutNullStreams, spawn } from "child_process"

type PhpstanResult = {
totals: {
errors: number
file_errors: number
}
files: {
[path: string]: {
errors: number
messages: {
message: string
line: number
ignorable: boolean
}[]
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
errors: any[]
}

type Settings = {
enabled: boolean
path: string
phpPath: string
fileWatcher: boolean
fileWatcherPattern: string
analysedDelay: number
memoryLimit: string
}

const EXT_NAME = "phpstan"

let diagnosticCollection: vscode.DiagnosticCollection
let outputChannel: vscode.OutputChannel
let statusBarItem: vscode.StatusBarItem
let watcher: vscode.FileSystemWatcher

let currentProcessTimeout: NodeJS.Timeout
let currentProcess: ChildProcessWithoutNullStreams | null
let currentProcessKilled: boolean | null

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function activate(context: vscode.ExtensionContext): void {
const config = vscode.workspace.getConfiguration(EXT_NAME)

if (!getConfigValue(config, "enabled")) return

outputChannel = vscode.window.createOutputChannel(EXT_NAME)
diagnosticCollection = vscode.languages.createDiagnosticCollection(EXT_NAME)
statusBarItem = vscode.window.createStatusBarItem()

if (getConfigValue(config, "fileWatcher")) {
watcher = vscode.workspace.createFileSystemWatcher(
getConfigValue(config, "fileWatcherPattern")
)
watcher.onDidChange(async () => phpstanAnalyseDelayed())
}

phpstanAnalyseDelayed(0)
}

export function deactivate(): void {
diagnosticCollection.dispose()
outputChannel.dispose()
statusBarItem.dispose()
watcher?.dispose()
}

function getConfigValue<T extends keyof Settings>(
config: vscode.WorkspaceConfiguration,
name: T
) {
return config.get(name) as Settings[T]
}

async function phpstanAnalyseDelayed(ms?: number) {
const config = vscode.workspace.getConfiguration(EXT_NAME)
clearTimeout(currentProcessTimeout)
currentProcessTimeout = setTimeout(async () => {
if (currentProcess) {
currentProcessKilled = true
currentProcess.kill()
}
await phpstanAnalyse()
currentProcess = currentProcessKilled = null
}, ms ?? (config.get<number>("analysedDelay") as number))
}

async function phpstanAnalyse() {
const config = vscode.workspace.getConfiguration(EXT_NAME)

const phpPath = getConfigValue(config, "phpPath")
const phpStanPath = getConfigValue(config, "path")
const memoryLimit = getConfigValue(config, "memoryLimit")

statusBarItem.text = "PHPStan analysing..."
statusBarItem.tooltip = ""
statusBarItem.show()

try {
const args = ["-f", phpStanPath, "analyse"]
.concat(memoryLimit ? ["--memory-limit=" + memoryLimit] : [])
.concat(["--error-format=json"])

const childProcess = (currentProcess = spawn(phpPath, args, {
cwd: vscode.workspace.rootPath,
}))

const [, stdout] = await waitForClose(childProcess)

if (currentProcessKilled) {
currentProcessKilled = false
return
}

const phpstanResult = parsePhpstanStdout(stdout)

refreshDiagnostics(phpstanResult)
} catch (error) {
outputChannel.appendLine((error as Error).message)
statusBarItem.text = `$(error) PHPStan`
statusBarItem.tooltip = `Spawn error: ${(error as Error).message}`
return
}

statusBarItem.tooltip = ""
statusBarItem.hide()
}

function parsePhpstanStdout(stdout: string): PhpstanResult {
return JSON.parse(stdout)
}

async function refreshDiagnostics(result: PhpstanResult) {
diagnosticCollection.clear()

for (const path in result.files) {
const pathItem = result.files[path]
const diagnostics: vscode.Diagnostic[] = []
for (const messageItem of pathItem.messages) {
const range = new vscode.Range(
messageItem.line,
0,
messageItem.line,
0
)
const diagnostic = new vscode.Diagnostic(
range,
messageItem.message,
vscode.DiagnosticSeverity.Error
)

diagnostics.push(diagnostic)
}
diagnosticCollection.set(vscode.Uri.file(path), diagnostics)
}
}

async function waitForClose(childProcess: ChildProcessWithoutNullStreams) {
return new Promise<[number, string]>((resolve, reject) => {
let result = ""
childProcess.stdout.on("data", (data) => {
result += data + "\n"
outputChannel.appendLine(data)
})
childProcess.stderr.on("data", (data) => outputChannel.appendLine(data))
childProcess.on("error", reject)
childProcess.on("close", (exitCode) => resolve([exitCode, result]))
})
}

0 comments on commit 3ecd4d1

Please sign in to comment.