Repository: nodejs/node
Pull request: repl: move completion logic to internal module

This PR moves the repl completion logic into its own internal module, my reasoning for proposing this change is that the repl completion logic currently occupies a big chunk of the repl module and it's hard to tell which functions are relevant to it and which are not.

So, also given the fact that the completion logic is pretty self contained, I believe that isolating it will lead to a better, more maintainable and more understandable code structure.

This is also consistent with the repl history which has its own internal module (and also the await module).

Later I am also planning to do other code restructuring/cleanups, but for this PR, in order to make it easy to review I tried to minimize as much as possible code changes and just tried to move things over the new internal completion module. So in this PR I basically only moved code from `lib/repl.js` to `lib/internal/repl/completion.js`, with the only exception being that I moved shared logic into the `lib/internal/repl/utils.js` (and imported it in the other 2 modules).

<!--
Before submitting a pull request, please read:

- the CONTRIBUTING guide at https://github.com/nodejs/node/blob/HEAD/CONTRIBUTING.md
- the commit message formatting guidelines at
  https://github.com/nodejs/node/blob/HEAD/doc/contributing/pull-requests.md#commit-message-guidelines

For code changes:
1. Include tests for any bug fixes or new features.
2. Update documentation if relevant.
3. Ensure that `make -j4 test` (UNIX), or `vcbuild test` (Windows) passes.

If you believe this PR should be highlighted in the Node.js CHANGELOG
please add the `notable-change` label.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.
-->


Changed files:
filename: lib/internal/repl/completion.js
patch: @@ -0,0 +1,801 @@
+'use strict';
+
+const {
+  ArrayPrototypeFilter,
+  ArrayPrototypeForEach,
+  ArrayPrototypeIncludes,
+  ArrayPrototypeJoin,
+  ArrayPrototypeMap,
+  ArrayPrototypePop,
+  ArrayPrototypePush,
+  ArrayPrototypePushApply,
+  ArrayPrototypeShift,
+  ArrayPrototypeSlice,
+  ArrayPrototypeSome,
+  ArrayPrototypeSort,
+  ArrayPrototypeUnshift,
+  ObjectGetOwnPropertyDescriptor,
+  ObjectGetPrototypeOf,
+  ObjectKeys,
+  ReflectApply,
+  RegExpPrototypeExec,
+  SafeSet,
+  StringPrototypeCodePointAt,
+  StringPrototypeEndsWith,
+  StringPrototypeIncludes,
+  StringPrototypeSlice,
+  StringPrototypeSplit,
+  StringPrototypeStartsWith,
+  StringPrototypeToLocaleLowerCase,
+  StringPrototypeTrimStart,
+} = primordials;
+
+const {
+  kContextId,
+  getREPLResourceName,
+  globalBuiltins,
+  getReplBuiltinLibs,
+  fixReplRequire,
+} = require('internal/repl/utils');
+
+const { sendInspectorCommand } = require('internal/util/inspector');
+
+const {
+  isProxy,
+} = require('internal/util/types');
+
+const CJSModule = require('internal/modules/cjs/loader').Module;
+
+const {
+  extensionFormatMap,
+} = require('internal/modules/esm/formats');
+
+const path = require('path');
+const fs = require('fs');
+
+const {
+  constants: {
+    ALL_PROPERTIES,
+    SKIP_SYMBOLS,
+  },
+  getOwnNonIndexProperties,
+} = internalBinding('util');
+
+const {
+  isIdentifierStart,
+  isIdentifierChar,
+  parse: acornParse,
+} = require('internal/deps/acorn/acorn/dist/acorn');
+const acornWalk = require('internal/deps/acorn/acorn-walk/dist/walk');
+
+const importRE = /\bimport\s*\(\s*['"`](([\w@./:-]+\/)?(?:[\w@./:-]*))(?![^'"`])$/;
+const requireRE = /\brequire\s*\(\s*['"`](([\w@./:-]+\/)?(?:[\w@./:-]*))(?![^'"`])$/;
+const fsAutoCompleteRE = /fs(?:\.promises)?\.\s*[a-z][a-zA-Z]+\(\s*["'](.*)/;
+const versionedFileNamesRe = /-\d+\.\d+/;
+
+fixReplRequire(module);
+
+const { BuiltinModule } = require('internal/bootstrap/realm');
+
+const nodeSchemeBuiltinLibs = ArrayPrototypeMap(getReplBuiltinLibs(), (lib) => `node:${lib}`);
+ArrayPrototypeForEach(
+  BuiltinModule.getSchemeOnlyModuleNames(),
+  (lib) => ArrayPrototypePush(nodeSchemeBuiltinLibs, `node:${lib}`),
+);
+
+function isIdentifier(str) {
+  if (str === '') {
+    return false;
+  }
+  const first = StringPrototypeCodePointAt(str, 0);
+  if (!isIdentifierStart(first)) {
+    return false;
+  }
+  const firstLen = first > 0xffff ? 2 : 1;
+  for (let i = firstLen; i < str.length; i += 1) {
+    const cp = StringPrototypeCodePointAt(str, i);
+    if (!isIdentifierChar(cp)) {
+      return false;
+    }
+    if (cp > 0xffff) {
+      i += 1;
+    }
+  }
+  return true;
+}
+
+function isNotLegacyObjectPrototypeMethod(str) {
+  return isIdentifier(str) &&
+    str !== '__defineGetter__' &&
+    str !== '__defineSetter__' &&
+    str !== '__lookupGetter__' &&
+    str !== '__lookupSetter__';
+}
+
+function getGlobalLexicalScopeNames(contextId) {
+  return sendInspectorCommand((session) => {
+    let names = [];
+    session.post('Runtime.globalLexicalScopeNames', {
+      executionContextId: contextId,
+    }, (error, result) => {
+      if (!error) names = result.names;
+    });
+    return names;
+  }, () => []);
+}
+
+function filteredOwnPropertyNames(obj) {
+  if (!obj) return [];
+  // `Object.prototype` is the only non-contrived object that fulfills
+  // `Object.getPrototypeOf(X) === null &&
+  //  Object.getPrototypeOf(Object.getPrototypeOf(X.constructor)) === X`.
+  let isObjectPrototype = false;
+  if (ObjectGetPrototypeOf(obj) === null) {
+    const ctorDescriptor = ObjectGetOwnPropertyDescriptor(obj, 'constructor');
+    if (ctorDescriptor?.value) {
+      const ctorProto = ObjectGetPrototypeOf(ctorDescriptor.value);
+      isObjectPrototype = ctorProto && ObjectGetPrototypeOf(ctorProto) === obj;
+    }
+  }
+  const filter = ALL_PROPERTIES | SKIP_SYMBOLS;
+  return ArrayPrototypeFilter(
+    getOwnNonIndexProperties(obj, filter),
+    isObjectPrototype ? isNotLegacyObjectPrototypeMethod : isIdentifier);
+}
+
+function addCommonWords(completionGroups) {
+  // Only words which do not yet exist as global property should be added to
+  // this list.
+  ArrayPrototypePush(completionGroups, [
+    'async', 'await', 'break', 'case', 'catch', 'const', 'continue',
+    'debugger', 'default', 'delete', 'do', 'else', 'export', 'false',
+    'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof', 'let',
+    'new', 'null', 'return', 'switch', 'this', 'throw', 'true', 'try',
+    'typeof', 'var', 'void', 'while', 'with', 'yield',
+  ]);
+}
+
+function gracefulReaddir(...args) {
+  try {
+    return ReflectApply(fs.readdirSync, null, args);
+  } catch {
+    // Continue regardless of error.
+  }
+}
+
+function completeFSFunctions(match) {
+  let baseName = '';
+  let filePath = match[1];
+  let fileList = gracefulReaddir(filePath, { withFileTypes: true });
+
+  if (!fileList) {
+    baseName = path.basename(filePath);
+    filePath = path.dirname(filePath);
+    fileList = gracefulReaddir(filePath, { withFileTypes: true }) || [];
+  }
+
+  const completions = ArrayPrototypeMap(
+    ArrayPrototypeFilter(
+      fileList,
+      (dirent) => StringPrototypeStartsWith(dirent.name, baseName),
+    ),
+    (d) => d.name,
+  );
+
+  return [[completions], baseName];
+}
+
+// Provide a list of completions for the given leading text. This is
+// given to the readline interface for handling tab completion.
+//
+// Example:
+//  complete('let foo = util.')
+//    -> [['util.print', 'util.debug', 'util.log', 'util.inspect'],
+//        'util.' ]
+//
+// Warning: This evals code like "foo.bar.baz", so it could run property
+// getter code. To avoid potential triggering side-effects with getters the completion
+// logic is skipped when getters or proxies are involved in the expression.
+// (see: https://github.com/nodejs/node/issues/57829).
+function complete(line, callback) {
+  // List of completion lists, one for each inheritance "level"
+  let completionGroups = [];
+  let completeOn, group;
+
+  // Ignore right whitespace. It could change the outcome.
+  line = StringPrototypeTrimStart(line);
+
+  let filter = '';
+
+  let match;
+  // REPL commands (e.g. ".break").
+  if ((match = RegExpPrototypeExec(/^\s*\.(\w*)$/, line)) !== null) {
+    ArrayPrototypePush(completionGroups, ObjectKeys(this.commands));
+    completeOn = match[1];
+    if (completeOn.length) {
+      filter = completeOn;
+    }
+  } else if ((match = RegExpPrototypeExec(requireRE, line)) !== null) {
+    // require('...<Tab>')
+    completeOn = match[1];
+    filter = completeOn;
+    if (this.allowBlockingCompletions) {
+      const subdir = match[2] || '';
+      const extensions = ObjectKeys(CJSModule._extensions);
+      const indexes = ArrayPrototypeMap(extensions,
+                                        (extension) => `index${extension}`);
+      ArrayPrototypePush(indexes, 'package.json', 'index');
+
+      group = [];
+      let paths = [];
+
+      if (completeOn === '.') {
+        group = ['./', '../'];
+      } else if (completeOn === '..') {
+        group = ['../'];
+      } else if (RegExpPrototypeExec(/^\.\.?\//, completeOn) !== null) {
+        paths = [process.cwd()];
+      } else {
+        paths = [];
+        ArrayPrototypePushApply(paths, module.paths);
+        ArrayPrototypePushApply(paths, CJSModule.globalPaths);
+      }
+
+      ArrayPrototypeForEach(paths, (dir) => {
+        dir = path.resolve(dir, subdir);
+        const dirents = gracefulReaddir(dir, { withFileTypes: true }) || [];
+        ArrayPrototypeForEach(dirents, (dirent) => {
+          if (RegExpPrototypeExec(versionedFileNamesRe, dirent.name) !== null ||
+              dirent.name === '.npm') {
+            // Exclude versioned names that 'npm' installs.
+            return;
+          }
+          const extension = path.extname(dirent.name);
+          const base = StringPrototypeSlice(dirent.name, 0, -extension.length);
+          if (!dirent.isDirectory()) {
+            if (StringPrototypeIncludes(extensions, extension) &&
+                (!subdir || base !== 'index')) {
+              ArrayPrototypePush(group, `${subdir}${base}`);
+            }
+            return;
+          }
+          ArrayPrototypePush(group, `${subdir}${dirent.name}/`);
+          const absolute = path.resolve(dir, dirent.name);
+          if (ArrayPrototypeSome(
+            gracefulReaddir(absolute) || [],
+            (subfile) => ArrayPrototypeIncludes(indexes, subfile),
+          )) {
+            ArrayPrototypePush(group, `${subdir}${dirent.name}`);
+          }
+        });
+      });
+      if (group.length) {
+        ArrayPrototypePush(completionGroups, group);
+      }
+    }
+
+    ArrayPrototypePush(completionGroups, getReplBuiltinLibs(), nodeSchemeBuiltinLibs);
+  } else if ((match = RegExpPrototypeExec(importRE, line)) !== null) {
+    // import('...<Tab>')
+    completeOn = match[1];
+    filter = completeOn;
+    if (this.allowBlockingCompletions) {
+      const subdir = match[2] || '';
+      // File extensions that can be imported:
+      const extensions = ObjectKeys(extensionFormatMap);
+
+      // Only used when loading bare module specifiers from `node_modules`:
+      const indexes = ArrayPrototypeMap(extensions, (ext) => `index${ext}`);
+      ArrayPrototypePush(indexes, 'package.json');
+
+      group = [];
+      let paths = [];
+      if (completeOn === '.') {
+        group = ['./', '../'];
+      } else if (completeOn === '..') {
+        group = ['../'];
+      } else if (RegExpPrototypeExec(/^\.\.?\//, completeOn) !== null) {
+        paths = [process.cwd()];
+      } else {
+        paths = ArrayPrototypeSlice(module.paths);
+      }
+
+      ArrayPrototypeForEach(paths, (dir) => {
+        dir = path.resolve(dir, subdir);
+        const isInNodeModules = path.basename(dir) === 'node_modules';
+        const dirents = gracefulReaddir(dir, { withFileTypes: true }) || [];
+        ArrayPrototypeForEach(dirents, (dirent) => {
+          const { name } = dirent;
+          if (RegExpPrototypeExec(versionedFileNamesRe, name) !== null ||
+              name === '.npm') {
+            // Exclude versioned names that 'npm' installs.
+            return;
+          }
+
+          if (!dirent.isDirectory()) {
+            const extension = path.extname(name);
+            if (StringPrototypeIncludes(extensions, extension)) {
+              ArrayPrototypePush(group, `${subdir}${name}`);
+            }
+            return;
+          }
+
+          ArrayPrototypePush(group, `${subdir}${name}/`);
+          if (!subdir && isInNodeModules) {
+            const absolute = path.resolve(dir, name);
+            const subfiles = gracefulReaddir(absolute) || [];
+            if (ArrayPrototypeSome(subfiles, (subfile) => {
+              return ArrayPrototypeIncludes(indexes, subfile);
+            })) {
+              ArrayPrototypePush(group, `${subdir}${name}`);
+            }
+          }
+        });
+      });
+
+      if (group.length) {
+        ArrayPrototypePush(completionGroups, group);
+      }
+    }
+
+    ArrayPrototypePush(completionGroups, getReplBuiltinLibs(), nodeSchemeBuiltinLibs);
+  } else if ((match = RegExpPrototypeExec(fsAutoCompleteRE, line)) !== null &&
+             this.allowBlockingCompletions) {
+    ({ 0: completionGroups, 1: completeOn } = completeFSFunctions(match));
+  } else if (line.length === 0 ||
+             RegExpPrototypeExec(/\w|\.|\$/, line[line.length - 1]) !== null) {
+    const completeTarget = line.length === 0 ? line : findExpressionCompleteTarget(line);
+
+    if (line.length !== 0 && !completeTarget) {
+      completionGroupsLoaded();
+      return;
+    }
+    let expr = '';
+    completeOn = completeTarget;
+    if (StringPrototypeEndsWith(line, '.')) {
+      expr = StringPrototypeSlice(completeTarget, 0, -1);
+    } else if (line.length !== 0) {
+      const bits = StringPrototypeSplit(completeTarget, '.');
+      filter = ArrayPrototypePop(bits);
+      expr = ArrayPrototypeJoin(bits, '.');
+    }
+
+    // Resolve expr and get its completions.
+    if (!expr) {
+      // Get global vars synchronously
+      ArrayPrototypePush(completionGroups,
+                         getGlobalLexicalScopeNames(this[kContextId]));
+      let contextProto = this.context;
+      while ((contextProto = ObjectGetPrototypeOf(contextProto)) !== null) {
+        ArrayPrototypePush(completionGroups,
+                           filteredOwnPropertyNames(contextProto));
+      }
+      const contextOwnNames = filteredOwnPropertyNames(this.context);
+      if (!this.useGlobal) {
+        // When the context is not `global`, builtins are not own
+        // properties of it.
+        // `globalBuiltins` is a `SafeSet`, not an Array-like.
+        ArrayPrototypePush(contextOwnNames, ...globalBuiltins);
+      }
+      ArrayPrototypePush(completionGroups, contextOwnNames);
+      if (filter !== '') addCommonWords(completionGroups);
+      completionGroupsLoaded();
+      return;
+    }
+
+    // If the target ends with a dot (e.g. `obj.foo.`) such code won't be valid for AST parsing
+    // so in order to make it correct we add an identifier to its end (e.g. `obj.foo.x`)
+    const parsableCompleteTarget = completeTarget.endsWith('.') ? `${completeTarget}x` : completeTarget;
+
+    let completeTargetAst;
+    try {
+      completeTargetAst = acornParse(
+        parsableCompleteTarget, { __proto__: null, sourceType: 'module', ecmaVersion: 'latest' },
+      );
+    } catch { /* No need to specifically handle parse errors */ }
+
+    if (!completeTargetAst) {
+      return completionGroupsLoaded();
+    }
+
+    return includesProxiesOrGetters(
+      completeTargetAst.body[0].expression,
+      parsableCompleteTarget,
+      this.eval,
+      this.context,
+      (includes) => {
+        if (includes) {
+        // The expression involves proxies or getters, meaning that it
+        // can trigger side-effectful behaviors, so bail out
+          return completionGroupsLoaded();
+        }
+
+        let chaining = '.';
+        if (StringPrototypeEndsWith(expr, '?')) {
+          expr = StringPrototypeSlice(expr, 0, -1);
+          chaining = '?.';
+        }
+
+        const memberGroups = [];
+        const evalExpr = `try { ${expr} } catch {}`;
+        this.eval(evalExpr, this.context, getREPLResourceName(), (e, obj) => {
+          try {
+            let p;
+            if ((typeof obj === 'object' && obj !== null) ||
+              typeof obj === 'function') {
+              ArrayPrototypePush(memberGroups, filteredOwnPropertyNames(obj));
+              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) {
+              ArrayPrototypePush(memberGroups, 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) {
+            expr += chaining;
+            ArrayPrototypeForEach(memberGroups, (group) => {
+              ArrayPrototypePush(completionGroups,
+                                 ArrayPrototypeMap(group,
+                                                   (member) => `${expr}${member}`));
+            });
+            filter &&= `${expr}${filter}`;
+          }
+
+          completionGroupsLoaded();
+        });
+      });
+  }
+
+  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 = [];
+      const lowerCaseFilter = StringPrototypeToLocaleLowerCase(filter);
+      ArrayPrototypeForEach(completionGroups, (group) => {
+        const filteredGroup = ArrayPrototypeFilter(group, (str) => {
+          // Filter is always case-insensitive following chromium autocomplete
+          // behavior.
+          return StringPrototypeStartsWith(
+            StringPrototypeToLocaleLowerCase(str),
+            lowerCaseFilter,
+          );
+        });
+        if (filteredGroup.length) {
+          ArrayPrototypePush(newCompletionGroups, filteredGroup);
+        }
+      });
+      completionGroups = newCompletionGroups;
+    }
+
+    const completions = [];
+    // Unique completions across all groups.
+    const uniqueSet = new SafeSet();
+    uniqueSet.add('');
+    // Completion group 0 is the "closest" (least far up the inheritance
+    // chain) so we put its completions last: to be closest in the REPL.
+    ArrayPrototypeForEach(completionGroups, (group) => {
+      ArrayPrototypeSort(group, (a, b) => (b > a ? 1 : -1));
+      const setSize = uniqueSet.size;
+      ArrayPrototypeForEach(group, (entry) => {
+        if (!uniqueSet.has(entry)) {
+          ArrayPrototypeUnshift(completions, entry);
+          uniqueSet.add(entry);
+        }
+      });
+      // Add a separator between groups.
+      if (uniqueSet.size !== setSize) {
+        ArrayPrototypeUnshift(completions, '');
+      }
+    });
+
+    // Remove obsolete group entry, if present.
+    if (completions[0] === '') {
+      ArrayPrototypeShift(completions);
+    }
+
+    callback(null, [completions, completeOn]);
+  }
+}
+
+/**
+ * This function tries to extract a target for tab completion from code representing an expression.
+ *
+ * Such target is basically the last piece of the expression that can be evaluated for the potential
+ * tab completion.
+ *
+ * Some examples:
+ * - The complete target for `const a = obj.b` is `obj.b`
+ *   (because tab completion will evaluate and check the `obj.b` object)
+ * - The complete target for `tru` is `tru`
+ *   (since we'd ideally want to complete that to `true`)
+ * - The complete target for `{ a: tru` is `tru`
+ *   (like the last example, we'd ideally want that to complete to true)
+ * - There is no complete target for `{ a: true }`
+ *   (there is nothing to complete)
+ * @param {string} code the code representing the expression to analyze
+ * @returns {string|null} a substring of the code representing the complete target is there was one, `null` otherwise
+ */
+function findExpressionCompleteTarget(code) {
+  if (!code) {
+    return null;
+  }
+
+  if (code.at(-1) === '.') {
+    if (code.at(-2) === '?') {
+      // The code ends with the optional chaining operator (`?.`),
+      // such code can't generate a valid AST so we need to strip
+      // the suffix, run this function's logic and add back the
+      // optional chaining operator to the result if present
+      const result = findExpressionCompleteTarget(code.slice(0, -2));
+      return !result ? result : `${result}?.`;
+    }
+
+    // The code ends with a dot, such code can't generate a valid AST
+    // so we need to strip the suffix, run this function's logic and
+    // add back the dot to the result if present
+    const result = findExpressionCompleteTarget(code.slice(0, -1));
+    return !result ? result : `${result}.`;
+  }
+
+  let ast;
+  try {
+    ast = acornParse(code, { __proto__: null, sourceType: 'module', ecmaVersion: 'latest' });
+  } catch {
+    const keywords = code.split(' ');
+
+    if (keywords.length > 1) {
+      // Something went wrong with the parsing, however this can be due to incomplete code
+      // (that is for example missing a closing bracket, as for example `{ a: obj.te`), in
+      // this case we take the last code keyword and try again
+      // TODO(dario-piotrowicz): make this more robust, right now we only split by spaces
+      //                         but that's not always enough, for example it doesn't handle
+      //                         this code: `{ a: obj['hello world'].te`
+      return findExpressionCompleteTarget(keywords.at(-1));
+    }
+
+    // The ast parsing has legitimately failed so we return null
+    return null;
+  }
+
+  const lastBodyStatement = ast.body[ast.body.length - 1];
+
+  if (!lastBodyStatement) {
+    return null;
+  }
+
+  // If the last statement is a block we know there is not going to be a potential
+  // completion target (e.g. in `{ a: true }` there is no completion to be done)
+  if (lastBodyStatement.type === 'BlockStatement') {
+    return null;
+  }
+
+  // If the last statement is an expression and it has a right side, that's what we
+  // want to potentially complete on, so let's re-run the function's logic on that
+  if (lastBodyStatement.type === 'ExpressionStatement' && lastBodyStatement.expression.right) {
+    const exprRight = lastBodyStatement.expression.right;
+    const exprRightCode = code.slice(exprRight.start, exprRight.end);
+    return findExpressionCompleteTarget(exprRightCode);
+  }
+
+  // If the last statement is a variable declaration statement the last declaration is
+  // what we can potentially complete on, so let's re-run the function's logic on that
+  if (lastBodyStatement.type === 'VariableDeclaration') {
+    const lastDeclarationInit = lastBodyStatement.declarations.at(-1).init;
+    if (!lastDeclarationInit) {
+      // If there is no initialization we can simply return
+      return null;
+    }
+    const lastDeclarationInitCode = code.slice(lastDeclarationInit.start, lastDeclarationInit.end);
+    return findExpressionCompleteTarget(lastDeclarationInitCode);
+  }
+
+  // If the last statement is an expression statement with a unary operator (delete, typeof, etc.)
+  // we want to extract the argument for completion (e.g. for `delete obj.prop` we want `obj.prop`)
+  if (lastBodyStatement.type === 'ExpressionStatement' &&
+      lastBodyStatement.expression.type === 'UnaryExpression' &&
+      lastBodyStatement.expression.argument) {
+    const argument = lastBodyStatement.expression.argument;
+    const argumentCode = code.slice(argument.start, argument.end);
+    return findExpressionCompleteTarget(argumentCode);
+  }
+
+  // Walk the AST for the current block of code, and check whether it contains any
+  // statement or expression type that would potentially have side effects if evaluated.
+  let isAllowed = true;
+  const disallow = () => isAllowed = false;
+  acornWalk.simple(lastBodyStatement, {
+    ForInStatement: disallow,
+    ForOfStatement: disallow,
+    CallExpression: disallow,
+    AssignmentExpression: disallow,
+    UpdateExpression: disallow,
+  });
+  if (!isAllowed) {
+    return null;
+  }
+
+  // If any of the above early returns haven't activated then it means that
+  // the potential complete target is the full code (e.g. the code represents
+  // a simple partial identifier, a member expression, etc...)
+  return code.slice(lastBodyStatement.start, lastBodyStatement.end);
+}
+
+/**
+ * Utility used to determine if an expression includes object getters or proxies.
+ *
+ * Example: given `obj.foo`, the function lets you know if `foo` has a getter function
+ * associated to it, or if `obj` is a proxy
+ * @param {any} expr The expression, in AST format to analyze
+ * @param {string} exprStr The string representation of the expression
+ * @param {(str: string, ctx: any, resourceName: string, cb: (error, evaled) => void) => void} evalFn
+ *   Eval function to use
+ * @param {any} ctx The context to use for any code evaluation
+ * @param {(includes: boolean) => void} callback Callback that will be called with the result of the operation
+ * @returns {void}
+ */
+function includesProxiesOrGetters(expr, exprStr, evalFn, ctx, callback) {
+  if (expr?.type !== 'MemberExpression') {
+    // If the expression is not a member one for obvious reasons no getters are involved
+    return callback(false);
+  }
+
+  if (expr.object.type === 'MemberExpression') {
+    // The object itself is a member expression, so we need to recurse (e.g. the expression is `obj.foo.bar`)
+    return includesProxiesOrGetters(
+      expr.object,
+      exprStr.slice(0, expr.object.end),
+      evalFn,
+      ctx,
+      (includes, lastEvaledObj) => {
+        if (includes) {
+          // If the recurred call found a getter we can also terminate
+          return callback(includes);
+        }
+
+        if (isProxy(lastEvaledObj)) {
+          return callback(true);
+        }
+
+        // If a getter/proxy hasn't been found by the recursion call we need to check if maybe a getter/proxy
+        // is present here (e.g. in `obj.foo.bar` we found that `obj.foo` doesn't involve any getters so we now
+        // need to check if `bar` on `obj.foo` (i.e. `lastEvaledObj`) has a getter or if `obj.foo.bar` is a proxy)
+        return hasGetterOrIsProxy(lastEvaledObj, expr.property, (doesHaveGetterOrIsProxy) => {
+          return callback(doesHaveGetterOrIsProxy);
+        });
+      },
+    );
+  }
+
+  // This is the base of the recursion we have an identifier for the object and an identifier or literal
+  // for the property (e.g. we have `obj.foo` or `obj['foo']`, `obj` is the object identifier and `foo`
+  // is the property identifier/literal)
+  if (expr.object.type === 'Identifier') {
+    return evalFn(`try { ${expr.object.name} } catch {}`, ctx, getREPLResourceName(), (err, obj) => {
+      if (err) {
+        return callback(false);
+      }
+
+      if (isProxy(obj)) {
+        return callback(true);
+      }
+
+      return hasGetterOrIsProxy(obj, expr.property, (doesHaveGetterOrIsProxy) => {
+        if (doesHaveGetterOrIsProxy) {
+          return callback(true);
+        }
+
+        return evalFn(
+          `try { ${exprStr} } catch {} `, ctx, getREPLResourceName(), (err, obj) => {
+            if (err) {
+              return callback(false);
+            }
+            return callback(false, obj);
+          });
+      });
+    });
+  }
+
+  /**
+   * Utility to see if a property has a getter associated to it or if
+   * the property itself is a proxy object.
+   * @returns {void}
+   */
+  function hasGetterOrIsProxy(obj, astProp, cb) {
+    if (!obj || !astProp) {
+      return cb(false);
+    }
+
+    if (astProp.type === 'Literal') {
+      // We have something like `obj['foo'].x` where `x` is the literal
+
+      if (safeIsProxyAccess(obj, astProp.value)) {
+        return cb(true);
+      }
+
+      const propDescriptor = ObjectGetOwnPropertyDescriptor(
+        obj,
+        `${astProp.value}`,
+      );
+      const propHasGetter = typeof propDescriptor?.get === 'function';
+      return cb(propHasGetter);
+    }
+
+    if (
+      astProp.type === 'Identifier' &&
+      exprStr.at(astProp.start - 1) === '.'
+    ) {
+      // We have something like `obj.foo.x` where `foo` is the identifier
+
+      if (safeIsProxyAccess(obj, astProp.name)) {
+        return cb(true);
+      }
+
+      const propDescriptor = ObjectGetOwnPropertyDescriptor(
+        obj,
+        `${astProp.name}`,
+      );
+      const propHasGetter = typeof propDescriptor?.get === 'function';
+      return cb(propHasGetter);
+    }
+
+    return evalFn(
+      // Note: this eval runs the property expression, which might be side-effectful, for example
+      //       the user could be running `obj[getKey()].` where `getKey()` has some side effects.
+      //       Arguably this behavior should not be too surprising, but if it turns out that it is,
+      //       then we can revisit this behavior and add logic to analyze the property expression
+      //       and eval it only if we can confidently say that it can't have any side effects
+      `try { ${exprStr.slice(astProp.start, astProp.end)} } catch {} `,
+      ctx,
+      getREPLResourceName(),
+      (err, evaledProp) => {
+        if (err) {
+          return callback(false);
+        }
+
+        if (typeof evaledProp === 'string') {
+          if (safeIsProxyAccess(obj, evaledProp)) {
+            return cb(true);
+          }
+
+          const propDescriptor = ObjectGetOwnPropertyDescriptor(
+            obj,
+            evaledProp,
+          );
+          const propHasGetter = typeof propDescriptor?.get === 'function';
+          return cb(propHasGetter);
+        }
+
+        return callback(false);
+      },
+    );
+  }
+
+  function safeIsProxyAccess(obj, prop) {
+    // Accessing `prop` may trigger a getter that throws, so we use try-catch to guard against it
+    try {
+      return isProxy(obj[prop]);
+    } catch {
+      return false;
+    }
+  }
+
+  return callback(false);
+}
+
+module.exports = {
+  complete,
+};

filename: lib/internal/repl/utils.js
patch: @@ -14,6 +14,7 @@ const {
   StringPrototypeLastIndexOf,
   StringPrototypeReplaceAll,
   StringPrototypeSlice,
+  StringPrototypeStartsWith,
   StringPrototypeToLowerCase,
   StringPrototypeTrim,
   Symbol,
@@ -50,6 +51,10 @@ const {
   inspect,
 } = require('internal/util/inspect');
 
+const CJSModule = require('internal/modules/cjs/loader').Module;
+
+const vm = require('vm');
+
 let debug = require('internal/util/debuglog').debuglog('repl', (fn) => {
   debug = fn;
 });
@@ -783,6 +788,55 @@ function isObjectLiteral(code) {
     RegExpPrototypeExec(endsWithSemicolonRegExp, code) === null;
 }
 
+const kContextId = Symbol('contextId');
+
+const path = require('path');
+
+function fixReplRequire(replModule) {
+  try {
+    // Hack for require.resolve("./relative") to work properly.
+    replModule.filename = path.resolve('repl');
+  } catch {
+    // path.resolve('repl') fails when the current working directory has been
+    // deleted.  Fall back to the directory name of the (absolute) executable
+    // path.  It's not really correct but what are the alternatives?
+    const dirname = path.dirname(process.execPath);
+    replModule.filename = path.resolve(dirname, 'repl');
+  }
+
+  // Hack for repl require to work properly with node_modules folders
+  replModule.paths = CJSModule._nodeModulePaths(replModule.filename);
+}
+
+let nextREPLResourceNumber = 1;
+// This prevents v8 code cache from getting confused and using a different
+// cache from a resource of the same name
+function getREPLResourceName() {
+  return `REPL${nextREPLResourceNumber++}`;
+}
+
+const globalBuiltins =
+  new SafeSet(vm.runInNewContext('Object.getOwnPropertyNames(globalThis)'));
+
+let _builtinLibs = ArrayPrototypeFilter(
+  CJSModule.builtinModules,
+  (e) => e[0] !== '_' && !StringPrototypeStartsWith(e, 'node:'),
+);
+
+// Note: the `getReplBuiltinLibs` and `setReplBuiltinLibs` are functions used to provide getters and
+//       setters for the `builtinModules` and `_builtinLibs` properties of the repl module and for making
+//       sure that all internal repl modules share the same value, which can potentially be updated by users.
+//       Also note that both `repl.builtinModules` and `repl._builtinLibs` are deprecated, once such properties
+//       are removed these two functions should also be removed as no longer necessary.
+
+function getReplBuiltinLibs() {
+  return _builtinLibs;
+}
+
+function setReplBuiltinLibs(value) {
+  _builtinLibs = value;
+}
+
 module.exports = {
   REPL_MODE_SLOPPY: Symbol('repl-sloppy'),
   REPL_MODE_STRICT,
@@ -792,4 +846,10 @@ module.exports = {
   setupReverseSearch,
   isObjectLiteral,
   isValidSyntax,
+  kContextId,
+  getREPLResourceName,
+  globalBuiltins,
+  getReplBuiltinLibs,
+  setReplBuiltinLibs,
+  fixReplRequire,
 };

filename: lib/repl.js
patch: @@ -47,17 +47,13 @@ const {
   ArrayPrototypeFilter,
   ArrayPrototypeFindLastIndex,
   ArrayPrototypeForEach,
-  ArrayPrototypeIncludes,
   ArrayPrototypeJoin,
   ArrayPrototypeMap,
   ArrayPrototypePop,
   ArrayPrototypePush,
-  ArrayPrototypePushApply,
   ArrayPrototypeShift,
   ArrayPrototypeSlice,
-  ArrayPrototypeSome,
   ArrayPrototypeSort,
-  ArrayPrototypeUnshift,
   Boolean,
   Error: MainContextError,
   FunctionPrototypeBind,
@@ -69,7 +65,6 @@ const {
   ObjectDefineProperty,
   ObjectGetOwnPropertyDescriptor,
   ObjectGetOwnPropertyNames,
-  ObjectGetPrototypeOf,
   ObjectKeys,
   Promise,
   ReflectApply,
@@ -79,33 +74,22 @@ const {
   SafeSet,
   SafeWeakSet,
   StringPrototypeCharAt,
-  StringPrototypeCodePointAt,
   StringPrototypeEndsWith,
   StringPrototypeIncludes,
   StringPrototypeRepeat,
   StringPrototypeSlice,
-  StringPrototypeSplit,
   StringPrototypeStartsWith,
-  StringPrototypeToLocaleLowerCase,
   StringPrototypeTrim,
-  StringPrototypeTrimStart,
   Symbol,
   SyntaxError,
   globalThis,
 } = primordials;
 
-const {
-  isProxy,
-} = require('internal/util/types');
-
-const { BuiltinModule } = require('internal/bootstrap/realm');
 const {
   makeRequireFunction,
   addBuiltinLibsToObject,
 } = require('internal/modules/helpers');
 const {
-  isIdentifierStart,
-  isIdentifierChar,
   parse: acornParse,
 } = require('internal/deps/acorn/acorn/dist/acorn');
 const acornWalk = require('internal/deps/acorn/acorn-walk/dist/walk');
@@ -130,16 +114,6 @@ const {
 const { Console } = require('console');
 const { shouldColorize } = require('internal/util/colors');
 const CJSModule = require('internal/modules/cjs/loader').Module;
-let _builtinLibs = ArrayPrototypeFilter(
-  CJSModule.builtinModules,
-  (e) => e[0] !== '_' && !StringPrototypeStartsWith(e, 'node:'),
-);
-const nodeSchemeBuiltinLibs = ArrayPrototypeMap(
-  _builtinLibs, (lib) => `node:${lib}`);
-ArrayPrototypeForEach(
-  BuiltinModule.getSchemeOnlyModuleNames(),
-  (lib) => ArrayPrototypePush(nodeSchemeBuiltinLibs, `node:${lib}`),
-);
 const domain = require('domain');
 let debug = require('internal/util/debuglog').debuglog('repl', (fn) => {
   debug = fn;
@@ -175,22 +149,21 @@ const {
   setupReverseSearch,
   isObjectLiteral,
   isValidSyntax,
+  kContextId,
+  getREPLResourceName,
+  globalBuiltins,
+  getReplBuiltinLibs,
+  setReplBuiltinLibs,
+  fixReplRequire,
 } = require('internal/repl/utils');
 const {
-  constants: {
-    ALL_PROPERTIES,
-    SKIP_SYMBOLS,
-  },
-  getOwnNonIndexProperties,
-} = internalBinding('util');
+  complete,
+} = require('internal/repl/completion');
 const {
   startSigintWatchdog,
   stopSigintWatchdog,
 } = internalBinding('contextify');
 
-const {
-  extensionFormatMap,
-} = require('internal/modules/esm/formats');
 const {
   makeContextifyScript,
 } = require('internal/vm');
@@ -199,41 +172,19 @@ const {
   kAddNewLineOnTTY,
   kLastCommandErrored,
 } = require('internal/readline/interface');
-let nextREPLResourceNumber = 1;
-// This prevents v8 code cache from getting confused and using a different
-// cache from a resource of the same name
-function getREPLResourceName() {
-  return `REPL${nextREPLResourceNumber++}`;
-}
 
 // Lazy-loaded.
 let processTopLevelAwait;
 
-const globalBuiltins =
-  new SafeSet(vm.runInNewContext('Object.getOwnPropertyNames(globalThis)'));
-
 const parentModule = module;
 const domainSet = new SafeWeakSet();
 
 const kBufferedCommandSymbol = Symbol('bufferedCommand');
-const kContextId = Symbol('contextId');
 const kLoadingSymbol = Symbol('loading');
 
 let addedNewListener = false;
 
-try {
-  // Hack for require.resolve("./relative") to work properly.
-  module.filename = path.resolve('repl');
-} catch {
-  // path.resolve('repl') fails when the current working directory has been
-  // deleted.  Fall back to the directory name of the (absolute) executable
-  // path.  It's not really correct but what are the alternatives?
-  const dirname = path.dirname(process.execPath);
-  module.filename = path.resolve(dirname, 'repl');
-}
-
-// Hack for repl require to work properly with node_modules folders
-module.paths = CJSModule._nodeModulePaths(module.filename);
+fixReplRequire(module);
 
 // This is the default "writer" value, if none is passed in the REPL options,
 // and it can be overridden by custom print functions, such as `probe` or
@@ -1238,714 +1189,6 @@ function start(prompt, source, eval_, useGlobal, ignoreUndefined, replMode) {
     prompt, source, eval_, useGlobal, ignoreUndefined, replMode);
 }
 
-const importRE = /\bimport\s*\(\s*['"`](([\w@./:-]+\/)?(?:[\w@./:-]*))(?![^'"`])$/;
-const requireRE = /\brequire\s*\(\s*['"`](([\w@./:-]+\/)?(?:[\w@./:-]*))(?![^'"`])$/;
-const fsAutoCompleteRE = /fs(?:\.promises)?\.\s*[a-z][a-zA-Z]+\(\s*["'](.*)/;
-const versionedFileNamesRe = /-\d+\.\d+/;
-
-function isIdentifier(str) {
-  if (str === '') {
-    return false;
-  }
-  const first = StringPrototypeCodePointAt(str, 0);
-  if (!isIdentifierStart(first)) {
-    return false;
-  }
-  const firstLen = first > 0xffff ? 2 : 1;
-  for (let i = firstLen; i < str.length; i += 1) {
-    const cp = StringPrototypeCodePointAt(str, i);
-    if (!isIdentifierChar(cp)) {
-      return false;
-    }
-    if (cp > 0xffff) {
-      i += 1;
-    }
-  }
-  return true;
-}
-
-function isNotLegacyObjectPrototypeMethod(str) {
-  return isIdentifier(str) &&
-    str !== '__defineGetter__' &&
-    str !== '__defineSetter__' &&
-    str !== '__lookupGetter__' &&
-    str !== '__lookupSetter__';
-}
-
-function filteredOwnPropertyNames(obj) {
-  if (!obj) return [];
-  // `Object.prototype` is the only non-contrived object that fulfills
-  // `Object.getPrototypeOf(X) === null &&
-  //  Object.getPrototypeOf(Object.getPrototypeOf(X.constructor)) === X`.
-  let isObjectPrototype = false;
-  if (ObjectGetPrototypeOf(obj) === null) {
-    const ctorDescriptor = ObjectGetOwnPropertyDescriptor(obj, 'constructor');
-    if (ctorDescriptor?.value) {
-      const ctorProto = ObjectGetPrototypeOf(ctorDescriptor.value);
-      isObjectPrototype = ctorProto && ObjectGetPrototypeOf(ctorProto) === obj;
-    }
-  }
-  const filter = ALL_PROPERTIES | SKIP_SYMBOLS;
-  return ArrayPrototypeFilter(
-    getOwnNonIndexProperties(obj, filter),
-    isObjectPrototype ? isNotLegacyObjectPrototypeMethod : isIdentifier);
-}
-
-function getGlobalLexicalScopeNames(contextId) {
-  return sendInspectorCommand((session) => {
-    let names = [];
-    session.post('Runtime.globalLexicalScopeNames', {
-      executionContextId: contextId,
-    }, (error, result) => {
-      if (!error) names = result.names;
-    });
-    return names;
-  }, () => []);
-}
-
-
-function gracefulReaddir(...args) {
-  try {
-    return ReflectApply(fs.readdirSync, null, args);
-  } catch {
-    // Continue regardless of error.
-  }
-}
-
-function completeFSFunctions(match) {
-  let baseName = '';
-  let filePath = match[1];
-  let fileList = gracefulReaddir(filePath, { withFileTypes: true });
-
-  if (!fileList) {
-    baseName = path.basename(filePath);
-    filePath = path.dirname(filePath);
-    fileList = gracefulReaddir(filePath, { withFileTypes: true }) || [];
-  }
-
-  const completions = ArrayPrototypeMap(
-    ArrayPrototypeFilter(
-      fileList,
-      (dirent) => StringPrototypeStartsWith(dirent.name, baseName),
-    ),
-    (d) => d.name,
-  );
-
-  return [[completions], baseName];
-}
-
-// Provide a list of completions for the given leading text. This is
-// given to the readline interface for handling tab completion.
-//
-// Example:
-//  complete('let foo = util.')
-//    -> [['util.print', 'util.debug', 'util.log', 'util.inspect'],
-//        'util.' ]
-//
-// Warning: This evals code like "foo.bar.baz", so it could run property
-// getter code. To avoid potential triggering side-effects with getters the completion
-// logic is skipped when getters or proxies are involved in the expression.
-// (see: https://github.com/nodejs/node/issues/57829).
-function complete(line, callback) {
-  // List of completion lists, one for each inheritance "level"
-  let completionGroups = [];
-  let completeOn, group;
-
-  // Ignore right whitespace. It could change the outcome.
-  line = StringPrototypeTrimStart(line);
-
-  let filter = '';
-
-  let match;
-  // REPL commands (e.g. ".break").
-  if ((match = RegExpPrototypeExec(/^\s*\.(\w*)$/, line)) !== null) {
-    ArrayPrototypePush(completionGroups, ObjectKeys(this.commands));
-    completeOn = match[1];
-    if (completeOn.length) {
-      filter = completeOn;
-    }
-  } else if ((match = RegExpPrototypeExec(requireRE, line)) !== null) {
-    // require('...<Tab>')
-    completeOn = match[1];
-    filter = completeOn;
-    if (this.allowBlockingCompletions) {
-      const subdir = match[2] || '';
-      const extensions = ObjectKeys(CJSModule._extensions);
-      const indexes = ArrayPrototypeMap(extensions,
-                                        (extension) => `index${extension}`);
-      ArrayPrototypePush(indexes, 'package.json', 'index');
-
-      group = [];
-      let paths = [];
-
-      if (completeOn === '.') {
-        group = ['./', '../'];
-      } else if (completeOn === '..') {
-        group = ['../'];
-      } else if (RegExpPrototypeExec(/^\.\.?\//, completeOn) !== null) {
-        paths = [process.cwd()];
-      } else {
-        paths = [];
-        ArrayPrototypePushApply(paths, module.paths);
-        ArrayPrototypePushApply(paths, CJSModule.globalPaths);
-      }
-
-      ArrayPrototypeForEach(paths, (dir) => {
-        dir = path.resolve(dir, subdir);
-        const dirents = gracefulReaddir(dir, { withFileTypes: true }) || [];
-        ArrayPrototypeForEach(dirents, (dirent) => {
-          if (RegExpPrototypeExec(versionedFileNamesRe, dirent.name) !== null ||
-              dirent.name === '.npm') {
-            // Exclude versioned names that 'npm' installs.
-            return;
-          }
-          const extension = path.extname(dirent.name);
-          const base = StringPrototypeSlice(dirent.name, 0, -extension.length);
-          if (!dirent.isDirectory()) {
-            if (StringPrototypeIncludes(extensions, extension) &&
-                (!subdir || base !== 'index')) {
-              ArrayPrototypePush(group, `${subdir}${base}`);
-            }
-            return;
-          }
-          ArrayPrototypePush(group, `${subdir}${dirent.name}/`);
-          const absolute = path.resolve(dir, dirent.name);
-          if (ArrayPrototypeSome(
-            gracefulReaddir(absolute) || [],
-            (subfile) => ArrayPrototypeIncludes(indexes, subfile),
-          )) {
-            ArrayPrototypePush(group, `${subdir}${dirent.name}`);
-          }
-        });
-      });
-      if (group.length) {
-        ArrayPrototypePush(completionGroups, group);
-      }
-    }
-
-    ArrayPrototypePush(completionGroups, _builtinLibs, nodeSchemeBuiltinLibs);
-  } else if ((match = RegExpPrototypeExec(importRE, line)) !== null) {
-    // import('...<Tab>')
-    completeOn = match[1];
-    filter = completeOn;
-    if (this.allowBlockingCompletions) {
-      const subdir = match[2] || '';
-      // File extensions that can be imported:
-      const extensions = ObjectKeys(extensionFormatMap);
-
-      // Only used when loading bare module specifiers from `node_modules`:
-      const indexes = ArrayPrototypeMap(extensions, (ext) => `index${ext}`);
-      ArrayPrototypePush(indexes, 'package.json');
-
-      group = [];
-      let paths = [];
-      if (completeOn === '.') {
-        group = ['./', '../'];
-      } else if (completeOn === '..') {
-        group = ['../'];
-      } else if (RegExpPrototypeExec(/^\.\.?\//, completeOn) !== null) {
-        paths = [process.cwd()];
-      } else {
-        paths = ArrayPrototypeSlice(module.paths);
-      }
-
-      ArrayPrototypeForEach(paths, (dir) => {
-        dir = path.resolve(dir, subdir);
-        const isInNodeModules = path.basename(dir) === 'node_modules';
-        const dirents = gracefulReaddir(dir, { withFileTypes: true }) || [];
-        ArrayPrototypeForEach(dirents, (dirent) => {
-          const { name } = dirent;
-          if (RegExpPrototypeExec(versionedFileNamesRe, name) !== null ||
-              name === '.npm') {
-            // Exclude versioned names that 'npm' installs.
-            return;
-          }
-
-          if (!dirent.isDirectory()) {
-            const extension = path.extname(name);
-            if (StringPrototypeIncludes(extensions, extension)) {
-              ArrayPrototypePush(group, `${subdir}${name}`);
-            }
-            return;
-          }
-
-          ArrayPrototypePush(group, `${subdir}${name}/`);
-          if (!subdir && isInNodeModules) {
-            const absolute = path.resolve(dir, name);
-            const subfiles = gracefulReaddir(absolute) || [];
-            if (ArrayPrototypeSome(subfiles, (subfile) => {
-              return ArrayPrototypeIncludes(indexes, subfile);
-            })) {
-              ArrayPrototypePush(group, `${subdir}${name}`);
-            }
-          }
-        });
-      });
-
-      if (group.length) {
-        ArrayPrototypePush(completionGroups, group);
-      }
-    }
-
-    ArrayPrototypePush(completionGroups, _builtinLibs, nodeSchemeBuiltinLibs);
-  } else if ((match = RegExpPrototypeExec(fsAutoCompleteRE, line)) !== null &&
-             this.allowBlockingCompletions) {
-    ({ 0: completionGroups, 1: completeOn } = completeFSFunctions(match));
-  } else if (line.length === 0 ||
-             RegExpPrototypeExec(/\w|\.|\$/, line[line.length - 1]) !== null) {
-    const completeTarget = line.length === 0 ? line : findExpressionCompleteTarget(line);
-
-    if (line.length !== 0 && !completeTarget) {
-      completionGroupsLoaded();
-      return;
-    }
-    let expr = '';
-    completeOn = completeTarget;
-    if (StringPrototypeEndsWith(line, '.')) {
-      expr = StringPrototypeSlice(completeTarget, 0, -1);
-    } else if (line.length !== 0) {
-      const bits = StringPrototypeSplit(completeTarget, '.');
-      filter = ArrayPrototypePop(bits);
-      expr = ArrayPrototypeJoin(bits, '.');
-    }
-
-    // Resolve expr and get its completions.
-    if (!expr) {
-      // Get global vars synchronously
-      ArrayPrototypePush(completionGroups,
-                         getGlobalLexicalScopeNames(this[kContextId]));
-      let contextProto = this.context;
-      while ((contextProto = ObjectGetPrototypeOf(contextProto)) !== null) {
-        ArrayPrototypePush(completionGroups,
-                           filteredOwnPropertyNames(contextProto));
-      }
-      const contextOwnNames = filteredOwnPropertyNames(this.context);
-      if (!this.useGlobal) {
-        // When the context is not `global`, builtins are not own
-        // properties of it.
-        // `globalBuiltins` is a `SafeSet`, not an Array-like.
-        ArrayPrototypePush(contextOwnNames, ...globalBuiltins);
-      }
-      ArrayPrototypePush(completionGroups, contextOwnNames);
-      if (filter !== '') addCommonWords(completionGroups);
-      completionGroupsLoaded();
-      return;
-    }
-
-    // If the target ends with a dot (e.g. `obj.foo.`) such code won't be valid for AST parsing
-    // so in order to make it correct we add an identifier to its end (e.g. `obj.foo.x`)
-    const parsableCompleteTarget = completeTarget.endsWith('.') ? `${completeTarget}x` : completeTarget;
-
-    let completeTargetAst;
-    try {
-      completeTargetAst = acornParse(
-        parsableCompleteTarget, { __proto__: null, sourceType: 'module', ecmaVersion: 'latest' },
-      );
-    } catch { /* No need to specifically handle parse errors */ }
-
-    if (!completeTargetAst) {
-      return completionGroupsLoaded();
-    }
-
-    return includesProxiesOrGetters(
-      completeTargetAst.body[0].expression,
-      parsableCompleteTarget,
-      this.eval,
-      this.context,
-      (includes) => {
-        if (includes) {
-        // The expression involves proxies or getters, meaning that it
-        // can trigger side-effectful behaviors, so bail out
-          return completionGroupsLoaded();
-        }
-
-        let chaining = '.';
-        if (StringPrototypeEndsWith(expr, '?')) {
-          expr = StringPrototypeSlice(expr, 0, -1);
-          chaining = '?.';
-        }
-
-        const memberGroups = [];
-        const evalExpr = `try { ${expr} } catch {}`;
-        this.eval(evalExpr, this.context, getREPLResourceName(), (e, obj) => {
-          try {
-            let p;
-            if ((typeof obj === 'object' && obj !== null) ||
-              typeof obj === 'function') {
-              ArrayPrototypePush(memberGroups, filteredOwnPropertyNames(obj));
-              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) {
-              ArrayPrototypePush(memberGroups, 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) {
-            expr += chaining;
-            ArrayPrototypeForEach(memberGroups, (group) => {
-              ArrayPrototypePush(completionGroups,
-                                 ArrayPrototypeMap(group,
-                                                   (member) => `${expr}${member}`));
-            });
-            filter &&= `${expr}${filter}`;
-          }
-
-          completionGroupsLoaded();
-        });
-      });
-  }
-
-  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 = [];
-      const lowerCaseFilter = StringPrototypeToLocaleLowerCase(filter);
-      ArrayPrototypeForEach(completionGroups, (group) => {
-        const filteredGroup = ArrayPrototypeFilter(group, (str) => {
-          // Filter is always case-insensitive following chromium autocomplete
-          // behavior.
-          return StringPrototypeStartsWith(
-            StringPrototypeToLocaleLowerCase(str),
-            lowerCaseFilter,
-          );
-        });
-        if (filteredGroup.length) {
-          ArrayPrototypePush(newCompletionGroups, filteredGroup);
-        }
-      });
-      completionGroups = newCompletionGroups;
-    }
-
-    const completions = [];
-    // Unique completions across all groups.
-    const uniqueSet = new SafeSet();
-    uniqueSet.add('');
-    // Completion group 0 is the "closest" (least far up the inheritance
-    // chain) so we put its completions last: to be closest in the REPL.
-    ArrayPrototypeForEach(completionGroups, (group) => {
-      ArrayPrototypeSort(group, (a, b) => (b > a ? 1 : -1));
-      const setSize = uniqueSet.size;
-      ArrayPrototypeForEach(group, (entry) => {
-        if (!uniqueSet.has(entry)) {
-          ArrayPrototypeUnshift(completions, entry);
-          uniqueSet.add(entry);
-        }
-      });
-      // Add a separator between groups.
-      if (uniqueSet.size !== setSize) {
-        ArrayPrototypeUnshift(completions, '');
-      }
-    });
-
-    // Remove obsolete group entry, if present.
-    if (completions[0] === '') {
-      ArrayPrototypeShift(completions);
-    }
-
-    callback(null, [completions, completeOn]);
-  }
-}
-
-/**
- * This function tries to extract a target for tab completion from code representing an expression.
- *
- * Such target is basically the last piece of the expression that can be evaluated for the potential
- * tab completion.
- *
- * Some examples:
- * - The complete target for `const a = obj.b` is `obj.b`
- *   (because tab completion will evaluate and check the `obj.b` object)
- * - The complete target for `tru` is `tru`
- *   (since we'd ideally want to complete that to `true`)
- * - The complete target for `{ a: tru` is `tru`
- *   (like the last example, we'd ideally want that to complete to true)
- * - There is no complete target for `{ a: true }`
- *   (there is nothing to complete)
- * @param {string} code the code representing the expression to analyze
- * @returns {string|null} a substring of the code representing the complete target is there was one, `null` otherwise
- */
-function findExpressionCompleteTarget(code) {
-  if (!code) {
-    return null;
-  }
-
-  if (code.at(-1) === '.') {
-    if (code.at(-2) === '?') {
-      // The code ends with the optional chaining operator (`?.`),
-      // such code can't generate a valid AST so we need to strip
-      // the suffix, run this function's logic and add back the
-      // optional chaining operator to the result if present
-      const result = findExpressionCompleteTarget(code.slice(0, -2));
-      return !result ? result : `${result}?.`;
-    }
-
-    // The code ends with a dot, such code can't generate a valid AST
-    // so we need to strip the suffix, run this function's logic and
-    // add back the dot to the result if present
-    const result = findExpressionCompleteTarget(code.slice(0, -1));
-    return !result ? result : `${result}.`;
-  }
-
-  let ast;
-  try {
-    ast = acornParse(code, { __proto__: null, sourceType: 'module', ecmaVersion: 'latest' });
-  } catch {
-    const keywords = code.split(' ');
-
-    if (keywords.length > 1) {
-      // Something went wrong with the parsing, however this can be due to incomplete code
-      // (that is for example missing a closing bracket, as for example `{ a: obj.te`), in
-      // this case we take the last code keyword and try again
-      // TODO(dario-piotrowicz): make this more robust, right now we only split by spaces
-      //                         but that's not always enough, for example it doesn't handle
-      //                         this code: `{ a: obj['hello world'].te`
-      return findExpressionCompleteTarget(keywords.at(-1));
-    }
-
-    // The ast parsing has legitimately failed so we return null
-    return null;
-  }
-
-  const lastBodyStatement = ast.body[ast.body.length - 1];
-
-  if (!lastBodyStatement) {
-    return null;
-  }
-
-  // If the last statement is a block we know there is not going to be a potential
-  // completion target (e.g. in `{ a: true }` there is no completion to be done)
-  if (lastBodyStatement.type === 'BlockStatement') {
-    return null;
-  }
-
-  // If the last statement is an expression and it has a right side, that's what we
-  // want to potentially complete on, so let's re-run the function's logic on that
-  if (lastBodyStatement.type === 'ExpressionStatement' && lastBodyStatement.expression.right) {
-    const exprRight = lastBodyStatement.expression.right;
-    const exprRightCode = code.slice(exprRight.start, exprRight.end);
-    return findExpressionCompleteTarget(exprRightCode);
-  }
-
-  // If the last statement is a variable declaration statement the last declaration is
-  // what we can potentially complete on, so let's re-run the function's logic on that
-  if (lastBodyStatement.type === 'VariableDeclaration') {
-    const lastDeclarationInit = lastBodyStatement.declarations.at(-1).init;
-    if (!lastDeclarationInit) {
-      // If there is no initialization we can simply return
-      return null;
-    }
-    const lastDeclarationInitCode = code.slice(lastDeclarationInit.start, lastDeclarationInit.end);
-    return findExpressionCompleteTarget(lastDeclarationInitCode);
-  }
-
-  // If the last statement is an expression statement with a unary operator (delete, typeof, etc.)
-  // we want to extract the argument for completion (e.g. for `delete obj.prop` we want `obj.prop`)
-  if (lastBodyStatement.type === 'ExpressionStatement' &&
-      lastBodyStatement.expression.type === 'UnaryExpression' &&
-      lastBodyStatement.expression.argument) {
-    const argument = lastBodyStatement.expression.argument;
-    const argumentCode = code.slice(argument.start, argument.end);
-    return findExpressionCompleteTarget(argumentCode);
-  }
-
-  // Walk the AST for the current block of code, and check whether it contains any
-  // statement or expression type that would potentially have side effects if evaluated.
-  let isAllowed = true;
-  const disallow = () => isAllowed = false;
-  acornWalk.simple(lastBodyStatement, {
-    ForInStatement: disallow,
-    ForOfStatement: disallow,
-    CallExpression: disallow,
-    AssignmentExpression: disallow,
-    UpdateExpression: disallow,
-  });
-  if (!isAllowed) {
-    return null;
-  }
-
-  // If any of the above early returns haven't activated then it means that
-  // the potential complete target is the full code (e.g. the code represents
-  // a simple partial identifier, a member expression, etc...)
-  return code.slice(lastBodyStatement.start, lastBodyStatement.end);
-}
-
-/**
- * Utility used to determine if an expression includes object getters or proxies.
- *
- * Example: given `obj.foo`, the function lets you know if `foo` has a getter function
- * associated to it, or if `obj` is a proxy
- * @param {any} expr The expression, in AST format to analyze
- * @param {string} exprStr The string representation of the expression
- * @param {(str: string, ctx: any, resourceName: string, cb: (error, evaled) => void) => void} evalFn
- *   Eval function to use
- * @param {any} ctx The context to use for any code evaluation
- * @param {(includes: boolean) => void} callback Callback that will be called with the result of the operation
- * @returns {void}
- */
-function includesProxiesOrGetters(expr, exprStr, evalFn, ctx, callback) {
-  if (expr?.type !== 'MemberExpression') {
-    // If the expression is not a member one for obvious reasons no getters are involved
-    return callback(false);
-  }
-
-  if (expr.object.type === 'MemberExpression') {
-    // The object itself is a member expression, so we need to recurse (e.g. the expression is `obj.foo.bar`)
-    return includesProxiesOrGetters(
-      expr.object,
-      exprStr.slice(0, expr.object.end),
-      evalFn,
-      ctx,
-      (includes, lastEvaledObj) => {
-        if (includes) {
-          // If the recurred call found a getter we can also terminate
-          return callback(includes);
-        }
-
-        if (isProxy(lastEvaledObj)) {
-          return callback(true);
-        }
-
-        // If a getter/proxy hasn't been found by the recursion call we need to check if maybe a getter/proxy
-        // is present here (e.g. in `obj.foo.bar` we found that `obj.foo` doesn't involve any getters so we now
-        // need to check if `bar` on `obj.foo` (i.e. `lastEvaledObj`) has a getter or if `obj.foo.bar` is a proxy)
-        return hasGetterOrIsProxy(lastEvaledObj, expr.property, (doesHaveGetterOrIsProxy) => {
-          return callback(doesHaveGetterOrIsProxy);
-        });
-      },
-    );
-  }
-
-  // This is the base of the recursion we have an identifier for the object and an identifier or literal
-  // for the property (e.g. we have `obj.foo` or `obj['foo']`, `obj` is the object identifier and `foo`
-  // is the property identifier/literal)
-  if (expr.object.type === 'Identifier') {
-    return evalFn(`try { ${expr.object.name} } catch {}`, ctx, getREPLResourceName(), (err, obj) => {
-      if (err) {
-        return callback(false);
-      }
-
-      if (isProxy(obj)) {
-        return callback(true);
-      }
-
-      return hasGetterOrIsProxy(obj, expr.property, (doesHaveGetterOrIsProxy) => {
-        if (doesHaveGetterOrIsProxy) {
-          return callback(true);
-        }
-
-        return evalFn(
-          `try { ${exprStr} } catch {} `, ctx, getREPLResourceName(), (err, obj) => {
-            if (err) {
-              return callback(false);
-            }
-            return callback(false, obj);
-          });
-      });
-    });
-  }
-
-  /**
-   * Utility to see if a property has a getter associated to it or if
-   * the property itself is a proxy object.
-   * @returns {void}
-   */
-  function hasGetterOrIsProxy(obj, astProp, cb) {
-    if (!obj || !astProp) {
-      return cb(false);
-    }
-
-    if (astProp.type === 'Literal') {
-      // We have something like `obj['foo'].x` where `x` is the literal
-
-      if (safeIsProxyAccess(obj, astProp.value)) {
-        return cb(true);
-      }
-
-      const propDescriptor = ObjectGetOwnPropertyDescriptor(
-        obj,
-        `${astProp.value}`,
-      );
-      const propHasGetter = typeof propDescriptor?.get === 'function';
-      return cb(propHasGetter);
-    }
-
-    if (
-      astProp.type === 'Identifier' &&
-      exprStr.at(astProp.start - 1) === '.'
-    ) {
-      // We have something like `obj.foo.x` where `foo` is the identifier
-
-      if (safeIsProxyAccess(obj, astProp.name)) {
-        return cb(true);
-      }
-
-      const propDescriptor = ObjectGetOwnPropertyDescriptor(
-        obj,
-        `${astProp.name}`,
-      );
-      const propHasGetter = typeof propDescriptor?.get === 'function';
-      return cb(propHasGetter);
-    }
-
-    return evalFn(
-      // Note: this eval runs the property expression, which might be side-effectful, for example
-      //       the user could be running `obj[getKey()].` where `getKey()` has some side effects.
-      //       Arguably this behavior should not be too surprising, but if it turns out that it is,
-      //       then we can revisit this behavior and add logic to analyze the property expression
-      //       and eval it only if we can confidently say that it can't have any side effects
-      `try { ${exprStr.slice(astProp.start, astProp.end)} } catch {} `,
-      ctx,
-      getREPLResourceName(),
-      (err, evaledProp) => {
-        if (err) {
-          return callback(false);
-        }
-
-        if (typeof evaledProp === 'string') {
-          if (safeIsProxyAccess(obj, evaledProp)) {
-            return cb(true);
-          }
-
-          const propDescriptor = ObjectGetOwnPropertyDescriptor(
-            obj,
-            evaledProp,
-          );
-          const propHasGetter = typeof propDescriptor?.get === 'function';
-          return cb(propHasGetter);
-        }
-
-        return callback(false);
-      },
-    );
-  }
-
-  function safeIsProxyAccess(obj, prop) {
-    // Accessing `prop` may trigger a getter that throws, so we use try-catch to guard against it
-    try {
-      return isProxy(obj[prop]);
-    } catch {
-      return false;
-    }
-  }
-
-  return callback(false);
-}
-
-
 // TODO(BridgeAR): This should be replaced with acorn to build an AST. The
 // language became more complex and using a simple approach like this is not
 // sufficient anymore.
@@ -2017,18 +1260,6 @@ function _memory(cmd) {
   }
 }
 
-function addCommonWords(completionGroups) {
-  // Only words which do not yet exist as global property should be added to
-  // this list.
-  ArrayPrototypePush(completionGroups, [
-    'async', 'await', 'break', 'case', 'catch', 'const', 'continue',
-    'debugger', 'default', 'delete', 'do', 'else', 'export', 'false',
-    'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof', 'let',
-    'new', 'null', 'return', 'switch', 'this', 'throw', 'true', 'try',
-    'typeof', 'var', 'void', 'while', 'with', 'yield',
-  ]);
-}
-
 function _turnOnEditorMode(repl) {
   repl.editorMode = true;
   ReflectApply(Interface.prototype.setPrompt, repl, ['']);
@@ -2169,31 +1400,31 @@ module.exports = {
 ObjectDefineProperty(module.exports, 'builtinModules', {
   __proto__: null,
   get: pendingDeprecation ? deprecate(
-    () => _builtinLibs,
+    () => getReplBuiltinLibs(),
     'repl.builtinModules is deprecated. Check module.builtinModules instead',
     'DEP0191',
-  ) : () => _builtinLibs,
+  ) : () => getReplBuiltinLibs(),
   set: pendingDeprecation ? deprecate(
-    (val) => _builtinLibs = val,
+    (val) => setReplBuiltinLibs(val),
     'repl.builtinModules is deprecated. Check module.builtinModules instead',
     'DEP0191',
-  ) : (val) => _builtinLibs = val,
+  ) : (val) => setReplBuiltinLibs(val),
   enumerable: false,
   configurable: true,
 });
 
 ObjectDefineProperty(module.exports, '_builtinLibs', {
   __proto__: null,
   get: pendingDeprecation ? deprecate(
-    () => _builtinLibs,
+    () => getReplBuiltinLibs(),
     'repl._builtinLibs is deprecated. Check module.builtinModules instead',
     'DEP0142',
-  ) : () => _builtinLibs,
+  ) : () => getReplBuiltinLibs(),
   set: pendingDeprecation ? deprecate(
-    (val) => _builtinLibs = val,
+    (val) => setReplBuiltinLibs(val),
     'repl._builtinLibs is deprecated. Check module.builtinModules instead',
     'DEP0142',
-  ) : (val) => _builtinLibs = val,
+  ) : (val) => setReplBuiltinLibs(val),
   enumerable: false,
   configurable: true,
 });