current commit
260718.6 · repl: use inspector over vm
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.

'use strict';

const common = require('../common');
const { startNewREPLServer, complete } = require('../common/repl');
const { describe, it } = require('node:test');
const assert = require('assert');

async function expectNoResults(replServer, query) {
  const data = await complete(replServer, query);
  assert.deepStrictEqual(data[0], []);
}

describe('REPL tab completion (core functionality)', () => {
  it('does not break with variable declarations without an initialization', async () => {
    const { replServer } = startNewREPLServer();
    await expectNoResults(replServer, 'let a');
    replServer.close();
  });

  it('does not break in an object literal', async () => {
    const { replServer, run } = startNewREPLServer();

    await run(['var inner = {', 'one:1']);

    await expectNoResults(replServer, 'inner.o');

    assert.deepStrictEqual(
      await complete(replServer, 'console.lo'),
      [['console.log'], 'console.lo']);

    replServer.close();
  });

  it('works with optional chaining', async () => {
    const { replServer } = startNewREPLServer();

    assert.deepStrictEqual(
      await complete(replServer, 'console?.lo'),
      [['console?.log'], 'console?.lo']);

    assert.deepStrictEqual(
      await complete(replServer, 'console?.zzz'),
      [[], 'console?.zzz']);

    const data = await complete(replServer, 'console?.');
    assert(data[0].includes('console?.log'));
    assert.strictEqual(data[1], 'console?.');

    replServer.close();
  });

  it('returns object completions', async () => {
    const { replServer, run } = startNewREPLServer();

    await run(['var inner = {', 'one:1']);

    await run(['};']);

    assert.deepStrictEqual(
      await complete(replServer, 'inner.o'),
      [['inner.one'], 'inner.o']);

    replServer.close();
  });

  it('does not break in a ternary operator with ()', async () => {
    const { replServer, run } = startNewREPLServer();

    await run(['var inner = ( true ', '?', '{one: 1} : ']);

    await expectNoResults(replServer, 'inner.o');

    replServer.close();
  });

  it('works on literals', async () => {
    const { replServer } = startNewREPLServer();

    assert.strictEqual((await complete(replServer, '``.a'))[0].includes('``.at'), true);
    assert.strictEqual((await complete(replServer, "''.a"))[0].includes("''.at"), true);
    assert.strictEqual((await complete(replServer, '"".a'))[0].includes('"".at'), true);
    assert.strictEqual((await complete(replServer, '("").a'))[0].includes('("").at'), true);
    assert.strictEqual((await complete(replServer, '[].a'))[0].includes('[].at'), true);
    assert.deepStrictEqual((await complete(replServer, '{}.a'))[0], []);

    replServer.close();
  });

  it("does not return a function's local variable", async () => {
    const { replServer, run } = startNewREPLServer();

    await run(['var top = function() {', 'var inner = {one:1};', '}']);

    await expectNoResults(replServer, 'inner.o');

    replServer.close();
  });

  it("does not return a function's local variable even when the function has parameters", async () => {
    const { replServer, run } = startNewREPLServer();

    await run([
      'var top = function(one, two) {',
      'var inner = {',
      ' one:1',
      '};',
    ]);

    await expectNoResults(replServer, 'inner.o');

    replServer.close();
  });

  it("does not return a function's local variable" +
    'even if the scope is nested inside an immediately executed function', async () => {
    const { replServer, run } = startNewREPLServer();

    await run([
      'var top = function() {',
      '(function test () {',
      'var inner = {',
      ' one:1',
      '};',
    ]);

    await expectNoResults(replServer, 'inner.o');

    replServer.close();
  });

  it("does not return a function's local variable" +
    'even if the scope is nested inside an immediately executed function' +
    '(the definition has the params and { on a separate line)', async () => {
    const { replServer, run } = startNewREPLServer();

    await run([
      'var top = function() {',
      'r = function test (',
      ' one, two) {',
      'var inner = {',
      ' one:1',
      '};',
    ]);

    await expectNoResults(replServer, 'inner.o');

    replServer.close();
  });

  it('currently does not work, but should not break (local inner)', async () => {
    const { replServer, run } = startNewREPLServer();

    await run([
      'var top = function() {',
      'r = function test ()',
      '{',
      'var inner = {',
      ' one:1',
      '};',
    ]);

    await expectNoResults(replServer, 'inner.o');

    replServer.close();
  });

  it('currently does not work, but should not break (local inner parens next line)', async () => {
    const { replServer, run } = startNewREPLServer();

    await run([
      'var top = function() {',
      'r = function test (',
      ')',
      '{',
      'var inner = {',
      ' one:1',
      '};',
    ]);

    await expectNoResults(replServer, 'inner.o');

    replServer.close();
  });

  it('works on non-Objects', async () => {
    const { replServer, run } = startNewREPLServer();

    await run(['var str = "test";']);

    assert.deepStrictEqual(
      await complete(replServer, 'str.len'),
      [['str.length'], 'str.len']);

    replServer.close();
  });

  it('should be case-insensitive if member part is lower-case', async () => {
    const { replServer, run } = startNewREPLServer();

    await run(['var foo = { barBar: 1, BARbuz: 2, barBLA: 3 };']);

    assert.deepStrictEqual(
      await complete(replServer, 'foo.b'),
      [['foo.BARbuz', 'foo.barBLA', 'foo.barBar'], 'foo.b']);

    replServer.close();
  });

  it('should be case-insensitive if member part is upper-case', async () => {
    const { replServer, run } = startNewREPLServer();

    await run(['var foo = { barBar: 1, BARbuz: 2, barBLA: 3 };']);

    assert.deepStrictEqual(
      await complete(replServer, 'foo.B'),
      [['foo.BARbuz', 'foo.barBLA', 'foo.barBar'], 'foo.B']);

    replServer.close();
  });

  it('should not break on spaces', async () => {
    const { replServer } = startNewREPLServer();

    const data = await complete(replServer, ' ');
    assert.strictEqual(data[1], '');
    assert.ok(data[0].includes('globalThis'));

    replServer.close();
  });

  it(`should pick up the global "toString" object, and any other properties up the "global" object's prototype chain`, async () => {
    const { replServer } = startNewREPLServer();

    assert.deepStrictEqual(
      await complete(replServer, 'toSt'),
      [['toString'], 'toSt']);

    replServer.close();
  });

  it('should make own properties shadow properties on the prototype', async () => {
    const { replServer, run } = startNewREPLServer();

    await run([
      'var x = Object.create(null);',
      'x.a = 1;',
      'x.b = 2;',
      'var y = Object.create(x);',
      'y.a = 3;',
      'y.c = 4;',
    ]);

    assert.deepStrictEqual(
      await complete(replServer, 'y.'),
      [['y.b', '', 'y.a', 'y.c'], 'y.']);

    replServer.close();
  });

  it('works on context properties', async () => {
    const { replServer, run } = startNewREPLServer();

    await run(['var custom = "test";']);

    assert.deepStrictEqual(
      await complete(replServer, 'cus'),
      [['CustomEvent', 'custom'], 'cus']);

    replServer.close();
  });

  it("doesn't crash REPL with half-baked proxy objects", async () => {
    const { replServer, run } = startNewREPLServer();

    await run([
      'var proxy = new Proxy({}, {ownKeys: () => { throw new Error(); }});',
    ]);

    const data = await complete(replServer, 'proxy.');
    assert(Array.isArray(data));

    replServer.close();
  });

  it('does not include integer members of an Array', async () => {
    const { replServer, run } = startNewREPLServer();

    await run(['var ary = [1,2,3];']);

    const data = await complete(replServer, 'ary.');
    assert.strictEqual(data[0].includes('ary.0'), false);
    assert.strictEqual(data[0].includes('ary.1'), false);
    assert.strictEqual(data[0].includes('ary.2'), false);

    replServer.close();
  });

  it('does not include integer keys in an object', async () => {
    const { replServer, run } = startNewREPLServer();

    await run(['var obj = {1:"a","1a":"b",a:"b"};']);

    const data = await complete(replServer, 'obj.');
    assert.strictEqual(data[0].includes('obj.1'), false);
    assert.strictEqual(data[0].includes('obj.1a'), false);
    assert(data[0].includes('obj.a'));

    replServer.close();
  });

  it('does not try to complete results of non-simple expressions', async () => {
    const { replServer, run } = startNewREPLServer();

    await run(['function a() {}']);

    await expectNoResults(replServer, 'a().b.');

    replServer.close();
  });

  it('works when prefixed with spaces', async () => {
    const { replServer, run } = startNewREPLServer();

    await run(['var obj = {1:"a","1a":"b",a:"b"};']);

    const data = await complete(replServer, ' obj.');
    assert.strictEqual(data[0].includes('obj.1'), false);
    assert.strictEqual(data[0].includes('obj.1a'), false);
    assert(data[0].includes('obj.a'));

    replServer.close();
  });

  it('works inside assignments', async () => {
    const { replServer } = startNewREPLServer();

    assert.deepStrictEqual(
      await complete(replServer, 'var log = console.lo'),
      [['console.log'], 'console.lo']);

    replServer.close();
  });

  it('does not complete identifiers inside an unterminated string literal', async () => {
    // The completion point sits inside the text of an open string, where there
    // is nothing to complete. Regression test: previously the partial path was
    // mistaken for a bare identifier and completed against the global scope.
    const { replServer } = startNewREPLServer();

    await expectNoResults(replServer, 'const x = "./n');
    await expectNoResults(replServer, "const x = './n");
    await expectNoResults(replServer, 'const x = `./n');

    replServer.close();
  });

  it('does not complete inside fs path strings when blocking is disabled', async () => {
    // With `allowBlockingCompletions` off, `fs.<fn>("…` is just an open string;
    // it must not fall back to completing the partial path as a global.
    const { replServer } = startNewREPLServer({ allowBlockingCompletions: false });

    await expectNoResults(replServer, 'fs.readFileSync("./n');
    await expectNoResults(replServer, 'fs.promises.readFile("./n');
    await expectNoResults(replServer, 'fs.readFileSync(`./n');

    replServer.close();
  });

  it('works for defined commands', async () => {
    const { replServer, run } = startNewREPLServer();

    assert.deepStrictEqual(
      await complete(replServer, '.b'),
      [['break'], 'b']);

    await run(['var obj = {"hello, world!": "some string", "key": 123}']);

    const data = await complete(replServer, 'obj.');
    assert.strictEqual(data[0].includes('obj.hello, world!'), false);
    assert(data[0].includes('obj.key'));

    replServer.close();
  });

  it('does not include __defineSetter__ and friends', async () => {
    const { replServer, run } = startNewREPLServer();

    await run(['var obj = {};']);

    const data = await complete(replServer, 'obj.');
    assert.strictEqual(data[0].includes('obj.__defineGetter__'), false);
    assert.strictEqual(data[0].includes('obj.__defineSetter__'), false);
    assert.strictEqual(data[0].includes('obj.__lookupGetter__'), false);
    assert.strictEqual(data[0].includes('obj.__lookupSetter__'), false);
    assert.strictEqual(data[0].includes('obj.__proto__'), true);

    replServer.close();
  });

  it('works with builtin values', async () => {
    const { replServer } = startNewREPLServer();

    assert.deepStrictEqual(
      await complete(replServer, 'I'),
      [
        [
          'if',
          'import',
          'in',
          'instanceof',
          '',
          'Infinity',
          'Int16Array',
          'Int32Array',
          'Int8Array',
          ...(common.hasIntl ? ['Intl'] : []),
          'Iterator',
          'inspector',
          'isFinite',
          'isNaN',
          '',
          'isPrototypeOf',
        ],
        'I',
      ]);

    replServer.close();
  });

  it('works with lexically scoped variables', async () => {
    const { replServer, run } = startNewREPLServer();

    await run([
      'let lexicalLet = true;',
      'const lexicalConst = true;',
      'class lexicalKlass {}',
    ]);

    for (const type of ['Let', 'Const', 'Klass']) {
      const query = `lexical${type[0]}`;
      const expected = [[`lexical${type}`], query];
      assert.deepStrictEqual(await complete(replServer, query), expected);
    }

    replServer.close();
  });
});