Move caching to language service

This commit is contained in:
Jacob Wallraff
2023-03-03 12:28:29 -08:00
parent d7c19f529d
commit ffcd06c8c5
7 changed files with 95 additions and 66 deletions
+8 -9
View File
@@ -1,11 +1,9 @@
import {complete as completeExpression, DescriptionDictionary} from "@github/actions-expressions"; import {complete as completeExpression, DescriptionDictionary} from "@github/actions-expressions";
import {CompletionItem as ExpressionCompletionItem} from "@github/actions-expressions/completion"; import {CompletionItem as ExpressionCompletionItem} from "@github/actions-expressions/completion";
import { import {
convertWorkflowTemplate,
isBasicExpression, isBasicExpression,
isSequence, isSequence,
isString, isString
parseWorkflow
} from "@github/actions-workflow-parser"; } from "@github/actions-workflow-parser";
import {ErrorPolicy} from "@github/actions-workflow-parser/model/convert"; import {ErrorPolicy} from "@github/actions-workflow-parser/model/convert";
import {OPEN_EXPRESSION} from "@github/actions-workflow-parser/templates/template-constants"; import {OPEN_EXPRESSION} from "@github/actions-workflow-parser/templates/template-constants";
@@ -21,7 +19,6 @@ import {getContext, Mode} from "./context-providers/default";
import {getWorkflowContext, WorkflowContext} from "./context/workflow-context"; import {getWorkflowContext, WorkflowContext} from "./context/workflow-context";
import {validatorFunctions} from "./expression-validation/functions"; import {validatorFunctions} from "./expression-validation/functions";
import {error} from "./log"; import {error} from "./log";
import {nullTrace} from "./nulltrace";
import {isPotentiallyExpression} from "./utils/expression-detection"; import {isPotentiallyExpression} from "./utils/expression-detection";
import {findToken} from "./utils/find-token"; import {findToken} from "./utils/find-token";
import {guessIndentation} from "./utils/indentation-guesser"; import {guessIndentation} from "./utils/indentation-guesser";
@@ -31,6 +28,7 @@ import {transform} from "./utils/transform";
import {Value, ValueProviderConfig} from "./value-providers/config"; import {Value, ValueProviderConfig} from "./value-providers/config";
import {defaultValueProviders} from "./value-providers/default"; import {defaultValueProviders} from "./value-providers/default";
import {definitionValues} from "./value-providers/definition"; import {definitionValues} from "./value-providers/definition";
import {getParsedWorkflow, getWorkflowTemplate} from "./utils/workflow-cache";
export function getExpressionInput(input: string, pos: number): string { export function getExpressionInput(input: string, pos: number): string {
// Find start marker around the cursor position // Find start marker around the cursor position
@@ -66,21 +64,22 @@ export async function complete(
// Fix the input to work around YAML parsing issues // Fix the input to work around YAML parsing issues
const [newDoc, newPos] = transform(textDocument, position); const [newDoc, newPos] = transform(textDocument, position);
const file: File = { const file: File = {
name: textDocument.uri, name: textDocument.uri,
content: newDoc.getText() content: newDoc.getText()
}; };
const result = parseWorkflow(file, nullTrace);
if (!result.value) { const parsedWorkflow = getParsedWorkflow(file, textDocument.uri);
if (!parsedWorkflow) {
return []; return [];
} }
const {token, keyToken, parent, path} = findToken(newPos, result.value); const template = await getWorkflowTemplate(file, parsedWorkflow, textDocument.uri, config, {
const template = await convertWorkflowTemplate(file.name, result.context, result.value, config?.fileProvider, {
fetchReusableWorkflowDepth: config?.fileProvider ? 1 : 0, fetchReusableWorkflowDepth: config?.fileProvider ? 1 : 0,
errorPolicy: ErrorPolicy.TryConversion errorPolicy: ErrorPolicy.TryConversion
}); });
const {token, keyToken, parent, path} = findToken(newPos, parsedWorkflow.value);
const workflowContext = getWorkflowContext(textDocument.uri, template, path); const workflowContext = getWorkflowContext(textDocument.uri, template, path);
// If we are inside an expression, take a different code-path. The workflow parser does not correctly create // If we are inside an expression, take a different code-path. The workflow parser does not correctly create
+4 -5
View File
@@ -1,12 +1,11 @@
import {convertWorkflowTemplate, parseWorkflow} from "@github/actions-workflow-parser";
import {ErrorPolicy} from "@github/actions-workflow-parser/model/convert"; import {ErrorPolicy} from "@github/actions-workflow-parser/model/convert";
import {isJob} from "@github/actions-workflow-parser/model/type-guards"; import {isJob} from "@github/actions-workflow-parser/model/type-guards";
import {File} from "@github/actions-workflow-parser/workflows/file"; import {File} from "@github/actions-workflow-parser/workflows/file";
import {TextDocument} from "vscode-languageserver-textdocument"; import {TextDocument} from "vscode-languageserver-textdocument";
import {DocumentLink} from "vscode-languageserver-types"; import {DocumentLink} from "vscode-languageserver-types";
import {parseActionReference} from "./action"; import {parseActionReference} from "./action";
import {nullTrace} from "./nulltrace";
import {mapRange} from "./utils/range"; import {mapRange} from "./utils/range";
import {getParsedWorkflow, getWorkflowTemplate} from "./utils/workflow-cache";
export async function documentLinks(document: TextDocument): Promise<DocumentLink[]> { export async function documentLinks(document: TextDocument): Promise<DocumentLink[]> {
const file: File = { const file: File = {
@@ -14,12 +13,12 @@ export async function documentLinks(document: TextDocument): Promise<DocumentLin
content: document.getText() content: document.getText()
}; };
const result = parseWorkflow(file, nullTrace); const parsedWorkflow = getParsedWorkflow(file, document.uri);
if (!result.value) { if (!parsedWorkflow) {
return []; return [];
} }
const template = await convertWorkflowTemplate(file.name, result.context, result.value!, undefined, { const template = await getWorkflowTemplate(file, parsedWorkflow, document.uri, undefined, {
errorPolicy: ErrorPolicy.TryConversion errorPolicy: ErrorPolicy.TryConversion
}); });
+10 -9
View File
@@ -1,7 +1,6 @@
import {DescriptionDictionary, Parser} from "@github/actions-expressions"; import {DescriptionDictionary, Parser} from "@github/actions-expressions";
import {FunctionInfo} from "@github/actions-expressions/funcs/info"; import {FunctionInfo} from "@github/actions-expressions/funcs/info";
import {Lexer} from "@github/actions-expressions/lexer"; import {Lexer} from "@github/actions-expressions/lexer";
import {convertWorkflowTemplate, parseWorkflow} from "@github/actions-workflow-parser";
import {ErrorPolicy} from "@github/actions-workflow-parser/model/convert"; import {ErrorPolicy} from "@github/actions-workflow-parser/model/convert";
import {getCronDescription} from "@github/actions-workflow-parser/model/converter/cron"; import {getCronDescription} from "@github/actions-workflow-parser/model/converter/cron";
import {splitAllowedContext} from "@github/actions-workflow-parser/templates/allowed-context"; import {splitAllowedContext} from "@github/actions-workflow-parser/templates/allowed-context";
@@ -23,10 +22,10 @@ import {ExpressionPos, mapToExpressionPos} from "./expression-hover/expression-p
import {HoverVisitor} from "./expression-hover/visitor"; import {HoverVisitor} from "./expression-hover/visitor";
import {validatorFunctions} from "./expression-validation/functions"; import {validatorFunctions} from "./expression-validation/functions";
import {info} from "./log"; import {info} from "./log";
import {nullTrace} from "./nulltrace";
import {isPotentiallyExpression} from "./utils/expression-detection"; import {isPotentiallyExpression} from "./utils/expression-detection";
import {findToken, TokenResult} from "./utils/find-token"; import {findToken, TokenResult} from "./utils/find-token";
import {mapRange} from "./utils/range"; import {mapRange} from "./utils/range";
import {getParsedWorkflow, getWorkflowTemplate} from "./utils/workflow-cache";
export type HoverConfig = { export type HoverConfig = {
descriptionProvider?: DescriptionProvider; descriptionProvider?: DescriptionProvider;
@@ -43,19 +42,21 @@ export async function hover(document: TextDocument, position: Position, config?:
name: document.uri, name: document.uri,
content: document.getText() content: document.getText()
}; };
const result = parseWorkflow(file, nullTrace);
if (!result.value) { const parsedWorkflow = getParsedWorkflow(file, document.uri);
if (!parsedWorkflow) {
return null; return null;
} }
const tokenResult = findToken(position, result.value); const template = await getWorkflowTemplate(file, parsedWorkflow, document.uri, config, {
const {token, keyToken, parent} = tokenResult;
const tokenDefinitionInfo = (keyToken || parent || token)?.definitionInfo;
const template = await convertWorkflowTemplate(file.name, result.context, result.value, config?.fileProvider, {
errorPolicy: ErrorPolicy.TryConversion, errorPolicy: ErrorPolicy.TryConversion,
fetchReusableWorkflowDepth: config?.fileProvider ? 1 : 0 fetchReusableWorkflowDepth: config?.fileProvider ? 1 : 0
}); });
const tokenResult = findToken(position, parsedWorkflow.value);
const {token, keyToken, parent} = tokenResult;
const tokenDefinitionInfo = (keyToken || parent || token)?.definitionInfo;
const workflowContext = getWorkflowContext(document.uri, template, tokenResult.path); const workflowContext = getWorkflowContext(document.uri, template, tokenResult.path);
if (token && tokenDefinitionInfo) { if (token && tokenDefinitionInfo) {
if (isBasicExpression(token) || isPotentiallyExpression(token)) { if (isBasicExpression(token) || isPotentiallyExpression(token)) {
@@ -0,0 +1,63 @@
import {convertWorkflowTemplate, parseWorkflow} from "@github/actions-workflow-parser";
import {WorkflowTemplateConverterOptions} from "@github/actions-workflow-parser/model/convert";
import {File} from "@github/actions-workflow-parser/workflows/file";
import {ParseWorkflowResult} from "@github/actions-workflow-parser";
import {WorkflowTemplate} from "@github/actions-workflow-parser";
import {nullTrace} from "../nulltrace";
import { CompletionConfig } from "../complete";
const parsedWorkflowCache = new Map<string, ParseWorkflowResult>();
const workflowTemplateCache = new Map<string, WorkflowTemplate>();
export function cacheParsedWorkflow(uri: string, parsedWorkflowResult: ParseWorkflowResult) {
parsedWorkflowCache.set(uri, parsedWorkflowResult);
}
export function getParsedWorkflowCacheEntry(uri: string): ParseWorkflowResult | undefined {
return parsedWorkflowCache.get(uri);
}
export function clearParsedCacheEntry(uri: string) {
parsedWorkflowCache.delete(uri);
}
export function clearParsedCache() {
parsedWorkflowCache.clear();
}
export function cacheWorkflowTemplate(uri: string, workflowTemplate: WorkflowTemplate) {
workflowTemplateCache.set(uri, workflowTemplate);
}
export function getWorkflowTemplateCacheEntry(uri: string): WorkflowTemplate | undefined {
return workflowTemplateCache.get(uri);
}
export function clearWorkflowTemplateCacheEntry(uri: string) {
workflowTemplateCache.delete(uri);
}
export function clearWorkflowTemplateCache() {
workflowTemplateCache.clear();
}
export function getParsedWorkflow(file: File, uri: string): ParseWorkflowResult | undefined {
let result = getParsedWorkflowCacheEntry(uri);
if (!result || !result.value) {
result = parseWorkflow(file, nullTrace);
if (!result.value) {
return undefined;
}
cacheParsedWorkflow(uri, result)
}
return result;
}
export async function getWorkflowTemplate(file: File, parsedWorkflow: ParseWorkflowResult, uri: string, config?: CompletionConfig, options?: WorkflowTemplateConverterOptions): Promise<WorkflowTemplate> {
let template = getWorkflowTemplateCacheEntry(uri);
if (!template) {
template = await convertWorkflowTemplate(file.name, parsedWorkflow.context, parsedWorkflow.value!, config?.fileProvider, options);
cacheWorkflowTemplate(uri, template);
}
return template;
}
+10 -10
View File
@@ -1,11 +1,8 @@
import {Evaluator, ExpressionEvaluationError, Lexer, Parser} from "@github/actions-expressions"; import {Evaluator, ExpressionEvaluationError, Lexer, Parser} from "@github/actions-expressions";
import {Expr} from "@github/actions-expressions/ast"; import {Expr} from "@github/actions-expressions/ast";
import { import {
convertWorkflowTemplate,
isBasicExpression, isBasicExpression,
isString, isString,
parseWorkflow,
ParseWorkflowResult,
WorkflowTemplate WorkflowTemplate
} from "@github/actions-workflow-parser"; } from "@github/actions-workflow-parser";
import {ErrorPolicy} from "@github/actions-workflow-parser/model/convert"; import {ErrorPolicy} from "@github/actions-workflow-parser/model/convert";
@@ -18,19 +15,18 @@ import {FileProvider} from "@github/actions-workflow-parser/workflows/file-provi
import {TextDocument} from "vscode-languageserver-textdocument"; import {TextDocument} from "vscode-languageserver-textdocument";
import {Diagnostic, DiagnosticSeverity, URI} from "vscode-languageserver-types"; import {Diagnostic, DiagnosticSeverity, URI} from "vscode-languageserver-types";
import {ActionMetadata, ActionReference} from "./action"; import {ActionMetadata, ActionReference} from "./action";
import {ContextProviderConfig} from "./context-providers/config"; import {ContextProviderConfig} from "./context-providers/config";
import {getContext, Mode} from "./context-providers/default"; import {getContext, Mode} from "./context-providers/default";
import {getWorkflowContext, WorkflowContext} from "./context/workflow-context"; import {getWorkflowContext, WorkflowContext} from "./context/workflow-context";
import {AccessError, wrapDictionary} from "./expression-validation/error-dictionary"; import {AccessError, wrapDictionary} from "./expression-validation/error-dictionary";
import {validatorFunctions} from "./expression-validation/functions"; import {validatorFunctions} from "./expression-validation/functions";
import {error} from "./log"; import {error} from "./log";
import {nullTrace} from "./nulltrace";
import {findToken} from "./utils/find-token"; import {findToken} from "./utils/find-token";
import {mapRange} from "./utils/range"; import {mapRange} from "./utils/range";
import {validateAction} from "./validate-action"; import {validateAction} from "./validate-action";
import {ValueProviderConfig, ValueProviderKind} from "./value-providers/config"; import {ValueProviderConfig, ValueProviderKind} from "./value-providers/config";
import {defaultValueProviders} from "./value-providers/default"; import {defaultValueProviders} from "./value-providers/default";
import { getParsedWorkflow, getWorkflowTemplate } from "./utils/workflow-cache";
export type ValidationConfig = { export type ValidationConfig = {
valueProviderConfig?: ValueProviderConfig; valueProviderConfig?: ValueProviderConfig;
@@ -54,20 +50,24 @@ export async function validate(textDocument: TextDocument, config?: ValidationCo
const diagnostics: Diagnostic[] = []; const diagnostics: Diagnostic[] = [];
try { try {
const result: ParseWorkflowResult = parseWorkflow(file, nullTrace); const parsedWorkflow = getParsedWorkflow(file, textDocument.uri);
if (result.value) { if (!parsedWorkflow) {
return [];
}
if (parsedWorkflow.value) {
// Errors will be updated in the context. Attempt to do the conversion anyway in order to give the user more information // Errors will be updated in the context. Attempt to do the conversion anyway in order to give the user more information
const template = await convertWorkflowTemplate(file.name, result.context, result.value, config?.fileProvider, { const template = await getWorkflowTemplate(file, parsedWorkflow, textDocument.uri, config, {
fetchReusableWorkflowDepth: config?.fileProvider ? 1 : 0, fetchReusableWorkflowDepth: config?.fileProvider ? 1 : 0,
errorPolicy: ErrorPolicy.TryConversion errorPolicy: ErrorPolicy.TryConversion
}); });
// Validate expressions and value providers // Validate expressions and value providers
await additionalValidations(diagnostics, textDocument.uri, template, result.value, config); await additionalValidations(diagnostics, textDocument.uri, template, parsedWorkflow.value, config);
} }
// For now map parser errors directly to diagnostics // For now map parser errors directly to diagnostics
for (const error of result.context.errors.getErrors()) { for (const error of parsedWorkflow.context.errors.getErrors()) {
let range = mapRange(error.range); let range = mapRange(error.range);
diagnostics.push({ diagnostics.push({
-16
View File
@@ -48,17 +48,11 @@ const defaultOptions: Required<WorkflowTemplateConverterOptions> = {
}; };
export async function convertWorkflowTemplate( export async function convertWorkflowTemplate(
uri: string,
context: TemplateContext, context: TemplateContext,
root: TemplateToken, root: TemplateToken,
fileProvider?: FileProvider, fileProvider?: FileProvider,
options: WorkflowTemplateConverterOptions = defaultOptions options: WorkflowTemplateConverterOptions = defaultOptions
): Promise<WorkflowTemplate> { ): Promise<WorkflowTemplate> {
const cachedResult = workflowTemplateCache.get(uri)
if (cachedResult) {
return cachedResult;
}
const result = {} as WorkflowTemplate; const result = {} as WorkflowTemplate;
const opts = getOptionsWithDefaults(options); const opts = getOptionsWithDefaults(options);
@@ -66,7 +60,6 @@ export async function convertWorkflowTemplate(
result.errors = context.errors.getErrors().map(x => ({ result.errors = context.errors.getErrors().map(x => ({
Message: x.message Message: x.message
})); }));
workflowTemplateCache.set(uri, result);
return result; return result;
} }
@@ -138,7 +131,6 @@ export async function convertWorkflowTemplate(
} }
} }
workflowTemplateCache.set(uri, result);
return result; return result;
} }
@@ -155,11 +147,3 @@ function getOptionsWithDefaults(options: WorkflowTemplateConverterOptions): Requ
errorPolicy: options.errorPolicy !== undefined ? options.errorPolicy : defaultOptions.errorPolicy errorPolicy: options.errorPolicy !== undefined ? options.errorPolicy : defaultOptions.errorPolicy
}; };
} }
export function clearWorkflowTemplateCacheEntry(uri: string) {
workflowTemplateCache.delete(uri);
}
export function clearWorkflowTemplateCache() {
workflowTemplateCache.clear();
}
@@ -7,8 +7,6 @@ import {WORKFLOW_ROOT} from "./workflow-constants";
import {getWorkflowSchema} from "./workflow-schema"; import {getWorkflowSchema} from "./workflow-schema";
import {YamlObjectReader} from "./yaml-object-reader"; import {YamlObjectReader} from "./yaml-object-reader";
const parsedWorkflowCache = new Map<string, ParseWorkflowResult>();
export interface ParseWorkflowResult { export interface ParseWorkflowResult {
context: TemplateContext; context: TemplateContext;
value: TemplateToken | undefined; value: TemplateToken | undefined;
@@ -17,11 +15,6 @@ export interface ParseWorkflowResult {
export function parseWorkflow(entryFile: File, trace: TraceWriter): ParseWorkflowResult; export function parseWorkflow(entryFile: File, trace: TraceWriter): ParseWorkflowResult;
export function parseWorkflow(entryFile: File, context: TemplateContext): ParseWorkflowResult; export function parseWorkflow(entryFile: File, context: TemplateContext): ParseWorkflowResult;
export function parseWorkflow(entryFile: File, contextOrTrace: TraceWriter | TemplateContext): ParseWorkflowResult { export function parseWorkflow(entryFile: File, contextOrTrace: TraceWriter | TemplateContext): ParseWorkflowResult {
const cachedResult = parsedWorkflowCache.get(entryFile.name)
if (cachedResult) {
return cachedResult;
}
const context = const context =
contextOrTrace instanceof TemplateContext contextOrTrace instanceof TemplateContext
? contextOrTrace ? contextOrTrace
@@ -38,7 +31,6 @@ export function parseWorkflow(entryFile: File, contextOrTrace: TraceWriter | Tem
context, context,
value: undefined value: undefined
}; };
parsedWorkflowCache.set(entryFile.name, result);
return result return result
} }
const templateToken = templateReader.readTemplate(context, WORKFLOW_ROOT, reader, fileId); const templateToken = templateReader.readTemplate(context, WORKFLOW_ROOT, reader, fileId);
@@ -47,14 +39,5 @@ export function parseWorkflow(entryFile: File, contextOrTrace: TraceWriter | Tem
context, context,
value: templateToken value: templateToken
} satisfies ParseWorkflowResult; } satisfies ParseWorkflowResult;
parsedWorkflowCache.set(entryFile.name, result);
return result; return result;
} }
export function clearParsedCacheEntry(path: string) {
parsedWorkflowCache.delete(path);
}
export function clearParsedCache() {
parsedWorkflowCache.clear();
}