Repository: nodejs/node
Pull request: [v24.x backport] repl: move completion logic to internal module

Backport of https://github.com/nodejs/node/pull/59889

<!--
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,
   ObjectSetPrototypeOf,
   Promise,
@@ -80,34 +75,23 @@ const {
   SafeSet,
   SafeWeakSet,
   StringPrototypeCharAt,
-  StringPrototypeCodePointAt,
   StringPrototypeEndsWith,
   StringPrototypeIncludes,
   StringPrototypeRepeat,
   StringPrototypeSlice,
-  StringPrototypeSplit,
   StringPrototypeStartsWith,
-  StringPrototypeToLocaleLowerCase,
   StringPrototypeTrim,
-  StringPrototypeTrimStart,
   Symbol,
   SyntaxError,
   SyntaxErrorPrototype,
   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');
@@ -133,16 +117,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;
@@ -178,22 +152,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');
@@ -202,41 +175,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
@@ -1227,716 +1178,10 @@ REPLServer.prototype.setPrompt = function setPrompt(prompt) {
   ReflectApply(Interface.prototype.setPrompt, this, [prompt]);
 };
 
-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;
-  }, () => []);
-}
-
 REPLServer.prototype.complete = function() {
   ReflectApply(this.completer, this, arguments);
 };
 
-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);
-}
-
 REPLServer.prototype.completeOnEditorMode = (callback) => (err, results) => {
   if (err) return callback(err);
 
@@ -2030,18 +1275,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, ['']);
@@ -2188,31 +1421,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,
 });