Setup CodeActions and add quickfix for missing inputs (#254)
* Setup CodeActions and add quickfix for missing inputs * PR feedback * Update languageservice/src/code-actions/quickfix/add-missing-inputs.ts Co-authored-by: Salman Chishti <[email protected]> * Fix indentSize detection for code actions after rebase - Add indentSize to MissingInputsDiagnosticData interface - Pass indentSize parameter from validate.ts to validateActionReference - Detect indentSize from workflow structure (jobs key to first child) - Fall back to detecting from with: block children when available * update typescript * formatting * linting * Gate missing inputs quickfix behind feature flag * Address PR review: rename files, move position calculation to quickfix - Rename index.ts files to follow repo patterns: - code-actions/index.ts → code-actions/code-actions.ts - code-actions/quickfix/index.ts → quickfix/quickfix-providers.ts - Move position calculation from validation to quickfix: - MissingInputsDiagnosticData now passes raw token ranges - Quickfix computes insertion position and indentation at code action time - detectIndentSize moved from validate.ts to validate-action-reference.ts * wip * Remove pointless comment --------- Co-authored-by: Salman Chishti <[email protected]>
This commit is contained in:
co-authored by
Salman Chishti
parent
67dd4fbd61
commit
0fe31c6656
@@ -1,8 +1,18 @@
|
|||||||
import {documentLinks, getInlayHints, hover, validate, ValidationConfig} from "@actions/languageservice";
|
import {
|
||||||
|
documentLinks,
|
||||||
|
getCodeActions,
|
||||||
|
getInlayHints,
|
||||||
|
hover,
|
||||||
|
validate,
|
||||||
|
ValidationConfig
|
||||||
|
} from "@actions/languageservice";
|
||||||
import {registerLogger, setLogLevel} from "@actions/languageservice/log";
|
import {registerLogger, setLogLevel} from "@actions/languageservice/log";
|
||||||
import {clearCache, clearCacheEntry} from "@actions/languageservice/utils/workflow-cache";
|
import {clearCache, clearCacheEntry} from "@actions/languageservice/utils/workflow-cache";
|
||||||
import {Octokit} from "@octokit/rest";
|
import {Octokit} from "@octokit/rest";
|
||||||
import {
|
import {
|
||||||
|
CodeAction,
|
||||||
|
CodeActionKind,
|
||||||
|
CodeActionParams,
|
||||||
CompletionItem,
|
CompletionItem,
|
||||||
Connection,
|
Connection,
|
||||||
DocumentLink,
|
DocumentLink,
|
||||||
@@ -79,7 +89,10 @@ export function initConnection(connection: Connection) {
|
|||||||
documentLinkProvider: {
|
documentLinkProvider: {
|
||||||
resolveProvider: false
|
resolveProvider: false
|
||||||
},
|
},
|
||||||
inlayHintProvider: true
|
inlayHintProvider: true,
|
||||||
|
codeActionProvider: {
|
||||||
|
codeActionKinds: [CodeActionKind.QuickFix]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -177,6 +190,17 @@ export function initConnection(connection: Connection) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
connection.onCodeAction((params: CodeActionParams): CodeAction[] => {
|
||||||
|
const document = getDocument(documents, params.textDocument);
|
||||||
|
return getCodeActions({
|
||||||
|
uri: params.textDocument.uri,
|
||||||
|
documentContent: document.getText(),
|
||||||
|
diagnostics: params.context.diagnostics,
|
||||||
|
only: params.context.only,
|
||||||
|
featureFlags
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Make the text document manager listen on the connection
|
// Make the text document manager listen on the connection
|
||||||
// for open, change and close text document events
|
// for open, change and close text document events
|
||||||
documents.listen(connection);
|
documents.listen(connection);
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import {FeatureFlags} from "@actions/expressions";
|
||||||
|
import {CodeAction, CodeActionKind, Diagnostic} from "vscode-languageserver-types";
|
||||||
|
import {CodeActionContext, CodeActionProvider} from "./types.js";
|
||||||
|
import {getQuickfixProviders} from "./quickfix/quickfix-providers.js";
|
||||||
|
|
||||||
|
export interface CodeActionParams {
|
||||||
|
uri: string;
|
||||||
|
documentContent: string;
|
||||||
|
diagnostics: Diagnostic[];
|
||||||
|
only?: string[];
|
||||||
|
featureFlags?: FeatureFlags;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCodeActions(params: CodeActionParams): CodeAction[] {
|
||||||
|
const actions: CodeAction[] = [];
|
||||||
|
const context: CodeActionContext = {
|
||||||
|
uri: params.uri,
|
||||||
|
documentContent: params.documentContent,
|
||||||
|
featureFlags: params.featureFlags
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build providers map based on feature flags
|
||||||
|
const providersByKind: Map<string, CodeActionProvider[]> = new Map([
|
||||||
|
[CodeActionKind.QuickFix, getQuickfixProviders(params.featureFlags)]
|
||||||
|
// [CodeActionKind.Refactor, getRefactorProviders(params.featureFlags)],
|
||||||
|
// [CodeActionKind.Source, getSourceProviders(params.featureFlags)],
|
||||||
|
// etc
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Filter to requested kinds, or use all if none specified
|
||||||
|
const requestedKinds = params.only;
|
||||||
|
const kindsToCheck = requestedKinds
|
||||||
|
? [...providersByKind.keys()].filter(kind => requestedKinds.some(requested => kind.startsWith(requested)))
|
||||||
|
: [...providersByKind.keys()];
|
||||||
|
|
||||||
|
for (const diagnostic of params.diagnostics) {
|
||||||
|
for (const kind of kindsToCheck) {
|
||||||
|
const providers = providersByKind.get(kind) ?? [];
|
||||||
|
for (const provider of providers) {
|
||||||
|
if (provider.diagnosticCodes.includes(diagnostic.code)) {
|
||||||
|
const action = provider.createCodeAction(context, diagnostic);
|
||||||
|
if (action) {
|
||||||
|
action.kind = kind;
|
||||||
|
action.diagnostics = [diagnostic];
|
||||||
|
actions.push(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type {CodeActionContext, CodeActionProvider} from "./types.js";
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
import {isMapping} from "@actions/workflow-parser";
|
||||||
|
import {MappingToken} from "@actions/workflow-parser/templates/tokens/mapping-token";
|
||||||
|
import {ScalarToken} from "@actions/workflow-parser/templates/tokens/scalar-token";
|
||||||
|
import {TemplateToken} from "@actions/workflow-parser/templates/tokens/template-token";
|
||||||
|
import {CodeAction, Position, TextEdit} from "vscode-languageserver-types";
|
||||||
|
import {error} from "../../log.js";
|
||||||
|
import {findToken} from "../../utils/find-token.js";
|
||||||
|
import {getOrParseWorkflow} from "../../utils/workflow-cache.js";
|
||||||
|
import {DiagnosticCode, MissingInputsDiagnosticData} from "../../validate-action-reference.js";
|
||||||
|
import {CodeActionContext, CodeActionProvider} from "../types.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information extracted from a step token needed to generate edits
|
||||||
|
*/
|
||||||
|
interface StepInfo {
|
||||||
|
/** Column where step keys start (1-indexed), e.g., the column of "uses:" */
|
||||||
|
stepKeyColumn: number;
|
||||||
|
/** End line of the step (1-indexed) */
|
||||||
|
stepEndLine: number;
|
||||||
|
/** Detected indent size (spaces per level) */
|
||||||
|
indentSize: number;
|
||||||
|
/** Information about existing with: block, if present */
|
||||||
|
withInfo?: {
|
||||||
|
keyColumn: number;
|
||||||
|
keyEndLine: number;
|
||||||
|
valueEndLine: number;
|
||||||
|
hasChildren: boolean;
|
||||||
|
/** Column of first child input (1-indexed), for indentation detection */
|
||||||
|
firstChildColumn?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addMissingInputsProvider: CodeActionProvider = {
|
||||||
|
diagnosticCodes: [DiagnosticCode.MissingRequiredInputs],
|
||||||
|
|
||||||
|
createCodeAction(context: CodeActionContext, diagnostic): CodeAction | undefined {
|
||||||
|
const data = diagnostic.data as MissingInputsDiagnosticData | undefined;
|
||||||
|
if (!data) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the document to get the step token
|
||||||
|
const stepInfo = getStepInfo(context, diagnostic.range.start);
|
||||||
|
if (!stepInfo) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const edits = createInputEdits(data.missingInputs, stepInfo);
|
||||||
|
if (!edits || edits.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputNames = data.missingInputs.map(i => i.name).join(", ");
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: `Add missing input${data.missingInputs.length > 1 ? "s" : ""}: ${inputNames}`,
|
||||||
|
edit: {
|
||||||
|
changes: {
|
||||||
|
[context.uri]: edits
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the document and extract step information needed for generating edits.
|
||||||
|
* Returns undefined if parsing fails or the step token cannot be found.
|
||||||
|
*/
|
||||||
|
function getStepInfo(context: CodeActionContext, diagnosticPosition: Position): StepInfo | undefined {
|
||||||
|
// Parse the document (uses cache if available from validation)
|
||||||
|
const file = {name: context.uri, content: context.documentContent};
|
||||||
|
const parseResult = getOrParseWorkflow(file, context.uri);
|
||||||
|
|
||||||
|
if (!parseResult.value) {
|
||||||
|
error("Failed to parse workflow for missing inputs quickfix");
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the token at the diagnostic position
|
||||||
|
const {path} = findToken(diagnosticPosition, parseResult.value);
|
||||||
|
|
||||||
|
// Walk up the path to find the step token (regular-step)
|
||||||
|
const stepToken = findStepInPath(path);
|
||||||
|
if (!stepToken) {
|
||||||
|
error("Could not find step token for missing inputs quickfix");
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return extractStepInfo(stepToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the step token (regular-step) in the token path
|
||||||
|
*/
|
||||||
|
function findStepInPath(path: TemplateToken[]): MappingToken | undefined {
|
||||||
|
// Walk backwards through path to find the step
|
||||||
|
for (let i = path.length - 1; i >= 0; i--) {
|
||||||
|
if (path[i].definition?.key === "regular-step" && isMapping(path[i])) {
|
||||||
|
return path[i] as MappingToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract position and indentation info from a step token
|
||||||
|
*/
|
||||||
|
function extractStepInfo(stepToken: MappingToken): StepInfo | undefined {
|
||||||
|
if (!stepToken.range) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the column of the first key in the step
|
||||||
|
let stepKeyColumn = stepToken.range.start.column;
|
||||||
|
if (stepToken.count > 0) {
|
||||||
|
const firstEntry = stepToken.get(0);
|
||||||
|
if (firstEntry?.key.range) {
|
||||||
|
stepKeyColumn = firstEntry.key.range.start.column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the with: block if present
|
||||||
|
let withKey: ScalarToken | undefined;
|
||||||
|
let withToken: TemplateToken | undefined;
|
||||||
|
for (const {key, value} of stepToken) {
|
||||||
|
if (key.toString() === "with") {
|
||||||
|
withKey = key;
|
||||||
|
withToken = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate indent size
|
||||||
|
let indentSize = 2; // Default
|
||||||
|
let withInfo: StepInfo["withInfo"];
|
||||||
|
|
||||||
|
if (withKey?.range && withToken?.range) {
|
||||||
|
// Has with: block - extract its info
|
||||||
|
const hasChildren = isMapping(withToken) && withToken.count > 0;
|
||||||
|
let firstChildColumn: number | undefined;
|
||||||
|
|
||||||
|
if (hasChildren) {
|
||||||
|
const firstChild = (withToken as MappingToken).get(0);
|
||||||
|
if (firstChild?.key.range) {
|
||||||
|
firstChildColumn = firstChild.key.range.start.column;
|
||||||
|
// Detect indent size from with: children
|
||||||
|
indentSize = firstChildColumn - withKey.range.start.column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
withInfo = {
|
||||||
|
keyColumn: withKey.range.start.column,
|
||||||
|
keyEndLine: withKey.range.end.line,
|
||||||
|
valueEndLine: withToken.range.end.line,
|
||||||
|
hasChildren,
|
||||||
|
firstChildColumn
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// No with: block - detect indent size using heuristics
|
||||||
|
// Based on the step key column position, estimate indent size
|
||||||
|
// 2-space indent files typically have step keys at column 7
|
||||||
|
// 4-space indent files typically have step keys at column 15
|
||||||
|
const zeroIndexedCol = stepKeyColumn - 1;
|
||||||
|
if (zeroIndexedCol >= 10) {
|
||||||
|
indentSize = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
stepKeyColumn,
|
||||||
|
stepEndLine: stepToken.range.end.line,
|
||||||
|
indentSize,
|
||||||
|
withInfo
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate text edits to add missing inputs
|
||||||
|
*/
|
||||||
|
function createInputEdits(missingInputs: MissingInputsDiagnosticData["missingInputs"], stepInfo: StepInfo): TextEdit[] {
|
||||||
|
const formatInputLines = (indent: string) =>
|
||||||
|
missingInputs.map(input => {
|
||||||
|
const value = input.default ?? '""';
|
||||||
|
return `${indent}${input.name}: ${value}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (stepInfo.withInfo) {
|
||||||
|
// `with:` exists - add inputs to existing block
|
||||||
|
const withIndent = stepInfo.withInfo.keyColumn - 1; // 0-indexed
|
||||||
|
const inputIndentSize = stepInfo.withInfo.firstChildColumn
|
||||||
|
? stepInfo.withInfo.firstChildColumn - stepInfo.withInfo.keyColumn
|
||||||
|
: stepInfo.indentSize;
|
||||||
|
|
||||||
|
const inputIndent = " ".repeat(withIndent + inputIndentSize);
|
||||||
|
const inputLines = formatInputLines(inputIndent);
|
||||||
|
|
||||||
|
// Calculate insert position
|
||||||
|
let insertLine: number;
|
||||||
|
if (stepInfo.withInfo.hasChildren) {
|
||||||
|
// Insert after the last child (at end of with: block)
|
||||||
|
// valueEndLine is 1-indexed, we want 0-indexed for Position
|
||||||
|
insertLine = stepInfo.withInfo.valueEndLine - 1;
|
||||||
|
} else {
|
||||||
|
// Empty with: block - insert on the next line after with:
|
||||||
|
// keyEndLine is 1-indexed, convert to 0-indexed and go to next line
|
||||||
|
insertLine = stepInfo.withInfo.keyEndLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertPosition: Position = {
|
||||||
|
line: insertLine,
|
||||||
|
character: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
range: {start: insertPosition, end: insertPosition},
|
||||||
|
newText: inputLines.map(line => line + "\n").join("")
|
||||||
|
}
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
// No `with:` key - add `with:` at the same level as other step keys
|
||||||
|
const withKeyIndent = stepInfo.stepKeyColumn - 1; // 0-indexed (columns are 1-based)
|
||||||
|
|
||||||
|
const withIndent = " ".repeat(withKeyIndent);
|
||||||
|
const inputIndent = " ".repeat(withKeyIndent + stepInfo.indentSize);
|
||||||
|
const inputLines = formatInputLines(inputIndent);
|
||||||
|
|
||||||
|
const newText = `${withIndent}with:\n` + inputLines.map(line => `${line}\n`).join("");
|
||||||
|
|
||||||
|
// Insert at end of step
|
||||||
|
// stepEndLine is 1-indexed, we want 0-indexed and insert before the line after
|
||||||
|
const insertPosition: Position = {
|
||||||
|
line: stepInfo.stepEndLine - 1,
|
||||||
|
character: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
range: {start: insertPosition, end: insertPosition},
|
||||||
|
newText
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import {FeatureFlags} from "@actions/expressions";
|
||||||
|
import {CodeActionProvider} from "../types.js";
|
||||||
|
import {addMissingInputsProvider} from "./add-missing-inputs.js";
|
||||||
|
|
||||||
|
export function getQuickfixProviders(featureFlags?: FeatureFlags): CodeActionProvider[] {
|
||||||
|
const providers: CodeActionProvider[] = [];
|
||||||
|
|
||||||
|
if (featureFlags?.isEnabled("missingInputsQuickfix")) {
|
||||||
|
providers.push(addMissingInputsProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
return providers;
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import * as path from "path";
|
||||||
|
import {fileURLToPath} from "url";
|
||||||
|
import {loadTestCases, runTestCase} from "./runner.js";
|
||||||
|
import {ValidationConfig} from "../../validate.js";
|
||||||
|
import {ActionMetadata, ActionReference} from "../../action.js";
|
||||||
|
import {clearCache} from "../../utils/workflow-cache.js";
|
||||||
|
|
||||||
|
// ESM-compatible __dirname
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
// Mock action metadata provider for tests
|
||||||
|
const validationConfig: ValidationConfig = {
|
||||||
|
actionsMetadataProvider: {
|
||||||
|
fetchActionMetadata: (ref: ActionReference): Promise<ActionMetadata | undefined> => {
|
||||||
|
const key = `${ref.owner}/${ref.name}@${ref.ref}`;
|
||||||
|
|
||||||
|
const metadata: Record<string, ActionMetadata> = {
|
||||||
|
"actions/cache@v1": {
|
||||||
|
name: "Cache",
|
||||||
|
description: "Cache dependencies",
|
||||||
|
inputs: {
|
||||||
|
path: {
|
||||||
|
description: "A list of files to cache",
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
key: {
|
||||||
|
description: "Cache key",
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
"restore-keys": {
|
||||||
|
description: "Restore keys",
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"actions/setup-node@v3": {
|
||||||
|
name: "Setup Node",
|
||||||
|
description: "Setup Node.js",
|
||||||
|
inputs: {
|
||||||
|
"node-version": {
|
||||||
|
description: "Node version",
|
||||||
|
required: true,
|
||||||
|
default: "16"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return Promise.resolve(metadata[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Point to the source testdata directory
|
||||||
|
const testdataDir = path.join(__dirname, "testdata");
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
clearCache();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("code action golden tests", () => {
|
||||||
|
const testCases = loadTestCases(testdataDir);
|
||||||
|
|
||||||
|
if (testCases.length === 0) {
|
||||||
|
it.todo("no test cases found - add .yml files to testdata/");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const testCase of testCases) {
|
||||||
|
it(testCase.name, async () => {
|
||||||
|
const result = await runTestCase(testCase, validationConfig);
|
||||||
|
|
||||||
|
if (!result.passed) {
|
||||||
|
let errorMessage = result.error || "Test failed";
|
||||||
|
|
||||||
|
if (result.expected !== undefined && result.actual !== undefined) {
|
||||||
|
errorMessage += "\n\n";
|
||||||
|
errorMessage += "=== EXPECTED (golden file) ===\n";
|
||||||
|
errorMessage += result.expected;
|
||||||
|
errorMessage += "\n\n";
|
||||||
|
errorMessage += "=== ACTUAL ===\n";
|
||||||
|
errorMessage += result.actual;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
import * as fs from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
import {TextEdit} from "vscode-languageserver-types";
|
||||||
|
import {TextDocument} from "vscode-languageserver-textdocument";
|
||||||
|
import {FeatureFlags} from "@actions/expressions";
|
||||||
|
import {validate, ValidationConfig} from "../../validate.js";
|
||||||
|
import {getCodeActions, CodeActionParams} from "../code-actions.js";
|
||||||
|
|
||||||
|
// Marker pattern: # want "diagnostic message" fix="code-action-name"
|
||||||
|
const MARKER_PATTERN = /#\s*want\s+"([^"]+)"(?:\s+fix="([^"]+)")?/;
|
||||||
|
|
||||||
|
export interface TestCase {
|
||||||
|
name: string;
|
||||||
|
inputPath: string;
|
||||||
|
goldenPath: string;
|
||||||
|
input: string;
|
||||||
|
golden: string;
|
||||||
|
markers: Marker[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Marker {
|
||||||
|
line: number;
|
||||||
|
message: string;
|
||||||
|
fix?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestResult {
|
||||||
|
name: string;
|
||||||
|
passed: boolean;
|
||||||
|
error?: string;
|
||||||
|
expected?: string;
|
||||||
|
actual?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse markers from input file content
|
||||||
|
*/
|
||||||
|
export function parseMarkers(content: string): Marker[] {
|
||||||
|
const lines = content.split("\n");
|
||||||
|
const markers: Marker[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const match = lines[i].match(MARKER_PATTERN);
|
||||||
|
if (match) {
|
||||||
|
markers.push({
|
||||||
|
line: i,
|
||||||
|
message: match[1],
|
||||||
|
fix: match[2]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return markers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip markers from content (for processing)
|
||||||
|
*/
|
||||||
|
export function stripMarkers(content: string): string {
|
||||||
|
return content
|
||||||
|
.split("\n")
|
||||||
|
.map(line => line.replace(MARKER_PATTERN, "").trimEnd())
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all test cases from a testdata directory
|
||||||
|
*/
|
||||||
|
export function loadTestCases(testdataDir: string): TestCase[] {
|
||||||
|
const testCases: TestCase[] = [];
|
||||||
|
|
||||||
|
function walkDir(dir: string) {
|
||||||
|
const entries = fs.readdirSync(dir, {withFileTypes: true});
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
walkDir(fullPath);
|
||||||
|
} else if (entry.isFile() && entry.name.endsWith(".yml") && !entry.name.endsWith(".golden.yml")) {
|
||||||
|
const goldenPath = fullPath.replace(".yml", ".golden.yml");
|
||||||
|
|
||||||
|
if (fs.existsSync(goldenPath)) {
|
||||||
|
const input = fs.readFileSync(fullPath, "utf-8");
|
||||||
|
const golden = fs.readFileSync(goldenPath, "utf-8");
|
||||||
|
|
||||||
|
testCases.push({
|
||||||
|
name: path.relative(testdataDir, fullPath),
|
||||||
|
inputPath: fullPath,
|
||||||
|
goldenPath,
|
||||||
|
input,
|
||||||
|
golden,
|
||||||
|
markers: parseMarkers(input)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
walkDir(testdataDir);
|
||||||
|
return testCases;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply text edits to a document
|
||||||
|
*/
|
||||||
|
export function applyEdits(content: string, edits: TextEdit[]): string {
|
||||||
|
// Sort edits in reverse order by position to apply from bottom to top
|
||||||
|
const sortedEdits = [...edits].sort((a, b) => {
|
||||||
|
if (b.range.start.line !== a.range.start.line) {
|
||||||
|
return b.range.start.line - a.range.start.line;
|
||||||
|
}
|
||||||
|
return b.range.start.character - a.range.start.character;
|
||||||
|
});
|
||||||
|
|
||||||
|
const lines = content.split("\n");
|
||||||
|
|
||||||
|
for (const edit of sortedEdits) {
|
||||||
|
const startLine = edit.range.start.line;
|
||||||
|
const startChar = edit.range.start.character;
|
||||||
|
const endLine = edit.range.end.line;
|
||||||
|
const endChar = edit.range.end.character;
|
||||||
|
|
||||||
|
const before = lines[startLine].slice(0, startChar);
|
||||||
|
const after = lines[endLine].slice(endChar);
|
||||||
|
|
||||||
|
const newLines = edit.newText.split("\n");
|
||||||
|
newLines[0] = before + newLines[0];
|
||||||
|
newLines[newLines.length - 1] = newLines[newLines.length - 1] + after;
|
||||||
|
|
||||||
|
lines.splice(startLine, endLine - startLine + 1, ...newLines);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a single test case
|
||||||
|
*/
|
||||||
|
export async function runTestCase(testCase: TestCase, validationConfig: ValidationConfig): Promise<TestResult> {
|
||||||
|
const strippedInput = stripMarkers(testCase.input);
|
||||||
|
const document = TextDocument.create("file:///test.yml", "yaml", 1, strippedInput);
|
||||||
|
|
||||||
|
// 1. Validate and get diagnostics
|
||||||
|
const diagnostics = await validate(document, validationConfig);
|
||||||
|
|
||||||
|
// 2. Verify all expected diagnostics are present
|
||||||
|
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));
|
||||||
|
if (!found) {
|
||||||
|
missingDiagnostics.push(`line ${marker.line}: "${marker.message}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missingDiagnostics.length > 0) {
|
||||||
|
return {
|
||||||
|
name: testCase.name,
|
||||||
|
passed: false,
|
||||||
|
error: `Missing expected diagnostics:\n ${missingDiagnostics.join(
|
||||||
|
"\n "
|
||||||
|
)}\n\nActual diagnostics:\n ${diagnostics.map(d => `line ${d.range.start.line}: "${d.message}"`).join("\n ")}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Collect all edits from all matching code actions
|
||||||
|
const allEdits: TextEdit[] = [];
|
||||||
|
|
||||||
|
for (const marker of testCase.markers) {
|
||||||
|
if (!marker.fix) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const diagnostic = diagnostics.find(d => d.range.start.line === marker.line && d.message.includes(marker.message));
|
||||||
|
|
||||||
|
if (!diagnostic) {
|
||||||
|
continue; // Already reported above
|
||||||
|
}
|
||||||
|
|
||||||
|
const params: CodeActionParams = {
|
||||||
|
uri: document.uri,
|
||||||
|
documentContent: strippedInput,
|
||||||
|
diagnostics: [diagnostic],
|
||||||
|
featureFlags: new FeatureFlags({all: true})
|
||||||
|
};
|
||||||
|
|
||||||
|
const actions = getCodeActions(params);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- marker.fix is checked at the start of the loop
|
||||||
|
const matchingAction = actions.find(a => a.title.toLowerCase().includes(marker.fix!.toLowerCase()));
|
||||||
|
|
||||||
|
if (!matchingAction) {
|
||||||
|
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)"
|
||||||
|
}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!matchingAction.edit?.changes) {
|
||||||
|
return {
|
||||||
|
name: testCase.name,
|
||||||
|
passed: false,
|
||||||
|
error: `Code action "${marker.fix}" has no edits`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const edits = matchingAction.edit.changes[document.uri] || [];
|
||||||
|
allEdits.push(...edits);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Apply all edits and compare to golden file
|
||||||
|
const actualOutput = applyEdits(strippedInput, allEdits);
|
||||||
|
const expectedOutput = testCase.golden;
|
||||||
|
|
||||||
|
if (actualOutput.trim() !== expectedOutput.trim()) {
|
||||||
|
return {
|
||||||
|
name: testCase.name,
|
||||||
|
passed: false,
|
||||||
|
error: "Output does not match golden file",
|
||||||
|
expected: expectedOutput,
|
||||||
|
actual: actualOutput
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: testCase.name,
|
||||||
|
passed: true
|
||||||
|
};
|
||||||
|
}
|
||||||
languageservice/src/code-actions/tests/testdata/quickfix/existing-with-key-without-inputs.golden.yml
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
on: push
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/cache@v1
|
||||||
|
with:
|
||||||
|
path: ""
|
||||||
|
key: ""
|
||||||
Vendored
+7
@@ -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"
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
on: push
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/cache@v1
|
||||||
|
with:
|
||||||
|
restore-keys: ${{ runner.os }}-
|
||||||
|
path: ""
|
||||||
|
key: ""
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
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"
|
||||||
|
restore-keys: ${{ runner.os }}-
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
on: push
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/cache@v1
|
||||||
|
with:
|
||||||
|
path: ""
|
||||||
|
key: ""
|
||||||
+6
@@ -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"
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
on: push
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/cache@v1
|
||||||
|
with:
|
||||||
|
path: ""
|
||||||
|
key: ""
|
||||||
@@ -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"
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import {FeatureFlags} from "@actions/expressions";
|
||||||
|
import {CodeAction, Diagnostic} from "vscode-languageserver-types";
|
||||||
|
|
||||||
|
export interface CodeActionContext {
|
||||||
|
uri: string;
|
||||||
|
documentContent: string;
|
||||||
|
featureFlags?: FeatureFlags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A provider that can produce a code action for a given diagnostic
|
||||||
|
*/
|
||||||
|
export interface CodeActionProvider {
|
||||||
|
/**
|
||||||
|
* The diagnostic codes this provider handles
|
||||||
|
*/
|
||||||
|
diagnosticCodes: (string | number | undefined)[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a code action for the diagnostic, if applicable
|
||||||
|
*/
|
||||||
|
createCodeAction(context: CodeActionContext, diagnostic: Diagnostic): CodeAction | undefined;
|
||||||
|
}
|
||||||
@@ -6,3 +6,4 @@ export {getInlayHints} from "./inlay-hints.js";
|
|||||||
export {Logger, LogLevel, registerLogger, setLogLevel} from "./log.js";
|
export {Logger, LogLevel, registerLogger, setLogLevel} from "./log.js";
|
||||||
export {validate, ValidationConfig, ActionsMetadataProvider} from "./validate.js";
|
export {validate, ValidationConfig, ActionsMetadataProvider} from "./validate.js";
|
||||||
export {ValueProviderConfig, ValueProviderKind} from "./value-providers/config.js";
|
export {ValueProviderConfig, ValueProviderKind} from "./value-providers/config.js";
|
||||||
|
export {getCodeActions, CodeActionParams} from "./code-actions/code-actions.js";
|
||||||
|
|||||||
@@ -249,7 +249,21 @@ jobs:
|
|||||||
line: 7
|
line: 7
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
severity: DiagnosticSeverity.Error
|
severity: DiagnosticSeverity.Error,
|
||||||
|
code: "missing-required-inputs",
|
||||||
|
data: {
|
||||||
|
action: {
|
||||||
|
name: "cache",
|
||||||
|
owner: "actions",
|
||||||
|
ref: "v1"
|
||||||
|
},
|
||||||
|
missingInputs: [
|
||||||
|
{
|
||||||
|
default: undefined,
|
||||||
|
name: "path"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -294,7 +308,25 @@ jobs:
|
|||||||
line: 7
|
line: 7
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
severity: DiagnosticSeverity.Error
|
severity: DiagnosticSeverity.Error,
|
||||||
|
code: "missing-required-inputs",
|
||||||
|
data: {
|
||||||
|
action: {
|
||||||
|
name: "cache",
|
||||||
|
owner: "actions",
|
||||||
|
ref: "v1"
|
||||||
|
},
|
||||||
|
missingInputs: [
|
||||||
|
{
|
||||||
|
default: undefined,
|
||||||
|
name: "path"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
default: undefined,
|
||||||
|
name: "key"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -323,7 +355,25 @@ jobs:
|
|||||||
line: 6
|
line: 6
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
severity: DiagnosticSeverity.Error
|
severity: DiagnosticSeverity.Error,
|
||||||
|
code: "missing-required-inputs",
|
||||||
|
data: {
|
||||||
|
action: {
|
||||||
|
name: "cache",
|
||||||
|
owner: "actions",
|
||||||
|
ref: "v1"
|
||||||
|
},
|
||||||
|
missingInputs: [
|
||||||
|
{
|
||||||
|
default: undefined,
|
||||||
|
name: "path"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
default: undefined,
|
||||||
|
name: "key"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,10 +4,22 @@ import {Step} from "@actions/workflow-parser/model/workflow-template";
|
|||||||
import {ScalarToken} from "@actions/workflow-parser/templates/tokens/scalar-token";
|
import {ScalarToken} from "@actions/workflow-parser/templates/tokens/scalar-token";
|
||||||
import {TemplateToken} from "@actions/workflow-parser/templates/tokens/template-token";
|
import {TemplateToken} from "@actions/workflow-parser/templates/tokens/template-token";
|
||||||
import {Diagnostic, DiagnosticSeverity} from "vscode-languageserver-types";
|
import {Diagnostic, DiagnosticSeverity} from "vscode-languageserver-types";
|
||||||
import {parseActionReference} from "./action.js";
|
import {ActionReference, parseActionReference} from "./action.js";
|
||||||
import {mapRange} from "./utils/range.js";
|
import {mapRange} from "./utils/range.js";
|
||||||
import {ValidationConfig} from "./validate.js";
|
import {ValidationConfig} from "./validate.js";
|
||||||
|
|
||||||
|
export const DiagnosticCode = {
|
||||||
|
MissingRequiredInputs: "missing-required-inputs"
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export interface MissingInputsDiagnosticData {
|
||||||
|
action: ActionReference;
|
||||||
|
missingInputs: Array<{
|
||||||
|
name: string;
|
||||||
|
default?: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates action references in workflow steps, checking for valid inputs and required inputs.
|
* Validates action references in workflow steps, checking for valid inputs and required inputs.
|
||||||
*/
|
*/
|
||||||
@@ -94,10 +106,22 @@ export async function validateActionReference(
|
|||||||
missingRequiredInputs.length === 1
|
missingRequiredInputs.length === 1
|
||||||
? `Missing required input \`${missingRequiredInputs[0][0]}\``
|
? `Missing required input \`${missingRequiredInputs[0][0]}\``
|
||||||
: `Missing required inputs: ${missingRequiredInputs.map(input => `\`${input[0]}\``).join(", ")}`;
|
: `Missing required inputs: ${missingRequiredInputs.map(input => `\`${input[0]}\``).join(", ")}`;
|
||||||
|
|
||||||
|
// Build minimal diagnostic data - position calculation happens in the quickfix
|
||||||
|
const diagnosticData: MissingInputsDiagnosticData = {
|
||||||
|
action,
|
||||||
|
missingInputs: missingRequiredInputs.map(([name, input]) => ({
|
||||||
|
name,
|
||||||
|
default: input.default
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
diagnostics.push({
|
diagnostics.push({
|
||||||
severity: DiagnosticSeverity.Error,
|
severity: DiagnosticSeverity.Error,
|
||||||
range: mapRange((withKey || stepToken).range), // Highlight the whole step if we don't have a with key
|
range: mapRange((withKey || stepToken).range),
|
||||||
message: message
|
message: message,
|
||||||
|
code: DiagnosticCode.MissingRequiredInputs,
|
||||||
|
data: diagnosticData
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+13
-1
@@ -480,6 +480,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.5.tgz",
|
||||||
"integrity": "sha512-vvmsN0r7rguA+FySiCsbaTTobSftpIDIpPW81trAmsv9TGxg3YCujAxRYp/Uy8xmDgYCzzgulG62H7KYUFmeIg==",
|
"integrity": "sha512-vvmsN0r7rguA+FySiCsbaTTobSftpIDIpPW81trAmsv9TGxg3YCujAxRYp/Uy8xmDgYCzzgulG62H7KYUFmeIg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-token": "^5.0.0",
|
"@octokit/auth-token": "^5.0.0",
|
||||||
"@octokit/graphql": "^8.2.2",
|
"@octokit/graphql": "^8.2.2",
|
||||||
@@ -1276,6 +1277,7 @@
|
|||||||
"version": "7.20.2",
|
"version": "7.20.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.1.0",
|
"@ampproject/remapping": "^2.1.0",
|
||||||
"@babel/code-frame": "^7.18.6",
|
"@babel/code-frame": "^7.18.6",
|
||||||
@@ -4118,6 +4120,7 @@
|
|||||||
"integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==",
|
"integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-token": "^4.0.0",
|
"@octokit/auth-token": "^4.0.0",
|
||||||
"@octokit/graphql": "^7.1.0",
|
"@octokit/graphql": "^7.1.0",
|
||||||
@@ -4645,6 +4648,7 @@
|
|||||||
"integrity": "sha512-sn1OZmBxUsgxMmR8a8U5QM/Wl+tyqlH//jTqCg8daTAmhAk26L2PFhcqPLlYBhYUJMZJK276qLXlHN3a83o2cg==",
|
"integrity": "sha512-sn1OZmBxUsgxMmR8a8U5QM/Wl+tyqlH//jTqCg8daTAmhAk26L2PFhcqPLlYBhYUJMZJK276qLXlHN3a83o2cg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "5.56.0",
|
"@typescript-eslint/scope-manager": "5.56.0",
|
||||||
"@typescript-eslint/types": "5.56.0",
|
"@typescript-eslint/types": "5.56.0",
|
||||||
@@ -4881,6 +4885,7 @@
|
|||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -5321,6 +5326,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -6522,6 +6528,7 @@
|
|||||||
"version": "7.32.0",
|
"version": "7.32.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "7.12.11",
|
"@babel/code-frame": "7.12.11",
|
||||||
"@eslint/eslintrc": "^0.4.3",
|
"@eslint/eslintrc": "^0.4.3",
|
||||||
@@ -8390,6 +8397,7 @@
|
|||||||
"version": "29.3.1",
|
"version": "29.3.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jest/core": "^29.3.1",
|
"@jest/core": "^29.3.1",
|
||||||
"@jest/types": "^29.3.1",
|
"@jest/types": "^29.3.1",
|
||||||
@@ -10270,6 +10278,7 @@
|
|||||||
"version": "2.6.7",
|
"version": "2.6.7",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"whatwg-url": "^5.0.0"
|
"whatwg-url": "^5.0.0"
|
||||||
},
|
},
|
||||||
@@ -10625,6 +10634,7 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@napi-rs/wasm-runtime": "0.2.4",
|
"@napi-rs/wasm-runtime": "0.2.4",
|
||||||
"@yarnpkg/lockfile": "^1.1.0",
|
"@yarnpkg/lockfile": "^1.1.0",
|
||||||
@@ -11262,6 +11272,7 @@
|
|||||||
"version": "2.8.3",
|
"version": "2.8.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"prettier": "bin-prettier.js"
|
"prettier": "bin-prettier.js"
|
||||||
},
|
},
|
||||||
@@ -12740,7 +12751,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "0BSD"
|
"license": "0BSD",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/tsutils": {
|
"node_modules/tsutils": {
|
||||||
"version": "3.21.0",
|
"version": "3.21.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user