UNPKG

@fromjs/backend

Version:
1,186 lines (1,185 loc) 96.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const core_1 = require("@fromjs/core"); const { instrumentAndRun, server } = core_1.testHelpers; const traverse_1 = require("./traverse"); const traverse = async function (firstStep, options = {}) { options = Object.assign({ optimistic: false }, options); const steps = (await traverse_1.traverse.call(null, firstStep, [], server, options)); return steps; }; function getStepTypeList(traversalResult) { return traversalResult.map((t) => t.operationLog.operation); } test("Can track concatenation of 'a' and 'b' - (simple)", async () => { const { normal, tracking, code } = await instrumentAndRun("return 'a' + 'b'"); expect(normal).toBe("ab"); var t1 = await traverse({ operationLog: tracking, charIndex: 0 }); var t2 = await traverse({ operationLog: tracking, charIndex: 1 }); expect(t1[0].operationLog.result.primitive).toBe("ab"); var t1LastStep = t1[t1.length - 1]; var t2LastStep = t2[t2.length - 1]; expect(t1LastStep.operationLog.operation).toBe("stringLiteral"); expect(t1LastStep.operationLog.result.primitive).toBe("a"); expect(t2LastStep.operationLog.operation).toBe("stringLiteral"); expect(t2LastStep.operationLog.result.primitive).toBe("b"); }); test("Can track concatenation of 'a' and 'b' in an add function", async () => { const { normal, tracking, code } = await instrumentAndRun(` function add(arg1, arg2) { return arg1 + arg2 } return add('a', 'b') `); expect(normal).toBe("ab"); var t1 = await traverse({ operationLog: tracking, charIndex: 0 }); var t2 = await traverse({ operationLog: tracking, charIndex: 1 }); var t1LastStep = t1[t1.length - 1]; var t2LastStep = t2[t2.length - 1]; expect(t1LastStep.operationLog.operation).toBe("stringLiteral"); expect(t1LastStep.operationLog.result.primitive).toBe("a"); expect(t2LastStep.operationLog.operation).toBe("stringLiteral"); expect(t2LastStep.operationLog.result.primitive).toBe("b"); expect(getStepTypeList(t1)).toEqual([ "callExpression", "returnStatement", "binaryExpression", "identifier", "stringLiteral", ]); }); test("Can traverse concatenation of a string and a number", async () => { const { normal, tracking, code } = await instrumentAndRun(` const num = 100 return num + "x" `); expect(normal).toBe("100x"); var t1 = await traverse({ operationLog: tracking, charIndex: 0 }); var t2 = await traverse({ operationLog: tracking, charIndex: 4 }); var t1LastStep = t1[t1.length - 1]; var t2LastStep = t2[t2.length - 1]; expect(t1LastStep.operationLog.operation).toBe("numericLiteral"); expect(t1LastStep.operationLog.result.primitive).toBe(100); expect(t2LastStep.operationLog.operation).toBe("stringLiteral"); expect(t2LastStep.operationLog.result.primitive).toBe("x"); }); test("Can traverse concatenation of a number and a string", async () => { const { normal, tracking, code } = await instrumentAndRun(` const num = 100 return "x" + num `); expect(normal).toBe("x100"); var t1 = await traverse({ operationLog: tracking, charIndex: 0 }); var t2 = await traverse({ operationLog: tracking, charIndex: 4 }); var t1LastStep = t1[t1.length - 1]; var t2LastStep = t2[t2.length - 1]; expect(t1LastStep.operationLog.operation).toBe("stringLiteral"); expect(t1LastStep.operationLog.result.primitive).toBe("x"); expect(t2LastStep.operationLog.operation).toBe("numericLiteral"); expect(t2LastStep.operationLog.result.primitive).toBe(100); }); describe("Assignment Expressions", () => { test("Can track values through object assignments", async () => { const { normal, tracking, code } = await instrumentAndRun(` var obj = {} obj.a = "x" return obj.a `); var t = await traverse({ operationLog: tracking, charIndex: 0 }); expect(getStepTypeList(t)).toEqual([ "memberExpression", "assignmentExpression", "stringLiteral", ]); }); test("Can track used return values of assignment expressions (to variables)", async () => { const { normal, tracking, code } = await instrumentAndRun(` var str = "a" return (str += "b") `); expect(normal).toBe("ab"); var t1 = await traverse({ operationLog: tracking, charIndex: 0 }); expect(t1[0].operationLog.result.primitive).toBe("ab"); const t1LastStep = t1[t1.length - 1]; expect(t1LastStep.operationLog.operation).toBe("stringLiteral"); expect(t1LastStep.operationLog.result.primitive).toBe("a"); var t2 = await traverse({ operationLog: tracking, charIndex: 1 }); const t2LastStep = t2[t2.length - 1]; expect(t2LastStep.operationLog.operation).toBe("stringLiteral"); expect(t2LastStep.operationLog.result.primitive).toBe("b"); }); test("Can traverse += operation for object assignments", async () => { const { normal, tracking, code } = await instrumentAndRun(` var obj = { a: "a" } obj.a += "b" return obj.a `); expect(normal).toBe("ab"); var t1 = await traverse({ operationLog: tracking, charIndex: 0 }); const t1LastStep = t1[t1.length - 1]; expect(t1LastStep.operationLog.operation).toBe("stringLiteral"); expect(t1LastStep.operationLog.result.primitive).toBe("a"); var t2 = await traverse({ operationLog: tracking, charIndex: 1 }); const t2LastStep = t2[t2.length - 1]; expect(t2LastStep.operationLog.operation).toBe("stringLiteral"); expect(t2LastStep.operationLog.result.primitive).toBe("b"); }); test("Can traverse return value of object assignments", async () => { const { normal, tracking, code } = await instrumentAndRun(` var obj = { a: "a" } return (obj.a += "b") `); expect(normal).toBe("ab"); var t1 = await traverse({ operationLog: tracking, charIndex: 0 }); const t1LastStep = t1[t1.length - 1]; expect(t1[0].operationLog.result.primitive).toBe("ab"); expect(t1LastStep.operationLog.operation).toBe("stringLiteral"); expect(t1LastStep.operationLog.result.primitive).toBe("a"); var t2 = await traverse({ operationLog: tracking, charIndex: 1 }); const t2LastStep = t2[t2.length - 1]; expect(t2LastStep.operationLog.operation).toBe("stringLiteral"); expect(t2LastStep.operationLog.result.primitive).toBe("b"); }); test("Can traverse results of assignment expression", async () => { const { normal, tracking, code } = await instrumentAndRun(` var v return v = "a" `); expect(normal).toBe("a"); var t1 = await traverse({ operationLog: tracking, charIndex: 0 }); const t1LastStep = t1[t1.length - 1]; expect(t1LastStep.operationLog.operation).toBe("stringLiteral"); expect(t1LastStep.operationLog.result.primitive).toBe("a"); expect(t1LastStep.charIndex).toBe(0); }); test("Can traverse results of assignment expression if assigning to an object", async () => { const { normal, tracking, code } = await instrumentAndRun(` var obj = {} return obj.a = "a" `); expect(normal).toBe("a"); var t1 = await traverse({ operationLog: tracking, charIndex: 0 }); const t1LastStep = t1[t1.length - 1]; expect(t1LastStep.operationLog.operation).toBe("stringLiteral"); expect(t1LastStep.operationLog.result.primitive).toBe("a"); expect(t1LastStep.charIndex).toBe(0); }); test("Can traverse if the previous value of a function argument was undefined", async () => { const { normal, tracking, code } = await instrumentAndRun(` function fn(a,b) { if (!b) { b = a } return a } return fn("a") `); expect(normal).toBe("a"); var t1 = await traverse({ operationLog: tracking, charIndex: 0 }); const t1LastStep = t1[t1.length - 1]; expect(t1LastStep.operationLog.operation).toBe("stringLiteral"); expect(t1LastStep.operationLog.result.primitive).toBe("a"); expect(t1LastStep.charIndex).toBe(0); }); test("Works in strict mode when assigning a value to something without a tracking identifier", async () => { const { normal, tracking, code } = await instrumentAndRun(` "use strict" function fn() {} function fn2() {} // neither fn___tv nor fv2___tv exist here fn2 = fn return "ok" `, {}, { logCode: false }); expect(normal).toBe("ok"); }); }); test("Can track values through object literals", async () => { const { normal, tracking, code } = await instrumentAndRun(` var obj = {a: "x"} return obj.a `); var t = await traverse({ operationLog: tracking, charIndex: 0 }); expect(getStepTypeList(t)).toEqual([ "memberExpression", "objectProperty", "stringLiteral", ]); }); test("Can traverse String.prototype.slice", async () => { const { normal, tracking, code } = await instrumentAndRun(` var str = "abcde" str = str.slice(2,4) return str `); expect(normal).toBe("cd"); var t = await traverse({ operationLog: tracking, charIndex: 0 }); var lastStep = t[t.length - 1]; expect(lastStep.charIndex).toBe(2); expect(getStepTypeList(t)).toEqual([ "identifier", "assignmentExpression", "callExpression", "identifier", "stringLiteral", ]); }); describe("Array.prototype.splice", () => { test("Can traverse Array.prototype.splice return value", async () => { const { normal, tracking, code } = await instrumentAndRun(` var arr = [1,2,3,4] return arr.splice(1,2).join("") `); expect(normal).toBe("23"); let step = await traverseAndGetLastStep(tracking, 0); expect(step.operationLog.operation).toBe("numericLiteral"); step = await traverseAndGetLastStep(tracking, 1); expect(step.operationLog.operation).toBe("numericLiteral"); }); }); describe("Object.entries", () => { test("Return value of Object.entries is tracked", async () => { const { normal, tracking, code } = await instrumentAndRun(` const entries = Object.entries({a:"A",b:"B"}) return entries[0][0] + entries[0][1] + entries[1][0] + entries[1][1] `); expect(normal).toBe("aAbB"); let step = await traverseAndGetLastStep(tracking, 0); expect(step.operationLog.operation).toBe("stringLiteral"); expect(step.charIndex).toBe(0); expect(step.operationLog.result.primitive).toBe("a"); step = await traverseAndGetLastStep(tracking, 3); expect(step.operationLog.operation).toBe("stringLiteral"); expect(step.charIndex).toBe(0); expect(step.operationLog.result.primitive).toBe("B"); }); }); test("Traverse str[n] character access", async () => { const { normal, tracking, code } = await instrumentAndRun(` var str = "abcde" return str[2] `); expect(normal).toBe("c"); var t = await traverse({ operationLog: tracking, charIndex: 0 }); var lastStep = t[t.length - 1]; expect(lastStep.charIndex).toBe(2); expect(lastStep.operationLog.operation).toBe("stringLiteral"); }); test("Traversing += assignment expressions", async () => { const { normal, tracking, code } = await instrumentAndRun(` var a = "a" a += "b" return a `); expect(normal).toBe("ab"); var t = await traverse({ operationLog: tracking, charIndex: 0 }); expect(t[0].operationLog.result.primitive).toBe("ab"); var lastStep = t[t.length - 1]; expect(lastStep.operationLog.result.primitive).toBe("a"); }); test("Can track values through conditional expression", async () => { const { normal, tracking, code } = await instrumentAndRun(` var b = false ? "a" : "b" return b `); expect(normal).toBe("b"); var t = await traverse({ operationLog: tracking, charIndex: 0 }); expect(getStepTypeList(t)).toEqual([ "identifier", "conditionalExpression", "stringLiteral", ]); }); describe("Can traverse string replacement calls", () => { test("works for simple replacements using regexes", async () => { const { normal, tracking, code } = await instrumentAndRun(` var ret = "ababab".replace(/b/g, "cc") // => "accaccacc" return ret `); expect(normal).toBe("accaccacc"); var t = await traverse({ operationLog: tracking, charIndex: 5 }); expect(getStepTypeList(t)).toEqual([ "identifier", "callExpression", "stringLiteral", ]); const lastStep = t[t.length - 1]; expect(lastStep.charIndex).toBe(1); expect(lastStep.operationLog.result.primitive).toBe("cc"); }); test("works for simple replacements using strings", async () => { const { normal, tracking, code } = await instrumentAndRun(` var ret = "Hello {{name}}!".replace("{{name}}", "Susan") return ret `); expect(normal).toBe("Hello Susan!"); var t1 = await traverse({ operationLog: tracking, charIndex: 2 }); const t1LastStep = t1[t1.length - 1]; expect(t1LastStep.charIndex).toBe(2); var t2 = await traverse({ operationLog: tracking, charIndex: 6 }); const t2LastStep = t2[t2.length - 1]; expect(t2LastStep.charIndex).toBe(0); }); test("works for non-match locations behind a match", async () => { const { normal, tracking, code } = await instrumentAndRun(` var ret = "abc".replace("a", "12345") return ret `); expect(normal).toBe("12345bc"); var t1 = await traverse({ operationLog: tracking, charIndex: 6 }); // "c" const t1LastStep = t1[t1.length - 1]; expect(t1LastStep.charIndex).toBe(2); }); test("Works for $n substitutions", async () => { const { normal, tracking, code } = await instrumentAndRun(` var ret = "abbbxy".replace(/(b+)/, "$1<>") return ret `); expect(normal).toBe("abbb<>xy"); var t1 = await traverse({ operationLog: tracking, charIndex: "abbb<>xy".indexOf("y"), }); const t1LastStep = t1[t1.length - 1]; expect(t1LastStep.charIndex).toBe("abbbxy".indexOf("y")); }); test("Can handle numbers after a dollar sign that don't identify the submatch", async () => { const { normal, tracking, code } = await instrumentAndRun(` var ret = "_abc#_".replace(/([a-z]+)#/g, '$1' + 123) return ret `); expect(normal).toBe("_abc123_"); var t1 = await traverse({ operationLog: tracking, charIndex: "abbb<>xy".indexOf("y"), }); const t1LastStep = t1[t1.length - 1]; expect(t1LastStep.charIndex).toBe("abbbxy".indexOf("y")); }); test("Doesn't break replace with numbers after dollar sign if there's a subgroup but no submatch", async () => { const { normal, tracking, code } = await instrumentAndRun(` var ret = "# insecure requests found".replace(/(^|[^\\\\])#/g, '$1' + 555) return ret `); expect(normal).toBe("555 insecure requests found"); var t1 = await traverse({ operationLog: tracking, charIndex: 1 }); const t1LastStep = t1[t1.length - 1]; expect(t1LastStep.operationLog.operation).toBe("numericLiteral"); }); }); describe("JSON.parse", () => { it("Can traverse JSON.parse", async () => { const { normal, tracking, code } = await instrumentAndRun(` var json = '{"a": {"b": "xyz"}}'; var obj = JSON.parse(json); return obj.a.b `); var t = await traverse({ operationLog: tracking, charIndex: 2 }); var lastStep = t[t.length - 1]; expect(lastStep.operationLog.operation).toBe("stringLiteral"); expect(lastStep.charIndex).toBe(15); }); it("Can traverse JSON.parse when using keys", async () => { const { normal, tracking, code } = await instrumentAndRun(` var json = '{"a":"abc", "b": "bcd"}'; var obj = JSON.parse(json); return Object.keys(obj)[1] `); var t = await traverse({ operationLog: tracking, charIndex: 0 }); var lastStep = t[t.length - 1]; expect(normal).toBe("b"); expect(lastStep.operationLog.operation).toBe("stringLiteral"); expect(lastStep.charIndex).toBe(13); }); it("Can traverse JSON.parse with JSON containing arrays", async () => { const { normal, tracking, code } = await instrumentAndRun(` var json = '{"a": {"b": ["one", "two"]}}'; var obj = JSON.parse(json); return obj.a.b[1] `); var t = await traverse({ operationLog: tracking, charIndex: 0 }); var lastStep = t[t.length - 1]; expect(normal).toBe("two"); expect(lastStep.operationLog.operation).toBe("stringLiteral"); expect(lastStep.charIndex).toBe(21); }); it("Returns the correct character index for longer strings", async () => { const text = `{"a": {"b": "Hello"}}`; const { normal, tracking, code } = await instrumentAndRun(` var json = '${text}'; var obj = JSON.parse(json); return obj.a.b `); var t = await traverse({ operationLog: tracking, charIndex: "Hello".indexOf("l"), }); var lastStep = t[t.length - 1]; expect(lastStep.operationLog.operation).toBe("stringLiteral"); expect(lastStep.charIndex).toBe(text.indexOf("l")); }); it("Can find property names in the JSON", async () => { const { normal, tracking, code } = await instrumentAndRun(` var obj = JSON.parse('{"a": {"b": 5}}'); let ret for (var name in obj) { ret = name } return ret `); expect(normal).toBe("a"); var t = await traverse({ operationLog: tracking, charIndex: 0 }); var lastStep = t[t.length - 1]; expect(lastStep.operationLog.operation).toBe("stringLiteral"); expect(lastStep.charIndex).toBe(2); expect(getStepTypeList(t)).toContain("jsonParseResult"); }); it("Can handle character mapping if the JSON contains an escaped line break", async () => { const { normal, tracking, code } = await instrumentAndRun(` var json = '{"a": "\\\\nHello"}'; var obj = JSON.parse(json); return obj.a + "|" + json `); const json = normal.split("|")[1]; var t = await traverse({ operationLog: tracking, charIndex: normal.indexOf("l"), }); var lastStep = t[t.length - 1]; expect(lastStep.operationLog.operation).toBe("stringLiteral"); expect(lastStep.charIndex).toBe(json.indexOf("l")); }); it("Can handle character mapping if the JSON contains an unicode escape sequence ", async () => { const { normal, tracking, code } = await instrumentAndRun(` var json = '{"a": "\\\\u003cHello"}'; var obj = JSON.parse(json); return obj.a + "|" + json `); const json = normal.split("|")[1]; var t = await traverse({ operationLog: tracking, charIndex: normal.indexOf("l"), }); var lastStep = t[t.length - 1]; expect(lastStep.operationLog.operation).toBe("stringLiteral"); expect(lastStep.charIndex).toBe(json.indexOf("l")); }); it("Can handle JSON that just contains a string", async () => { const { normal, tracking, code } = await instrumentAndRun(` var json = '"abc"'; var str = JSON.parse(json); return str `); var t = await traverse({ operationLog: tracking, charIndex: normal.indexOf("b"), }); var lastStep = t[t.length - 1]; expect(lastStep.operationLog.operation).toBe("stringLiteral"); expect(lastStep.charIndex).toBe('"abc"'.indexOf("b")); }); it("Can handle JSON that just contains a number", async () => { const { normal, tracking, code } = await instrumentAndRun(` var json = '5'; var str = JSON.parse(json); return str `); expect(normal).toBe(5); var t = await traverse({ operationLog: tracking, charIndex: 0, }); var lastStep = t[t.length - 1]; expect(lastStep.operationLog.operation).toBe("stringLiteral"); }); }); describe("JSON.stringify", () => { it("Can traverse a JSON.stringify result", async () => { const { normal, tracking, code } = await instrumentAndRun(` var obj = {greeting: "Hello ", name: {first: "w", last: "orld"}} var str = JSON.stringify(obj, null, 4); return str `); var lastStep = await traverseAndGetLastStep(tracking, normal.indexOf("Hello")); expect(lastStep.operationLog.operation).toBe("stringLiteral"); expect(lastStep.charIndex).toBe(0); expect(lastStep.operationLog.result.primitive).toBe("Hello "); var lastStep = await traverseAndGetLastStep(tracking, normal.indexOf("orld")); expect(lastStep.charIndex).toBe(0); expect(lastStep.operationLog.result.primitive).toBe("orld"); var lastStep = await traverseAndGetLastStep(tracking, normal.indexOf("first")); expect(lastStep.charIndex).toBe(0); expect(lastStep.operationLog.result.primitive).toBe("first"); }); it("Can traverse JSON.stringify result that's not prettified", async () => { const { normal, tracking, code } = await instrumentAndRun(` var obj = {greeting: "Hello ", name: {first: "w", last: "orld"}} var str = JSON.stringify(obj); return str `); var lastStep = await traverseAndGetLastStep(tracking, normal.indexOf("orld") + 2); expect(lastStep.operationLog.operation).toBe("stringLiteral"); expect(lastStep.charIndex).toBe(2); expect(lastStep.operationLog.result.primitive).toBe("orld"); }); it("Can traverse JSON.stringify correctly when looking at keys", async () => { const { normal, tracking, code } = await instrumentAndRun(` var obj = {greeting: "Hello "}; var str = JSON.stringify(obj); return str `); var lastStep = await traverseAndGetLastStep(tracking, normal.indexOf("greeting")); expect(lastStep.operationLog.operation).toBe("stringLiteral"); expect(lastStep.charIndex).toBe(0); expect(lastStep.operationLog.result.primitive).toBe("greeting"); }); it("Can traverse JSON.stringify result where keys are used multiple times", async () => { const { normal, tracking, code } = await instrumentAndRun(` var obj = { one: {hello: 123}, two: {} } obj.two["he" + "llo"] = 456 var str = JSON.stringify(obj); return str `); var lastStep = await traverseAndGetLastStep(tracking, normal.lastIndexOf("hello")); expect(lastStep.operationLog.operation).toBe("stringLiteral"); expect(lastStep.charIndex).toBe(0); expect(lastStep.operationLog.result.primitive).toBe("he"); }); it("Can handle arrays", async () => { const { normal, tracking, code } = await instrumentAndRun(` var arr = ["one", "two"] var str = JSON.stringify(arr); return str `); var lastStep = await traverseAndGetLastStep(tracking, normal.indexOf("two")); expect(lastStep.operationLog.operation).toBe("stringLiteral"); expect(lastStep.charIndex).toBe(0); expect(lastStep.operationLog.result.primitive).toBe("two"); }); it("Can handle nested arrays", async () => { const { normal, tracking, code } = await instrumentAndRun(` var arr = ["one", ["two", "three"]] var str = JSON.stringify(arr); return str `); var lastStep = await traverseAndGetLastStep(tracking, normal.indexOf("three")); expect(lastStep.operationLog.operation).toBe("stringLiteral"); expect(lastStep.charIndex).toBe(0); expect(lastStep.operationLog.result.primitive).toBe("three"); }); it("Doesn't break if property names contain dots", async () => { const { normal, tracking, code } = await instrumentAndRun(` var obj = {"a.b": "c"} var str = JSON.stringify(obj); return str `); var lastStep = await traverseAndGetLastStep(tracking, normal.indexOf("c")); expect(lastStep.operationLog.operation).toBe("stringLiteral"); expect(lastStep.charIndex).toBe(0); expect(lastStep.operationLog.result.primitive).toBe("c"); }); it("Doesn't break if there are undefiend properties", async () => { const { normal, tracking, code } = await instrumentAndRun(` var obj = {"a": undefined, "b": 5} var str = JSON.stringify(obj); return str `); var lastStep = await traverseAndGetLastStep(tracking, normal.indexOf("5")); expect(lastStep.operationLog.operation).toBe("numericLiteral"); }); it("Can handle JSON that just contains a string", async () => { const { normal, tracking, code } = await instrumentAndRun(` var json = "abc"; var str = JSON.stringify(json); return str `); expect(normal).toBe('"abc"'); var lastStep = await traverseAndGetLastStep(tracking, normal.indexOf("abc")); expect(lastStep.operationLog.operation).toBe("stringLiteral"); }); it("Can handle JSON that just contains a number", async () => { const { normal, tracking, code } = await instrumentAndRun(` var json = 5; var str = JSON.stringify(json); return str `); expect(normal).toBe("5"); var t = await traverse({ operationLog: tracking, charIndex: 0, }); var lastStep = t[t.length - 1]; expect(lastStep.operationLog.operation).toBe("numericLiteral"); }); it("Can handle values that are Symbols", async () => { const { normal, tracking, code } = await instrumentAndRun(` var json = {a: Symbol("sth")}; var str = JSON.stringify(json); return str `); expect(normal).toBe("{}"); }); }); it("Can traverse arguments for a function expression (rather than a function declaration)", async () => { const { normal, tracking, code } = await instrumentAndRun(` var fn = function(a) { return a } return fn("a") `); expect(normal).toBe("a"); var t = await traverse({ operationLog: tracking, charIndex: 0 }); const tLastStep = t[t.length - 1]; expect(getStepTypeList(t)).toEqual([ "callExpression", "returnStatement", "identifier", "stringLiteral", ]); }); describe("String.prototype.substr", () => { it("Works in when passing a positive start and length", async () => { const { normal, tracking, code } = await instrumentAndRun(` return "abcde".substr(2, 2) `); expect(normal).toBe("cd"); var t = await traverse({ operationLog: tracking, charIndex: 1 }); // char "d" const tLastStep = t[t.length - 1]; expect(tLastStep.charIndex).toBe(3); }); it("Works in when passing a start argument only", async () => { const { normal, tracking, code } = await instrumentAndRun(` return "abcde".substr(2) `); expect(normal).toBe("cde"); var t = await traverse({ operationLog: tracking, charIndex: 1 }); // char "c" const tLastStep = t[t.length - 1]; expect(tLastStep.charIndex).toBe(3); }); it("Works in when passing a negative start argument", async () => { const { normal, tracking, code } = await instrumentAndRun(` return "abcde".substr(-2, 1) `); expect(normal).toBe("d"); var t = await traverse({ operationLog: tracking, charIndex: 0 }); // char "d" const tLastStep = t[t.length - 1]; expect(tLastStep.charIndex).toBe(3); }); }); describe("String.prototype.trim", () => { it("Adjusts char index based on amount of whitespace removed", async () => { const { normal, tracking, code } = await instrumentAndRun(` const str = String.fromCharCode(10) + " ab cd" return str.trim() `); expect(normal).toBe("ab cd"); var t = await traverse({ operationLog: tracking, charIndex: 3 }); // char "c" const tLastStep = t[t.length - 1]; expect(tLastStep.charIndex).toBe(8); }); it("Doesn't break if there's no whitespace at the start", async () => { const { normal, tracking, code } = await instrumentAndRun(` const str = "abc" return str.trim() `); expect(normal).toBe("abc"); var t = await traverse({ operationLog: tracking, charIndex: 2 }); // char "c" const tLastStep = t[t.length - 1]; expect(tLastStep.charIndex).toBe(2); }); it("Doesn't break when called with apply", async () => { const { normal, tracking, code } = await instrumentAndRun(` var str = " a" return String.prototype.trim.apply(str, []) `); expect(normal).toBe("a"); var t = await traverse({ operationLog: tracking, charIndex: 0 }); const tLastStep = t[t.length - 1]; expect(tLastStep.charIndex).toBe(1); }); }); describe("call/apply", () => { it("Correctly traverses argument values when they are passed in with apply", async () => { const { normal, tracking, code } = await instrumentAndRun(` function fn(str1, str2, str3) { return str3 } return fn.apply(null, ["a", "b", "c"]) `); expect(normal).toBe("c"); var t = await traverseAndGetLastStep(tracking, 0); expect(t.operationLog.operation).toBe("stringLiteral"); }); it("Correctly traverses argument values when they are passed in with call", async () => { const { normal, tracking, code } = await instrumentAndRun(` function fn(str1, str2, str3) { return str3 } return fn.call(null, "a", "b", "c") `); expect(normal).toBe("c"); var t = await traverseAndGetLastStep(tracking, 0); expect(t.operationLog.operation).toBe("stringLiteral"); }); }); describe("It can traverse logical expressions", () => { it("Can traverse ||", async () => { const { normal, tracking, code } = await instrumentAndRun(` function fn(a, b) { b = b || a return b } return fn("a") + fn("x", "y") `, {}, { logCode: false }); expect(normal).toBe("ay"); var t = await traverseAndGetLastStep(tracking, 0); console.log(JSON.stringify(t, null, 2)); // I disabled the scopeHasIdentifierWithTrackingIdentifier check it broke when injecting // the code of a function into the browser – we assumed that var__tv is in scope // but it's not in the browser // Disabling this means that we check if b___tv is typeof undefined, and it is // but that's not because the value isn't in scope but because it's actually undefined // expect(t.operationLog.operation).toBe("stringLiteral"); expect(t.operationLog.result.primitive).toBe("a"); var t = await traverseAndGetLastStep(tracking, 1); // expect(t.operationLog.operation).toBe("stringLiteral"); expect(t.operationLog.result.primitive).toBe("y"); }); it("Doesn't break the execution logic of || expressions", async () => { const { normal, tracking, code } = await instrumentAndRun(` let str = "a"; function setStrToB() { str = "b" } ("a" || setStrToB()) return str ` // {}, // { logCode: true } ); expect(normal).toBe("a"); }); it("Doesn't break the execution logic of nested || expressions", async () => { const { normal, tracking, code } = await instrumentAndRun(` let str = "a"; function setStrToB() { str = "b" } function setStrToC() { str = "c" return true } (null || setStrToC() || setStrToB()) return str ` // {}, // { logCode: true } ); expect(normal).toBe("c"); }); it("Doesn't break the execution logic of && expressions", async () => { const { normal, tracking, code } = await instrumentAndRun(` let str = "a"; function setStrToB() { str = "b" } false && setStrToB() return str ` // {}, // { logCode: true } ); expect(normal).toBe("a"); }); }); it("Can traverse arguments fn.apply(this, arguments)", async () => { const { normal, tracking, code } = await instrumentAndRun(` function f1() { return f2.apply(this, arguments) } function f2(a) { return a + a } return f1("a") `); expect(normal).toBe("aa"); var t = await traverse({ operationLog: tracking, charIndex: 0 }); const tLastStep = t[t.length - 1]; expect(tLastStep.operationLog.operation).toBe("stringLiteral"); }); describe("Arrays", () => { it("Can traverse values in array literals", async () => { const { normal, tracking, code } = await instrumentAndRun(` var arr = ["a", "b"] return arr[1] `); expect(normal).toBe("b"); var t = await traverse({ operationLog: tracking, charIndex: 0 }); const tLastStep = t[t.length - 1]; expect(tLastStep.operationLog.operation).toBe("stringLiteral"); }); it("Can traverse pushed values", async () => { const { normal, tracking, code } = await instrumentAndRun(` var arr = [] arr.push("a", "b") return arr[1] `); expect(normal).toBe("b"); var t = await traverse({ operationLog: tracking, charIndex: 0 }); const tLastStep = t[t.length - 1]; expect(tLastStep.operationLog.operation).toBe("stringLiteral"); }); it("Can traverse shifted/unshifted values", async () => { const { normal, tracking, code } = await instrumentAndRun(` const arr = ["a", "c"] const v = arr.shift() arr.unshift("b") return v + arr[0] + arr[1] `); expect(normal).toBe("abc"); var t = await traverse({ operationLog: tracking, charIndex: 0 }); var tLastStep = t[t.length - 1]; expect(tLastStep.operationLog.operation).toBe("stringLiteral"); expect(tLastStep.operationLog.result.primitive).toBe("a"); var t = await traverse({ operationLog: tracking, charIndex: 1 }); var tLastStep = t[t.length - 1]; expect(tLastStep.operationLog.operation).toBe("stringLiteral"); expect(tLastStep.operationLog.result.primitive).toBe("b"); var t = await traverse({ operationLog: tracking, charIndex: 2 }); var tLastStep = t[t.length - 1]; console.log(JSON.stringify(tLastStep, null, 2)); expect(tLastStep.operationLog.operation).toBe("stringLiteral"); expect(tLastStep.operationLog.result.primitive).toBe("c"); }); describe("Array.prototype.join", () => { it("Can traverse basic join call", async () => { const { normal, tracking, code } = await instrumentAndRun(` var arr = ["ab", "cd"] return arr.join("-#-") `); expect(normal).toBe("ab-#-cd"); var t1 = await traverse({ operationLog: tracking, charIndex: 6 }); const t1LastStep = t1[t1.length - 1]; expect(t1LastStep.charIndex).toBe(1); expect(t1LastStep.operationLog.result.primitive).toBe("cd"); var t2 = await traverse({ operationLog: tracking, charIndex: 3 }); const t2LastStep = t2[t2.length - 1]; expect(t2LastStep.charIndex).toBe(1); expect(t2LastStep.operationLog.result.primitive).toBe("-#-"); }); }); it("Can traverse join calls with the default separator (,)", async () => { const { normal, tracking, code } = await instrumentAndRun(` var arr = ["a", "b"] return arr.join() `); expect(normal).toBe("a,b"); var t1 = await traverse({ operationLog: tracking, charIndex: 1 }); const t1LastStep = t1[t1.length - 1]; expect(t1LastStep.charIndex).toBe(0); expect(t1LastStep.operationLog.result.primitive).toBe(","); }); it("Can traverse join calls with undefined/null values", async () => { const { normal, tracking, code } = await instrumentAndRun(` return [null, undefined].join("-") `); expect(normal).toBe("-"); var t1 = await traverse({ operationLog: tracking, charIndex: 0 }); const t1LastStep = t1[t1.length - 1]; expect(t1LastStep.charIndex).toBe(0); expect(t1LastStep.operationLog.result.primitive).toBe("-"); }); it("Can traverse join called with .call", async () => { const { normal, tracking, code } = await instrumentAndRun(` return Array.prototype.join.call(["a","b"], "-") `); expect(normal).toBe("a-b"); var t1 = await traverse({ operationLog: tracking, charIndex: 2 }); const t1LastStep = t1[t1.length - 1]; expect(t1LastStep.charIndex).toBe(0); expect(t1LastStep.operationLog.result.primitive).toBe("b"); }); it("Works on array like objects", async () => { const { normal, tracking, code } = await instrumentAndRun(` return Array.prototype.join.call({0: "a", 1: "b", length: 2}) `); expect(normal).toBe("a,b"); var t1 = await traverse({ operationLog: tracking, charIndex: 2 }); const t1LastStep = t1[t1.length - 1]; expect(t1LastStep.charIndex).toBe(0); expect(t1LastStep.operationLog.result.primitive).toBe("b"); }); }); it("Tracks Object.keys", async () => { const { normal, tracking, code } = await instrumentAndRun(` const obj = {} obj["a"] = 100 return Object.keys(obj)[0] `); expect(normal).toBe("a"); var t1 = await traverse({ operationLog: tracking, charIndex: 0 }); const t1LastStep = t1[t1.length - 1]; expect(t1LastStep.operationLog.operation).toBe("stringLiteral"); }); it("Can traverse Object.assign", async () => { const { normal, tracking, code } = await instrumentAndRun(` let obj = {a: "a"} let obj2 = {b: "b"} obj = Object.assign(obj, obj2) return obj.b `); expect(normal).toBe("b"); var t1 = await traverse({ operationLog: tracking, charIndex: 0 }); const t1LastStep = t1[t1.length - 1]; expect(t1LastStep.operationLog.operation).toBe("stringLiteral"); }); describe("Array.slice", () => { it("Supports traversal of normal usage", async () => { const { normal, tracking, code } = await instrumentAndRun(` var arr = ["1", "2", "3"] return arr.slice(1,3)[1] `); expect(normal).toBe("3"); var t1 = await traverse({ operationLog: tracking, charIndex: 0 }); const t1LastStep = t1[t1.length - 1]; expect(t1LastStep.operationLog.operation).toBe("stringLiteral"); }); it("Supports traversal with negative start index", async () => { const { normal, tracking, code } = await instrumentAndRun(` var arr = ["1", "2", "3", "4"] return arr.slice(-3, 3)[1] `); expect(normal).toBe("3"); var t1 = await traverse({ operationLog: tracking, charIndex: 0 }); const t1LastStep = t1[t1.length - 1]; expect(t1LastStep.operationLog.operation).toBe("stringLiteral"); }); it("Supports traversal with negative start index and no end index", async () => { const { normal, tracking, code } = await instrumentAndRun(` var arr = ["1", "2", "3", "4"] return arr.slice(-2)[1] `); expect(normal).toBe("4"); var t1 = await traverse({ operationLog: tracking, charIndex: 0 }); const t1LastStep = t1[t1.length - 1]; expect(t1LastStep.operationLog.operation).toBe("stringLiteral"); }); it("Supports traversal with positive start index and no end index", async () => { const { normal, tracking, code } = await instrumentAndRun(` var arr = ["1", "2", "3", "4"] return arr.slice(1)[2] `); expect(normal).toBe("4"); var t1 = await traverse({ operationLog: tracking, charIndex: 0 }); const t1LastStep = t1[t1.length - 1]; expect(t1LastStep.operationLog.operation).toBe("stringLiteral"); }); it("Supports traversal with positive start index and negative end index", async () => { const { normal, tracking, code } = await instrumentAndRun(` var arr = ["1", "2", "3", "4"] return arr.slice(1, -1)[1] `); expect(normal).toBe("3"); var t1 = await traverse({ operationLog: tracking, charIndex: 0 }); const t1LastStep = t1[t1.length - 1]; expect(t1LastStep.operationLog.operation).toBe("stringLiteral"); }); it("Supports being called with .call and arguments object", async () => { const { normal, tracking, code } = await instrumentAndRun(` const slice = Array.prototype.slice function fn() { const arr = slice.call(arguments) return arr[0] } return fn("Hi") `); expect(normal).toBe("Hi"); var t1 = await traverse({ operationLog: tracking, charIndex: 0 }); const t1LastStep = t1[t1.length - 1]; expect(t1LastStep.operationLog.operation).toBe("stringLiteral"); }); }); describe("Array.prototype.map", () => { it("Passes values through to mapping function", async () => { const { normal, tracking, code } = await instrumentAndRun(` var arr = ["1", "2"] return arr.map(function(value){ return value })[1] `); expect(normal).toBe("2"); var t1 = await traverse({ operationLog: tracking, charIndex: 0 }); const t1LastStep = t1[t1.length - 1]; expect(t1LastStep.operationLog.operation).toBe("stringLiteral"); }); it("Supports all callback arguments and thisArg", async () => { const { normal, tracking, code } = await instrumentAndRun(` var arr = ["1", "2"] return arr.map(function(value, index, array){ return value + index + array.length + this }, "x")[0] `); expect(normal).toBe("102x"); }); it("When invoked with .apply, still supports all callback arguments and thisArg", async () => { const { normal, tracking, code } = await instrumentAndRun(` var arr = ["1", "2"] const map = arr.map var arr2 = map.apply(arr, [function(value, index, array){ return value + index + array.length + this }, "x"]) return arr2[0] `); expect(normal).toBe("102x"); }); it("When invoked with .call, still supports all callback arguments and thisArg", async () => { const { normal, tracking, code } = await instrumentAndRun(` var arr = ["1", "2"] const map = arr.map var arr2 = map.call(arr, function(value, index, array){ return value + index + array.length + this }, "x") return arr2[0] `); expect(normal).toBe("102x"); }); }); describe("Array.prototype.filter", () => { it("Passes values through to filter function and tracks result values", async () => { const { normal, tracking, code } = await instrumentAndRun(` var arr = ["1", "2", "3", "4"] let v const greaterThan2 = arr.filter(function(value){ v = value return parseFloat(value) > 2 }) return v + "-" + greaterThan2[0] `); expect(normal).toBe("4-3"); var t1 = await traverse({ operationLog: tracking, charIndex: 0 }); const t1LastStep = t1[t1.length - 1]; expect(t1LastStep.operationLog.operation).toBe("stringLiteral"); var t2 = await traverse({ operationLog: tracking, charIndex: 2 }); const t2LastStep = t2[t2.length - 1]; expect(t2LastStep.operationLog.operation).toBe("stringLiteral"); }); it("Doesn't break this argument", async () => { const { normal, tracking, code } = await instrumentAndRun(` let arr = ["1","2","3","4"] let t arr = arr.filter(function(value){ t = this.toString() return value > parseFloat(this) }, "2") return arr[0] + t `); expect(normal).toBe("32"); var t1 = await traverse({ operationLog: tracking, charIndex: 0 }); const t1LastStep = t1[t1.length - 1]; expect(t1LastStep.operationLog.operation).toBe("stringLiteral"); }); }); describe("Array.concat", () => { it("Works when calling .concat with an array argument", async () => { const { normal, tracking, code } = await instrumentAndRun(` var arr = ["1"].concat(["2"]) return arr[0] + arr[1] `); expect(normal).toBe("12"); var t1 = await traverse({ operationLog: tracking, charIndex: 0 }); const t1LastStep = t1[t1.length - 1]; expect(t1LastStep.operationLog.operation).toBe("stringLiteral"); expect(t1LastStep.operationLog.result.primitive).toBe("1"); var t2 = await traverse({ operationLog: tracking, charIndex: 1 }); const t2LastStep = t2[t2.length - 1]; expect(t2LastStep.operationLog.operation).toBe("stringLiteral"); expect(t2LastStep.operationLog.result.primitive).toBe("2"); }); it("Works when calling .concat with an non-array argument", async () => { const { normal, tracking, code } = await instrumentAndRun(` var arr = ["1"].concat("2") return arr[0] + arr[1] `); expect(normal).toBe("12"); var t2 = await traverse({ operationLog: tracking, charIndex: 1 }); const t2LastStep = t2[t2.length - 1]; expect(t2LastStep.operationLog.operation).toBe("stringLiteral"); expect(t2LastStep.operationLog.result.primitive).toBe("2"); }); }); describe("encodeURICompoennt", () => { it("Can traverse encodeURIComponent", async () => { const { normal, tracking, code } = await instrumentAndRun(` return encodeURIComponent("a@b#c") `); expect(normal).toBe("a%40b%23c"); var t1 = await traverse({ operationLog: tracking, charIndex: 7 }); const t1LastStep = t1[t1.length - 1]; expect(t1LastStep.operationLog.operation).toBe("stringLiteral"); expect(t1LastStep.charIndex).toBe(3); var t2 = await traverse({ operationLog: tracking, charIndex: 9 }); const t2LastStep = t2[t2.length - 1]; expect(t2LastStep.charIndex).toBe(5); }); it("Can traverse encodeURIComponent when called with .call", async () => { const { normal, tracking, code } = await instrumentAndRun(` return encodeURIComponent.call(null, "a@b#c") `); expect(normal).toBe("a%40b%23c"); var t1 = await traverse({ operationLog: tracking, charIndex: 7 }); const t1LastStep = t1[t1.length - 1]; expect(t1LastStep.operationLog.operation).toBe("stringLiteral"); expect(t1LastStep.charIndex).toBe(3); var t2 = await traverse({ operationLog: tracking, charIndex: 9 }); const t2LastStep = t2[t2.length - 1]; expect(t2LastStep.charIndex).toBe(5); }); }); it("Can traverse decodeURIComponent", async () => { const { normal, tracking, code } = await instrumentAndRun(` return decodeURIComponent("a%40b%23c") `); expect(normal).toBe("a@b#c"); var t1 = await traverse({ operationLog: tracking, charIndex: 3 }); const t1LastStep = t1[t1.length - 1]; expect(t1LastStep.operationLog.operation).toBe("stringLiteral"); expect(t1LastStep.charIndex).toBe(5); var t2 = await traverse({ operationLog: tracking, charIndex: 4 }); const t2LastStep = t2[t2.length - 1]; expect(t2LastStep.charIndex).toBe(8); }); describe("String.prototype.match", () => { test("Can traverse String.prototype.match results", async () => { const { normal, tracking, code } = await instrumentAndRun(` const arr = "a4c89a".match(/[0-9]+/g) return arr[0] + arr[1] `); expect(normal).toBe("489"); var t1 = await traverse({ operationLog: tracking, charIndex: 0 }); const t1LastStep = t1[t1.length - 1]; expect(t1LastStep.operationLog.operation).toBe("stringLiteral"); expect(t1LastStep.charIndex).toBe(1); var t2 = await traverse({ operationLog: tracking, charIndex: 2 }); const t2LastStep = t2[t2.length - 1]; expect(t2LastStep.charIndex).toBe(4); }); // not technically a traversal test but it makes sense to have .match tests in one place // ... maybe we should just merge core.test.ts with traverse.test.ts // ... or move them next to the relevant files, like callexpression.ts it("Doesn't break on non-global regexes with multiple match groups", async () => { const { normal, tracking, code } = await instrumentAndRun(` const arr = "zzabc".match(/(a)(b)/) return arr[0] + arr[1] + arr[2] `); expect(normal).toBe("abab"); }); it("Tries to not get confused with same matched characters in mutliple groups", async () => { const { normal, tracking, code } = await instrumentAndRun(` const arr = "zzaba".match(/(a)(b)(a)/) return arr[1] + arr[2] + arr[3] `); expect(normal).toBe("aba"); var t2 = await traverse({ operationLog: tracking, charIndex: 2 }); const t2LastStep = t2[t2.len