Files
languageservices/workflow-parser/src/templates/template-context.ts
T
2023-03-15 15:58:44 -04:00

162 lines
4.9 KiB
TypeScript

import {FunctionInfo} from "@github/actions-expressions/funcs/info";
import {TemplateSchema} from "./schema/template-schema";
import {TemplateValidationError} from "./template-validation-error";
import {TemplateToken} from "./tokens";
import {TokenRange} from "./tokens/token-range";
import {TraceWriter} from "./trace-writer";
/**
* Context object that is flowed through while loading and evaluating object templates
*/
export class TemplateContext {
private readonly _fileIds: {[name: string]: number} = {};
private readonly _fileNames: string[] = [];
/**
* Available functions within expression contexts
*/
public readonly expressionFunctions: FunctionInfo[] = [];
/**
* Available values within expression contexts
*/
public readonly expressionNamedContexts: string[] = [];
public readonly errors: TemplateValidationErrors;
public readonly schema: TemplateSchema;
public readonly trace: TraceWriter;
public readonly state: {[key: string]: unknown} = {};
public constructor(errors: TemplateValidationErrors, schema: TemplateSchema, trace: TraceWriter) {
this.errors = errors;
this.schema = schema;
this.trace = trace;
}
public error(token: TemplateToken | undefined, err: string, tokenRange?: TokenRange): void;
public error(token: TemplateToken | undefined, err: Error, tokenRange?: TokenRange): void;
public error(token: TemplateToken | undefined, err: unknown): void;
public error(fileId: number | undefined, err: string, tokenRange?: TokenRange): void;
public error(fileId: number | undefined, err: Error, tokenRange?: TokenRange): void;
public error(fileId: number | undefined, err: unknown, tokenRange?: TokenRange): void;
public error(
tokenOrFileId: TemplateToken | number | undefined,
err: string | Error | unknown,
tokenRange?: TokenRange
): void {
const token = tokenOrFileId as TemplateToken | undefined;
const range = tokenRange || token?.range;
const prefix = this.getErrorPrefix(token?.file ?? (tokenOrFileId as number | undefined), token?.line, token?.col);
const message = (err as Error | undefined)?.message ?? String(err);
const e = new TemplateValidationError(message, prefix, undefined, range);
this.errors.add(e);
this.trace.error(e.message);
}
/**
* Gets or adds the file ID
*/
public getFileId(file: string) {
const key = file.toUpperCase();
let id: number | undefined = this._fileIds[key];
if (id === undefined) {
id = this._fileNames.length + 1;
this._fileIds[key] = id;
this._fileNames.push(file);
}
return id;
}
/**
* Looks up a file name by ID. Returns undefined if not found.
*/
public getFileName(fileId: number): string | undefined {
return this._fileNames.length >= fileId ? this._fileNames[fileId - 1] : undefined;
}
/**
* Gets a copy of the file table
*/
public getFileTable(): string[] {
return this._fileNames.slice();
}
private getErrorPrefix(fileId?: number, line?: number, column?: number): string {
const fileName = fileId !== undefined ? this.getFileName(fileId) : undefined;
if (fileName) {
if (line !== undefined && column !== undefined) {
return `${fileName} (Line: ${line}, Col: ${column})`;
} else {
return fileName;
}
} else if (line !== undefined && column !== undefined) {
return `(Line: ${line}, Col: ${column})`;
} else {
return "";
}
}
}
/**
* Provides information about errors which occurred during validation
*/
export class TemplateValidationErrors {
private readonly _maxErrors: number;
private readonly _maxMessageLength: number;
private _errors: TemplateValidationError[] = [];
public constructor(maxErrors?: number, maxMessageLength?: number) {
this._maxErrors = maxErrors ?? 0;
this._maxMessageLength = maxMessageLength ?? 0;
}
public get count(): number {
return this._errors.length;
}
public add(err: TemplateValidationError | TemplateValidationError[]): void {
for (let e of Array.isArray(err) ? err : [err]) {
// Check max errors
if (this._maxErrors <= 0 || this._errors.length < this._maxErrors) {
// Check max message length
if (this._maxMessageLength > 0 && e.message.length > this._maxMessageLength) {
e = new TemplateValidationError(
e.message.substring(0, this._maxMessageLength) + "[...]",
e.prefix,
e.code,
e.range
);
}
this._errors.push(e);
}
}
}
/**
* Throws if any errors
* @param prefix The error message prefix
*/
public check(prefix?: string): void {
if (this._errors.length <= 0) {
return;
}
if (!prefix) {
prefix = "The template is not valid.";
}
throw new Error(`${prefix} ${this._errors.map(x => x.message).join(",")}`);
}
public clear(): void {
this._errors = [];
}
public getErrors(): TemplateValidationError[] {
return this._errors.slice();
}
}