// Any copyright is dedicated to the Public Domain. // http://creativecommons.org/publicdomain/zero/1.0/ "use strict"; const { fallibleJsPropertyProvider: jsPropertyProvider, } = require("resource://devtools/shared/webconsole/js-property-provider.js"); const { addDebuggerToGlobal } = ChromeUtils.importESModule( "resource://gre/modules/jsdebugger.sys.mjs" ); addDebuggerToGlobal(globalThis); function run_test() { Services.prefs.setBoolPref( "security.allow_parent_unrestricted_js_loads", true ); registerCleanupFunction(() => { Services.prefs.clearUserPref("security.allow_parent_unrestricted_js_loads"); }); const testArray = `var testArray = [ {propA: "A"}, { propB: "B", propC: [ "D" ] }, [ {propE: "E"} ] ]`; const testObject = 'var testObject = {"propA": [{"propB": "B"}]}'; const testHyphenated = 'var testHyphenated = {"prop-A": "res-A"}'; const testLet = "let foobar = {a: ''}; const blargh = {a: 1};"; const testGenerators = ` // Test with generator using a named function. function* genFunc() { for (let i = 0; i < 10; i++) { yield i; } } let gen1 = genFunc(); gen1.next(); // Test with generator using an anonymous function. let gen2 = (function* () { for (let i = 0; i < 10; i++) { yield i; } })();`; const testGetters = ` var testGetters = { get x() { return Object.create(null, Object.getOwnPropertyDescriptors({ hello: "", world: "", })); }, get y() { return Object.create(null, Object.getOwnPropertyDescriptors({ get y() { return "plop"; }, })); } }; `; const testProxies = ` var testSelfPrototypeProxy = new Proxy({ hello: 1 }, { getPrototypeOf: () => testProxy }); var testArrayPrototypeProxy = new Proxy({ world: 2 }, { getPrototypeOf: () => Array.prototype }) `; const sandbox = Cu.Sandbox("http://example.com"); const dbg = new Debugger(); const dbgObject = dbg.addDebuggee(sandbox); const dbgEnv = dbgObject.asEnvironment(); Cu.evalInSandbox( ` const hello = Object.create(null, Object.getOwnPropertyDescriptors({world: 1})); String.prototype.hello = hello; Number.prototype.hello = hello; Array.prototype.hello = hello; `, sandbox ); Cu.evalInSandbox(testArray, sandbox); Cu.evalInSandbox(testObject, sandbox); Cu.evalInSandbox(testHyphenated, sandbox); Cu.evalInSandbox(testLet, sandbox); Cu.evalInSandbox(testGenerators, sandbox); Cu.evalInSandbox(testGetters, sandbox); Cu.evalInSandbox(testProxies, sandbox); info("Running tests with dbgObject"); runChecks(dbgObject, null, sandbox); info("Running tests with dbgEnv"); runChecks(null, dbgEnv, sandbox); } function runChecks(dbgObject, environment, sandbox) { const propertyProvider = (inputValue, options) => jsPropertyProvider({ dbgObject, environment, inputValue, ...options, }); info("Test that suggestions are given for 'this'"); let results = propertyProvider("t"); test_has_result(results, "this"); if (dbgObject != null) { info("Test that suggestions are given for 'this.'"); results = propertyProvider("this."); test_has_result(results, "testObject"); info("Test that suggestions are given for '(this).'"); results = propertyProvider("(this)."); test_has_result(results, "testObject"); info("Test that suggestions are given for deep 'this' properties access"); results = propertyProvider("(this).testObject.propA."); test_has_result(results, "shift"); results = propertyProvider("(this).testObject.propA["); test_has_result(results, `"shift"`); results = propertyProvider("(this)['testObject']['propA']["); test_has_result(results, `"shift"`); results = propertyProvider("(this).testObject['propA']."); test_has_result(results, "shift"); info("Test that no suggestions are given for 'this.this'"); results = propertyProvider("this.this"); test_has_no_results(results); } info("Test that suggestions are given for 'globalThis'"); results = propertyProvider("g"); test_has_result(results, "globalThis"); info("Test that suggestions are given for 'globalThis.'"); results = propertyProvider("globalThis."); test_has_result(results, "testObject"); info("Test that suggestions are given for '(globalThis).'"); results = propertyProvider("(globalThis)."); test_has_result(results, "testObject"); info( "Test that suggestions are given for deep 'globalThis' properties access" ); results = propertyProvider("(globalThis).testObject.propA."); test_has_result(results, "shift"); results = propertyProvider("(globalThis).testObject.propA["); test_has_result(results, `"shift"`); results = propertyProvider("(globalThis)['testObject']['propA']["); test_has_result(results, `"shift"`); results = propertyProvider("(globalThis).testObject['propA']."); test_has_result(results, "shift"); info("Testing lexical scope issues (Bug 1207868)"); results = propertyProvider("foobar"); test_has_result(results, "foobar"); results = propertyProvider("foobar."); test_has_result(results, "a"); results = propertyProvider("blargh"); test_has_result(results, "blargh"); results = propertyProvider("blargh."); test_has_result(results, "a"); info("Test that suggestions are given for 'foo[n]' where n is an integer."); results = propertyProvider("testArray[0]."); test_has_result(results, "propA"); info("Test that suggestions are given for multidimensional arrays."); results = propertyProvider("testArray[2][0]."); test_has_result(results, "propE"); info("Test that suggestions are given for nested arrays."); results = propertyProvider("testArray[1].propC[0]."); test_has_result(results, "indexOf"); info("Test that suggestions are given for literal arrays."); results = propertyProvider("[1,2,3]."); test_has_result(results, "indexOf"); results = propertyProvider("[1,2,3].h"); test_has_result(results, "hello"); results = propertyProvider("[1,2,3].hello.w"); test_has_result(results, "world"); info("Test that suggestions are given for literal arrays with newlines."); results = propertyProvider("[1,2,3,\n4\n]."); test_has_result(results, "indexOf"); info("Test that suggestions are given for literal strings."); results = propertyProvider("'foo'."); test_has_result(results, "charAt"); results = propertyProvider('"foo".'); test_has_result(results, "charAt"); results = propertyProvider("`foo`."); test_has_result(results, "charAt"); results = propertyProvider("`foo doc`."); test_has_result(results, "charAt"); results = propertyProvider('`foo " doc`.'); test_has_result(results, "charAt"); results = propertyProvider("`foo ' doc`."); test_has_result(results, "charAt"); results = propertyProvider("'[1,2,3]'."); test_has_result(results, "charAt"); results = propertyProvider("'foo'.h"); test_has_result(results, "hello"); results = propertyProvider("'foo'.hello.w"); test_has_result(results, "world"); results = propertyProvider(`"\\n".`); test_has_result(results, "charAt"); results = propertyProvider(`'\\r'.`); test_has_result(results, "charAt"); results = propertyProvider("`\\\\`."); test_has_result(results, "charAt"); info("Test that suggestions are not given for syntax errors."); results = propertyProvider("'foo\""); Assert.equal(null, results); results = propertyProvider("'foo d"); Assert.equal(null, results); results = propertyProvider(`"foo d`); Assert.equal(null, results); results = propertyProvider("`foo d"); Assert.equal(null, results); results = propertyProvider("[1,',2]"); Assert.equal(null, results); results = propertyProvider("'[1,2]."); Assert.equal(null, results); results = propertyProvider("'foo'.."); Assert.equal(null, results); info("Test that suggestions are not given without a dot."); results = propertyProvider("'foo'"); test_has_no_results(results); results = propertyProvider("`foo`"); test_has_no_results(results); results = propertyProvider("[1,2,3]"); test_has_no_results(results); results = propertyProvider("[1,2,3].\n'foo'"); test_has_no_results(results); info("Test that suggestions are not given for index that's out of bounds."); results = propertyProvider("testArray[10]."); Assert.equal(null, results); info("Test that invalid element access syntax does not return anything"); results = propertyProvider("testArray[][1]."); Assert.equal(null, results); info("Test that deep element access works."); results = propertyProvider("testObject['propA'][0]."); test_has_result(results, "propB"); results = propertyProvider("testArray[1]['propC']."); test_has_result(results, "shift"); results = propertyProvider("testArray[1].propC[0]["); test_has_result(results, `"trim"`); results = propertyProvider("testArray[1].propC[0]."); test_has_result(results, "trim"); info( "Test that suggestions are displayed when variable is wrapped in parens" ); results = propertyProvider("(testObject)['propA'][0]."); test_has_result(results, "propB"); results = propertyProvider("(testArray)[1]['propC']."); test_has_result(results, "shift"); results = propertyProvider("(testArray)[1].propC[0]["); test_has_result(results, `"trim"`); results = propertyProvider("(testArray)[1].propC[0]."); test_has_result(results, "trim"); info("Test that suggestions are given if there is an hyphen in the chain."); results = propertyProvider("testHyphenated['prop-A']."); test_has_result(results, "trim"); info("Test that we have suggestions for generators."); const gen1Result = Cu.evalInSandbox("gen1.next().value", sandbox); results = propertyProvider("gen1."); test_has_result(results, "next"); info("Test that the generator next() was not executed"); const gen1NextResult = Cu.evalInSandbox("gen1.next().value", sandbox); Assert.equal(gen1Result + 1, gen1NextResult); info("Test with an anonymous generator."); const gen2Result = Cu.evalInSandbox("gen2.next().value", sandbox); results = propertyProvider("gen2."); test_has_result(results, "next"); const gen2NextResult = Cu.evalInSandbox("gen2.next().value", sandbox); Assert.equal(gen2Result + 1, gen2NextResult); info( "Test that getters are not executed if authorizedEvaluations is undefined" ); results = propertyProvider("testGetters.x."); Assert.deepEqual(results, { isUnsafeGetter: true, getterPath: ["testGetters", "x"], }); results = propertyProvider("testGetters.x["); Assert.deepEqual(results, { isUnsafeGetter: true, getterPath: ["testGetters", "x"], }); results = propertyProvider("testGetters.x.hell"); Assert.deepEqual(results, { isUnsafeGetter: true, getterPath: ["testGetters", "x"], }); results = propertyProvider("testGetters.x['hell"); Assert.deepEqual(results, { isUnsafeGetter: true, getterPath: ["testGetters", "x"], }); info( "Test that getters are not executed if authorizedEvaluations does not match" ); results = propertyProvider("testGetters.x.", { authorizedEvaluations: [] }); Assert.deepEqual(results, { isUnsafeGetter: true, getterPath: ["testGetters", "x"], }); results = propertyProvider("testGetters.x.", { authorizedEvaluations: [["testGetters"]], }); Assert.deepEqual(results, { isUnsafeGetter: true, getterPath: ["testGetters", "x"], }); results = propertyProvider("testGetters.x.", { authorizedEvaluations: [["testGtrs", "x"]], }); Assert.deepEqual(results, { isUnsafeGetter: true, getterPath: ["testGetters", "x"], }); results = propertyProvider("testGetters.x.", { authorizedEvaluations: [["x"]], }); Assert.deepEqual(results, { isUnsafeGetter: true, getterPath: ["testGetters", "x"], }); info("Test that deep getter property access returns intermediate getters"); results = propertyProvider("testGetters.y.y."); Assert.deepEqual(results, { isUnsafeGetter: true, getterPath: ["testGetters", "y"], }); results = propertyProvider("testGetters['y'].y."); Assert.deepEqual(results, { isUnsafeGetter: true, getterPath: ["testGetters", "y"], }); results = propertyProvider("testGetters['y']['y']."); Assert.deepEqual(results, { isUnsafeGetter: true, getterPath: ["testGetters", "y"], }); results = propertyProvider("testGetters.y['y']."); Assert.deepEqual(results, { isUnsafeGetter: true, getterPath: ["testGetters", "y"], }); info("Test that deep getter property access invoke intermediate getters"); results = propertyProvider("testGetters.y.y.", { authorizedEvaluations: [["testGetters", "y"]], }); Assert.deepEqual(results, { isUnsafeGetter: true, getterPath: ["testGetters", "y", "y"], }); results = propertyProvider("testGetters['y'].y.", { authorizedEvaluations: [["testGetters", "y"]], }); Assert.deepEqual(results, { isUnsafeGetter: true, getterPath: ["testGetters", "y", "y"], }); results = propertyProvider("testGetters['y']['y'].", { authorizedEvaluations: [["testGetters", "y"]], }); Assert.deepEqual(results, { isUnsafeGetter: true, getterPath: ["testGetters", "y", "y"], }); results = propertyProvider("testGetters.y['y'].", { authorizedEvaluations: [["testGetters", "y"]], }); Assert.deepEqual(results, { isUnsafeGetter: true, getterPath: ["testGetters", "y", "y"], }); info( "Test that getters are executed if matching an authorizedEvaluation element" ); results = propertyProvider("testGetters.x.", { authorizedEvaluations: [["testGetters", "x"]], }); test_has_exact_results(results, ["hello", "world"]); Assert.ok(Object.keys(results).includes("isUnsafeGetter") === false); Assert.ok(Object.keys(results).includes("getterPath") === false); results = propertyProvider("testGetters.x.", { authorizedEvaluations: [["testGetters", "x"], ["y"]], }); test_has_exact_results(results, ["hello", "world"]); Assert.ok(Object.keys(results).includes("isUnsafeGetter") === false); Assert.ok(Object.keys(results).includes("getterPath") === false); info("Test that executing getters filters with provided string"); results = propertyProvider("testGetters.x.hell", { authorizedEvaluations: [["testGetters", "x"]], }); test_has_exact_results(results, ["hello"]); results = propertyProvider("testGetters.x['hell", { authorizedEvaluations: [["testGetters", "x"]], }); test_has_exact_results(results, ["'hello'"]); info( "Test children getters are not executed if not included in authorizedEvaluation" ); results = propertyProvider("testGetters.y.y.", { authorizedEvaluations: [["testGetters", "y", "y"]], }); Assert.deepEqual(results, { isUnsafeGetter: true, getterPath: ["testGetters", "y"], }); info( "Test children getters are executed if matching an authorizedEvaluation element" ); results = propertyProvider("testGetters.y.y.", { authorizedEvaluations: [ ["testGetters", "y"], ["testGetters", "y", "y"], ], }); test_has_result(results, "trim"); info("Test with number literals"); results = propertyProvider("1."); Assert.ok(results === null, "Does not complete on possible floating number"); results = propertyProvider("(1).."); Assert.ok(results === null, "Does not complete on invalid syntax"); results = propertyProvider("(1.1.)."); Assert.ok(results === null, "Does not complete on invalid syntax"); results = propertyProvider("1.."); test_has_result(results, "toFixed"); results = propertyProvider("1 ."); test_has_result(results, "toFixed"); results = propertyProvider("1\n."); test_has_result(results, "toFixed"); results = propertyProvider(".1."); test_has_result(results, "toFixed"); results = propertyProvider("1["); test_has_result(results, `"toFixed"`); results = propertyProvider("1[toFixed"); test_has_exact_results(results, [`"toFixed"`]); results = propertyProvider("1['toFixed"); test_has_exact_results(results, ["'toFixed'"]); results = propertyProvider("1.1["); test_has_result(results, `"toFixed"`); results = propertyProvider("(1)."); test_has_result(results, "toFixed"); results = propertyProvider("(.1)."); test_has_result(results, "toFixed"); results = propertyProvider("(1.1)."); test_has_result(results, "toFixed"); results = propertyProvider("(1).toFixed"); test_has_exact_results(results, ["toFixed"]); results = propertyProvider("(1)["); test_has_result(results, `"toFixed"`); results = propertyProvider("(1.1)["); test_has_result(results, `"toFixed"`); results = propertyProvider("(1)[toFixed"); test_has_exact_results(results, [`"toFixed"`]); results = propertyProvider("(1)['toFixed"); test_has_exact_results(results, ["'toFixed'"]); results = propertyProvider("(1).h"); test_has_result(results, "hello"); results = propertyProvider("(1).hello.w"); test_has_result(results, "world"); info("Test access on dot-notation invalid property name"); results = propertyProvider("testHyphenated.prop"); Assert.ok( !results.matches.has("prop-A"), "Does not return invalid property name on dot access" ); results = propertyProvider("testHyphenated['prop"); test_has_result(results, `'prop-A'`); results = propertyProvider(`//t`); Assert.ok(results === null, "Does not complete in inline comment"); results = propertyProvider(`// t`); Assert.ok( results === null, "Does not complete in inline comment after space" ); results = propertyProvider(`//I'm a comment\nt`); test_has_result(results, "testObject"); results = propertyProvider(`1/t`); test_has_result(results, "testObject"); results = propertyProvider(`/* t`); Assert.ok(results === null, "Does not complete in multiline comment"); results = propertyProvider(`/*I'm\nt`); Assert.ok( results === null, "Does not complete in multiline comment after line break" ); results = propertyProvider(`/*I'm a comment\n \t * /t`); Assert.ok( results === null, "Does not complete in multiline comment after line break and invalid comment end" ); results = propertyProvider(`/*I'm a comment\n \t */t`); test_has_result(results, "testObject"); results = propertyProvider(`/*I'm a comment\n \t */\n\nt`); test_has_result(results, "testObject"); info("Test local expression variables"); results = propertyProvider("b", { expressionVars: ["a", "b", "c"] }); test_has_result(results, "b"); Assert.equal(results.matches.has("a"), false); Assert.equal(results.matches.has("c"), false); info( "Test that local expression variables are not included when accessing an object properties" ); results = propertyProvider("testObject.prop", { expressionVars: ["propLocal"], }); Assert.equal(results.matches.has("propLocal"), false); test_has_result(results, "propA"); results = propertyProvider("testObject['prop", { expressionVars: ["propLocal"], }); test_has_result(results, "'propA'"); Assert.equal(results.matches.has("propLocal"), false); info("Test that expression with optional chaining operator are completed"); results = propertyProvider("testObject?.prop"); test_has_result(results, "propA"); results = propertyProvider("testObject?.propA[0]?.propB?.to"); test_has_result(results, "toString"); results = propertyProvider("testObject?.propA?.[0]?.propB?.to"); test_has_result(results, "toString"); results = propertyProvider( "testObject ?. propA[0] ?. propB ?. to" ); test_has_result(results, "toString"); results = propertyProvider("testObject?.[prop"); test_has_result(results, '"propA"'); results = propertyProvider(`testObject?.["prop`); test_has_result(results, '"propA"'); results = propertyProvider(`testObject?.['prop`); test_has_result(results, `'propA'`); results = propertyProvider(`testObject?.["propA"]?.[0]?.["propB"]?.["to`); test_has_result(results, `"toString"`); results = propertyProvider( `testObject ?. ["propA"] ?. [0] ?. ["propB"] ?. ['to` ); test_has_result(results, "'toString'"); results = propertyProvider("[1,2,3]?."); test_has_result(results, "indexOf"); results = propertyProvider("'foo'?."); test_has_result(results, "charAt"); results = propertyProvider("1?."); test_has_result(results, "toFixed"); // check this doesn't throw since `propC` is not defined. results = propertyProvider("testObject?.propC?.this?.does?.not?.exist?.d"); // check that ternary operator isn't mistaken for optional chaining results = propertyProvider(`true?.3.to`); test_has_result(results, `toExponential`); results = propertyProvider(`true?.3?.to`); test_has_result(results, `toExponential`); // Test more ternary results = propertyProvider(`true?t`); test_has_result(results, `testObject`); results = propertyProvider(`true??t`); test_has_result(results, `testObject`); results = propertyProvider(`true?/* comment */t`); test_has_result(results, `testObject`); results = propertyProvider(`true? 0); Assert.ok( results.matches.has(requiredSuggestion), `<${requiredSuggestion}> found in ${[...results.matches.values()].join( " - " )}` ); } /** * A helper that ensures results are the expected ones. * @param Object results * The results returned by jsPropertyProvider. * @param Array expectedMatches * An array of the properties that should be returned by jsPropertyProvider. */ function test_has_exact_results(results, expectedMatches) { Assert.deepEqual([...results.matches], expectedMatches); }