mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 18:25:13 +08:00
Signed-off-by: Aviv Keller <me@aviv.sh> PR-URL: https://github.com/nodejs/node/pull/64034 Fixes: https://github.com/nodejs/node/issues/64523 Closes: https://github.com/nodejs/node/pull/64534 Fixes: https://github.com/nodejs/node/issues/39387 Closes: https://github.com/nodejs/node/pull/39392 Fixes: https://github.com/nodejs/node/issues/61390 Fixes: https://github.com/nodejs/node/issues/63126 Fixes: https://github.com/nodejs/node/issues/38503 Fixes: https://github.com/nodejs/node/issues/37445 Fixes: https://github.com/nodejs/node/issues/38145 Fixes: https://github.com/nodejs/node/issues/33369 Fixes: https://github.com/nodejs/node/issues/48131 Fixes: https://github.com/nodejs/node/issues/8309 Fixes: https://github.com/nodejs/node/issues/39689 Fixes: https://github.com/nodejs/node/issues/18931 Reviewed-By: Dario Piotrowicz <dario.piotrowicz@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Filip Skokan <panva.ip@gmail.com>
743 lines
23 KiB
JavaScript
743 lines
23 KiB
JavaScript
'use strict';
|
|
|
|
const {
|
|
ArrayPrototypeFilter,
|
|
ArrayPrototypeForEach,
|
|
ArrayPrototypeIncludes,
|
|
ArrayPrototypeMap,
|
|
ArrayPrototypePop,
|
|
ArrayPrototypePush,
|
|
ArrayPrototypePushApply,
|
|
ArrayPrototypeShift,
|
|
ArrayPrototypeSlice,
|
|
ArrayPrototypeSome,
|
|
ArrayPrototypeSort,
|
|
ArrayPrototypeUnshift,
|
|
ObjectGetOwnPropertyDescriptor,
|
|
ObjectGetPrototypeOf,
|
|
ObjectKeys,
|
|
ReflectApply,
|
|
RegExpPrototypeExec,
|
|
SafeSet,
|
|
StringPrototypeCharCodeAt,
|
|
StringPrototypeCodePointAt,
|
|
StringPrototypeEndsWith,
|
|
StringPrototypeSlice,
|
|
StringPrototypeStartsWith,
|
|
StringPrototypeToLocaleLowerCase,
|
|
StringPrototypeTrimStart,
|
|
} = primordials;
|
|
|
|
const {
|
|
kContextId,
|
|
getGlobalBuiltins,
|
|
getReplBuiltinLibs,
|
|
fixReplRequire,
|
|
} = require('internal/repl/utils');
|
|
|
|
const { getReplInspector } = require('internal/repl/inspector');
|
|
|
|
const CJSModule = require('internal/modules/cjs/loader').Module;
|
|
|
|
const {
|
|
extensionFormatMap,
|
|
} = require('internal/modules/esm/get_format');
|
|
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
|
|
const {
|
|
constants: {
|
|
ALL_PROPERTIES,
|
|
SKIP_SYMBOLS,
|
|
},
|
|
getOwnNonIndexProperties,
|
|
} = internalBinding('util');
|
|
|
|
const replCommandRE = /^\s*\.(\w*)$/;
|
|
const importRE = /\bimport\s*(?:\(\s*|[^'"`]*?\bfrom\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+/;
|
|
const relativePathRE = /^\.\.?\//;
|
|
const completableLastCharRE = /\w|\.|\$/;
|
|
const declarationKeywordRE = /(?:^|[^\w$])(?:var|let|const|class|function)\s+$/;
|
|
|
|
const kCompletionObjectGroup = 'node-repl-completion';
|
|
|
|
fixReplRequire(module);
|
|
|
|
const { BuiltinModule } = require('internal/bootstrap/realm');
|
|
|
|
const nodeSchemeBuiltinLibs = ArrayPrototypeMap(getReplBuiltinLibs(), (lib) => `node:${lib}`);
|
|
ArrayPrototypeForEach(
|
|
BuiltinModule.getSchemeOnlyModuleNames(),
|
|
(lib) => ArrayPrototypePush(nodeSchemeBuiltinLibs, `node:${lib}`),
|
|
);
|
|
|
|
async function complete(line, repl) {
|
|
// Ignore leading whitespace; it could change the outcome (e.g. a lone space
|
|
// should still offer the global scope rather than nothing).
|
|
line = StringPrototypeTrimStart(line);
|
|
|
|
let match;
|
|
let result;
|
|
if ((match = RegExpPrototypeExec(replCommandRE, line)) !== null) {
|
|
result = completeReplCommand(repl, match);
|
|
} else if ((match = RegExpPrototypeExec(requireRE, line)) !== null) {
|
|
result = completeRequireSpecifier(repl, match);
|
|
} else if ((match = RegExpPrototypeExec(importRE, line)) !== null) {
|
|
result = completeImportSpecifier(repl, match);
|
|
} else if ((match = RegExpPrototypeExec(fsAutoCompleteRE, line)) !== null &&
|
|
repl.allowBlockingCompletions) {
|
|
result = completeFsPath(match);
|
|
} else if (line.length === 0 ||
|
|
RegExpPrototypeExec(completableLastCharRE, line[line.length - 1]) !== null) {
|
|
result = await completeExpression(repl, line);
|
|
} else {
|
|
result = emptyCompletion();
|
|
}
|
|
|
|
return finalizeCompletions(result.completionGroups, result.filter, result.completeOn);
|
|
}
|
|
|
|
/**
|
|
* Filters, sorts, de-duplicates and merges the completion groups into the
|
|
* `[completions, completeOn]` tuple readline expects.
|
|
* @returns {[string[], string]} The completions and the string to complete on.
|
|
*/
|
|
function finalizeCompletions(completionGroups, filter, completeOn) {
|
|
if (completionGroups.length && filter) {
|
|
const lowerCaseFilter = StringPrototypeToLocaleLowerCase(filter);
|
|
const newCompletionGroups = [];
|
|
ArrayPrototypeForEach(completionGroups, (group) => {
|
|
const filteredGroup = ArrayPrototypeFilter(group, (str) =>
|
|
// Filtering is always case-insensitive, following chromium autocomplete
|
|
// behavior.
|
|
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('');
|
|
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);
|
|
}
|
|
|
|
return [completions, completeOn];
|
|
}
|
|
|
|
// A result with nothing to complete (also the shape every handler returns).
|
|
function emptyCompletion() {
|
|
return { __proto__: null, completionGroups: [], completeOn: undefined, filter: '' };
|
|
}
|
|
|
|
// Completes REPL dot-commands, e.g. `.bre` -> `.break`.
|
|
function completeReplCommand(repl, match) {
|
|
const completeOn = match[1];
|
|
return {
|
|
__proto__: null,
|
|
completionGroups: [ObjectKeys(repl.commands)],
|
|
completeOn,
|
|
filter: completeOn,
|
|
};
|
|
}
|
|
|
|
// Completes the specifier inside `require('...')` with the builtin module names
|
|
// and, when blocking completions are allowed, on-disk files and directories.
|
|
function completeRequireSpecifier(repl, match) {
|
|
return completeModuleSpecifier(repl, match, {
|
|
__proto__: null,
|
|
// `CJSModule._extensions` can be augmented at runtime (e.g. by loaders), so
|
|
// it is read on every completion rather than cached at module load.
|
|
extensions: ObjectKeys(CJSModule._extensions),
|
|
extraIndexes: ['package.json', 'index'],
|
|
getBasePaths() {
|
|
const paths = [];
|
|
ArrayPrototypePushApply(paths, module.paths);
|
|
ArrayPrototypePushApply(paths, CJSModule.globalPaths);
|
|
return paths;
|
|
},
|
|
// `require('foo')` resolves without the extension, and `require('foo/index')`
|
|
// is redundant with `require('foo')`.
|
|
formatFile(subdir, name, extension) {
|
|
const base = StringPrototypeSlice(name, 0, name.length - extension.length);
|
|
if (subdir && base === 'index') {
|
|
return null;
|
|
}
|
|
return `${subdir}${base}`;
|
|
},
|
|
// CommonJS resolution treats any directory holding an index/`package.json`
|
|
// as requirable, not only those under `node_modules`.
|
|
listsDirectoryIndex() {
|
|
return true;
|
|
},
|
|
});
|
|
}
|
|
|
|
// Completes the specifier inside `import('...')` with the builtin module names
|
|
// and, when blocking completions are allowed, importable on-disk files and
|
|
// directories.
|
|
function completeImportSpecifier(repl, match) {
|
|
return completeModuleSpecifier(repl, match, {
|
|
__proto__: null,
|
|
extensions: ObjectKeys(extensionFormatMap),
|
|
extraIndexes: ['package.json'],
|
|
getBasePaths() {
|
|
return ArrayPrototypeSlice(module.paths);
|
|
},
|
|
// `import()` needs the explicit file extension.
|
|
formatFile(subdir, name) {
|
|
return `${subdir}${name}`;
|
|
},
|
|
// Only bare specifiers resolved from `node_modules` honor a directory's
|
|
// index/`package.json`; relative imports must be spelled out in full.
|
|
listsDirectoryIndex(subdir, isInNodeModules) {
|
|
return !subdir && isInNodeModules;
|
|
},
|
|
});
|
|
}
|
|
|
|
// Shared driver for `require('...')` and `import('...')` specifier completion.
|
|
// Both offer the builtin module names plus, when blocking completions are
|
|
// allowed, the files and directories reachable from a set of base paths; the
|
|
// `options` object captures the few places their resolution rules differ.
|
|
function completeModuleSpecifier(repl, match, options) {
|
|
const completeOn = match[1];
|
|
const completionGroups = [];
|
|
|
|
if (repl.allowBlockingCompletions) {
|
|
const { extensions, getBasePaths, formatFile, listsDirectoryIndex } = options;
|
|
const subdir = match[2] || '';
|
|
// Filenames that mark a directory as loadable without a trailing slash.
|
|
const indexes = ArrayPrototypeMap(extensions, (ext) => `index${ext}`);
|
|
ArrayPrototypePushApply(indexes, options.extraIndexes);
|
|
|
|
const group = [];
|
|
let paths = [];
|
|
if (completeOn === '.') {
|
|
ArrayPrototypePush(group, './', '../');
|
|
} else if (completeOn === '..') {
|
|
ArrayPrototypePush(group, '../');
|
|
} else if (RegExpPrototypeExec(relativePathRE, completeOn) !== null) {
|
|
paths = [process.cwd()];
|
|
} else {
|
|
paths = getBasePaths();
|
|
}
|
|
|
|
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 (ArrayPrototypeIncludes(extensions, extension)) {
|
|
const completion = formatFile(subdir, name, extension);
|
|
if (completion !== null) {
|
|
ArrayPrototypePush(group, completion);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
ArrayPrototypePush(group, `${subdir}${name}/`);
|
|
if (listsDirectoryIndex(subdir, isInNodeModules) &&
|
|
hasIndexFile(path.resolve(dir, name), indexes)) {
|
|
ArrayPrototypePush(group, `${subdir}${name}`);
|
|
}
|
|
});
|
|
});
|
|
if (group.length) {
|
|
ArrayPrototypePush(completionGroups, group);
|
|
}
|
|
}
|
|
|
|
ArrayPrototypePush(completionGroups, getReplBuiltinLibs(), nodeSchemeBuiltinLibs);
|
|
return { __proto__: null, completionGroups, completeOn, filter: completeOn };
|
|
}
|
|
|
|
// Whether `dir` contains any of the `indexes` files, which makes it loadable by
|
|
// its bare name (e.g. `require('foo')` resolving to `foo/index.js`).
|
|
function hasIndexFile(dir, indexes) {
|
|
return ArrayPrototypeSome(
|
|
gracefulReaddir(dir) || [],
|
|
(subfile) => ArrayPrototypeIncludes(indexes, subfile),
|
|
);
|
|
}
|
|
|
|
// Completes a filesystem path passed to an `fs` function, e.g.
|
|
// `fs.readFile('./<Tab>')`.
|
|
function completeFsPath(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),
|
|
),
|
|
(dirent) => dirent.name,
|
|
);
|
|
|
|
return {
|
|
__proto__: null,
|
|
completionGroups: [completions],
|
|
completeOn: baseName,
|
|
filter: '',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Completes a trailing expression: either a member access (`obj.foo`) or a bare
|
|
* identifier against the global/lexical scope (`Strin`).
|
|
* @returns {Promise<{completionGroups: string[][], completeOn: string, filter: string}>}
|
|
*/
|
|
async function completeExpression(repl, line) {
|
|
const analysis = analyzeCompletion(line);
|
|
if (analysis === null) {
|
|
return emptyCompletion();
|
|
}
|
|
|
|
const { receiver, chaining, completeOn } = analysis;
|
|
// Completions are emitted fully qualified (e.g. `console.log`), so filtering
|
|
// happens against the whole text being completed.
|
|
const filter = completeOn;
|
|
|
|
const completionGroups = receiver === null ?
|
|
await completeGlobalScope(repl, filter) :
|
|
await completeMemberExpression(
|
|
getReplInspector(repl), receiver, chaining, repl[kContextId]);
|
|
|
|
return { __proto__: null, completionGroups, completeOn, filter };
|
|
}
|
|
|
|
/**
|
|
* Lists completion groups for a bare identifier
|
|
* @returns {Promise<string[][]>} The completion groups
|
|
*/
|
|
async function completeGlobalScope(repl, filter) {
|
|
const completionGroups = [];
|
|
|
|
const lexicalNames =
|
|
await getReplInspector(repl).globalLexicalScopeNames(repl[kContextId]);
|
|
ArrayPrototypePush(completionGroups, lexicalNames);
|
|
|
|
let contextProto = repl.context;
|
|
while ((contextProto = ObjectGetPrototypeOf(contextProto)) !== null) {
|
|
ArrayPrototypePush(completionGroups, filteredOwnPropertyNames(contextProto));
|
|
}
|
|
|
|
const contextOwnNames = filteredOwnPropertyNames(repl.context);
|
|
if (!repl.useGlobal) {
|
|
// When the context is not `global`, builtins are not own properties of it.
|
|
// `getGlobalBuiltins()` is a `SafeSet`, not an Array-like.
|
|
ArrayPrototypePush(contextOwnNames, ...getGlobalBuiltins());
|
|
}
|
|
ArrayPrototypePush(completionGroups, contextOwnNames);
|
|
|
|
if (filter !== '') {
|
|
addCommonWords(completionGroups);
|
|
}
|
|
|
|
return completionGroups;
|
|
}
|
|
|
|
/**
|
|
* Lists member-completion groups for `receiver` using the inspector.
|
|
* @returns {Promise<string[][]>} The completion groups
|
|
*/
|
|
async function completeMemberExpression(inspector, receiver, chaining, contextId) {
|
|
const evaluate = (expression, throwOnSideEffect) => {
|
|
const params = {
|
|
__proto__: null,
|
|
expression,
|
|
throwOnSideEffect,
|
|
objectGroup: kCompletionObjectGroup,
|
|
silent: true,
|
|
};
|
|
if (contextId !== undefined) {
|
|
params.contextId = contextId;
|
|
}
|
|
return inspector.post('Runtime.evaluate', params);
|
|
};
|
|
|
|
try {
|
|
let response;
|
|
try {
|
|
response = await evaluate(receiver, true);
|
|
} catch {
|
|
return [];
|
|
}
|
|
// A side effect (getter/call/proxy trap) or any error (e.g. a ReferenceError
|
|
// for an undeclared receiver) means there is nothing safe to complete.
|
|
if (response.exceptionDetails) {
|
|
return [];
|
|
}
|
|
|
|
let { objectId } = response.result;
|
|
if (objectId === undefined) {
|
|
const { type, subtype } = response.result;
|
|
if (type === 'undefined' || subtype === 'null') {
|
|
// `null`/`undefined` have no properties to offer.
|
|
return [];
|
|
}
|
|
// A primitive: box it so its wrapper and prototype properties (e.g. a
|
|
// string's `length` and the `String.prototype` methods) are reachable.
|
|
// The receiver was just proven side-effect-free, so re-evaluating it as
|
|
// the argument to `Object(...)` is safe.
|
|
let boxed;
|
|
try {
|
|
boxed = await evaluate(`Object(${receiver})`, false);
|
|
} catch {
|
|
return [];
|
|
}
|
|
if (boxed.exceptionDetails) {
|
|
return [];
|
|
}
|
|
objectId = boxed.result.objectId;
|
|
}
|
|
|
|
let properties;
|
|
try {
|
|
properties = await inspector.post('Runtime.getProperties', {
|
|
__proto__: null,
|
|
objectId,
|
|
// `false` walks the whole prototype chain in one call; each descriptor's
|
|
// `isOwn` flag still lets us keep own properties in their own group.
|
|
ownProperties: false,
|
|
// Indexed elements are never useful for property-name completion and
|
|
// asking the inspector to materialize them makes large arrays and typed
|
|
// arrays needlessly expensive.
|
|
nonIndexedPropertiesOnly: true,
|
|
generatePreview: false,
|
|
});
|
|
} catch {
|
|
// e.g. a revoked proxy. Nothing safe to complete.
|
|
return [];
|
|
}
|
|
|
|
// Own properties form the first ("closest") group; inherited ones follow.
|
|
const prefix = `${receiver}${chaining}`;
|
|
const own = [];
|
|
const inherited = [];
|
|
ArrayPrototypeForEach(properties.result || [], (descriptor) => {
|
|
if (isNotLegacyObjectPrototypeMethod(descriptor.name)) {
|
|
ArrayPrototypePush(
|
|
descriptor.isOwn ? own : inherited, `${prefix}${descriptor.name}`);
|
|
}
|
|
});
|
|
|
|
const groups = [];
|
|
if (own.length) {
|
|
ArrayPrototypePush(groups, own);
|
|
}
|
|
if (inherited.length) {
|
|
ArrayPrototypePush(groups, inherited);
|
|
}
|
|
return groups;
|
|
} finally {
|
|
try {
|
|
await inspector.post('Runtime.releaseObjectGroup', {
|
|
__proto__: null,
|
|
objectGroup: kCompletionObjectGroup,
|
|
});
|
|
} catch {
|
|
// Best-effort cleanup; ignore failures.
|
|
}
|
|
}
|
|
}
|
|
|
|
// Determines what kind of completion `line` is requesting using a backward scan
|
|
function analyzeCompletion(line) {
|
|
if (line.length === 0) {
|
|
return { __proto__: null, receiver: null, chaining: '', completeOn: '' };
|
|
}
|
|
|
|
if (endsInsideStringLiteral(line)) {
|
|
return null;
|
|
}
|
|
|
|
const start = scanReceiverStart(line);
|
|
if (start < 0) {
|
|
// The completion point is inside an unterminated string literal.
|
|
return null;
|
|
}
|
|
const target = StringPrototypeSlice(line, start);
|
|
if (target === '') {
|
|
return null;
|
|
}
|
|
|
|
let rest = target;
|
|
if (!StringPrototypeEndsWith(target, '.')) {
|
|
// Strip the trailing partial identifier (e.g. the `lo` in `console.lo`).
|
|
let k = target.length;
|
|
while (k > 0 &&
|
|
isScannerIdentifierPart(StringPrototypeCharCodeAt(target, k - 1))) {
|
|
k--;
|
|
}
|
|
rest = StringPrototypeSlice(target, 0, k);
|
|
}
|
|
|
|
if (StringPrototypeEndsWith(rest, '?.')) {
|
|
const receiver = StringPrototypeSlice(rest, 0, -2);
|
|
return receiver === '' ?
|
|
null :
|
|
{ __proto__: null, receiver, chaining: '?.', completeOn: target };
|
|
}
|
|
if (StringPrototypeEndsWith(rest, '.')) {
|
|
const receiver = StringPrototypeSlice(rest, 0, -1);
|
|
return receiver === '' ?
|
|
null :
|
|
{ __proto__: null, receiver, chaining: '.', completeOn: target };
|
|
}
|
|
if (rest !== '') {
|
|
// A receiver with no trailing member operator (e.g. ending in `]`). There is
|
|
// nothing to scope a member completion to, so bail conservatively.
|
|
return null;
|
|
}
|
|
|
|
// A bare identifier. Skip it when it is the name being introduced by a
|
|
// declaration (e.g. `let a`), since there is nothing to complete there.
|
|
if (RegExpPrototypeExec(declarationKeywordRE,
|
|
StringPrototypeSlice(line, 0, start)) !== null) {
|
|
return null;
|
|
}
|
|
return { __proto__: null, receiver: null, chaining: '', completeOn: target };
|
|
}
|
|
|
|
// Determines whether the completion point sits inside the text
|
|
// of an unterminated string or template literal.
|
|
function endsInsideStringLiteral(code) {
|
|
const stack = [];
|
|
for (let i = 0; i < code.length; i++) {
|
|
const top = stack.length ? stack[stack.length - 1] : '';
|
|
const ch = code[i];
|
|
if (top === "'" || top === '"') {
|
|
// Inside a single/double-quoted string.
|
|
if (ch === '\\') {
|
|
i++; // Skip the escaped character.
|
|
} else if (ch === top) {
|
|
ArrayPrototypePop(stack);
|
|
}
|
|
} else if (top === '`') {
|
|
// Inside the text portion of a template literal.
|
|
if (ch === '\\') {
|
|
i++;
|
|
} else if (ch === '`') {
|
|
ArrayPrototypePop(stack);
|
|
} else if (ch === '$' && code[i + 1] === '{') {
|
|
ArrayPrototypePush(stack, '{'); // Enter a `${ ... }` substitution.
|
|
i++;
|
|
}
|
|
} else if (ch === "'" || ch === '"' || ch === '`') {
|
|
// Code context: a quote opens a new string or template literal.
|
|
ArrayPrototypePush(stack, ch);
|
|
} else if (ch === '{') {
|
|
ArrayPrototypePush(stack, '{');
|
|
} else if (ch === '}' && top === '{') {
|
|
ArrayPrototypePop(stack);
|
|
}
|
|
}
|
|
const top = stack.length ? stack[stack.length - 1] : '';
|
|
return top === "'" || top === '"' || top === '`';
|
|
}
|
|
|
|
// Scans backward from the end of `code` over a trailing member-access
|
|
// expression, returning the index where it starts.
|
|
function scanReceiverStart(code) {
|
|
let i = code.length;
|
|
while (i > 0) {
|
|
const ch = code[i - 1];
|
|
if (isScannerIdentifierPart(StringPrototypeCharCodeAt(code, i - 1))) {
|
|
i--;
|
|
} else if (ch === '.') {
|
|
i--;
|
|
} else if (ch === '?' && code[i] === '.') {
|
|
// The optional-chaining operator `?.`.
|
|
i--;
|
|
} else if (ch === ')' || ch === ']') {
|
|
const open = matchBracketStart(code, i - 1);
|
|
if (open < 0) {
|
|
break;
|
|
}
|
|
i = open;
|
|
} else if (ch === '"' || ch === "'" || ch === '`') {
|
|
const open = matchStringStart(code, i - 1, ch);
|
|
if (open < 0) {
|
|
// No matching opening quote before this one means the quote opens an
|
|
// unterminated string literal that the completion point sits inside
|
|
// (e.g. `"n`). There is nothing to complete there.
|
|
return -1;
|
|
}
|
|
i = open;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
return i;
|
|
}
|
|
|
|
// Given the index of a closing `)` or `]`, returns the index of the matching
|
|
// opener
|
|
function matchBracketStart(code, closeIndex) {
|
|
let depth = 0;
|
|
for (let i = closeIndex; i >= 0; i--) {
|
|
const ch = code[i];
|
|
if (ch === '"' || ch === "'" || ch === '`') {
|
|
i = matchStringStart(code, i, ch);
|
|
if (i < 0) {
|
|
return -1;
|
|
}
|
|
continue;
|
|
}
|
|
if (ch === ')' || ch === ']' || ch === '}') {
|
|
depth++;
|
|
} else if (ch === '(' || ch === '[' || ch === '{') {
|
|
if (--depth === 0) {
|
|
return i;
|
|
}
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// Given the index of a closing quote, returns the index of the matching opening
|
|
// quote
|
|
function matchStringStart(code, closeIndex, quote) {
|
|
for (let i = closeIndex - 1; i >= 0; i--) {
|
|
if (code[i] !== quote) {
|
|
continue;
|
|
}
|
|
let backslashes = 0;
|
|
for (let k = i - 1; k >= 0 && code[k] === '\\'; k--) {
|
|
backslashes++;
|
|
}
|
|
if (backslashes % 2 === 0) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
function isScannerIdentifierPart(code) {
|
|
return (code >= 48 && code <= 57) || // 0-9
|
|
(code >= 65 && code <= 90) || // A-Z
|
|
(code >= 97 && code <= 122) || // a-z
|
|
code === 36 || // $
|
|
code === 95 || // _
|
|
code > 127; // non-ASCII
|
|
}
|
|
|
|
function isIdentifier(str) {
|
|
if (str === '') {
|
|
return false;
|
|
}
|
|
const { isIdentifierStart, isIdentifierChar } =
|
|
require('internal/deps/acorn/acorn/dist/acorn');
|
|
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 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.
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
complete,
|
|
};
|