| /* eslint-disable @typescript-eslint/no-explicit-any */ |
| /*--------------------------------------------------------- |
| * Copyright (C) Microsoft Corporation. All rights reserved. |
| * Licensed under the MIT License. See LICENSE in the project root for license information. |
| *--------------------------------------------------------*/ |
| |
| import cp = require('child_process'); |
| import fs = require('fs'); |
| import os = require('os'); |
| import path = require('path'); |
| import semver = require('semver'); |
| import util = require('util'); |
| import vscode = require('vscode'); |
| import { NearestNeighborDict, Node } from './avlTree'; |
| import { DefaultConfig, getGoConfig } from './config'; |
| import { extensionId } from './const'; |
| import { toolExecutionEnvironment } from './goEnv'; |
| import { languageClient } from './goLanguageServer'; |
| import { buildDiagnosticCollection, lintDiagnosticCollection, vetDiagnosticCollection } from './goMain'; |
| import { getCurrentPackage } from './goModules'; |
| import { outputChannel } from './goStatus'; |
| import { getFromWorkspaceState } from './stateUtils'; |
| import { |
| envPath, |
| fixDriveCasingInWindows, |
| getBinPathWithPreferredGopathGorootWithExplanation, |
| getCurrentGoRoot, |
| getInferredGopath, |
| resolveHomeDir |
| } from './utils/pathUtils'; |
| import { killProcessTree } from './utils/processUtils'; |
| |
| let userNameHash = 0; |
| |
| export const goKeywords: string[] = [ |
| 'break', |
| 'case', |
| 'chan', |
| 'const', |
| 'continue', |
| 'default', |
| 'defer', |
| 'else', |
| 'fallthrough', |
| 'for', |
| 'func', |
| 'go', |
| 'goto', |
| 'if', |
| 'import', |
| 'interface', |
| 'map', |
| 'package', |
| 'range', |
| 'return', |
| 'select', |
| 'struct', |
| 'switch', |
| 'type', |
| 'var' |
| ]; |
| |
| export const goBuiltinTypes: Set<string> = new Set<string>([ |
| 'bool', |
| 'byte', |
| 'complex128', |
| 'complex64', |
| 'error', |
| 'float32', |
| 'float64', |
| 'int', |
| 'int16', |
| 'int32', |
| 'int64', |
| 'int8', |
| 'rune', |
| 'string', |
| 'uint', |
| 'uint16', |
| 'uint32', |
| 'uint64', |
| 'uint8', |
| 'uintptr' |
| ]); |
| |
| export class GoVersion { |
| public sv?: semver.SemVer; |
| // Go version tags are not following the strict semver format |
| // so semver drops the prerelease tags used in Go version. |
| // If sv is valid, let's keep the original version string |
| // including the prerelease tag parts. |
| public svString?: string; |
| |
| public isDevel?: boolean; |
| private devVersion?: string; |
| |
| constructor(public binaryPath: string, public version: string) { |
| const matchesRelease = /^go version go(\d\.\d+\S*)\s+/.exec(version); |
| const matchesDevel = /^go version devel (\S+)\s+/.exec(version); |
| if (matchesRelease) { |
| // note: semver.parse does not work with Go version string like go1.14. |
| const sv = semver.coerce(matchesRelease[1]); |
| if (sv) { |
| this.sv = sv; |
| this.svString = matchesRelease[1]; |
| } |
| } else if (matchesDevel) { |
| this.isDevel = true; |
| this.devVersion = matchesDevel[1]; |
| } |
| } |
| |
| public isValid(): boolean { |
| return !!this.sv || !!this.isDevel; |
| } |
| |
| public format(includePrerelease?: boolean): string { |
| if (this.sv) { |
| if (includePrerelease && this.svString) { |
| return this.svString; |
| } |
| return this.sv.format(); |
| } |
| if (this.isDevel) { |
| return `devel ${this.devVersion}`; |
| } |
| return 'unknown'; |
| } |
| |
| public lt(version: string): boolean { |
| // Assume a developer version is always above any released version. |
| // This is not necessarily true. |
| if (this.isDevel || !this.sv) { |
| return false; |
| } |
| const v = semver.coerce(version); |
| if (!v) { |
| return false; |
| } |
| return semver.lt(this.sv, v); |
| } |
| |
| public gt(version: string): boolean { |
| // Assume a developer version is always above any released version. |
| // This is not necessarily true. |
| if (this.isDevel || !this.sv) { |
| return true; |
| } |
| const v = semver.coerce(version); |
| if (!v) { |
| return false; |
| } |
| return semver.gt(this.sv, v); |
| } |
| } |
| |
| let cachedGoBinPath: string | undefined; |
| let cachedGoVersion: GoVersion | undefined; |
| let vendorSupport: boolean | undefined; |
| let toolsGopath: string; |
| |
| // getCheckForToolsUpdatesConfig returns go.toolsManagement.checkForUpdates configuration. |
| export function getCheckForToolsUpdatesConfig(gocfg: vscode.WorkspaceConfiguration) { |
| // useGoProxyToCheckForToolUpdates deprecation |
| // TODO: Step 1. mark as deprecated in Dec 2020 release, and update dev containers. |
| // Step 2. prompt users to switch config. Jan 2020 |
| // Step 3. delete useGoProxyToCheckForToolUpdates support. Feb 2020 |
| const legacyCfg = gocfg.get('useGoProxyToCheckForToolUpdates'); |
| if (legacyCfg === false) { |
| const cfg = gocfg.inspect('toolsManagement.checkForUpdates'); |
| if (cfg.globalValue === undefined && cfg.workspaceValue === undefined) { |
| return 'local'; |
| } |
| } |
| return gocfg.get('toolsManagement.checkForUpdates') as string; |
| } |
| |
| export function byteOffsetAt(document: vscode.TextDocument, position: vscode.Position): number { |
| const offset = document.offsetAt(position); |
| const text = document.getText(); |
| return Buffer.byteLength(text.substr(0, offset)); |
| } |
| |
| export interface Prelude { |
| imports: Array<{ kind: string; start: number; end: number; pkgs: string[] }>; |
| pkg: { start: number; end: number; name: string }; |
| } |
| |
| export function parseFilePrelude(text: string): Prelude { |
| const lines = text.split('\n'); |
| const ret: Prelude = { imports: [], pkg: null }; |
| for (let i = 0; i < lines.length; i++) { |
| const line = lines[i]; |
| const pkgMatch = line.match(/^(\s)*package(\s)+(\w+)/); |
| if (pkgMatch) { |
| ret.pkg = { start: i, end: i, name: pkgMatch[3] }; |
| } |
| if (line.match(/^(\s)*import(\s)+\(/)) { |
| ret.imports.push({ kind: 'multi', start: i, end: -1, pkgs: [] }); |
| } else if (line.match(/^\s*import\s+"C"/)) { |
| ret.imports.push({ kind: 'pseudo', start: i, end: i, pkgs: [] }); |
| } else if (line.match(/^(\s)*import(\s)+[^(]/)) { |
| ret.imports.push({ kind: 'single', start: i, end: i, pkgs: [] }); |
| } |
| if (line.match(/^(\s)*(\/\*.*\*\/)*\s*\)/)) { |
| // /* comments */ |
| if (ret.imports[ret.imports.length - 1].end === -1) { |
| ret.imports[ret.imports.length - 1].end = i; |
| } |
| } else if (ret.imports.length) { |
| if (ret.imports[ret.imports.length - 1].end === -1) { |
| const importPkgMatch = line.match(/"([^"]+)"/); |
| if (importPkgMatch) { |
| ret.imports[ret.imports.length - 1].pkgs.push(importPkgMatch[1]); |
| } |
| } |
| } |
| |
| if (line.match(/^(\s)*(func|const|type|var)\s/)) { |
| break; |
| } |
| } |
| return ret; |
| } |
| |
| // Takes a Go function signature like: |
| // (foo, bar string, baz number) (string, string) |
| // and returns an array of parameter strings: |
| // ["foo", "bar string", "baz string"] |
| // Takes care of balancing parens so to not get confused by signatures like: |
| // (pattern string, handler func(ResponseWriter, *Request)) { |
| export function getParametersAndReturnType(signature: string): { params: string[]; returnType: string } { |
| const params: string[] = []; |
| let parenCount = 0; |
| let lastStart = 1; |
| for (let i = 1; i < signature.length; i++) { |
| switch (signature[i]) { |
| case '(': |
| parenCount++; |
| break; |
| case ')': |
| parenCount--; |
| if (parenCount < 0) { |
| if (i > lastStart) { |
| params.push(signature.substring(lastStart, i)); |
| } |
| return { |
| params, |
| returnType: i < signature.length - 1 ? signature.substr(i + 1) : '' |
| }; |
| } |
| break; |
| case ',': |
| if (parenCount === 0) { |
| params.push(signature.substring(lastStart, i)); |
| lastStart = i + 2; |
| } |
| break; |
| } |
| } |
| return { params: [], returnType: '' }; |
| } |
| |
| export function canonicalizeGOPATHPrefix(filename: string): string { |
| const gopath: string = getCurrentGoPath(); |
| if (!gopath) { |
| return filename; |
| } |
| const workspaces = gopath.split(path.delimiter); |
| const filenameLowercase = filename.toLowerCase(); |
| |
| // In case of multiple workspaces, find current workspace by checking if current file is |
| // under any of the workspaces in $GOPATH |
| let currentWorkspace: string = null; |
| for (const workspace of workspaces) { |
| // In case of nested workspaces, (example: both /Users/me and /Users/me/a/b/c are in $GOPATH) |
| // both parent & child workspace in the nested workspaces pair can make it inside the above if block |
| // Therefore, the below check will take longer (more specific to current file) of the two |
| if ( |
| filenameLowercase.substring(0, workspace.length) === workspace.toLowerCase() && |
| (!currentWorkspace || workspace.length > currentWorkspace.length) |
| ) { |
| currentWorkspace = workspace; |
| } |
| } |
| |
| if (!currentWorkspace) { |
| return filename; |
| } |
| return currentWorkspace + filename.slice(currentWorkspace.length); |
| } |
| |
| /** |
| * Gets a numeric hash based on given string. |
| * Returns a number between 0 and 4294967295. |
| */ |
| export function getStringHash(value: string): number { |
| let hash = 5381; |
| let i = value.length; |
| |
| while (i) { |
| hash = (hash * 33) ^ value.charCodeAt(--i); |
| } |
| |
| /* JavaScript does bitwise operations (like XOR, above) on 32-bit signed |
| * integers. Since we want the results to be always positive, convert the |
| * signed int to an unsigned by doing an unsigned bitshift. */ |
| return hash >>> 0; |
| } |
| |
| export function getUserNameHash() { |
| if (userNameHash) { |
| return userNameHash; |
| } |
| try { |
| userNameHash = getStringHash(os.userInfo().username); |
| } catch (error) { |
| userNameHash = 1; |
| } |
| return userNameHash; |
| } |
| |
| /** |
| * Gets version of Go based on the output of the command `go version`. |
| * Returns undefined if go version can't be determined because |
| * go is not available or `go version` fails. |
| */ |
| export async function getGoVersion(goBinPath?: string): Promise<GoVersion | undefined> { |
| // TODO(hyangah): limit the number of concurrent getGoVersion call. |
| // When the extension starts, at least 4 concurrent calls race |
| // and end up calling `go version`. |
| |
| const goRuntimePath = goBinPath ?? getBinPath('go'); |
| |
| const warn = (msg: string) => { |
| outputChannel.appendLine(msg); |
| console.warn(msg); |
| }; |
| |
| if (!goRuntimePath) { |
| warn(`unable to locate "go" binary in GOROOT (${getCurrentGoRoot()}) or PATH (${envPath})`); |
| return; |
| } |
| if (cachedGoBinPath === goRuntimePath && cachedGoVersion) { |
| if (cachedGoVersion.isValid()) { |
| return Promise.resolve(cachedGoVersion); |
| } |
| warn(`cached Go version (${JSON.stringify(cachedGoVersion)}) is invalid, recomputing`); |
| } |
| const docUri = vscode.window.activeTextEditor?.document.uri; |
| const cwd = getWorkspaceFolderPath(docUri && docUri.fsPath.endsWith('.go') ? docUri : undefined); |
| |
| let goVersion: GoVersion; |
| try { |
| const env = toolExecutionEnvironment(); |
| const execFile = util.promisify(cp.execFile); |
| const { stdout, stderr } = await execFile(goRuntimePath, ['version'], { env, cwd }); |
| if (stderr) { |
| warn(`failed to run "${goRuntimePath} version": stdout: ${stdout}, stderr: ${stderr}`); |
| return; |
| } |
| goVersion = new GoVersion(goRuntimePath, stdout); |
| } catch (err) { |
| warn(`failed to run "${goRuntimePath} version": ${err} cwd: ${cwd}`); |
| return; |
| } |
| if (!goBinPath) { |
| // if getGoVersion was called with a given goBinPath, don't cache the result. |
| cachedGoBinPath = goRuntimePath; |
| cachedGoVersion = goVersion; |
| if (!cachedGoVersion.isValid()) { |
| warn(`unable to determine version from the output of "${goRuntimePath} version": "${goVersion.svString}"`); |
| } |
| } |
| return goVersion; |
| } |
| |
| /** |
| * Returns the output of `go env` from the specified directory. |
| * Throws an error if the command fails. |
| */ |
| export async function getGoEnv(cwd?: string): Promise<string> { |
| const goRuntime = getBinPath('go'); |
| const execFile = util.promisify(cp.execFile); |
| const opts = { cwd, env: toolExecutionEnvironment() }; |
| const { stdout, stderr } = await execFile(goRuntime, ['env'], opts); |
| if (stderr) { |
| throw new Error(`failed to run 'go env': ${stderr}`); |
| } |
| return stdout; |
| } |
| |
| /** |
| * Returns boolean denoting if current version of Go supports vendoring |
| */ |
| export async function isVendorSupported(): Promise<boolean> { |
| if (vendorSupport !== null) { |
| return Promise.resolve(vendorSupport); |
| } |
| const goVersion = await getGoVersion(); |
| if (!goVersion.sv) { |
| return process.env['GO15VENDOREXPERIMENT'] === '0' ? false : true; |
| } |
| switch (goVersion.sv.major) { |
| case 0: |
| vendorSupport = false; |
| break; |
| case 1: |
| vendorSupport = |
| goVersion.sv.minor > 6 || |
| ((goVersion.sv.minor === 5 || goVersion.sv.minor === 6) && process.env['GO15VENDOREXPERIMENT'] === '1') |
| ? true |
| : false; |
| break; |
| default: |
| vendorSupport = true; |
| break; |
| } |
| return vendorSupport; |
| } |
| |
| /** |
| * Returns boolean indicating if GOPATH is set or not |
| * If not set, then prompts user to do set GOPATH |
| */ |
| export function isGoPathSet(): boolean { |
| if (!getCurrentGoPath()) { |
| // TODO(hyangah): is it still possible after go1.8? (https://golang.org/doc/go1.8#gopath) |
| vscode.window |
| .showInformationMessage( |
| 'Set GOPATH environment variable and restart VS Code or set GOPATH in Workspace settings', |
| 'Set GOPATH in Workspace Settings' |
| ) |
| .then((selected) => { |
| if (selected === 'Set GOPATH in Workspace Settings') { |
| vscode.commands.executeCommand('workbench.action.openWorkspaceSettings'); |
| } |
| }); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| export function isPositionInString(document: vscode.TextDocument, position: vscode.Position): boolean { |
| const lineText = document.lineAt(position.line).text; |
| const lineTillCurrentPosition = lineText.substr(0, position.character); |
| |
| // Count the number of double quotes in the line till current position. Ignore escaped double quotes |
| let doubleQuotesCnt = (lineTillCurrentPosition.match(/"/g) || []).length; |
| const escapedDoubleQuotesCnt = (lineTillCurrentPosition.match(/\\"/g) || []).length; |
| |
| doubleQuotesCnt -= escapedDoubleQuotesCnt; |
| return doubleQuotesCnt % 2 === 1; |
| } |
| |
| export function getToolsGopath(useCache = true): string { |
| if (!useCache || !toolsGopath) { |
| toolsGopath = resolveToolsGopath(); |
| } |
| return toolsGopath; |
| } |
| |
| function resolveToolsGopath(): string { |
| let toolsGopathForWorkspace = substituteEnv(getGoConfig()['toolsGopath'] || ''); |
| |
| // In case of single root |
| if (!vscode.workspace.workspaceFolders || vscode.workspace.workspaceFolders.length <= 1) { |
| return resolvePath(toolsGopathForWorkspace); |
| } |
| |
| // In case of multi-root, resolve ~ and ${workspaceFolder} |
| if (toolsGopathForWorkspace.startsWith('~')) { |
| toolsGopathForWorkspace = path.join(os.homedir(), toolsGopathForWorkspace.substr(1)); |
| } |
| if ( |
| toolsGopathForWorkspace && |
| toolsGopathForWorkspace.trim() && |
| !/\${workspaceFolder}|\${workspaceRoot}/.test(toolsGopathForWorkspace) |
| ) { |
| return toolsGopathForWorkspace; |
| } |
| |
| if (DefaultConfig().workspaceIsTrusted() === false) { |
| return toolsGopathForWorkspace; |
| } |
| |
| // If any of the folders in multi root have toolsGopath set and the workspace is trusted, use it. |
| for (const folder of vscode.workspace.workspaceFolders) { |
| let toolsGopathFromConfig = <string>getGoConfig(folder.uri).inspect('toolsGopath').workspaceFolderValue; |
| toolsGopathFromConfig = resolvePath(toolsGopathFromConfig, folder.uri.fsPath); |
| if (toolsGopathFromConfig) { |
| return toolsGopathFromConfig; |
| } |
| } |
| return toolsGopathForWorkspace; |
| } |
| |
| // getBinPath returns the path to the tool. |
| export function getBinPath(tool: string, useCache = true): string { |
| const r = getBinPathWithExplanation(tool, useCache); |
| return r.binPath; |
| } |
| |
| // getBinPathWithExplanation returns the path to the tool, and the explanation on why |
| // the path was chosen. See getBinPathWithPreferredGopathGorootWithExplanation for details. |
| export function getBinPathWithExplanation(tool: string, useCache = true): { binPath: string; why?: string } { |
| const cfg = getGoConfig(); |
| const alternateTools: { [key: string]: string } = cfg.get('alternateTools'); |
| const alternateToolPath: string = alternateTools[tool]; |
| |
| const gorootInSetting = resolvePath(cfg.get('goroot')); |
| |
| let selectedGoPath: string | undefined; |
| if (tool === 'go' && !gorootInSetting) { |
| selectedGoPath = getFromWorkspaceState('selectedGo')?.binpath; |
| } |
| |
| return getBinPathWithPreferredGopathGorootWithExplanation( |
| tool, |
| tool === 'go' ? [] : [getToolsGopath(), getCurrentGoPath()], |
| tool === 'go' ? gorootInSetting : undefined, |
| selectedGoPath ?? resolvePath(alternateToolPath), |
| useCache |
| ); |
| } |
| |
| export function getFileArchive(document: vscode.TextDocument): string { |
| const fileContents = document.getText(); |
| return document.fileName + '\n' + Buffer.byteLength(fileContents, 'utf8') + '\n' + fileContents; |
| } |
| |
| export function substituteEnv(input: string): string { |
| return input.replace(/\${env:([^}]+)}/g, (match, capture) => { |
| return process.env[capture.trim()] || ''; |
| }); |
| } |
| |
| let currentGopath = ''; |
| export function getCurrentGoPath(workspaceUri?: vscode.Uri): string { |
| const activeEditorUri = vscode.window.activeTextEditor && vscode.window.activeTextEditor.document.uri; |
| const currentFilePath = fixDriveCasingInWindows(activeEditorUri && activeEditorUri.fsPath); |
| const currentRoot = (workspaceUri && workspaceUri.fsPath) || getWorkspaceFolderPath(activeEditorUri); |
| const config = getGoConfig(workspaceUri || activeEditorUri); |
| |
| // Infer the GOPATH from the current root or the path of the file opened in current editor |
| // Last resort: Check for the common case where GOPATH itself is opened directly in VS Code |
| let inferredGopath: string; |
| if (config['inferGopath'] === true) { |
| inferredGopath = getInferredGopath(currentRoot) || getInferredGopath(currentFilePath); |
| if (!inferredGopath) { |
| try { |
| if (fs.statSync(path.join(currentRoot, 'src')).isDirectory()) { |
| inferredGopath = currentRoot; |
| } |
| } catch (e) { |
| // No op |
| } |
| } |
| if (inferredGopath && process.env['GOPATH'] && inferredGopath !== process.env['GOPATH']) { |
| inferredGopath += path.delimiter + process.env['GOPATH']; |
| } |
| } |
| |
| const configGopath = config['gopath'] ? resolvePath(substituteEnv(config['gopath']), currentRoot) : ''; |
| currentGopath = inferredGopath ? inferredGopath : configGopath || process.env['GOPATH']; |
| return currentGopath; |
| } |
| |
| export function getModuleCache(): string { |
| if (process.env['GOMODCACHE']) { |
| return process.env['GOMODCACHE']; |
| } |
| if (currentGopath) { |
| return path.join(currentGopath.split(path.delimiter)[0], 'pkg', 'mod'); |
| } |
| } |
| |
| export function getExtensionCommands(): any[] { |
| const pkgJSON = vscode.extensions.getExtension(extensionId).packageJSON; |
| if (!pkgJSON.contributes || !pkgJSON.contributes.commands) { |
| return; |
| } |
| const extensionCommands: any[] = vscode.extensions |
| .getExtension(extensionId) |
| .packageJSON.contributes.commands.filter((x: any) => x.command !== 'go.show.commands'); |
| return extensionCommands; |
| } |
| |
| export class LineBuffer { |
| private buf = ''; |
| private lineListeners: { (line: string): void }[] = []; |
| private lastListeners: { (last: string): void }[] = []; |
| |
| public append(chunk: string) { |
| this.buf += chunk; |
| for (;;) { |
| const idx = this.buf.indexOf('\n'); |
| if (idx === -1) { |
| break; |
| } |
| |
| this.fireLine(this.buf.substring(0, idx)); |
| this.buf = this.buf.substring(idx + 1); |
| } |
| } |
| |
| public done() { |
| this.fireDone(this.buf !== '' ? this.buf : null); |
| } |
| |
| public onLine(listener: (line: string) => void) { |
| this.lineListeners.push(listener); |
| } |
| |
| public onDone(listener: (last: string) => void) { |
| this.lastListeners.push(listener); |
| } |
| |
| private fireLine(line: string) { |
| this.lineListeners.forEach((listener) => listener(line)); |
| } |
| |
| private fireDone(last: string) { |
| this.lastListeners.forEach((listener) => listener(last)); |
| } |
| } |
| |
| export function timeout(millis: number): Promise<void> { |
| return new Promise<void>((resolve) => { |
| setTimeout(() => resolve(), millis); |
| }); |
| } |
| |
| /** |
| * Expands ~ to homedir in non-Windows platform and resolves ${workspaceFolder} or ${workspaceRoot} |
| */ |
| export function resolvePath(inputPath: string, workspaceFolder?: string): string { |
| if (!inputPath || !inputPath.trim()) { |
| return inputPath; |
| } |
| |
| if (!workspaceFolder && vscode.workspace.workspaceFolders) { |
| workspaceFolder = getWorkspaceFolderPath( |
| vscode.window.activeTextEditor && vscode.window.activeTextEditor.document.uri |
| ); |
| } |
| |
| if (workspaceFolder) { |
| inputPath = inputPath.replace(/\${workspaceFolder}|\${workspaceRoot}/g, workspaceFolder); |
| } |
| return resolveHomeDir(inputPath); |
| } |
| |
| /** |
| * Returns the import path in a passed in string. |
| * @param text The string to search for an import path |
| */ |
| export function getImportPath(text: string): string { |
| // Catch cases like `import alias "importpath"` and `import "importpath"` |
| const singleLineImportMatches = text.match(/^\s*import\s+([a-z,A-Z,_,.]\w*\s+)?"([^"]+)"/); |
| if (singleLineImportMatches) { |
| return singleLineImportMatches[2]; |
| } |
| |
| // Catch cases like `alias "importpath"` and "importpath" |
| const groupImportMatches = text.match(/^\s*([a-z,A-Z,_,.]\w*\s+)?"([^"]+)"/); |
| if (groupImportMatches) { |
| return groupImportMatches[2]; |
| } |
| |
| return ''; |
| } |
| |
| // TODO: Add unit tests for the below |
| |
| /** |
| * Guess the package name based on parent directory name of the given file |
| * |
| * Cases: |
| * - dir 'go-i18n' -> 'i18n' |
| * - dir 'go-spew' -> 'spew' |
| * - dir 'kingpin' -> 'kingpin' |
| * - dir 'go-expand-tilde' -> 'tilde' |
| * - dir 'gax-go' -> 'gax' |
| * - dir 'go-difflib' -> 'difflib' |
| * - dir 'jwt-go' -> 'jwt' |
| * - dir 'go-radix' -> 'radix' |
| * |
| * @param {string} filePath. |
| */ |
| export function guessPackageNameFromFile(filePath: string): Promise<string[]> { |
| return new Promise((resolve, reject) => { |
| const goFilename = path.basename(filePath); |
| if (goFilename === 'main.go') { |
| return resolve(['main']); |
| } |
| |
| const directoryPath = path.dirname(filePath); |
| const dirName = path.basename(directoryPath); |
| let segments = dirName.split(/[.-]/); |
| segments = segments.filter((val) => val !== 'go'); |
| |
| if (segments.length === 0 || !/[a-zA-Z_]\w*/.test(segments[segments.length - 1])) { |
| return reject(); |
| } |
| |
| const proposedPkgName = segments[segments.length - 1]; |
| |
| fs.stat(path.join(directoryPath, 'main.go'), (err, stats) => { |
| if (stats && stats.isFile()) { |
| return resolve(['main']); |
| } |
| |
| if (goFilename.endsWith('_test.go')) { |
| return resolve([proposedPkgName, proposedPkgName + '_test']); |
| } |
| |
| return resolve([proposedPkgName]); |
| }); |
| }); |
| } |
| |
| export interface ICheckResult { |
| file: string; |
| line: number; |
| col: number; |
| msg: string; |
| severity: string; |
| } |
| |
| /** |
| * Runs given Go tool and returns errors/warnings that can be fed to the Problems Matcher |
| * @param args Arguments to be passed while running given tool |
| * @param cwd cwd that will passed in the env object while running given tool |
| * @param severity error or warning |
| * @param useStdErr If true, the stderr of the output of the given tool will be used, else stdout will be used |
| * @param toolName The name of the Go tool to run. If none is provided, the go runtime itself is used |
| * @param printUnexpectedOutput If true, then output that doesnt match expected format is printed to the output channel |
| */ |
| export function runTool( |
| args: string[], |
| cwd: string, |
| severity: string, |
| useStdErr: boolean, |
| toolName: string, |
| env: any, |
| printUnexpectedOutput: boolean, |
| token?: vscode.CancellationToken |
| ): Promise<ICheckResult[]> { |
| let cmd: string; |
| if (toolName) { |
| cmd = getBinPath(toolName); |
| } else { |
| const goRuntimePath = getBinPath('go'); |
| if (!goRuntimePath) { |
| return Promise.reject(new Error('Cannot find "go" binary. Update PATH or GOROOT appropriately')); |
| } |
| cmd = goRuntimePath; |
| } |
| |
| let p: cp.ChildProcess; |
| if (token) { |
| token.onCancellationRequested(() => { |
| if (p) { |
| killProcessTree(p); |
| } |
| }); |
| } |
| cwd = fixDriveCasingInWindows(cwd); |
| return new Promise((resolve, reject) => { |
| p = cp.execFile(cmd, args, { env, cwd }, (err, stdout, stderr) => { |
| try { |
| if (err && (<any>err).code === 'ENOENT') { |
| // Since the tool is run on save which can be frequent |
| // we avoid sending explicit notification if tool is missing |
| console.log(`Cannot find ${toolName ? toolName : 'go'}`); |
| return resolve([]); |
| } |
| if (err && stderr && !useStdErr) { |
| outputChannel.appendLine(['Error while running tool:', cmd, ...args].join(' ')); |
| outputChannel.appendLine(stderr); |
| return resolve([]); |
| } |
| const lines = (useStdErr ? stderr : stdout).toString().split('\n'); |
| outputChannel.appendLine([cwd + '>Finished running tool:', cmd, ...args].join(' ')); |
| |
| const ret: ICheckResult[] = []; |
| let unexpectedOutput = false; |
| let atLeastSingleMatch = false; |
| for (const l of lines) { |
| if (l[0] === '\t' && ret.length > 0) { |
| ret[ret.length - 1].msg += '\n' + l; |
| continue; |
| } |
| const match = /^([^:]*: )?((.:)?[^:]*):(\d+)(:(\d+)?)?:(?:\w+:)? (.*)$/.exec(l); |
| if (!match) { |
| if (printUnexpectedOutput && useStdErr && stderr) { |
| unexpectedOutput = true; |
| } |
| continue; |
| } |
| atLeastSingleMatch = true; |
| const [, , file, , lineStr, , colStr, msg] = match; |
| const line = +lineStr; |
| const col = colStr ? +colStr : undefined; |
| |
| // Building skips vendor folders, |
| // But vet and lint take in directories and not import paths, so no way to skip them |
| // So prune out the results from vendor folders here. |
| if ( |
| !path.isAbsolute(file) && |
| (file.startsWith(`vendor${path.sep}`) || file.indexOf(`${path.sep}vendor${path.sep}`) > -1) |
| ) { |
| continue; |
| } |
| |
| const filePath = path.resolve(cwd, file); |
| ret.push({ file: filePath, line, col, msg, severity }); |
| outputChannel.appendLine(`${filePath}:${line}:${col ?? ''} ${msg}`); |
| } |
| if (!atLeastSingleMatch && unexpectedOutput && vscode.window.activeTextEditor) { |
| outputChannel.appendLine(stderr); |
| if (err) { |
| ret.push({ |
| file: vscode.window.activeTextEditor.document.fileName, |
| line: 1, |
| col: 1, |
| msg: stderr, |
| severity: 'error' |
| }); |
| } |
| } |
| outputChannel.appendLine(''); |
| resolve(ret); |
| } catch (e) { |
| reject(e); |
| } |
| }); |
| }); |
| } |
| |
| export function handleDiagnosticErrors( |
| document: vscode.TextDocument, |
| errors: ICheckResult[], |
| diagnosticCollection: vscode.DiagnosticCollection, |
| diagnosticSource?: string |
| ) { |
| diagnosticCollection.clear(); |
| |
| const diagnosticMap: Map<string, vscode.Diagnostic[]> = new Map(); |
| |
| const textDocumentMap: Map<string, vscode.TextDocument> = new Map(); |
| if (document) { |
| textDocumentMap.set(document.uri.toString(), document); |
| } |
| // Also add other open .go files known to vscode for fast lookup. |
| vscode.workspace.textDocuments.forEach((t) => { |
| const fileName = t.uri.toString(); |
| if (!fileName.endsWith('.go')) { |
| return; |
| } |
| textDocumentMap.set(fileName, t); |
| }); |
| |
| errors.forEach((error) => { |
| const canonicalFile = vscode.Uri.file(error.file).toString(); |
| let startColumn = error.col ? error.col - 1 : 0; |
| let endColumn = startColumn + 1; |
| // Some tools output only the line number or the start position. |
| // If the file content is available, adjust the diagnostic range so |
| // the squiggly underline for the error message is more visible. |
| const doc = textDocumentMap.get(canonicalFile); |
| if (doc) { |
| const tempRange = new vscode.Range( |
| error.line - 1, |
| 0, |
| error.line - 1, |
| doc.lineAt(error.line - 1).range.end.character + 1 // end of the line |
| ); |
| const text = doc.getText(tempRange); |
| const [, leading, trailing] = /^(\s*).*(\s*)$/.exec(text); |
| if (!error.col) { |
| startColumn = leading.length; // beginning of the non-white space. |
| } else { |
| startColumn = error.col - 1; // range is 0-indexed |
| } |
| endColumn = text.length - trailing.length; |
| } |
| const range = new vscode.Range(error.line - 1, startColumn, error.line - 1, endColumn); |
| const severity = mapSeverityToVSCodeSeverity(error.severity); |
| const diagnostic = new vscode.Diagnostic(range, error.msg, severity); |
| // vscode uses source for deduping diagnostics. |
| diagnostic.source = diagnosticSource || diagnosticCollection.name; |
| let diagnostics = diagnosticMap.get(canonicalFile); |
| if (!diagnostics) { |
| diagnostics = []; |
| } |
| diagnostics.push(diagnostic); |
| diagnosticMap.set(canonicalFile, diagnostics); |
| }); |
| |
| diagnosticMap.forEach((newDiagnostics, file) => { |
| const fileUri = vscode.Uri.parse(file); |
| |
| if (diagnosticCollection === buildDiagnosticCollection) { |
| // If there are lint/vet warnings on current file, remove the ones co-inciding with the new build errors |
| removeDuplicateDiagnostics(lintDiagnosticCollection, fileUri, newDiagnostics); |
| removeDuplicateDiagnostics(vetDiagnosticCollection, fileUri, newDiagnostics); |
| } else if (buildDiagnosticCollection && buildDiagnosticCollection.has(fileUri)) { |
| // If there are build errors on current file, ignore the new lint/vet warnings co-inciding with them |
| newDiagnostics = deDupeDiagnostics(buildDiagnosticCollection.get(fileUri).slice(), newDiagnostics); |
| } |
| // If there are errors from the language client that are on the current file, ignore the warnings co-inciding |
| // with them. |
| if (languageClient && languageClient.diagnostics?.has(fileUri)) { |
| newDiagnostics = deDupeDiagnostics(languageClient.diagnostics.get(fileUri).slice(), newDiagnostics); |
| } |
| diagnosticCollection.set(fileUri, newDiagnostics); |
| }); |
| } |
| |
| /** |
| * Removes any diagnostics in collection, where there is a diagnostic in |
| * newDiagnostics on the same line in fileUri. |
| */ |
| export function removeDuplicateDiagnostics( |
| collection: vscode.DiagnosticCollection, |
| fileUri: vscode.Uri, |
| newDiagnostics: vscode.Diagnostic[] |
| ) { |
| if (collection && collection.has(fileUri)) { |
| collection.set(fileUri, deDupeDiagnostics(newDiagnostics, collection.get(fileUri).slice())); |
| } |
| } |
| |
| /** |
| * Removes any diagnostics in otherDiagnostics, where there is a diagnostic in |
| * buildDiagnostics on the same line. |
| */ |
| function deDupeDiagnostics( |
| buildDiagnostics: vscode.Diagnostic[], |
| otherDiagnostics: vscode.Diagnostic[] |
| ): vscode.Diagnostic[] { |
| const buildDiagnosticsLines = buildDiagnostics.map((x) => x.range.start.line); |
| return otherDiagnostics.filter((x) => buildDiagnosticsLines.indexOf(x.range.start.line) === -1); |
| } |
| |
| function mapSeverityToVSCodeSeverity(sev: string): vscode.DiagnosticSeverity { |
| switch (sev) { |
| case 'error': |
| return vscode.DiagnosticSeverity.Error; |
| case 'warning': |
| return vscode.DiagnosticSeverity.Warning; |
| default: |
| return vscode.DiagnosticSeverity.Error; |
| } |
| } |
| |
| export function getWorkspaceFolderPath(fileUri?: vscode.Uri): string | undefined { |
| if (fileUri) { |
| const workspace = vscode.workspace.getWorkspaceFolder(fileUri); |
| if (workspace) { |
| return fixDriveCasingInWindows(workspace.uri.fsPath); |
| } |
| } |
| |
| // fall back to the first workspace |
| const folders = vscode.workspace.workspaceFolders; |
| if (folders && folders.length) { |
| return fixDriveCasingInWindows(folders[0].uri.fsPath); |
| } |
| return undefined; |
| } |
| |
| export function makeMemoizedByteOffsetConverter(buffer: Buffer): (byteOffset: number) => number { |
| const defaultValue = new Node<number, number>(0, 0); // 0 bytes will always be 0 characters |
| const memo = new NearestNeighborDict(defaultValue, NearestNeighborDict.NUMERIC_DISTANCE_FUNCTION); |
| return (byteOffset: number) => { |
| const nearest = memo.getNearest(byteOffset); |
| const byteDelta = byteOffset - nearest.key; |
| |
| if (byteDelta === 0) { |
| return nearest.value; |
| } |
| |
| let charDelta: number; |
| if (byteDelta > 0) { |
| charDelta = buffer.toString('utf8', nearest.key, byteOffset).length; |
| } else { |
| charDelta = -buffer.toString('utf8', byteOffset, nearest.key).length; |
| } |
| |
| memo.insert(byteOffset, nearest.value + charDelta); |
| return nearest.value + charDelta; |
| }; |
| } |
| |
| export function rmdirRecursive(dir: string) { |
| if (fs.existsSync(dir)) { |
| fs.readdirSync(dir).forEach((file) => { |
| const relPath = path.join(dir, file); |
| if (fs.lstatSync(relPath).isDirectory()) { |
| rmdirRecursive(relPath); |
| } else { |
| try { |
| fs.unlinkSync(relPath); |
| } catch (err) { |
| console.log(`failed to remove ${relPath}: ${err}`); |
| } |
| } |
| }); |
| fs.rmdirSync(dir); |
| } |
| } |
| |
| let tmpDir: string; |
| |
| /** |
| * Returns file path for given name in temp dir |
| * @param name Name of the file |
| */ |
| export function getTempFilePath(name: string): string { |
| if (!tmpDir) { |
| tmpDir = fs.mkdtempSync(os.tmpdir() + path.sep + 'vscode-go'); |
| } |
| |
| if (!fs.existsSync(tmpDir)) { |
| fs.mkdirSync(tmpDir); |
| } |
| |
| return path.normalize(path.join(tmpDir, name)); |
| } |
| |
| export function cleanupTempDir() { |
| if (tmpDir) { |
| rmdirRecursive(tmpDir); |
| } |
| tmpDir = undefined; |
| } |
| |
| /** |
| * Runs `go doc` to get documentation for given symbol |
| * @param cwd The cwd where the go doc process will be run |
| * @param packagePath Either the absolute path or import path of the package. |
| * @param symbol Symbol for which docs need to be found |
| * @param token Cancellation token |
| */ |
| export function runGodoc( |
| cwd: string, |
| packagePath: string, |
| receiver: string, |
| symbol: string, |
| token: vscode.CancellationToken |
| ) { |
| if (!packagePath) { |
| return Promise.reject(new Error('Package Path not provided')); |
| } |
| if (!symbol) { |
| return Promise.reject(new Error('Symbol not provided')); |
| } |
| |
| const goRuntimePath = getBinPath('go'); |
| if (!goRuntimePath) { |
| return Promise.reject(new Error('Cannot find "go" binary. Update PATH or GOROOT appropriately')); |
| } |
| |
| const getCurrentPackagePromise = path.isAbsolute(packagePath) |
| ? getCurrentPackage(packagePath) |
| : Promise.resolve(packagePath); |
| return getCurrentPackagePromise.then((packageImportPath) => { |
| return new Promise<string>((resolve, reject) => { |
| if (receiver) { |
| receiver = receiver.replace(/^\*/, ''); |
| symbol = receiver + '.' + symbol; |
| } |
| |
| const env = toolExecutionEnvironment(); |
| const args = ['doc', '-c', '-cmd', '-u', packageImportPath, symbol]; |
| const p = cp.execFile(goRuntimePath, args, { env, cwd }, (err, stdout, stderr) => { |
| if (err) { |
| return reject(err.message || stderr); |
| } |
| let doc = ''; |
| const godocLines = stdout.split('\n'); |
| if (!godocLines.length) { |
| return resolve(doc); |
| } |
| |
| // Recent versions of Go have started to include the package statement |
| // tht we dont need. |
| if (godocLines[0].startsWith('package ')) { |
| godocLines.splice(0, 1); |
| if (!godocLines[0].trim()) { |
| godocLines.splice(0, 1); |
| } |
| } |
| |
| // Skip trailing empty lines |
| let lastLine = godocLines.length - 1; |
| for (; lastLine > 1; lastLine--) { |
| if (godocLines[lastLine].trim()) { |
| break; |
| } |
| } |
| |
| for (let i = 1; i <= lastLine; i++) { |
| if (godocLines[i].startsWith(' ')) { |
| doc += godocLines[i].substring(4) + '\n'; |
| } else if (!godocLines[i].trim()) { |
| doc += '\n'; |
| } |
| } |
| return resolve(doc); |
| }); |
| |
| if (token) { |
| token.onCancellationRequested(() => { |
| killProcessTree(p); |
| }); |
| } |
| }); |
| }); |
| } |
| |
| /** |
| * Returns a boolean whether the current position lies within a comment or not |
| * @param document |
| * @param position |
| */ |
| export function isPositionInComment(document: vscode.TextDocument, position: vscode.Position): boolean { |
| const lineText = document.lineAt(position.line).text; |
| const commentIndex = lineText.indexOf('//'); |
| |
| if (commentIndex >= 0 && position.character > commentIndex) { |
| const commentPosition = new vscode.Position(position.line, commentIndex); |
| const isCommentInString = isPositionInString(document, commentPosition); |
| |
| return !isCommentInString; |
| } |
| return false; |
| } |