diff --git a/index.html b/index.html
index 86bbb49..f678355 100644
--- a/index.html
+++ b/index.html
@@ -29,13 +29,7 @@
JSPython development console
-m = ''
-try:
- raise 'My Message'
- x.push(1)
-except:
- m = error.message
-m
+"1,2,3".split(',')[0]
diff --git a/package.json b/package.json
index f281ed8..b491c98 100644
--- a/package.json
+++ b/package.json
@@ -59,5 +59,5 @@
"hooks": {
"pre-commit": "npm run lint && npm run test"
}
-}
+ }
}
diff --git a/src/common/ast-types.ts b/src/common/ast-types.ts
index a26edac..f4b7185 100644
--- a/src/common/ast-types.ts
+++ b/src/common/ast-types.ts
@@ -8,8 +8,8 @@ export type AstNodeType =
| 'logicalOp'
| 'getSingleVar'
| 'setSingleVar'
- | 'dotObjectAccess'
- | 'bracketObjectAccess'
+ | 'chainingCalls'
+ | 'chainingObjectAccess'
| 'funcCall'
| 'funcDef'
| 'arrowFuncDef'
@@ -211,9 +211,9 @@ export class GetSingleVarNode extends AstNode implements IsNullCoelsing {
}
}
-export class DotObjectAccessNode extends AstNode {
- constructor(public nestedProps: AstNode[], public loc: Uint16Array) {
- super('dotObjectAccess');
+export class ChainingCallsNode extends AstNode {
+ constructor(public innerNodes: AstNode[], public loc: Uint16Array) {
+ super('chainingCalls');
this.loc = loc;
}
}
@@ -232,18 +232,18 @@ export class CreateArrayNode extends AstNode {
}
}
-export class BracketObjectAccessNode extends AstNode {
+export class ChainingObjectAccessNode extends AstNode {
constructor(
- public propertyName: string,
- public bracketBody: AstNode,
- public nullCoalescing: boolean | undefined = undefined,
+ public indexerBody: AstNode,
+ public nullCoelsing: boolean | undefined = undefined,
public loc: Uint16Array
) {
- super('bracketObjectAccess');
+ super('chainingObjectAccess');
this.loc = loc;
}
}
+
export interface LogicalNodeItem {
node: AstNode;
op: LogicalOperators | undefined;
diff --git a/src/common/token-types.ts b/src/common/token-types.ts
index c61fe49..5b632fa 100644
--- a/src/common/token-types.ts
+++ b/src/common/token-types.ts
@@ -65,26 +65,38 @@ export function getEndColumn(token: Token): number {
return token[1][4];
}
-export function splitTokens(tokens: Token[], separator: string): Token[][] {
+export function splitTokensByIndexes(tokens: Token[], sepIndexes: number[]): Token[][] {
const result: Token[][] = [];
if (!tokens.length) {
return [];
}
- const sepIndexes = findTokenValueIndexes(tokens, value => value === separator);
-
let start = 0;
for (let i = 0; i < sepIndexes.length; i++) {
const ind = sepIndexes[i];
+ if (getTokenValue(tokens[start - 1]) === '[') {
+ start = start - 1;
+ }
result.push(tokens.slice(start, ind));
start = ind + 1;
}
+ if (getTokenValue(tokens[start - 1]) === '[') {
+ start = start - 1;
+ }
result.push(tokens.slice(start, tokens.length));
return result;
}
+export function splitTokens(tokens: Token[], separator: string): Token[][] {
+ if (!tokens.length) {
+ return [];
+ }
+ const sepIndexes = findTokenValueIndexes(tokens, value => value === separator);
+ return splitTokensByIndexes(tokens, sepIndexes);
+}
+
export function findTokenValueIndex(
tokens: Token[],
predicate: (value: TokenValue) => boolean,
@@ -109,6 +121,34 @@ export function findTokenValueIndex(
return -1;
}
+export function findChainingCallTokensIndexes(tokens: Token[]): number[] {
+ const opIndexes: number[] = [];
+
+ for (let i = 0; i < tokens.length; i++) {
+ const tValue = getTokenValue(tokens[i]);
+ const tType = getTokenType(tokens[i]);
+
+ if (tType === TokenTypes.LiteralString) {
+ continue;
+ }
+
+ if (tValue === '.') {
+ opIndexes.push(i);
+ } else if (tValue === '(') {
+ i = skipInnerBrackets(tokens, i, '(', ')');
+ } else if (tValue === '[' && i === 0) {
+ i = skipInnerBrackets(tokens, i, '[', ']');
+ } else if (tValue === '[' && i !== 0) {
+ opIndexes.push(i);
+ i = skipInnerBrackets(tokens, i, '[', ']');
+ } else if (tValue === '{') {
+ i = skipInnerBrackets(tokens, i, '{', '}');
+ }
+ }
+
+ return opIndexes;
+}
+
export function findTokenValueIndexes(
tokens: Token[],
predicate: (value: TokenValue) => boolean
diff --git a/src/evaluator/evaluator.ts b/src/evaluator/evaluator.ts
index eb77f2f..214577c 100644
--- a/src/evaluator/evaluator.ts
+++ b/src/evaluator/evaluator.ts
@@ -4,11 +4,11 @@ import {
AstBlock,
AstNode,
BinOpNode,
- BracketObjectAccessNode,
+ ChainingCallsNode,
+ ChainingObjectAccessNode,
ConstNode,
CreateArrayNode,
CreateObjectNode,
- DotObjectAccessNode,
ForNode,
FuncDefNode,
FunctionCallNode,
@@ -369,104 +369,48 @@ export class Evaluator {
if (assignNode.target.type === 'getSingleVar') {
const node = assignNode.target as SetSingleVarNode;
- blockContext.blockScope.set(node.name, this.evalNode(assignNode.source, blockContext));
- } else if (assignNode.target.type === 'dotObjectAccess') {
- const targetNode = assignNode.target as DotObjectAccessNode;
+ blockContext.blockScope.set(
+ node.name,
+ this.evalNode(assignNode.source, blockContext)
+ );
+ } else if (assignNode.target.type === 'chainingCalls') {
+ const targetNode = assignNode.target as ChainingCallsNode;
// create a node for all but last property token
// potentially it can go to parser
- const targetObjectNode = new DotObjectAccessNode(
- targetNode.nestedProps.slice(0, targetNode.nestedProps.length - 1),
+ const targetObjectNode = new ChainingCallsNode(
+ targetNode.innerNodes.slice(0, targetNode.innerNodes.length - 1),
targetNode.loc
);
- const targetObject = this.evalNode(targetObjectNode, blockContext) as Record<
+ const targetObject = (this.evalNode(targetObjectNode, blockContext)) as Record<
string,
unknown
>;
- // not sure nested properties should be GetSingleVarNode
- // can be factored in the parser
- const lastPropertyName = (
- targetNode.nestedProps[targetNode.nestedProps.length - 1] as GetSingleVarNode
- ).name;
+ const lastInnerNode = targetNode.innerNodes[targetNode.innerNodes.length - 1];
+
+ let lastPropertyName = '';
+ if (lastInnerNode.type === 'getSingleVar') {
+ lastPropertyName = (lastInnerNode as GetSingleVarNode).name;
+ } else if (lastInnerNode.type === 'chainingObjectAccess') {
+ lastPropertyName = (this.evalNode(
+ (lastInnerNode as ChainingObjectAccessNode).indexerBody,
+ blockContext
+ )) as string;
+ } else {
+ throw Error('Not implemented Assign operation with chaining calls');
+ }
targetObject[lastPropertyName] = this.evalNode(assignNode.source, blockContext);
- } else if (assignNode.target.type === 'bracketObjectAccess') {
- const targetNode = assignNode.target as BracketObjectAccessNode;
- const keyValue = this.evalNode(targetNode.bracketBody, blockContext) as string | number;
- const targetObject = blockContext.blockScope.get(
- targetNode.propertyName as string
- ) as Record
;
-
- targetObject[keyValue] = this.evalNode(assignNode.source, blockContext);
- } else {
- throw Error('Not implemented Assign operation');
- // get chaining calls
}
return null;
}
- if (node.type === 'bracketObjectAccess') {
- const sbNode = node as BracketObjectAccessNode;
- const key = this.evalNode(sbNode.bracketBody, blockContext) as string;
- const obj = blockContext.blockScope.get(sbNode.propertyName as string) as Record<
- string,
- unknown
- >;
- return obj[key] === undefined ? null : obj[key];
+ if (node.type === 'chainingCalls') {
+ return this.resolveChainingCallsNode(node as ChainingCallsNode, blockContext);
}
- if (node.type === 'dotObjectAccess') {
- const dotObject = node as DotObjectAccessNode;
-
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- let startObject = this.evalNode(dotObject.nestedProps[0], blockContext) as any;
- for (let i = 1; i < dotObject.nestedProps.length; i++) {
- const nestedProp = dotObject.nestedProps[i];
-
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- if ((dotObject.nestedProps[i - 1] as any).nullCoelsing && !startObject) {
- startObject = {};
- }
-
- if (nestedProp.type === 'getSingleVar') {
- startObject = startObject[(nestedProp as SetSingleVarNode).name] as unknown;
- } else if (nestedProp.type === 'bracketObjectAccess') {
- const node = nestedProp as BracketObjectAccessNode;
- startObject = startObject[node.propertyName] as unknown;
- startObject = startObject[
- this.evalNode(node.bracketBody, blockContext) as string
- ] as unknown;
- } else if (nestedProp.type === 'funcCall') {
- const funcCallNode = nestedProp as FunctionCallNode;
- const func = startObject[funcCallNode.name] as (...args: unknown[]) => unknown;
-
- if (
- (func === undefined || func === null) &&
- (dotObject.nestedProps[i - 1] as unknown as IsNullCoelsing).nullCoelsing
- ) {
- startObject = null;
- continue;
- }
-
- if (typeof func !== 'function') {
- throw Error(`'${funcCallNode.name}' is not a function or not defined.`);
- }
- const pms = funcCallNode.paramNodes?.map(n => this.evalNode(n, blockContext)) || [];
- startObject = this.invokeFunction(func.bind(startObject), pms, {
- moduleName: blockContext.moduleName,
- line: funcCallNode.loc[0],
- column: funcCallNode.loc[1]
- });
- } else {
- throw Error("Can't resolve dotObjectAccess node");
- }
- }
-
- // no undefined values, make it rather null
- return startObject === undefined ? null : startObject;
- }
if (node.type === 'createObject') {
const createObjectNode = node as CreateObjectNode;
@@ -490,4 +434,60 @@ export class Evaluator {
return res;
}
}
+
+ private resolveChainingCallsNode(
+ chNode: ChainingCallsNode,
+ blockContext: BlockContext
+ ): unknown {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ let startObject = (this.evalNode(chNode.innerNodes[0], blockContext)) as any;
+
+ for (let i = 1; i < chNode.innerNodes.length; i++) {
+ const nestedProp = chNode.innerNodes[i];
+
+ if ((chNode.innerNodes[i - 1] as unknown as IsNullCoelsing).nullCoelsing && !startObject) {
+ startObject = {};
+ }
+
+ if (nestedProp.type === 'getSingleVar') {
+ startObject = startObject[(nestedProp as SetSingleVarNode).name] as unknown;
+ } else if (nestedProp.type === 'chainingObjectAccess') {
+ const node = nestedProp as ChainingObjectAccessNode;
+ // startObject = startObject[node.] as unknown;
+ startObject = startObject[
+ (this.evalNode(node.indexerBody, blockContext)) as string
+ ] as unknown;
+ } else if (nestedProp.type === 'funcCall') {
+ const funcCallNode = nestedProp as FunctionCallNode;
+ const func = startObject[funcCallNode.name] as (...args: unknown[]) => unknown;
+
+ if (
+ (func === undefined || func === null) &&
+ (chNode.innerNodes[i - 1] as unknown as IsNullCoelsing).nullCoelsing
+ ) {
+ startObject = null;
+ continue;
+ }
+
+ if (typeof func !== 'function') {
+ throw Error(`'${funcCallNode.name}' is not a function or not defined.`);
+ }
+ const pms = [];
+ for (const p of funcCallNode.paramNodes || []) {
+ pms.push(this.evalNode(p, blockContext));
+ }
+
+ startObject = this.invokeFunction(func.bind(startObject), pms, {
+ moduleName: blockContext.moduleName,
+ line: funcCallNode.loc[0],
+ column: funcCallNode.loc[0]
+ });
+ } else {
+ throw Error("Can't resolve chainingCalls node");
+ }
+ }
+
+ return startObject === undefined ? null : startObject;
+ }
+
}
diff --git a/src/evaluator/evaluatorAsync.ts b/src/evaluator/evaluatorAsync.ts
index 9e9fbf5..01fa6ab 100644
--- a/src/evaluator/evaluatorAsync.ts
+++ b/src/evaluator/evaluatorAsync.ts
@@ -4,11 +4,11 @@ import {
AstBlock,
AstNode,
BinOpNode,
- BracketObjectAccessNode,
+ ChainingCallsNode,
+ ChainingObjectAccessNode,
ConstNode,
CreateArrayNode,
CreateObjectNode,
- DotObjectAccessNode,
ForNode,
FuncDefNode,
FunctionCallNode,
@@ -480,13 +480,13 @@ export class EvaluatorAsync {
node.name,
await this.evalNodeAsync(assignNode.source, blockContext)
);
- } else if (assignNode.target.type === 'dotObjectAccess') {
- const targetNode = assignNode.target as DotObjectAccessNode;
+ } else if (assignNode.target.type === 'chainingCalls') {
+ const targetNode = assignNode.target as ChainingCallsNode;
// create a node for all but last property token
// potentially it can go to parser
- const targetObjectNode = new DotObjectAccessNode(
- targetNode.nestedProps.slice(0, targetNode.nestedProps.length - 1),
+ const targetObjectNode = new ChainingCallsNode(
+ targetNode.innerNodes.slice(0, targetNode.innerNodes.length - 1),
targetNode.loc
);
const targetObject = (await this.evalNodeAsync(targetObjectNode, blockContext)) as Record<
@@ -494,96 +494,28 @@ export class EvaluatorAsync {
unknown
>;
- // not sure nested properties should be GetSingleVarNode
- // can be factored in the parser
- const lastPropertyName = (
- targetNode.nestedProps[targetNode.nestedProps.length - 1] as GetSingleVarNode
- ).name;
+ const lastInnerNode = targetNode.innerNodes[targetNode.innerNodes.length - 1];
+
+ let lastPropertyName = '';
+ if (lastInnerNode.type === 'getSingleVar') {
+ lastPropertyName = (lastInnerNode as GetSingleVarNode).name;
+ } else if (lastInnerNode.type === 'chainingObjectAccess') {
+ lastPropertyName = (await this.evalNodeAsync(
+ (lastInnerNode as ChainingObjectAccessNode).indexerBody,
+ blockContext
+ )) as string;
+ } else {
+ throw Error('Not implemented Assign operation with chaining calls');
+ }
targetObject[lastPropertyName] = await this.evalNodeAsync(assignNode.source, blockContext);
- } else if (assignNode.target.type === 'bracketObjectAccess') {
- const targetNode = assignNode.target as BracketObjectAccessNode;
- const keyValue = (await this.evalNodeAsync(targetNode.bracketBody, blockContext)) as
- | string
- | number;
- const targetObject = blockContext.blockScope.get(
- targetNode.propertyName as string
- ) as Record;
-
- targetObject[keyValue] = await this.evalNodeAsync(assignNode.source, blockContext);
- } else {
- throw Error('Not implemented Assign operation');
- // get chaining calls
}
return null;
}
- if (node.type === 'bracketObjectAccess') {
- const sbNode = node as BracketObjectAccessNode;
- const key = (await this.evalNodeAsync(sbNode.bracketBody, blockContext)) as string;
- const obj = blockContext.blockScope.get(sbNode.propertyName as string) as Record<
- string,
- unknown
- >;
- return obj[key] === undefined ? null : obj[key];
- }
-
- if (node.type === 'dotObjectAccess') {
- const dotObject = node as DotObjectAccessNode;
-
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- let startObject = (await this.evalNodeAsync(dotObject.nestedProps[0], blockContext)) as any;
- for (let i = 1; i < dotObject.nestedProps.length; i++) {
- const nestedProp = dotObject.nestedProps[i];
-
- if (
- (dotObject.nestedProps[i - 1] as unknown as IsNullCoelsing).nullCoelsing &&
- !startObject
- ) {
- startObject = {};
- }
-
- if (nestedProp.type === 'getSingleVar') {
- startObject = startObject[(nestedProp as SetSingleVarNode).name] as unknown;
- } else if (nestedProp.type === 'bracketObjectAccess') {
- const node = nestedProp as BracketObjectAccessNode;
- startObject = startObject[node.propertyName] as unknown;
- startObject = startObject[
- (await this.evalNodeAsync(node.bracketBody, blockContext)) as string
- ] as unknown;
- } else if (nestedProp.type === 'funcCall') {
- const funcCallNode = nestedProp as FunctionCallNode;
- const func = startObject[funcCallNode.name] as (...args: unknown[]) => unknown;
-
- if (
- (func === undefined || func === null) &&
- (dotObject.nestedProps[i - 1] as unknown as IsNullCoelsing).nullCoelsing
- ) {
- startObject = null;
- continue;
- }
-
- if (typeof func !== 'function') {
- throw Error(`'${funcCallNode.name}' is not a function or not defined.`);
- }
- const pms = [];
- for (const p of funcCallNode.paramNodes || []) {
- pms.push(await this.evalNodeAsync(p, blockContext));
- }
-
- startObject = await this.invokeFunctionAsync(func.bind(startObject), pms, {
- moduleName: blockContext.moduleName,
- line: funcCallNode.loc[0],
- column: funcCallNode.loc[0]
- });
- } else {
- throw Error("Can't resolve dotObjectAccess node");
- }
- }
-
- // no undefined values, make it rather null
- return startObject === undefined ? null : startObject;
+ if (node.type === 'chainingCalls') {
+ return await this.resolveChainingCallsNode(node as ChainingCallsNode, blockContext);
}
if (node.type === 'createObject') {
@@ -611,4 +543,59 @@ export class EvaluatorAsync {
return res;
}
}
+
+ private async resolveChainingCallsNode(
+ chNode: ChainingCallsNode,
+ blockContext: BlockContext
+ ): Promise {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ let startObject = (await this.evalNodeAsync(chNode.innerNodes[0], blockContext)) as any;
+
+ for (let i = 1; i < chNode.innerNodes.length; i++) {
+ const nestedProp = chNode.innerNodes[i];
+
+ if ((chNode.innerNodes[i - 1] as unknown as IsNullCoelsing).nullCoelsing && !startObject) {
+ startObject = {};
+ }
+
+ if (nestedProp.type === 'getSingleVar') {
+ startObject = startObject[(nestedProp as SetSingleVarNode).name] as unknown;
+ } else if (nestedProp.type === 'chainingObjectAccess') {
+ const node = nestedProp as ChainingObjectAccessNode;
+ // startObject = startObject[node.] as unknown;
+ startObject = startObject[
+ (await this.evalNodeAsync(node.indexerBody, blockContext)) as string
+ ] as unknown;
+ } else if (nestedProp.type === 'funcCall') {
+ const funcCallNode = nestedProp as FunctionCallNode;
+ const func = startObject[funcCallNode.name] as (...args: unknown[]) => unknown;
+
+ if (
+ (func === undefined || func === null) &&
+ (chNode.innerNodes[i - 1] as unknown as IsNullCoelsing).nullCoelsing
+ ) {
+ startObject = null;
+ continue;
+ }
+
+ if (typeof func !== 'function') {
+ throw Error(`'${funcCallNode.name}' is not a function or not defined.`);
+ }
+ const pms = [];
+ for (const p of funcCallNode.paramNodes || []) {
+ pms.push(await this.evalNodeAsync(p, blockContext));
+ }
+
+ startObject = await this.invokeFunctionAsync(func.bind(startObject), pms, {
+ moduleName: blockContext.moduleName,
+ line: funcCallNode.loc[0],
+ column: funcCallNode.loc[0]
+ });
+ } else {
+ throw Error("Can't resolve chainingCalls node");
+ }
+ }
+
+ return startObject === undefined ? null : startObject;
+ }
}
diff --git a/src/interpreter.spec.ts b/src/interpreter.spec.ts
index 2a17670..3fac785 100644
--- a/src/interpreter.spec.ts
+++ b/src/interpreter.spec.ts
@@ -413,7 +413,7 @@ describe('Interpreter', () => {
};
check(e.eval(script) as number[]);
- check(await e.evaluate(script) as any);
+ check((await e.evaluate(script)) as any);
});
it('try catch no error', async () => {
@@ -436,7 +436,7 @@ describe('Interpreter', () => {
expect(result[2]).toBe(3);
};
- check(await e.evaluate(script) as any);
+ check((await e.evaluate(script)) as any);
check(e.eval(script) as number[]);
});
@@ -728,11 +728,11 @@ describe('Interpreter', () => {
`);
});
- const res = await interpreter.evaluate(`
+ const res = (await interpreter.evaluate(`
import './some.json' as obj
return obj
- `) as any;
+ `)) as any;
expect(res.x).toBe('test1');
expect(res.n).toBe(22);
@@ -742,13 +742,13 @@ describe('Interpreter', () => {
const interpreter = Interpreter.create();
interpreter.registerPackagesLoader(path =>
- path === 'service'
+ (path === 'service'
? {
add: (x: number, y: number): number => x + y,
remove: (x: number, y: number): number => x - y,
times: (x: number, y: number): number => x * y
}
- : null
+ : null) as any
);
interpreter.registerModuleLoader(() => {
@@ -784,7 +784,7 @@ describe('Interpreter', () => {
it('semicolon as a string', async () => {
const interpreter = Interpreter.create();
- const res = await interpreter.evaluate(`"first;second".split(';')`) as any;
+ const res = (await interpreter.evaluate(`"first;second".split(';')`)) as any;
expect(res.length).toBe(2);
});
@@ -815,4 +815,90 @@ describe('Interpreter', () => {
expect(await interpreter.evalAsync(script)).toBe(55);
expect(interpreter.eval(script)).toBe(55);
});
+
+ it('complex chaining call', async () => {
+ const interpreter = Interpreter.create();
+
+ const script = `
+ p = {f:{}}
+ p.f.x = 9
+ p.f.o = {v: 9}
+ p["test" + p.f.x] = "test9 value"
+ return p.test9
+ `;
+ expect(await interpreter.evalAsync(script)).toBe('test9 value');
+ expect(interpreter.eval(script)).toBe('test9 value');
+ });
+
+ it('chaining calls - split', async () => {
+ const interpreter = Interpreter.create();
+
+ const script = `
+ "12,13,14".split(',')[1]
+ `;
+ expect(await interpreter.evalAsync(script)).toBe('13');
+ expect(interpreter.eval(script)).toBe('13');
+ });
+
+ it('chaining calls - array indexer', async () => {
+ const interpreter = Interpreter.create();
+
+ const script = `
+[
+ ["ss1", "ss21", 5],
+ ["ss2", "test value", 6],
+ ["ss3", "2020-03-07", 7],
+ []
+][1][1]
+ `;
+ expect(await interpreter.evalAsync(script)).toBe('test value');
+ expect(interpreter.eval(script)).toBe('test value');
+ });
+
+ it('chaining calls - array indexer with ?', async () => {
+ const interpreter = Interpreter.create();
+
+ const script = `
+[
+ ["ss1", "ss21", 5],
+ ["ss2", "test value", 6],
+ ["ss3", "2020-03-07", 7],
+ []
+][1]?[1]
+ `;
+ expect(await interpreter.evalAsync(script)).toBe('test value');
+ expect(interpreter.eval(script)).toBe('test value');
+ });
+
+ it('chaining calls - object indexer with ?', async () => {
+ const interpreter = Interpreter.create();
+
+ const script = `
+ {x: 5, y: 10}.x
+ `;
+ expect(await interpreter.evalAsync(script)).toBe(5);
+ expect(interpreter.eval(script)).toBe(5);
+ });
+
+ it('chaining calls - object indexer with ?', async () => {
+ const interpreter = Interpreter.create();
+
+ const script = `
+ {x: 5, y: 10, xy: 15}["x"+"y"]
+ `;
+ expect(await interpreter.evalAsync(script)).toBe(15);
+ expect(interpreter.eval(script)).toBe(15);
+ });
+
+ it('chaining calls - object indexer with ?', async () => {
+ const interpreter = Interpreter.create();
+
+ const script = `
+ {value: "1st,2nd,3d"}["value"]?.split(',')?[1]
+ `;
+ expect(await interpreter.evalAsync(script)).toBe('2nd');
+ expect(interpreter.eval(script)).toBe('2nd');
+ });
+
+//
});
diff --git a/src/parser/parser.spec.ts b/src/parser/parser.spec.ts
index ff1d102..d8b55d5 100644
--- a/src/parser/parser.spec.ts
+++ b/src/parser/parser.spec.ts
@@ -1,4 +1,4 @@
-import { BinOpNode, ConstNode, ImportNode } from '../common';
+import { BinOpNode, ChainingCallsNode, ChainingObjectAccessNode, ConstNode, ImportNode } from '../common';
import { Tokenizer } from '../tokenizer';
import { Parser } from './parser';
@@ -58,4 +58,105 @@ describe('Parser => ', () => {
expect(importNode.parts[1].alias).toBe('f');
}
});
+
+ it('chaining calls 1 ', async () => {
+ const script = `"1,2,3".split(',')[0]`;
+ const ast = new Parser().parse(new Tokenizer().tokenize(script));
+ expect(ast.body.length).toBe(1);
+ expect(ast.body[0].type).toBe("chainingCalls");
+ const innerNodes = (ast.body[0] as ChainingCallsNode).innerNodes;
+ expect(innerNodes.length).toBe(3);
+ expect(innerNodes[2].type).toBe("chainingObjectAccess");
+
+ });
+
+ it('chaining calls 2 starts with JSON array', async () => {
+ const script = `["1,2,3"][0].split(',')[0]`;
+ const ast = new Parser().parse(new Tokenizer().tokenize(script));
+ expect(ast.body[0].type).toBe("chainingCalls");
+ expect((ast.body[0] as ChainingCallsNode).innerNodes.length).toBe(4);
+ expect((ast.body[0] as ChainingCallsNode).innerNodes[0].type).toBe("createArray");
+ expect((ast.body[0] as ChainingCallsNode).innerNodes[1].type).toBe("chainingObjectAccess");
+ expect((ast.body[0] as ChainingCallsNode).innerNodes[3].type).toBe("chainingObjectAccess");
+ });
+
+ it('chaining calls 3 start with JSON Object', async () => {
+ const script = `{value: "1,2,3"}["value"].split(',')[0]`;
+ const ast = new Parser().parse(new Tokenizer().tokenize(script));
+ expect(ast.body[0].type).toBe("chainingCalls");
+ expect((ast.body[0] as ChainingCallsNode).innerNodes.length).toBe(4);
+ expect((ast.body[0] as ChainingCallsNode).innerNodes[0].type).toBe("createObject");
+ expect((ast.body[0] as ChainingCallsNode).innerNodes[1].type).toBe("chainingObjectAccess");
+ expect((ast.body[0] as ChainingCallsNode).innerNodes[3].type).toBe("chainingObjectAccess");
+ });
+
+ it('chaining calls 1 with ? ', async () => {
+ const script = `"1,2,3".split(',')?[0]`;
+ const ast = new Parser().parse(new Tokenizer().tokenize(script));
+ expect(ast.body.length).toBe(1);
+ const innerNodes = (ast.body[0] as ChainingCallsNode).innerNodes;
+
+ expect(ast.body[0].type).toBe("chainingCalls");
+ expect(innerNodes.length).toBe(3);
+ expect(innerNodes[2].type).toBe("chainingObjectAccess");
+
+ expect(!!(innerNodes[0] as ChainingObjectAccessNode).nullCoelsing).toBe(false);
+ expect((innerNodes[1] as ChainingObjectAccessNode).nullCoelsing).toBe(true);
+ expect(!!(innerNodes[2] as ChainingObjectAccessNode).nullCoelsing).toBe(false);
+ });
+
+ it('chaining calls 2 with ? starts with JSON array', async () => {
+ const script = `["1,2,3"][0]?.split(',')?[0]`;
+ const ast = new Parser().parse(new Tokenizer().tokenize(script));
+ expect(ast.body[0].type).toBe("chainingCalls");
+ const innerNodes = (ast.body[0] as ChainingCallsNode).innerNodes;
+ expect(innerNodes.length).toBe(4);
+ expect(innerNodes[0].type).toBe("createArray");
+ expect(innerNodes[1].type).toBe("chainingObjectAccess");
+ expect(innerNodes[1].type).toBe("chainingObjectAccess");
+ expect(innerNodes[3].type).toBe("chainingObjectAccess");
+
+ expect(!!(innerNodes[0] as ChainingObjectAccessNode).nullCoelsing).toBe(false);
+ expect((innerNodes[1] as ChainingObjectAccessNode).nullCoelsing).toBe(true);
+ expect((innerNodes[2] as ChainingObjectAccessNode).nullCoelsing).toBe(true);
+ expect(!!(innerNodes[3] as ChainingObjectAccessNode).nullCoelsing).toBe(false);
+ });
+
+ it('chaining calls 3 with ? start with JSON Object', async () => {
+ const script = `{value: "1,2,3"}["value"]?.split(',')?[0]`;
+ const ast = new Parser().parse(new Tokenizer().tokenize(script));
+ expect(ast.body[0].type).toBe("chainingCalls");
+ const innerNodes = (ast.body[0] as ChainingCallsNode).innerNodes;
+ expect(innerNodes.length).toBe(4);
+ expect(innerNodes[0].type).toBe("createObject");
+ expect(innerNodes[1].type).toBe("chainingObjectAccess");
+ expect(innerNodes[3].type).toBe("chainingObjectAccess");
+
+ expect(!!(innerNodes[0] as ChainingObjectAccessNode).nullCoelsing).toBe(false);
+ expect((innerNodes[1] as ChainingObjectAccessNode).nullCoelsing).toBe(true);
+ expect((innerNodes[2] as ChainingObjectAccessNode).nullCoelsing).toBe(true);
+ expect(!!(innerNodes[3] as ChainingObjectAccessNode).nullCoelsing).toBe(false);
+ });
+
+ it('chaining calls 4 with ? 2d array access and ?', async () => {
+ const script = `["1,2,3"][0]?[0]?[0]?.split(',')?[0]`;
+ const ast = new Parser().parse(new Tokenizer().tokenize(script));
+ expect(ast.body[0].type).toBe("chainingCalls");
+ const innerNodes = (ast.body[0] as ChainingCallsNode).innerNodes;
+ expect(innerNodes.length).toBe(6);
+ expect(innerNodes[0].type).toBe("createArray");
+ expect(innerNodes[1].type).toBe("chainingObjectAccess");
+ expect(innerNodes[2].type).toBe("chainingObjectAccess");
+ expect(innerNodes[3].type).toBe("chainingObjectAccess");
+ expect(innerNodes[4].type).toBe("funcCall");
+ expect(innerNodes[5].type).toBe("chainingObjectAccess");
+
+ expect(!!(innerNodes[0] as ChainingObjectAccessNode).nullCoelsing).toBe(false);
+ expect((innerNodes[1] as ChainingObjectAccessNode).nullCoelsing).toBe(true);
+ expect((innerNodes[2] as ChainingObjectAccessNode).nullCoelsing).toBe(true);
+ expect(!!(innerNodes[3] as ChainingObjectAccessNode).nullCoelsing).toBe(true);
+ expect(!!(innerNodes[4] as ChainingObjectAccessNode).nullCoelsing).toBe(true);
+ expect(!!(innerNodes[5] as ChainingObjectAccessNode).nullCoelsing).toBe(false);
+ });
+
});
diff --git a/src/parser/parser.ts b/src/parser/parser.ts
index 6a61f2c..7222ccc 100644
--- a/src/parser/parser.ts
+++ b/src/parser/parser.ts
@@ -18,8 +18,6 @@ import {
getEndLine,
findOperators,
splitTokens,
- DotObjectAccessNode,
- BracketObjectAccessNode,
findTokenValueIndex,
FunctionDefNode,
CreateObjectNode,
@@ -44,7 +42,11 @@ import {
ComparisonOperators,
TryExceptNode,
ExceptBody,
- RaiseNode
+ RaiseNode,
+ findChainingCallTokensIndexes,
+ splitTokensByIndexes,
+ ChainingCallsNode,
+ ChainingObjectAccessNode
} from '../common';
import { JspyParserError } from '../common/utils';
@@ -604,13 +606,39 @@ export class Parser {
return prevNode;
}
- // create DotObjectAccessNode
- const subObjects = splitTokens(tokens, '.');
- if (subObjects.length > 1) {
- return new DotObjectAccessNode(
- subObjects.map(tkns => this.createExpressionNode(tkns)),
- getTokenLoc(tokens[0])
- );
+ // create chaining calls
+
+ const inds = findChainingCallTokensIndexes(tokens);
+
+ if (inds.length > 0) {
+ const chainingGroup = splitTokensByIndexes(tokens, inds);
+ const innerNodes: AstNode[] = [];
+
+ for (let i = 0; i < chainingGroup.length; i++) {
+ const chainLinkTokenks = chainingGroup[i];
+
+ if (i !== 0 && getTokenValue(chainLinkTokenks[0]) === '[') {
+ const nullCoelsing = getTokenValue(chainLinkTokenks[chainLinkTokenks.length - 1]) === '?';
+ if (nullCoelsing) {
+ chainLinkTokenks.pop();
+ }
+ const paramsTokensSlice = chainLinkTokenks.slice(1, chainLinkTokenks.length - 1);
+ const paramsNodes = this.createExpressionNode(paramsTokensSlice);
+
+ innerNodes.push(
+ new ChainingObjectAccessNode(
+ paramsNodes,
+ nullCoelsing,
+ getTokenLoc(chainLinkTokenks[0])
+ )
+ );
+ continue;
+ }
+
+ innerNodes.push(this.createExpressionNode(chainLinkTokenks));
+ }
+
+ return new ChainingCallsNode(innerNodes, getTokenLoc(tokens[0]));
}
// create function call node
@@ -682,14 +710,6 @@ export class Parser {
return new CreateArrayNode(items, getTokenLoc(tokens[0]));
}
- // bracket access object node
- if (tokens.length > 2 && getTokenValue(tokens[1]) === '[') {
- const name = getTokenValue(tokens[0]) as string;
- const paramsTokensSlice = tokens.slice(2, tokens.length - 1);
- const paramsNodes = this.createExpressionNode(paramsTokensSlice);
- return new BracketObjectAccessNode(name, paramsNodes, false, getTokenLoc(tokens[0]));
- }
-
throw Error(`Undefined node '${getTokenValue(tokens[0])}'.`);
}
}