• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright Joyent, Inc. and other Node contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a
4// copy of this software and associated documentation files (the
5// "Software"), to deal in the Software without restriction, including
6// without limitation the rights to use, copy, modify, merge, publish,
7// distribute, sublicense, and/or sell copies of the Software, and to permit
8// persons to whom the Software is furnished to do so, subject to the
9// following conditions:
10//
11// The above copyright notice and this permission notice shall be included
12// in all copies or substantial portions of the Software.
13//
14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
17// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
20// USE OR OTHER DEALINGS IN THE SOFTWARE.
21
22'use strict';
23
24const common = require('../common');
25const ArrayStream = require('../common/arraystream');
26const {
27  hijackStderr,
28  restoreStderr
29} = require('../common/hijackstdio');
30const assert = require('assert');
31const path = require('path');
32const fixtures = require('../common/fixtures');
33const { builtinModules } = require('module');
34const publicModules = builtinModules.filter(
35  (lib) => !lib.startsWith('_') && !lib.includes('/'),
36);
37
38const hasInspector = process.features.inspector;
39
40if (!common.isMainThread)
41  common.skip('process.chdir is not available in Workers');
42
43// We have to change the directory to ../fixtures before requiring repl
44// in order to make the tests for completion of node_modules work properly
45// since repl modifies module.paths.
46process.chdir(fixtures.fixturesDir);
47
48const repl = require('repl');
49
50function getNoResultsFunction() {
51  return common.mustSucceed((data) => {
52    assert.deepStrictEqual(data[0], []);
53  });
54}
55
56const works = [['inner.one'], 'inner.o'];
57const putIn = new ArrayStream();
58const testMe = repl.start({
59  prompt: '',
60  input: putIn,
61  output: process.stdout,
62  allowBlockingCompletions: true
63});
64
65// Some errors are passed to the domain, but do not callback
66testMe._domain.on('error', assert.ifError);
67
68// Tab Complete will not break in an object literal
69putIn.run([
70  'var inner = {',
71  'one:1',
72]);
73testMe.complete('inner.o', getNoResultsFunction());
74
75testMe.complete('console.lo', common.mustCall(function(error, data) {
76  assert.deepStrictEqual(data, [['console.log'], 'console.lo']);
77}));
78
79testMe.complete('console?.lo', common.mustCall((error, data) => {
80  assert.deepStrictEqual(data, [['console?.log'], 'console?.lo']);
81}));
82
83testMe.complete('console?.zzz', common.mustCall((error, data) => {
84  assert.deepStrictEqual(data, [[], 'console?.zzz']);
85}));
86
87testMe.complete('console?.', common.mustCall((error, data) => {
88  assert(data[0].includes('console?.log'));
89  assert.strictEqual(data[1], 'console?.');
90}));
91
92// Tab Complete will return globally scoped variables
93putIn.run(['};']);
94testMe.complete('inner.o', common.mustCall(function(error, data) {
95  assert.deepStrictEqual(data, works);
96}));
97
98putIn.run(['.clear']);
99
100// Tab Complete will not break in an ternary operator with ()
101putIn.run([
102  'var inner = ( true ',
103  '?',
104  '{one: 1} : ',
105]);
106testMe.complete('inner.o', getNoResultsFunction());
107
108putIn.run(['.clear']);
109
110// Tab Complete will return a simple local variable
111putIn.run([
112  'var top = function() {',
113  'var inner = {one:1};',
114]);
115testMe.complete('inner.o', getNoResultsFunction());
116
117// When you close the function scope tab complete will not return the
118// locally scoped variable
119putIn.run(['};']);
120testMe.complete('inner.o', getNoResultsFunction());
121
122putIn.run(['.clear']);
123
124// Tab Complete will return a complex local variable
125putIn.run([
126  'var top = function() {',
127  'var inner = {',
128  ' one:1',
129  '};',
130]);
131testMe.complete('inner.o', getNoResultsFunction());
132
133putIn.run(['.clear']);
134
135// Tab Complete will return a complex local variable even if the function
136// has parameters
137putIn.run([
138  'var top = function(one, two) {',
139  'var inner = {',
140  ' one:1',
141  '};',
142]);
143testMe.complete('inner.o', getNoResultsFunction());
144
145putIn.run(['.clear']);
146
147// Tab Complete will return a complex local variable even if the
148// scope is nested inside an immediately executed function
149putIn.run([
150  'var top = function() {',
151  '(function test () {',
152  'var inner = {',
153  ' one:1',
154  '};',
155]);
156testMe.complete('inner.o', getNoResultsFunction());
157
158putIn.run(['.clear']);
159
160// The definition has the params and { on a separate line.
161putIn.run([
162  'var top = function() {',
163  'r = function test (',
164  ' one, two) {',
165  'var inner = {',
166  ' one:1',
167  '};',
168]);
169testMe.complete('inner.o', getNoResultsFunction());
170
171putIn.run(['.clear']);
172
173// Currently does not work, but should not break, not the {
174putIn.run([
175  'var top = function() {',
176  'r = function test ()',
177  '{',
178  'var inner = {',
179  ' one:1',
180  '};',
181]);
182testMe.complete('inner.o', getNoResultsFunction());
183
184putIn.run(['.clear']);
185
186// Currently does not work, but should not break
187putIn.run([
188  'var top = function() {',
189  'r = function test (',
190  ')',
191  '{',
192  'var inner = {',
193  ' one:1',
194  '};',
195]);
196testMe.complete('inner.o', getNoResultsFunction());
197
198putIn.run(['.clear']);
199
200// Make sure tab completion works on non-Objects
201putIn.run([
202  'var str = "test";',
203]);
204testMe.complete('str.len', common.mustCall(function(error, data) {
205  assert.deepStrictEqual(data, [['str.length'], 'str.len']);
206}));
207
208putIn.run(['.clear']);
209
210// Tab completion should not break on spaces
211const spaceTimeout = setTimeout(function() {
212  throw new Error('timeout');
213}, 1000);
214
215testMe.complete(' ', common.mustSucceed((data) => {
216  assert.strictEqual(data[1], '');
217  assert.ok(data[0].includes('globalThis'));
218  clearTimeout(spaceTimeout);
219}));
220
221// Tab completion should pick up the global "toString" object, and
222// any other properties up the "global" object's prototype chain
223testMe.complete('toSt', common.mustCall(function(error, data) {
224  assert.deepStrictEqual(data, [['toString'], 'toSt']);
225}));
226
227// Own properties should shadow properties on the prototype
228putIn.run(['.clear']);
229putIn.run([
230  'var x = Object.create(null);',
231  'x.a = 1;',
232  'x.b = 2;',
233  'var y = Object.create(x);',
234  'y.a = 3;',
235  'y.c = 4;',
236]);
237testMe.complete('y.', common.mustCall(function(error, data) {
238  assert.deepStrictEqual(data, [['y.b', '', 'y.a', 'y.c'], 'y.']);
239}));
240
241// Tab complete provides built in libs for require()
242putIn.run(['.clear']);
243
244testMe.complete('require(\'', common.mustCall(function(error, data) {
245  assert.strictEqual(error, null);
246  publicModules.forEach((lib) => {
247    assert(
248      data[0].includes(lib) && data[0].includes(`node:${lib}`),
249      `${lib} not found`
250    );
251  });
252  const newModule = 'foobar';
253  assert(!builtinModules.includes(newModule));
254  repl.builtinModules.push(newModule);
255  testMe.complete('require(\'', common.mustCall((_, [modules]) => {
256    assert.strictEqual(data[0].length + 1, modules.length);
257    assert(modules.includes(newModule));
258  }));
259}));
260
261testMe.complete("require\t( 'n", common.mustCall(function(error, data) {
262  assert.strictEqual(error, null);
263  assert.strictEqual(data.length, 2);
264  assert.strictEqual(data[1], 'n');
265  // require(...) completions include `node:`-prefixed modules:
266  publicModules.forEach((lib, index) =>
267    assert.strictEqual(data[0][index], `node:${lib}`));
268  assert.strictEqual(data[0][publicModules.length], '');
269  // There is only one Node.js module that starts with n:
270  assert.strictEqual(data[0][publicModules.length + 1], 'net');
271  assert.strictEqual(data[0][publicModules.length + 2], '');
272  // It's possible to pick up non-core modules too
273  data[0].slice(publicModules.length + 3).forEach((completion) => {
274    assert.match(completion, /^n/);
275  });
276}));
277
278{
279  const expected = ['@nodejsscope', '@nodejsscope/'];
280  // Require calls should handle all types of quotation marks.
281  for (const quotationMark of ["'", '"', '`']) {
282    putIn.run(['.clear']);
283    testMe.complete('require(`@nodejs', common.mustCall((err, data) => {
284      assert.strictEqual(err, null);
285      assert.deepStrictEqual(data, [expected, '@nodejs']);
286    }));
287
288    putIn.run(['.clear']);
289    // Completions should not be greedy in case the quotation ends.
290    const input = `require(${quotationMark}@nodejsscope${quotationMark}`;
291    testMe.complete(input, common.mustCall((err, data) => {
292      assert.strictEqual(err, null);
293      assert.deepStrictEqual(data, [[], undefined]);
294    }));
295  }
296}
297
298{
299  putIn.run(['.clear']);
300  // Completions should find modules and handle whitespace after the opening
301  // bracket.
302  testMe.complete('require \t("no_ind', common.mustCall((err, data) => {
303    assert.strictEqual(err, null);
304    assert.deepStrictEqual(data, [['no_index', 'no_index/'], 'no_ind']);
305  }));
306}
307
308// Test tab completion for require() relative to the current directory
309{
310  putIn.run(['.clear']);
311
312  const cwd = process.cwd();
313  process.chdir(__dirname);
314
315  ['require(\'.', 'require(".'].forEach((input) => {
316    testMe.complete(input, common.mustCall((err, data) => {
317      assert.strictEqual(err, null);
318      assert.strictEqual(data.length, 2);
319      assert.strictEqual(data[1], '.');
320      assert.strictEqual(data[0].length, 2);
321      assert.ok(data[0].includes('./'));
322      assert.ok(data[0].includes('../'));
323    }));
324  });
325
326  ['require(\'..', 'require("..'].forEach((input) => {
327    testMe.complete(input, common.mustCall((err, data) => {
328      assert.strictEqual(err, null);
329      assert.deepStrictEqual(data, [['../'], '..']);
330    }));
331  });
332
333  ['./', './test-'].forEach((path) => {
334    [`require('${path}`, `require("${path}`].forEach((input) => {
335      testMe.complete(input, common.mustCall((err, data) => {
336        assert.strictEqual(err, null);
337        assert.strictEqual(data.length, 2);
338        assert.strictEqual(data[1], path);
339        assert.ok(data[0].includes('./test-repl-tab-complete'));
340      }));
341    });
342  });
343
344  ['../parallel/', '../parallel/test-'].forEach((path) => {
345    [`require('${path}`, `require("${path}`].forEach((input) => {
346      testMe.complete(input, common.mustCall((err, data) => {
347        assert.strictEqual(err, null);
348        assert.strictEqual(data.length, 2);
349        assert.strictEqual(data[1], path);
350        assert.ok(data[0].includes('../parallel/test-repl-tab-complete'));
351      }));
352    });
353  });
354
355  {
356    const path = '../fixtures/repl-folder-extensions/f';
357    testMe.complete(`require('${path}`, common.mustSucceed((data) => {
358      assert.strictEqual(data.length, 2);
359      assert.strictEqual(data[1], path);
360      assert.ok(data[0].includes('../fixtures/repl-folder-extensions/foo.js'));
361    }));
362  }
363
364  process.chdir(cwd);
365}
366
367// Make sure tab completion works on context properties
368putIn.run(['.clear']);
369
370putIn.run([
371  'var custom = "test";',
372]);
373testMe.complete('cus', common.mustCall(function(error, data) {
374  assert.deepStrictEqual(data, [['custom'], 'cus']);
375}));
376
377// Make sure tab completion doesn't crash REPL with half-baked proxy objects.
378// See: https://github.com/nodejs/node/issues/2119
379putIn.run(['.clear']);
380
381putIn.run([
382  'var proxy = new Proxy({}, {ownKeys: () => { throw new Error(); }});',
383]);
384
385testMe.complete('proxy.', common.mustCall(function(error, data) {
386  assert.strictEqual(error, null);
387  assert(Array.isArray(data));
388}));
389
390// Make sure tab completion does not include integer members of an Array
391putIn.run(['.clear']);
392
393putIn.run(['var ary = [1,2,3];']);
394testMe.complete('ary.', common.mustCall(function(error, data) {
395  assert.strictEqual(data[0].includes('ary.0'), false);
396  assert.strictEqual(data[0].includes('ary.1'), false);
397  assert.strictEqual(data[0].includes('ary.2'), false);
398}));
399
400// Make sure tab completion does not include integer keys in an object
401putIn.run(['.clear']);
402putIn.run(['var obj = {1:"a","1a":"b",a:"b"};']);
403
404testMe.complete('obj.', common.mustCall(function(error, data) {
405  assert.strictEqual(data[0].includes('obj.1'), false);
406  assert.strictEqual(data[0].includes('obj.1a'), false);
407  assert(data[0].includes('obj.a'));
408}));
409
410// Don't try to complete results of non-simple expressions
411putIn.run(['.clear']);
412putIn.run(['function a() {}']);
413
414testMe.complete('a().b.', getNoResultsFunction());
415
416// Works when prefixed with spaces
417putIn.run(['.clear']);
418putIn.run(['var obj = {1:"a","1a":"b",a:"b"};']);
419
420testMe.complete(' obj.', common.mustCall((error, data) => {
421  assert.strictEqual(data[0].includes('obj.1'), false);
422  assert.strictEqual(data[0].includes('obj.1a'), false);
423  assert(data[0].includes('obj.a'));
424}));
425
426// Works inside assignments
427putIn.run(['.clear']);
428
429testMe.complete('var log = console.lo', common.mustCall((error, data) => {
430  assert.deepStrictEqual(data, [['console.log'], 'console.lo']);
431}));
432
433// Tab completion for defined commands
434putIn.run(['.clear']);
435
436testMe.complete('.b', common.mustCall((error, data) => {
437  assert.deepStrictEqual(data, [['break'], 'b']);
438}));
439putIn.run(['.clear']);
440putIn.run(['var obj = {"hello, world!": "some string", "key": 123}']);
441testMe.complete('obj.', common.mustCall((error, data) => {
442  assert.strictEqual(data[0].includes('obj.hello, world!'), false);
443  assert(data[0].includes('obj.key'));
444}));
445
446// Make sure tab completion does not include __defineSetter__ and friends.
447putIn.run(['.clear']);
448
449putIn.run(['var obj = {};']);
450testMe.complete('obj.', common.mustCall(function(error, data) {
451  assert.strictEqual(data[0].includes('obj.__defineGetter__'), false);
452  assert.strictEqual(data[0].includes('obj.__defineSetter__'), false);
453  assert.strictEqual(data[0].includes('obj.__lookupGetter__'), false);
454  assert.strictEqual(data[0].includes('obj.__lookupSetter__'), false);
455  assert.strictEqual(data[0].includes('obj.__proto__'), true);
456}));
457
458// Tab completion for files/directories
459{
460  putIn.run(['.clear']);
461  process.chdir(__dirname);
462
463  const readFileSyncs = ['fs.readFileSync("', 'fs.promises.readFileSync("'];
464  if (!common.isWindows) {
465    readFileSyncs.forEach((readFileSync) => {
466      const fixturePath = `${readFileSync}../fixtures/test-repl-tab-completion`;
467      testMe.complete(fixturePath, common.mustCall((err, data) => {
468        assert.strictEqual(err, null);
469        assert.ok(data[0][0].includes('.hiddenfiles'));
470        assert.ok(data[0][1].includes('hellorandom.txt'));
471        assert.ok(data[0][2].includes('helloworld.js'));
472      }));
473
474      testMe.complete(`${fixturePath}/hello`,
475                      common.mustCall((err, data) => {
476                        assert.strictEqual(err, null);
477                        assert.ok(data[0][0].includes('hellorandom.txt'));
478                        assert.ok(data[0][1].includes('helloworld.js'));
479                      })
480      );
481
482      testMe.complete(`${fixturePath}/.h`,
483                      common.mustCall((err, data) => {
484                        assert.strictEqual(err, null);
485                        assert.ok(data[0][0].includes('.hiddenfiles'));
486                      })
487      );
488
489      testMe.complete(`${readFileSync}./xxxRandom/random`,
490                      common.mustCall((err, data) => {
491                        assert.strictEqual(err, null);
492                        assert.strictEqual(data[0].length, 0);
493                      })
494      );
495
496      const testPath = fixturePath.slice(0, -1);
497      testMe.complete(testPath, common.mustCall((err, data) => {
498        assert.strictEqual(err, null);
499        assert.ok(data[0][0].includes('test-repl-tab-completion'));
500        assert.strictEqual(
501          data[1],
502          path.basename(testPath)
503        );
504      }));
505    });
506  }
507}
508
509[
510  Array,
511  Buffer,
512
513  Uint8Array,
514  Uint16Array,
515  Uint32Array,
516
517  Uint8ClampedArray,
518  Int8Array,
519  Int16Array,
520  Int32Array,
521  Float32Array,
522  Float64Array,
523].forEach((type) => {
524  putIn.run(['.clear']);
525
526  if (type === Array) {
527    putIn.run([
528      'var ele = [];',
529      'for (let i = 0; i < 1e6 + 1; i++) ele[i] = 0;',
530      'ele.biu = 1;',
531    ]);
532  } else if (type === Buffer) {
533    putIn.run(['var ele = Buffer.alloc(1e6 + 1); ele.biu = 1;']);
534  } else {
535    putIn.run([`var ele = new ${type.name}(1e6 + 1); ele.biu = 1;`]);
536  }
537
538  hijackStderr(common.mustNotCall());
539  testMe.complete('ele.', common.mustCall((err, data) => {
540    restoreStderr();
541    assert.ifError(err);
542
543    const ele = (type === Array) ?
544      [] :
545      (type === Buffer ?
546        Buffer.alloc(0) :
547        new type(0));
548
549    assert.strictEqual(data[0].includes('ele.biu'), true);
550
551    data[0].forEach((key) => {
552      if (!key || key === 'ele.biu') return;
553      assert.notStrictEqual(ele[key.substr(4)], undefined);
554    });
555  }));
556});
557
558// check Buffer.prototype.length not crashing.
559// Refs: https://github.com/nodejs/node/pull/11961
560putIn.run(['.clear']);
561testMe.complete('Buffer.prototype.', common.mustCall());
562
563const testNonGlobal = repl.start({
564  input: putIn,
565  output: putIn,
566  useGlobal: false
567});
568
569const builtins = [['Infinity', 'Int16Array', 'Int32Array',
570                   'Int8Array'], 'I'];
571
572if (common.hasIntl) {
573  builtins[0].push('Intl');
574}
575testNonGlobal.complete('I', common.mustCall((error, data) => {
576  assert.deepStrictEqual(data, builtins);
577}));
578
579// To test custom completer function.
580// Sync mode.
581const customCompletions = 'aaa aa1 aa2 bbb bb1 bb2 bb3 ccc ddd eee'.split(' ');
582const testCustomCompleterSyncMode = repl.start({
583  prompt: '',
584  input: putIn,
585  output: putIn,
586  completer: function completer(line) {
587    const hits = customCompletions.filter((c) => c.startsWith(line));
588    // Show all completions if none found.
589    return [hits.length ? hits : customCompletions, line];
590  }
591});
592
593// On empty line should output all the custom completions
594// without complete anything.
595testCustomCompleterSyncMode.complete('', common.mustCall((error, data) => {
596  assert.deepStrictEqual(data, [
597    customCompletions,
598    '',
599  ]);
600}));
601
602// On `a` should output `aaa aa1 aa2` and complete until `aa`.
603testCustomCompleterSyncMode.complete('a', common.mustCall((error, data) => {
604  assert.deepStrictEqual(data, [
605    'aaa aa1 aa2'.split(' '),
606    'a',
607  ]);
608}));
609
610// To test custom completer function.
611// Async mode.
612const testCustomCompleterAsyncMode = repl.start({
613  prompt: '',
614  input: putIn,
615  output: putIn,
616  completer: function completer(line, callback) {
617    const hits = customCompletions.filter((c) => c.startsWith(line));
618    // Show all completions if none found.
619    callback(null, [hits.length ? hits : customCompletions, line]);
620  }
621});
622
623// On empty line should output all the custom completions
624// without complete anything.
625testCustomCompleterAsyncMode.complete('', common.mustCall((error, data) => {
626  assert.deepStrictEqual(data, [
627    customCompletions,
628    '',
629  ]);
630}));
631
632// On `a` should output `aaa aa1 aa2` and complete until `aa`.
633testCustomCompleterAsyncMode.complete('a', common.mustCall((error, data) => {
634  assert.deepStrictEqual(data, [
635    'aaa aa1 aa2'.split(' '),
636    'a',
637  ]);
638}));
639
640// Tab completion in editor mode
641const editorStream = new ArrayStream();
642const editor = repl.start({
643  stream: editorStream,
644  terminal: true,
645  useColors: false
646});
647
648editorStream.run(['.clear']);
649editorStream.run(['.editor']);
650
651editor.completer('Uin', common.mustCall((error, data) => {
652  assert.deepStrictEqual(data, [['Uint'], 'Uin']);
653}));
654
655editorStream.run(['.clear']);
656editorStream.run(['.editor']);
657
658editor.completer('var log = console.l', common.mustCall((error, data) => {
659  assert.deepStrictEqual(data, [['console.log'], 'console.l']);
660}));
661
662{
663  // Tab completion of lexically scoped variables
664  const stream = new ArrayStream();
665  const testRepl = repl.start({ stream });
666
667  stream.run([`
668    let lexicalLet = true;
669    const lexicalConst = true;
670    class lexicalKlass {}
671  `]);
672
673  ['Let', 'Const', 'Klass'].forEach((type) => {
674    const query = `lexical${type[0]}`;
675    const expected = hasInspector ? [[`lexical${type}`], query] :
676      [[], `lexical${type[0]}`];
677    testRepl.complete(query, common.mustCall((error, data) => {
678      assert.deepStrictEqual(data, expected);
679    }));
680  });
681}
682