repl: move completion logic to internal module

PR-URL: https://github.com/nodejs/node/pull/59889
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
This commit is contained in:
Dario Piotrowicz 2025-10-08 08:37:22 +01:00 committed by GitHub
parent b13f24c2da
commit 200fe9e7f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 878 additions and 786 deletions

View File

@ -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,
};

View File

@ -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,
};

View File

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