node/test/parallel/test-repl-tab-complete.js
Anna Henningsen 82dd23f5ec
repl: disable blocking completions by default
It’s not okay for the REPL to be blocked for multiple seconds after
entering `require('` because the completion is performing blocking
fs operations on potentially huge directories. Turning the REPL
completion function asynchronous would be the right thing to do here,
but unfortunately the way the code is structured doesn’t play well
with that (in particular, it breaks the preview feature).
Therefore, disable these blocking calls by default.

Refs: https://github.com/nodejs/node/pull/33282#issuecomment-733646794

PR-URL: https://github.com/nodejs/node/pull/36564
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Rich Trott <rtrott@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
2020-12-21 12:52:02 +01:00

662 lines
19 KiB
JavaScript

// 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 ArrayStream = require('../common/arraystream');
const {
hijackStderr,
restoreStderr
} = require('../common/hijackstdio');
const assert = require('assert');
const path = require('path');
const fixtures = require('../common/fixtures');
const { builtinModules } = require('module');
const hasInspector = process.features.inspector;
if (!common.isMainThread)
common.skip('process.chdir is not available in Workers');
// We have to change the directory to ../fixtures before requiring repl
// in order to make the tests for completion of node_modules work properly
// since repl modifies module.paths.
process.chdir(fixtures.fixturesDir);
const repl = require('repl');
function getNoResultsFunction() {
return common.mustSucceed((data) => {
assert.deepStrictEqual(data[0], []);
});
}
const works = [['inner.one'], 'inner.o'];
const putIn = new ArrayStream();
const testMe = repl.start({
prompt: '',
input: putIn,
output: process.stdout,
allowBlockingCompletions: true
});
// Some errors are passed to the domain, but do not callback
testMe._domain.on('error', assert.ifError);
// Tab Complete will not break in an object literal
putIn.run([
'var inner = {',
'one:1'
]);
testMe.complete('inner.o', getNoResultsFunction());
testMe.complete('console.lo', common.mustCall(function(error, data) {
assert.deepStrictEqual(data, [['console.log'], 'console.lo']);
}));
testMe.complete('console?.lo', common.mustCall((error, data) => {
assert.deepStrictEqual(data, [['console?.log'], 'console?.lo']);
}));
testMe.complete('console?.zzz', common.mustCall((error, data) => {
assert.deepStrictEqual(data, [[], 'console?.zzz']);
}));
testMe.complete('console?.', common.mustCall((error, data) => {
assert(data[0].includes('console?.log'));
assert.strictEqual(data[1], 'console?.');
}));
// Tab Complete will return globally scoped variables
putIn.run(['};']);
testMe.complete('inner.o', common.mustCall(function(error, data) {
assert.deepStrictEqual(data, works);
}));
putIn.run(['.clear']);
// Tab Complete will not break in an ternary operator with ()
putIn.run([
'var inner = ( true ',
'?',
'{one: 1} : '
]);
testMe.complete('inner.o', getNoResultsFunction());
putIn.run(['.clear']);
// Tab Complete will return a simple local variable
putIn.run([
'var top = function() {',
'var inner = {one:1};'
]);
testMe.complete('inner.o', getNoResultsFunction());
// When you close the function scope tab complete will not return the
// locally scoped variable
putIn.run(['};']);
testMe.complete('inner.o', getNoResultsFunction());
putIn.run(['.clear']);
// Tab Complete will return a complex local variable
putIn.run([
'var top = function() {',
'var inner = {',
' one:1',
'};'
]);
testMe.complete('inner.o', getNoResultsFunction());
putIn.run(['.clear']);
// Tab Complete will return a complex local variable even if the function
// has parameters
putIn.run([
'var top = function(one, two) {',
'var inner = {',
' one:1',
'};'
]);
testMe.complete('inner.o', getNoResultsFunction());
putIn.run(['.clear']);
// Tab Complete will return a complex local variable even if the
// scope is nested inside an immediately executed function
putIn.run([
'var top = function() {',
'(function test () {',
'var inner = {',
' one:1',
'};'
]);
testMe.complete('inner.o', getNoResultsFunction());
putIn.run(['.clear']);
// The definition has the params and { on a separate line.
putIn.run([
'var top = function() {',
'r = function test (',
' one, two) {',
'var inner = {',
' one:1',
'};'
]);
testMe.complete('inner.o', getNoResultsFunction());
putIn.run(['.clear']);
// Currently does not work, but should not break, not the {
putIn.run([
'var top = function() {',
'r = function test ()',
'{',
'var inner = {',
' one:1',
'};'
]);
testMe.complete('inner.o', getNoResultsFunction());
putIn.run(['.clear']);
// Currently does not work, but should not break
putIn.run([
'var top = function() {',
'r = function test (',
')',
'{',
'var inner = {',
' one:1',
'};'
]);
testMe.complete('inner.o', getNoResultsFunction());
putIn.run(['.clear']);
// Make sure tab completion works on non-Objects
putIn.run([
'var str = "test";'
]);
testMe.complete('str.len', common.mustCall(function(error, data) {
assert.deepStrictEqual(data, [['str.length'], 'str.len']);
}));
putIn.run(['.clear']);
// Tab completion should not break on spaces
const spaceTimeout = setTimeout(function() {
throw new Error('timeout');
}, 1000);
testMe.complete(' ', common.mustSucceed((data) => {
assert.strictEqual(data[1], '');
assert.ok(data[0].includes('globalThis'));
clearTimeout(spaceTimeout);
}));
// Tab completion should pick up the global "toString" object, and
// any other properties up the "global" object's prototype chain
testMe.complete('toSt', common.mustCall(function(error, data) {
assert.deepStrictEqual(data, [['toString'], 'toSt']);
}));
// Own properties should shadow properties on the prototype
putIn.run(['.clear']);
putIn.run([
'var x = Object.create(null);',
'x.a = 1;',
'x.b = 2;',
'var y = Object.create(x);',
'y.a = 3;',
'y.c = 4;'
]);
testMe.complete('y.', common.mustCall(function(error, data) {
assert.deepStrictEqual(data, [['y.b', '', 'y.a', 'y.c'], 'y.']);
}));
// Tab complete provides built in libs for require()
putIn.run(['.clear']);
testMe.complete('require(\'', common.mustCall(function(error, data) {
assert.strictEqual(error, null);
builtinModules.forEach((lib) => {
assert(
data[0].includes(lib) || lib.startsWith('_') || lib.includes('/'),
`${lib} not found`
);
});
const newModule = 'foobar';
assert(!builtinModules.includes(newModule));
repl.builtinModules.push(newModule);
testMe.complete('require(\'', common.mustCall((_, [modules]) => {
assert.strictEqual(data[0].length + 1, modules.length);
assert(modules.includes(newModule));
}));
}));
testMe.complete("require\t( 'n", common.mustCall(function(error, data) {
assert.strictEqual(error, null);
assert.strictEqual(data.length, 2);
assert.strictEqual(data[1], 'n');
// There is only one Node.js module that starts with n:
assert.strictEqual(data[0][0], 'net');
assert.strictEqual(data[0][1], '');
// It's possible to pick up non-core modules too
data[0].slice(2).forEach((completion) => {
assert.match(completion, /^n/);
});
}));
{
const expected = ['@nodejsscope', '@nodejsscope/'];
// Require calls should handle all types of quotation marks.
for (const quotationMark of ["'", '"', '`']) {
putIn.run(['.clear']);
testMe.complete('require(`@nodejs', common.mustCall((err, data) => {
assert.strictEqual(err, null);
assert.deepStrictEqual(data, [expected, '@nodejs']);
}));
putIn.run(['.clear']);
// Completions should not be greedy in case the quotation ends.
const input = `require(${quotationMark}@nodejsscope${quotationMark}`;
testMe.complete(input, common.mustCall((err, data) => {
assert.strictEqual(err, null);
assert.deepStrictEqual(data, [[], undefined]);
}));
}
}
{
putIn.run(['.clear']);
// Completions should find modules and handle whitespace after the opening
// bracket.
testMe.complete('require \t("no_ind', common.mustCall((err, data) => {
assert.strictEqual(err, null);
assert.deepStrictEqual(data, [['no_index', 'no_index/'], 'no_ind']);
}));
}
// Test tab completion for require() relative to the current directory
{
putIn.run(['.clear']);
const cwd = process.cwd();
process.chdir(__dirname);
['require(\'.', 'require(".'].forEach((input) => {
testMe.complete(input, common.mustCall((err, data) => {
assert.strictEqual(err, null);
assert.strictEqual(data.length, 2);
assert.strictEqual(data[1], '.');
assert.strictEqual(data[0].length, 2);
assert.ok(data[0].includes('./'));
assert.ok(data[0].includes('../'));
}));
});
['require(\'..', 'require("..'].forEach((input) => {
testMe.complete(input, common.mustCall((err, data) => {
assert.strictEqual(err, null);
assert.deepStrictEqual(data, [['../'], '..']);
}));
});
['./', './test-'].forEach((path) => {
[`require('${path}`, `require("${path}`].forEach((input) => {
testMe.complete(input, common.mustCall((err, data) => {
assert.strictEqual(err, null);
assert.strictEqual(data.length, 2);
assert.strictEqual(data[1], path);
assert.ok(data[0].includes('./test-repl-tab-complete'));
}));
});
});
['../parallel/', '../parallel/test-'].forEach((path) => {
[`require('${path}`, `require("${path}`].forEach((input) => {
testMe.complete(input, common.mustCall((err, data) => {
assert.strictEqual(err, null);
assert.strictEqual(data.length, 2);
assert.strictEqual(data[1], path);
assert.ok(data[0].includes('../parallel/test-repl-tab-complete'));
}));
});
});
{
const path = '../fixtures/repl-folder-extensions/f';
testMe.complete(`require('${path}`, common.mustSucceed((data) => {
assert.strictEqual(data.length, 2);
assert.strictEqual(data[1], path);
assert.ok(data[0].includes('../fixtures/repl-folder-extensions/foo.js'));
}));
}
process.chdir(cwd);
}
// Make sure tab completion works on context properties
putIn.run(['.clear']);
putIn.run([
'var custom = "test";'
]);
testMe.complete('cus', common.mustCall(function(error, data) {
assert.deepStrictEqual(data, [['custom'], 'cus']);
}));
// Make sure tab completion doesn't crash REPL with half-baked proxy objects.
// See: https://github.com/nodejs/node/issues/2119
putIn.run(['.clear']);
putIn.run([
'var proxy = new Proxy({}, {ownKeys: () => { throw new Error(); }});'
]);
testMe.complete('proxy.', common.mustCall(function(error, data) {
assert.strictEqual(error, null);
assert(Array.isArray(data));
}));
// Make sure tab completion does not include integer members of an Array
putIn.run(['.clear']);
putIn.run(['var ary = [1,2,3];']);
testMe.complete('ary.', common.mustCall(function(error, data) {
assert.strictEqual(data[0].includes('ary.0'), false);
assert.strictEqual(data[0].includes('ary.1'), false);
assert.strictEqual(data[0].includes('ary.2'), false);
}));
// Make sure tab completion does not include integer keys in an object
putIn.run(['.clear']);
putIn.run(['var obj = {1:"a","1a":"b",a:"b"};']);
testMe.complete('obj.', common.mustCall(function(error, data) {
assert.strictEqual(data[0].includes('obj.1'), false);
assert.strictEqual(data[0].includes('obj.1a'), false);
assert(data[0].includes('obj.a'));
}));
// Don't try to complete results of non-simple expressions
putIn.run(['.clear']);
putIn.run(['function a() {}']);
testMe.complete('a().b.', getNoResultsFunction());
// Works when prefixed with spaces
putIn.run(['.clear']);
putIn.run(['var obj = {1:"a","1a":"b",a:"b"};']);
testMe.complete(' obj.', common.mustCall((error, data) => {
assert.strictEqual(data[0].includes('obj.1'), false);
assert.strictEqual(data[0].includes('obj.1a'), false);
assert(data[0].includes('obj.a'));
}));
// Works inside assignments
putIn.run(['.clear']);
testMe.complete('var log = console.lo', common.mustCall((error, data) => {
assert.deepStrictEqual(data, [['console.log'], 'console.lo']);
}));
// Tab completion for defined commands
putIn.run(['.clear']);
testMe.complete('.b', common.mustCall((error, data) => {
assert.deepStrictEqual(data, [['break'], 'b']);
}));
putIn.run(['.clear']);
putIn.run(['var obj = {"hello, world!": "some string", "key": 123}']);
testMe.complete('obj.', common.mustCall((error, data) => {
assert.strictEqual(data[0].includes('obj.hello, world!'), false);
assert(data[0].includes('obj.key'));
}));
// Tab completion for files/directories
{
putIn.run(['.clear']);
process.chdir(__dirname);
const readFileSyncs = ['fs.readFileSync("', 'fs.promises.readFileSync("'];
if (!common.isWindows) {
readFileSyncs.forEach((readFileSync) => {
const fixturePath = `${readFileSync}../fixtures/test-repl-tab-completion`;
testMe.complete(fixturePath, common.mustCall((err, data) => {
assert.strictEqual(err, null);
assert.ok(data[0][0].includes('.hiddenfiles'));
assert.ok(data[0][1].includes('hellorandom.txt'));
assert.ok(data[0][2].includes('helloworld.js'));
}));
testMe.complete(`${fixturePath}/hello`,
common.mustCall((err, data) => {
assert.strictEqual(err, null);
assert.ok(data[0][0].includes('hellorandom.txt'));
assert.ok(data[0][1].includes('helloworld.js'));
})
);
testMe.complete(`${fixturePath}/.h`,
common.mustCall((err, data) => {
assert.strictEqual(err, null);
assert.ok(data[0][0].includes('.hiddenfiles'));
})
);
testMe.complete(`${readFileSync}./xxxRandom/random`,
common.mustCall((err, data) => {
assert.strictEqual(err, null);
assert.strictEqual(data[0].length, 0);
})
);
const testPath = fixturePath.slice(0, -1);
testMe.complete(testPath, common.mustCall((err, data) => {
assert.strictEqual(err, null);
assert.ok(data[0][0].includes('test-repl-tab-completion'));
assert.strictEqual(
data[1],
path.basename(testPath)
);
}));
});
}
}
[
Array,
Buffer,
Uint8Array,
Uint16Array,
Uint32Array,
Uint8ClampedArray,
Int8Array,
Int16Array,
Int32Array,
Float32Array,
Float64Array,
].forEach((type) => {
putIn.run(['.clear']);
if (type === Array) {
putIn.run([
'var ele = [];',
'for (let i = 0; i < 1e6 + 1; i++) ele[i] = 0;',
'ele.biu = 1;'
]);
} else if (type === Buffer) {
putIn.run(['var ele = Buffer.alloc(1e6 + 1); ele.biu = 1;']);
} else {
putIn.run([`var ele = new ${type.name}(1e6 + 1); ele.biu = 1;`]);
}
hijackStderr(common.mustNotCall());
testMe.complete('ele.', common.mustCall((err, data) => {
restoreStderr();
assert.ifError(err);
const ele = (type === Array) ?
[] :
(type === Buffer ?
Buffer.alloc(0) :
new type(0));
assert.strictEqual(data[0].includes('ele.biu'), true);
data[0].forEach((key) => {
if (!key || key === 'ele.biu') return;
assert.notStrictEqual(ele[key.substr(4)], undefined);
});
}));
});
// check Buffer.prototype.length not crashing.
// Refs: https://github.com/nodejs/node/pull/11961
putIn.run(['.clear']);
testMe.complete('Buffer.prototype.', common.mustCall());
const testNonGlobal = repl.start({
input: putIn,
output: putIn,
useGlobal: false
});
const builtins = [['Infinity', 'Int16Array', 'Int32Array',
'Int8Array'], 'I'];
if (common.hasIntl) {
builtins[0].push('Intl');
}
testNonGlobal.complete('I', common.mustCall((error, data) => {
assert.deepStrictEqual(data, builtins);
}));
// To test custom completer function.
// Sync mode.
const customCompletions = 'aaa aa1 aa2 bbb bb1 bb2 bb3 ccc ddd eee'.split(' ');
const testCustomCompleterSyncMode = repl.start({
prompt: '',
input: putIn,
output: putIn,
completer: function completer(line) {
const hits = customCompletions.filter((c) => c.startsWith(line));
// Show all completions if none found.
return [hits.length ? hits : customCompletions, line];
}
});
// On empty line should output all the custom completions
// without complete anything.
testCustomCompleterSyncMode.complete('', common.mustCall((error, data) => {
assert.deepStrictEqual(data, [
customCompletions,
''
]);
}));
// On `a` should output `aaa aa1 aa2` and complete until `aa`.
testCustomCompleterSyncMode.complete('a', common.mustCall((error, data) => {
assert.deepStrictEqual(data, [
'aaa aa1 aa2'.split(' '),
'a'
]);
}));
// To test custom completer function.
// Async mode.
const testCustomCompleterAsyncMode = repl.start({
prompt: '',
input: putIn,
output: putIn,
completer: function completer(line, callback) {
const hits = customCompletions.filter((c) => c.startsWith(line));
// Show all completions if none found.
callback(null, [hits.length ? hits : customCompletions, line]);
}
});
// On empty line should output all the custom completions
// without complete anything.
testCustomCompleterAsyncMode.complete('', common.mustCall((error, data) => {
assert.deepStrictEqual(data, [
customCompletions,
''
]);
}));
// On `a` should output `aaa aa1 aa2` and complete until `aa`.
testCustomCompleterAsyncMode.complete('a', common.mustCall((error, data) => {
assert.deepStrictEqual(data, [
'aaa aa1 aa2'.split(' '),
'a'
]);
}));
// Tab completion in editor mode
const editorStream = new ArrayStream();
const editor = repl.start({
stream: editorStream,
terminal: true,
useColors: false
});
editorStream.run(['.clear']);
editorStream.run(['.editor']);
editor.completer('Uin', common.mustCall((error, data) => {
assert.deepStrictEqual(data, [['Uint'], 'Uin']);
}));
editorStream.run(['.clear']);
editorStream.run(['.editor']);
editor.completer('var log = console.l', common.mustCall((error, data) => {
assert.deepStrictEqual(data, [['console.log'], 'console.l']);
}));
{
// Tab completion of lexically scoped variables
const stream = new ArrayStream();
const testRepl = repl.start({ stream });
stream.run([`
let lexicalLet = true;
const lexicalConst = true;
class lexicalKlass {}
`]);
['Let', 'Const', 'Klass'].forEach((type) => {
const query = `lexical${type[0]}`;
const expected = hasInspector ? [[`lexical${type}`], query] :
[[], `lexical${type[0]}`];
testRepl.complete(query, common.mustCall((error, data) => {
assert.deepStrictEqual(data, expected);
}));
});
}