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