mirror of
https://github.com/zebrajr/node.git
synced 2026-01-15 12:15:26 +00:00
PR-URL: https://github.com/nodejs/node/pull/61043 Reviewed-By: Chemi Atlow <chemi@atlow.co.il> Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de> Reviewed-By: Aviv Keller <me@aviv.sh>
803 lines
28 KiB
JavaScript
803 lines
28 KiB
JavaScript
'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);
|
|
}
|
|
|
|
// If the last statement is an expression statement with "new" syntax
|
|
// we want to extract the callee for completion (e.g. for `new Sample` we want `Sample`)
|
|
if (lastBodyStatement.type === 'ExpressionStatement' &&
|
|
lastBodyStatement.expression.type === 'NewExpression' &&
|
|
lastBodyStatement.expression.callee) {
|
|
const callee = lastBodyStatement.expression.callee;
|
|
const calleeCode = code.slice(callee.start, callee.end);
|
|
return findExpressionCompleteTarget(calleeCode);
|
|
}
|
|
|
|
// 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
|
|
return propHasGetterOrIsProxy(obj, astProp.value, cb);
|
|
}
|
|
|
|
if (
|
|
astProp.type === 'Identifier' &&
|
|
exprStr.at(astProp.start - 1) === '.'
|
|
) {
|
|
// We have something like `obj.foo.x` where `foo` is the identifier
|
|
return propHasGetterOrIsProxy(obj, astProp.name, cb);
|
|
}
|
|
|
|
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 cb(false);
|
|
}
|
|
|
|
if (typeof evaledProp === 'string') {
|
|
return propHasGetterOrIsProxy(obj, evaledProp, cb);
|
|
}
|
|
|
|
return cb(false);
|
|
},
|
|
);
|
|
}
|
|
|
|
return callback(false);
|
|
}
|
|
|
|
/**
|
|
* Given an object and a property name, checks whether the property has a getter, if not checks whether its
|
|
* value is a proxy.
|
|
*
|
|
* Note: the order is relevant here, we want to check whether the property has a getter _before_ we check
|
|
* whether its value is a proxy, to ensure that is the property does have a getter we don't end up
|
|
* triggering it when checking its value
|
|
* @param {any} obj The target object
|
|
* @param {string | number | bigint | boolean | RegExp} prop The target property
|
|
* @param {(includes: boolean) => void} cb Callback that will be called with the result of the operation
|
|
* @returns {void}
|
|
*/
|
|
function propHasGetterOrIsProxy(obj, prop, cb) {
|
|
const propDescriptor = ObjectGetOwnPropertyDescriptor(
|
|
obj,
|
|
prop,
|
|
);
|
|
const propHasGetter = typeof propDescriptor?.get === 'function';
|
|
if (propHasGetter) {
|
|
return cb(true);
|
|
}
|
|
|
|
if (isProxy(obj[prop])) {
|
|
return cb(true);
|
|
}
|
|
|
|
return cb(false);
|
|
}
|
|
|
|
module.exports = {
|
|
complete,
|
|
};
|