• 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 hasInspector = process.features.inspector;
34
35if (!common.isMainThread)
36  common.skip('process.chdir is not available in Workers');
37
38// We have to change the directory to ../fixtures before requiring repl
39// in order to make the tests for completion of node_modules work properly
40// since repl modifies module.paths.
41process.chdir(fixtures.fixturesDir);
42
43const repl = require('repl');
44
45function getNoResultsFunction() {
46  return common.mustCall((err, data) => {
47    assert.ifError(err);
48    assert.deepStrictEqual(data[0], []);
49  });
50}
51
52const works = [['inner.one'], 'inner.o'];
53const putIn = new ArrayStream();
54const testMe = repl.start('', putIn);
55
56// Some errors are passed to the domain, but do not callback
57testMe._domain.on('error', assert.ifError);
58
59// Tab Complete will not break in an object literal
60putIn.run([
61  'var inner = {',
62  'one:1'
63]);
64testMe.complete('inner.o', getNoResultsFunction());
65
66testMe.complete('console.lo', common.mustCall(function(error, data) {
67  assert.deepStrictEqual(data, [['console.log'], 'console.lo']);
68}));
69
70// Tab Complete will return globally scoped variables
71putIn.run(['};']);
72testMe.complete('inner.o', common.mustCall(function(error, data) {
73  assert.deepStrictEqual(data, works);
74}));
75
76putIn.run(['.clear']);
77
78// Tab Complete will not break in an ternary operator with ()
79putIn.run([
80  'var inner = ( true ',
81  '?',
82  '{one: 1} : '
83]);
84testMe.complete('inner.o', getNoResultsFunction());
85
86putIn.run(['.clear']);
87
88// Tab Complete will return a simple local variable
89putIn.run([
90  'var top = function() {',
91  'var inner = {one:1};'
92]);
93testMe.complete('inner.o', getNoResultsFunction());
94
95// When you close the function scope tab complete will not return the
96// locally scoped variable
97putIn.run(['};']);
98testMe.complete('inner.o', getNoResultsFunction());
99
100putIn.run(['.clear']);
101
102// Tab Complete will return a complex local variable
103putIn.run([
104  'var top = function() {',
105  'var inner = {',
106  ' one:1',
107  '};'
108]);
109testMe.complete('inner.o', getNoResultsFunction());
110
111putIn.run(['.clear']);
112
113// Tab Complete will return a complex local variable even if the function
114// has parameters
115putIn.run([
116  'var top = function(one, two) {',
117  'var inner = {',
118  ' one:1',
119  '};'
120]);
121testMe.complete('inner.o', getNoResultsFunction());
122
123putIn.run(['.clear']);
124
125// Tab Complete will return a complex local variable even if the
126// scope is nested inside an immediately executed function
127putIn.run([
128  'var top = function() {',
129  '(function test () {',
130  'var inner = {',
131  ' one:1',
132  '};'
133]);
134testMe.complete('inner.o', getNoResultsFunction());
135
136putIn.run(['.clear']);
137
138// The definition has the params and { on a separate line.
139putIn.run([
140  'var top = function() {',
141  'r = function test (',
142  ' one, two) {',
143  'var inner = {',
144  ' one:1',
145  '};'
146]);
147testMe.complete('inner.o', getNoResultsFunction());
148
149putIn.run(['.clear']);
150
151// Currently does not work, but should not break, not the {
152putIn.run([
153  'var top = function() {',
154  'r = function test ()',
155  '{',
156  'var inner = {',
157  ' one:1',
158  '};'
159]);
160testMe.complete('inner.o', getNoResultsFunction());
161
162putIn.run(['.clear']);
163
164// Currently does not work, but should not break
165putIn.run([
166  'var top = function() {',
167  'r = function test (',
168  ')',
169  '{',
170  'var inner = {',
171  ' one:1',
172  '};'
173]);
174testMe.complete('inner.o', getNoResultsFunction());
175
176putIn.run(['.clear']);
177
178// Make sure tab completion works on non-Objects
179putIn.run([
180  'var str = "test";'
181]);
182testMe.complete('str.len', common.mustCall(function(error, data) {
183  assert.deepStrictEqual(data, [['str.length'], 'str.len']);
184}));
185
186putIn.run(['.clear']);
187
188// Tab completion should not break on spaces
189const spaceTimeout = setTimeout(function() {
190  throw new Error('timeout');
191}, 1000);
192
193testMe.complete(' ', common.mustCall(function(error, data) {
194  assert.ifError(error);
195  assert.strictEqual(data[1], '');
196  assert.ok(data[0].includes('globalThis'));
197  clearTimeout(spaceTimeout);
198}));
199
200// Tab completion should pick up the global "toString" object, and
201// any other properties up the "global" object's prototype chain
202testMe.complete('toSt', common.mustCall(function(error, data) {
203  assert.deepStrictEqual(data, [['toString'], 'toSt']);
204}));
205
206// Own properties should shadow properties on the prototype
207putIn.run(['.clear']);
208putIn.run([
209  'var x = Object.create(null);',
210  'x.a = 1;',
211  'x.b = 2;',
212  'var y = Object.create(x);',
213  'y.a = 3;',
214  'y.c = 4;'
215]);
216testMe.complete('y.', common.mustCall(function(error, data) {
217  assert.deepStrictEqual(data, [['y.b', '', 'y.a', 'y.c'], 'y.']);
218}));
219
220// Tab complete provides built in libs for require()
221putIn.run(['.clear']);
222
223testMe.complete('require(\'', common.mustCall(function(error, data) {
224  assert.strictEqual(error, null);
225  repl._builtinLibs.forEach(function(lib) {
226    assert(data[0].includes(lib), `${lib} not found`);
227  });
228}));
229
230testMe.complete('require(\'n', common.mustCall(function(error, data) {
231  assert.strictEqual(error, null);
232  assert.strictEqual(data.length, 2);
233  assert.strictEqual(data[1], 'n');
234  assert(data[0].includes('net'));
235  // It's possible to pick up non-core modules too
236  data[0].forEach(function(completion) {
237    if (completion)
238      assert(/^n/.test(completion));
239  });
240}));
241
242{
243  const expected = ['@nodejsscope', '@nodejsscope/'];
244  putIn.run(['.clear']);
245  testMe.complete('require(\'@nodejs', common.mustCall((err, data) => {
246    assert.strictEqual(err, null);
247    assert.deepStrictEqual(data, [expected, '@nodejs']);
248  }));
249}
250
251// Test tab completion for require() relative to the current directory
252{
253  putIn.run(['.clear']);
254
255  const cwd = process.cwd();
256  process.chdir(__dirname);
257
258  ['require(\'.', 'require(".'].forEach((input) => {
259    testMe.complete(input, common.mustCall((err, data) => {
260      assert.strictEqual(err, null);
261      assert.strictEqual(data.length, 2);
262      assert.strictEqual(data[1], '.');
263      assert.strictEqual(data[0].length, 2);
264      assert.ok(data[0].includes('./'));
265      assert.ok(data[0].includes('../'));
266    }));
267  });
268
269  ['require(\'..', 'require("..'].forEach((input) => {
270    testMe.complete(input, common.mustCall((err, data) => {
271      assert.strictEqual(err, null);
272      assert.deepStrictEqual(data, [['../'], '..']);
273    }));
274  });
275
276  ['./', './test-'].forEach((path) => {
277    [`require('${path}`, `require("${path}`].forEach((input) => {
278      testMe.complete(input, common.mustCall((err, data) => {
279        assert.strictEqual(err, null);
280        assert.strictEqual(data.length, 2);
281        assert.strictEqual(data[1], path);
282        assert.ok(data[0].includes('./test-repl-tab-complete'));
283      }));
284    });
285  });
286
287  ['../parallel/', '../parallel/test-'].forEach((path) => {
288    [`require('${path}`, `require("${path}`].forEach((input) => {
289      testMe.complete(input, common.mustCall((err, data) => {
290        assert.strictEqual(err, null);
291        assert.strictEqual(data.length, 2);
292        assert.strictEqual(data[1], path);
293        assert.ok(data[0].includes('../parallel/test-repl-tab-complete'));
294      }));
295    });
296  });
297
298  {
299    const path = '../fixtures/repl-folder-extensions/f';
300    testMe.complete(`require('${path}`, common.mustCall((err, data) => {
301      assert.ifError(err);
302      assert.strictEqual(data.length, 2);
303      assert.strictEqual(data[1], path);
304      assert.ok(data[0].includes('../fixtures/repl-folder-extensions/foo.js'));
305    }));
306  }
307
308  process.chdir(cwd);
309}
310
311// Make sure tab completion works on context properties
312putIn.run(['.clear']);
313
314putIn.run([
315  'var custom = "test";'
316]);
317testMe.complete('cus', common.mustCall(function(error, data) {
318  assert.deepStrictEqual(data, [['custom'], 'cus']);
319}));
320
321// Make sure tab completion doesn't crash REPL with half-baked proxy objects.
322// See: https://github.com/nodejs/node/issues/2119
323putIn.run(['.clear']);
324
325putIn.run([
326  'var proxy = new Proxy({}, {ownKeys: () => { throw new Error(); }});'
327]);
328
329testMe.complete('proxy.', common.mustCall(function(error, data) {
330  assert.strictEqual(error, null);
331  assert(Array.isArray(data));
332}));
333
334// Make sure tab completion does not include integer members of an Array
335putIn.run(['.clear']);
336
337putIn.run(['var ary = [1,2,3];']);
338testMe.complete('ary.', common.mustCall(function(error, data) {
339  assert.strictEqual(data[0].includes('ary.0'), false);
340  assert.strictEqual(data[0].includes('ary.1'), false);
341  assert.strictEqual(data[0].includes('ary.2'), false);
342}));
343
344// Make sure tab completion does not include integer keys in an object
345putIn.run(['.clear']);
346putIn.run(['var obj = {1:"a","1a":"b",a:"b"};']);
347
348testMe.complete('obj.', common.mustCall(function(error, data) {
349  assert.strictEqual(data[0].includes('obj.1'), false);
350  assert.strictEqual(data[0].includes('obj.1a'), false);
351  assert(data[0].includes('obj.a'));
352}));
353
354// Don't try to complete results of non-simple expressions
355putIn.run(['.clear']);
356putIn.run(['function a() {}']);
357
358testMe.complete('a().b.', getNoResultsFunction());
359
360// Works when prefixed with spaces
361putIn.run(['.clear']);
362putIn.run(['var obj = {1:"a","1a":"b",a:"b"};']);
363
364testMe.complete(' obj.', common.mustCall((error, data) => {
365  assert.strictEqual(data[0].includes('obj.1'), false);
366  assert.strictEqual(data[0].includes('obj.1a'), false);
367  assert(data[0].includes('obj.a'));
368}));
369
370// Works inside assignments
371putIn.run(['.clear']);
372
373testMe.complete('var log = console.lo', common.mustCall((error, data) => {
374  assert.deepStrictEqual(data, [['console.log'], 'console.lo']);
375}));
376
377// Tab completion for defined commands
378putIn.run(['.clear']);
379
380testMe.complete('.b', common.mustCall((error, data) => {
381  assert.deepStrictEqual(data, [['break'], 'b']);
382}));
383putIn.run(['.clear']);
384putIn.run(['var obj = {"hello, world!": "some string", "key": 123}']);
385testMe.complete('obj.', common.mustCall((error, data) => {
386  assert.strictEqual(data[0].includes('obj.hello, world!'), false);
387  assert(data[0].includes('obj.key'));
388}));
389
390// Tab completion for files/directories
391{
392  putIn.run(['.clear']);
393  process.chdir(__dirname);
394
395  const readFileSyncs = ['fs.readFileSync("', 'fs.promises.readFileSync("'];
396  if (!common.isWindows) {
397    readFileSyncs.forEach((readFileSync) => {
398      const fixturePath = `${readFileSync}../fixtures/test-repl-tab-completion`;
399      testMe.complete(fixturePath, common.mustCall((err, data) => {
400        assert.strictEqual(err, null);
401        assert.ok(data[0][0].includes('.hiddenfiles'));
402        assert.ok(data[0][1].includes('hellorandom.txt'));
403        assert.ok(data[0][2].includes('helloworld.js'));
404      }));
405
406      testMe.complete(`${fixturePath}/hello`,
407                      common.mustCall((err, data) => {
408                        assert.strictEqual(err, null);
409                        assert.ok(data[0][0].includes('hellorandom.txt'));
410                        assert.ok(data[0][1].includes('helloworld.js'));
411                      })
412      );
413
414      testMe.complete(`${fixturePath}/.h`,
415                      common.mustCall((err, data) => {
416                        assert.strictEqual(err, null);
417                        assert.ok(data[0][0].includes('.hiddenfiles'));
418                      })
419      );
420
421      testMe.complete(`${readFileSync}./xxxRandom/random`,
422                      common.mustCall((err, data) => {
423                        assert.strictEqual(err, null);
424                        assert.strictEqual(data[0].length, 0);
425                      })
426      );
427
428      const testPath = fixturePath.slice(0, -1);
429      testMe.complete(testPath, common.mustCall((err, data) => {
430        assert.strictEqual(err, null);
431        assert.ok(data[0][0].includes('test-repl-tab-completion'));
432        assert.strictEqual(
433          data[1],
434          path.basename(testPath)
435        );
436      }));
437    });
438  }
439}
440
441[
442  Array,
443  Buffer,
444
445  Uint8Array,
446  Uint16Array,
447  Uint32Array,
448
449  Uint8ClampedArray,
450  Int8Array,
451  Int16Array,
452  Int32Array,
453  Float32Array,
454  Float64Array,
455].forEach((type) => {
456  putIn.run(['.clear']);
457
458  if (type === Array) {
459    putIn.run([
460      'var ele = [];',
461      'for (let i = 0; i < 1e6 + 1; i++) ele[i] = 0;',
462      'ele.biu = 1;'
463    ]);
464  } else if (type === Buffer) {
465    putIn.run(['var ele = Buffer.alloc(1e6 + 1); ele.biu = 1;']);
466  } else {
467    putIn.run([`var ele = new ${type.name}(1e6 + 1); ele.biu = 1;`]);
468  }
469
470  hijackStderr(common.mustNotCall());
471  testMe.complete('ele.', common.mustCall((err, data) => {
472    restoreStderr();
473    assert.ifError(err);
474
475    const ele = (type === Array) ?
476      [] :
477      (type === Buffer ?
478        Buffer.alloc(0) :
479        new type(0));
480
481    assert.strictEqual(data[0].includes('ele.biu'), true);
482
483    data[0].forEach((key) => {
484      if (!key || key === 'ele.biu') return;
485      assert.notStrictEqual(ele[key.substr(4)], undefined);
486    });
487  }));
488});
489
490// check Buffer.prototype.length not crashing.
491// Refs: https://github.com/nodejs/node/pull/11961
492putIn.run['.clear'];
493testMe.complete('Buffer.prototype.', common.mustCall());
494
495const testNonGlobal = repl.start({
496  input: putIn,
497  output: putIn,
498  useGlobal: false
499});
500
501const builtins = [['Infinity', 'Int16Array', 'Int32Array',
502                   'Int8Array'], 'I'];
503
504if (common.hasIntl) {
505  builtins[0].push('Intl');
506}
507testNonGlobal.complete('I', common.mustCall((error, data) => {
508  assert.deepStrictEqual(data, builtins);
509}));
510
511// To test custom completer function.
512// Sync mode.
513const customCompletions = 'aaa aa1 aa2 bbb bb1 bb2 bb3 ccc ddd eee'.split(' ');
514const testCustomCompleterSyncMode = repl.start({
515  prompt: '',
516  input: putIn,
517  output: putIn,
518  completer: function completer(line) {
519    const hits = customCompletions.filter((c) => c.startsWith(line));
520    // Show all completions if none found.
521    return [hits.length ? hits : customCompletions, line];
522  }
523});
524
525// On empty line should output all the custom completions
526// without complete anything.
527testCustomCompleterSyncMode.complete('', common.mustCall((error, data) => {
528  assert.deepStrictEqual(data, [
529    customCompletions,
530    ''
531  ]);
532}));
533
534// On `a` should output `aaa aa1 aa2` and complete until `aa`.
535testCustomCompleterSyncMode.complete('a', common.mustCall((error, data) => {
536  assert.deepStrictEqual(data, [
537    'aaa aa1 aa2'.split(' '),
538    'a'
539  ]);
540}));
541
542// To test custom completer function.
543// Async mode.
544const testCustomCompleterAsyncMode = repl.start({
545  prompt: '',
546  input: putIn,
547  output: putIn,
548  completer: function completer(line, callback) {
549    const hits = customCompletions.filter((c) => c.startsWith(line));
550    // Show all completions if none found.
551    callback(null, [hits.length ? hits : customCompletions, line]);
552  }
553});
554
555// On empty line should output all the custom completions
556// without complete anything.
557testCustomCompleterAsyncMode.complete('', common.mustCall((error, data) => {
558  assert.deepStrictEqual(data, [
559    customCompletions,
560    ''
561  ]);
562}));
563
564// On `a` should output `aaa aa1 aa2` and complete until `aa`.
565testCustomCompleterAsyncMode.complete('a', common.mustCall((error, data) => {
566  assert.deepStrictEqual(data, [
567    'aaa aa1 aa2'.split(' '),
568    'a'
569  ]);
570}));
571
572// Tab completion in editor mode
573const editorStream = new ArrayStream();
574const editor = repl.start({
575  stream: editorStream,
576  terminal: true,
577  useColors: false
578});
579
580editorStream.run(['.clear']);
581editorStream.run(['.editor']);
582
583editor.completer('Uin', common.mustCall((error, data) => {
584  assert.deepStrictEqual(data, [['Uint'], 'Uin']);
585}));
586
587editorStream.run(['.clear']);
588editorStream.run(['.editor']);
589
590editor.completer('var log = console.l', common.mustCall((error, data) => {
591  assert.deepStrictEqual(data, [['console.log'], 'console.l']);
592}));
593
594{
595  // Tab completion of lexically scoped variables
596  const stream = new ArrayStream();
597  const testRepl = repl.start({ stream });
598
599  stream.run([`
600    let lexicalLet = true;
601    const lexicalConst = true;
602    class lexicalKlass {}
603  `]);
604
605  ['Let', 'Const', 'Klass'].forEach((type) => {
606    const query = `lexical${type[0]}`;
607    const expected = hasInspector ? [[`lexical${type}`], query] :
608      [[], `lexical${type[0]}`];
609    testRepl.complete(query, common.mustCall((error, data) => {
610      assert.deepStrictEqual(data, expected);
611    }));
612  });
613}
614