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