diff --git a/lib/repl.js b/lib/repl.js index 369c12852b42b2..65f86cb2c17a25 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -1111,12 +1111,28 @@ REPLServer.prototype.complete = function() { this.completer.apply(this, arguments); }; -function gracefulOperation(fn, args, alternative) { +function gracefulReaddir(...args) { try { - return fn(...args); - } catch { - return alternative; + return fs.readdirSync(...args); + } catch {} +} + +function completeFSFunctions(line) { + let baseName = ''; + let filePath = line.match(fsAutoCompleteRE)[1]; + let fileList = gracefulReaddir(filePath, { withFileTypes: true }); + + if (!fileList) { + baseName = path.basename(filePath); + filePath = path.dirname(filePath); + fileList = gracefulReaddir(filePath, { withFileTypes: true }) || []; } + + const completions = fileList + .filter((dirent) => dirent.name.startsWith(baseName)) + .map((d) => d.name); + + return [[completions], baseName]; } // Provide a list of completions for the given leading text. This is @@ -1145,8 +1161,6 @@ function complete(line, callback) { if (completeOn.length) { filter = completeOn; } - - completionGroupsLoaded(); } else if (requireRE.test(line)) { // require('...') const extensions = ObjectKeys(this.context.require.extensions); @@ -1173,11 +1187,7 @@ function complete(line, callback) { for (let dir of paths) { dir = path.resolve(dir, subdir); - const dirents = gracefulOperation( - fs.readdirSync, - [dir, { withFileTypes: true }], - [] - ); + const dirents = gracefulReaddir(dir, { withFileTypes: true }) || []; for (const dirent of dirents) { if (versionedFileNamesRe.test(dirent.name) || dirent.name === '.npm') { // Exclude versioned names that 'npm' installs. @@ -1193,7 +1203,7 @@ function complete(line, callback) { } group.push(`${subdir}${dirent.name}/`); const absolute = path.resolve(dir, dirent.name); - const subfiles = gracefulOperation(fs.readdirSync, [absolute], []); + const subfiles = gracefulReaddir(absolute) || []; for (const subfile of subfiles) { if (indexes.includes(subfile)) { group.push(`${subdir}${dirent.name}`); @@ -1209,31 +1219,8 @@ function complete(line, callback) { if (!subdir) { completionGroups.push(_builtinLibs); } - - completionGroupsLoaded(); } else if (fsAutoCompleteRE.test(line)) { - filter = ''; - let filePath = line.match(fsAutoCompleteRE)[1]; - let fileList; - - try { - fileList = fs.readdirSync(filePath, { withFileTypes: true }); - completionGroups.push(fileList.map((dirent) => dirent.name)); - completeOn = ''; - } catch { - try { - const baseName = path.basename(filePath); - filePath = path.dirname(filePath); - fileList = fs.readdirSync(filePath, { withFileTypes: true }); - const filteredValue = fileList.filter((d) => - d.name.startsWith(baseName)) - .map((d) => d.name); - completionGroups.push(filteredValue); - completeOn = baseName; - } catch {} - } - - completionGroupsLoaded(); + [completionGroups, completeOn] = completeFSFunctions(line); // Handle variable member lookup. // We support simple chained expressions like the following (no function // calls, etc.). That is for simplicity and also because we *eval* that @@ -1245,25 +1232,22 @@ function complete(line, callback) { // foo<|> # all scope vars with filter 'foo' // foo.<|> # completions for 'foo' with filter '' } else if (line.length === 0 || /\w|\.|\$/.test(line[line.length - 1])) { - const match = simpleExpressionRE.exec(line); + const [match] = simpleExpressionRE.exec(line) || ['']; if (line.length !== 0 && !match) { completionGroupsLoaded(); return; } - let expr; - completeOn = (match ? match[0] : ''); - if (line.length === 0) { - expr = ''; - } else if (line[line.length - 1] === '.') { - expr = match[0].slice(0, match[0].length - 1); - } else { - const bits = match[0].split('.'); + let expr = ''; + completeOn = match; + if (line.endsWith('.')) { + expr = match.slice(0, -1); + } else if (line.length !== 0) { + const bits = match.split('.'); filter = bits.pop(); expr = bits.join('.'); } // Resolve expr and get its completions. - const memberGroups = []; if (!expr) { // Get global vars synchronously completionGroups.push(getGlobalLexicalScopeNames(this[kContextId])); @@ -1284,39 +1268,34 @@ function complete(line, callback) { } let chaining = '.'; - if (expr[expr.length - 1] === '?') { + if (expr.endsWith('?')) { expr = expr.slice(0, -1); chaining = '?.'; } + const memberGroups = []; const evalExpr = `try { ${expr} } catch {}`; this.eval(evalExpr, this.context, 'repl', (e, obj) => { - if (obj != null) { - if (typeof obj === 'object' || typeof obj === 'function') { - try { - memberGroups.push(filteredOwnPropertyNames(obj)); - } catch { - // Probably a Proxy object without `getOwnPropertyNames` trap. - // We simply ignore it here, as we don't want to break the - // autocompletion. Fixes the bug - // https://github.com/nodejs/node/issues/2119 - } + try { + let p; + if ((typeof obj === 'object' && obj !== null) || + typeof obj === 'function') { + memberGroups.push(filteredOwnPropertyNames(obj)); + p = ObjectGetPrototypeOf(obj); + } else { + p = obj.constructor ? obj.constructor.prototype : null; } - // Works for non-objects - try { - let p; - if (typeof obj === 'object' || typeof obj === 'function') { - p = ObjectGetPrototypeOf(obj); - } else { - p = obj.constructor ? obj.constructor.prototype : null; - } - // Circular refs possible? Let's guard against that. - let sentinel = 5; - while (p !== null && sentinel-- !== 0) { - memberGroups.push(filteredOwnPropertyNames(p)); - p = ObjectGetPrototypeOf(p); - } - } catch {} + // Circular refs possible? Let's guard against that. + let sentinel = 5; + while (p !== null && sentinel-- !== 0) { + memberGroups.push(filteredOwnPropertyNames(p)); + p = ObjectGetPrototypeOf(p); + } + } catch { + // Maybe a Proxy object without `getOwnPropertyNames` trap. + // We simply ignore it here, as we don't want to break the + // autocompletion. Fixes the bug + // https://github.com/nodejs/node/issues/2119 } if (memberGroups.length) { @@ -1331,21 +1310,21 @@ function complete(line, callback) { completionGroupsLoaded(); }); - } else { - completionGroupsLoaded(); + return; } + return completionGroupsLoaded(); + // Will be called when all completionGroups are in place // Useful for async autocompletion function completionGroupsLoaded() { // Filter, sort (within each group), uniq and merge the completion groups. if (completionGroups.length && filter) { const newCompletionGroups = []; - for (let i = 0; i < completionGroups.length; i++) { - group = completionGroups[i] - .filter((elem) => elem.indexOf(filter) === 0); - if (group.length) { - newCompletionGroups.push(group); + for (const group of completionGroups) { + const filteredGroup = group.filter((str) => str.startsWith(filter)); + if (filteredGroup.length) { + newCompletionGroups.push(filteredGroup); } } completionGroups = newCompletionGroups;