Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(interp): Preventing multiple calls for dot-chained methods #22

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ lib/
types/
node_modules/
.vscode/
coverage/
coverage/
.DS_Store
36 changes: 23 additions & 13 deletions src/interpreter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ Object.freeze(defaultExecutionOptions);

export class Interpreter implements Expr.Visitor<BrsType>, Stmt.Visitor<BrsType> {
private _environment = new Environment();
private _lastDotGetAA: RoAssociativeArray = this._environment.getRootM();
private coverageCollector: CoverageCollector | null = null;
private _manifest: PP.Manifest | undefined;

Expand Down Expand Up @@ -1093,21 +1094,27 @@ export class Interpreter implements Expr.Visitor<BrsType>, Stmt.Visitor<BrsType>
expression.callee instanceof Expr.DottedGet ||
expression.callee instanceof Expr.IndexedGet
) {
let maybeM = this.evaluate(expression.callee.obj);
maybeM = isBoxable(maybeM) ? maybeM.box() : maybeM;

if (maybeM.kind === ValueKind.Object) {
if (maybeM instanceof RoAssociativeArray) {
mPointer = maybeM;
}
if (expression.callee.obj instanceof Expr.Call) {
mPointer = this._lastDotGetAA;
} else {
return this.addError(
new BrsError(
"Attempted to retrieve a function from a primitive value",
expression.closingParen.location
)
);
let maybeM = this.evaluate(expression.callee.obj);
maybeM = isBoxable(maybeM) ? maybeM.box() : maybeM;

if (maybeM.kind === ValueKind.Object) {
if (maybeM instanceof RoAssociativeArray) {
mPointer = maybeM;
}
} else {
return this.addError(
new BrsError(
"Attempted to retrieve a function from a primitive value",
expression.closingParen.location
)
);
}
}
} else {
this._lastDotGetAA = this.environment.getRootM();
}

return this.inSubEnv((subInterpreter) => {
Expand Down Expand Up @@ -1182,6 +1189,9 @@ export class Interpreter implements Expr.Visitor<BrsType>, Stmt.Visitor<BrsType>

if (isIterable(source)) {
try {
if (source instanceof RoAssociativeArray) {
this._lastDotGetAA = source;
}
return source.get(new BrsString(expression.name.text));
} catch (err: any) {
return this.addError(new BrsError(err.message, expression.name.location));
Expand Down
13 changes: 11 additions & 2 deletions test/e2e/Syntax.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,8 @@ describe("end to end syntax", () => {
"run",
"stop",
"then",
"promise-like resolved to 'foo'",
"Hello from line ",
"18",
"14",
]);
});

Expand All @@ -216,4 +215,14 @@ describe("end to end syntax", () => {
"14", // arr = [13]: arr[0]++
]);
});

test("dot-chaining.brs", async () => {
await execute([resourceFile("dot-chaining.brs")], outputStreams);

expect(allArgs(outputStreams.stdout.write).filter((arg) => arg !== "\n")).toEqual([
"removed number '3' from array, remaining 2",
"promise-like resolved to 'foo'",
"optional chaining works",
]);
});
});
36 changes: 36 additions & 0 deletions test/e2e/resources/dot-chaining.brs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
sub main()
a = [" 1 ", " 2 ", " 3 "]
b = a.pop().trim().toInt()
print "removed number '" + b.toStr() + "' from array, remaining " + a.count().toStr()
m.__result = "bad"
immediatePromise("foo").then(sub(result)
print "promise-like resolved to '" + result + "'"
end sub)
print "optional chaining " + testOptionalChaining()
end sub

' A simple promise-like function that immediately resolves to the provided value.
' You probably don't want to use it in production.
' @param {*} val the value this promise-like should immediately resolve to
' @returns {AssociativeArray} an associative array contianing a `then` property, used to chain
' promise-like constructs.
function immediatePromise(val as dynamic) as object
return {
__result: val
then: sub(resolved as function)
resolved(m.__result)
end sub
}
end function

function testOptionalChaining()
responseData = {
data:{
metricsData:{
addOnsStepStart : "works"
}
}
}
result = responseData?.data.metricsData?.addOnsStepStart
return result
end function
12 changes: 0 additions & 12 deletions test/e2e/resources/multi-file/test1.brs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ sub Main()
print("function in same file: " + sameFileFunc())
print("function in different file: " + differentFileFunc())
print("function with dependency: " + dependentFunc())
print("result" + testOptionalChaining())
end sub

function sameFileFunc()
Expand All @@ -13,14 +12,3 @@ function dependencyFunc()
return "from dependencyFunc()"
end function

function testOptionalChaining()
result = responseData?.data.metricsData?.addOnsStepStart
responseData = {
data:{
metricsData:{
addOnsStepStart : "print"
}
}
}
return result
end function
18 changes: 0 additions & 18 deletions test/e2e/resources/reserved-words.brs
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,5 @@ sub main()
print word
end for

immediatePromise("foo").then(sub(result)
print "promise-like resolved to '" + result + "'"
end sub)

print "Hello from line " LINE_NUM
end sub

' A simple promise-like function that immediately resolves to the provided value.
' You probably don't want to use it in production.
' @param {*} val the value this promise-like should immediately resolve to
' @returns {AssociativeArray} an associative array contianing a `then` property, used to chain
' promise-like constructs.
function immediatePromise(val as dynamic) as object
return {
__result: val
then: sub(resolved as function)
resolved(m.__result)
end sub
}
end function