From 5b0a033db37a291a1958ed851013ea9c45323a7e Mon Sep 17 00:00:00 2001 From: Sorin Sbarnea Date: Tue, 16 Apr 2024 16:56:59 +0100 Subject: [PATCH] eslint: no-unsafe-member-access (partial) Related: #1225 --- eslint.config.mjs | 2 +- .../src/ansibleLanguageService.ts | 75 +++++++++++-------- .../src/providers/completionProvider.ts | 4 +- .../src/providers/validationProvider.ts | 3 +- .../src/services/ansibleInventory.ts | 33 +++++--- .../src/services/docsLibrary.ts | 4 +- .../src/services/executionEnvironment.ts | 7 +- .../src/services/settingsManager.ts | 2 +- .../src/utils/docsFinder.ts | 4 +- .../src/utils/docsParser.ts | 2 +- .../src/utils/getAnsibleMetaData.ts | 36 +++++---- .../src/utils/imagePuller.ts | 7 +- .../src/utils/webUtils.ts | 2 +- .../ansible-language-server/src/utils/yaml.ts | 2 +- src/extensionConflicts.ts | 10 ++- .../lightspeed/playbookExplanation.ts | 4 +- 16 files changed, 123 insertions(+), 74 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 840f6303c..3792be3fe 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -56,7 +56,7 @@ export default tseslint.config( // "@typescript-eslint/require-await": "error", // "@typescript-eslint/await-thenable": "error", // "@typescript-eslint/unbound-method": "error", - // "@typescript-eslint/no-unsafe-member-access": "error", + "@typescript-eslint/no-unsafe-member-access": "warn", // "@typescript-eslint/no-floating-promises": "error", // "@typescript-eslint/restrict-template-expressions": "error", // "@typescript-eslint/no-unsafe-argument": "error", diff --git a/packages/ansible-language-server/src/ansibleLanguageService.ts b/packages/ansible-language-server/src/ansibleLanguageService.ts index b2bfc05e8..a019b149c 100644 --- a/packages/ansible-language-server/src/ansibleLanguageService.ts +++ b/packages/ansible-language-server/src/ansibleLanguageService.ts @@ -1,4 +1,5 @@ import { + CompletionItem, Connection, DidChangeConfigurationNotification, DidChangeWatchedFilesNotification, @@ -26,6 +27,11 @@ import { getAnsibleMetaData } from "./utils/getAnsibleMetaData"; import axios from "axios"; import { getBaseUri } from "./utils/webUtils"; +export type PlaybookExplanation = { + accessToken: string; + URL: string; + content: string; +}; /** * Initializes the connection and registers all lifecycle event handlers. * @@ -281,21 +287,23 @@ export class AnsibleLanguageService { return null; }); - this.connection.onCompletionResolve(async (completionItem) => { - try { - if (completionItem.data?.documentUri) { - const context = this.workspaceManager.getContext( - completionItem.data?.documentUri, - ); - if (context) { - return await doCompletionResolve(completionItem, context); + this.connection.onCompletionResolve( + async (completionItem: CompletionItem) => { + try { + if (completionItem.data?.documentUri) { + const context = this.workspaceManager.getContext( + completionItem.data?.documentUri, + ); + if (context) { + return await doCompletionResolve(completionItem, context); + } } + } catch (error) { + this.handleError(error, "onCompletionResolve"); } - } catch (error) { - this.handleError(error, "onCompletionResolve"); - } - return completionItem; - }); + return completionItem; + }, + ); this.connection.onDefinition(async (params) => { try { @@ -352,29 +360,32 @@ export class AnsibleLanguageService { }, ); - this.connection.onRequest("playbook/explanation", async (params) => { - const accessToken: string = params["accessToken"]; - const URL: string = params["URL"]; - const content: string = params["content"]; + this.connection.onRequest( + "playbook/explanation", + async (params: PlaybookExplanation) => { + const accessToken: string = params["accessToken"]; + const URL: string = params["URL"]; + const content: string = params["content"]; - const headers = { - "Content-Type": "application/json", - Authorization: `Bearer ${accessToken}`, - }; - - const axiosInstance = axios.create({ - baseURL: `${getBaseUri(URL)}/api/v0`, - headers: headers, - }); + const headers = { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }; - const explanation: string = await axiosInstance - .post("/ai/explanations/", { content: content }) - .then((response) => { - return response.data.content; + const axiosInstance = axios.create({ + baseURL: `${getBaseUri(URL)}/api/v0`, + headers: headers, }); - return explanation; - }); + const explanation: string = await axiosInstance + .post("/ai/explanations/", { content: content }) + .then((response) => { + return response.data.content; + }); + + return explanation; + }, + ); this.connection.onRequest("playbook/summary", async (params) => { const accessToken: string = params["accessToken"]; diff --git a/packages/ansible-language-server/src/providers/completionProvider.ts b/packages/ansible-language-server/src/providers/completionProvider.ts index 3009b0596..a51a79e00 100644 --- a/packages/ansible-language-server/src/providers/completionProvider.ts +++ b/packages/ansible-language-server/src/providers/completionProvider.ts @@ -278,7 +278,7 @@ export async function doCompletion( .map((option, index) => { // translate option documentation to CompletionItem const details = getDetails(option.specs); - let priority; + let priority: number; if (isAlias(option)) { priority = priorityMap.aliasOption; } else if (option.specs.required) { @@ -363,7 +363,7 @@ export async function doCompletion( choices.push(defaultChoice); } return choices.map((choice, index) => { - let priority; + let priority: number; if (choice === defaultChoice) { priority = priorityMap.defaultChoice; } else { diff --git a/packages/ansible-language-server/src/providers/validationProvider.ts b/packages/ansible-language-server/src/providers/validationProvider.ts index 09113cb3e..91b8013f3 100644 --- a/packages/ansible-language-server/src/providers/validationProvider.ts +++ b/packages/ansible-language-server/src/providers/validationProvider.ts @@ -25,7 +25,7 @@ export async function doValidate( context?: WorkspaceFolderContext, connection?: Connection, ): Promise> { - let diagnosticsByFile; + let diagnosticsByFile: Map | undefined; if (quick || !context) { // get validation from cache diagnosticsByFile = @@ -34,6 +34,7 @@ export async function doValidate( } else { // full validation with ansible-lint or ansible syntax-check (if ansible-lint is not installed or disabled) + let diagnosticsByFile: Map; const settings = await context.documentSettings.get(textDocument.uri); if (!settings.validation.enabled) { connection?.console.log("Validation disabled"); diff --git a/packages/ansible-language-server/src/services/ansibleInventory.ts b/packages/ansible-language-server/src/services/ansibleInventory.ts index b5f517b8b..224157343 100644 --- a/packages/ansible-language-server/src/services/ansibleInventory.ts +++ b/packages/ansible-language-server/src/services/ansibleInventory.ts @@ -3,6 +3,19 @@ import { WorkspaceFolderContext } from "./workspaceManager"; import { CommandRunner } from "../utils/commandRunner"; import { URI } from "vscode-uri"; +type AnsibleHost = { + host: string; + priority: number; +}; + +type AnsibleHostObject = { + [name: string]: { + hosts?: string[]; + children?: string[]; + }; + _meta: object; +}; +// & { _meta?: never }; /** * Class to extend ansible-inventory executable as a service */ @@ -41,9 +54,11 @@ export class AnsibleInventory { defaultHostListPath, ); - let inventoryHostsObject = []; + let inventoryHostsObject: AnsibleHostObject; try { - inventoryHostsObject = JSON.parse(ansibleInventoryResult.stdout); + inventoryHostsObject = JSON.parse( + ansibleInventoryResult.stdout, + ) as AnsibleHostObject; } catch (error) { this.connection.console.error( `Exception in AnsibleInventory service: ${JSON.stringify(error)}`, @@ -64,7 +79,7 @@ export class AnsibleInventory { * @param hostObj - nested object of hosts * @returns an array of object with host and priority as keys */ -function parseInventoryHosts(hostObj) { +function parseInventoryHosts(hostObj: AnsibleHostObject) { const topLevelGroups = hostObj.all.children.filter( (item: string) => item !== "ungrouped", ); @@ -77,16 +92,16 @@ function parseInventoryHosts(hostObj) { // Set priorities: top level groups (1), other groups (2), ungrouped (3), hosts for groups (4), localhost (5) const topLevelGroupsObjList = topLevelGroups.map((item) => { - return { host: item, priority: 1 }; + return { host: item, priority: 1 } as AnsibleHost; }); const otherGroupsObjList = otherGroups.map((item) => { - return { host: item, priority: 2 }; + return { host: item, priority: 2 } as AnsibleHost; }); const allGroups = [...topLevelGroupsObjList, ...otherGroupsObjList]; - let ungroupedHostsObjList = []; + let ungroupedHostsObjList: AnsibleHost[] = []; if (hostObj.ungrouped) { ungroupedHostsObjList = hostObj.ungrouped.hosts.map((item) => { return { host: item, priority: 3 }; @@ -94,15 +109,15 @@ function parseInventoryHosts(hostObj) { } // Add 'localhost' and 'all' to the inventory list - const localhostObj = { host: "localhost", priority: 5 }; - const allHostObj = { host: "all", priority: 6 }; + const localhostObj = { host: "localhost", priority: 5 } as AnsibleHost; + const allHostObj = { host: "all", priority: 6 } as AnsibleHost; let allHosts = [localhostObj, allHostObj, ...ungroupedHostsObjList]; for (const group of allGroups) { if (hostObj[`${group.host}`] && hostObj[`${group.host}`].hosts) { const hostsObj = hostObj[`${group.host}`].hosts.map((item) => { - return { host: item, priority: 4 }; + return { host: item, priority: 4 } as AnsibleHost; }); allHosts = [...allHosts, ...hostsObj]; } diff --git a/packages/ansible-language-server/src/services/docsLibrary.ts b/packages/ansible-language-server/src/services/docsLibrary.ts index fb9ad48dc..d8c408acd 100644 --- a/packages/ansible-language-server/src/services/docsLibrary.ts +++ b/packages/ansible-language-server/src/services/docsLibrary.ts @@ -127,7 +127,7 @@ export class DocsLibrary { ); // check routing - let moduleRoute; + let moduleRoute: IPluginRoute | undefined; for (const fqcn of candidateFqcns) { moduleRoute = this.getModuleRoute(fqcn); if (moduleRoute) { @@ -137,7 +137,7 @@ export class DocsLibrary { } // find module - let module; + let module: IModuleMetadata; if (moduleRoute && moduleRoute.redirect) { module = this.modules.get(moduleRoute.redirect); } else { diff --git a/packages/ansible-language-server/src/services/executionEnvironment.ts b/packages/ansible-language-server/src/services/executionEnvironment.ts index f74bbdd97..875a3e1c4 100644 --- a/packages/ansible-language-server/src/services/executionEnvironment.ts +++ b/packages/ansible-language-server/src/services/executionEnvironment.ts @@ -2,7 +2,10 @@ import * as child_process from "child_process"; import * as fs from "fs"; import * as path from "path"; import { URI } from "vscode-uri"; -import { Connection } from "vscode-languageserver"; +import { + Connection, + WorkDoneProgressServerReporter, +} from "vscode-languageserver"; import { v4 as uuidv4 } from "uuid"; import { AnsibleConfig } from "./ansibleConfig"; import { ImagePuller } from "../utils/imagePuller"; @@ -88,7 +91,7 @@ export class ExecutionEnvironment { /[^a-z0-9]/gi, "_", )}`; - let progressTracker; + let progressTracker: WorkDoneProgressServerReporter; try { const containerImageIdCommand = `${this._container_engine} images ${this._container_image} --format="{{.ID}}" | head -n 1`; diff --git a/packages/ansible-language-server/src/services/settingsManager.ts b/packages/ansible-language-server/src/services/settingsManager.ts index 7db092920..173094fd0 100644 --- a/packages/ansible-language-server/src/services/settingsManager.ts +++ b/packages/ansible-language-server/src/services/settingsManager.ts @@ -225,7 +225,7 @@ export class SettingsManager { */ private _settingsAdjustment(settingsObject) { for (const key in settingsObject) { - const value = settingsObject[key]; + const value: any = settingsObject[key]; if (value && typeof value === "object") { if (value.default !== undefined) { diff --git a/packages/ansible-language-server/src/utils/docsFinder.ts b/packages/ansible-language-server/src/utils/docsFinder.ts index ce4a7dc20..90992b9c7 100644 --- a/packages/ansible-language-server/src/utils/docsFinder.ts +++ b/packages/ansible-language-server/src/utils/docsFinder.ts @@ -20,7 +20,7 @@ export async function findDocumentation( if (!fs.existsSync(dir) || fs.lstatSync(dir).isFile()) { return []; } - let files; + let files: string[]; switch (kind) { case "builtin": files = await globArray([`${dir}/**/*.py`, "!/**/_*.py"]); @@ -90,7 +90,7 @@ export async function findPluginRouting( if (!fs.existsSync(dir) || fs.lstatSync(dir).isFile()) { return pluginRouting; } - let files; + let files: string[]; switch (kind) { case "builtin": files = await globArray([`${dir}/config/ansible_builtin_runtime.yml`]); diff --git a/packages/ansible-language-server/src/utils/docsParser.ts b/packages/ansible-language-server/src/utils/docsParser.ts index 0256dde46..a6951e229 100644 --- a/packages/ansible-language-server/src/utils/docsParser.ts +++ b/packages/ansible-language-server/src/utils/docsParser.ts @@ -247,7 +247,7 @@ export class LazyModuleDocumentation implements IModuleMetadata { if (!this._contents) { this._contents = new Map>(); const contents = fs.readFileSync(this.source, { encoding: "utf8" }); - let m; + let m: RegExpExecArray | null; while ((m = LazyModuleDocumentation.docsRegex.exec(contents)) !== null) { if (m && m.groups && m.groups.name && m.groups.doc && m.groups.pre) { if (m.groups.name === DOCUMENTATION) { diff --git a/packages/ansible-language-server/src/utils/getAnsibleMetaData.ts b/packages/ansible-language-server/src/utils/getAnsibleMetaData.ts index fef235172..cfd090fd9 100644 --- a/packages/ansible-language-server/src/utils/getAnsibleMetaData.ts +++ b/packages/ansible-language-server/src/utils/getAnsibleMetaData.ts @@ -7,6 +7,14 @@ import * as child_process from "child_process"; let context: WorkspaceFolderContext; let connection: Connection; +type AnsibleMetaData = { + "ansible information": AnsibleInfo; +}; + +type AnsibleInfo = { + x: string; +}; + export async function getAnsibleMetaData( contextLocal: WorkspaceFolderContext, connectionLocal: Connection, @@ -14,7 +22,7 @@ export async function getAnsibleMetaData( context = contextLocal; connection = connectionLocal; - const ansibleMetaData = {}; + const ansibleMetaData = {} as AnsibleMetaData; ansibleMetaData["ansible information"] = await getAnsibleInfo(); ansibleMetaData["python information"] = await getPythonInfo(); @@ -40,9 +48,8 @@ export async function getResultsThroughCommandRunner(cmd, arg) { const workingDirectory = URI.parse(context.workspaceFolder.uri).path; const mountPaths = new Set([workingDirectory]); - let result; try { - result = await commandRunner.runCommand( + const result = await commandRunner.runCommand( cmd, arg, workingDirectory, @@ -56,17 +63,18 @@ export async function getResultsThroughCommandRunner(cmd, arg) { return result; } } catch (error) { + const msg = error instanceof Error ? error.message : (error as string); console.log( - `cmd '${cmd} ${arg}' was not executed with the following error: ' ${error.toString()}`, + `cmd '${cmd} ${arg}' was not executed with the following error: ' ${msg}`, ); return undefined; } - return result; + return undefined; } async function getAnsibleInfo() { - const ansibleInfo = {}; + const ansibleInfo = {} as AnsibleInfo; const ansibleVersionObj = (await context.ansibleConfig).ansible_meta_data; const ansibleVersionObjKeys = Object.keys(ansibleVersionObj); @@ -76,7 +84,7 @@ async function getAnsibleInfo() { return ansibleInfo; } - let ansibleCoreVersion; + let ansibleCoreVersion: string[]; if (ansibleVersionObjKeys[0].includes(" [")) { ansibleCoreVersion = ansibleVersionObjKeys[0].split(" ["); } else { @@ -136,7 +144,7 @@ async function getPythonInfo() { async function getAnsibleLintInfo() { const ansibleLintInfo = {}; - let ansibleLintVersionResult = await getResultsThroughCommandRunner( + const ansibleLintVersionResult = await getResultsThroughCommandRunner( "ansible-lint", "--version", ); @@ -153,10 +161,12 @@ async function getAnsibleLintInfo() { // ansible-lint version reports if a newer version of the ansible-lint is available or not // along with the current version itself // so the following lines of code are to segregate the two information into to keys - ansibleLintVersionResult = ansibleLintVersionResult.stdout.trim().split("\n"); - const ansibleLintVersion = ansibleLintVersionResult[0]; - const ansibleLintUpgradeStatus = ansibleLintVersionResult[1] - ? ansibleLintVersionResult[1] + const ansibleLintVersionArray = ansibleLintVersionResult.stdout + .trim() + .split("\n"); + const ansibleLintVersion = ansibleLintVersionArray[0]; + const ansibleLintUpgradeStatus = ansibleLintVersionArray[1] + ? ansibleLintVersionArray[1] : undefined; ansibleLintInfo["version"] = ansibleLintVersion @@ -186,7 +196,7 @@ async function getExecutionEnvironmentInfo() { eeInfo["container image ID"] = basicDetails.containerImageId; let eeServiceWorking = false; - let inspectResult; + let inspectResult: unknown; try { inspectResult = JSON.parse( child_process diff --git a/packages/ansible-language-server/src/utils/imagePuller.ts b/packages/ansible-language-server/src/utils/imagePuller.ts index 90264efca..a14f0ea95 100644 --- a/packages/ansible-language-server/src/utils/imagePuller.ts +++ b/packages/ansible-language-server/src/utils/imagePuller.ts @@ -1,5 +1,8 @@ import * as child_process from "child_process"; -import { Connection } from "vscode-languageserver"; +import { + Connection, + WorkDoneProgressServerReporter, +} from "vscode-languageserver"; import { WorkspaceFolderContext } from "../services/workspaceManager"; export class ImagePuller { @@ -35,7 +38,7 @@ export class ImagePuller { const imagePresent = this.checkForImage(); const pullRequired = this.determinePull(imagePresent, imageTag); - let progressTracker; + let progressTracker: WorkDoneProgressServerReporter; if (this.useProgressTracker) { progressTracker = await this.connection.window.createWorkDoneProgress(); } diff --git a/packages/ansible-language-server/src/utils/webUtils.ts b/packages/ansible-language-server/src/utils/webUtils.ts index f95c75d82..b7b4c64b6 100644 --- a/packages/ansible-language-server/src/utils/webUtils.ts +++ b/packages/ansible-language-server/src/utils/webUtils.ts @@ -1,6 +1,6 @@ // partially duplicated from ./src/features/lightspeed/utils/webUtils.ts /* Get base uri in a correct formatted way */ -export function getBaseUri(URL) { +export function getBaseUri(URL: string): string { const baseUri = URL.trim(); return baseUri.endsWith("/") ? baseUri.slice(0, -1) : baseUri; } diff --git a/packages/ansible-language-server/src/utils/yaml.ts b/packages/ansible-language-server/src/utils/yaml.ts index 0c7ef2ec6..bd16be7a8 100644 --- a/packages/ansible-language-server/src/utils/yaml.ts +++ b/packages/ansible-language-server/src/utils/yaml.ts @@ -405,7 +405,7 @@ export async function getPossibleOptionsForPath( const taskParamNode = taskParamPath[taskParamPath.length - 1]; if (!isScalar(taskParamNode)) return null; - let module; + let module: IModuleMetadata | undefined; // Module options can either be directly under module or in 'args' if (taskParamNode.value === "args") { module = await findProvidedModule(taskParamPath, document, docsLibrary); diff --git a/src/extensionConflicts.ts b/src/extensionConflicts.ts index ea7b0c241..06911ba26 100644 --- a/src/extensionConflicts.ts +++ b/src/extensionConflicts.ts @@ -18,7 +18,7 @@ const conflictingIDs = [ const uninstallingIDs = new Set(); // eslint-disable-next-line @typescript-eslint/no-explicit-any -function isExtensionPresent(obj: any): obj is Extension { +function isExtensionPresent(obj: Extension | undefined): boolean { return typeof obj !== "undefined" && !uninstallingIDs.has(obj.id); } @@ -49,6 +49,10 @@ export async function showUninstallConflictsNotification( uninstallingIDs.add(ext.id); } + function getExtName(ext: Extension): string { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + return ext?.packageJSON?.["displayName"] ?? ""; + } const uninstallMsg = "Uninstall"; if (!conflictingExts.length) { @@ -57,10 +61,10 @@ export async function showUninstallConflictsNotification( // Gather all the conflicting display names let conflictMsg = ""; if (conflictingExts.length === 1) { - conflictMsg = `${conflictingExts[0].packageJSON.displayName} (${conflictingExts[0].id}) extension is incompatible with redhat.ansible. Please uninstall it.`; + conflictMsg = `${getExtName(conflictingExts[0])} (${conflictingExts[0].id}) extension is incompatible with redhat.ansible. Please uninstall it.`; } else { const extNames: string = conflictingExts - .map((ext) => `${ext.packageJSON.displayName} (${ext.id})`) + .map((ext) => `${getExtName(ext)} (${ext.id})`) .join(", "); conflictMsg = `The ${extNames} extensions are incompatible with redhat.ansible. Please uninstall them.`; } diff --git a/src/features/lightspeed/playbookExplanation.ts b/src/features/lightspeed/playbookExplanation.ts index 45df3ca8a..10521abfc 100644 --- a/src/features/lightspeed/playbookExplanation.ts +++ b/src/features/lightspeed/playbookExplanation.ts @@ -7,6 +7,8 @@ import * as marked from "marked"; import { SettingsManager } from "../../settings"; import { lightSpeedManager } from "../../extension"; +import { PlaybookExplanation } from "@ansible/ansible-language-server/src/ansibleLanguageService"; + export const playbookExplanation = async ( extensionUri: vscode.Uri, client: LanguageClient, @@ -40,7 +42,7 @@ export const playbookExplanation = async ( accessToken: accessToken, URL: settingsManager.settings.lightSpeedService.URL, content: content, - }); + } as PlaybookExplanation); } catch (e) { console.log(e); currentPanel.setContent(