Files
languageservices/expressions/src/evaluator.ts
T
eric sciple 656a821a94 ESM migration: Add .js extensions for node16 moduleResolution (#257)
Migrate expressions, workflow-parser, and languageservice packages to use
proper ESM imports with .js extensions that work with node16 moduleResolution.

Changes:
- Update tsconfig.build.json in each package to use module: node16 and
  moduleResolution: node16
- Add .js extensions to all relative import paths (Option B approach)
- Fix yaml internal type imports in workflow-parser by defining local types
- Add skipLibCheck to handle @types/node compatibility issues
- Add TypeScript 5.8.3 override in root package.json
- Add ESM migration plan documentation

The languageserver package is deferred due to test hang issues that need
further investigation.

Related #154 - Upgrade moduleResolution from node to node16 or nodenext
Related #110 - Published ESM code has imports without file extensions
Related #64 - expressions: ERR_MODULE_NOT_FOUND attempting to run example
Related #146 - Can not import @actions/workflow-parser

Test results:
- expressions: 1068 tests passed
- workflow-parser: 292 tests passed
- languageservice: 452 tests passed

* docs: update ESM migration plan with findings

- Update languageserver blocker: vscode-languageserver v8.0.2 lacks ESM
  exports (not a test hang issue)
- Document that Option B (manual .js extensions) was chosen over Option A
  due to ts-jest compatibility issues
- Add workaround for yaml package internal types (LinePos, NodeBase)
- Update migration status table with accurate reason for deferral
- Add skipLibCheck note for @types/node compatibility
2025-12-18 13:35:48 -06:00

240 lines
6.2 KiB
TypeScript

import {
Binary,
ContextAccess,
Expr,
ExprVisitor,
FunctionCall,
Grouping,
IndexAccess,
Literal,
Logical,
Star,
Unary
} from "./ast.js";
import * as data from "./data/index.js";
import {FilteredArray} from "./filtered_array.js";
import {wellKnownFunctions} from "./funcs.js";
import {FunctionDefinition} from "./funcs/info.js";
import {idxHelper} from "./idxHelper.js";
import {TokenType} from "./lexer.js";
import {equals, falsy, greaterThan, lessThan, truthy} from "./result.js";
export class Evaluator implements ExprVisitor<data.ExpressionData> {
/**
* Creates a new evaluator
* @param n Parsed expression to evaluate
* @param context Context data to use
* @param functions Optional map of function implementations. If given, these will be preferred over the built-in functions.
*/
constructor(private n: Expr, private context: data.Dictionary, private functions?: Map<string, FunctionDefinition>) {}
public evaluate(): data.ExpressionData {
return this.eval(this.n);
}
protected eval(n: Expr): data.ExpressionData {
return n.accept(this);
}
visitLiteral(literal: Literal): data.ExpressionData {
return literal.literal;
}
visitUnary(unary: Unary): data.ExpressionData {
const r = this.eval(unary.expr);
if (unary.operator.type === TokenType.BANG) {
return new data.BooleanData(falsy(r));
}
throw new Error(`unknown unary operator: ${unary.operator.lexeme}`);
}
visitBinary(binary: Binary): data.ExpressionData {
const left = this.eval(binary.left);
const right = this.eval(binary.right);
switch (binary.operator.type) {
case TokenType.EQUAL_EQUAL:
return new data.BooleanData(equals(left, right));
case TokenType.BANG_EQUAL:
return new data.BooleanData(!equals(left, right));
case TokenType.GREATER:
return new data.BooleanData(greaterThan(left, right));
case TokenType.GREATER_EQUAL:
return new data.BooleanData(equals(left, right) || greaterThan(left, right));
case TokenType.LESS:
return new data.BooleanData(lessThan(left, right));
case TokenType.LESS_EQUAL:
return new data.BooleanData(equals(left, right) || lessThan(left, right));
}
throw new Error(`unknown binary operator: ${binary.operator.lexeme}`);
}
visitLogical(logical: Logical): data.ExpressionData {
let result: data.ExpressionData;
for (const arg of logical.args) {
result = this.eval(arg);
// Break?
if (
(logical.operator.type === TokenType.AND && falsy(result)) ||
(logical.operator.type === TokenType.OR && truthy(result))
) {
break;
}
}
// result is always assigned before we return here
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return result!;
}
visitGrouping(grouping: Grouping): data.ExpressionData {
return this.eval(grouping.group);
}
visitContextAccess(contextAccess: ContextAccess): data.ExpressionData {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const r = this.context.get(contextAccess.name.lexeme)!;
return r;
}
visitIndexAccess(ia: IndexAccess): data.ExpressionData {
let idx: idxHelper;
if (ia.index instanceof Star) {
idx = new idxHelper(true, undefined);
} else {
let idxResult: data.ExpressionData;
try {
idxResult = this.eval(ia.index);
} catch (e) {
throw new Error(`could not evaluate index for index access: ${(e as Error).message}`, {cause: e});
}
idx = new idxHelper(false, idxResult);
}
const objResult = this.eval(ia.expr);
let result: data.ExpressionData;
switch (objResult.kind) {
case data.Kind.Array: {
const tobjResult = objResult;
if (tobjResult instanceof FilteredArray) {
result = filteredArrayAccess(tobjResult, idx);
} else {
result = arrayAccess(tobjResult, idx);
}
break;
}
case data.Kind.Dictionary: {
const tobjResult = objResult;
result = objectAccess(tobjResult, idx);
break;
}
default:
if (idx.star) {
result = new FilteredArray();
} else {
result = new data.Null();
}
}
return result;
}
visitFunctionCall(functionCall: FunctionCall): data.ExpressionData {
// Evaluate arguments
const args = functionCall.args.map(arg => this.eval(arg));
// Get function definitions
const functionName = functionCall.functionName.lexeme.toLowerCase();
const f = this.functions?.get(functionName) || wellKnownFunctions[functionName];
return f.call(...args);
}
}
function filteredArrayAccess(fa: FilteredArray, idx: idxHelper): data.ExpressionData {
const result = new FilteredArray();
for (const item of fa.values()) {
// Check the type of the nested item
switch (item.kind) {
case data.Kind.Dictionary: {
const ti = item;
if (idx.star) {
for (const v of ti.values()) {
result.add(v);
}
} else if (idx.str !== undefined) {
const v = ti.get(idx.str);
if (v !== undefined) {
result.add(v);
}
}
break;
}
case data.Kind.Array: {
const ti = item;
if (idx.star) {
for (const v of ti.values()) {
result.add(v);
}
} else if (idx.int !== undefined && idx.int < ti.values().length) {
result.add(ti.get(idx.int));
}
break;
}
}
}
return result;
}
function arrayAccess(a: data.Array, idx: idxHelper): data.ExpressionData {
if (idx.star) {
const fa = new FilteredArray();
for (const item of a.values()) {
fa.add(item);
}
return fa;
}
if (idx.int !== undefined && idx.int < a.values().length) {
return a.get(idx.int);
}
return new data.Null();
}
function objectAccess(obj: data.Dictionary, idx: idxHelper): data.ExpressionData {
if (idx.star) {
const fa = new FilteredArray(...obj.values());
return fa;
}
if (idx.str !== undefined) {
const r = obj.get(idx.str);
if (r !== undefined) {
return r;
}
}
return new data.Null();
}