diff --git a/extension/src/debugAdapter/goDebug.ts b/extension/src/debugAdapter/goDebug.ts index b85704b063..daa69b3b36 100644 --- a/extension/src/debugAdapter/goDebug.ts +++ b/extension/src/debugAdapter/goDebug.ts @@ -406,7 +406,7 @@ export function normalizeSeparators(filePath: string): string { // casing. // This is a workaround for issue in https://github.com/Microsoft/vscode/issues/9448#issuecomment-244804026 if (filePath.indexOf(':') === 1) { - filePath = filePath.substr(0, 1).toUpperCase() + filePath.substr(1); + filePath = filePath.substring(0, 1).toUpperCase() + filePath.substring(1); } return filePath.replace(/\/|\\/g, '/'); } @@ -634,7 +634,7 @@ export class Delve { if (mode === 'exec' || (mode === 'debug' && !isProgramDirectory)) { dlvArgs.push(program); } else if (currentGOWorkspace && !launchArgs.packagePathToGoModPathMap[dirname]) { - dlvArgs.push(dirname.substr(currentGOWorkspace.length + 1)); + dlvArgs.push(dirname.substring(currentGOWorkspace.length + 1)); } // add user-specified dlv flags first. When duplicate flags are specified, // dlv doesn't mind but accepts the last flag value. @@ -1212,7 +1212,7 @@ export class GoDebugSession extends LoggingDebugSession { } const relativeRemotePath = remotePath - .substr(importPathIndex) + .substring(importPathIndex) .split(this.remotePathSeparator) .join(this.localPathSeparator); const pathToConvertWithLocalSeparator = remotePath @@ -1253,7 +1253,7 @@ export class GoDebugSession extends LoggingDebugSession { const goroot = this.getGOROOT(); const localGoRootImportPath = path.join( goroot, - srcIndex >= 0 ? remotePathWithLocalSeparator.substr(srcIndex) : path.join('src', relativeRemotePath) + srcIndex >= 0 ? remotePathWithLocalSeparator.substring(srcIndex) : path.join('src', relativeRemotePath) ); if (this.fileSystem.existsSync(localGoRootImportPath)) { return localGoRootImportPath; @@ -1281,7 +1281,7 @@ export class GoDebugSession extends LoggingDebugSession { const localGoPathImportPath = path.join( gopath, indexGoModCache >= 0 - ? remotePathWithLocalSeparator.substr(indexGoModCache) + ? remotePathWithLocalSeparator.substring(indexGoModCache) : path.join('pkg', 'mod', relativeRemotePath) ); if (this.fileSystem.existsSync(localGoPathImportPath)) { @@ -1340,7 +1340,7 @@ export class GoDebugSession extends LoggingDebugSession { const index = pathToConvert.indexOf(`${this.remotePathSeparator}src${this.remotePathSeparator}`); const goroot = this.getGOROOT(); if (goroot && index > 0) { - return path.join(goroot, pathToConvert.substr(index)); + return path.join(goroot, pathToConvert.substring(index)); } const indexGoModCache = pathToConvert.indexOf( @@ -1352,7 +1352,7 @@ export class GoDebugSession extends LoggingDebugSession { return path.join( gopath, pathToConvert - .substr(indexGoModCache) + .substring(indexGoModCache) .split(this.remotePathSeparator ?? '') .join(this.localPathSeparator) ); @@ -1647,7 +1647,7 @@ export class GoDebugSession extends LoggingDebugSession { : (listPkgVarsOut).Variables; let initdoneIndex = -1; for (let i = 0; i < globals.length; i++) { - globals[i].name = globals[i].name.substr(packageName.length + 1); + globals[i].name = globals[i].name.substring(packageName.length + 1); if (initdoneIndex === -1 && globals[i].name === this.initdone) { initdoneIndex = i; } @@ -2308,7 +2308,7 @@ export class GoDebugSession extends LoggingDebugSession { return resolve(); } const spaceIndex = stdout.indexOf(' '); - const result = stdout.substr(0, spaceIndex) === 'main' ? 'main' : stdout.substr(spaceIndex).trim(); + const result = stdout.substring(0, spaceIndex) === 'main' ? 'main' : stdout.substring(spaceIndex).trim(); this.packageInfo.set(dir, result); resolve(result); } diff --git a/extension/src/diffUtils.ts b/extension/src/diffUtils.ts index 04d036c677..9181cc7ac0 100644 --- a/extension/src/diffUtils.ts +++ b/extension/src/diffUtils.ts @@ -95,7 +95,7 @@ function parseUniDiffs(diffOutput: jsDiff.IUniDiff[]): FilePatch[] { uniDiff.hunks.forEach((hunk: jsDiff.IHunk) => { let startLine = hunk.oldStart; hunk.lines.forEach((line) => { - switch (line.substr(0, 1)) { + switch (line.substring(0, 1)) { case '-': edit = new Edit(EditTypes.EDIT_DELETE, new Position(startLine - 1, 0)); edit.end = new Position(startLine, 0); @@ -104,7 +104,7 @@ function parseUniDiffs(diffOutput: jsDiff.IUniDiff[]): FilePatch[] { break; case '+': edit = new Edit(EditTypes.EDIT_INSERT, new Position(startLine - 1, 0)); - edit.text += line.substr(1) + '\n'; + edit.text += line.substring(1) + '\n'; edits.push(edit); break; case ' ': diff --git a/extension/src/goBuild.ts b/extension/src/goBuild.ts index 2f1349d47b..20b34189dd 100644 --- a/extension/src/goBuild.ts +++ b/extension/src/goBuild.ts @@ -153,7 +153,7 @@ export async function goBuild( if (!isMod) { // Find the right importPath instead of directly using `.`. Fixes https://github.com/Microsoft/vscode-go/issues/846 if (currentGoWorkspace && !isMod) { - importPath = cwd.substr(currentGoWorkspace.length + 1); + importPath = cwd.substring(currentGoWorkspace.length + 1); } else { outputChannel.error( `Not able to determine import path of current package by using cwd: ${cwd} and Go workspace: ${currentGoWorkspace}` diff --git a/extension/src/goCover.ts b/extension/src/goCover.ts index 53a78a2043..3c16660dec 100644 --- a/extension/src/goCover.ts +++ b/extension/src/goCover.ts @@ -214,95 +214,91 @@ function clearCoverage() { * @param packageDirPath Absolute path of the package for which the coverage was calculated * @param dir Directory to execute go list in */ -export function applyCodeCoverageToAllEditors(coverProfilePath: string, dir?: string): Promise { - const v = new Promise((resolve, reject) => { - try { - const showCounts = getGoConfig().get('coverShowCounts') as boolean; - const coveragePath = new Map(); // from the cover profile to the coverage data. - - // Clear existing coverage files - clearCoverage(); - - // collect the packages named in the coverage file - const seenPaths = new Set(); - // for now read synchronously and hope for no errors - const contents = fs.readFileSync(coverProfilePath).toString(); - contents.split('\n').forEach((line) => { - // go test coverageprofile generates output: - // filename:StartLine.StartColumn,EndLine.EndColumn Hits CoverCount - // where the filename is either the import path + '/' + base file name, or - // the actual file path (either absolute or starting with .) - // See https://golang.org/issues/40251. - // - // The first line will be like "mode: set" which we will ignore. - // TODO: port https://golang.org/cl/179377 for faster parsing. - - const parse = line.match(/^(\S+)\:(\d+)\.(\d+)\,(\d+)\.(\d+)\s(\d+)\s(\d+)/); - if (!parse) { - return; - } +export async function applyCodeCoverageToAllEditors(coverProfilePath: string, dir?: string): Promise { + try { + const showCounts = getGoConfig().get('coverShowCounts') as boolean; + const coveragePath = new Map(); // from the cover profile to the coverage data. - let filename = parse[1]; - if (filename.startsWith('.' + path.sep)) { - // If it's a relative file path, convert it to an absolute path. - // From now on, we can assume that it's a real file name if it is - // an absolute path. - filename = path.resolve(filename); - } - // If this is not a real file name, that's package_path + file name, - // Record it in seenPaths for `go list` call to resolve package path -> - // directory mapping. - if (!path.isAbsolute(filename)) { - const lastSlash = filename.lastIndexOf('/'); - if (lastSlash !== -1) { - seenPaths.add(filename.slice(0, lastSlash)); - } - } + // Clear existing coverage files + clearCoverage(); - // and fill in coveragePath - const coverage = coveragePath.get(parse[1]) || emptyCoverageData(); - // When line directive is used this information is artificial and - // the source code file can be non-existent or wrong (go.dev/issues/41222). - // There is no perfect way to guess whether the line/col in coverage profile - // is bogus. At least, we know that 0 or negative values are not true line/col. - const startLine = parseInt(parse[2], 10); - const startCol = parseInt(parse[3], 10); - const endLine = parseInt(parse[4], 10); - const endCol = parseInt(parse[5], 10); - if (startLine < 1 || startCol < 1 || endLine < 1 || endCol < 1) { - return; - } - const range = new vscode.Range( - // Convert lines and columns to 0-based - startLine - 1, - startCol - 1, - endLine - 1, - endCol - 1 - ); - - const counts = parseInt(parse[7], 10); - // If is Covered (CoverCount > 0) - if (counts > 0) { - coverage.coveredOptions.push(...elaborate(range, counts, showCounts)); - } else { - coverage.uncoveredOptions.push(...elaborate(range, counts, showCounts)); + // collect the packages named in the coverage file + const seenPaths = new Set(); + // Read coverage file asynchronously to avoid blocking the UI + const contents = await fs.promises.readFile(coverProfilePath, 'utf8'); + contents.split('\n').forEach((line) => { + // go test coverageprofile generates output: + // filename:StartLine.StartColumn,EndLine.EndColumn Hits CoverCount + // where the filename is either the import path + '/' + base file name, or + // the actual file path (either absolute or starting with .) + // See https://golang.org/issues/40251. + // + // The first line will be like "mode: set" which we will ignore. + // TODO: port https://golang.org/cl/179377 for faster parsing. + + const parse = line.match(/^(\S+)\:(\d+)\.(\d+)\,(\d+)\.(\d+)\s(\d+)\s(\d+)/); + if (!parse) { + return; + } + + let filename = parse[1]; + if (filename.startsWith('.' + path.sep)) { + // If it's a relative file path, convert it to an absolute path. + // From now on, we can assume that it's a real file name if it is + // an absolute path. + filename = path.resolve(filename); + } + // If this is not a real file name, that's package_path + file name, + // Record it in seenPaths for `go list` call to resolve package path -> + // directory mapping. + if (!path.isAbsolute(filename)) { + const lastSlash = filename.lastIndexOf('/'); + if (lastSlash !== -1) { + seenPaths.add(filename.slice(0, lastSlash)); } + } - coveragePath.set(filename, coverage); - }); - - getImportPathToFolder([...seenPaths], dir).then((pathsToDirs) => { - createCoverageData(pathsToDirs, coveragePath); - setDecorators(); - vscode.window.visibleTextEditors.forEach(applyCodeCoverage); - resolve(); - }); - } catch (e) { - vscode.window.showInformationMessage((e as any).msg); - reject(e); - } - }); - return v; + // and fill in coveragePath + const coverage = coveragePath.get(parse[1]) || emptyCoverageData(); + // When line directive is used this information is artificial and + // the source code file can be non-existent or wrong (go.dev/issues/41222). + // There is no perfect way to guess whether the line/col in coverage profile + // is bogus. At least, we know that 0 or negative values are not true line/col. + const startLine = parseInt(parse[2], 10); + const startCol = parseInt(parse[3], 10); + const endLine = parseInt(parse[4], 10); + const endCol = parseInt(parse[5], 10); + if (startLine < 1 || startCol < 1 || endLine < 1 || endCol < 1) { + return; + } + const range = new vscode.Range( + // Convert lines and columns to 0-based + startLine - 1, + startCol - 1, + endLine - 1, + endCol - 1 + ); + + const counts = parseInt(parse[7], 10); + // If is Covered (CoverCount > 0) + if (counts > 0) { + coverage.coveredOptions.push(...elaborate(range, counts, showCounts)); + } else { + coverage.uncoveredOptions.push(...elaborate(range, counts, showCounts)); + } + + coveragePath.set(filename, coverage); + }); + + const pathsToDirs = await getImportPathToFolder([...seenPaths], dir); + createCoverageData(pathsToDirs, coveragePath); + setDecorators(); + vscode.window.visibleTextEditors.forEach(applyCodeCoverage); + } catch (e) { + const errorMsg = (e as any).msg || String(e); + vscode.window.showInformationMessage(errorMsg); + throw e; + } } // add decorations to the range @@ -356,7 +352,7 @@ function createCoverageData(pathsToDirs: Map, coveragePath: Map< */ function setCoverageDataByFilePath(filePath: string, data: CoverageData) { if (filePath.startsWith('_')) { - filePath = filePath.substr(1); + filePath = filePath.substring(1); } if (process.platform === 'win32') { const parts = filePath.split('/'); diff --git a/extension/src/goDebugConfiguration.ts b/extension/src/goDebugConfiguration.ts index a1de509828..248fc9d7b2 100644 --- a/extension/src/goDebugConfiguration.ts +++ b/extension/src/goDebugConfiguration.ts @@ -394,14 +394,27 @@ export class GoDebugConfigurationProvider implements vscode.DebugConfigurationPr const child = spawn(getBinPath('dlv'), ['substitute-path-guess-helper']); let stdoutData = ''; let stderrData = ''; - child.stdout.on('data', (data) => { + + const stdoutHandler = (data: Buffer) => { stdoutData += data; - }); - child.stderr.on('data', (data) => { + }; + const stderrHandler = (data: Buffer) => { stderrData += data; - }); + }; + + // Cleanup function to remove all listeners + const cleanup = () => { + child.stdout?.removeListener('data', stdoutHandler); + child.stderr?.removeListener('data', stderrHandler); + child.removeAllListeners('close'); + child.removeAllListeners('error'); + }; + + child.stdout.on('data', stdoutHandler); + child.stderr.on('data', stderrHandler); child.on('close', (code) => { + cleanup(); if (code !== 0) { resolve(null); } else { @@ -414,6 +427,7 @@ export class GoDebugConfigurationProvider implements vscode.DebugConfigurationPr }); child.on('error', (error) => { + cleanup(); resolve(null); }); }); diff --git a/extension/src/goGenerateTests.ts b/extension/src/goGenerateTests.ts index a5c60c659a..be342d199d 100644 --- a/extension/src/goGenerateTests.ts +++ b/extension/src/goGenerateTests.ts @@ -61,9 +61,9 @@ export const toggleTestFile: CommandFactory = () => () => { } let targetFilePath = ''; if (currentFilePath.endsWith('_test.go')) { - targetFilePath = currentFilePath.substr(0, currentFilePath.lastIndexOf('_test.go')) + '.go'; + targetFilePath = currentFilePath.substring(0, currentFilePath.lastIndexOf('_test.go')) + '.go'; } else { - targetFilePath = currentFilePath.substr(0, currentFilePath.lastIndexOf('.go')) + '_test.go'; + targetFilePath = currentFilePath.substring(0, currentFilePath.lastIndexOf('.go')) + '_test.go'; } for (const doc of vscode.window.visibleTextEditors) { if (doc.document.fileName === targetFilePath) { @@ -269,7 +269,7 @@ function generateTests( return element.startsWith(generatedWord); }) .map((element) => { - return element.substr(generatedWord.length); + return element.substring(generatedWord.length); }); message = `Generated ${lines.join(', ')}`; testsGenerated = true; diff --git a/extension/src/goImport.ts b/extension/src/goImport.ts index 1693d0b185..441e5d8e83 100644 --- a/extension/src/goImport.ts +++ b/extension/src/goImport.ts @@ -55,7 +55,7 @@ async function askUserForImport(goCtx: GoExtensionContext): Promise async () => { // Find the right importPath instead of directly using `.`. Fixes https://github.com/Microsoft/vscode-go/issues/846 const currentGoWorkspace = getCurrentGoWorkspaceFromGOPATH(getCurrentGoPath(), cwd); - const importPath = currentGoWorkspace && !isMod ? cwd.substr(currentGoWorkspace.length + 1) : '.'; + const importPath = currentGoWorkspace && !isMod ? cwd.substring(currentGoWorkspace.length + 1) : '.'; args.push(importPath); outputChannel.info(`Installing ${importPath === '.' ? 'current package' : importPath}`); diff --git a/extension/src/goLint.ts b/extension/src/goLint.ts index cf2ba90892..254c99e6f8 100644 --- a/extension/src/goLint.ts +++ b/extension/src/goLint.ts @@ -101,12 +101,12 @@ export async function goLint( return; } if (flag.startsWith('--config=') || flag.startsWith('-config=')) { - let configFilePath = flag.substr(flag.indexOf('=') + 1).trim(); + let configFilePath = flag.substring(flag.indexOf('=') + 1).trim(); if (!configFilePath) { return; } configFilePath = resolvePath(configFilePath); - args.push(`${flag.substr(0, flag.indexOf('=') + 1)}${configFilePath}`); + args.push(`${flag.substring(0, flag.indexOf('=') + 1)}${configFilePath}`); return; } args.push(flag); diff --git a/extension/src/goMain.ts b/extension/src/goMain.ts index fc15077f44..87badafdaa 100644 --- a/extension/src/goMain.ts +++ b/extension/src/goMain.ts @@ -83,6 +83,29 @@ interface ExtensionTestAPI { globalState: vscode.Memento; } +/** + * Extension activation entry point called by VS Code when the Go extension loads. + * This is the main initialization function that sets up all Go development features. + * + * Activation sequence: + * 1. Initialize global and workspace state + * 2. Configure GOROOT and environment variables + * 3. Build and install vscgo helper tool + * 4. Register all extension commands + * 5. Set up language features (code lens, formatting, testing, etc.) + * 6. Start gopls language server (if enabled) + * 7. Install missing/outdated Go tools + * 8. Configure telemetry and surveys + * + * @param ctx - VS Code extension context providing subscriptions, storage, and paths + * @returns ExtensionAPI for production use or ExtensionTestAPI for testing + * + * @example + * // Called automatically by VS Code, not by user code + * // Returns API that can be accessed by other extensions via: + * const goExt = vscode.extensions.getExtension('golang.go'); + * const api = await goExt?.activate(); + */ export async function activate(ctx: vscode.ExtensionContext): Promise { if (process.env['VSCODE_GO_IN_TEST'] === '1') { // TODO: VSCODE_GO_IN_TEST was introduced long before we learned about @@ -240,6 +263,26 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { + vscode.workspace.onDidChangeConfiguration(async (e: vscode.ConfigurationChangeEvent) => { const goConfig = getGoConfig(); const goplsConfig = getGoplsConfig(); + // Validate configuration once for all changes validateConfig(goConfig, goplsConfig); + // Early return if this doesn't affect Go configuration if (!e.affectsConfiguration('go')) { return; } + // Handle language server restart if ( e.affectsConfiguration('go.useLanguageServer') || e.affectsConfiguration('go.languageServerFlags') || @@ -276,27 +328,20 @@ export function addConfigChangeListener(ctx: vscode.ExtensionContext) { vscode.commands.executeCommand('go.languageserver.restart', RestartReason.CONFIG_CHANGE); } + // Handle gopls opt-out if (e.affectsConfiguration('go.useLanguageServer') && goConfig['useLanguageServer'] === false) { promptAboutGoplsOptOut(goCtx); } - }) - ); - ctx.subscriptions.push( - vscode.workspace.onDidChangeConfiguration(async (e: vscode.ConfigurationChangeEvent) => { - if (!e.affectsConfiguration('go')) { - return; - } - const goConfig = getGoConfig(); - const goplsConfig = getGoplsConfig(); - - validateConfig(goConfig, goplsConfig); + // Handle GOROOT changes if (e.affectsConfiguration('go.goroot')) { const configGOROOT = goConfig['goroot']; if (configGOROOT) { await setGOROOTEnvVar(configGOROOT); } } + + // Update Go variables when relevant settings change if ( e.affectsConfiguration('go.goroot') || e.affectsConfiguration('go.alternateTools') || @@ -306,20 +351,28 @@ export function addConfigChangeListener(ctx: vscode.ExtensionContext) { ) { updateGoVarsFromConfig(goCtx); } - // If there was a change in "toolsGopath" setting, then clear cache for go tools - if (getToolsGopath() !== getToolsGopath(false)) { + + // Clear tool cache if toolsGopath changed + if (e.affectsConfiguration('go.toolsGopath') || e.affectsConfiguration('go.alternateTools')) { clearCacheForTools(); } + // Check tool existence for format tool if (e.affectsConfiguration('go.formatTool')) { checkToolExists(getFormatTool(goConfig)); } + + // Check tool existence for docs tool if (e.affectsConfiguration('go.docsTool')) { checkToolExists(goConfig['docsTool']); } + + // Update coverage decorators if (e.affectsConfiguration('go.coverageDecorator')) { updateCodeCoverageDecorators(goConfig['coverageDecorator']); } + + // Handle GO111MODULE changes if (e.affectsConfiguration('go.toolsEnvVars')) { const env = toolExecutionEnvironment(); if (GO111MODULE !== env['GO111MODULE']) { @@ -332,6 +385,8 @@ export function addConfigChangeListener(ctx: vscode.ExtensionContext) { }); } } + + // Handle lint tool changes if (e.affectsConfiguration('go.lintTool')) { checkToolExists(goConfig['lintTool']); @@ -397,7 +452,11 @@ function addOnChangeActiveTextEditorListeners(ctx: vscode.ExtensionContext) { }); } -function checkToolExists(tool: string) { +/** + * Checks if a tool exists at its expected location and prompts for installation if missing. + * @param tool - The name of the tool to check + */ +function checkToolExists(tool: string): void { if (tool === '') { return; } @@ -406,7 +465,12 @@ function checkToolExists(tool: string) { } } -function lintDiagnosticCollectionName(lintToolName: string) { +/** + * Returns the diagnostic collection name for a given lint tool. + * @param lintToolName - The name of the lint tool (e.g., 'golangci-lint', 'staticcheck') + * @returns The diagnostic collection name (e.g., 'go-golangci-lint') + */ +function lintDiagnosticCollectionName(lintToolName: string): string { if (!lintToolName || lintToolName === 'golint') { return 'go-lint'; } diff --git a/extension/src/goModules.ts b/extension/src/goModules.ts index 367b5f0be9..a1ecd18ec0 100644 --- a/extension/src/goModules.ts +++ b/extension/src/goModules.ts @@ -85,10 +85,10 @@ export async function getCurrentPackage(cwd: string): Promise { const moduleCache = getModuleCache(); if (moduleCache && cwd.startsWith(moduleCache)) { - let importPath = cwd.substr(moduleCache.length + 1); + let importPath = cwd.substring(moduleCache.length + 1); const matches = /@v\d+(\.\d+)?(\.\d+)?/.exec(importPath); if (matches) { - importPath = importPath.substr(0, matches.index); + importPath = importPath.substring(0, matches.index); } folderToPackageMapping[cwd] = importPath; diff --git a/extension/src/goPackages.ts b/extension/src/goPackages.ts index 99105b01b6..6206c432a6 100644 --- a/extension/src/goPackages.ts +++ b/extension/src/goPackages.ts @@ -242,7 +242,7 @@ function getRelativePackagePath(currentFileDirPath: string, currentWorkspace: st // If yes, then vendor pkg can be replaced with its relative path to the "vendor" folder // If not, then the vendor pkg should not be allowed to be imported. if (vendorIndex > -1) { - const rootProjectForVendorPkg = path.join(currentWorkspace, pkgPath.substr(0, vendorIndex)); + const rootProjectForVendorPkg = path.join(currentWorkspace, pkgPath.substring(0, vendorIndex)); const relativePathForVendorPkg = pkgPath.substring(vendorIndex + magicVendorString.length); const subVendor = relativePathForVendorPkg.indexOf('/vendor/') !== -1; @@ -317,7 +317,7 @@ function isAllowToImportPackage(toDirPath: string, currentWorkspace: string, pkg const internalPkgFound = pkgPath.match(/\/internal\/|\/internal$/); if (internalPkgFound) { - const rootProjectForInternalPkg = path.join(currentWorkspace, pkgPath.substr(0, internalPkgFound.index)); + const rootProjectForInternalPkg = path.join(currentWorkspace, pkgPath.substring(0, internalPkgFound.index)); return toDirPath.startsWith(rootProjectForInternalPkg + path.sep) || toDirPath === rootProjectForInternalPkg; } return true; diff --git a/extension/src/goTest/run.ts b/extension/src/goTest/run.ts index 6619b0b2f8..5e9cc99ef1 100644 --- a/extension/src/goTest/run.ts +++ b/extension/src/goTest/run.ts @@ -241,7 +241,12 @@ export class GoTestRunner { const p = this.collectTests(item, true, request.exclude || [], collected, files); promises.push(p); }); - await Promise.all(promises); + // Use allSettled to handle partial failures gracefully + const results = await Promise.allSettled(promises); + const failures = results.filter((r) => r.status === 'rejected'); + if (failures.length > 0) { + outputChannel.error(`Failed to collect ${failures.length} test(s): ${failures.map((f) => (f as PromiseRejectedResult).reason).join(', ')}`); + } } // Save all documents that contain a test we're about to run, to ensure `go diff --git a/extension/src/goVet.ts b/extension/src/goVet.ts index 8883a54212..784da319b6 100644 --- a/extension/src/goVet.ts +++ b/extension/src/goVet.ts @@ -88,12 +88,12 @@ export async function goVet( vetFlags.forEach((flag) => { if (flag.startsWith('--vettool=') || flag.startsWith('-vettool=')) { - let vetToolPath = flag.substr(flag.indexOf('=') + 1).trim(); + let vetToolPath = flag.substring(flag.indexOf('=') + 1).trim(); if (!vetToolPath) { return; } vetToolPath = resolvePath(vetToolPath); - args.push(`${flag.substr(0, flag.indexOf('=') + 1)}${vetToolPath}`); + args.push(`${flag.substring(0, flag.indexOf('=') + 1)}${vetToolPath}`); return; } args.push(flag); diff --git a/extension/src/goVulncheck.ts b/extension/src/goVulncheck.ts index dddb43f2fc..8db7d6ec96 100644 --- a/extension/src/goVulncheck.ts +++ b/extension/src/goVulncheck.ts @@ -36,19 +36,37 @@ export async function writeVulns( cwd: getWorkspaceFolderPath() }); - p.stdout.on('data', (data) => { + const stdoutHandler = (data: Buffer) => { stdout += data; - }); - p.stderr.on('data', (data) => { + }; + const stderrHandler = (data: Buffer) => { stderr += data; - }); + }; + + // Cleanup function to remove all listeners and prevent memory leaks + const cleanup = () => { + p.stdout?.removeListener('data', stdoutHandler); + p.stderr?.removeListener('data', stderrHandler); + p.removeAllListeners('close'); + p.removeAllListeners('error'); + }; + + p.stdout.on('data', stdoutHandler); + p.stderr.on('data', stderrHandler); + // 'close' fires after exit or error when the subprocess closes all stdio. p.on('close', (exitCode) => { + cleanup(); // When vulnerabilities are found, vulncheck -mode=convert returns a non-zero exit code. // TODO: can we use the exitCode to set the status of terminal? resolve(exitCode); }); + p.on('error', (err) => { + cleanup(); + resolve(null); + }); + // vulncheck -mode=convert expects a stream of osv.Entry and govulncheck Finding json objects. if (res.Entries) { Object.values(res.Entries).forEach((osv) => { diff --git a/extension/src/language/goLanguageServer.ts b/extension/src/language/goLanguageServer.ts index 735f75111f..c159bf4eae 100644 --- a/extension/src/language/goLanguageServer.ts +++ b/extension/src/language/goLanguageServer.ts @@ -70,6 +70,14 @@ import { COMMAND as GOPLS_ADD_TEST_COMMAND } from '../goGenerateTests'; import { COMMAND as GOPLS_MODIFY_TAGS_COMMAND } from '../goModifytags'; import { TelemetryKey, telemetryReporter } from '../goTelemetry'; +// Constants for scheduled task delays and thresholds +const UPDATE_CHECK_DELAY_MINUTES = 10; +const SURVEY_PROMPT_DELAY_MINUTES = 30; +const TELEMETRY_PROMPT_DELAY_MINUTES = 6; +const MIN_IDLE_TIME_FOR_TELEMETRY_MINUTES = 5; +const GOPLS_SHUTDOWN_TIMEOUT_MS = 2000; +const MAX_GOPLS_CRASHES_BEFORE_SHUTDOWN = 5; + export interface LanguageServerConfig { serverName: string; path: string; @@ -77,7 +85,7 @@ export interface LanguageServerConfig { modtime?: Date; enabled: boolean; flags: string[]; - env: any; + env: NodeJS.ProcessEnv; features: { // A custom formatter can be configured to run instead of gopls. // This is enabled when the user has configured a specific format @@ -87,6 +95,12 @@ export interface LanguageServerConfig { checkForUpdates: string; } +/** + * Represents a configuration object for gopls or Go settings. + * This is a flexible object that can contain any configuration key-value pairs. + */ +export type ConfigurationObject = { [key: string]: unknown }; + export interface ServerInfo { Name: string; Version?: string; @@ -209,9 +223,9 @@ export function scheduleGoplsSuggestions(goCtx: GoExtensionContext) { } maybePromptForTelemetry(goCtx); }; - setTimeout(update, 10 * timeMinute); - setTimeout(survey, 30 * timeMinute); - setTimeout(telemetry, 6 * timeMinute); + setTimeout(update, UPDATE_CHECK_DELAY_MINUTES * timeMinute); + setTimeout(survey, SURVEY_PROMPT_DELAY_MINUTES * timeMinute); + setTimeout(telemetry, TELEMETRY_PROMPT_DELAY_MINUTES * timeMinute); } // Ask users to fill out opt-out survey. @@ -374,8 +388,27 @@ type VulncheckEvent = { message?: string; }; -// buildLanguageClient returns a language client built using the given language server config. -// The returned language client need to be started before use. +/** + * Builds a VS Code Language Server Protocol (LSP) client for gopls. + * The returned client is configured but not started - caller must call .start() on it. + * + * This function sets up: + * - Output channels for server logs and traces + * - Language server executable options (path, flags, environment) + * - Client options (document selectors, middleware, synchronization) + * - Progress handlers for gopls operations + * - Command execution middleware + * - Configuration synchronization with gopls + * + * @param goCtx - Go extension context containing output channels and state + * @param cfg - Language server configuration with path, flags, and features + * @returns Configured GoLanguageClient instance ready to be started + * + * @example + * const cfg = await buildLanguageServerConfig(getGoConfig()); + * const client = await buildLanguageClient(goCtx, cfg); + * await client.start(); // Start the language server + */ export async function buildLanguageClient( goCtx: GoExtensionContext, cfg: LanguageServerConfig @@ -400,7 +433,7 @@ export async function buildLanguageClient( // TODO(hxjiang): deprecate special handling for async call gopls.run_govulncheck. let govulncheckTerminal: IProgressTerminal | undefined; - const pendingVulncheckProgressToken = new Map(); + const pendingVulncheckProgressToken = new Map(); const onDidChangeVulncheckResultEmitter = new vscode.EventEmitter(); // VSCode-Go prepares the information needed to start the language server. @@ -436,7 +469,7 @@ export async function buildLanguageClient( errorHandler: { error: (error: Error, message: Message, count: number) => { // Allow 5 crashes before shutdown. - if (count < 5) { + if (count < MAX_GOPLS_CRASHES_BEFORE_SHUTDOWN) { return { message: '', // suppresses error popups action: ErrorAction.Continue @@ -842,12 +875,12 @@ export async function buildLanguageClient( // and selects only those the user explicitly specifies in their settings. // This returns a new object created based on the filtered properties of workspaceConfig. // Exported for testing. -export function filterGoplsDefaultConfigValues(workspaceConfig: any, resource?: vscode.Uri): any { +export function filterGoplsDefaultConfigValues(workspaceConfig: ConfigurationObject, resource?: vscode.Uri): ConfigurationObject { if (!workspaceConfig) { workspaceConfig = {}; } const cfg = getGoplsConfig(resource); - const filtered = {} as { [key: string]: any }; + const filtered: ConfigurationObject = {}; for (const [key, value] of Object.entries(workspaceConfig)) { if (typeof value === 'function') { continue; @@ -880,7 +913,7 @@ export function filterGoplsDefaultConfigValues(workspaceConfig: any, resource?: // - go.buildTags and go.buildFlags are passed as gopls.build.buildFlags // if goplsWorkspaceConfig doesn't explicitly set it yet. // Exported for testing. -export function passGoConfigToGoplsConfigValues(goplsWorkspaceConfig: any, goWorkspaceConfig: any): any { +export function passGoConfigToGoplsConfigValues(goplsWorkspaceConfig: ConfigurationObject, goWorkspaceConfig: ConfigurationObject): ConfigurationObject { if (!goplsWorkspaceConfig) { goplsWorkspaceConfig = {}; } @@ -905,10 +938,10 @@ export function passGoConfigToGoplsConfigValues(goplsWorkspaceConfig: any, goWor // If this is for the nightly extension, we also request to activate features under experiments. async function adjustGoplsWorkspaceConfiguration( cfg: LanguageServerConfig, - workspaceConfig: any, + workspaceConfig: ConfigurationObject, section?: string, resource?: vscode.Uri -): Promise { +): Promise { // We process only gopls config if (section !== 'gopls') { return workspaceConfig; @@ -932,7 +965,7 @@ async function adjustGoplsWorkspaceConfiguration( return workspaceConfig; } -async function passInlayHintConfigToGopls(cfg: LanguageServerConfig, goplsConfig: any, goConfig: any) { +async function passInlayHintConfigToGopls(cfg: LanguageServerConfig, goplsConfig: ConfigurationObject, goConfig: vscode.WorkspaceConfiguration): Promise { const goplsVersion = await getLocalGoplsVersion(cfg); if (!goplsVersion) return goplsConfig ?? {}; const version = semver.parse(goplsVersion.version); @@ -945,7 +978,7 @@ async function passInlayHintConfigToGopls(cfg: LanguageServerConfig, goplsConfig return goplsConfig; } -async function passVulncheckConfigToGopls(cfg: LanguageServerConfig, goplsConfig: any, goConfig: any) { +async function passVulncheckConfigToGopls(cfg: LanguageServerConfig, goplsConfig: ConfigurationObject, goConfig: vscode.WorkspaceConfiguration): Promise { const goplsVersion = await getLocalGoplsVersion(cfg); if (!goplsVersion) return goplsConfig ?? {}; const version = semver.parse(goplsVersion.version); @@ -958,7 +991,7 @@ async function passVulncheckConfigToGopls(cfg: LanguageServerConfig, goplsConfig return goplsConfig; } -async function passLinkifyShowMessageToGopls(cfg: LanguageServerConfig, goplsConfig: any) { +async function passLinkifyShowMessageToGopls(cfg: LanguageServerConfig, goplsConfig: ConfigurationObject): Promise { goplsConfig = goplsConfig ?? {}; const goplsVersion = await getLocalGoplsVersion(cfg); @@ -1017,6 +1050,29 @@ function createBenchmarkCodeLens(lens: vscode.CodeLens): vscode.CodeLens[] { ]; } +/** + * Builds the configuration object for the Go language server (gopls). + * This function locates gopls, validates it exists, and constructs the config needed to start it. + * + * Configuration includes: + * - Server executable path (from go.languageServerPath setting or auto-detected) + * - Server command-line flags (from go.languageServerFlags) + * - Environment variables for gopls process + * - Custom formatter (if go.formatTool is set to override gopls formatting) + * - Update check preferences (from go.toolsManagement.checkForUpdates) + * - Server version and modification time (for tracking updates) + * + * @param goConfig - VS Code workspace configuration for the Go extension + * @returns LanguageServerConfig object, with `enabled: false` if gopls is disabled or not found + * + * @example + * const goConfig = getGoConfig(); + * const cfg = await buildLanguageServerConfig(goConfig); + * if (cfg.enabled) { + * console.log(`Found gopls at: ${cfg.path}`); + * const client = await buildLanguageClient(goCtx, cfg); + * } + */ export async function buildLanguageServerConfig( goConfig: vscode.WorkspaceConfiguration ): Promise { @@ -1531,8 +1587,8 @@ export function maybePromptForTelemetry(goCtx: GoExtensionContext) { // Make sure the user has been idle for at least 5 minutes. const idleTime = currentTime.getTime() - lastUserAction.getTime(); - if (idleTime < 5 * timeMinute) { - setTimeout(callback, 5 * timeMinute - Math.max(idleTime, 0)); + if (idleTime < MIN_IDLE_TIME_FOR_TELEMETRY_MINUTES * timeMinute) { + setTimeout(callback, MIN_IDLE_TIME_FOR_TELEMETRY_MINUTES * timeMinute - Math.max(idleTime, 0)); return; } goCtx.telemetryService?.promptForTelemetry(); diff --git a/extension/src/pickProcess.ts b/extension/src/pickProcess.ts index 45a6a30015..a01854cb5d 100644 --- a/extension/src/pickProcess.ts +++ b/extension/src/pickProcess.ts @@ -139,7 +139,7 @@ export function parseGoVersionOutput(stdout: string): string[] { lines.forEach((line) => { const match = line.match(goVersionRegexp); if (match && match.length > 0) { - const exe = line.substr(0, line.length - match[0].length); + const exe = line.substring(0, line.length - match[0].length); goProcessExes.push(exe); } }); diff --git a/extension/src/testUtils.ts b/extension/src/testUtils.ts index 72a185fd70..702b01901c 100644 --- a/extension/src/testUtils.ts +++ b/extension/src/testUtils.ts @@ -508,7 +508,7 @@ async function getTestTargetPackages(testconfig: TestConfig, outputChannel: vsco // GOPATH mode currentGoWorkspace = getCurrentGoWorkspaceFromGOPATH(getCurrentGoPath(), testconfig.dir); getCurrentPackagePromise = Promise.resolve( - currentGoWorkspace ? testconfig.dir.substr(currentGoWorkspace.length + 1) : '' + currentGoWorkspace ? testconfig.dir.substring(currentGoWorkspace.length + 1) : '' ); // We dont need mapping, as we can derive the absolute paths from package path pkgMapPromise = Promise.resolve(); diff --git a/extension/src/util.ts b/extension/src/util.ts index 578dab5992..139ec9ddd6 100644 --- a/extension/src/util.ts +++ b/extension/src/util.ts @@ -27,6 +27,16 @@ import { } from './utils/pathUtils'; import { killProcessTree } from './utils/processUtils'; +/** + * Represents a command defined in the extension's package.json. + * Used for command palette and keybindings. + */ +export interface ExtensionCommand { + command: string; + title: string; + category?: string; +} + export class GoVersion { public sv?: semver.SemVer; // Go version tags are not following the strict semver format @@ -110,7 +120,7 @@ export function getCheckForToolsUpdatesConfig(gocfg: vscode.WorkspaceConfigurati 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)); + return Buffer.byteLength(text.substring(0, offset)); } export interface Prelude { @@ -199,11 +209,13 @@ export async function getGoVersion(goBinPath?: string, GOTOOLCHAIN?: string): Pr const execFile = util.promisify(cp.execFile); const { stdout, stderr } = await execFile(goRuntimePath, ['version'], { env, cwd }); if (stderr) { - error(`failed to run "${goRuntimePath} version": stdout: ${stdout}, stderr: ${stderr}`); + // Log stderr but don't fail - some Go versions output warnings + outputChannel.debug(`stderr from "${goRuntimePath} version": ${stderr}`); } goVersion = new GoVersion(goRuntimePath, stdout); } catch (err) { - throw error(`failed to run "${goRuntimePath} version": ${err} cwd: ${cwd}`); + const errMsg = `failed to run "${goRuntimePath} version": ${err} cwd: ${cwd}`; + throw error(errMsg); } if (!goBinPath && GOTOOLCHAIN === undefined) { // if getGoVersion was called with a given goBinPath or an explicit GOTOOLCHAIN env var, don't cache the result. @@ -232,8 +244,8 @@ export async function getGoEnv(cwd?: string): Promise { } /** - * Returns boolean indicating if GOPATH is set or not - * If not set, then prompts user to do set GOPATH + * Checks if GOPATH is set and prompts the user if not. + * @returns True if GOPATH is set, false otherwise */ export function isGoPathSet(): boolean { if (!getCurrentGoPath()) { @@ -271,7 +283,7 @@ function resolveToolsGopath(): string { // In case of multi-root, resolve ~ and ${workspaceFolder} if (toolsGopathForWorkspace.startsWith('~')) { - toolsGopathForWorkspace = path.join(os.homedir(), toolsGopathForWorkspace.substr(1)); + toolsGopathForWorkspace = path.join(os.homedir(), toolsGopathForWorkspace.substring(1)); } if ( toolsGopathForWorkspace && @@ -296,14 +308,48 @@ function resolveToolsGopath(): string { return toolsGopathForWorkspace; } -// getBinPath returns the path to the tool. +/** + * Returns the absolute path to a Go tool's executable. + * This is the primary function used throughout the extension to locate Go tools. + * + * Search order: + * 1. Alternate tools configured in go.alternateTools + * 2. GOBIN environment variable + * 3. Configured toolsGopath (go.toolsGopath setting) + * 4. Current workspace GOPATH + * 5. GOROOT/bin (for go, gofmt, godoc) + * 6. PATH environment variable + * 7. Default system locations (e.g., /usr/local/go/bin) + * + * @param tool - Name of the tool (e.g., 'gopls', 'dlv', 'staticcheck') + * @param useCache - Whether to use cached tool paths (default: true) + * @returns Absolute path to the tool executable, or just the tool name if not found + * + * @example + * const goplsPath = getBinPath('gopls'); // Returns '/Users/me/go/bin/gopls' + * const goPath = getBinPath('go'); // Returns '/usr/local/go/bin/go' + */ 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. +/** + * Returns the absolute path to a Go tool's executable along with an explanation + * of where the path was found. Useful for diagnostics and troubleshooting. + * + * @param tool - Name of the tool (e.g., 'gopls', 'dlv', 'staticcheck') + * @param useCache - Whether to use cached tool paths (default: true) + * @param uri - Optional workspace URI for multi-root workspace support + * @returns Object containing the tool path and optional explanation + * - binPath: Absolute path to the tool executable + * - why: Optional explanation ('gobin', 'gopath', 'goroot', 'path', 'alternateTool', 'cached', 'default') + * + * @example + * const result = getBinPathWithExplanation('gopls'); + * console.log(`Found gopls at ${result.binPath} (source: ${result.why})`); + * // Output: "Found gopls at /Users/me/go/bin/gopls (source: gobin)" + */ export function getBinPathWithExplanation( tool: string, useCache = true, @@ -330,18 +376,43 @@ export function getBinPathWithExplanation( ); } +/** + * Serializes a VS Code document into the archive format expected by Go tools. + * This format is used to pass modified/unsaved file contents to tools like gopls and goimports. + * + * Format: `filename\nbytelength\ncontents` + * + * @param document - The VS Code text document to serialize + * @returns Serialized document in archive format + * + * @example + * const archive = getFileArchive(document); + * // Returns: "/path/to/file.go\n1234\npackage main..." + */ export function getFileArchive(document: vscode.TextDocument): string { const fileContents = document.getText(); return document.fileName + '\n' + Buffer.byteLength(fileContents, 'utf8') + '\n' + fileContents; } +/** + * Substitutes environment variables in a string using the ${env:VAR_NAME} syntax. + * @param input - String containing environment variable references + * @returns String with environment variables substituted + * @example + * substituteEnv("${env:HOME}/go") // Returns "/Users/username/go" on macOS + */ export function substituteEnv(input: string): string { return input.replace(/\${env:([^}]+)}/g, (match, capture) => { return process.env[capture.trim()] || ''; }); } -let currentGopath = ''; +/** + * Returns the current GOPATH based on configuration and workspace context. + * Falls back to inferred GOPATH or environment variable if not explicitly configured. + * @param workspaceUri - Optional workspace URI for multi-root workspaces + * @returns The current GOPATH as a string + */ export function getCurrentGoPath(workspaceUri?: vscode.Uri): string { const activeEditorUri = vscode.window.activeTextEditor?.document.uri; const currentFilePath = fixDriveCasingInWindows(activeEditorUri?.fsPath ?? ''); @@ -359,7 +430,8 @@ export function getCurrentGoPath(workspaceUri?: vscode.Uri): string { inferredGopath = currentRoot; } } catch (e) { - // No op + // Directory doesn't exist or can't be accessed - not a GOPATH + outputChannel.debug(`Could not infer GOPATH from current root: ${e}`); } } if (inferredGopath) { @@ -369,7 +441,8 @@ export function getCurrentGoPath(workspaceUri?: vscode.Uri): string { inferredGopath = ''; } } catch (e) { - // No op + // File system error checking for go.mod - treat as if no go.mod exists + outputChannel.debug(`Error checking for go.mod in inferred GOPATH: ${e}`); } } if (inferredGopath && process.env['GOPATH'] && inferredGopath !== process.env['GOPATH']) { @@ -391,14 +464,18 @@ export function getModuleCache(): string | undefined { } } -export function getExtensionCommands(): any[] { +/** + * Returns all commands defined in the extension's package.json. + * @returns Array of extension commands (excluding 'go.show.commands') + */ +export function getExtensionCommands(): ExtensionCommand[] { const pkgJSON = vscode.extensions.getExtension(extensionId)?.packageJSON; if (!pkgJSON.contributes || !pkgJSON.contributes.commands) { return []; } - const extensionCommands: any[] = vscode.extensions + const extensionCommands: ExtensionCommand[] = vscode.extensions .getExtension(extensionId) - ?.packageJSON.contributes.commands.filter((x: any) => x.command !== 'go.show.commands'); + ?.packageJSON.contributes.commands.filter((x: ExtensionCommand) => x.command !== 'go.show.commands'); return extensionCommands; } @@ -442,8 +519,11 @@ export class LineBuffer { } /** - * Expands ~ to homedir in non-Windows platform and resolves - * ${workspaceFolder}, ${workspaceRoot} and ${workspaceFolderBasename} + * Resolves workspace variables and home directory in a given path. + * Supports ${workspaceFolder}, ${workspaceRoot}, ${workspaceFolderBasename}, and ~ expansion. + * @param inputPath - Path that may contain variables to resolve + * @param workspaceFolder - Optional workspace folder path for variable resolution + * @returns Resolved absolute path */ export function resolvePath(inputPath: string, workspaceFolder?: string): string { if (!inputPath || !inputPath.trim()) { @@ -464,8 +544,13 @@ export function resolvePath(inputPath: string, workspaceFolder?: string): string } /** - * Returns the import path in a passed in string. - * @param text The string to search for an import path + * Extracts the import path from a Go import statement. + * Handles both single-line imports and imports within import blocks. + * @param text - Line of Go code containing an import statement + * @returns The extracted import path, or empty string if not found + * @example + * getImportPath('import "fmt"') // Returns "fmt" + * getImportPath('import alias "github.com/pkg/errors"') // Returns "github.com/pkg/errors" */ export function getImportPath(text: string): string { // Catch cases like `import alias "importpath"` and `import "importpath"` @@ -506,7 +591,7 @@ export function runTool( severity: string, useStdErr: boolean, toolName: string, - env: any, + env: NodeJS.ProcessEnv, printUnexpectedOutput: boolean, token?: vscode.CancellationToken ): Promise { @@ -533,7 +618,7 @@ export function runTool( return new Promise((resolve, reject) => { p = cp.execFile(cmd, args, { env, cwd }, (err, stdout, stderr) => { try { - if (err && (err).code === 'ENOENT') { + if (err && (err as NodeJS.ErrnoException).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'}`); @@ -602,6 +687,28 @@ export function runTool( }); } +/** + * Converts tool output errors into VS Code diagnostics and displays them in the Problems panel. + * This is the central function for surfacing errors from Go tools (gopls, staticcheck, golint, etc.) + * to the VS Code UI. + * + * Features: + * - Clears existing diagnostics before adding new ones + * - Maps error positions to VS Code ranges using token-based column calculation + * - Groups diagnostics by file for efficient display + * - Handles both open and closed files + * - Filters out diagnostics that overlap with gopls diagnostics (if gopls is enabled) + * + * @param goCtx - Go extension context for accessing configuration + * @param document - Optional document that triggered the check (for optimization) + * @param errors - Array of check results from Go tools + * @param diagnosticCollection - VS Code diagnostic collection to update (e.g., 'go-lint', 'go-vet') + * @param diagnosticSource - Optional source label for the diagnostics (e.g., 'staticcheck', 'golint') + * + * @example + * const errors = await runGoVet(document); + * handleDiagnosticErrors(goCtx, document, errors, vetDiagnosticCollection, 'go-vet'); + */ export function handleDiagnosticErrors( goCtx: GoExtensionContext, document: vscode.TextDocument | undefined, @@ -721,6 +828,12 @@ function mapSeverityToVSCodeSeverity(sev: string): vscode.DiagnosticSeverity { } } +/** + * Returns the workspace folder path for a given file URI. + * Falls back to the first workspace folder if the file is not in any workspace. + * @param fileUri - Optional file URI to find the containing workspace + * @returns The workspace folder path, or undefined if no workspace is open + */ export function getWorkspaceFolderPath(fileUri?: vscode.Uri): string | undefined { if (fileUri) { const workspace = vscode.workspace.getWorkspaceFolder(fileUri); diff --git a/extension/src/utils/envUtils.ts b/extension/src/utils/envUtils.ts index bd798f6f0a..37898bf8e5 100644 --- a/extension/src/utils/envUtils.ts +++ b/extension/src/utils/envUtils.ts @@ -11,7 +11,7 @@ import fs = require('fs'); function stripBOM(s: string): string { if (s && s[0] === '\uFEFF') { - s = s.substr(1); + s = s.substring(1); } return s; } @@ -23,7 +23,7 @@ function stripBOM(s: string): string { * Values can be optionally enclosed in single or double quotes. Double-quoted values support `\n` for newlines. * Environment variable substitution using `${VAR}` syntax is also supported within values. */ -export function parseEnvFile(envFilePath: string, globalVars?: NodeJS.Dict): { [key: string]: string } { +export async function parseEnvFile(envFilePath: string, globalVars?: NodeJS.Dict): Promise<{ [key: string]: string }> { const env: { [key: string]: string } = {}; if (!envFilePath) { return env; @@ -33,7 +33,8 @@ export function parseEnvFile(envFilePath: string, globalVars?: NodeJS.Dict { const r = line.match(/^\s*(export\s+)?([\w\.\-]+)\s*=\s*(.*)?\s*$/); if (r !== null) { @@ -78,18 +79,18 @@ function substituteEnvVars( return value.replace(/\\\$/g, '$'); } -export function parseEnvFiles( +export async function parseEnvFiles( envFiles: string[] | string | undefined, globalVars?: NodeJS.Dict -): { [key: string]: string } { +): Promise<{ [key: string]: string }> { const fileEnvs = []; if (typeof envFiles === 'string') { - fileEnvs.push(parseEnvFile(envFiles, globalVars)); + fileEnvs.push(await parseEnvFile(envFiles, globalVars)); } if (Array.isArray(envFiles)) { - envFiles.forEach((envFile) => { - fileEnvs.push(parseEnvFile(envFile, globalVars)); - }); + for (const envFile of envFiles) { + fileEnvs.push(await parseEnvFile(envFile, globalVars)); + } } return Object.assign({}, ...fileEnvs); } diff --git a/extension/src/utils/lsofProcessParser.ts b/extension/src/utils/lsofProcessParser.ts index 1c9a15c1c0..730453ed47 100644 --- a/extension/src/utils/lsofProcessParser.ts +++ b/extension/src/utils/lsofProcessParser.ts @@ -36,7 +36,7 @@ function parseProcessesFromLsofArray(processArray: string[], includesEnv?: boole } // The output for each process begins with a line containing the pid. const out = line[0]; - const val = line.substr(1); + const val = line.substring(1); if (out !== 'p') { continue; } @@ -74,7 +74,7 @@ function parseFile(start: number, lines: string[]): { fd?: string; name?: string continue; } const out = line[0]; - const val = line.substr(1); + const val = line.substring(1); switch (out) { case 'f': file.fd = val; diff --git a/extension/src/utils/pathUtils.ts b/extension/src/utils/pathUtils.ts index b3ce43b9ab..37637abf4f 100644 --- a/extension/src/utils/pathUtils.ts +++ b/extension/src/utils/pathUtils.ts @@ -69,7 +69,8 @@ export function getBinPathWithPreferredGopathGorootWithExplanation( return { binPath: alternateTool, why: 'alternateTool' }; } - // FIXIT: this cache needs to be invalidated when go.goroot or go.alternateTool is changed. + // Note: Cache is invalidated when go.toolsGopath or go.alternateTools changes (see goMain.ts) + // The clearCacheForTools() function is called from the configuration change listener if (useCache && binPathCache[toolName]) { return { binPath: binPathCache[toolName], why: 'cached' }; } @@ -186,13 +187,13 @@ export function clearCacheForTools() { } /** - * Exapnds ~ to homedir in non-Windows platform + * Expands ~ to homedir in non-Windows platform */ export function resolveHomeDir(inputPath: string): string { if (!inputPath || !inputPath.trim()) { return inputPath; } - return inputPath.startsWith('~') ? path.join(os.homedir(), inputPath.substr(1)) : inputPath; + return inputPath.startsWith('~') ? path.join(os.homedir(), inputPath.substring(1)) : inputPath; } // Walks up given folder path to return the closest ancestor that has `src` as a child @@ -206,7 +207,7 @@ export function getInferredGopath(folderPath: string): string | undefined { // find src directory closest to given folder path const srcIdx = dirs.lastIndexOf('src'); if (srcIdx > 0) { - return folderPath.substr(0, dirs.slice(0, srcIdx).join(path.sep).length); + return folderPath.substring(0, dirs.slice(0, srcIdx).join(path.sep).length); } } @@ -236,7 +237,7 @@ export function getCurrentGoWorkspaceFromGOPATH(gopath: string | undefined, curr // 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 (possibleCurrentWorkspace.length > currentWorkspace.length) { - currentWorkspace = currentFileDirPath.substr(0, possibleCurrentWorkspace.length); + currentWorkspace = currentFileDirPath.substring(0, possibleCurrentWorkspace.length); } } } @@ -246,7 +247,7 @@ export function getCurrentGoWorkspaceFromGOPATH(gopath: string | undefined, curr // Workaround for issue in https://github.com/Microsoft/vscode/issues/9448#issuecomment-244804026 export function fixDriveCasingInWindows(pathToFix: string): string { return process.platform === 'win32' && pathToFix - ? pathToFix.substr(0, 1).toUpperCase() + pathToFix.substr(1) + ? pathToFix.substring(0, 1).toUpperCase() + pathToFix.substring(1) : pathToFix; } @@ -260,7 +261,7 @@ export function getToolFromToolPath(toolPath: string): string | undefined { } let tool = path.basename(toolPath); if (process.platform === 'win32' && tool.endsWith('.exe')) { - tool = tool.substr(0, tool.length - 4); + tool = tool.slice(0, -4); } return tool; }