blob: 9cf93fb8e87bf10c603b2b0d3a7d7a0afa62aca9 [file] [log] [blame]
/* 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.
*--------------------------------------------------------*/
'use strict';
import cp = require('child_process');
import moment = require('moment');
import path = require('path');
import semver = require('semver');
import util = require('util');
import { getFormatTool, usingCustomFormatTool } from './goFormat';
import { goLiveErrorsEnabled } from './goLiveErrors';
import { getBinPath, GoVersion } from './util';
export interface Tool {
name: string;
importPath: string;
modulePath: string;
isImportant: boolean;
replacedByGopls?: boolean;
description: string;
// If true, consider prerelease version in preview mode
// (nightly & dev)
usePrereleaseInPreviewMode?: boolean;
// If set, this string will be used when installing the tool
// instead of the default 'latest'. It can be used when
// we need to pin a tool version (`deadbeaf`) or to use
// a dev version available in a branch (e.g. `master`).
defaultVersion?: string;
// latestVersion and latestVersionTimestamp are hardcoded default values
// for the last known version of the given tool. We also hardcode values
// for the latest known pre-release of the tool for the Nightly extension.
latestVersion?: semver.SemVer;
latestVersionTimestamp?: moment.Moment;
latestPrereleaseVersion?: semver.SemVer;
latestPrereleaseVersionTimestamp?: moment.Moment;
// minimumGoVersion and maximumGoVersion set the range for the versions of
// Go with which this tool can be used.
minimumGoVersion?: semver.SemVer;
maximumGoVersion?: semver.SemVer;
// close performs any shutdown tasks that a tool must execute before a new
// version is installed. It returns a string containing an error message on
// failure.
close?: (env: NodeJS.Dict<string>) => Promise<string>;
}
/**
* ToolAtVersion is a Tool at a specific version.
* Lack of version implies the latest version.
*/
export interface ToolAtVersion extends Tool {
version?: semver.SemVer;
}
/**
* Returns the import path for a given tool, at a given Go version.
* @param tool Object of type `Tool` for the Go tool.
* @param goVersion The current Go version.
*/
export function getImportPath(tool: Tool, goVersion: GoVersion): string {
// For older versions of Go, install the older version of gocode.
if (tool.name === 'gocode' && goVersion.lt('1.10')) {
return 'github.com/nsf/gocode';
}
return tool.importPath;
}
export function getImportPathWithVersion(
tool: Tool,
version: semver.SemVer | string | undefined,
goVersion: GoVersion
): string {
const importPath = getImportPath(tool, goVersion);
if (version) {
if (version instanceof semver.SemVer) {
return importPath + '@v' + version;
} else {
return importPath + '@' + version;
}
}
return importPath;
}
export function containsTool(tools: Tool[], tool: Tool): boolean {
return tools.indexOf(tool) > -1;
}
export function containsString(tools: Tool[], toolName: string): boolean {
return tools.some((tool) => tool.name === toolName);
}
export function getTool(name: string): Tool {
return allToolsInformation[name];
}
export function getToolAtVersion(name: string, version?: semver.SemVer): ToolAtVersion {
return { ...allToolsInformation[name], version };
}
// hasModSuffix returns true if the given tool has a different, module-specific
// name to avoid conflicts.
export function hasModSuffix(tool: Tool): boolean {
return tool.name.endsWith('-gomod');
}
export function isGocode(tool: Tool): boolean {
return tool.name === 'gocode' || tool.name === 'gocode-gomod';
}
export function getConfiguredTools(
goVersion: GoVersion,
goConfig: { [key: string]: any },
goplsConfig: { [key: string]: any }
): Tool[] {
// If language server is enabled, don't suggest tools that are replaced by gopls.
// TODO(github.com/golang/vscode-go/issues/388): decide what to do when
// the go version is no longer supported by gopls while the legacy tools are
// no longer working (or we remove the legacy language feature providers completely).
const useLanguageServer = goConfig['useLanguageServer'] && goVersion.gt('1.11');
const tools: Tool[] = [];
function maybeAddTool(name: string) {
const tool = allToolsInformation[name];
if (tool) {
if (!useLanguageServer || !tool.replacedByGopls) {
tools.push(tool);
}
}
}
// Start with default tools that should always be installed.
for (const name of [
'gocode',
'gopkgs',
'go-outline',
'go-symbols',
'guru',
'gorename',
'gotests',
'gomodifytags',
'impl',
'fillstruct',
'goplay',
'godoctor'
]) {
maybeAddTool(name);
}
// Check if the system supports dlv, i.e. is 64-bit.
// There doesn't seem to be a good way to check if the mips and s390
// families are 64-bit, so just try to install it and hope for the best.
if (process.arch.match(/^(arm64|mips|mipsel|ppc64|s390|s390x|x64)$/)) {
maybeAddTool('dlv');
maybeAddTool('dlv-dap');
}
// gocode-gomod needed in go 1.11 & higher
if (goVersion.gt('1.10')) {
maybeAddTool('gocode-gomod');
}
// Add the doc/def tool that was chosen by the user.
switch (goConfig['docsTool']) {
case 'godoc':
maybeAddTool('godef');
break;
default:
maybeAddTool(goConfig['docsTool']);
break;
}
// Only add format tools if the language server is disabled and the
// format tool is known to us.
if (goConfig['useLanguageServer'] === false && !usingCustomFormatTool(goConfig)) {
maybeAddTool(getFormatTool(goConfig));
}
// Add the linter that was chosen by the user, but don't add staticcheck
// if it is enabled via gopls.
const goplsStaticheckEnabled = useLanguageServer && goplsStaticcheckEnabled(goConfig, goplsConfig);
if (goConfig['lintTool'] !== 'staticcheck' || !goplsStaticheckEnabled) {
maybeAddTool(goConfig['lintTool']);
}
// Add the language server if the user has chosen to do so.
// Even though we arranged this to run after the first attempt to start gopls
// this is still useful if we've fail to start gopls.
if (useLanguageServer) {
maybeAddTool('gopls');
}
if (goLiveErrorsEnabled()) {
maybeAddTool('gotype-live');
}
return tools;
}
export function goplsStaticcheckEnabled(
goConfig: { [key: string]: any },
goplsConfig: { [key: string]: any }
): boolean {
if (goConfig['useLanguageServer'] !== true || goplsConfig['ui.diagnostic.staticcheck'] !== true) {
return false;
}
const features = goConfig['languageServerExperimentalFeatures'];
return !features || features['diagnostics'] === true;
}
export const allToolsInformation: { [key: string]: Tool } = {
'gocode': {
name: 'gocode',
importPath: 'github.com/mdempsky/gocode',
modulePath: 'github.com/mdempsky/gocode',
isImportant: true,
replacedByGopls: true,
description: 'Auto-completion, does not work with modules',
close: async (env: NodeJS.Dict<string>): Promise<string> => {
const toolBinPath = getBinPath('gocode');
if (!path.isAbsolute(toolBinPath)) {
return '';
}
try {
const execFile = util.promisify(cp.execFile);
const { stderr } = await execFile(toolBinPath, ['close'], { env, timeout: 10000 }); // give 10sec.
if (stderr.indexOf("rpc: can't find service Server.") > -1) {
return 'Installing gocode aborted as existing process cannot be closed. Please kill the running process for gocode and try again.';
}
} catch (err) {
// This may fail if gocode isn't already running.
console.log(`gocode close failed: ${err}`);
}
return '';
}
},
'gocode-gomod': {
name: 'gocode-gomod',
importPath: 'github.com/stamblerre/gocode',
modulePath: 'github.com/stamblerre/gocode',
isImportant: true,
replacedByGopls: true,
description: 'Auto-completion, works with modules',
minimumGoVersion: semver.coerce('1.11')
},
'gopkgs': {
name: 'gopkgs',
importPath: 'github.com/uudashr/gopkgs/v2/cmd/gopkgs',
modulePath: 'github.com/uudashr/gopkgs/v2',
replacedByGopls: false, // TODO(github.com/golang/vscode-go/issues/258): disable Add Import command.
isImportant: true,
description: 'Auto-completion of unimported packages & Add Import feature'
},
'go-outline': {
name: 'go-outline',
importPath: 'github.com/ramya-rao-a/go-outline',
modulePath: 'github.com/ramya-rao-a/go-outline',
replacedByGopls: false, // TODO(github.com/golang/vscode-go/issues/1020): replace with Gopls.
isImportant: true,
description: 'Go to symbol in file' // GoDocumentSymbolProvider, used by 'run test' codelens
},
'go-symbols': {
name: 'go-symbols',
importPath: 'github.com/acroca/go-symbols',
modulePath: 'github.com/acroca/go-symbols',
replacedByGopls: true,
isImportant: false,
description: 'Go to symbol in workspace'
},
'guru': {
name: 'guru',
importPath: 'golang.org/x/tools/cmd/guru',
modulePath: 'golang.org/x/tools',
replacedByGopls: true,
isImportant: false,
description: 'Find all references and Go to implementation of symbols'
},
'gorename': {
name: 'gorename',
importPath: 'golang.org/x/tools/cmd/gorename',
modulePath: 'golang.org/x/tools',
replacedByGopls: true,
isImportant: false,
description: 'Rename symbols'
},
'gomodifytags': {
name: 'gomodifytags',
importPath: 'github.com/fatih/gomodifytags',
modulePath: 'github.com/fatih/gomodifytags',
replacedByGopls: false,
isImportant: false,
description: 'Modify tags on structs'
},
'goplay': {
name: 'goplay',
importPath: 'github.com/haya14busa/goplay/cmd/goplay',
modulePath: 'github.com/haya14busa/goplay',
replacedByGopls: false,
isImportant: false,
description: 'The Go playground'
},
'impl': {
name: 'impl',
importPath: 'github.com/josharian/impl',
modulePath: 'github.com/josharian/impl',
replacedByGopls: false,
isImportant: false,
description: 'Stubs for interfaces'
},
'gotype-live': {
name: 'gotype-live',
importPath: 'github.com/tylerb/gotype-live',
modulePath: 'github.com/tylerb/gotype-live',
replacedByGopls: true, // TODO(github.com/golang/vscode-go/issues/1021): recommend users to turn off.
isImportant: false,
description: 'Show errors as you type'
},
'godef': {
name: 'godef',
importPath: 'github.com/rogpeppe/godef',
modulePath: 'github.com/rogpeppe/godef',
replacedByGopls: true,
isImportant: true,
description: 'Go to definition'
},
'gogetdoc': {
name: 'gogetdoc',
importPath: 'github.com/zmb3/gogetdoc',
modulePath: 'github.com/zmb3/gogetdoc',
replacedByGopls: true,
isImportant: true,
description: 'Go to definition & text shown on hover'
},
'gofumports': {
name: 'gofumports',
importPath: 'mvdan.cc/gofumpt/gofumports',
modulePath: 'mvdan.cc/gofumpt',
replacedByGopls: true,
isImportant: false,
description: 'Formatter'
},
'gofumpt': {
name: 'gofumpt',
importPath: 'mvdan.cc/gofumpt',
modulePath: 'mvdan.cc/gofumpt',
replacedByGopls: true,
isImportant: false,
description: 'Formatter'
},
'goimports': {
name: 'goimports',
importPath: 'golang.org/x/tools/cmd/goimports',
modulePath: 'golang.org/x/tools',
replacedByGopls: true,
isImportant: true,
description: 'Formatter'
},
'goreturns': {
name: 'goreturns',
importPath: 'github.com/sqs/goreturns',
modulePath: 'github.com/sqs/goreturns',
replacedByGopls: true,
isImportant: true,
description: 'Formatter'
},
'goformat': {
name: 'goformat',
importPath: 'winterdrache.de/goformat/goformat',
modulePath: 'winterdrache.de/goformat/goformat',
replacedByGopls: true,
isImportant: false,
description: 'Formatter'
},
'gotests': {
name: 'gotests',
importPath: 'github.com/cweill/gotests/gotests',
modulePath: 'github.com/cweill/gotests',
replacedByGopls: false,
isImportant: false,
description: 'Generate unit tests',
minimumGoVersion: semver.coerce('1.9')
},
// TODO(github.com/golang/vscode-go/issues/189): consider disabling lint when gopls is turned on.
'golint': {
name: 'golint',
importPath: 'golang.org/x/lint/golint',
modulePath: 'golang.org/x/lint',
replacedByGopls: false,
isImportant: false,
description: 'Linter',
minimumGoVersion: semver.coerce('1.9')
},
'staticcheck': {
name: 'staticcheck',
importPath: 'honnef.co/go/tools/cmd/staticcheck',
modulePath: 'honnef.co/go/tools',
replacedByGopls: false,
isImportant: true,
description: 'Linter'
},
'golangci-lint': {
name: 'golangci-lint',
importPath: 'github.com/golangci/golangci-lint/cmd/golangci-lint',
modulePath: 'github.com/golangci/golangci-lint',
replacedByGopls: false,
isImportant: true,
description: 'Linter'
},
'revive': {
name: 'revive',
importPath: 'github.com/mgechev/revive',
modulePath: 'github.com/mgechev/revive',
isImportant: true,
description: 'Linter'
},
'gopls': {
name: 'gopls',
importPath: 'golang.org/x/tools/gopls',
modulePath: 'golang.org/x/tools/gopls',
replacedByGopls: false, // lol
isImportant: true,
description: 'Language Server from Google',
usePrereleaseInPreviewMode: true,
minimumGoVersion: semver.coerce('1.12'),
latestVersion: semver.coerce('0.6.8'),
latestVersionTimestamp: moment('2021-03-17', 'YYYY-MM-DD'),
latestPrereleaseVersion: semver.coerce('0.6.8'),
latestPrereleaseVersionTimestamp: moment('2021-03-17', 'YYYY-MM-DD')
},
'dlv': {
name: 'dlv',
importPath: 'github.com/go-delve/delve/cmd/dlv',
modulePath: 'github.com/go-delve/delve',
replacedByGopls: false,
isImportant: true,
description: 'Go debugger (Delve)'
},
'dlv-dap': {
name: 'dlv-dap',
importPath: 'github.com/go-delve/delve/cmd/dlv',
modulePath: 'github.com/go-delve/delve',
replacedByGopls: false,
isImportant: false,
description: 'Go debugger (Delve built for DAP experiment)',
defaultVersion: 'master', // Always build from the master.
minimumGoVersion: semver.coerce('1.14'), // last 3 versions per delve policy
latestVersion: semver.parse('1.6.1-0.20210414072240-747f03788315'),
latestVersionTimestamp: moment('2021-04-14', 'YYYY-MM-DD')
},
'fillstruct': {
name: 'fillstruct',
importPath: 'github.com/davidrjenni/reftools/cmd/fillstruct',
modulePath: 'github.com/davidrjenni/reftools',
replacedByGopls: true,
isImportant: false,
description: 'Fill structs with defaults'
},
'godoctor': {
name: 'godoctor',
importPath: 'github.com/godoctor/godoctor',
modulePath: 'github.com/godoctor/godoctor',
replacedByGopls: true,
isImportant: false,
description: 'Extract to functions and variables'
}
};