From e8060b6e156674679b9e7017bda598c7569158e4 Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Wed, 8 Feb 2023 17:37:45 -0500 Subject: [PATCH] Remove unused template evaluation code --- .../src/templates/evaluate-expression.ts | 131 --- .../src/templates/template-evaluator.ts | 394 -------- .../src/templates/template-unraveler.ts | 912 ------------------ .../src/workflows/workflow-constants.ts | 1 - .../src/workflows/workflow-evaluator.ts | 38 - .../src/workflows/workflow-v1.0.json | 1 - 6 files changed, 1477 deletions(-) delete mode 100644 actions-workflow-parser/src/templates/evaluate-expression.ts delete mode 100644 actions-workflow-parser/src/templates/template-evaluator.ts delete mode 100644 actions-workflow-parser/src/templates/template-unraveler.ts delete mode 100644 actions-workflow-parser/src/workflows/workflow-evaluator.ts delete mode 120000 actions-workflow-parser/src/workflows/workflow-v1.0.json diff --git a/actions-workflow-parser/src/templates/evaluate-expression.ts b/actions-workflow-parser/src/templates/evaluate-expression.ts deleted file mode 100644 index 7f43f52..0000000 --- a/actions-workflow-parser/src/templates/evaluate-expression.ts +++ /dev/null @@ -1,131 +0,0 @@ -import {Evaluator, Lexer, Parser} from "@github/actions-expressions"; -import {Expr} from "@github/actions-expressions/ast"; -import {Dictionary, ExpressionData, Kind} from "@github/actions-expressions/data/index"; -import {FunctionInfo} from "@github/actions-expressions/funcs/info"; -import {TemplateContext} from "./template-context"; -import { - BasicExpressionToken, - BooleanToken, - LiteralToken, - MappingToken, - NullToken, - NumberToken, - SequenceToken, - StringToken, - TemplateToken -} from "./tokens"; -import {TokenType} from "./tokens/types"; - -export function evaluateStringToken(token: BasicExpressionToken, context: TemplateContext): TemplateToken { - const expr = parseExpression(token.expression, context.expressionNamedContexts, context.expressionFunctions); - if (!expr) { - throw new Error("Unexpected empty expression"); - } - // TODO: Pass in context - const evaluator = new Evaluator(expr, new Dictionary()); - const result = evaluator.evaluate(); - if (!result.primitive) { - context.error(token, "Expected a string"); - return new StringToken(token.file, token.range, token.expression, token.definitionInfo); - } - - return new StringToken(token.file, token.range, result.coerceString(), token.definitionInfo); -} - -export function evaluateSequenceToken(token: BasicExpressionToken, context: TemplateContext): TemplateToken { - const expr = parseExpression(token.expression, context.expressionNamedContexts, context.expressionFunctions); - if (!expr) { - throw new Error("Unexpected empty expression"); - } - // TODO: Pass in context - const evaluator = new Evaluator(expr, new Dictionary()); - const result = evaluator.evaluate(); - const value = convertToTemplateToken(token, result); - if (value.templateTokenType !== TokenType.Sequence) { - context.error(token, "Expected a sequence"); - return new SequenceToken(token.file, token.range, token.definitionInfo); - } - return value; -} - -export function evaluateMappingToken(token: BasicExpressionToken, context: TemplateContext): MappingToken { - const expr = parseExpression(token.expression, context.expressionNamedContexts, context.expressionFunctions); - if (!expr) { - throw new Error("Unexpected empty expression"); - } - // TODO: Pass in context - const evaluator = new Evaluator(expr, new Dictionary()); - const result = evaluator.evaluate(); - const value = convertToTemplateToken(token, result); - if (value.templateTokenType !== TokenType.Mapping) { - context.error(token, "Expected a mapping"); - return new MappingToken(token.file, token.range, token.definitionInfo); - } - return value as MappingToken; -} - -export function evaluateTemplateToken(token: BasicExpressionToken, context: TemplateContext): TemplateToken { - const expr = parseExpression(token.expression, context.expressionNamedContexts, context.expressionFunctions); - if (!expr) { - throw new Error("Unexpected empty expression"); - } - // TODO: Pass in context - const evaluator = new Evaluator(expr, new Dictionary()); - const result = evaluator.evaluate(); - return convertToTemplateToken(token, result); -} - -function convertToTemplateToken(token: BasicExpressionToken, result: ExpressionData): TemplateToken { - // Literal - const literal = convertToLiteralToken(token, result); - if (literal) { - return literal; - } - - // TODO: Support expressions that return a sequence or mapping token - - // Leverage the expression SDK to traverse the object - switch (result.kind) { - case Kind.Dictionary: { - const mapping = new MappingToken(token.file, token.range, token.definitionInfo); - for (const {key, value} of result.pairs()) { - const keyToken = new StringToken(token.file, token.range, key, token.definitionInfo); - const valueToken = convertToTemplateToken(token, value); - mapping.add(keyToken, valueToken); - } - return mapping; - } - case Kind.Array: { - const sequence = new SequenceToken(token.file, token.range, token.definitionInfo); - for (const value of result.values()) { - const itemToken = convertToTemplateToken(token, value); - sequence.add(itemToken); - } - return sequence; - } - default: - throw new Error("Unable to convert the object to a template token"); - } -} - -function convertToLiteralToken(token: BasicExpressionToken, result: ExpressionData): LiteralToken | undefined { - switch (result.kind) { - case Kind.Null: - return new NullToken(token.file, token.range, token.definitionInfo); - case Kind.Boolean: - return new BooleanToken(token.file, token.range, result.value, token.definitionInfo); - case Kind.Number: - return new NumberToken(token.file, token.range, result.value, token.definitionInfo); - case Kind.String: - return new StringToken(token.file, token.range, result.value, token.definitionInfo); - } - - return undefined; -} - -function parseExpression(expression: string, contexts: string[], functions: FunctionInfo[]): Expr { - const lexer = new Lexer(expression); - const result = lexer.lex(); - const p = new Parser(result.tokens, contexts, functions); - return p.parse(); -} diff --git a/actions-workflow-parser/src/templates/template-evaluator.ts b/actions-workflow-parser/src/templates/template-evaluator.ts deleted file mode 100644 index eba3f35..0000000 --- a/actions-workflow-parser/src/templates/template-evaluator.ts +++ /dev/null @@ -1,394 +0,0 @@ -// called at runtime. expands expressions and re-validates the schema result after expansion - -import {TemplateSchema} from "./schema"; -import {Definition} from "./schema/definition"; -import {DefinitionType} from "./schema/definition-type"; -import {MappingDefinition} from "./schema/mapping-definition"; -import {ScalarDefinition} from "./schema/scalar-definition"; -import {SequenceDefinition} from "./schema/sequence-definition"; -import {ANY} from "./template-constants"; -import {TemplateContext} from "./template-context"; -import {TemplateUnraveler} from "./template-unraveler"; -import {LiteralToken, MappingToken, ScalarToken, StringToken, TemplateToken} from "./tokens"; -import {TokenType} from "./tokens/types"; - -export function evaluateTemplate( - context: TemplateContext, - type: string, - template: TemplateToken, - removeBytes: number, - fileId: number | undefined -): TemplateToken | undefined { - let result: TemplateToken | undefined; - - const evaluator = new TemplateEvaluator(context, template, removeBytes); - try { - const definitionInfo = new DefinitionInfo(context, type); - result = evaluator.evaluate(definitionInfo); - - if (result) { - evaluator.unraveler.readEnd(); - } - } catch (err) { - context.error(fileId, err); - result = undefined; - } - - return result; -} - -class TemplateEvaluator { - public readonly context: TemplateContext; - public readonly schema: TemplateSchema; - public readonly unraveler: TemplateUnraveler; - - public constructor(context: TemplateContext, template: TemplateToken, removeBytes: number) { - this.context = context; - this.schema = context.schema; - this.unraveler = new TemplateUnraveler(context, template, removeBytes); - } - - public evaluate(definition: DefinitionInfo): TemplateToken { - // Scalar - const scalar = this.unraveler.allowScalar(definition.expand); - if (scalar) { - if (scalar.isLiteral) { - return this.validate(scalar as LiteralToken, definition); - } else { - return scalar; - } - } - - // Sequence - const sequence = this.unraveler.allowSequenceStart(definition.expand); - if (sequence) { - const sequenceDefinition = definition.getDefinitionsOfType(DefinitionType.Sequence)[0] as - | SequenceDefinition - | undefined; - - // Legal - if (sequenceDefinition) { - const itemDefinition = new DefinitionInfo(definition, sequenceDefinition.itemType); - - // Add each item - while (!this.unraveler.allowSequenceEnd(definition.expand)) { - const item = this.evaluate(itemDefinition); - sequence.add(item); - } - } - // Illegal - else { - // Error - this.context.error(sequence, "A sequence was not expected"); - - // Skip each item - while (!this.unraveler.allowSequenceEnd(false)) { - this.unraveler.skipSequenceItem(); - } - } - - return sequence; - } - - // Mapping - const mapping = this.unraveler.allowMappingStart(definition.expand); - if (mapping) { - const mappingDefinitions = definition.getDefinitionsOfType(DefinitionType.Mapping) as MappingDefinition[]; - - // Legal - if (mappingDefinitions.length > 0) { - if ( - mappingDefinitions.length > 1 || - Object.keys(mappingDefinitions[0].properties).length > 0 || - !mappingDefinitions[0].looseKeyType - ) { - this.handleMappingWithWellKnownProperties(definition, mappingDefinitions, mapping); - } else { - const keyDefinition = new DefinitionInfo(definition, mappingDefinitions[0].looseKeyType); - const valueDefinition = new DefinitionInfo(definition, mappingDefinitions[0].looseValueType); - this.handleMappingWithAllLooseProperties(definition, keyDefinition, valueDefinition, mapping); - } - } - // Illegal - else { - this.context.error(mapping, "A mapping was not expected"); - - while (!this.unraveler.allowMappingEnd(false)) { - this.unraveler.skipMappingKey(); - this.unraveler.skipMappingValue(); - } - } - - return mapping; - } - - throw new Error("Expected a scalar value, a sequence, or a mapping"); - } - - private handleMappingWithWellKnownProperties( - definition: DefinitionInfo, - mappingDefinitions: MappingDefinition[], - mapping: MappingToken - ) { - // Check if loose properties are allowed - let looseKeyType: string | undefined; - let looseValueType: string | undefined; - let looseKeyDefinition: DefinitionInfo | undefined; - let looseValueDefinition: DefinitionInfo | undefined; - if (mappingDefinitions[0].looseKeyType) { - looseKeyType = mappingDefinitions[0].looseKeyType; - looseValueType = mappingDefinitions[0].looseValueType; - } - - const upperKeys: {[upperKey: string]: boolean} = {}; - let hasExpressionKey = false; - - let nextKeyScalar: ScalarToken | undefined; - while ((nextKeyScalar = this.unraveler.allowScalar(definition.expand))) { - // Expression - if (nextKeyScalar.isExpression) { - hasExpressionKey = true; - const anyDefinition = new DefinitionInfo(definition, ANY); - mapping.add(nextKeyScalar, this.evaluate(anyDefinition)); - continue; - } - - // Convert to StringToken if required - const nextKey = - nextKeyScalar.templateTokenType === TokenType.String - ? (nextKeyScalar as StringToken) - : new StringToken( - nextKeyScalar.file, - nextKeyScalar.range, - nextKeyScalar.toString(), - nextKeyScalar.definitionInfo - ); - - // Duplicate - const upperKey = nextKey.value.toUpperCase(); - if (upperKeys[upperKey]) { - this.context.error(nextKey, `'${nextKey.value}' is already defined`); - this.unraveler.skipMappingValue(); - continue; - } - upperKeys[upperKey] = true; - - // Well known - const nextValuePropertyDef = this.schema.matchPropertyAndFilter(mappingDefinitions, nextKey.value); - if (nextValuePropertyDef?.type) { - const nextValueDefinition = new DefinitionInfo(definition, nextValuePropertyDef.type); - const nextValue = this.evaluate(nextValueDefinition); - nextValue.propertyDefinition = nextValuePropertyDef; - mapping.add(nextKey, nextValue); - continue; - } - - // Loose - if (looseKeyType) { - if (!looseKeyDefinition) { - looseKeyDefinition = new DefinitionInfo(definition, looseKeyType); - looseValueDefinition = new DefinitionInfo(definition, looseValueType!); - } - - this.validate(nextKey, looseKeyDefinition); - const nextValue = this.evaluate(looseValueDefinition!); - mapping.add(nextKey, nextValue); - continue; - } - - // Error - this.context.error(nextKey, `Unexpected value '${nextKey.value}'`); - this.unraveler.skipMappingValue(); - } - - // Unable to filter to one definition - if (mappingDefinitions.length > 1) { - const hitCount: {[key: string]: number} = {}; - for (const mappingDefinition of mappingDefinitions) { - for (const key of Object.keys(mappingDefinition.properties)) { - hitCount[key] = (hitCount[key] ?? 0) + 1; - } - } - - const nonDuplicates: string[] = []; - for (const key of Object.keys(hitCount)) { - if (hitCount[key] === 1) { - nonDuplicates.push(key); - } - } - - this.context.error( - mapping, - `There's not enough info to determine what you meant. Add one of these properties: ${nonDuplicates - .sort() - .join(", ")}` - ); - } - // Check required properties - else if (mappingDefinitions.length === 1 && !hasExpressionKey) { - for (const propertyName of Object.keys(mappingDefinitions[0].properties)) { - const propertyDef = mappingDefinitions[0].properties[propertyName]; - if (propertyDef.required && !upperKeys[propertyName.toUpperCase()]) { - this.context.error(mapping, `Required property is missing: ${propertyName}`); - } - } - } - - this.unraveler.readMappingEnd(); - } - - private handleMappingWithAllLooseProperties( - mappingDefinition: DefinitionInfo, - keyDefinition: DefinitionInfo, - valueDefinition: DefinitionInfo, - mapping: MappingToken - ): void { - const upperKeys: {[key: string]: boolean} = {}; - - let nextKeyScalar: ScalarToken | undefined; - while ((nextKeyScalar = this.unraveler.allowScalar(mappingDefinition.expand))) { - // Expression - if (nextKeyScalar.isExpression) { - if (nextKeyScalar.templateTokenType === TokenType.BasicExpression) { - mapping.add(nextKeyScalar, this.evaluate(valueDefinition)); - } else { - const anyDefinition = new DefinitionInfo(mappingDefinition, ANY); - mapping.add(nextKeyScalar, this.evaluate(anyDefinition)); - } - - continue; - } - - // Convert to StringToken if required - const nextKey = - nextKeyScalar.templateTokenType === TokenType.String - ? (nextKeyScalar as StringToken) - : new StringToken( - nextKeyScalar.file, - nextKeyScalar.range, - nextKeyScalar.toString(), - nextKeyScalar.definitionInfo - ); - - // Duplicate - const upperKey = nextKey.value.toUpperCase(); - if (upperKeys[upperKey]) { - this.context.error(nextKey, `'${nextKey.value}' is already defined`); - this.unraveler.skipMappingValue(); - continue; - } - upperKeys[upperKey] = true; - - // Validate - this.validate(nextKey, keyDefinition); - - // Add the pair - const nextValue = this.evaluate(valueDefinition); - mapping.add(nextKey, nextValue); - } - - this.unraveler.readMappingEnd(); - } - - private validate(literal: LiteralToken, definition: DefinitionInfo): LiteralToken { - // Legal - const scalarDefinitions = definition.getScalarDefinitions(); - if (scalarDefinitions.some(x => x.isMatch(literal))) { - return literal; - } - - // Not a string, convert - if (literal.templateTokenType !== TokenType.String) { - const stringLiteral = new StringToken(literal.file, literal.range, literal.toString(), literal.definitionInfo); - - // Legal - if (scalarDefinitions.some(x => x.isMatch(stringLiteral))) { - return stringLiteral; - } - } - - // Illegal - this.context.error(literal, `Unexpected value '${literal.toString()}'`); - return literal; - } -} - -class DefinitionInfo { - /** - * Hashtable of available contexts - */ - private readonly _upperAvailable: {[context: string]: boolean}; - /** - * Allowed context - */ - private readonly _allowed: string[]; - private readonly _schema: TemplateSchema; - public readonly isDefinitionInfo = true; - public readonly definition: Definition; - public readonly expand: boolean; - - public constructor(context: TemplateContext, name: string); - public constructor(parent: DefinitionInfo, name: string); - public constructor(contextOrParent: TemplateContext | DefinitionInfo, name: string) { - // "parent" overload - let parent: DefinitionInfo | undefined; - if ((contextOrParent as DefinitionInfo | undefined)?.isDefinitionInfo === true) { - parent = contextOrParent as DefinitionInfo; - this._schema = parent._schema; - this._upperAvailable = parent._upperAvailable; - } - // "context" overload - else { - const context = contextOrParent as TemplateContext; - this._schema = context.schema; - this._upperAvailable = {}; - for (const namedContext of context.expressionNamedContexts) { - this._upperAvailable[namedContext.toUpperCase()] = true; - } - for (const func of context.expressionFunctions) { - this._upperAvailable[`${func.name}()`.toUpperCase()] = true; - } - } - - // Lookup the definition - this.definition = this._schema.getDefinition(name); - - // Record allowed context - if (this.definition.evaluatorContext.length > 0) { - this._allowed = []; - this.expand = true; - - // Copy parent allowed context - const upperSeen: {[upper: string]: boolean} = {}; - for (const context of parent?._allowed ?? []) { - this._allowed.push(context); - const upper = context.toUpperCase(); - upperSeen[upper] = true; - if (!this._upperAvailable[upper]) { - this.expand = false; - } - } - - // Append context if unseen - for (const context of this.definition.evaluatorContext) { - const upper = context.toUpperCase(); - if (!upperSeen[upper]) { - this._allowed.push(context); - upperSeen[upper] = true; - if (!this._upperAvailable[upper]) [(this.expand = false)]; - } - } - } else { - this._allowed = parent?._allowed ?? []; - this.expand = parent?.expand ?? false; - } - } - - public getScalarDefinitions(): ScalarDefinition[] { - return this._schema.getScalarDefinitions(this.definition); - } - - public getDefinitionsOfType(type: DefinitionType): Definition[] { - return this._schema.getDefinitionsOfType(this.definition, type); - } -} diff --git a/actions-workflow-parser/src/templates/template-unraveler.ts b/actions-workflow-parser/src/templates/template-unraveler.ts deleted file mode 100644 index 88d799a..0000000 --- a/actions-workflow-parser/src/templates/template-unraveler.ts +++ /dev/null @@ -1,912 +0,0 @@ -// Does just-in-time expression expansion - -import {evaluateMappingToken, evaluateStringToken, evaluateTemplateToken} from "./evaluate-expression"; -import {CLOSE_EXPRESSION, OPEN_EXPRESSION} from "./template-constants"; -import {TemplateContext} from "./template-context"; -import { - BasicExpressionToken, - InsertExpressionToken, - LiteralToken, - MappingToken, - ScalarToken, - SequenceToken, - StringToken, - TemplateToken -} from "./tokens"; -import {TokenType} from "./tokens/types"; - -/** - * This class allows callers to easily traverse a template object. - * This class hides the details of expression expansion, depth tracking, - * and memory tracking. - */ -export class TemplateUnraveler { - private readonly _context: TemplateContext; - private _current: ReaderState | undefined; - private _expanded = false; - - public constructor(context: TemplateContext, template: TemplateToken, removeBytes: number) { - this._context = context; - - // Initialize the reader state - this.moveFirst(template, removeBytes); - } - - public allowScalar(expand: boolean): ScalarToken | undefined { - if (expand) { - this.unravel(true); - } - - if (this._current?.value.isScalar) { - const scalar = this._current.value as ScalarToken; - this.moveNext(); - return scalar; - } - - return undefined; - } - - public allowSequenceStart(expand: boolean): SequenceToken | undefined { - if (expand) { - this.unravel(true); - } - - if (this._current?.value.templateTokenType === TokenType.Sequence && (this._current as SequenceState).isStart) { - const sequence = new SequenceToken( - this._current.value.file, - this._current.value.range, - this._current.value.definitionInfo - ); - this.moveNext(); - return sequence; - } - - return undefined; - } - - public allowSequenceEnd(expand: boolean): boolean { - if (expand) { - this.unravel(true); - } - - if (this._current?.value.templateTokenType === TokenType.Sequence && (this._current as SequenceState).isEnd) { - this.moveNext(); - return true; - } - - return false; - } - - public allowMappingStart(expand: boolean): MappingToken | undefined { - if (expand) { - this.unravel(true); - } - - if (this._current?.value.templateTokenType === TokenType.Mapping && (this._current as MappingState).isStart) { - const mapping = new MappingToken( - this._current.value.file, - this._current.value.range, - this._current.value.definitionInfo - ); - this.moveNext(); - return mapping; - } - - return undefined; - } - - public allowMappingEnd(expand: boolean): boolean { - if (expand) { - this.unravel(true); - } - - if (this._current?.value.templateTokenType === TokenType.Mapping && (this._current as MappingState).isEnd) { - this.moveNext(); - return true; - } - - return false; - } - - public readEnd(): void { - if (this._current !== undefined) { - throw new Error(`Expected end of template object. ${this.dumpState("readEnd")}`); - } - } - - public readMappingEnd(): void { - if (!this.allowMappingEnd(false)) { - throw new Error(`Unexpected state while attempting to read the mapping end. ${this.dumpState("readMappingEnd")}`); - } - } - - public skipSequenceItem(): void { - if (this._current?.parent?.value.templateTokenType !== TokenType.Sequence) { - throw new Error( - `Unexpected state while attempting to skip the current sequence item. ${this.dumpState("skipSequenceItem")}` - ); - } - - this.moveNext(true); - } - - public skipMappingKey(): void { - if ( - this._current?.parent?.value.templateTokenType !== TokenType.Mapping || - !(this._current.parent as MappingState).isKey - ) { - throw new Error( - `Unexpected state while attempting to skip the current mapping key. ${this.dumpState("skipMappingKey")}` - ); - } - - this.moveNext(true); - } - - public skipMappingValue(): void { - if ( - this._current?.parent?.value.templateTokenType !== TokenType.Mapping || - (this._current.parent as MappingState).isKey - ) { - throw new Error( - `Unexpected state while attempting to skip the current mapping value. ${this.dumpState("skipMappingValue")}` - ); - } - - this.moveNext(true); - } - - private dumpState(operation: string): string { - const result: string[] = []; - - if (operation) { - result.push(`Operation: ${operation}`); - } - - if (this._current === undefined) { - result.push(`State: (null)`); - } else { - result.push(`State:`); - result.push(""); - - // Push state hierarchy - const stack: ReaderState[] = []; - let curr: ReaderState | undefined = this._current; - while (curr) { - result.push(curr.toString()); - curr = curr.parent; - } - } - - return result.join("\n"); - } - - private moveFirst(value: TemplateToken, removeBytes: number): void { - switch (value.templateTokenType) { - case TokenType.Null: - case TokenType.Boolean: - case TokenType.Number: - case TokenType.String: - case TokenType.Sequence: - case TokenType.Mapping: - case TokenType.BasicExpression: - break; - default: - throw new Error(`Unexpected type '${value.typeName()}' when initializing object reader state`); - } - - this._current = ReaderState.createState(undefined, value, this._context); - } - - private moveNext(skipNestedEvents?: boolean): void { - if (this._current === undefined) { - return; - } - - // Sequence start - if ( - this._current.value.templateTokenType === TokenType.Sequence && - (this._current as SequenceState).isStart && - !skipNestedEvents - ) { - // Move to the first item or sequence end - const sequenceState = this._current as SequenceState; - this._current = sequenceState.next(); - } - // Mapping state - else if ( - this._current.value.templateTokenType === TokenType.Mapping && - (this._current as MappingState).isStart && - !skipNestedEvents - ) { - // Move to the first item key or mapping end - const mappingState = this._current as MappingState; - this._current = mappingState.next(); - } - // Parent is a sequence - else if (this._current.parent?.value.templateTokenType === TokenType.Sequence) { - // Move to the next item or sequence end - const parentSequenceState = this._current.parent as SequenceState; - this._current = parentSequenceState.next(); - } - // Parent is a mapping - else if (this._current.parent?.value.templateTokenType === TokenType.Mapping) { - // Move to the next item value, item key, or mapping end - const parentMappingState = this._current.parent as MappingState; - this._current = parentMappingState.next(); - } - // Parent is an expression end - else if (this._current.parent !== undefined) { - this._current = this._current.parent; - } - // Parent is undefined - else { - this._current = undefined; - } - - this._expanded = false; - this.unravel(false); - } - - private unravel(expand: boolean): void { - if (this._expanded) { - return; - } - - for (;;) { - if (this._current === undefined) { - break; - } - // Literal - else if (this._current.value.isLiteral) { - break; - } - // Basic expression - else if (this._current.value.templateTokenType === TokenType.BasicExpression) { - const basicExpressionState = this._current as BasicExpressionState; - - // Sequence item is a basic expression start - // For example: - // steps: - // - script: credscan - // - ${{ parameters.preBuild }} - // - script: build - if (basicExpressionState.isStart && this._current.parent?.value.templateTokenType === TokenType.Sequence) { - if (expand) { - this.sequenceItemBasicExpression(); - } else { - break; - } - } - // Mapping key is a basic expression start - // For example: - // steps: - // - ${{ parameters.scriptHost }}: echo hi - else if ( - basicExpressionState.isStart && - this._current.parent?.value.templateTokenType === TokenType.Mapping && - (this._current.parent as MappingState).isKey - ) { - if (expand) { - this.mappingKeyBasicExpression(); - } else { - break; - } - } - // Mapping value is a basic expression start - // For example: - // steps: - // - script: credscan - // - script: ${{ parameters.tool }} - else if ( - basicExpressionState.isStart && - this._current.parent?.value.templateTokenType === TokenType.Mapping && - !(this._current.parent as MappingState).isKey - ) { - if (expand) { - this.mappingValueBasicExpression(); - } else { - break; - } - } - // Root basic expression start - else if (basicExpressionState.isStart && this._current.parent === undefined) { - if (expand) { - this.rootBasicExpression(); - } else { - break; - } - } - // Basic expression end - else if (basicExpressionState.isEnd) { - this.endExpression(); - } else { - this.unexpectedState("unravel basic expression"); - } - } - // Mapping - else if (this._current.value.templateTokenType === TokenType.Mapping) { - const mappingState = this._current as MappingState; - - // Mapping end, closing an "insert" mapping insertion - if (mappingState.isEnd && this._current.parent?.value.templateTokenType === TokenType.InsertExpression) { - this._current = this._current.parent; // Skip to the expression end - } - // Normal mapping start - else if (mappingState.isStart) { - break; - } - // Normal mapping end - else if (mappingState.isEnd) { - break; - } else { - this.unexpectedState("unravel mapping"); - } - } - // Sequence - else if (this._current.value.templateTokenType === TokenType.Sequence) { - const sequenceState = this._current as SequenceState; - - // Sequence end, closing a sequence insertion - if ( - sequenceState.isEnd && - this._current.parent?.value.templateTokenType === TokenType.BasicExpression && - this._current.parent.parent?.value.templateTokenType === TokenType.Sequence - ) { - this._current = this._current.parent; // Skip to the expression end - } - // Normal sequence start - else if (sequenceState.isStart) { - break; - } - // Normal sequence end - else if (sequenceState.isEnd) { - break; - } else { - this.unexpectedState("unravel sequence"); - } - } - // Insert expression - else if (this._current.value.templateTokenType === TokenType.InsertExpression) { - const insertExpressionState = this._current as InsertExpressionState; - - // Mapping key, beginning an "insert" mapping insertion - // For example: - // - job: a - // variables: - // ${{ insert }}: ${{ parameters.jobVariables }} - if ( - insertExpressionState.isStart && - this._current.parent?.value.templateTokenType === TokenType.Mapping && - (this._current.parent as MappingState).isKey - ) { - if (expand) { - this.startMappingInsertion(); - } else { - break; - } - } - // Expression end - else if (insertExpressionState.isEnd) { - this.endExpression(); - } - // Not allowed - else if (insertExpressionState.isStart) { - this._context.error( - insertExpressionState.value, - `The expression directive '${insertExpressionState.expression.directive}' is not supported in this context` - ); - this._current = insertExpressionState.toStringToken(); - } else { - this.unexpectedState("unravel insert expression"); - } - } else { - this.unexpectedState("unravel"); - } - } - - this._expanded = expand; - } - - private sequenceItemBasicExpression() { - // The template looks like: - // - // steps: - // - ${{ parameters.preSteps }} - // - script: build - // - // The current state looks like: - // - // MappingState // The document starts with a mapping - // - // SequenceState // The "steps" sequence - // - // BasicExpressionState // m_current - - const expressionState = this._current as BasicExpressionState; - const expression = expressionState.value as BasicExpressionToken; - let value: TemplateToken | undefined; - try { - value = evaluateTemplateToken(expression, expressionState.context); - } catch (err) { - this._context.error(expression, err); - } - - // Move to the nested sequence, skip the sequence start - if (value?.templateTokenType === TokenType.Sequence) { - this._current = expressionState.next(value, true); - } - // Move to the new value - else if (value !== undefined) { - this._current = expressionState.next(value, false); - } - // Move to the expression end - else if (value === undefined) { - expressionState.end(); - } - } - - private mappingKeyBasicExpression(): void { - // The template looks like: - // - // steps: - // - ${{ parameters.scriptHost }}: echo hi - // - // The current state looks like: - // - // MappingState // The document starts with a mapping - // - // SequenceState // The "steps" sequence - // - // MappingState // The step mapping - // - // BasicExpressionState // m_current - - // The expression should evaluate to a string - const expressionState = this._current as BasicExpressionState; - const expression = expressionState.value as BasicExpressionToken; - let stringToken: StringToken | undefined; - try { - const result = evaluateStringToken(expression, expressionState.context); - stringToken = result as StringToken; - } catch (err) { - this._context.error(expression, err); - } - - // Move to the stringToken - if (stringToken !== undefined) { - this._current = expressionState.next(stringToken, false); - } - // Move to the next key or mapping end - else { - const parentMappingState = this._current!.parent as MappingState; - this._current = parentMappingState.next(); // Next key or mapping end - } - } - - private mappingValueBasicExpression(): void { - // The template looks like: - // - // steps: - // - script: credScan - // - script: ${{ parameters.tool }} - // - // The current state looks like: - // - // MappingState // The document starts with a mapping - // - // SequenceState // The "steps" sequence - // - // MappingState // The step mapping - // - // BasicExpressionState // m_current - - const expressionState = this._current as BasicExpressionState; - const expression = expressionState.value as BasicExpressionToken; - let value: TemplateToken; - try { - value = evaluateTemplateToken(expression, expressionState.context); - } catch (err) { - this._context.error(expression, err); - value = new StringToken(expression.file, expression.range, "", expression.definitionInfo); - } - - // Move to the new value - this._current = expressionState.next(value, false); - } - - private rootBasicExpression() { - // The template looks like: - // - // ${{ parameters.tool }} - // - // The current state looks like: - // - // BasicExpressionState // m_current - - const expressionState = this._current as BasicExpressionState; - const expression = expressionState.value as BasicExpressionToken; - let value: TemplateToken; - try { - value = evaluateTemplateToken(expression, expressionState.context); - } catch (err) { - this._context.error(expression, err); - value = new StringToken(expression.file, expression.range, "", expression.definitionInfo); - } - - // Move to the new value - this._current = expressionState.next(value, false); - } - - private startMappingInsertion() { - // The template looks like: - // - // jobs: - // - job: a - // variables: - // ${{ insert }}: ${{ parameters.jobVariables }} - // - // The current state looks like: - // - // MappingState // The document starts with a mapping - // - // SequenceState // The "jobs" sequence - // - // MappingState // The "job" mapping - // - // MappingState // The "variables" mapping - // - // InsertExpressionState // m_current - - const expressionState = this._current as InsertExpressionState; - const parentMappingState = expressionState.parent as MappingState; - const nestedValue = parentMappingState.mapping.get(parentMappingState.index).value; - let nestedMapping: MappingToken | undefined; - if (nestedValue.templateTokenType === TokenType.Mapping) { - nestedMapping = nestedValue as MappingToken; - } else if (nestedValue.templateTokenType === TokenType.BasicExpression) { - const basicExpression = nestedValue as BasicExpressionToken; - - // The expression should evaluate to a mapping - try { - nestedMapping = evaluateMappingToken(basicExpression, expressionState.context); - } catch (err) { - this._context.error(basicExpression, err); - } - } else { - this._context.error(nestedValue, "Expected a mapping"); - } - - // Move to the nested first key - if ((nestedMapping?.count ?? 0) > 0) { - this._current = expressionState.next(nestedMapping!); - } - // Move to the expression end - else { - expressionState.end(); - } - } - - private endExpression(): void { - if (!this._current) { - throw new Error("_current should not be null"); - } - - // End of document - if (this._current.parent === undefined) { - this._current = undefined; - } - // End basic expression - else if (this._current.value.templateTokenType === TokenType.BasicExpression) { - // Move to the next item or sequence end - if (this._current.parent.value.templateTokenType === TokenType.Sequence) { - const parentSequenceState = this._current.parent as SequenceState; - this._current = parentSequenceState.next(); - } - // Move to the next key, next value, or mapping end - else { - const parentMappingState = this._current.parent as MappingState; - this._current = parentMappingState.next(); - } - } - // End "insert" mapping insertion - else { - // Move to the next key or mapping end - const parentMappingState = this._current.parent as MappingState; - this._current = parentMappingState.next(); - } - } - - private unexpectedState(operation: string): void { - throw new Error(`Unexpected state while unraveling expressions. ${this.dumpState(operation)}`); - } -} - -abstract class ReaderState { - public readonly parent: ReaderState | undefined; - public readonly value: TemplateToken; - public readonly context: TemplateContext; - - public constructor(parent: ReaderState | undefined, value: TemplateToken, context: TemplateContext) { - this.parent = parent; - this.value = value; - this.context = context; - } - - public abstract toString(): string; - - public static createState(parent: ReaderState | undefined, value: TemplateToken, context: TemplateContext) { - switch (value.templateTokenType) { - case TokenType.Null: - case TokenType.Boolean: - case TokenType.Number: - case TokenType.String: - return new LiteralState(parent, value as LiteralToken, context); - case TokenType.Sequence: - return new SequenceState(parent, value as SequenceToken, context); - case TokenType.Mapping: - return new MappingState(parent, value as MappingToken, context); - case TokenType.BasicExpression: - return new BasicExpressionState(parent, value as BasicExpressionToken, context); - case TokenType.InsertExpression: - return new InsertExpressionState(parent, value as InsertExpressionToken, context); - default: - throw new Error(`Unexpected type '${value.typeName()}' when constructing reader state`); - } - } -} - -class LiteralState extends ReaderState { - public constructor(parent: ReaderState | undefined, literal: LiteralToken, context: TemplateContext) { - super(parent, literal, context); - } - - public override toString(): string { - const result: string[] = []; - result.push("LiteralState"); - return `${result.join("\n")}\n`; - } -} - -class SequenceState extends ReaderState { - private _isStart = true; - private _index = 0; - - public constructor(parent: ReaderState | undefined, sequence: SequenceToken, context: TemplateContext) { - super(parent, sequence, context); - } - - /** - * Indicates whether the state represents the sequence-start event - */ - public get isStart(): boolean { - return this._isStart; - } - - /** - * The current index within the sequence - */ - public get index(): number { - return this._index; - } - - /** - * Indicates whether the state represents the sequence-end event - */ - public get isEnd(): boolean { - return !this.isStart && this.index >= this.sequence.count; - } - - public get sequence(): SequenceToken { - return this.value as SequenceToken; - } - - public next(): ReaderState { - // Adjust the state - if (this._isStart) { - this._isStart = false; - } else { - this._index++; - } - - // Return the next event - if (!this.isEnd) { - return ReaderState.createState(this, this.sequence.get(this._index), this.context); - } else { - return this; - } - } - - public override toString(): string { - const result: string[] = []; - result.push("SequenceState:"); - result.push(` isStart: ${this._isStart}`); - result.push(` index: ${this._index}`); - result.push(` isEnd: ${this.isEnd}`); - return `${result.join("\n")}\n`; - } -} - -class MappingState extends ReaderState { - private _isStart = true; - private _index = 0; - private _isKey = false; - - public constructor(parent: ReaderState | undefined, mapping: MappingToken, context: TemplateContext) { - super(parent, mapping, context); - } - - /** - * Indicates whether the state represents the mapping-start event - */ - public get isStart(): boolean { - return this._isStart; - } - - /** - * The current index within the mapping - */ - public get index(): number { - return this._index; - } - - /** - * Indicates whether the state represents a mapping-key position - */ - public get isKey(): boolean { - return this._isKey; - } - - /** - * Indicates whether the state represents the mapping-end event - */ - public get isEnd(): boolean { - return !this._isStart && this._index >= this.mapping.count; - } - - public get mapping(): MappingToken { - return this.value as MappingToken; - } - - public next(): ReaderState { - // Adjust the state - if (this._isStart) { - this._isStart = false; - this._isKey = true; - } else if (this._isKey) { - this._isKey = false; - } else { - this._index++; - this._isKey = true; - } - - // Return the next event - if (!this.isEnd) { - if (this._isKey) { - return ReaderState.createState(this, this.mapping.get(this._index).key, this.context); - } else { - return ReaderState.createState(this, this.mapping.get(this._index).value, this.context); - } - } else { - return this; - } - } - - public override toString(): string { - const result: string[] = []; - result.push("MappingState:"); - result.push(` isStart: ${this._isStart}`); - result.push(` index: ${this._index}`); - result.push(` isKey: ${this._isKey}`); - result.push(` isEnd: ${this.isEnd}`); - return `${result.join("\n")}\n`; - } -} - -class BasicExpressionState extends ReaderState { - private _isStart = true; - - public constructor(parent: ReaderState | undefined, expression: BasicExpressionToken, context: TemplateContext) { - super(parent, expression, context); - } - - /** - * Indicates whether entering the expression - */ - public get isStart(): boolean { - return this._isStart; - } - - /** - * Indicates whether leaving the expression - */ - public get isEnd(): boolean { - return !this._isStart; - } - - public next(value: TemplateToken, isSequenceInsertion: boolean): ReaderState { - // Adjust the state - this._isStart = false; - - // Create the nested state - const nestedState = ReaderState.createState(this, value, this.context); - if (isSequenceInsertion) { - const nestedSequenceState = nestedState as SequenceState; - return nestedSequenceState.next(); // Skip the sequence start - } else { - return nestedState; - } - } - - public end(): ReaderState { - this._isStart = false; - return this; - } - - public override toString(): string { - const result: string[] = []; - result.push("BasicExpressionState:"); - result.push(` isStart: ${this._isStart}`); - return `${result.join("\n")}\n`; - } -} - -class InsertExpressionState extends ReaderState { - private _isStart = true; - - public constructor(parent: ReaderState | undefined, expression: InsertExpressionToken, context: TemplateContext) { - super(parent, expression, context); - } - - /** - * Indicates whether entering or leaving the expression - */ - public get isStart(): boolean { - return this._isStart; - } - - /** - * Indicates whether leaving the expression - */ - public get isEnd(): boolean { - return !this._isStart; - } - - public get expression(): InsertExpressionToken { - return this.value as InsertExpressionToken; - } - - public next(value: MappingToken): ReaderState { - // Adjust the state - this._isStart = false; - - // Create the nested state - const nestedState = ReaderState.createState(this, value, this.context) as MappingState; - return nestedState.next(); // Skip the mapping start - } - - public end(): ReaderState { - this._isStart = false; - return this; - } - - /** - * This happens when the expression is not allowed - */ - public toStringToken(): ReaderState { - const literal = new StringToken( - this.value.file, - this.value.range, - `${OPEN_EXPRESSION} ${this.expression.directive} ${CLOSE_EXPRESSION}`, - this.value.definitionInfo - ); - return ReaderState.createState(this.parent, literal, this.context); - } - - public override toString(): string { - const result: string[] = []; - result.push("InsertExpressionState:"); - result.push(` isStart: ${this._isStart}`); - return `${result.join("\n")}\n`; - } -} diff --git a/actions-workflow-parser/src/workflows/workflow-constants.ts b/actions-workflow-parser/src/workflows/workflow-constants.ts index 2abbb26..ccb9387 100644 --- a/actions-workflow-parser/src/workflows/workflow-constants.ts +++ b/actions-workflow-parser/src/workflows/workflow-constants.ts @@ -1,2 +1 @@ export const WORKFLOW_ROOT = "workflow-root-strict"; -export const STRATEGY = "strategy"; diff --git a/actions-workflow-parser/src/workflows/workflow-evaluator.ts b/actions-workflow-parser/src/workflows/workflow-evaluator.ts deleted file mode 100644 index 16419d7..0000000 --- a/actions-workflow-parser/src/workflows/workflow-evaluator.ts +++ /dev/null @@ -1,38 +0,0 @@ -import {Dictionary} from "@github/actions-expressions/data/dictionary"; -import {TemplateContext, TemplateValidationErrors} from "../templates/template-context"; -import * as templateEvaluator from "../templates/template-evaluator"; -import {TemplateValidationError} from "../templates/template-validation-error"; -import {TemplateToken} from "../templates/tokens/template-token"; -import {TraceWriter} from "../templates/trace-writer"; -import {STRATEGY} from "./workflow-constants"; -import {getWorkflowSchema} from "./workflow-schema"; - -export interface EvaluateWorkflowResult { - value: TemplateToken | undefined; - errors: TemplateValidationError[]; -} - -export function evaluateStrategy( - fileTable: string[], - context: Dictionary, - token: TemplateToken, - trace: TraceWriter -): EvaluateWorkflowResult { - const templateContext = new TemplateContext(new TemplateValidationErrors(), getWorkflowSchema(), trace); - - // Add each file name - for (const fileName of fileTable) { - templateContext.getFileId(fileName); - } - - // Add expression named contexts - for (const pair of context.pairs()) { - templateContext.expressionNamedContexts.push(pair.key); - } - - const value = templateEvaluator.evaluateTemplate(templateContext, STRATEGY, token, 0, undefined); - return { - value: value, - errors: templateContext.errors.getErrors() - }; -} diff --git a/actions-workflow-parser/src/workflows/workflow-v1.0.json b/actions-workflow-parser/src/workflows/workflow-v1.0.json deleted file mode 120000 index e0e80b9..0000000 --- a/actions-workflow-parser/src/workflows/workflow-v1.0.json +++ /dev/null @@ -1 +0,0 @@ -../../../schema/workflow-v1.0.json \ No newline at end of file