diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2b4c74be..08fa9650 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: fail-fast: false matrix: os: [macos-11, ubuntu-latest, windows-latest] - ghc: [9.0.1, 8.10.4] + ghc: [9.0.2, 8.10.4] runs-on: ${{ matrix.os }} steps: - name: Checkout diff --git a/src/errors.ts b/src/errors.ts index 139da6a1..6db35587 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -54,6 +54,6 @@ export class NoMatchingHls extends Error { super(`HLS does not support GHC ${ghcProjVersion} yet.`); } public docLink(): Uri { - return Uri.parse('https://haskell-language-server.readthedocs.io/en/latest/supported-versions.html'); + return Uri.parse('https://haskell-language-server.readthedocs.io/en/latest/supported-versions.html'); } } diff --git a/src/extension.ts b/src/extension.ts index eb483881..0eb22da6 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -223,7 +223,7 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold let serverEnvironment: IEnvVars = await workspace.getConfiguration('haskell', uri).serverEnvironment; if (addInternalServerPath !== undefined) { - const newPath = await addPathToProcessPath(addInternalServerPath, logger); + const newPath = await addPathToProcessPath(addInternalServerPath); serverEnvironment = { ...serverEnvironment, ...{ PATH: newPath }, diff --git a/src/hlsBinaries.ts b/src/hlsBinaries.ts index 0be9b046..bd4ced2e 100644 --- a/src/hlsBinaries.ts +++ b/src/hlsBinaries.ts @@ -5,7 +5,6 @@ import { stat } from 'fs/promises'; import * as https from 'https'; import * as path from 'path'; import { match } from 'ts-pattern'; -import * as url from 'url'; import { promisify } from 'util'; import { ConfigurationTarget, ExtensionContext, ProgressLocation, window, workspace, WorkspaceFolder } from 'vscode'; import { Logger } from 'vscode-languageclient'; @@ -68,7 +67,7 @@ async function callAsync( envAdd?: IEnvVars, callback?: ProcessCallback ): Promise { - let newEnv: IEnvVars = await resolveServerEnvironmentPATH( + let newEnv: IEnvVars = resolveServerEnvironmentPATH( workspace.getConfiguration('haskell').get('serverEnvironment') || {} ); newEnv = { ...(process.env as IEnvVars), ...newEnv, ...(envAdd || {}) }; @@ -135,7 +134,6 @@ async function callAsync( /** Gets serverExecutablePath and fails if it's not set. */ async function findServerExecutable( - context: ExtensionContext, logger: Logger, folder?: WorkspaceFolder ): Promise { @@ -143,7 +141,7 @@ async function findServerExecutable( logger.info(`Trying to find the server executable in: ${exePath}`); exePath = resolvePathPlaceHolders(exePath, folder); logger.log(`Location after path variables substitution: ${exePath}`); - if (await executableExists(exePath)) { + if (executableExists(exePath)) { return exePath; } else { const msg = `Could not find a HLS binary at ${exePath}! Consider installing HLS via ghcup or change "haskell.manageHLS" in your settings.`; @@ -153,13 +151,13 @@ async function findServerExecutable( /** Searches the PATH. Fails if nothing is found. */ -async function findHLSinPATH(context: ExtensionContext, logger: Logger, folder?: WorkspaceFolder): Promise { +async function findHLSinPATH(_context: ExtensionContext, logger: Logger): Promise { // try PATH const exes: string[] = ['haskell-language-server-wrapper', 'haskell-language-server']; logger.info(`Searching for server executables ${exes.join(',')} in $PATH`); logger.info(`$PATH environment variable: ${process.env.PATH}`); for (const exe of exes) { - if (await executableExists(exe)) { + if (executableExists(exe)) { logger.info(`Found server executable in $PATH: ${exe}`); return exe; } @@ -189,7 +187,7 @@ export async function findHaskellLanguageServer( logger.info('Finding haskell-language-server'); if (workspace.getConfiguration('haskell').get('serverExecutablePath') as string) { - const exe = await findServerExecutable(context, logger, folder); + const exe = await findServerExecutable(logger, folder); return [exe, undefined]; } @@ -226,7 +224,7 @@ export async function findHaskellLanguageServer( } if (manageHLS === 'PATH') { - const exe = await findHLSinPATH(context, logger, folder); + const exe = await findHLSinPATH(context, logger); return [exe, undefined]; } else { // we manage HLS, make sure ghcup is installed/available @@ -267,7 +265,7 @@ export async function findHaskellLanguageServer( latestStack = await getLatestToolFromGHCup(context, logger, 'stack'); } if (recGHC === undefined) { - recGHC = !(await executableExists('ghc')) + recGHC = !executableExists('ghc') ? await getLatestAvailableToolFromGHCup(context, logger, 'ghc', 'recommended') : null; } @@ -275,32 +273,34 @@ export async function findHaskellLanguageServer( // download popups const promptBeforeDownloads = workspace.getConfiguration('haskell').get('promptBeforeDownloads') as boolean; if (promptBeforeDownloads) { - const hlsInstalled = latestHLS - ? await toolInstalled(context, logger, 'hls', latestHLS) - : undefined; - const cabalInstalled = latestCabal - ? await toolInstalled(context, logger, 'cabal', latestCabal) - : undefined; - const stackInstalled = latestStack - ? await toolInstalled(context, logger, 'stack', latestStack) - : undefined; + const hlsInstalled = latestHLS ? await toolInstalled(context, logger, 'hls', latestHLS) : undefined; + const cabalInstalled = latestCabal ? await toolInstalled(context, logger, 'cabal', latestCabal) : undefined; + const stackInstalled = latestStack ? await toolInstalled(context, logger, 'stack', latestStack) : undefined; const ghcInstalled = executableExists('ghc') - ? new InstalledTool('ghc', await callAsync(`ghc${exeExt}`, ['--numeric-version'], logger, undefined, undefined, false)) - // if recGHC is null, that means user disabled automatic handling, - : (recGHC !== null ? await toolInstalled(context, logger, 'ghc', recGHC) : undefined); - const toInstall: InstalledTool[] = [hlsInstalled, cabalInstalled, stackInstalled, ghcInstalled] - .filter((tool) => tool && !tool.installed) as InstalledTool[]; + ? new InstalledTool( + 'ghc', + await callAsync(`ghc${exeExt}`, ['--numeric-version'], logger, undefined, undefined, false) + ) + : // if recGHC is null, that means user disabled automatic handling, + recGHC !== null + ? await toolInstalled(context, logger, 'ghc', recGHC) + : undefined; + const toInstall: InstalledTool[] = [hlsInstalled, cabalInstalled, stackInstalled, ghcInstalled].filter( + (tool) => tool && !tool.installed + ) as InstalledTool[]; if (toInstall.length > 0) { const decision = await window.showInformationMessage( - `Need to download ${toInstall.map(t => t.nameWithVersion).join(', ')}, continue?`, + `Need to download ${toInstall.map((t) => t.nameWithVersion).join(', ')}, continue?`, 'Yes', 'No', "Yes, don't ask again" ); if (decision === 'Yes') { - logger.info(`User accepted download for ${toInstall.map(t => t.nameWithVersion).join(', ')}.`); + logger.info(`User accepted download for ${toInstall.map((t) => t.nameWithVersion).join(', ')}.`); } else if (decision === "Yes, don't ask again") { - logger.info(`User accepted download for ${toInstall.map(t => t.nameWithVersion).join(', ')} and won't be asked again.`); + logger.info( + `User accepted download for ${toInstall.map((t) => t.nameWithVersion).join(', ')} and won't be asked again.` + ); workspace.getConfiguration('haskell').update('promptBeforeDownloads', false); } else { toInstall.forEach((tool) => { @@ -353,26 +353,25 @@ export async function findHaskellLanguageServer( // more download popups if (promptBeforeDownloads) { - const hlsInstalled = projectHls - ? await toolInstalled(context, logger, 'hls', projectHls) - : undefined; - const ghcInstalled = projectGhc - ? await toolInstalled(context, logger, 'ghc', projectGhc) - : undefined; - const toInstall: InstalledTool[] = [hlsInstalled, ghcInstalled] - .filter((tool) => tool && !tool.installed) as InstalledTool[]; + const hlsInstalled = projectHls ? await toolInstalled(context, logger, 'hls', projectHls) : undefined; + const ghcInstalled = projectGhc ? await toolInstalled(context, logger, 'ghc', projectGhc) : undefined; + const toInstall: InstalledTool[] = [hlsInstalled, ghcInstalled].filter( + (tool) => tool && !tool.installed + ) as InstalledTool[]; if (toInstall.length > 0) { const decision = await window.showInformationMessage( - `Need to download ${toInstall.map(t => t.nameWithVersion).join(', ')}, continue?`, + `Need to download ${toInstall.map((t) => t.nameWithVersion).join(', ')}, continue?`, { modal: true }, 'Yes', 'No', "Yes, don't ask again" ); if (decision === 'Yes') { - logger.info(`User accepted download for ${toInstall.map(t => t.nameWithVersion).join(', ')}.`); + logger.info(`User accepted download for ${toInstall.map((t) => t.nameWithVersion).join(', ')}.`); } else if (decision === "Yes, don't ask again") { - logger.info(`User accepted download for ${toInstall.map(t => t.nameWithVersion).join(', ')} and won't be asked again.`); + logger.info( + `User accepted download for ${toInstall.map((t) => t.nameWithVersion).join(', ')} and won't be asked again.` + ); workspace.getConfiguration('haskell').update('promptBeforeDownloads', false); } else { toInstall.forEach((tool) => { @@ -400,14 +399,22 @@ export async function findHaskellLanguageServer( ...(projectGhc ? ['--ghc', projectGhc] : []), '--install', ], - `Installing project specific toolchain: ${[['hls', projectHls], ['GHC', projectGhc], ['cabal', latestCabal], ['stack', latestStack]].filter(t => t[1]).map(t => `${t[0]}-${t[1]}`).join(', ')}`, + `Installing project specific toolchain: ${[ + ['hls', projectHls], + ['GHC', projectGhc], + ['cabal', latestCabal], + ['stack', latestStack], + ] + .filter((t) => t[1]) + .map((t) => `${t[0]}-${t[1]}`) + .join(', ')}`, true ); if (projectHls) { return [path.join(hlsBinDir, `haskell-language-server-wrapper${exeExt}`), hlsBinDir]; } else { - const exe = await findHLSinPATH(context, logger, folder); + const exe = await findHLSinPATH(context, logger); return [exe, hlsBinDir]; } } @@ -470,8 +477,8 @@ async function getLatestProjectHLS( const merged = new Map([...metadataMap, ...ghcupMap]); // right-biased // now sort and get the latest suitable version const latest = [...merged] - .filter(([k, v]) => v.some((x) => x === projectGhc)) - .sort(([k1, v1], [k2, v2]) => comparePVP(k1, k2)) + .filter(([_k, v]) => v.some((x) => x === projectGhc)) + .sort(([k1, _v1], [k2, _v2]) => comparePVP(k1, k2)) .pop(); if (!latest) { @@ -484,7 +491,7 @@ async function getLatestProjectHLS( /** * Obtain the project ghc version from the HLS - Wrapper (which must be in PATH now). * Also, serves as a sanity check. - * @param wrapper Path to the Haskell-Language-Server wrapper + * @param toolchainBindir Path to the toolchainn bin directory (added to PATH) * @param workingDir Directory to run the process, usually the root of the workspace. * @param logger Logger for feedback. * @returns The GHC version, or fail with an `Error`. @@ -499,7 +506,7 @@ export async function getProjectGHCVersion( const args = ['--project-ghc-version']; - const newPath = await addPathToProcessPath(toolchainBindir, logger); + const newPath = await addPathToProcessPath(toolchainBindir); const environmentNew: IEnvVars = { PATH: newPath, }; @@ -550,14 +557,14 @@ export async function upgradeGHCup(context: ExtensionContext, logger: Logger): P } } -export async function findGHCup(context: ExtensionContext, logger: Logger, folder?: WorkspaceFolder): Promise { +export async function findGHCup(_context: ExtensionContext, logger: Logger, folder?: WorkspaceFolder): Promise { logger.info('Checking for ghcup installation'); let exePath = workspace.getConfiguration('haskell').get('ghcupExecutablePath') as string; if (exePath) { logger.info(`Trying to find the ghcup executable in: ${exePath}`); exePath = resolvePathPlaceHolders(exePath, folder); logger.log(`Location after path variables substitution: ${exePath}`); - if (await executableExists(exePath)) { + if (executableExists(exePath)) { return exePath; } else { throw new Error(`Could not find a ghcup binary at ${exePath}!`); @@ -684,8 +691,8 @@ async function toolInstalled( version: string ): Promise { const b = await callGHCup(context, logger, ['whereis', tool, version], undefined, false) - .then((x) => true) - .catch((x) => false); + .then((_x) => true) + .catch((_x) => false); return new InstalledTool(tool, version, b); } @@ -737,7 +744,7 @@ export type ReleaseMetadata = Map>>; */ async function getHLSesfromMetadata(context: ExtensionContext, logger: Logger): Promise | null> { const storagePath: string = await getStoragePath(context); - const metadata = await getReleaseMetadata(context, storagePath, logger).catch((e) => null); + const metadata = await getReleaseMetadata(context, storagePath, logger).catch((_e) => null); if (!metadata) { window.showErrorMessage('Could not get release metadata'); return null; @@ -803,23 +810,23 @@ export function findSupportedHlsPerGhc( /** * Download GHCUP metadata. * - * @param context Extension context. + * @param _context Extension context. * @param storagePath Path to put in binary files and caches. * @param logger Logger for feedback. * @returns Metadata of releases, or null if the cache can not be found. */ async function getReleaseMetadata( - context: ExtensionContext, + _context: ExtensionContext, storagePath: string, logger: Logger ): Promise { const releasesUrl = workspace.getConfiguration('haskell').releasesURL - ? url.parse(workspace.getConfiguration('haskell').releasesURL) + ? new URL(workspace.getConfiguration('haskell').releasesURL) : undefined; const opts: https.RequestOptions = releasesUrl ? { host: releasesUrl.host, - path: releasesUrl.path, + path: releasesUrl.pathname, } : { host: 'raw.githubusercontent.com', diff --git a/src/utils.ts b/src/utils.ts index 05f329e3..8aba68f2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -6,7 +6,6 @@ import * as http from 'http'; import * as https from 'https'; import * as os from 'os'; import { extname } from 'path'; -import * as url from 'url'; import { promisify } from 'util'; import { OutputChannel, ProgressLocation, window, workspace, WorkspaceFolder } from 'vscode'; import { Logger } from 'vscode-languageclient'; @@ -215,10 +214,10 @@ export async function downloadFile(titleMsg: string, src: string, dest: string): }, async (progress) => { const p = new Promise((resolve, reject) => { - const srcUrl = url.parse(src); + const srcUrl = new URL(src); const opts: https.RequestOptions = { host: srcUrl.host, - path: srcUrl.path, + path: srcUrl.pathname, protocol: srcUrl.protocol, port: srcUrl.port, headers: userAgentHeader, @@ -230,9 +229,9 @@ export async function downloadFile(titleMsg: string, src: string, dest: string): // Decompress it if it's a gzip or zip const needsGunzip = - res.headers['content-type'] === 'application/gzip' || extname(srcUrl.path ?? '') === '.gz'; + res.headers['content-type'] === 'application/gzip' || extname(srcUrl.pathname ?? '') === '.gz'; const needsUnzip = - res.headers['content-type'] === 'application/zip' || extname(srcUrl.path ?? '') === '.zip'; + res.headers['content-type'] === 'application/zip' || extname(srcUrl.pathname ?? '') === '.zip'; if (needsGunzip) { const gunzip = createGunzip(); gunzip.on('error', reject); @@ -360,7 +359,7 @@ export function resolvePATHPlaceHolders(path: string) { } // also honours serverEnvironment.PATH -export async function addPathToProcessPath(extraPath: string, logger: Logger): Promise { +export async function addPathToProcessPath(extraPath: string): Promise { const pathSep = process.platform === 'win32' ? ';' : ':'; const serverEnvironment: IEnvVars = (await workspace.getConfiguration('haskell').get('serverEnvironment')) || {}; const path: string[] = serverEnvironment.PATH