Lint the expressions package

This commit is contained in:
Josh Gross
2023-03-16 18:08:51 -04:00
parent ac5b14b4c0
commit aca7ee3df1
9 changed files with 70 additions and 45 deletions
+1 -1
View File
@@ -97,7 +97,7 @@ export class IndexAccess extends Expr {
}
export class Star extends Expr {
accept<R>(v: ExprVisitor<R>): R {
accept<R>(): R {
throw new Error("Method not implemented.");
}
}
+3
View File
@@ -81,6 +81,9 @@ export function complete(
extensionFunctions
);
const expr = p.parse();
if (!expr) {
return [];
}
const ev = new Evaluator(expr, context, functions);
const result = ev.evaluate();
+6
View File
@@ -1,9 +1,14 @@
import {Expr} from "./ast";
import * as data from "./data";
import {ExpressionEvaluationError} from "./errors";
import {Evaluator} from "./evaluator";
import {Lexer} from "./lexer";
import {Parser} from "./parser";
function assertDefined(x: Expr | undefined): asserts x is Expr {
expect(x).toBeDefined();
}
describe("evaluator", () => {
const lexAndParse = (input: string) => {
const lexer = new Lexer(input);
@@ -12,6 +17,7 @@ describe("evaluator", () => {
// Parse
const parser = new Parser(result.tokens, ["foo"], []);
const expr = parser.parse();
assertDefined(expr);
return expr;
};
+8 -6
View File
@@ -93,6 +93,7 @@ export class Evaluator implements ExprVisitor<data.ExpressionData> {
}
// result is always assigned before we return here
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return result!;
}
@@ -101,6 +102,7 @@ export class Evaluator implements ExprVisitor<data.ExpressionData> {
}
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;
}
@@ -114,7 +116,7 @@ export class Evaluator implements ExprVisitor<data.ExpressionData> {
try {
idxResult = this.eval(ia.index);
} catch (e) {
throw new Error(`could not evaluate index for index access: ${e}`, {cause: e});
throw new Error(`could not evaluate index for index access: ${(e as Error).message}`, {cause: e});
}
idx = new idxHelper(false, idxResult);
}
@@ -124,9 +126,9 @@ export class Evaluator implements ExprVisitor<data.ExpressionData> {
let result: data.ExpressionData;
switch (objResult.kind) {
case data.Kind.Array: {
const tobjResult = objResult as data.Array;
const tobjResult = objResult;
if (tobjResult instanceof FilteredArray) {
result = filteredArrayAccess(tobjResult as FilteredArray, idx);
result = filteredArrayAccess(tobjResult, idx);
} else {
result = arrayAccess(tobjResult, idx);
}
@@ -135,7 +137,7 @@ export class Evaluator implements ExprVisitor<data.ExpressionData> {
}
case data.Kind.Dictionary: {
const tobjResult = objResult as data.Dictionary;
const tobjResult = objResult;
result = objectAccess(tobjResult, idx);
break;
}
@@ -170,7 +172,7 @@ function filteredArrayAccess(fa: FilteredArray, idx: idxHelper): data.Expression
// Check the type of the nested item
switch (item.kind) {
case data.Kind.Dictionary: {
const ti = item as data.Dictionary;
const ti = item;
if (idx.star) {
for (const v of ti.values()) {
result.add(v);
@@ -186,7 +188,7 @@ function filteredArrayAccess(fa: FilteredArray, idx: idxHelper): data.Expression
}
case data.Kind.Array: {
const ti = item as data.Array;
const ti = item;
if (idx.star) {
for (const v of ti.values()) {
result.add(v);
+6 -3
View File
@@ -5,12 +5,15 @@ export class idxHelper {
public readonly int: number | undefined;
constructor(public readonly star: boolean, idx: ExpressionData | undefined) {
if (!idx) {
return;
}
if (!star) {
if (idx!.primitive) {
this.str = idx!.coerceString();
if (idx.primitive) {
this.str = idx.coerceString();
}
let f = idx!.number();
let f = idx.number();
if (!isNaN(f) && isFinite(f) && f >= 0) {
f = Math.floor(f);
this.int = f;
+1
View File
@@ -57,6 +57,7 @@ export function tokenString(tok: Token): string {
case TokenType.NUMBER:
return tok.lexeme;
case TokenType.STRING:
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return tok.value!.toString();
default:
return tok.lexeme;
+10 -11
View File
@@ -9,8 +9,8 @@ export class Parser {
private extContexts: Map<string, boolean>;
private extFuncs: Map<string, FunctionInfo>;
private offset: number = 0;
private depth: number = 0;
private offset = 0;
private depth = 0;
private context: ParseContext;
@@ -43,15 +43,13 @@ export class Parser {
};
}
public parse(): Expr {
let result!: Expr;
public parse(): Expr | undefined {
// No tokens
if (this.atEnd()) {
return result;
return;
}
result = this.expression();
const result = this.expression();
if (!this.atEnd()) {
throw this.buildError(ErrorType.ErrorUnexpectedSymbol, this.peek());
@@ -173,7 +171,7 @@ export class Parser {
let cont = true;
while (cont) {
switch (true) {
case this.match(TokenType.LEFT_BRACKET):
case this.match(TokenType.LEFT_BRACKET): {
let indexExpr: Expr;
if (this.match(TokenType.STAR)) {
indexExpr = new Star();
@@ -188,6 +186,7 @@ export class Parser {
depthIncreased++;
expr = new IndexAccess(expr, indexExpr);
break;
}
case this.match(TokenType.DOT):
// Track depth
@@ -195,7 +194,7 @@ export class Parser {
depthIncreased++;
if (this.match(TokenType.IDENTIFIER)) {
let property = this.previous();
const property = this.previous();
expr = new IndexAccess(expr, new Literal(new data.StringData(property.lexeme), property));
} else if (this.match(TokenType.STAR)) {
expr = new IndexAccess(expr, new Star());
@@ -268,7 +267,7 @@ export class Parser {
case this.match(TokenType.STRING):
return new Literal(new data.StringData(this.previous().value as string), this.previous());
case this.match(TokenType.LEFT_PAREN):
case this.match(TokenType.LEFT_PAREN): {
const expr = this.expression();
if (this.atEnd()) {
@@ -277,7 +276,7 @@ export class Parser {
this.consume(TokenType.RIGHT_PAREN, ErrorType.ErrorUnexpectedSymbol);
return new Grouping(expr);
}
case this.atEnd():
throw this.buildError(ErrorType.ErrorUnexpectedEndOfExpression, this.previous()); // Back up to get the last token before the EOF
}
+23 -14
View File
@@ -8,10 +8,10 @@ export function falsy(d: data.ExpressionData): boolean {
case data.Kind.Boolean:
return !d.value;
case data.Kind.Number:
case data.Kind.Number: {
const dv = d.value;
return dv === 0 || isNaN(dv);
}
case data.Kind.String:
return d.value.length === 0;
}
@@ -77,7 +77,7 @@ export function coerceTypes(
// Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3.
// Except string comparison is OrdinalIgnoreCase, and objects are not coerced to primitives.
export function equals(lhs: data.ExpressionData, rhs: data.ExpressionData): boolean {
let [lv, rv] = coerceTypes(lhs, rhs);
const [lv, rv] = coerceTypes(lhs, rhs);
if (lv.kind != rv.kind) {
return false;
@@ -89,7 +89,7 @@ export function equals(lhs: data.ExpressionData, rhs: data.ExpressionData): bool
return true;
// Number, Number
case data.Kind.Number:
case data.Kind.Number: {
const ld = lv.value;
const rd = (rv as data.NumberData).value;
if (isNaN(ld) || isNaN(rd)) {
@@ -97,18 +97,21 @@ export function equals(lhs: data.ExpressionData, rhs: data.ExpressionData): bool
}
return ld == rd;
}
// String, String
case data.Kind.String:
case data.Kind.String: {
const ls = lv.value;
const rs = (rv as data.StringData).value;
return toUpperSpecial(ls) === toUpperSpecial(rs);
}
// Boolean, Boolean
case data.Kind.Boolean:
case data.Kind.Boolean: {
const lb = lv.value;
const rb = (rv as data.BooleanData).value;
return lb == rb;
}
// Object, Object
case data.Kind.Dictionary:
@@ -131,7 +134,7 @@ export function greaterThan(lhs: data.ExpressionData, rhs: data.ExpressionData):
switch (lv.kind) {
// Number, Number
case data.Kind.Number:
case data.Kind.Number: {
const lf = lv.value;
const rf = (rv as data.NumberData).value;
if (isNaN(lf) || isNaN(rf)) {
@@ -139,20 +142,23 @@ export function greaterThan(lhs: data.ExpressionData, rhs: data.ExpressionData):
}
return lf > rf;
}
// String, String
case data.Kind.String:
case data.Kind.String: {
let ls = lv.value;
let rs = (rv as data.StringData).value;
ls = toUpperSpecial(ls);
rs = toUpperSpecial(rs);
return ls > rs;
}
// Boolean, Boolean
case data.Kind.Boolean:
const lb = (lv as data.BooleanData).value;
case data.Kind.Boolean: {
const lb = lv.value;
const rb = (rv as data.BooleanData).value;
return lb && !rb;
}
}
return false;
@@ -169,7 +175,7 @@ export function lessThan(lhs: data.ExpressionData, rhs: data.ExpressionData): bo
switch (lv.kind) {
// Number, Number
case data.Kind.Number:
case data.Kind.Number: {
const lf = lv.value;
const rf = (rv as data.NumberData).value;
if (isNaN(lf) || isNaN(rf)) {
@@ -177,20 +183,23 @@ export function lessThan(lhs: data.ExpressionData, rhs: data.ExpressionData): bo
}
return lf < rf;
}
// String, String
case data.Kind.String:
case data.Kind.String: {
let ls = lv.value;
let rs = (rv as data.StringData).value;
ls = toUpperSpecial(ls);
rs = toUpperSpecial(rs);
return ls < rs;
}
// Boolean, Boolean
case data.Kind.Boolean:
case data.Kind.Boolean: {
const lb = lv.value;
const rb = (rv as data.BooleanData).value;
return !lb && rb;
}
}
return false;
@@ -200,7 +209,7 @@ export function lessThan(lhs: data.ExpressionData, rhs: data.ExpressionData): bo
export function toUpperSpecial(s: string): string {
const sb: string[] = [];
let i = 0;
let len = s.length;
const len = s.length;
let found = s.indexOf("ı");
while (i < len) {
if (i < found) {
+12 -10
View File
@@ -42,7 +42,7 @@ interface TestCase {
options: TestOptions;
}
function testCaseReviver(key: string, val: any): any {
function testCaseReviver(key: string, val: unknown): unknown {
if (key === "contexts") {
const tmp = JSON.stringify(val);
return JSON.parse(tmp, reviver);
@@ -105,27 +105,28 @@ describe("x-lang tests", () => {
if (testCase.err && testCase.err.kind === TestErrorKind.Lexing) {
throw new Error("expected error lexing expression, but got none");
}
} catch (e: any) {
} catch (e) {
const err = e as Error;
// Did test expect lexing error? If so, compare error message.
if (testCase.err && testCase.err.kind === TestErrorKind.Lexing) {
expect(e.message).toContain(testCase.err.value);
expect(err.message).toContain(testCase.err.value);
return;
}
throw new Error(`unexpected error lexing expression: ${e.message} ${e.stack}`);
throw new Error(`unexpected error lexing expression: ${err.message} ${err.stack || ""}`);
}
// Parse
const contextNames = testCase.contexts.pairs().map(x => x.key);
const parser = new Parser(result.tokens, contextNames, []);
let expr: Expr;
let expr: Expr | undefined;
try {
expr = parser.parse();
if (testCase.err?.kind === TestErrorKind.Parsing) {
throw new Error("expected error parsing expression, but got none");
}
} catch (e: any) {
} catch (e) {
// Did test expect parsing error?
if (testCase.err?.kind === TestErrorKind.Parsing) {
// Test expects parsing error
@@ -133,8 +134,8 @@ describe("x-lang tests", () => {
expect(errorWithExpression(pe, testCase.expr)).toContain(testCase.err.value);
return;
}
throw new Error(`unexpected error parsing expression: ${e.message} ${e.stack}`);
const err = e as Error;
throw new Error(`unexpected error parsing expression: ${err.message} ${err.stack || ""}`);
}
// Evaluate expression
@@ -154,14 +155,15 @@ describe("x-lang tests", () => {
expect(kindStr(result.kind)).toBe(testCase.result.kind);
expect(JSON.stringify(result, replacer)).toEqual(JSON.stringify(testCase.result.value, replacer));
} catch (e: any) {
} catch (e) {
if (testCase.err?.kind === TestErrorKind.Evaluation) {
const pe = e as ExpressionError;
expect(errorWithExpression(pe, testCase.expr)).toContain(testCase.err.value);
return;
}
throw new Error(`unexpected error evaluating expression: ${e.message} ${e.stack}`);
const err = e as Error;
throw new Error(`unexpected error evaluating expression: ${err.message} ${err.stack || ""}`);
}
});
});