UNPKG

typescript

Version:

TypeScript is a language for application scale JavaScript development

496 lines • 21.8 kB
import { computeLineStarts, NodeFlags, SpanMap, SpanMapFeature, SpanMapKind, SyntaxKind, TokenFlags, } from "../../ast/index.js"; import { tryPathKeyFromCanonical } from "../path.js"; import { MsgpackReader } from "./msgpack.js"; import { RemoteNode, RemoteNodeList, } from "./node.generated.js"; import { NODE_EXTENDED_DATA_MASK, readParseOptionsKey, readSourceFileHash, readSourceFileNodeId, } from "./node.infrastructure.js"; import { HEADER_OFFSET_EXTENDED_DATA, HEADER_OFFSET_NODES, HEADER_OFFSET_SOURCE_FILE_ID, HEADER_OFFSET_STRING_TABLE, HEADER_OFFSET_STRING_TABLE_OFFSETS, HEADER_OFFSET_STRUCTURED_DATA, KIND_NODE_LIST, NODE_LEN, NODE_OFFSET_KIND, NODE_OFFSET_PARENT, } from "./protocol.js"; import { Wtf8Decoder } from "./wtf8.js"; // Re-export everything consumers need from the other two files. export { RemoteNode, RemoteNodeList } from "./node.generated.js"; export { readParseOptionsKey, readSourceFileHash, readSourceFileLease, readSourceFileNodeId, RemoteNodeBase } from "./node.infrastructure.js"; const sourceFileExtendedDataOffsets = { Text: 0, FileName: 4, Path: 8, LanguageVariant: 12, ScriptKind: 16, ReferencedFiles: 20, TypeReferenceDirectives: 24, LibReferenceDirectives: 28, Imports: 32, ModuleAugmentations: 36, AmbientModuleNames: 40, ExternalModuleIndicator: 44, OriginalText: 48, SpanMap: 52, SupplementalSourceFileNames: 56, CanonicalSourceFileName: 60, ContentMapper: 64, VirtualFileName: 68, DiagnosticDirectives: 72, }; for (const [index, offset] of Object.values(sourceFileExtendedDataOffsets).entries()) { if (offset !== index * Uint32Array.BYTES_PER_ELEMENT) { throw new Error(`Invalid SourceFile extended data offset ${offset} at index ${index}`); } } // ═══════════════════════════════════════════════════════════════════════════ // RemoteSourceFile // ═══════════════════════════════════════════════════════════════════════════ const NO_STRUCTURED_DATA = 0xFFFFFFFF; export class RemoteSourceFile extends RemoteNode { api; symbolCache; nodes; _offsetNodes; _offsetStringTableOffsets; _offsetStringTable; _offsetExtendedData; _offsetStructuredData; _decoder; _timing; _lineStarts; _cachedText; _cachedReferencedFiles; _cachedTypeReferenceDirectives; _cachedLibReferenceDirectives; _cachedImports; _cachedModuleAugmentations; _cachedAmbientModuleNames; _cachedSpanMap; _spanMapRead = false; _cachedSupplementalSourceFileNames; _cachedDiagnosticDirectives; _diagnosticDirectivesRead = false; _handleFileKey; constructor(data, decoder, timing, api) { const view = new DataView(data.buffer, data.byteOffset, data.byteLength); const offsetNodes = view.getUint32(HEADER_OFFSET_NODES, true); super(view, 1, undefined, undefined, offsetNodes); this._sourceFile = this; this._offsetNodes = offsetNodes; this._offsetStringTableOffsets = view.getUint32(HEADER_OFFSET_STRING_TABLE_OFFSETS, true); this._offsetStringTable = view.getUint32(HEADER_OFFSET_STRING_TABLE, true); this._offsetExtendedData = view.getUint32(HEADER_OFFSET_EXTENDED_DATA, true); this._offsetStructuredData = view.getUint32(HEADER_OFFSET_STRUCTURED_DATA, true); this._decoder = decoder; this._timing = timing; this.api = api; this.nodes = Array((view.byteLength - offsetNodes) / NODE_LEN); this.nodes[1] = this; // Every node slot is materializable on demand except the nil sentinel at // index 0 and the source-file node at index 1, which is pre-materialized. timing?.recordSourceFileFetched(Math.max(0, this.nodes.length - 2)); } /** @internal */ get contentHash() { return readSourceFileHash(this.view); } /** @internal */ get parseOptionsKey() { return readParseOptionsKey(this.view); } /** @internal */ get nodeId() { return readSourceFileNodeId(this.view); } /** * The file component of node handles sent to the server: `#` followed by the * server's identity for this exact source file. * @internal */ get handleFileKey() { if (this._handleFileKey === undefined) { const nodeId = this.view.getBigUint64(HEADER_OFFSET_SOURCE_FILE_ID, true); if (nodeId === 0n) { throw new Error("Node has no server source-file identity"); } this._handleFileKey = `#${nodeId}`; } return this._handleFileKey; } readFileReferences(structuredDataOffset) { if (structuredDataOffset === NO_STRUCTURED_DATA) { return []; } const buf = new Uint8Array(this.view.buffer, this.view.byteOffset, this.view.byteLength); const reader = new MsgpackReader(buf, this._offsetStructuredData + structuredDataOffset); const count = reader.readArrayHeader(); const result = []; for (let i = 0; i < count; i++) { reader.readArrayHeader(); // 5-element tuple const pos = reader.readUint(); const end = reader.readUint(); const fileName = reader.readString(); const resolutionMode = reader.readUint(); const preserve = reader.readBool(); result.push({ pos, end, fileName, resolutionMode, preserve }); } return result; } readNodeIndexArray(structuredDataOffset) { if (structuredDataOffset === NO_STRUCTURED_DATA) { return []; } const buf = new Uint8Array(this.view.buffer, this.view.byteOffset, this.view.byteLength); const reader = new MsgpackReader(buf, this._offsetStructuredData + structuredDataOffset); const count = reader.readArrayHeader(); const result = []; for (let i = 0; i < count; i++) { const nodeIndex = reader.readUint(); result.push(this.getOrCreateNodeAtIndex(nodeIndex)); } return result; } readStringArray(structuredDataOffset) { if (structuredDataOffset === NO_STRUCTURED_DATA) { return []; } const buf = new Uint8Array(this.view.buffer, this.view.byteOffset, this.view.byteLength); const reader = new MsgpackReader(buf, this._offsetStructuredData + structuredDataOffset); const count = reader.readArrayHeader(); const result = []; for (let i = 0; i < count; i++) { result.push(reader.readString()); } return result; } getOrCreateNodeAtIndex(index) { let node = this.nodes[index]; if (!node) { // Resolve the real parent so that nodes looked up directly by index (e.g. via // NodeHandle.resolve) report the correct `parent`, rather than always pointing at // the source file. The stored parent index can refer to a synthetic NodeList // container; skip those to mirror normal traversal, where list elements take the // list's parent. The walk terminates at the source file, which occupies index 1 // and is already cached. let parentIndex = this.view.getUint32(this._offsetNodes + index * NODE_LEN + NODE_OFFSET_PARENT, true); while (parentIndex !== index && this.view.getUint32(this._offsetNodes + parentIndex * NODE_LEN + NODE_OFFSET_KIND, true) === KIND_NODE_LIST) { parentIndex = this.view.getUint32(this._offsetNodes + parentIndex * NODE_LEN + NODE_OFFSET_PARENT, true); } const parent = parentIndex === index ? this : this.getOrCreateNodeAtIndex(parentIndex); node = new RemoteNode(this.view, index, parent, this, this._offsetNodes); this.nodes[index] = node; this._timing?.recordMaterialization(); } return node; } // ═══ SourceFile-specific extended data getters ═══ get extendedDataOffset() { return this._offsetExtendedData + (this.data & NODE_EXTENDED_DATA_MASK); } getFileName(stringIndex) { return this.getString(stringIndex); } getPathKey(stringIndex) { return this.getString(stringIndex); } readFileNameArray(structuredDataOffset) { return this.readStringArray(structuredDataOffset); } get fileName() { const stringIndex = this.view.getUint32(this.extendedDataOffset + sourceFileExtendedDataOffsets.FileName, true); return this.getFileName(stringIndex); } get path() { const stringIndex = this.view.getUint32(this.extendedDataOffset + sourceFileExtendedDataOffsets.Path, true); return this.getPathKey(stringIndex); } get languageVariant() { return this.view.getUint32(this.extendedDataOffset + sourceFileExtendedDataOffsets.LanguageVariant, true); } get scriptKind() { return this.view.getUint32(this.extendedDataOffset + sourceFileExtendedDataOffsets.ScriptKind, true); } get referencedFiles() { if (this._cachedReferencedFiles !== undefined) return this._cachedReferencedFiles; const offset = this.view.getUint32(this.extendedDataOffset + sourceFileExtendedDataOffsets.ReferencedFiles, true); const files = this.readFileReferences(offset); this._cachedReferencedFiles = files; return files; } get typeReferenceDirectives() { if (this._cachedTypeReferenceDirectives !== undefined) return this._cachedTypeReferenceDirectives; const offset = this.view.getUint32(this.extendedDataOffset + sourceFileExtendedDataOffsets.TypeReferenceDirectives, true); const directives = this.readFileReferences(offset); this._cachedTypeReferenceDirectives = directives; return directives; } get libReferenceDirectives() { if (this._cachedLibReferenceDirectives !== undefined) return this._cachedLibReferenceDirectives; const offset = this.view.getUint32(this.extendedDataOffset + sourceFileExtendedDataOffsets.LibReferenceDirectives, true); const directives = this.readFileReferences(offset); this._cachedLibReferenceDirectives = directives; return directives; } get imports() { if (this._cachedImports !== undefined) return this._cachedImports; const offset = this.view.getUint32(this.extendedDataOffset + sourceFileExtendedDataOffsets.Imports, true); const imports = this.readNodeIndexArray(offset); this._cachedImports = imports; return imports; } get moduleAugmentations() { if (this._cachedModuleAugmentations !== undefined) return this._cachedModuleAugmentations; const offset = this.view.getUint32(this.extendedDataOffset + sourceFileExtendedDataOffsets.ModuleAugmentations, true); const moduleAugmentations = this.readNodeIndexArray(offset); this._cachedModuleAugmentations = moduleAugmentations; return moduleAugmentations; } get ambientModuleNames() { if (this._cachedAmbientModuleNames !== undefined) return this._cachedAmbientModuleNames; const offset = this.view.getUint32(this.extendedDataOffset + sourceFileExtendedDataOffsets.AmbientModuleNames, true); const names = this.readStringArray(offset); this._cachedAmbientModuleNames = names; return names; } get externalModuleIndicator() { const nodeIndex = this.view.getUint32(this.extendedDataOffset + sourceFileExtendedDataOffsets.ExternalModuleIndicator, true); if (nodeIndex === 0) return undefined; if (nodeIndex === this.index) return true; return this.getOrCreateNodeAtIndex(nodeIndex); } get originalText() { const stringIndex = this.view.getUint32(this.extendedDataOffset + sourceFileExtendedDataOffsets.OriginalText, true); return this.getString(stringIndex); } get spanMap() { if (this._spanMapRead) return this._cachedSpanMap; this._spanMapRead = true; const offset = this.view.getUint32(this.extendedDataOffset + sourceFileExtendedDataOffsets.SpanMap, true); if (offset === NO_STRUCTURED_DATA) return undefined; const buf = new Uint8Array(this.view.buffer, this.view.byteOffset, this.view.byteLength); const reader = new MsgpackReader(buf, this._offsetStructuredData + offset); const count = reader.readArrayHeader(); const segments = Array(count); for (let i = 0; i < count; i++) { const tupleLength = reader.readArrayHeader(); if (tupleLength !== 5 && tupleLength !== 6) throw new Error("Invalid span map segment"); const virtualStart = reader.readUint(); const virtualLength = reader.readUint(); const originalStart = reader.readUint(); const originalLength = reader.readUint(); const kind = reader.readUint(); const features = tupleLength === 6 ? reader.readUint() : SpanMapFeature.All; if (kind !== SpanMapKind.Verbatim && kind !== SpanMapKind.Atom && kind !== SpanMapKind.Alias) throw new Error(`Invalid span map kind: ${kind}`); segments[i] = { virtualStart, virtualEnd: virtualStart + virtualLength, originalStart, originalEnd: originalStart + originalLength, kind, features, }; } return this._cachedSpanMap = new SpanMap(segments); } get supplementalSourceFileNames() { if (this._cachedSupplementalSourceFileNames !== undefined) return this._cachedSupplementalSourceFileNames; const offset = this.view.getUint32(this.extendedDataOffset + sourceFileExtendedDataOffsets.SupplementalSourceFileNames, true); if (offset === NO_STRUCTURED_DATA) return undefined; return this._cachedSupplementalSourceFileNames = this.readFileNameArray(offset); } get canonicalSourceFileName() { const stringIndex = this.view.getUint32(this.extendedDataOffset + sourceFileExtendedDataOffsets.CanonicalSourceFileName, true); return stringIndex === NO_STRUCTURED_DATA ? undefined : this.getFileName(stringIndex); } get contentMapper() { const stringIndex = this.view.getUint32(this.extendedDataOffset + sourceFileExtendedDataOffsets.ContentMapper, true); return stringIndex === NO_STRUCTURED_DATA ? undefined : this.getString(stringIndex); } get virtualFileName() { const stringIndex = this.view.getUint32(this.extendedDataOffset + sourceFileExtendedDataOffsets.VirtualFileName, true); return stringIndex === NO_STRUCTURED_DATA ? undefined : this.getFileName(stringIndex); } get diagnosticDirectives() { if (this._diagnosticDirectivesRead) return this._cachedDiagnosticDirectives; this._diagnosticDirectivesRead = true; const offset = this.view.getUint32(this.extendedDataOffset + sourceFileExtendedDataOffsets.DiagnosticDirectives, true); if (offset === NO_STRUCTURED_DATA) return undefined; const buf = new Uint8Array(this.view.buffer, this.view.byteOffset, this.view.byteLength); const reader = new MsgpackReader(buf, this._offsetStructuredData + offset); const count = reader.readArrayHeader(); const directives = Array(count); for (let i = 0; i < count; i++) { if (reader.readArrayHeader() !== 6) throw new Error("Invalid diagnostic directive"); const originalStart = reader.readUint(); const originalLength = reader.readUint(); const virtualStart = reader.readUint(); const virtualLength = reader.readUint(); directives[i] = { originalRange: { pos: originalStart, end: originalStart + originalLength }, virtualRange: { pos: virtualStart, end: virtualStart + virtualLength }, policy: reader.readUint(), unusedCode: reader.readUint(), }; } return this._cachedDiagnosticDirectives = directives; } get isDeclarationFile() { return (this.flags & NodeFlags.Ambient) !== 0; } get text() { if (this._cachedText !== undefined) return this._cachedText; const text = super.text; this._cachedText = text; return text; } // ═══ Line/character position mapping ═══ getLineStarts() { return this._lineStarts ??= computeLineStarts(this.text ?? ""); } getLineAndCharacterOfPosition(position) { const lineStarts = this.getLineStarts(); const line = computeLineOfPosition(lineStarts, position); return { line, character: position - lineStarts[line] }; } getPositionOfLineAndCharacter(line, character) { const lineStarts = this.getLineStarts(); if (line < 0 || line >= lineStarts.length) { throw new Error(`Bad line number. Line: ${line}, lineStarts.length: ${lineStarts.length}`); } return lineStarts[line] + character; } } /** * Find the 0-based line number containing the given position via binary search. * Assumes the first line starts at position 0 and `position` is non-negative. */ function computeLineOfPosition(lineStarts, position) { let low = 0; let high = lineStarts.length - 1; while (low <= high) { const middle = low + ((high - low) >> 1); const value = lineStarts[middle]; if (value < position) { low = middle + 1; } else if (value > position) { high = middle - 1; } else { return middle; } } return low - 1; } /** * Find a descendant node at a specific position with matching kind and end position. */ export function findDescendant(root, pos, end, kind) { if (root.pos === pos && root.end === end && root.kind === kind) { return root; } // Search children let result; root.forEachChild(child => { if (result) return result; // Already found // Only search in children that could contain our target if (child.pos <= pos && child.end >= end) { result = findDescendant(child, pos, end, kind); } return undefined; }); return result; } /** * Parse a compiler-produced node handle into its components. * Handle format: "index.kind.#sourceFileId.path" where path may contain dots. */ export function parseNodeHandleFromCompiler(handle) { const firstDot = handle.indexOf("."); if (firstDot === -1) { throw new Error(`Invalid node handle: ${handle}`); } const secondDot = handle.indexOf(".", firstDot + 1); if (secondDot === -1) { throw new Error(`Invalid node handle: ${handle}`); } const indexText = handle.slice(0, firstDot); const kindText = handle.slice(firstDot + 1, secondDot); const thirdDot = handle.indexOf(".", secondDot + 1); if (thirdDot === -1) { throw new Error(`Invalid node handle: ${handle}`); } const fileKey = handle.slice(secondDot + 1, thirdDot); const sourceFileId = fileKey.slice(1); const path = handle.slice(thirdDot + 1); const index = Number(indexText); const kind = Number(kindText); const key = tryPathKeyFromCanonical(path); if (!Number.isSafeInteger(index) || index < 0 || String(index) !== indexText || !Number.isSafeInteger(kind) || kind < 0 || String(kind) !== kindText || !/^#[1-9][0-9]*$/.test(fileKey) || BigInt(sourceFileId) > 0xffffffffffffffffn || key === undefined) { throw new Error(`Invalid node handle: ${handle}`); } return { index, kind: kind, sourceFileId, path: key, }; } /** * Decode binary-encoded AST data into a Node. * Works for any binary-encoded node, including synthetic nodes * (e.g. from typeToTypeNode) that don't have a source file. */ export function decodeNode(data) { const sf = new RemoteSourceFile(data, new Wtf8Decoder()); return sf; } /** * Get the unique ID string for a remote node. * Throws if the node is not a RemoteNode (i.e. not decoded from binary data). */ export function getNodeId(node) { if (!(node instanceof RemoteNode)) { throw new Error("getNodeId requires a RemoteNode"); } const sourceFile = node["_sourceFile"]; if (sourceFile instanceof RemoteSourceFile) { return `${node["index"]}.${node.kind}.${sourceFile.handleFileKey}.${sourceFile.path}`; } throw new Error("Node has no server source-file identity"); } /** * Get the compact handle identifying a node in its exact server source-file AST. * @internal */ export function getNodeRequestHandle(node) { if (!(node instanceof RemoteNode)) { throw new Error("getNodeRequestHandle requires a RemoteNode"); } const sourceFile = node["_sourceFile"]; if (sourceFile instanceof RemoteSourceFile) { return `${node["index"]}.${sourceFile.handleFileKey}`; } throw new Error("Node has no server source-file identity"); } //# sourceMappingURL=node.js.map