From f8ea05739d0960e3e1b3adf23e5998917270261e Mon Sep 17 00:00:00 2001 From: Francesco Renzi Date: Fri, 28 Nov 2025 15:37:32 +0000 Subject: [PATCH] Add more tests --- .../quickfix/add-missing-inputs.ts | 28 +++++------ .../src/code-actions/tests/runner.ts | 16 +++--- ...xisting-with-key-without-inputs.golden.yml | 9 ++++ .../existing-with-key-without-inputs.yml | 7 +++ .../testdata/quickfix/no-with-key.golden.yml | 9 ++++ .../tests/testdata/quickfix/no-with-key.yml | 6 +++ languageservice/src/validate-action.ts | 49 ++++++++++++------- 7 files changed, 85 insertions(+), 39 deletions(-) create mode 100644 languageservice/src/code-actions/tests/testdata/quickfix/existing-with-key-without-inputs.golden.yml create mode 100644 languageservice/src/code-actions/tests/testdata/quickfix/existing-with-key-without-inputs.yml create mode 100644 languageservice/src/code-actions/tests/testdata/quickfix/no-with-key.golden.yml create mode 100644 languageservice/src/code-actions/tests/testdata/quickfix/no-with-key.yml diff --git a/languageservice/src/code-actions/quickfix/add-missing-inputs.ts b/languageservice/src/code-actions/quickfix/add-missing-inputs.ts index 2b4a6a3..e1b7bb2 100644 --- a/languageservice/src/code-actions/quickfix/add-missing-inputs.ts +++ b/languageservice/src/code-actions/quickfix/add-missing-inputs.ts @@ -1,6 +1,6 @@ -import {CodeAction, TextEdit} from "vscode-languageserver-types"; -import {CodeActionContext, CodeActionProvider} from "../types"; -import {DiagnosticCode, MissingInputsDiagnosticData} from "../../validate-action"; +import { CodeAction, TextEdit } from "vscode-languageserver-types"; +import { CodeActionContext, CodeActionProvider } from "../types"; +import { DiagnosticCode, MissingInputsDiagnosticData } from "../../validate-action"; export const addMissingInputsProvider: CodeActionProvider = { diagnosticCodes: [DiagnosticCode.MissingRequiredInputs], @@ -22,11 +22,11 @@ export const addMissingInputsProvider: CodeActionProvider = { title: `Add missing input${data.missingInputs.length > 1 ? "s" : ""}: ${inputNames}`, edit: { changes: { - [context.uri]: edits - } - } + [context.uri]: edits, + }, + }, }; - } + }, }; function createInputEdits(data: MissingInputsDiagnosticData): TextEdit[] | undefined { @@ -42,13 +42,13 @@ function createInputEdits(data: MissingInputsDiagnosticData): TextEdit[] | undef }); edits.push({ - range: {start: data.insertPosition, end: data.insertPosition}, - newText: inputLines.map(line => line + "\n").join("") + range: { start: data.insertPosition, end: data.insertPosition }, + newText: inputLines.map(line => line + "\n").join(""), }); } else { - // No `with:` key - use step indentation for `with:`, +2 for inputs - const withIndent = " ".repeat(data.stepIndent + 2); - const inputIndent = " ".repeat(data.stepIndent + 4); + // No `with:` key - `with:` at step indentation, inputs at step indentation + 2 + const withIndent = " ".repeat(data.stepIndent); + const inputIndent = " ".repeat(data.stepIndent + 2); const inputLines = data.missingInputs.map(input => { const value = input.default !== undefined ? input.default : '""'; @@ -58,8 +58,8 @@ function createInputEdits(data: MissingInputsDiagnosticData): TextEdit[] | undef const newText = [`${withIndent}with:\n`, ...inputLines.map(line => `${line}\n`)].join(""); edits.push({ - range: {start: data.insertPosition, end: data.insertPosition}, - newText + range: { start: data.insertPosition, end: data.insertPosition }, + newText, }); } diff --git a/languageservice/src/code-actions/tests/runner.ts b/languageservice/src/code-actions/tests/runner.ts index 5f057a2..5fb01e7 100644 --- a/languageservice/src/code-actions/tests/runner.ts +++ b/languageservice/src/code-actions/tests/runner.ts @@ -1,9 +1,9 @@ import * as fs from "fs"; import * as path from "path"; -import {TextEdit} from "vscode-languageserver-types"; -import {TextDocument} from "vscode-languageserver-textdocument"; -import {validate, ValidationConfig} from "../../validate"; -import {getCodeActions, CodeActionParams} from "../index"; +import { TextEdit } from "vscode-languageserver-types"; +import { TextDocument } from "vscode-languageserver-textdocument"; +import { validate, ValidationConfig } from "../../validate"; +import { getCodeActions, CodeActionParams } from "../index"; // Marker pattern: # want "diagnostic message" fix="code-action-name" const MARKER_PATTERN = /#\s*want\s+"([^"]+)"(?:\s+fix="([^"]+)")?/; @@ -69,7 +69,7 @@ export function loadTestCases(testdataDir: string): TestCase[] { const testCases: TestCase[] = []; function walkDir(dir: string) { - const entries = fs.readdirSync(dir, {withFileTypes: true}); + const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); @@ -147,6 +147,7 @@ export async function runTestCase(testCase: TestCase, validationConfig: Validati const missingDiagnostics: string[] = []; for (const marker of testCase.markers) { const found = diagnostics.find(d => d.range.start.line === marker.line && d.message.includes(marker.message)); + console.log(found); if (!found) { missingDiagnostics.push(`line ${marker.line}: "${marker.message}"`); } @@ -188,9 +189,8 @@ export async function runTestCase(testCase: TestCase, validationConfig: Validati return { name: testCase.name, passed: false, - error: `Code action "${marker.fix}" not found for diagnostic on line ${marker.line}.\nAvailable actions: ${ - actions.map(a => a.title).join(", ") || "(none)" - }` + error: `Code action "${marker.fix}" not found for diagnostic on line ${marker.line}.\nAvailable actions: ${actions.map(a => a.title).join(", ") || "(none)" + }` }; } diff --git a/languageservice/src/code-actions/tests/testdata/quickfix/existing-with-key-without-inputs.golden.yml b/languageservice/src/code-actions/tests/testdata/quickfix/existing-with-key-without-inputs.golden.yml new file mode 100644 index 0000000..c6f6411 --- /dev/null +++ b/languageservice/src/code-actions/tests/testdata/quickfix/existing-with-key-without-inputs.golden.yml @@ -0,0 +1,9 @@ +on: push +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/cache@v1 + with: + path: "" + key: "" diff --git a/languageservice/src/code-actions/tests/testdata/quickfix/existing-with-key-without-inputs.yml b/languageservice/src/code-actions/tests/testdata/quickfix/existing-with-key-without-inputs.yml new file mode 100644 index 0000000..50fa2f1 --- /dev/null +++ b/languageservice/src/code-actions/tests/testdata/quickfix/existing-with-key-without-inputs.yml @@ -0,0 +1,7 @@ +on: push +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/cache@v1 + with: # want "Missing required inputs: `path`, `key`" fix="Add missing inputs: path, key" diff --git a/languageservice/src/code-actions/tests/testdata/quickfix/no-with-key.golden.yml b/languageservice/src/code-actions/tests/testdata/quickfix/no-with-key.golden.yml new file mode 100644 index 0000000..c6f6411 --- /dev/null +++ b/languageservice/src/code-actions/tests/testdata/quickfix/no-with-key.golden.yml @@ -0,0 +1,9 @@ +on: push +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/cache@v1 + with: + path: "" + key: "" diff --git a/languageservice/src/code-actions/tests/testdata/quickfix/no-with-key.yml b/languageservice/src/code-actions/tests/testdata/quickfix/no-with-key.yml new file mode 100644 index 0000000..bf3fff2 --- /dev/null +++ b/languageservice/src/code-actions/tests/testdata/quickfix/no-with-key.yml @@ -0,0 +1,6 @@ +on: push +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/cache@v1 # want "Missing required inputs: `path`, `key`" fix="Add missing inputs: path, key" diff --git a/languageservice/src/validate-action.ts b/languageservice/src/validate-action.ts index ae9a4c2..1b3a2e1 100644 --- a/languageservice/src/validate-action.ts +++ b/languageservice/src/validate-action.ts @@ -1,12 +1,12 @@ -import {isMapping} from "@actions/workflow-parser"; -import {isActionStep} from "@actions/workflow-parser/model/type-guards"; -import {Step} from "@actions/workflow-parser/model/workflow-template"; -import {ScalarToken} from "@actions/workflow-parser/templates/tokens/scalar-token"; -import {TemplateToken} from "@actions/workflow-parser/templates/tokens/template-token"; -import {Diagnostic, DiagnosticSeverity} from "vscode-languageserver-types"; -import {ActionReference, parseActionReference} from "./action"; -import {mapRange} from "./utils/range"; -import {ValidationConfig} from "./validate"; +import { isMapping } from "@actions/workflow-parser"; +import { isActionStep } from "@actions/workflow-parser/model/type-guards"; +import { Step } from "@actions/workflow-parser/model/workflow-template"; +import { ScalarToken } from "@actions/workflow-parser/templates/tokens/scalar-token"; +import { TemplateToken } from "@actions/workflow-parser/templates/tokens/template-token"; +import { Diagnostic, DiagnosticSeverity } from "vscode-languageserver-types"; +import { ActionReference, parseActionReference } from "./action"; +import { mapRange } from "./utils/range"; +import { ValidationConfig } from "./validate"; export const DiagnosticCode = { MissingRequiredInputs: "missing-required-inputs" @@ -23,7 +23,7 @@ export interface MissingInputsDiagnosticData { withIndent?: number; stepIndent: number; // Position where new content should be inserted - insertPosition: {line: number; character: number}; + insertPosition: { line: number; character: number }; } export async function validateAction( @@ -53,7 +53,7 @@ export async function validateAction( let withKey: ScalarToken | undefined; let withToken: TemplateToken | undefined; - for (const {key, value} of stepToken) { + for (const { key, value } of stepToken) { if (key.toString() === "with") { withKey = key; withToken = value; @@ -63,7 +63,7 @@ export async function validateAction( const stepInputs = new Map(); if (withToken && isMapping(withToken)) { - for (const {key} of withToken) { + for (const { key } of withToken) { stepInputs.set(key.toString(), key); } } @@ -105,6 +105,25 @@ export async function validateAction( const stepIndent = stepToken.range ? stepToken.range.start.column - 1 : 0; // 0-indexed const withIndent = withKey?.range ? withKey.range.start.column - 1 : undefined; + // Calculate insert position + // For withToken, we need to handle empty mappings specially - insert after the with: line + let insertPosition: { line: number; character: number }; + if (withToken?.range) { + // Check if with: has any children by comparing start and end lines + const hasChildren = stepInputs.size > 0; + if (hasChildren) { + // Insert after the last child + insertPosition = { line: withToken.range.end.line - 1, character: 0 }; + } else { + // Empty with: block - insert on the next line after with: + insertPosition = { line: withKey!.range!.end.line, character: 0 }; + } + } else if (stepToken.range) { + insertPosition = { line: stepToken.range.end.line - 1, character: 0 }; + } else { + insertPosition = { line: 0, character: 0 }; + } + const diagnosticData: MissingInputsDiagnosticData = { action, missingInputs: missingRequiredInputs.map(([name, input]) => ({ @@ -114,11 +133,7 @@ export async function validateAction( hasWithKey: withKey !== undefined, withIndent, stepIndent, - insertPosition: withToken?.range - ? {line: withToken.range.end.line - 1, character: 0} - : stepToken.range - ? {line: stepToken.range.end.line - 1, character: 0} - : {line: 0, character: 0} + insertPosition }; diagnostics.push({