From f596c8e1baac0bbf7521fe6098f2a0230fa22cc0 Mon Sep 17 00:00:00 2001 From: Sorin Sbarnea Date: Wed, 24 Apr 2024 15:47:22 +0100 Subject: [PATCH] More types Related: #1225 --- packages/ansible-language-server/Taskfile.yml | 4 +- .../src/providers/completionProvider.ts | 90 ++++++++++--------- .../src/services/ansibleInventory.ts | 44 ++++++--- .../test/providers/hoverProvider.test.ts | 44 +++++++-- 4 files changed, 122 insertions(+), 60 deletions(-) diff --git a/packages/ansible-language-server/Taskfile.yml b/packages/ansible-language-server/Taskfile.yml index 6f16f843c..db5d9c5a6 100644 --- a/packages/ansible-language-server/Taskfile.yml +++ b/packages/ansible-language-server/Taskfile.yml @@ -43,7 +43,7 @@ tasks: desc: Update dependencies cmds: - npm install -g npm@latest - - # installs tools from .tool-versions + # - installs tools from .tool-versions - asdf install - "{{.VIRTUAL_ENV}}/bin/python3 -m pre_commit autoupdate" - npm outdated @@ -88,6 +88,7 @@ tasks: dir: "{{ .TASKFILE_DIR }}" desc: Run only ee tests cmds: + - task: build - > source {{.VIRTUAL_ENV}}/bin/activate && bash -c 'npm run test-with-ee' @@ -96,6 +97,7 @@ tasks: desc: Run only non-ee tests dir: "{{ .TASKFILE_DIR }}" cmds: + - task: build - > source {{.VIRTUAL_ENV}}/bin/activate && bash -c 'npm run test-without-ee' diff --git a/packages/ansible-language-server/src/providers/completionProvider.ts b/packages/ansible-language-server/src/providers/completionProvider.ts index 3009b0596..75a0fbc70 100644 --- a/packages/ansible-language-server/src/providers/completionProvider.ts +++ b/packages/ansible-language-server/src/providers/completionProvider.ts @@ -156,7 +156,7 @@ export async function doCompletion( const inlineCollections = getDeclaredCollections(path); const cursorAtEndOfLine = atEndOfLine(document, position); - let textEdit: TextEdit | undefined; + let textEdit: TextEdit; const nodeRange = getNodeRange(node, document); if (nodeRange) { textEdit = { @@ -347,47 +347,49 @@ export async function doCompletion( const nodeRange = getNodeRange(node, document); const option = keyOptions.get(keyNode.value as string); - const choices = []; - let defaultChoice = option.default; - if (option.type === "bool" && typeof option.default === "string") { - // the YAML parser does not recognize values such as 'Yes'/'no' as booleans - defaultChoice = - option.default.toLowerCase() === "yes" ? true : false; - } - if (option.choices) { - choices.push(...option.choices); - } else if (option.type === "bool") { - choices.push(true); - choices.push(false); - } else if (defaultChoice !== undefined) { - choices.push(defaultChoice); - } - return choices.map((choice, index) => { - let priority; - if (choice === defaultChoice) { - priority = priorityMap.defaultChoice; - } else { - priority = priorityMap.choice; + if (option) { + const choices = []; + let defaultChoice = option.default; + if (option.type === "bool" && typeof option.default === "string") { + // the YAML parser does not recognize values such as 'Yes'/'no' as booleans + defaultChoice = + option.default.toLowerCase() === "yes" ? true : false; } - const insertValue = new String(choice).toString(); - const completionItem: CompletionItem = { - label: insertValue, - detail: choice === defaultChoice ? "default" : undefined, - // using index preserves order from the specification - // except when overridden by the priority - sortText: priority.toString() + index.toString().padStart(3), - kind: CompletionItemKind.Value, - }; - if (nodeRange) { - completionItem.textEdit = { - range: nodeRange, - newText: insertValue, - }; - } else { - completionItem.insertText = insertValue; + if (option.choices) { + choices.push(...option.choices); + } else if (option.type === "bool") { + choices.push(true); + choices.push(false); + } else if (defaultChoice !== undefined) { + choices.push(defaultChoice); } - return completionItem; - }); + return choices.map((choice, index) => { + let priority; + if (choice === defaultChoice) { + priority = priorityMap.defaultChoice; + } else { + priority = priorityMap.choice; + } + const insertValue = new String(choice).toString(); + const completionItem: CompletionItem = { + label: insertValue, + detail: choice === defaultChoice ? "default" : undefined, + // using index preserves order from the specification + // except when overridden by the priority + sortText: priority.toString() + index.toString().padStart(3), + kind: CompletionItemKind.Value, + }; + if (nodeRange) { + completionItem.textEdit = { + range: nodeRange, + newText: insertValue, + }; + } else { + completionItem.insertText = insertValue; + } + return completionItem; + }); + } } } @@ -616,7 +618,13 @@ function atEndOfLine(document: TextDocument, position: Position): boolean { * @param nodeRange - range of the keyword in the document * @returns boolean true if the key is the first element of the list, else false */ -function firstElementOfList(document: TextDocument, nodeRange: Range): boolean { +function firstElementOfList( + document: TextDocument, + nodeRange: Range | undefined, +): boolean { + if (!nodeRange) { + return false; + } const checkNodeRange = { start: { line: nodeRange.start.line, character: 0 }, end: nodeRange.start, diff --git a/packages/ansible-language-server/src/services/ansibleInventory.ts b/packages/ansible-language-server/src/services/ansibleInventory.ts index d7d81c65a..ff85dc8fa 100644 --- a/packages/ansible-language-server/src/services/ansibleInventory.ts +++ b/packages/ansible-language-server/src/services/ansibleInventory.ts @@ -3,8 +3,23 @@ import { WorkspaceFolderContext } from "./workspaceManager"; import { CommandRunner } from "../utils/commandRunner"; import { URI } from "vscode-uri"; +type HostType = { host: string; priority: number }; + +type inventoryHostEntry = { + children: string[]; + hosts: string[]; +}; + +type inventoryType = Omit< + { + [name: string]: inventoryHostEntry; + }, + "_meta" +>; + /* Example of minimal inventory object, anything else may be missing. + { "_meta": { "hostvars": {} @@ -60,7 +75,7 @@ Example of more complex inventory. export class AnsibleInventory { private connection: Connection; private context: WorkspaceFolderContext; - private _hostList: unknown[]; + private _hostList: HostType[] = []; constructor(connection: Connection, context: WorkspaceFolderContext) { this.connection = connection; @@ -92,9 +107,11 @@ export class AnsibleInventory { defaultHostListPath, ); - let inventoryHostsObject = []; + let inventoryHostsObject = {} as inventoryType; try { - inventoryHostsObject = JSON.parse(ansibleInventoryResult.stdout); + inventoryHostsObject = JSON.parse( + ansibleInventoryResult.stdout, + ) as inventoryType; } catch (error) { this.connection.console.error( `Exception in AnsibleInventory service: ${JSON.stringify(error)}`, @@ -115,7 +132,7 @@ export class AnsibleInventory { * @param hostObj - nested object of hosts * @returns an array of object with host and priority as keys */ -function parseInventoryHosts(hostObj: object): unknown[] { +function parseInventoryHosts(hostObj: inventoryType): HostType[] { if ( !( "all" in hostObj && @@ -145,9 +162,12 @@ function parseInventoryHosts(hostObj: object): unknown[] { return { host: item, priority: 2 }; }); - const allGroups = [...topLevelGroupsObjList, ...otherGroupsObjList]; + const allGroups: HostType[] = [ + ...topLevelGroupsObjList, + ...otherGroupsObjList, + ]; - let ungroupedHostsObjList = []; + let ungroupedHostsObjList: HostType[] = []; if ( "ungrouped" in hostObj && @@ -157,13 +177,13 @@ function parseInventoryHosts(hostObj: object): unknown[] { hostObj.ungrouped ) { ungroupedHostsObjList = hostObj.ungrouped.hosts.map((item) => { - return { host: item, priority: 3 }; + return { host: item, priority: 3 } as HostType; }); } // Add 'localhost' and 'all' to the inventory list - const localhostObj = { host: "localhost", priority: 5 }; - const allHostObj = { host: "all", priority: 6 }; + const localhostObj: HostType = { host: "localhost", priority: 5 }; + const allHostObj: HostType = { host: "all", priority: 6 }; let allHosts = [localhostObj, allHostObj, ...ungroupedHostsObjList]; @@ -179,7 +199,11 @@ function parseInventoryHosts(hostObj: object): unknown[] { return [...allGroups, ...allHosts]; } -function getChildGroups(groupList, hostObj, res = []) { +function getChildGroups( + groupList: string[], + hostObj: inventoryType, + res: string[] = [], +): string[] { for (const host of groupList) { if (hostObj[`${host}`].children) { getChildGroups(hostObj[`${host}`].children, hostObj, res); diff --git a/packages/ansible-language-server/test/providers/hoverProvider.test.ts b/packages/ansible-language-server/test/providers/hoverProvider.test.ts index 2ac997d74..834272f90 100644 --- a/packages/ansible-language-server/test/providers/hoverProvider.test.ts +++ b/packages/ansible-language-server/test/providers/hoverProvider.test.ts @@ -1,5 +1,5 @@ import { TextDocument } from "vscode-languageserver-textdocument"; -import { Position } from "vscode-languageserver"; +import { Hover, MarkupContent, Position } from "vscode-languageserver"; import { expect } from "chai"; import { createTestWorkspaceManager, @@ -12,6 +12,18 @@ import { import { doHover } from "../../src/providers/hoverProvider"; import { WorkspaceFolderContext } from "../../src/services/workspaceManager"; +function get_hover_value(hover: Hover | undefined | null): string { + if (hover) { + if (Array.isArray(hover)) { + return ""; + } else { + if (Object.hasOwn(hover.contents as object, "value")) { + return (hover.contents as MarkupContent)["value"]; + } + } + } + return ""; +} function testPlayKeywords( context: WorkspaceFolderContext, textDoc: TextDocument, @@ -41,7 +53,11 @@ function testPlayKeywords( position, await context.docsLibrary, ); - expect(actualHover.contents["value"]).includes(doc); + if (actualHover) { + expect(get_hover_value(actualHover)).includes(doc); + } else { + expect(false); + } }); }); } @@ -65,7 +81,11 @@ function testTaskKeywords( position, await context.docsLibrary, ); - expect(actualHover.contents["value"]).includes(doc); + if (actualHover) { + expect(get_hover_value(actualHover)).includes(doc); + } else { + expect(false); + } }); }); } @@ -89,7 +109,11 @@ function testBlockKeywords( position, await context.docsLibrary, ); - expect(actualHover.contents["value"]).includes(doc); + if (actualHover) { + expect(get_hover_value(actualHover)).includes(doc); + } else { + expect(false); + } }); }); } @@ -113,7 +137,11 @@ function testRoleKeywords( position, await context.docsLibrary, ); - expect(actualHover.contents["value"]).includes(doc); + if (actualHover) { + expect(get_hover_value(actualHover)).includes(doc); + } else { + expect(false); + } }); }); } @@ -142,7 +170,7 @@ function testModuleNames( position, await context.docsLibrary, ); - expect(actualHover.contents["value"]).includes(doc); + expect(get_hover_value(actualHover)).includes(doc); }); }); } @@ -205,7 +233,7 @@ function testPlaybookAdjacentCollection( position, await context.docsLibrary, ); - expect(actualHover.contents["value"]).includes(doc); + expect(get_hover_value(actualHover)).includes(doc); }); }); } @@ -239,7 +267,7 @@ function testNonPlaybookAdjacentCollection( expect(actualHover).to.be.null; } else { expect( - actualHover.contents["value"], + get_hover_value(actualHover), `actual hover -> ${actualHover}`, ).includes(doc); }