/
githubmirror
/
node
Обзор
Документация
Войти
/
githubmirror
/
node
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
lib/internal/repl/completion.js
742 строки
23 KB
avivkeller
repl: use inspector over vm
18 июл 2026, 22:59
18 июл 2026, 22:59
4f844f4
Код
Авторство
О чём код?
'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, };