Merge pull request #142 from github/joshmgross/rm-eval

Remove unused template evaluation code
This commit is contained in:
Josh Gross
2023-02-08 17:57:04 -05:00
committed by GitHub
6 changed files with 0 additions and 1477 deletions
@@ -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();
}
@@ -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);
}
}
@@ -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`;
}
}
@@ -1,2 +1 @@
export const WORKFLOW_ROOT = "workflow-root-strict";
export const STRATEGY = "strategy";
@@ -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 <EvaluateWorkflowResult>{
value: value,
errors: templateContext.errors.getErrors()
};
}
@@ -1 +0,0 @@
../../../schema/workflow-v1.0.json