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])}'.`); } }