node/lib/repl.js
Dario Piotrowicz 69453378fc
repl: improve REPL disabling completion on proxies and getters
https://github.com/nodejs/node/pull/57909 introduced the disabling
of REPL tab completion on object containing proxies and getters
(since such completion triggers code evaluation which can be
unexpected/disruptive for the user)

the solution in 57909 did not address all possible such cases,
the changes here improve on such solution by using acorn and
AST analysis to cover most if not all possible cases

PR-URL: https://github.com/nodejs/node/pull/58891
Reviewed-By: James M Snell <jasnell@gmail.com>
2025-07-09 20:52:25 +00:00

2177 lines
70 KiB
JavaScript

// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
/* A REPL library that you can include in your own code to get a runtime
* interface to your program.
*
* const repl = require("repl");
* // start repl on stdin
* repl.start("prompt> ");
*
* // listen for unix socket connections and start repl on them
* net.createServer(function(socket) {
* repl.start("node via Unix socket> ", socket);
* }).listen("/tmp/node-repl-sock");
*
* // listen for TCP socket connections and start repl on them
* net.createServer(function(socket) {
* repl.start("node via TCP socket> ", socket);
* }).listen(5001);
*
* // expose foo to repl context
* repl.start("node > ").context.foo = "stdin is fun";
*/
'use strict';
const {
ArrayPrototypeAt,
ArrayPrototypeFilter,
ArrayPrototypeFindLastIndex,
ArrayPrototypeForEach,
ArrayPrototypeIncludes,
ArrayPrototypeJoin,
ArrayPrototypeMap,
ArrayPrototypePop,
ArrayPrototypePush,
ArrayPrototypePushApply,
ArrayPrototypeShift,
ArrayPrototypeSlice,
ArrayPrototypeSome,
ArrayPrototypeSort,
ArrayPrototypeUnshift,
Boolean,
Error: MainContextError,
FunctionPrototypeBind,
JSONStringify,
MathMaxApply,
NumberIsNaN,
NumberParseFloat,
ObjectAssign,
ObjectDefineProperty,
ObjectGetOwnPropertyDescriptor,
ObjectGetOwnPropertyNames,
ObjectGetPrototypeOf,
ObjectKeys,
ObjectSetPrototypeOf,
Promise,
ReflectApply,
RegExp,
RegExpPrototypeExec,
SafePromiseRace,
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');
const {
decorateErrorStack,
isError,
deprecate,
deprecateInstantiation,
SideEffectFreeRegExpPrototypeSymbolReplace,
SideEffectFreeRegExpPrototypeSymbolSplit,
} = require('internal/util');
const { inspect } = require('internal/util/inspect');
const vm = require('vm');
const { runInThisContext, runInContext } = vm.Script.prototype;
const path = require('path');
const fs = require('fs');
const { Interface } = require('readline');
const {
commonPrefix,
} = require('internal/readline/utils');
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;
});
const {
ErrorPrepareStackTrace,
codes: {
ERR_CANNOT_WATCH_SIGINT,
ERR_INVALID_REPL_EVAL_CONFIG,
ERR_INVALID_REPL_INPUT,
ERR_MISSING_ARGS,
ERR_SCRIPT_EXECUTION_INTERRUPTED,
},
isErrorStackTraceLimitWritable,
overrideStackTrace,
} = require('internal/errors');
const { sendInspectorCommand } = require('internal/util/inspector');
const { getOptionValue } = require('internal/options');
const {
validateFunction,
validateObject,
} = require('internal/validators');
const experimentalREPLAwait = getOptionValue(
'--experimental-repl-await',
);
const pendingDeprecation = getOptionValue('--pending-deprecation');
const {
REPL_MODE_SLOPPY,
REPL_MODE_STRICT,
isRecoverableError,
kStandaloneREPL,
setupPreview,
setupReverseSearch,
isObjectLiteral,
} = require('internal/repl/utils');
const {
constants: {
ALL_PROPERTIES,
SKIP_SYMBOLS,
},
getOwnNonIndexProperties,
} = internalBinding('util');
const {
startSigintWatchdog,
stopSigintWatchdog,
} = internalBinding('contextify');
const {
extensionFormatMap,
} = require('internal/modules/esm/formats');
const {
makeContextifyScript,
} = require('internal/vm');
const {
kMultilinePrompt,
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);
// 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
// `eyes.js`.
const writer = (obj) => inspect(obj, writer.options);
writer.options = { ...inspect.defaultOptions, showProxy: true };
// Converts static import statement to dynamic import statement
const toDynamicImport = (codeLine) => {
let dynamicImportStatement = '';
const ast = acornParse(codeLine, { __proto__: null, sourceType: 'module', ecmaVersion: 'latest' });
acornWalk.ancestor(ast, {
ImportDeclaration(node) {
const awaitDynamicImport = `await import(${JSONStringify(node.source.value)});`;
if (node.specifiers.length === 0) {
dynamicImportStatement += awaitDynamicImport;
} else if (node.specifiers.length === 1 && node.specifiers[0].type === 'ImportNamespaceSpecifier') {
dynamicImportStatement += `const ${node.specifiers[0].local.name} = ${awaitDynamicImport}`;
} else {
const importNames = ArrayPrototypeJoin(ArrayPrototypeMap(node.specifiers, ({ local, imported }) =>
(local.name === imported?.name ? local.name : `${imported?.name ?? 'default'}: ${local.name}`),
), ', ');
dynamicImportStatement += `const { ${importNames} } = ${awaitDynamicImport}`;
}
},
});
return dynamicImportStatement;
};
function REPLServer(prompt,
stream,
eval_,
useGlobal,
ignoreUndefined,
replMode) {
if (!(this instanceof REPLServer)) {
return deprecateInstantiation(REPLServer, 'DEP0185', prompt, stream, eval_, useGlobal, ignoreUndefined, replMode);
}
let options;
if (prompt !== null && typeof prompt === 'object') {
// An options object was given.
options = { ...prompt };
stream = options.stream || options.socket;
eval_ = options.eval;
useGlobal = options.useGlobal;
ignoreUndefined = options.ignoreUndefined;
prompt = options.prompt;
replMode = options.replMode;
} else {
options = {};
}
if (!options.input && !options.output) {
// Legacy API, passing a 'stream'/'socket' option.
// Use stdin and stdout as the default streams if none were given.
stream ||= process;
// We're given a duplex readable/writable Stream, like a `net.Socket`
// or a custom object with 2 streams, or the `process` object.
options.input = stream.stdin || stream;
options.output = stream.stdout || stream;
}
if (options.terminal === undefined) {
options.terminal = options.output.isTTY;
}
options.terminal = !!options.terminal;
if (options.terminal && options.useColors === undefined) {
// If possible, check if stdout supports colors or not.
options.useColors = shouldColorize(options.output);
}
const preview = options.terminal &&
(options.preview !== undefined ? !!options.preview : !eval_);
ObjectDefineProperty(this, 'inputStream', {
__proto__: null,
get: pendingDeprecation ?
deprecate(() => this.input,
'repl.inputStream and repl.outputStream are deprecated. ' +
'Use repl.input and repl.output instead',
'DEP0141') :
() => this.input,
set: pendingDeprecation ?
deprecate((val) => this.input = val,
'repl.inputStream and repl.outputStream are deprecated. ' +
'Use repl.input and repl.output instead',
'DEP0141') :
(val) => this.input = val,
enumerable: false,
configurable: true,
});
ObjectDefineProperty(this, 'outputStream', {
__proto__: null,
get: pendingDeprecation ?
deprecate(() => this.output,
'repl.inputStream and repl.outputStream are deprecated. ' +
'Use repl.input and repl.output instead',
'DEP0141') :
() => this.output,
set: pendingDeprecation ?
deprecate((val) => this.output = val,
'repl.inputStream and repl.outputStream are deprecated. ' +
'Use repl.input and repl.output instead',
'DEP0141') :
(val) => this.output = val,
enumerable: false,
configurable: true,
});
this.allowBlockingCompletions = !!options.allowBlockingCompletions;
this.useColors = !!options.useColors;
this._domain = options.domain || domain.create();
this.useGlobal = !!useGlobal;
this.ignoreUndefined = !!ignoreUndefined;
this.replMode = replMode || module.exports.REPL_MODE_SLOPPY;
this.underscoreAssigned = false;
this.last = undefined;
this.underscoreErrAssigned = false;
this.lastError = undefined;
this.breakEvalOnSigint = !!options.breakEvalOnSigint;
this.editorMode = false;
// Context id for use with the inspector protocol.
this[kContextId] = undefined;
this[kLastCommandErrored] = false;
if (this.breakEvalOnSigint && eval_) {
// Allowing this would not reflect user expectations.
// breakEvalOnSigint affects only the behavior of the default eval().
throw new ERR_INVALID_REPL_EVAL_CONFIG();
}
if (options[kStandaloneREPL]) {
// It is possible to introspect the running REPL accessing this variable
// from inside the REPL. This is useful for anyone working on the REPL.
module.exports.repl = this;
} else if (!addedNewListener) {
// Add this listener only once and use a WeakSet that contains the REPLs
// domains. Otherwise we'd have to add a single listener to each REPL
// instance and that could trigger the `MaxListenersExceededWarning`.
process.prependListener('newListener', (event, listener) => {
if (event === 'uncaughtException' &&
process.domain &&
listener.name !== 'domainUncaughtExceptionClear' &&
domainSet.has(process.domain)) {
// Throw an error so that the event will not be added and the current
// domain takes over. That way the user is notified about the error
// and the current code evaluation is stopped, just as any other code
// that contains an error.
throw new ERR_INVALID_REPL_INPUT(
'Listeners for `uncaughtException` cannot be used in the REPL');
}
});
addedNewListener = true;
}
domainSet.add(this._domain);
const savedRegExMatches = ['', '', '', '', '', '', '', '', '', ''];
const sep = '\u0000\u0000\u0000';
const regExMatcher = new RegExp(`^${sep}(.*)${sep}(.*)${sep}(.*)${sep}(.*)` +
`${sep}(.*)${sep}(.*)${sep}(.*)${sep}(.*)` +
`${sep}(.*)$`);
eval_ ||= defaultEval;
const self = this;
// Pause taking in new input, and store the keys in a buffer.
const pausedBuffer = [];
let paused = false;
function pause() {
paused = true;
}
function unpause() {
if (!paused) return;
paused = false;
let entry;
const tmpCompletionEnabled = self.isCompletionEnabled;
while ((entry = ArrayPrototypeShift(pausedBuffer)) !== undefined) {
const { 0: type, 1: payload, 2: isCompletionEnabled } = entry;
switch (type) {
case 'key': {
const { 0: d, 1: key } = payload;
self.isCompletionEnabled = isCompletionEnabled;
self._ttyWrite(d, key);
break;
}
case 'close':
self.emit('exit');
break;
}
if (paused) {
break;
}
}
self.isCompletionEnabled = tmpCompletionEnabled;
}
function defaultEval(code, context, file, cb) {
let result, script, wrappedErr;
let err = null;
let wrappedCmd = false;
let awaitPromise = false;
const input = code;
if (isObjectLiteral(code)) {
// Add parentheses to make sure `code` is parsed as an expression
code = `(${StringPrototypeTrim(code)})\n`;
wrappedCmd = true;
}
const hostDefinedOptionId = Symbol(`eval:${file}`);
let parentURL;
try {
const { pathToFileURL } = require('internal/url');
// Adding `/repl` prevents dynamic imports from loading relative
// to the parent of `process.cwd()`.
parentURL = pathToFileURL(path.join(process.cwd(), 'repl')).href;
} catch {
// Continue regardless of error.
}
async function importModuleDynamically(specifier, _, importAttributes, phase) {
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
return cascadedLoader.import(specifier, parentURL, importAttributes,
phase === 'evaluation' ? cascadedLoader.kEvaluationPhase :
cascadedLoader.kSourcePhase);
}
// `experimentalREPLAwait` is set to true by default.
// Shall be false in case `--no-experimental-repl-await` flag is used.
if (experimentalREPLAwait && StringPrototypeIncludes(code, 'await')) {
if (processTopLevelAwait === undefined) {
({ processTopLevelAwait } = require('internal/repl/await'));
}
try {
const potentialWrappedCode = processTopLevelAwait(code);
if (potentialWrappedCode !== null) {
code = potentialWrappedCode;
wrappedCmd = true;
awaitPromise = true;
}
} catch (e) {
let recoverableError = false;
if (e.name === 'SyntaxError') {
// Remove all "await"s and attempt running the script
// in order to detect if error is truly non recoverable
const fallbackCode = SideEffectFreeRegExpPrototypeSymbolReplace(/\bawait\b/g, code, '');
try {
makeContextifyScript(
fallbackCode, // code
file, // filename,
0, // lineOffset
0, // columnOffset,
undefined, // cachedData
false, // produceCachedData
undefined, // parsingContext
hostDefinedOptionId, // hostDefinedOptionId
importModuleDynamically, // importModuleDynamically
);
} catch (fallbackError) {
if (isRecoverableError(fallbackError, fallbackCode)) {
recoverableError = true;
err = new Recoverable(e);
}
}
}
if (!recoverableError) {
decorateErrorStack(e);
err = e;
}
}
}
// First, create the Script object to check the syntax
if (code === '\n')
return cb(null);
if (err === null) {
while (true) {
try {
if (self.replMode === module.exports.REPL_MODE_STRICT &&
RegExpPrototypeExec(/^\s*$/, code) === null) {
// "void 0" keeps the repl from returning "use strict" as the result
// value for statements and declarations that don't return a value.
code = `'use strict'; void 0;\n${code}`;
}
script = makeContextifyScript(
code, // code
file, // filename,
0, // lineOffset
0, // columnOffset,
undefined, // cachedData
false, // produceCachedData
undefined, // parsingContext
hostDefinedOptionId, // hostDefinedOptionId
importModuleDynamically, // importModuleDynamically
);
} catch (e) {
debug('parse error %j', code, e);
if (wrappedCmd) {
// Unwrap and try again
wrappedCmd = false;
awaitPromise = false;
code = input;
wrappedErr = e;
continue;
}
// Preserve original error for wrapped command
const error = wrappedErr || e;
if (isRecoverableError(error, code))
err = new Recoverable(error);
else
err = error;
}
break;
}
}
// This will set the values from `savedRegExMatches` to corresponding
// predefined RegExp properties `RegExp.$1`, `RegExp.$2` ... `RegExp.$9`
RegExpPrototypeExec(regExMatcher,
ArrayPrototypeJoin(savedRegExMatches, sep));
let finished = false;
function finishExecution(err, result) {
if (finished) return;
finished = true;
// After executing the current expression, store the values of RegExp
// predefined properties back in `savedRegExMatches`
for (let idx = 1; idx < savedRegExMatches.length; idx += 1) {
savedRegExMatches[idx] = RegExp[`$${idx}`];
}
cb(err, result);
}
if (!err) {
// Unset raw mode during evaluation so that Ctrl+C raises a signal.
let previouslyInRawMode;
if (self.breakEvalOnSigint) {
// Start the SIGINT watchdog before entering raw mode so that a very
// quick Ctrl+C doesn't lead to aborting the process completely.
if (!startSigintWatchdog())
throw new ERR_CANNOT_WATCH_SIGINT();
previouslyInRawMode = self._setRawMode(false);
}
try {
try {
const scriptOptions = {
displayErrors: false,
breakOnSigint: self.breakEvalOnSigint,
};
if (self.useGlobal) {
result = ReflectApply(runInThisContext, script, [scriptOptions]);
} else {
result = ReflectApply(runInContext, script, [context, scriptOptions]);
}
} finally {
if (self.breakEvalOnSigint) {
// Reset terminal mode to its previous value.
self._setRawMode(previouslyInRawMode);
// Returns true if there were pending SIGINTs *after* the script
// has terminated without being interrupted itself.
if (stopSigintWatchdog()) {
self.emit('SIGINT');
}
}
}
} catch (e) {
err = e;
if (process.domain) {
debug('not recoverable, send to domain');
process.domain.emit('error', err);
process.domain.exit();
return;
}
}
if (awaitPromise && !err) {
let sigintListener;
pause();
let promise = result;
if (self.breakEvalOnSigint) {
const interrupt = new Promise((resolve, reject) => {
sigintListener = () => {
const tmp = MainContextError.stackTraceLimit;
if (isErrorStackTraceLimitWritable()) MainContextError.stackTraceLimit = 0;
const err = new ERR_SCRIPT_EXECUTION_INTERRUPTED();
if (isErrorStackTraceLimitWritable()) MainContextError.stackTraceLimit = tmp;
reject(err);
};
prioritizedSigintQueue.add(sigintListener);
});
promise = SafePromiseRace([promise, interrupt]);
}
(async () => {
try {
const result = (await promise)?.value;
finishExecution(null, result);
} catch (err) {
if (err && process.domain) {
debug('not recoverable, send to domain');
process.domain.emit('error', err);
process.domain.exit();
return;
}
finishExecution(err);
} finally {
// Remove prioritized SIGINT listener if it was not called.
prioritizedSigintQueue.delete(sigintListener);
unpause();
}
})();
}
}
if (!awaitPromise || err) {
finishExecution(err, result);
}
}
self.eval = self._domain.bind(eval_);
self._domain.on('error', function debugDomainError(e) {
debug('domain error');
let errStack = '';
if (typeof e === 'object' && e !== null) {
overrideStackTrace.set(e, (error, stackFrames) => {
let frames;
if (typeof stackFrames === 'object') {
// Search from the bottom of the call stack to
// find the first frame with a null function name
const idx = ArrayPrototypeFindLastIndex(
stackFrames,
(frame) => frame.getFunctionName() === null,
);
// If found, get rid of it and everything below it
frames = ArrayPrototypeSlice(stackFrames, 0, idx);
} else {
frames = stackFrames;
}
// FIXME(devsnek): this is inconsistent with the checks
// that the real prepareStackTrace dispatch uses in
// lib/internal/errors.js.
if (typeof MainContextError.prepareStackTrace === 'function') {
return MainContextError.prepareStackTrace(error, frames);
}
return ErrorPrepareStackTrace(error, frames);
});
decorateErrorStack(e);
if (e.domainThrown) {
delete e.domain;
delete e.domainThrown;
}
if (isError(e)) {
if (e.stack) {
if (e.name === 'SyntaxError') {
// Remove stack trace.
e.stack = SideEffectFreeRegExpPrototypeSymbolReplace(
/^\s+at\s.*\n?/gm,
SideEffectFreeRegExpPrototypeSymbolReplace(/^REPL\d+:\d+\r?\n/, e.stack, ''),
'');
const importErrorStr = 'Cannot use import statement outside a ' +
'module';
if (StringPrototypeIncludes(e.message, importErrorStr)) {
e.message = 'Cannot use import statement inside the Node.js ' +
'REPL, alternatively use dynamic import: ' + toDynamicImport(ArrayPrototypeAt(self.lines, -1));
e.stack = SideEffectFreeRegExpPrototypeSymbolReplace(
/SyntaxError:.*\n/,
e.stack,
`SyntaxError: ${e.message}\n`);
}
} else if (self.replMode === module.exports.REPL_MODE_STRICT) {
e.stack = SideEffectFreeRegExpPrototypeSymbolReplace(
/(\s+at\s+REPL\d+:)(\d+)/,
e.stack,
(_, pre, line) => pre + (line - 1),
);
}
}
errStack = self.writer(e);
// Remove one line error braces to keep the old style in place.
if (errStack[0] === '[' && errStack[errStack.length - 1] === ']') {
errStack = StringPrototypeSlice(errStack, 1, -1);
}
}
}
if (!self.underscoreErrAssigned) {
self.lastError = e;
}
if (options[kStandaloneREPL] &&
process.listenerCount('uncaughtException') !== 0) {
process.nextTick(() => {
process.emit('uncaughtException', e);
self.clearBufferedCommand();
self.lines.level = [];
if (!self.closed) {
self.displayPrompt();
}
});
} else {
if (errStack === '') {
errStack = self.writer(e);
}
const lines = SideEffectFreeRegExpPrototypeSymbolSplit(/(?<=\n)/, errStack);
let matched = false;
errStack = '';
ArrayPrototypeForEach(lines, (line) => {
if (!matched &&
RegExpPrototypeExec(/^\[?([A-Z][a-z0-9_]*)*Error/, line) !== null) {
errStack += writer.options.breakLength >= line.length ?
`Uncaught ${line}` :
`Uncaught:\n${line}`;
matched = true;
} else {
errStack += line;
}
});
if (!matched) {
const ln = lines.length === 1 ? ' ' : ':\n';
errStack = `Uncaught${ln}${errStack}`;
}
// Normalize line endings.
errStack += StringPrototypeEndsWith(errStack, '\n') ? '' : '\n';
self.output.write(errStack);
self.clearBufferedCommand();
self.lines.level = [];
if (!self.closed) {
self.displayPrompt();
}
}
});
self.clearBufferedCommand();
function completer(text, cb) {
ReflectApply(complete, self,
[text, self.editorMode ? self.completeOnEditorMode(cb) : cb]);
}
// All the parameters in the object are defining the "input" param of the
// InterfaceConstructor.
ReflectApply(Interface, this, [{
input: options.input,
output: options.output,
completer: options.completer || completer,
terminal: options.terminal,
historySize: options.historySize,
prompt,
}]);
self.resetContext();
this.commands = { __proto__: null };
defineDefaultCommands(this);
// Figure out which "writer" function to use
self.writer = options.writer || module.exports.writer;
if (self.writer === writer) {
// Conditionally turn on ANSI coloring.
writer.options.colors = self.useColors;
if (options[kStandaloneREPL]) {
ObjectDefineProperty(inspect, 'replDefaults', {
__proto__: null,
get() {
return writer.options;
},
set(options) {
validateObject(options, 'options');
return ObjectAssign(writer.options, options);
},
enumerable: true,
configurable: true,
});
}
}
function _parseREPLKeyword(keyword, rest) {
const cmd = this.commands[keyword];
if (cmd) {
ReflectApply(cmd.action, this, [rest]);
return true;
}
return false;
}
self.on('close', function emitExit() {
if (paused) {
ArrayPrototypePush(pausedBuffer, ['close']);
return;
}
self.emit('exit');
});
let sawSIGINT = false;
let sawCtrlD = false;
const prioritizedSigintQueue = new SafeSet();
self.on('SIGINT', function onSigInt() {
if (prioritizedSigintQueue.size > 0) {
for (const task of prioritizedSigintQueue) {
task();
}
return;
}
const empty = self.line.length === 0;
self.clearLine();
_turnOffEditorMode(self);
const cmd = self[kBufferedCommandSymbol];
if (!(cmd && cmd.length > 0) && empty) {
if (sawSIGINT) {
self.close();
sawSIGINT = false;
return;
}
self.output.write(
'(To exit, press Ctrl+C again or Ctrl+D or type .exit)\n',
);
sawSIGINT = true;
} else {
sawSIGINT = false;
}
self.clearBufferedCommand();
self.lines.level = [];
self.displayPrompt();
});
self.on('line', function onLine(cmd) {
debug('line %j', cmd);
cmd ||= '';
sawSIGINT = false;
if (self.editorMode) {
self[kBufferedCommandSymbol] += cmd + '\n';
// code alignment
const matches = self._sawKeyPress && !self[kLoadingSymbol] ?
RegExpPrototypeExec(/^\s+/, cmd) : null;
if (matches) {
const prefix = matches[0];
self.write(prefix);
self.line = prefix;
self.cursor = prefix.length;
}
ReflectApply(_memory, self, [cmd]);
return;
}
// Check REPL keywords and empty lines against a trimmed line input.
const trimmedCmd = StringPrototypeTrim(cmd);
// Check to see if a REPL keyword was used. If it returns true,
// display next prompt and return.
if (trimmedCmd) {
if (StringPrototypeCharAt(trimmedCmd, 0) === '.' &&
StringPrototypeCharAt(trimmedCmd, 1) !== '.' &&
NumberIsNaN(NumberParseFloat(trimmedCmd))) {
const matches = RegExpPrototypeExec(/^\.([^\s]+)\s*(.*)$/, trimmedCmd);
const keyword = matches?.[1];
const rest = matches?.[2];
if (ReflectApply(_parseREPLKeyword, self, [keyword, rest]) === true) {
return;
}
if (!self[kBufferedCommandSymbol]) {
self.output.write('Invalid REPL keyword\n');
finish(null);
return;
}
}
}
const evalCmd = self[kBufferedCommandSymbol] + cmd + '\n';
debug('eval %j', evalCmd);
self.eval(evalCmd, self.context, getREPLResourceName(), finish);
function finish(e, ret) {
debug('finish', e, ret);
ReflectApply(_memory, self, [cmd]);
if (e && !self[kBufferedCommandSymbol] &&
StringPrototypeStartsWith(StringPrototypeTrim(cmd), 'npm ') &&
!(e instanceof Recoverable)
) {
self.output.write('npm should be run outside of the ' +
'Node.js REPL, in your normal shell.\n' +
'(Press Ctrl+D to exit.)\n');
self.displayPrompt();
return;
}
// If error was SyntaxError and not JSON.parse error
// We can start a multiline command
if (e instanceof Recoverable && !sawCtrlD) {
if (self.terminal) {
self[kAddNewLineOnTTY]();
} else {
self[kBufferedCommandSymbol] += cmd + '\n';
self.displayPrompt();
}
return;
}
if (e) {
self._domain.emit('error', e.err || e);
self[kLastCommandErrored] = true;
}
// Clear buffer if no SyntaxErrors
self.clearBufferedCommand();
sawCtrlD = false;
// If we got any output - print it (if no error)
if (!e &&
// When an invalid REPL command is used, error message is printed
// immediately. We don't have to print anything else. So, only when
// the second argument to this function is there, print it.
arguments.length === 2 &&
(!self.ignoreUndefined || ret !== undefined)) {
if (!self.underscoreAssigned) {
self.last = ret;
}
self.output.write(self.writer(ret) + '\n');
}
// If the REPL sever hasn't closed display prompt again (unless we already
// did by emitting the 'error' event on the domain instance).
if (!self.closed && !e) {
self[kLastCommandErrored] = false;
self.displayPrompt();
}
}
});
self.on('SIGCONT', function onSigCont() {
if (self.editorMode) {
self.output.write(`${self._initialPrompt}.editor\n`);
self.output.write(
'// Entering editor mode (Ctrl+D to finish, Ctrl+C to cancel)\n');
self.output.write(`${self[kBufferedCommandSymbol]}\n`);
self.prompt(true);
} else {
self.displayPrompt(true);
}
});
const { reverseSearch } = setupReverseSearch(this);
const {
clearPreview,
showPreview,
} = setupPreview(
this,
kContextId,
kBufferedCommandSymbol,
preview,
);
// Wrap readline tty to enable editor mode and pausing.
const ttyWrite = FunctionPrototypeBind(self._ttyWrite, self);
self._ttyWrite = (d, key) => {
key ||= {};
if (paused && !(self.breakEvalOnSigint && key.ctrl && key.name === 'c')) {
ArrayPrototypePush(pausedBuffer,
['key', [d, key], self.isCompletionEnabled]);
return;
}
if (!self.editorMode || !self.terminal) {
// Before exiting, make sure to clear the line.
if (key.ctrl && key.name === 'd' &&
self.cursor === 0 && self.line.length === 0) {
self.clearLine();
}
clearPreview(key);
if (!reverseSearch(d, key)) {
ttyWrite(d, key);
const showCompletionPreview = key.name !== 'escape';
showPreview(showCompletionPreview);
}
return;
}
// Editor mode
if (key.ctrl && !key.shift) {
switch (key.name) {
// TODO(BridgeAR): There should not be a special mode necessary for full
// multiline support.
case 'd': // End editor mode
_turnOffEditorMode(self);
sawCtrlD = true;
ttyWrite(d, { name: 'return' });
break;
case 'n': // Override next history item
case 'p': // Override previous history item
break;
default:
ttyWrite(d, key);
}
} else {
switch (key.name) {
case 'up': // Override previous history item
case 'down': // Override next history item
break;
case 'tab':
// Prevent double tab behavior
self._previousKey = null;
ttyWrite(d, key);
break;
default:
ttyWrite(d, key);
}
}
};
self.displayPrompt();
}
ObjectSetPrototypeOf(REPLServer.prototype, Interface.prototype);
ObjectSetPrototypeOf(REPLServer, Interface);
// Prompt is a string to print on each line for the prompt,
// source is a stream to use for I/O, defaulting to stdin/stdout.
function start(prompt, source, eval_, useGlobal, ignoreUndefined, replMode) {
return new REPLServer(
prompt, source, eval_, useGlobal, ignoreUndefined, replMode);
}
REPLServer.prototype.setupHistory = function setupHistory(historyConfig = {}, cb) {
// TODO(puskin94): necessary because historyConfig can be a string for backwards compatibility
const options = typeof historyConfig === 'string' ?
{ filePath: historyConfig } :
historyConfig;
if (typeof cb === 'function') {
options.onHistoryFileLoaded = cb;
}
this.setupHistoryManager(options);
};
REPLServer.prototype.clearBufferedCommand = function clearBufferedCommand() {
this[kBufferedCommandSymbol] = '';
};
REPLServer.prototype.close = function close() {
if (this.terminal && this.historyManager.isFlushing && !this._closingOnFlush) {
this._closingOnFlush = true;
this.once('flushHistory', () =>
ReflectApply(Interface.prototype.close, this, []),
);
return;
}
process.nextTick(() =>
ReflectApply(Interface.prototype.close, this, []),
);
};
REPLServer.prototype.createContext = function() {
let context;
if (this.useGlobal) {
context = globalThis;
} else {
sendInspectorCommand((session) => {
session.post('Runtime.enable');
session.once('Runtime.executionContextCreated', ({ params }) => {
this[kContextId] = params.context.id;
});
context = vm.createContext();
session.post('Runtime.disable');
}, () => {
context = vm.createContext();
});
ArrayPrototypeForEach(ObjectGetOwnPropertyNames(globalThis), (name) => {
// Only set properties that do not already exist as a global builtin.
if (!globalBuiltins.has(name)) {
ObjectDefineProperty(context, name,
{
__proto__: null,
...ObjectGetOwnPropertyDescriptor(globalThis, name),
});
}
});
context.global = context;
const _console = new Console(this.output);
ObjectDefineProperty(context, 'console', {
__proto__: null,
configurable: true,
writable: true,
value: _console,
});
}
const replModule = new CJSModule('<repl>');
replModule.paths = CJSModule._resolveLookupPaths('<repl>', parentModule);
ObjectDefineProperty(context, 'module', {
__proto__: null,
configurable: true,
writable: true,
value: replModule,
});
ObjectDefineProperty(context, 'require', {
__proto__: null,
configurable: true,
writable: true,
value: makeRequireFunction(replModule),
});
addBuiltinLibsToObject(context, '<REPL>');
return context;
};
REPLServer.prototype.resetContext = function() {
this.context = this.createContext();
this.underscoreAssigned = false;
this.underscoreErrAssigned = false;
// TODO(BridgeAR): Deprecate the lines.
this.lines = [];
this.lines.level = [];
ObjectDefineProperty(this.context, '_', {
__proto__: null,
configurable: true,
get: () => this.last,
set: (value) => {
this.last = value;
if (!this.underscoreAssigned) {
this.underscoreAssigned = true;
this.output.write('Expression assignment to _ now disabled.\n');
}
},
});
ObjectDefineProperty(this.context, '_error', {
__proto__: null,
configurable: true,
get: () => this.lastError,
set: (value) => {
this.lastError = value;
if (!this.underscoreErrAssigned) {
this.underscoreErrAssigned = true;
this.output.write(
'Expression assignment to _error now disabled.\n');
}
},
});
// Allow REPL extensions to extend the new context
this.emit('reset', this.context);
};
REPLServer.prototype.displayPrompt = function(preserveCursor) {
let prompt = this._initialPrompt;
if (this[kBufferedCommandSymbol].length) {
prompt = kMultilinePrompt.description;
}
// Do not overwrite `_initialPrompt` here
ReflectApply(Interface.prototype.setPrompt, this, [prompt]);
this.prompt(preserveCursor);
};
// When invoked as an API method, overwrite _initialPrompt
REPLServer.prototype.setPrompt = function setPrompt(prompt) {
this._initialPrompt = 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;
const lastDeclarationInitCode = code.slice(lastDeclarationInit.start, lastDeclarationInit.end);
return findExpressionCompleteTarget(lastDeclarationInitCode);
}
// 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;
}
/**
* 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.
*/
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 (isProxy(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 (isProxy(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 (isProxy(obj[evaledProp])) {
return cb(true);
}
const propDescriptor = ObjectGetOwnPropertyDescriptor(
obj,
evaledProp,
);
const propHasGetter = typeof propDescriptor?.get === 'function';
return cb(propHasGetter);
}
return callback(false);
},
);
}
return callback(false);
}
REPLServer.prototype.completeOnEditorMode = (callback) => (err, results) => {
if (err) return callback(err);
const { 0: completions, 1: completeOn = '' } = results;
let result = ArrayPrototypeFilter(completions, Boolean);
if (completeOn && result.length !== 0) {
result = [commonPrefix(result)];
}
callback(null, [result, completeOn]);
};
REPLServer.prototype.defineCommand = function(keyword, cmd) {
if (typeof cmd === 'function') {
cmd = { action: cmd };
} else {
validateFunction(cmd.action, 'cmd.action');
}
this.commands[keyword] = cmd;
};
// 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.
function _memory(cmd) {
const self = this;
self.lines ||= [];
self.lines.level ||= [];
// Save the line so I can do magic later
if (cmd) {
const len = self.lines.level.length ? self.lines.level.length - 1 : 0;
ArrayPrototypePush(self.lines, StringPrototypeRepeat(' ', len) + cmd);
} else {
// I don't want to not change the format too much...
ArrayPrototypePush(self.lines, '');
}
if (!cmd) {
self.lines.level = [];
return;
}
// I need to know "depth."
// Because I can not tell the difference between a } that
// closes an object literal and a } that closes a function
const countMatches = (regex, str) => {
let count = 0;
while (RegExpPrototypeExec(regex, str) !== null) count++;
return count;
};
// Going down is { and ( e.g. function() {
// going up is } and )
const dw = countMatches(/[{(]/g, cmd);
const up = countMatches(/[})]/g, cmd);
let depth = dw.length - up.length;
if (depth) {
(function workIt() {
if (depth > 0) {
// Going... down.
// Push the line#, depth count, and if the line is a function.
// Since JS only has functional scope I only need to remove
// "function() {" lines, clearly this will not work for
// "function()
// {" but nothing should break, only tab completion for local
// scope will not work for this function.
ArrayPrototypePush(self.lines.level, {
line: self.lines.length - 1,
depth: depth,
});
} else if (depth < 0) {
// Going... up.
const curr = ArrayPrototypePop(self.lines.level);
if (curr) {
const tmp = curr.depth + depth;
if (tmp < 0) {
// More to go, recurse
depth += curr.depth;
workIt();
} else if (tmp > 0) {
// Remove and push back
curr.depth += depth;
ArrayPrototypePush(self.lines.level, curr);
}
}
}
}());
}
}
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, ['']);
}
function _turnOffEditorMode(repl) {
repl.editorMode = false;
repl.setPrompt(repl._initialPrompt);
}
function defineDefaultCommands(repl) {
repl.defineCommand('break', {
help: 'Sometimes you get stuck, this gets you out',
action: function() {
this.clearBufferedCommand();
this.displayPrompt();
},
});
let clearMessage;
if (repl.useGlobal) {
clearMessage = 'Alias for .break';
} else {
clearMessage = 'Break, and also clear the local context';
}
repl.defineCommand('clear', {
help: clearMessage,
action: function() {
this.clearBufferedCommand();
if (!this.useGlobal) {
this.output.write('Clearing context...\n');
this.resetContext();
}
this.displayPrompt();
},
});
repl.defineCommand('exit', {
help: 'Exit the REPL',
action: function() {
this.close();
},
});
repl.defineCommand('help', {
help: 'Print this help message',
action: function() {
const names = ArrayPrototypeSort(ObjectKeys(this.commands));
const longestNameLength = MathMaxApply(
ArrayPrototypeMap(names, (name) => name.length),
);
ArrayPrototypeForEach(names, (name) => {
const cmd = this.commands[name];
const spaces =
StringPrototypeRepeat(' ', longestNameLength - name.length + 3);
const line = `.${name}${cmd.help ? spaces + cmd.help : ''}\n`;
this.output.write(line);
});
this.output.write('\nPress Ctrl+C to abort current expression, ' +
'Ctrl+D to exit the REPL\n');
this.displayPrompt();
},
});
repl.defineCommand('save', {
help: 'Save all evaluated commands in this REPL session to a file',
action: function(file) {
try {
if (file === '') {
throw new ERR_MISSING_ARGS('file');
}
fs.writeFileSync(file, ArrayPrototypeJoin(this.lines, '\n'));
this.output.write(`Session saved to: ${file}\n`);
} catch (error) {
if (error instanceof ERR_MISSING_ARGS) {
this.output.write(`${error.message}\n`);
} else {
this.output.write(`Failed to save: ${file}\n`);
}
}
this.displayPrompt();
},
});
repl.defineCommand('load', {
help: 'Load JS from a file into the REPL session',
action: function(file) {
try {
if (file === '') {
throw new ERR_MISSING_ARGS('file');
}
const stats = fs.statSync(file);
if (stats && stats.isFile()) {
_turnOnEditorMode(this);
this[kLoadingSymbol] = true;
const data = fs.readFileSync(file, 'utf8');
this.write(data);
this[kLoadingSymbol] = false;
_turnOffEditorMode(this);
this.write('\n');
} else {
this.output.write(
`Failed to load: ${file} is not a valid file\n`,
);
}
} catch (error) {
if (error instanceof ERR_MISSING_ARGS) {
this.output.write(`${error.message}\n`);
} else {
this.output.write(`Failed to load: ${file}\n`);
}
}
this.displayPrompt();
},
});
if (repl.terminal) {
repl.defineCommand('editor', {
help: 'Enter editor mode',
action() {
_turnOnEditorMode(this);
this.output.write(
'// Entering editor mode (Ctrl+D to finish, Ctrl+C to cancel)\n');
},
});
}
}
function Recoverable(err) {
this.err = err;
}
ObjectSetPrototypeOf(Recoverable.prototype, SyntaxErrorPrototype);
ObjectSetPrototypeOf(Recoverable, SyntaxError);
module.exports = {
start,
writer,
REPLServer,
REPL_MODE_SLOPPY,
REPL_MODE_STRICT,
Recoverable,
};
ObjectDefineProperty(module.exports, 'builtinModules', {
__proto__: null,
get: pendingDeprecation ? deprecate(
() => _builtinLibs,
'repl.builtinModules is deprecated. Check module.builtinModules instead',
'DEP0191',
) : () => _builtinLibs,
set: pendingDeprecation ? deprecate(
(val) => _builtinLibs = val,
'repl.builtinModules is deprecated. Check module.builtinModules instead',
'DEP0191',
) : (val) => _builtinLibs = val,
enumerable: false,
configurable: true,
});
ObjectDefineProperty(module.exports, '_builtinLibs', {
__proto__: null,
get: pendingDeprecation ? deprecate(
() => _builtinLibs,
'repl._builtinLibs is deprecated. Check module.builtinModules instead',
'DEP0142',
) : () => _builtinLibs,
set: pendingDeprecation ? deprecate(
(val) => _builtinLibs = val,
'repl._builtinLibs is deprecated. Check module.builtinModules instead',
'DEP0142',
) : (val) => _builtinLibs = val,
enumerable: false,
configurable: true,
});