vendor tsgo

This commit is contained in:
2026-07-09 16:50:43 -04:00
parent c06ea2e5a4
commit 98978e4930
5804 changed files with 1556156 additions and 101 deletions

View File

@@ -0,0 +1,226 @@
package api
import (
"context"
"fmt"
"time"
"github.com/microsoft/typescript-go/internal/json"
"github.com/microsoft/typescript-go/internal/vfs"
)
// callbackFS wraps a base filesystem and delegates certain operations
// to the client via RPC callbacks. This allows the API client to provide
// a virtual filesystem (e.g., in-memory files for testing).
//
// The callbacks to enable are specified at construction time via the
// --callbacks CLI flag. The connection is set via SetConnection after
// the transport connection is established.
type callbackFS struct {
base vfs.FS
enabledCallbacks map[string]bool
// conn and ctx are set after connection is established
conn Conn
ctx context.Context
}
// Callback names that can be enabled
const (
callbackReadFile = "readFile"
callbackFileExists = "fileExists"
callbackDirectoryExists = "directoryExists"
callbackGetAccessibleEntries = "getAccessibleEntries"
callbackRealpath = "realpath"
)
func isCallbackName(name string) bool {
switch name {
case callbackReadFile,
callbackFileExists,
callbackDirectoryExists,
callbackGetAccessibleEntries,
callbackRealpath:
return true
default:
return false
}
}
// newCallbackFS creates a new callbackFS wrapping the given base filesystem.
// The callbacks slice specifies which filesystem operations should be delegated
// to the client (e.g., "readFile", "fileExists").
func newCallbackFS(base vfs.FS, callbacks []string) *callbackFS {
enabled := make(map[string]bool, len(callbacks))
for _, cb := range callbacks {
if !isCallbackName(cb) {
panic("unknown callback name: " + cb)
}
enabled[cb] = true
}
return &callbackFS{
base: base,
enabledCallbacks: enabled,
}
}
// SetConnection sets the RPC connection for callbacks.
// This must be called after the transport connection is established
// but before any filesystem operations that need callbacks.
func (fs *callbackFS) SetConnection(ctx context.Context, conn Conn) {
fs.ctx = ctx
fs.conn = conn
}
// isEnabled returns true if the named callback is enabled.
func (fs *callbackFS) isEnabled(name string) bool {
return fs.enabledCallbacks[name]
}
// call invokes a callback on the client and returns the result.
func (fs *callbackFS) call(name string, arg any) ([]byte, error) {
if fs.conn == nil {
return nil, fmt.Errorf("CallbackFS: %s called before connection set", name)
}
result, err := fs.conn.Call(fs.ctx, name, arg)
if err != nil {
return nil, err
}
return result, nil
}
// UseCaseSensitiveFileNames implements vfs.FS.
func (fs *callbackFS) UseCaseSensitiveFileNames() bool {
return fs.base.UseCaseSensitiveFileNames()
}
// ReadFile implements vfs.FS.
//
// The readFile callback uses a wrapped response format to distinguish three states:
// - undefined (fall back to real FS): null or empty on wire
// - null (not found, no fallback): {"content": null}
// - string content: {"content": "..."}
func (fs *callbackFS) ReadFile(path string) (contents string, ok bool) {
if fs.isEnabled(callbackReadFile) {
result, err := fs.call(callbackReadFile, path)
if err != nil {
panic(err)
}
if len(result) > 0 && string(result) != "null" {
var wrapper struct {
Content *string `json:"content"`
}
if err := json.Unmarshal(result, &wrapper); err != nil {
panic(err)
}
if wrapper.Content == nil {
return "", false
}
return *wrapper.Content, true
}
}
return fs.base.ReadFile(path)
}
// FileExists implements vfs.FS.
func (fs *callbackFS) FileExists(path string) bool {
if fs.isEnabled(callbackFileExists) {
result, err := fs.call(callbackFileExists, path)
if err != nil {
panic(err)
}
if len(result) > 0 && string(result) != "null" {
return string(result) == "true"
}
}
return fs.base.FileExists(path)
}
// DirectoryExists implements vfs.FS.
func (fs *callbackFS) DirectoryExists(path string) bool {
if fs.isEnabled(callbackDirectoryExists) {
result, err := fs.call(callbackDirectoryExists, path)
if err != nil {
panic(err)
}
if len(result) > 0 && string(result) != "null" {
return string(result) == "true"
}
}
return fs.base.DirectoryExists(path)
}
// GetAccessibleEntries implements vfs.FS.
func (fs *callbackFS) GetAccessibleEntries(path string) vfs.Entries {
if fs.isEnabled(callbackGetAccessibleEntries) {
result, err := fs.call(callbackGetAccessibleEntries, path)
if err != nil {
panic(err)
}
if len(result) > 0 {
var rawEntries *struct {
Files []string `json:"files"`
Directories []string `json:"directories"`
}
if err := json.Unmarshal(result, &rawEntries); err != nil {
panic(err)
}
if rawEntries != nil {
return vfs.Entries{
Files: rawEntries.Files,
Directories: rawEntries.Directories,
}
}
}
}
return fs.base.GetAccessibleEntries(path)
}
// Realpath implements vfs.FS.
func (fs *callbackFS) Realpath(path string) string {
if fs.isEnabled(callbackRealpath) {
result, err := fs.call(callbackRealpath, path)
if err != nil {
panic(err)
}
if len(result) > 0 && string(result) != "null" {
var realpath string
if err := json.Unmarshal(result, &realpath); err != nil {
panic(err)
}
return realpath
}
}
return fs.base.Realpath(path)
}
// WriteFile implements vfs.FS - always delegates to base (no callback support).
func (fs *callbackFS) WriteFile(path string, data string) error {
return fs.base.WriteFile(path, data)
}
// AppendFile implements vfs.FS - always delegates to base (no callback support).
func (fs *callbackFS) AppendFile(path string, data string) error {
return fs.base.AppendFile(path, data)
}
// Remove implements vfs.FS - always delegates to base (no callback support).
func (fs *callbackFS) Remove(path string) error {
return fs.base.Remove(path)
}
// Chtimes implements vfs.FS - always delegates to base (no callback support).
func (fs *callbackFS) Chtimes(path string, aTime time.Time, mTime time.Time) error {
return fs.base.Chtimes(path, aTime, mTime)
}
// Stat implements vfs.FS - always delegates to base (no callback support).
func (fs *callbackFS) Stat(path string) vfs.FileInfo {
return fs.base.Stat(path)
}
// WalkDir implements vfs.FS - always delegates to base (no callback support).
func (fs *callbackFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error {
return fs.base.WalkDir(root, walkFn)
}

View File

@@ -0,0 +1,46 @@
package api
import (
"context"
"errors"
"github.com/microsoft/typescript-go/internal/json"
)
var (
ErrConnClosed = errors.New("api: connection closed")
ErrRequestTimeout = errors.New("api: request timeout")
)
// Handler processes incoming API requests and notifications.
type Handler interface {
// HandleRequest handles an incoming request and returns a result or error.
HandleRequest(ctx context.Context, method string, params json.Value) (any, error)
// HandleNotification handles an incoming notification.
HandleNotification(ctx context.Context, method string, params json.Value) error
}
// Conn represents a bidirectional connection for API communication.
type Conn interface {
// Run starts processing messages on the connection.
// It blocks until the context is cancelled or an error occurs.
Run(ctx context.Context) error
// Call sends a request to the client and waits for a response.
Call(ctx context.Context, method string, params any) (json.Value, error)
// Notify sends a notification to the client (no response expected).
Notify(ctx context.Context, method string, params any) error
}
// UnmarshalParams is a helper to unmarshal params into a typed struct.
func UnmarshalParams[T any](params json.Value) (*T, error) {
if len(params) == 0 {
return nil, nil
}
var v T
if err := json.Unmarshal(params, &v); err != nil {
return nil, err
}
return &v, nil
}

View File

@@ -0,0 +1,231 @@
package api
import (
"context"
"errors"
"fmt"
"io"
"runtime/debug"
"sync"
"sync/atomic"
"time"
"github.com/microsoft/typescript-go/internal/json"
"github.com/microsoft/typescript-go/internal/jsonrpc"
)
// AsyncConn manages bidirectional JSON-RPC communication with async request handling.
// Each incoming request is handled in its own goroutine, allowing concurrent processing.
// This is the standard implementation for LSP-style JSON-RPC protocols.
type AsyncConn struct {
rwc io.ReadWriteCloser
protocol Protocol
handler Handler
// timing, when non-nil, accumulates the wall-clock time spent handling each
// request. Clients retrieve the collected data via a getServerTiming request.
timing *timingCollector
// For server→client requests
seq atomic.Int64
pending map[jsonrpc.ID]chan *Message
pendingMu sync.Mutex
writeMu sync.Mutex
}
// NewAsyncConn creates a new async connection with the given transport and handler.
// It uses JSONRPCProtocol (LSP-style Content-Length framing) by default.
func NewAsyncConn(rwc io.ReadWriteCloser, handler Handler) *AsyncConn {
return NewAsyncConnWithProtocol(rwc, NewJSONRPCProtocol(rwc), handler)
}
// NewAsyncConnWithProtocol creates a new async connection with a custom protocol.
func NewAsyncConnWithProtocol(rwc io.ReadWriteCloser, protocol Protocol, handler Handler) *AsyncConn {
return &AsyncConn{
rwc: rwc,
protocol: protocol,
handler: handler,
pending: make(map[jsonrpc.ID]chan *Message),
}
}
// SetCollectTiming enables or disables per-request server processing-time
// measurement. When enabled, the connection accumulates timing that clients can
// retrieve via a getServerTiming request.
func (c *AsyncConn) SetCollectTiming(enabled bool) {
if enabled {
c.timing = newTimingCollector()
} else {
c.timing = nil
}
}
// Run starts processing messages on the connection.
// It blocks until the context is cancelled or an error occurs.
func (c *AsyncConn) Run(ctx context.Context) error {
for {
if ctx.Err() != nil {
return ctx.Err()
}
msg, err := c.protocol.ReadMessage()
if err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return err
}
if msg.IsResponse() {
c.handleResponse(msg)
} else if msg.IsRequest() {
go c.handleRequest(ctx, msg)
} else if msg.IsNotification() {
go c.handleNotification(ctx, msg)
}
}
}
// handleResponse matches a response to a pending request.
func (c *AsyncConn) handleResponse(msg *Message) {
c.pendingMu.Lock()
ch, ok := c.pending[*msg.ID]
if ok {
delete(c.pending, *msg.ID)
}
c.pendingMu.Unlock()
if ok {
ch <- msg
close(ch)
}
}
// handleRequest processes an incoming request.
func (c *AsyncConn) handleRequest(ctx context.Context, msg *Message) {
// Intercept the meta-requests for collected server timing before dispatching
// to the handler, so they are answered directly and not themselves recorded.
switch msg.Method {
case string(MethodGetServerTiming):
c.writeMu.Lock()
writeErr := c.protocol.WriteResponse(msg.ID, serverTimingSnapshot(c.timing))
c.writeMu.Unlock()
if writeErr != nil {
panic(fmt.Sprintf("api: failed to write server timing response: %v", writeErr))
}
return
case string(MethodResetServerTiming):
if c.timing != nil {
c.timing.reset()
}
c.writeMu.Lock()
writeErr := c.protocol.WriteResponse(msg.ID, nil)
c.writeMu.Unlock()
if writeErr != nil {
panic(fmt.Sprintf("api: failed to write reset server timing response: %v", writeErr))
}
return
}
var result any
var err error
start := time.Time{}
if c.timing != nil {
start = time.Now()
}
// Recover from panics and convert to error response with stack trace
defer func() {
if r := recover(); r != nil {
stack := string(debug.Stack())
err = fmt.Errorf("panic: %v\n%s", r, stack)
c.writeMu.Lock()
writeErr := c.protocol.WriteError(msg.ID, &jsonrpc.ResponseError{
Code: jsonrpc.CodeInternalError,
Message: err.Error(),
})
c.writeMu.Unlock()
if writeErr != nil {
panic(fmt.Sprintf("api: failed to write panic error response: %v (original panic: %v)", writeErr, r))
}
}
}()
result, err = c.handler.HandleRequest(ctx, msg.Method, msg.Params)
if c.timing != nil {
c.timing.record(msg.Method, time.Since(start))
}
c.writeMu.Lock()
defer c.writeMu.Unlock()
var writeErr error
if err != nil {
writeErr = c.protocol.WriteError(msg.ID, &jsonrpc.ResponseError{
Code: jsonrpc.CodeInternalError,
Message: err.Error(),
})
} else {
writeErr = c.protocol.WriteResponse(msg.ID, result)
}
if writeErr != nil {
panic(fmt.Sprintf("api: failed to write response: %v", writeErr))
}
}
// handleNotification processes an incoming notification.
func (c *AsyncConn) handleNotification(ctx context.Context, msg *Message) {
_ = c.handler.HandleNotification(ctx, msg.Method, msg.Params)
}
// Call sends a request to the client and waits for a response.
func (c *AsyncConn) Call(ctx context.Context, method string, params any) (json.Value, error) {
// Create unique request ID
id := jsonrpc.NewIDString(fmt.Sprintf("api%d", c.seq.Add(1)))
// Register response channel BEFORE sending request to avoid race
responseChan := make(chan *Message, 1)
c.pendingMu.Lock()
c.pending[*id] = responseChan
c.pendingMu.Unlock()
defer func() {
c.pendingMu.Lock()
defer c.pendingMu.Unlock()
if ch, ok := c.pending[*id]; ok {
close(ch)
delete(c.pending, *id)
}
}()
// Send the request
c.writeMu.Lock()
err := c.protocol.WriteRequest(id, method, params)
c.writeMu.Unlock()
if err != nil {
return nil, err
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case resp := <-responseChan:
if resp.Error != nil {
return nil, fmt.Errorf("api: remote error [%d]: %s", resp.Error.Code, resp.Error.Message)
}
return resp.Result, nil
}
}
// Notify sends a notification to the client (no response expected).
func (c *AsyncConn) Notify(ctx context.Context, method string, params any) error {
c.writeMu.Lock()
defer c.writeMu.Unlock()
return c.protocol.WriteNotification(method, params)
}

View File

@@ -0,0 +1,208 @@
package api
import (
"context"
"errors"
"fmt"
"io"
"runtime/debug"
"sync"
"time"
"github.com/microsoft/typescript-go/internal/json"
"github.com/microsoft/typescript-go/internal/jsonrpc"
)
// SyncConn manages bidirectional communication with synchronous request handling.
// Requests are handled one at a time inline, and outgoing calls are serialized.
type SyncConn struct {
rwc io.ReadWriteCloser
protocol Protocol
handler Handler
// timing, when non-nil, accumulates the wall-clock time spent handling each
// request. Clients retrieve the collected data via a getServerTiming request.
timing *timingCollector
// mu serializes all protocol operations (reads and writes).
// This ensures that concurrent calls from handler goroutines (e.g., project code
// spawning goroutines that invoke filesystem callbacks) don't corrupt the stream.
mu sync.Mutex
}
// NewSyncConn creates a new sync connection with the given transport and handler.
func NewSyncConn(rwc io.ReadWriteCloser, protocol Protocol, handler Handler) *SyncConn {
return &SyncConn{
rwc: rwc,
protocol: protocol,
handler: handler,
}
}
// SetCollectTiming enables or disables per-request server processing-time
// measurement. When enabled, the connection accumulates timing that clients can
// retrieve via a getServerTiming request.
func (c *SyncConn) SetCollectTiming(enabled bool) {
if enabled {
c.timing = newTimingCollector()
} else {
c.timing = nil
}
}
// Run starts processing messages on the connection.
// It blocks until the context is cancelled or an error occurs.
func (c *SyncConn) Run(ctx context.Context) error {
for {
if ctx.Err() != nil {
return ctx.Err()
}
c.mu.Lock()
msg, err := c.protocol.ReadMessage()
c.mu.Unlock()
if err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return err
}
if msg.IsRequest() {
c.handleRequest(ctx, msg)
} else if msg.IsNotification() {
c.handleNotification(ctx, msg)
} else {
// Responses are not expected in the main loop - they are read inline by Call().
return errors.New("api: unexpected response message in sync connection")
}
}
}
// handleRequest processes an incoming request.
func (c *SyncConn) handleRequest(ctx context.Context, msg *Message) {
// Intercept the meta-requests for collected server timing before dispatching
// to the handler, so they are answered directly and not themselves recorded.
switch msg.Method {
case string(MethodGetServerTiming):
c.mu.Lock()
writeErr := c.protocol.WriteResponse(msg.ID, serverTimingSnapshot(c.timing))
c.mu.Unlock()
if writeErr != nil {
panic(fmt.Sprintf("api: failed to write server timing response: %v", writeErr))
}
return
case string(MethodResetServerTiming):
if c.timing != nil {
c.timing.reset()
}
c.mu.Lock()
writeErr := c.protocol.WriteResponse(msg.ID, nil)
c.mu.Unlock()
if writeErr != nil {
panic(fmt.Sprintf("api: failed to write reset server timing response: %v", writeErr))
}
return
}
var result any
var err error
start := time.Time{}
if c.timing != nil {
start = time.Now()
}
// Recover from panics and convert to error response with stack trace
defer func() {
if r := recover(); r != nil {
stack := string(debug.Stack())
err = fmt.Errorf("panic: %v\n%s", r, stack)
c.mu.Lock()
writeErr := c.protocol.WriteError(msg.ID, &jsonrpc.ResponseError{
Code: jsonrpc.CodeInternalError,
Message: err.Error(),
})
c.mu.Unlock()
if writeErr != nil {
panic(fmt.Sprintf("api: failed to write panic error response: %v (original panic: %v)", writeErr, r))
}
}
}()
result, err = c.handler.HandleRequest(ctx, msg.Method, msg.Params)
if c.timing != nil {
c.timing.record(msg.Method, time.Since(start))
}
c.mu.Lock()
defer c.mu.Unlock()
var writeErr error
if err != nil {
writeErr = c.protocol.WriteError(msg.ID, &jsonrpc.ResponseError{
Code: jsonrpc.CodeInternalError,
Message: err.Error(),
})
} else {
writeErr = c.protocol.WriteResponse(msg.ID, result)
}
if writeErr != nil {
panic(fmt.Sprintf("api: failed to write response: %v", writeErr))
}
}
// handleNotification processes an incoming notification.
func (c *SyncConn) handleNotification(ctx context.Context, msg *Message) {
_ = c.handler.HandleNotification(ctx, msg.Method, msg.Params)
}
// Call sends a request to the client and waits for a response.
// This method is safe to call from multiple goroutines - calls are serialized.
func (c *SyncConn) Call(ctx context.Context, method string, params any) (json.Value, error) {
// Serialize all Call operations. This is critical because:
// 1. The msgpack protocol uses method names as response IDs
// 2. The handler code (project internals) may spawn goroutines that call
// filesystem callbacks concurrently
// 3. We need to ensure write/read pairs are atomic
c.mu.Lock()
defer c.mu.Unlock()
id := jsonrpc.NewIDString(method)
if err := c.protocol.WriteRequest(id, method, params); err != nil {
return nil, err
}
if ctx.Err() != nil {
return nil, ctx.Err()
}
// Read the response inline.
msg, err := c.protocol.ReadMessage()
if err != nil {
return nil, err
}
if msg.IsResponse() && msg.ID != nil && msg.ID.String() == method {
if msg.Error != nil {
return nil, fmt.Errorf("api: remote error [%d]: %s", msg.Error.Code, msg.Error.Message)
}
return msg.Result, nil
}
// Unexpected message while waiting for response
return nil, fmt.Errorf("api: unexpected message while waiting for %q response", method)
}
// Notify sends a notification to the client (no response expected).
func (c *SyncConn) Notify(ctx context.Context, method string, params any) error {
c.mu.Lock()
defer c.mu.Unlock()
return c.protocol.WriteNotification(method, params)
}

View File

@@ -0,0 +1,381 @@
package encoder
import (
"encoding/binary"
"errors"
"fmt"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/tspath"
)
// astDecoder reconstructs real *ast.Node objects from binary-encoded data.
type astDecoder struct {
raw []byte
strTable uint32
strData uint32
extData uint32
nodeOff uint32
nodeCount int
factory *ast.NodeFactory
childBuf []int
// Single Go string covering all string data; substrings are zero-alloc slices.
allStringData string
// Arena for batch-allocating []*ast.Node slices used by NodeLists.
nodeArena []*ast.Node
// Results
nodes []*ast.Node
nodeLists []*ast.NodeList
}
// DecodeSourceFile decodes binary-encoded data into an *ast.SourceFile.
func DecodeSourceFile(data []byte) (*ast.SourceFile, error) {
node, err := DecodeNodes(data)
if err != nil {
return nil, err
}
if node.Kind != ast.KindSourceFile {
return nil, fmt.Errorf("expected SourceFile root, got %v", node.Kind)
}
return node.AsSourceFile(), nil
}
// DecodeNodes decodes binary-encoded AST data into a tree of *ast.Node objects.
func DecodeNodes(data []byte) (*ast.Node, error) {
d, err := newASTDecoder(data)
if err != nil {
return nil, err
}
return d.decode()
}
func newASTDecoder(data []byte) (*astDecoder, error) {
if len(data) < HeaderSize {
return nil, fmt.Errorf("data too short for header: %d bytes", len(data))
}
version := data[HeaderOffsetMetadata+3]
if version != ProtocolVersion {
return nil, fmt.Errorf("unsupported protocol version %d (expected %d)", version, ProtocolVersion)
}
strTable := readLE32(data, HeaderOffsetStringOffsets)
strData := readLE32(data, HeaderOffsetStringData)
extData := readLE32(data, HeaderOffsetExtendedData)
nodeOff := readLE32(data, HeaderOffsetNodes)
dataLen := uint32(len(data))
// Validate that all offsets are within the buffer.
if strTable > dataLen || strData > dataLen || extData > dataLen || nodeOff > dataLen {
return nil, fmt.Errorf("invalid AST header offsets: offsets exceed data length (%d)", dataLen)
}
// Validate monotonic non-decreasing order of regions.
if !(strTable <= strData && strData <= extData && extData <= nodeOff) {
return nil, fmt.Errorf("invalid AST header offsets: expected strTable <= strData <= extData <= nodeOff (got %d, %d, %d, %d)", strTable, strData, extData, nodeOff)
}
d := &astDecoder{
raw: data,
strTable: strTable,
strData: strData,
extData: extData,
nodeOff: nodeOff,
factory: ast.NewNodeFactory(ast.NodeFactoryHooks{}),
}
d.nodeCount = (len(data) - int(d.nodeOff)) / NodeSize
// Convert entire string data region to a single Go string upfront.
// Substringing a Go string shares the backing array, so subsequent
// getString calls produce substrings with zero allocations.
d.allStringData = string(data[d.strData:])
return d, nil
}
// allocNodeSlice returns a zero-length slice with the given capacity, backed by
// the pre-allocated nodeArena. This avoids a heap allocation per NodeList.
func (d *astDecoder) allocNodeSlice(capacity int) []*ast.Node {
start := len(d.nodeArena)
d.nodeArena = d.nodeArena[:start+capacity]
return d.nodeArena[start : start : start+capacity]
}
// nodeField reads a uint32 field from node i at the given field offset.
func (d *astDecoder) nodeField(i int, field int) uint32 {
return readLE32(d.raw, int(d.nodeOff)+i*NodeSize+field)
}
func (d *astDecoder) getString(idx uint32) string {
offBase := int(d.strTable) + int(idx)*4
start := readLE32(d.raw, offBase)
end := readLE32(d.raw, offBase+4)
return d.allStringData[start:end]
}
// collectChildren returns indices of direct children of node i.
// The returned slice is reused across calls; callers must not retain it.
func (d *astDecoder) collectChildren(i int) []int {
d.childBuf = d.childBuf[:0]
if i+1 >= d.nodeCount {
return d.childBuf
}
firstChild := i + 1
if d.nodeField(firstChild, NodeOffsetParent) != uint32(i) {
return d.childBuf
}
d.childBuf = append(d.childBuf, firstChild)
next := int(d.nodeField(firstChild, NodeOffsetNext))
for next != 0 {
d.childBuf = append(d.childBuf, next)
next = int(d.nodeField(next, NodeOffsetNext))
}
return d.childBuf
}
func (d *astDecoder) decode() (*ast.Node, error) {
if d.nodeCount < 2 {
return nil, errors.New("no nodes to decode")
}
d.nodes = make([]*ast.Node, d.nodeCount)
d.nodeLists = make([]*ast.NodeList, d.nodeCount)
// Pre-allocate arena for NodeList child slices. Each node can appear as a
// child at most once, so nodeCount is an upper bound on total child pointers.
d.nodeArena = make([]*ast.Node, 0, d.nodeCount)
// Process bottom-up so children exist before parents.
for i := d.nodeCount - 1; i >= 1; i-- {
kind := d.nodeField(i, NodeOffsetKind)
pos := d.nodeField(i, NodeOffsetPos)
end := d.nodeField(i, NodeOffsetEnd)
data := d.nodeField(i, NodeOffsetData)
childIndices := d.collectChildren(i)
if kind == SyntaxKindNodeList {
childNodes := d.allocNodeSlice(len(childIndices))
for _, ci := range childIndices {
if d.nodes[ci] != nil {
childNodes = append(childNodes, d.nodes[ci])
}
}
nl := d.factory.NewNodeList(childNodes)
nl.Loc = core.NewTextRange(int(pos), int(end))
d.nodeLists[i] = nl
continue
}
node, err := d.createNode(ast.Kind(kind), data, childIndices)
if err != nil {
return nil, fmt.Errorf("at node %d (kind %v): %w", i, ast.Kind(kind), err)
}
node.Loc = core.NewTextRange(int(pos), int(end))
node.Flags = ast.NodeFlags(d.nodeField(i, NodeOffsetFlags))
d.nodes[i] = node
}
return d.nodes[1], nil
}
// getModifierList creates a *ast.ModifierList from a child index that is a NodeList.
func (d *astDecoder) getModifierList(ci int) *ast.ModifierList {
nl := d.nodeLists[ci]
if nl == nil {
return nil
}
ml := d.factory.NewModifierList(nl.Nodes)
ml.Loc = nl.Loc
return ml
}
// childIterator helps walk through children based on a bitmask.
type childIterator struct {
indices []int
pos int
}
func newChildIter(indices []int) childIterator {
return childIterator{indices: indices}
}
// next returns the index of the next child, advancing the position.
func (it *childIterator) next() int {
if it.pos >= len(it.indices) {
return 0
}
ci := it.indices[it.pos]
it.pos++
return ci
}
// nextIf returns the index of the next child if the corresponding mask bit is set.
func (it *childIterator) nextIf(mask uint8, bit uint8) int {
if mask&(1<<bit) == 0 {
return 0
}
return it.next()
}
func (d *astDecoder) nodeAt(ci int) *ast.Node {
if ci == 0 {
return nil
}
return d.nodes[ci]
}
func (d *astDecoder) nodeListAt(ci int) *ast.NodeList {
if ci == 0 {
return nil
}
return d.nodeLists[ci]
}
func (d *astDecoder) modifierListAt(ci int) *ast.ModifierList {
if ci == 0 {
return nil
}
return d.getModifierList(ci)
}
func (d *astDecoder) createNode(kind ast.Kind, data uint32, childIndices []int) (*ast.Node, error) {
dataType := data & NodeDataTypeMask
commonData := uint8((data >> 24) & 0x3f)
switch dataType {
case NodeDataTypeString:
return d.createStringNode(kind, data, commonData)
case NodeDataTypeExtendedData:
return d.createExtendedNode(kind, data, childIndices, commonData)
default:
return d.createChildrenNode(kind, data, childIndices, commonData)
}
}
func (d *astDecoder) decodeExtendedData_SourceFile(data uint32, childIndices []int, commonData uint8) (*ast.Node, error) {
extOff := int(d.extData) + int(data&NodeDataStringIndexMask)
textIdx := readLE32(d.raw, extOff)
fileNameIdx := readLE32(d.raw, extOff+4)
pathIdx := readLE32(d.raw, extOff+8)
text := d.getString(textIdx)
fileName := d.getString(fileNameIdx)
path := d.getString(pathIdx)
// Recover parse options from header.
parseOpts := readLE32(d.raw, HeaderOffsetParseOptions)
opts := ast.SourceFileParseOptions{
FileName: fileName,
Path: tspath.Path(path),
ExternalModuleIndicatorOptions: ast.ExternalModuleIndicatorOptions{
JSX: parseOpts&1 != 0,
Force: parseOpts&2 != 0,
},
}
// Collect children: first is statements NodeList, second is EndOfFile.
var stmts *ast.NodeList
var endOfFile *ast.Node
for _, ci := range childIndices {
if d.nodeField(ci, NodeOffsetKind) == SyntaxKindNodeList {
stmts = d.nodeListAt(ci)
} else if d.nodes[ci] != nil && d.nodes[ci].Kind == ast.KindEndOfFile {
endOfFile = d.nodes[ci]
}
}
if endOfFile == nil {
endOfFile = d.factory.NewToken(ast.KindEndOfFile)
}
return d.factory.NewSourceFile(opts, text, stmts, endOfFile), nil
}
func (d *astDecoder) decodeExtendedData_TemplateHead(data uint32, childIndices []int, commonData uint8) (*ast.Node, error) {
extOff := int(d.extData) + int(data&NodeDataStringIndexMask)
textIdx := readLE32(d.raw, extOff)
rawTextIdx := readLE32(d.raw, extOff+4)
flags := readLE32(d.raw, extOff+8)
return d.factory.NewTemplateHead(d.getString(textIdx), d.getString(rawTextIdx), ast.TokenFlags(flags)), nil
}
func (d *astDecoder) decodeExtendedData_TemplateMiddle(data uint32, childIndices []int, commonData uint8) (*ast.Node, error) {
extOff := int(d.extData) + int(data&NodeDataStringIndexMask)
textIdx := readLE32(d.raw, extOff)
rawTextIdx := readLE32(d.raw, extOff+4)
flags := readLE32(d.raw, extOff+8)
return d.factory.NewTemplateMiddle(d.getString(textIdx), d.getString(rawTextIdx), ast.TokenFlags(flags)), nil
}
func (d *astDecoder) decodeExtendedData_TemplateTail(data uint32, childIndices []int, commonData uint8) (*ast.Node, error) {
extOff := int(d.extData) + int(data&NodeDataStringIndexMask)
textIdx := readLE32(d.raw, extOff)
rawTextIdx := readLE32(d.raw, extOff+4)
flags := readLE32(d.raw, extOff+8)
return d.factory.NewTemplateTail(d.getString(textIdx), d.getString(rawTextIdx), ast.TokenFlags(flags)), nil
}
func (d *astDecoder) singleChild(childIndices []int) *ast.Node {
if len(childIndices) == 0 {
return nil
}
return d.nodes[childIndices[0]]
}
func (d *astDecoder) singleNodeListChild(childIndices []int) *ast.NodeList {
if len(childIndices) == 0 {
return nil
}
return d.nodeLists[childIndices[0]]
}
func readLE32(data []byte, offset int) uint32 {
if offset < 0 || offset+4 > len(data) {
return 0
}
return binary.LittleEndian.Uint32(data[offset : offset+4])
}
// Hand-written commonData decoding functions. Each extracts the original values
// from the 6-bit commonData that were packed by the corresponding
// getNodeCommonData_* function.
func decodeNodeCommonData_SyntheticExpression(_ uint8) (any, bool) {
panic("SyntheticExpression should never be decoded")
}
// Hand-written extended data decoding functions for literal nodes.
func (d *astDecoder) decodeExtendedData_StringLiteral(data uint32, _ []int, _ uint8) (*ast.Node, error) {
extOff := int(d.extData) + int(data&NodeDataStringIndexMask)
textIdx := readLE32(d.raw, extOff)
flags := readLE32(d.raw, extOff+4)
return d.factory.NewStringLiteral(d.getString(textIdx), ast.TokenFlags(flags)), nil
}
func (d *astDecoder) decodeExtendedData_NumericLiteral(data uint32, _ []int, _ uint8) (*ast.Node, error) {
extOff := int(d.extData) + int(data&NodeDataStringIndexMask)
textIdx := readLE32(d.raw, extOff)
flags := readLE32(d.raw, extOff+4)
return d.factory.NewNumericLiteral(d.getString(textIdx), ast.TokenFlags(flags)), nil
}
func (d *astDecoder) decodeExtendedData_BigIntLiteral(data uint32, _ []int, _ uint8) (*ast.Node, error) {
extOff := int(d.extData) + int(data&NodeDataStringIndexMask)
textIdx := readLE32(d.raw, extOff)
flags := readLE32(d.raw, extOff+4)
return d.factory.NewBigIntLiteral(d.getString(textIdx), ast.TokenFlags(flags)), nil
}
func (d *astDecoder) decodeExtendedData_RegularExpressionLiteral(data uint32, _ []int, _ uint8) (*ast.Node, error) {
extOff := int(d.extData) + int(data&NodeDataStringIndexMask)
textIdx := readLE32(d.raw, extOff)
flags := readLE32(d.raw, extOff+4)
return d.factory.NewRegularExpressionLiteral(d.getString(textIdx), ast.TokenFlags(flags)), nil
}
func (d *astDecoder) decodeExtendedData_NoSubstitutionTemplateLiteral(data uint32, _ []int, _ uint8) (*ast.Node, error) {
extOff := int(d.extData) + int(data&NodeDataStringIndexMask)
textIdx := readLE32(d.raw, extOff)
flags := readLE32(d.raw, extOff+4)
return d.factory.NewNoSubstitutionTemplateLiteral(d.getString(textIdx), ast.TokenFlags(flags)), nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,450 @@
package encoder_test
import (
"os"
"path/filepath"
"testing"
"github.com/microsoft/typescript-go/internal/api/encoder"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/parser"
"github.com/microsoft/typescript-go/internal/repo"
"gotest.tools/v3/assert"
)
func parseSourceFile(code string) *ast.SourceFile {
return parser.ParseSourceFile(ast.SourceFileParseOptions{
FileName: "/test.ts",
Path: "/test.ts",
}, code, core.ScriptKindTS)
}
func TestDecodeSourceFile_Basic(t *testing.T) {
t.Parallel()
sf := parseSourceFile("let x = 1;")
buf, _, err := encoder.EncodeSourceFile(sf)
assert.NilError(t, err)
decoded, err := encoder.DecodeSourceFile(buf)
assert.NilError(t, err)
assert.Equal(t, decoded.AsNode().Kind, ast.KindSourceFile)
assert.Equal(t, decoded.FileName(), "/test.ts")
assert.Equal(t, decoded.Text(), "let x = 1;")
assert.Assert(t, decoded.Statements != nil)
assert.Assert(t, decoded.EndOfFileToken != nil)
}
func TestDecodeSourceFile_Statements(t *testing.T) {
t.Parallel()
sf := parseSourceFile("let a = 1;\nlet b = 2;\nlet c = 3;")
buf, _, err := encoder.EncodeSourceFile(sf)
assert.NilError(t, err)
decoded, err := encoder.DecodeSourceFile(buf)
assert.NilError(t, err)
assert.Equal(t, len(decoded.Statements.Nodes), 3)
for i, stmt := range decoded.Statements.Nodes {
assert.Equal(t, stmt.Kind, ast.KindVariableStatement, "statement %d", i)
}
}
func TestDecodeSourceFile_VariableDeclaration(t *testing.T) {
t.Parallel()
sf := parseSourceFile("let x = 1;")
buf, _, err := encoder.EncodeSourceFile(sf)
assert.NilError(t, err)
decoded, err := encoder.DecodeSourceFile(buf)
assert.NilError(t, err)
varStmt := decoded.Statements.Nodes[0].AsVariableStatement()
assert.Assert(t, varStmt.DeclarationList != nil)
declList := varStmt.DeclarationList.AsVariableDeclarationList()
assert.Assert(t, declList.Declarations != nil)
assert.Equal(t, len(declList.Declarations.Nodes), 1)
decl := declList.Declarations.Nodes[0].AsVariableDeclaration()
assert.Equal(t, decl.Name().Kind, ast.KindIdentifier)
assert.Equal(t, decl.Name().AsIdentifier().Text, "x")
assert.Assert(t, decl.Initializer != nil)
assert.Equal(t, decl.Initializer.Kind, ast.KindNumericLiteral)
assert.Equal(t, decl.Initializer.AsNumericLiteral().Text, "1")
}
func TestDecodeSourceFile_VariableDeclarationListFlags(t *testing.T) {
t.Parallel()
tests := []struct {
name string
code string
expected ast.NodeFlags
}{
{"const", "const x = 1;", ast.NodeFlagsConst},
{"let", "let x = 1;", ast.NodeFlagsLet},
{"var", "var x = 1;", ast.NodeFlagsNone},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
sf := parseSourceFile(tt.code)
buf, _, err := encoder.EncodeSourceFile(sf)
assert.NilError(t, err)
decoded, err := encoder.DecodeSourceFile(buf)
assert.NilError(t, err)
declList := decoded.Statements.Nodes[0].AsVariableStatement().DeclarationList.AsVariableDeclarationList()
got := declList.Flags & (ast.NodeFlagsLet | ast.NodeFlagsConst)
assert.Equal(t, got, tt.expected, "flags for %q: got %d, want %d", tt.code, got, tt.expected)
})
}
}
func TestDecodeSourceFile_FunctionDeclaration(t *testing.T) {
t.Parallel()
sf := parseSourceFile("function add(a: number, b: number): number { return a + b; }")
buf, _, err := encoder.EncodeSourceFile(sf)
assert.NilError(t, err)
decoded, err := encoder.DecodeSourceFile(buf)
assert.NilError(t, err)
funcDecl := decoded.Statements.Nodes[0].AsFunctionDeclaration()
assert.Assert(t, funcDecl.Name() != nil)
assert.Equal(t, funcDecl.Name().AsIdentifier().Text, "add")
assert.Assert(t, funcDecl.Parameters != nil)
assert.Equal(t, len(funcDecl.Parameters.Nodes), 2)
assert.Assert(t, funcDecl.Type != nil)
assert.Assert(t, funcDecl.Body != nil)
param0 := funcDecl.Parameters.Nodes[0].AsParameterDeclaration()
assert.Equal(t, param0.Name().AsIdentifier().Text, "a")
assert.Assert(t, param0.Type != nil)
}
func TestDecodeSourceFile_ImportDeclaration(t *testing.T) {
t.Parallel()
sf := parseSourceFile(`import { bar } from "bar";`)
buf, _, err := encoder.EncodeSourceFile(sf)
assert.NilError(t, err)
decoded, err := encoder.DecodeSourceFile(buf)
assert.NilError(t, err)
imp := decoded.Statements.Nodes[0].AsImportDeclaration()
assert.Assert(t, imp.ImportClause != nil)
assert.Assert(t, imp.ModuleSpecifier != nil)
assert.Equal(t, imp.ModuleSpecifier.AsStringLiteral().Text, "bar")
clause := imp.ImportClause.AsImportClause()
assert.Assert(t, clause.NamedBindings != nil)
namedImports := clause.NamedBindings.AsNamedImports()
assert.Assert(t, namedImports.Elements != nil)
assert.Equal(t, len(namedImports.Elements.Nodes), 1)
spec := namedImports.Elements.Nodes[0].AsImportSpecifier()
assert.Equal(t, spec.Name().AsIdentifier().Text, "bar")
}
func TestDecodeSourceFile_IfStatement(t *testing.T) {
t.Parallel()
sf := parseSourceFile("if (true) { } else { }")
buf, _, err := encoder.EncodeSourceFile(sf)
assert.NilError(t, err)
decoded, err := encoder.DecodeSourceFile(buf)
assert.NilError(t, err)
ifStmt := decoded.Statements.Nodes[0].AsIfStatement()
assert.Assert(t, ifStmt.Expression != nil)
assert.Assert(t, ifStmt.ThenStatement != nil)
assert.Assert(t, ifStmt.ElseStatement != nil)
assert.Equal(t, ifStmt.ThenStatement.Kind, ast.KindBlock)
assert.Equal(t, ifStmt.ElseStatement.Kind, ast.KindBlock)
}
func TestDecodeSourceFile_TemplateExpression(t *testing.T) {
t.Parallel()
sf := parseSourceFile("let x = `hello ${name} world`;")
buf, _, err := encoder.EncodeSourceFile(sf)
assert.NilError(t, err)
decoded, err := encoder.DecodeSourceFile(buf)
assert.NilError(t, err)
varDecl := decoded.Statements.Nodes[0].AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes[0].AsVariableDeclaration()
tmplExpr := varDecl.Initializer.AsTemplateExpression()
assert.Assert(t, tmplExpr.Head != nil)
assert.Equal(t, tmplExpr.Head.AsTemplateHead().Text, "hello ")
assert.Assert(t, tmplExpr.TemplateSpans != nil)
assert.Equal(t, len(tmplExpr.TemplateSpans.Nodes), 1)
span := tmplExpr.TemplateSpans.Nodes[0].AsTemplateSpan()
assert.Assert(t, span.Expression != nil)
assert.Equal(t, span.Expression.Kind, ast.KindIdentifier)
assert.Assert(t, span.Literal != nil)
assert.Equal(t, span.Literal.AsTemplateTail().Text, " world")
}
func TestDecodeSourceFile_ExportModifier(t *testing.T) {
t.Parallel()
sf := parseSourceFile("export function foo() {}")
buf, _, err := encoder.EncodeSourceFile(sf)
assert.NilError(t, err)
decoded, err := encoder.DecodeSourceFile(buf)
assert.NilError(t, err)
funcDecl := decoded.Statements.Nodes[0].AsFunctionDeclaration()
assert.Assert(t, funcDecl.Modifiers() != nil)
assert.Equal(t, len(funcDecl.Modifiers().Nodes), 1)
assert.Equal(t, funcDecl.Modifiers().Nodes[0].Kind, ast.KindExportKeyword)
}
func TestDecodeSourceFile_Positions(t *testing.T) {
t.Parallel()
code := "let x = 1;"
sf := parseSourceFile(code)
buf, _, err := encoder.EncodeSourceFile(sf)
assert.NilError(t, err)
decoded, err := encoder.DecodeSourceFile(buf)
assert.NilError(t, err)
assert.Equal(t, decoded.AsNode().Pos(), 0)
assert.Equal(t, decoded.AsNode().End(), len(code))
}
func TestDecodeSourceFile_ClassDeclaration(t *testing.T) {
t.Parallel()
sf := parseSourceFile("class Foo { bar(): void {} }")
buf, _, err := encoder.EncodeSourceFile(sf)
assert.NilError(t, err)
decoded, err := encoder.DecodeSourceFile(buf)
assert.NilError(t, err)
classDecl := decoded.Statements.Nodes[0].AsClassDeclaration()
assert.Assert(t, classDecl.Name() != nil)
assert.Equal(t, classDecl.Name().AsIdentifier().Text, "Foo")
assert.Assert(t, classDecl.Members != nil)
assert.Equal(t, len(classDecl.Members.Nodes), 1)
assert.Equal(t, classDecl.Members.Nodes[0].Kind, ast.KindMethodDeclaration)
}
func TestDecodeNodes_SubtreeRoundTrip(t *testing.T) {
t.Parallel()
sf := parseSourceFile("function greet(name: string) { return `Hello, ${name}!`; }")
var funcNode *ast.Node
visitor := &ast.NodeVisitor{}
visitor.Visit = func(node *ast.Node) *ast.Node {
if node.Kind == ast.KindFunctionDeclaration && funcNode == nil {
funcNode = node
}
return node
}
visitor.VisitEachChild(sf.AsNode())
assert.Assert(t, funcNode != nil)
buf, _, err := encoder.EncodeNode(funcNode, sf)
assert.NilError(t, err)
decoded, err := encoder.DecodeNodes(buf)
assert.NilError(t, err)
assert.Equal(t, decoded.Kind, ast.KindFunctionDeclaration)
funcDecl := decoded.AsFunctionDeclaration()
assert.Assert(t, funcDecl.Name() != nil)
assert.Equal(t, funcDecl.Name().AsIdentifier().Text, "greet")
assert.Assert(t, funcDecl.Parameters != nil)
assert.Equal(t, len(funcDecl.Parameters.Nodes), 1)
assert.Assert(t, funcDecl.Body != nil)
}
func TestDecodeSourceFile_BinaryExpression(t *testing.T) {
t.Parallel()
sf := parseSourceFile("let x = 1 + 2;")
buf, _, err := encoder.EncodeSourceFile(sf)
assert.NilError(t, err)
decoded, err := encoder.DecodeSourceFile(buf)
assert.NilError(t, err)
decl := decoded.Statements.Nodes[0].AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes[0].AsVariableDeclaration()
binExpr := decl.Initializer.AsBinaryExpression()
assert.Assert(t, binExpr.Left != nil)
assert.Assert(t, binExpr.Right != nil)
assert.Assert(t, binExpr.OperatorToken != nil)
assert.Equal(t, binExpr.Left.Kind, ast.KindNumericLiteral)
assert.Equal(t, binExpr.Right.Kind, ast.KindNumericLiteral)
}
func TestDecodeSourceFile_KeywordExpressions(t *testing.T) {
t.Parallel()
// "this" must decode as KeywordExpression, not Token, or the printer panics
sf := parseSourceFile("const x = this;")
buf, _, err := encoder.EncodeSourceFile(sf)
assert.NilError(t, err)
decoded, err := encoder.DecodeSourceFile(buf)
assert.NilError(t, err)
// Navigate: const x = this -> VariableStatement -> declaration -> initializer
decl := decoded.Statements.Nodes[0].AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes[0].AsVariableDeclaration()
thisExpr := decl.Initializer
assert.Equal(t, thisExpr.Kind, ast.KindThisKeyword)
// This would panic if decoded as Token instead of KeywordExpression
assert.Assert(t, thisExpr.AsKeywordExpression() != nil)
}
func TestDecodeSourceFile_EmptyModuleBlock(t *testing.T) {
t.Parallel()
sf := parseSourceFile("namespace N { }")
buf, _, err := encoder.EncodeSourceFile(sf)
assert.NilError(t, err)
decoded, err := encoder.DecodeSourceFile(buf)
assert.NilError(t, err)
// Navigate: namespace N { } -> ModuleDeclaration -> ModuleBlock
mod := decoded.Statements.Nodes[0].AsModuleDeclaration()
assert.Assert(t, mod.Body != nil)
block := mod.Body.AsModuleBlock()
// Statements must be non-nil even when empty, otherwise the printer panics
assert.Assert(t, block.Statements != nil)
assert.Equal(t, len(block.Statements.Nodes), 0)
}
func TestDecodeSourceFile_EmptyBlockAndParams(t *testing.T) {
t.Parallel()
// Empty blocks and parameter lists must decode with non-nil NodeLists (not nil),
// matching parser behavior. Previously the decoder left them nil, crashing the printer.
sf := parseSourceFile("function foo() {}")
buf, _, err := encoder.EncodeSourceFile(sf)
assert.NilError(t, err)
decoded, err := encoder.DecodeSourceFile(buf)
assert.NilError(t, err)
funcDecl := decoded.Statements.Nodes[0].AsFunctionDeclaration()
assert.Assert(t, funcDecl.Parameters != nil, "FunctionDeclaration.Parameters must be non-nil for foo()")
assert.Equal(t, len(funcDecl.Parameters.Nodes), 0)
assert.Assert(t, funcDecl.Body != nil)
block := funcDecl.Body.AsBlock()
assert.Assert(t, block.Statements != nil, "Block.Statements must be non-nil for empty blocks")
assert.Equal(t, len(block.Statements.Nodes), 0)
}
func TestDecodeSourceFile_ArrowFunctionEmptyParams(t *testing.T) {
t.Parallel()
// `() => {}` must decode with non-nil Parameters (empty NodeList),
// matching parser behavior. Previously the decoder left it nil, crashing the printer.
sf := parseSourceFile("const f = () => {};")
buf, _, err := encoder.EncodeSourceFile(sf)
assert.NilError(t, err)
decoded, err := encoder.DecodeSourceFile(buf)
assert.NilError(t, err)
decl := decoded.Statements.Nodes[0].AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes[0].AsVariableDeclaration()
arrow := decl.Initializer.AsArrowFunction()
assert.Assert(t, arrow.Parameters != nil, "ArrowFunction.Parameters must be non-nil for () => {}")
assert.Equal(t, len(arrow.Parameters.Nodes), 0)
assert.Assert(t, arrow.Body != nil)
block := arrow.Body.AsBlock()
assert.Assert(t, block.Statements != nil, "Block.Statements must be non-nil for empty body")
assert.Equal(t, len(block.Statements.Nodes), 0)
}
func TestDecodeSourceFile_FunctionExpressionEmptyParams(t *testing.T) {
t.Parallel()
// `function() {}` must decode with non-nil Parameters (empty NodeList).
sf := parseSourceFile("const f = function() {};")
buf, _, err := encoder.EncodeSourceFile(sf)
assert.NilError(t, err)
decoded, err := encoder.DecodeSourceFile(buf)
assert.NilError(t, err)
decl := decoded.Statements.Nodes[0].AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes[0].AsVariableDeclaration()
funcExpr := decl.Initializer.AsFunctionExpression()
assert.Assert(t, funcExpr.Parameters != nil, "FunctionExpression.Parameters must be non-nil for function() {}")
assert.Equal(t, len(funcExpr.Parameters.Nodes), 0)
}
func TestDecodeSourceFile_PostfixUnaryOperator(t *testing.T) {
t.Parallel()
sf := parseSourceFile("let i = 0; i++;")
buf, _, err := encoder.EncodeSourceFile(sf)
assert.NilError(t, err)
decoded, err := encoder.DecodeSourceFile(buf)
assert.NilError(t, err)
exprStmt := decoded.Statements.Nodes[1].AsExpressionStatement()
postfix := exprStmt.Expression.AsPostfixUnaryExpression()
assert.Equal(t, postfix.Operator, ast.KindPlusPlusToken)
assert.Equal(t, postfix.Operand.Kind, ast.KindIdentifier)
}
func TestDecodeSourceFile_PrefixUnaryOperator(t *testing.T) {
t.Parallel()
sf := parseSourceFile("let x = true; !x;")
buf, _, err := encoder.EncodeSourceFile(sf)
assert.NilError(t, err)
decoded, err := encoder.DecodeSourceFile(buf)
assert.NilError(t, err)
exprStmt := decoded.Statements.Nodes[1].AsExpressionStatement()
prefix := exprStmt.Expression.AsPrefixUnaryExpression()
assert.Equal(t, prefix.Operator, ast.KindExclamationToken)
assert.Equal(t, prefix.Operand.Kind, ast.KindIdentifier)
}
func TestDecodeSourceFile_PostfixDecrement(t *testing.T) {
t.Parallel()
sf := parseSourceFile("let n = 5; n--;")
buf, _, err := encoder.EncodeSourceFile(sf)
assert.NilError(t, err)
decoded, err := encoder.DecodeSourceFile(buf)
assert.NilError(t, err)
exprStmt := decoded.Statements.Nodes[1].AsExpressionStatement()
postfix := exprStmt.Expression.AsPostfixUnaryExpression()
assert.Equal(t, postfix.Operator, ast.KindMinusMinusToken)
}
func BenchmarkDecodeSourceFile(b *testing.B) {
repo.SkipIfNoTypeScriptSubmodule(b)
filePath := filepath.Join(repo.TypeScriptSubmodulePath(), "src/compiler/checker.ts")
fileContent, err := os.ReadFile(filePath)
assert.NilError(b, err)
code := string(fileContent)
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
FileName: "/checker.ts",
Path: "/checker.ts",
}, code, core.ScriptKindTS)
buf, _, err := encoder.EncodeSourceFile(sourceFile)
assert.NilError(b, err)
b.Run("parse", func(b *testing.B) {
for b.Loop() {
parser.ParseSourceFile(ast.SourceFileParseOptions{
FileName: "/checker.ts",
Path: "/checker.ts",
}, code, core.ScriptKindTS)
}
})
b.Run("decode", func(b *testing.B) {
for b.Loop() {
_, decodeErr := encoder.DecodeSourceFile(buf)
assert.NilError(b, decodeErr)
}
})
}

View File

@@ -0,0 +1,844 @@
package encoder
import (
"cmp"
"encoding/binary"
"fmt"
"slices"
"sync"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/core"
"github.com/zeebo/xxh3"
)
func init() {
if ast.KindLastUnaryOperator > 0x3f {
panic(fmt.Sprintf("KindLastUnaryOperator (%d) exceeds the 6-bit commonData capacity (max 63)", ast.KindLastUnaryOperator))
}
}
const (
NodeOffsetKind = iota * 4
NodeOffsetPos
NodeOffsetEnd
NodeOffsetNext
NodeOffsetParent
NodeOffsetData
NodeOffsetFlags
// NodeSize is the number of bytes that represents a single node in the encoded format.
NodeSize
)
const (
NodeDataTypeChildren uint32 = iota << 30
NodeDataTypeString
NodeDataTypeExtendedData
)
const (
NodeDataTypeMask uint32 = 0xc0_00_00_00
NodeDataChildMask uint32 = 0x00_00_00_ff
NodeDataStringIndexMask uint32 = 0x00_ff_ff_ff
)
const (
SyntaxKindNodeList uint32 = 1<<32 - 1
)
const (
HeaderOffsetMetadata = iota * 4
HeaderOffsetHashLo0
HeaderOffsetHashLo1
HeaderOffsetHashHi0
HeaderOffsetHashHi1
HeaderOffsetParseOptions
HeaderOffsetStringOffsets
HeaderOffsetStringData
HeaderOffsetExtendedData
HeaderOffsetStructuredData
HeaderOffsetNodes
HeaderSize
)
const (
ProtocolVersion uint8 = 5
)
// Source File Binary Format
// =========================
//
// The following defines a protocol for serializing TypeScript SourceFile objects to a compact binary format. All integer
// values are little-endian.
//
// Overview
// --------
//
// The format comprises seven sections:
//
// | Section | Length | Description |
// | ------------------ | ------------------ | ----------------------------------------------------------------------------------------------- |
// | Header | 44 bytes | Contains the content hash, parse options, flags, and byte offsets to the start of each section. |
// | String offsets | 8 bytes per string | Pairs of starting byte offsets and ending byte offsets into the **string data** section. |
// | String data | variable | UTF-8 encoded string data. |
// | Extended node data | variable | Extra data for some kinds of nodes. |
// | Structured data | variable | Msgpack-encoded metadata blobs (e.g. file references). |
// | Nodes | 28 bytes per node | Defines the AST structure of the file, with references to strings and extended data. |
//
// Header (44 bytes)
// -----------------
//
// The header contains the following fields:
//
// | Byte offset | Type | Field |
// | ----------- | --------- | ------------------------------------------------- |
// | 0 | uint8 | Protocol version |
// | 1-3 | | Reserved |
// | 4-19 | uint128 | Source file content hash (xxh3, LE) |
// | 20-23 | uint32 | Parse options (bitmask; bit 0: JSX, bit 1: Force) |
// | 24-27 | uint32 | Byte offset to string offsets section |
// | 28-31 | uint32 | Byte offset to string data section |
// | 32-35 | uint32 | Byte offset to extended node data section |
// | 36-39 | uint32 | Byte offset to structured data section |
// | 40-43 | uint32 | Byte offset to nodes section |
//
// String offsets (8 bytes per string)
// -----------------------------------
//
// Each string offset entry consists of two 4-byte unsigned integers, representing the start and end byte offsets into the
// **string data** section.
//
// String data (variable)
// ----------------------
//
// The string data section contains UTF-8 encoded string data, with WTF-8 used for JS strings containing lone UTF-16
// surrogates. In typical cases, the entirety of the string data is the source file text, and individual nodes with
// string properties reference their positional slice of the file text. In cases where a node's string property is not
// equal to the slice of file text at its position, the unique string is appended to the string data section after the
// file text.
//
// Extended node data (variable)
// -----------------------------
//
// The extended node data section contains additional data for specific node types. The length and meaning of each entry
// is defined by the node type.
//
// Currently, the only node types that use this section are `TemplateHead`, `TemplateMiddle`, `TemplateTail`, and
// `SourceFile`. The extended data format for the first three is:
//
// | Byte offset | Type | Field |
// | ----------- | ------ | ------------------------------------------------ |
// | 0-4 | uint32 | Index of `text` in the string offsets section |
// | 4-8 | uint32 | Index of `rawText` in the string offsets section |
// | 8-12 | uint32 | Value of `templateFlags` |
//
// and for `SourceFile` is:
//
// | Byte offset | Type | Field |
// | ----------- | ------ | -------------------------------------------------------------- |
// | 0-4 | uint32 | Index of `text` in the string offsets section |
// | 4-8 | uint32 | Index of `fileName` in the string offsets section |
// | 8-12 | uint32 | Index of `path` in the string offsets section |
// | 12-16 | uint32 | Value of `languageVariant` |
// | 16-20 | uint32 | Value of `scriptKind` |
// | 20-24 | uint32 | Byte offset of `referencedFiles` in structured data section |
// | 24-28 | uint32 | Byte offset of `typeReferenceDirectives` in structured data |
// | 28-32 | uint32 | Byte offset of `libReferenceDirectives` in structured data |
// | 32-36 | uint32 | Byte offset of `imports` node index array in structured data |
// | 36-40 | uint32 | Byte offset of `moduleAugmentations` node index array |
// | 40-44 | uint32 | Byte offset of `ambientModuleNames` string array |
// | 44-48 | uint32 | Node index of `externalModuleIndicator` (0 = nil) |
//
// Structured data (variable)
// --------------------------
//
// The structured data section contains msgpack-encoded metadata blobs. Each blob is a self-contained
// msgpack value. File reference arrays use the following tuple format:
//
// [pos: uint, end: uint, fileName: string, resolutionMode: uint, preserve: bool]
//
// Node index arrays (imports, moduleAugmentations) are msgpack arrays of uint values, where each
// value is a node index into the nodes section. String arrays (ambientModuleNames) are msgpack
// arrays of string values.
//
// An offset of 0xFFFFFFFF indicates no data (empty array).
//
// Nodes (28 bytes per node)
// -------------------------
//
// The nodes section contains the AST structure of the file. Nodes are represented in a flat array in source order,
// heavily inspired by https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-11/. Each node has the following
// structure:
//
// | Byte offset | Type | Field |
// | ----------- | ------ | -------------------------- |
// | 0-4 | uint32 | Kind |
// | 4-8 | uint32 | Pos |
// | 8-12 | uint32 | End |
// | 12-16 | uint32 | Node index of next sibling |
// | 16-20 | uint32 | Node index of parent |
// | 20-24 | | Node data |
// | 24-28 | uint32 | Node flags |
//
// The first 28 bytes of the nodes section are zeros representing a nil node, such that nodes without a parent or next
// sibling can unambiuously use `0` for those indices.
//
// NodeLists are represented as normal nodes with the special `kind` value `0xff_ff_ff_ff`. They are considered the parent
// of their contents in the encoded format. A client reconstructing an AST similar to TypeScript's internal representation
// should instead set the `parent` pointers of a NodeList's children to the NodeList's parent. A NodeList's `data` field
// is the uint32 length of the list, and does not use one of the data types described below.
//
// For node types other than NodeList, the node data field encodes one of the following, determined by the first 2 bits of
// the field:
//
// | Value | Data type | Description |
// | ----- | --------- | ------------------------------------------------------------------------------------ |
// | 0b00 | Children | Disambiguates which named properties of the node its children should be assigned to. |
// | 0b01 | String | The index of the node's string property in the **string offsets** section. |
// | 0b10 | Extended | The byte offset of the node's extended data into the **extended node data** section. |
// | 0b11 | Reserved | Reserved for future use. |
//
// In all node data types, the remaining 6 bits of the first byte are used to encode small values specific to the node
// type. For most node types, these are individual boolean flags. For unary expressions, all 6 bits encode the operator's
// SyntaxKind value (e.g., PlusPlusToken=45, TildeToken=54), which fits because KindLastUnaryOperator (54) <= 0x3f (63).
//
// | Node type | Bits 0-5 | Notes |
// | ---------------------------- | ------------------------------------- | ------------------------------ |
// | `ImportSpecifier` | Bit 0: `isTypeOnly` | |
// | `ImportClause` | Bit 0: `isTypeOnly`, Bit 1: `isDefer` | |
// | `ExportSpecifier` | Bit 0: `isTypeOnly` | |
// | `ImportEqualsDeclaration` | Bit 0: `isTypeOnly` | |
// | `ExportDeclaration` | Bit 0: `isTypeOnly` | |
// | `ImportTypeNode` | Bit 0: `isTypeOf` | |
// | `ExportAssignment` | Bit 0: `isExportEquals` | |
// | `Block` | Bit 0: `multiline` | |
// | `ArrayLiteralExpression` | Bit 0: `multiline` | |
// | `ObjectLiteralExpression` | Bit 0: `multiline` | |
// | `JsxText` | Bit 0: `containsOnlyTriviaWhiteSpaces`| |
// | `JSDocTypeLiteral` | Bit 0: `isArrayType` | |
// | `JsDocPropertyTag` | Bit 0: `isBracketed`, Bit 1: `isNameFirst` | |
// | `JsDocParameterTag` | Bit 0: `isBracketed`, Bit 1: `isNameFirst` | |
// | `VariableDeclarationList` | Bit 0: is `let`, Bit 1: is `const` | |
// | `ImportAttributes` | Bit 0: `multiline`, Bit 1: is `assert`| |
// | `PrefixUnaryExpression` | Bits 0-5: operator SyntaxKind | e.g., `!`, `~`, `++`, `--` |
// | `PostfixUnaryExpression` | Bits 0-5: operator SyntaxKind | e.g., `++`, `--` |
//
// The remaining 3 bytes of the node data field vary by data type:
//
// ### Children (0b00)
//
// If a node has fewer children than its type allows, additional data is needed to determine which properties the children
// correspond to. The last byte of the 4-byte data field is a bitmask representing the child properties of the node type,
// in visitor order, where `1` indicates that the child at that property is present and `0` indicates that the property is
// nil. For example, a `MethodDeclaration` has the following child properties:
//
// | Property name | Bit position |
// | -------------- | ------------ |
// | modifiers | 0 |
// | asteriskToken | 1 |
// | name | 2 |
// | postfixToken | 3 |
// | typeParameters | 4 |
// | parameters | 5 |
// | returnType | 6 |
// | body | 7 |
//
// A bitmask with value `0b01100101` would indicate that the next four direct descendants (i.e., node records that have a
// `parent` set to the node index of the `MethodDeclaration`) of the node are its `modifiers`, `name`, `parameters`, and
// `body` properties, in that order. The remaining properties are nil. (To reconstruct the node with named properties, the
// client must consult a static table of each node type's child property names.)
//
// The bitmask may be zero for node types that can only have a single child, since no disambiguation is needed.
// Additionally, the children data type may be used for nodes that can never have children, but do not require other
// data types.
//
// ### String (0b01)
//
// The string data type is used for nodes with a single string property. (Currently, the name of that property is always
// `text`.) The last three bytes of the 4-byte data field form a single 24-bit unsigned integer (i.e.,
// `uint32(0x00_ff_ff_ff & node.data)`) _N_ that is an index into the **string offsets** section. The *N*th 32-bit
// unsigned integer in the **string offsets** section is the byte offset of the start of the string in the **string data**
// section, and the *N+1*th 32-bit unsigned integer is the byte offset of the end of the string in the
// **string data** section.
//
// ### Extended (0b10)
//
// The extended data type is used for nodes with properties that don't fit into either the children or string data types.
// The last three bytes of the 4-byte data field form a single 24-bit unsigned integer (i.e.,
// `uint32(0x00_ff_ff_ff & node.data)`) _N_ that is a byte offset into the **extended node data** section. The length and
// meaning of the data at that offset is defined by the node type. See the **Extended node data** section for details on
// the format of the extended data for specific node types.
//
// Encoding Arbitrary Nodes
// ------------------------
//
// The same binary format can be used to encode an arbitrary subtree of a SourceFile, not just a whole SourceFile. When
// encoding a non-SourceFile node, the format is identical with the following differences:
//
// - The content hash fields in the header (bytes 4-19) are zero.
// - The parse options field in the header (bytes 20-23) is zero.
// - The root node in the nodes section uses its actual node kind and data encoding (via getNodeData) rather than the
// SourceFile-specific extended data format.
//
// The string data section contains only the strings referenced by nodes in the subtree, rather than the full source
// file text. The EncodeNode function provides this entrypoint.
// SourceFileHash returns the 128-bit content hash for a source file as a hex string.
func SourceFileHash(sourceFile *ast.SourceFile) string {
h := sourceFile.Hash
return fmt.Sprintf("%016x%016x", h.Hi, h.Lo)
}
// encodeParseOptions encodes the per-file ExternalModuleIndicatorOptions as a uint32 bitmask.
func encodeParseOptions(opts ast.ExternalModuleIndicatorOptions) uint32 {
var bits uint32
if opts.JSX {
bits |= 1
}
if opts.Force {
bits |= 2
}
return bits
}
// NodeIndexTable maps between AST nodes and their encoder indices for O(1) node handle resolution.
type NodeIndexTable struct {
Nodes []*ast.Node // index → node (for resolution)
sortedOnce sync.Once
sortedIdx []uint32 // indices into Nodes, sorted by node ID; built lazily
}
var nodeIndexTableKey = ast.NewSourceFileDataKey[*NodeIndexTable]()
// GetIndex returns the encoder index for the given node.
// On the first call the sortedIdx array is built (O(n log n) sort on a flat []uint32),
// then subsequent calls use binary search (O(log n)). This turns out to be much faster than
// building a map[*ast.Node]uint32 and not significantly slower for lookups.
func (t *NodeIndexTable) GetIndex(node *ast.Node) uint32 {
t.sortedOnce.Do(func() {
idx := make([]uint32, 0, len(t.Nodes))
for i, n := range t.Nodes {
if n != nil {
idx = append(idx, uint32(i))
}
}
nodes := t.Nodes
slices.SortFunc(idx, func(a, b uint32) int {
return cmp.Compare(ast.GetNodeId(nodes[a]), ast.GetNodeId(nodes[b]))
})
t.sortedIdx = idx
})
target := ast.GetNodeId(node)
i, found := core.BinarySearchUniqueFunc(t.sortedIdx, func(_ int, el uint32) int {
return cmp.Compare(ast.GetNodeId(t.Nodes[el]), target)
})
if found {
return t.sortedIdx[i]
}
return 0
}
// BuildNodeIndexTable walks the AST in the same order as encodeTree and builds
// a NodeIndexTable without performing the full binary encoding. This is used to
// eagerly create index tables for files that need node handles before getSourceFile
// is called. The indices produced are guaranteed to match those from EncodeSourceFile.
func BuildNodeIndexTable(sourceFile *ast.SourceFile) *NodeIndexTable {
var nodeCount uint32
nodeTable := make([]*ast.Node, 1, sourceFile.NodeCount+1) // index 0 = nil sentinel
visitor := &ast.NodeVisitor{
Hooks: ast.NodeVisitorHooks{
VisitNodes: func(nodeList *ast.NodeList, visitor *ast.NodeVisitor) *ast.NodeList {
if nodeList == nil {
return nodeList
}
nodeCount++
nodeTable = append(nodeTable, nil) // NodeLists are not *ast.Node
visitor.VisitSlice(nodeList.Nodes)
return nodeList
},
VisitModifiers: func(modifiers *ast.ModifierList, visitor *ast.NodeVisitor) *ast.ModifierList {
if modifiers != nil && len(modifiers.Nodes) > 0 {
visitor.Hooks.VisitNodes(&modifiers.NodeList, visitor)
}
return modifiers
},
},
}
visitor.Visit = func(node *ast.Node) *ast.Node {
nodeCount++
nodeTable = append(nodeTable, node)
visitor.VisitEachChild(node)
for _, jsdoc := range node.JSDoc(sourceFile) {
visitor.Visit(jsdoc)
}
return node
}
rootNode := sourceFile.AsNode()
// Index 1 = root node (matches encodeTree)
nodeCount++
nodeTable = append(nodeTable, rootNode)
visitor.VisitEachChild(rootNode)
for _, jsdoc := range rootNode.JSDoc(sourceFile) {
visitor.Visit(jsdoc)
}
return &NodeIndexTable{Nodes: nodeTable}
}
func GetNodeIndexTable(sourceFile *ast.SourceFile) *NodeIndexTable {
return ast.GetOrComputeSourceFileData(sourceFile, nodeIndexTableKey, BuildNodeIndexTable)
}
// EncodeSourceFile encodes an entire source file AST into the binary format.
// Returns the encoded bytes and a NodeIndexTable mapping encoder indices to AST nodes.
func EncodeSourceFile(sourceFile *ast.SourceFile) ([]byte, *NodeIndexTable, error) {
data, nodeTable, err := encodeTree(sourceFile.AsNode(), sourceFile)
if err != nil {
return nil, nil, err
}
nodeTable = ast.GetOrComputeSourceFileData(sourceFile, nodeIndexTableKey, func(*ast.SourceFile) *NodeIndexTable {
return nodeTable
})
return data, nodeTable, nil
}
// EncodeNode encodes an arbitrary AST node and its descendants into the binary format.
// The sourceFile is needed to provide the source text for efficient string encoding.
// When encoding a non-SourceFile node, the header hash and parse options fields will be zero.
// Returns the encoded bytes and a NodeIndexTable mapping encoder indices to AST nodes.
func EncodeNode(node *ast.Node, sourceFile *ast.SourceFile) ([]byte, *NodeIndexTable, error) {
return encodeTree(node, sourceFile)
}
func encodeTree(rootNode *ast.Node, sourceFile *ast.SourceFile) ([]byte, *NodeIndexTable, error) {
var parentIndex, nodeCount, prevIndex uint32
var extendedData []byte
var structuredData []byte
var strs *stringTable
var positionMap *ast.PositionMap
if rootNode.Kind == ast.KindSourceFile {
strs = newStringTable(sourceFile.Text(), sourceFile.TextCount)
positionMap = sourceFile.GetPositionMap()
} else {
strs = newStringTable("", 0)
if sourceFile != nil {
positionMap = sourceFile.GetPositionMap()
}
}
if positionMap == nil {
positionMap = ast.ComputePositionMap("")
}
utf16 := func(pos int) uint32 {
return uint32(positionMap.UTF8ToUTF16(pos))
}
var initialNodeCount int
if sourceFile != nil {
initialNodeCount = sourceFile.NodeCount
}
nodes := make([]byte, 0, (initialNodeCount+1)*NodeSize)
// Build node index table for O(1) handle resolution.
// Index 0 is a nil sentinel; real nodes start at index 1.
nodeTable := make([]*ast.Node, 1, initialNodeCount+1) // index 0 = nil sentinel
// Build a small map of nodes we need to track indices for (imports + moduleAugmentations).
// Values start at 0 and are filled in during the walk.
var nodeIndexMap map[*ast.Node]uint32
var sfExtendedDataOffset int // byte offset in extendedData where SourceFile fields start
if rootNode.Kind == ast.KindSourceFile {
sf := rootNode.AsSourceFile()
total := len(sf.Imports()) + len(sf.ModuleAugmentations)
if sf.ExternalModuleIndicator != nil && sf.ExternalModuleIndicator != rootNode {
total++
}
if total > 0 {
nodeIndexMap = make(map[*ast.Node]uint32, total)
for _, imp := range sf.Imports() {
nodeIndexMap[imp.AsNode()] = 0
}
for _, aug := range sf.ModuleAugmentations {
nodeIndexMap[aug.AsNode()] = 0
}
if sf.ExternalModuleIndicator != nil && sf.ExternalModuleIndicator != rootNode {
nodeIndexMap[sf.ExternalModuleIndicator] = 0
}
}
}
visitor := &ast.NodeVisitor{
Hooks: ast.NodeVisitorHooks{
VisitNodes: func(nodeList *ast.NodeList, visitor *ast.NodeVisitor) *ast.NodeList {
if nodeList == nil {
return nodeList
}
nodeCount++
nodeTable = append(nodeTable, nil) // NodeLists are not *ast.Node
if prevIndex != 0 {
// this is the next sibling of `prevNode`
b0, b1, b2, b3 := uint8(nodeCount), uint8(nodeCount>>8), uint8(nodeCount>>16), uint8(nodeCount>>24)
nodes[prevIndex*NodeSize+NodeOffsetNext+0] = b0
nodes[prevIndex*NodeSize+NodeOffsetNext+1] = b1
nodes[prevIndex*NodeSize+NodeOffsetNext+2] = b2
nodes[prevIndex*NodeSize+NodeOffsetNext+3] = b3
}
nodes = appendUint32s(nodes, SyntaxKindNodeList, utf16(nodeList.Pos()), utf16(nodeList.End()), 0, parentIndex, uint32(len(nodeList.Nodes)), 0)
saveParentIndex := parentIndex
currentIndex := nodeCount
prevIndex = 0
parentIndex = currentIndex
visitor.VisitSlice(nodeList.Nodes)
prevIndex = currentIndex
parentIndex = saveParentIndex
return nodeList
},
VisitModifiers: func(modifiers *ast.ModifierList, visitor *ast.NodeVisitor) *ast.ModifierList {
if modifiers != nil && len(modifiers.Nodes) > 0 {
visitor.Hooks.VisitNodes(&modifiers.NodeList, visitor)
}
return modifiers
},
},
}
visitor.Visit = func(node *ast.Node) *ast.Node {
nodeCount++
nodeTable = append(nodeTable, node)
if prevIndex != 0 {
// this is the next sibling of `prevNode`
b0, b1, b2, b3 := uint8(nodeCount), uint8(nodeCount>>8), uint8(nodeCount>>16), uint8(nodeCount>>24)
nodes[prevIndex*NodeSize+NodeOffsetNext+0] = b0
nodes[prevIndex*NodeSize+NodeOffsetNext+1] = b1
nodes[prevIndex*NodeSize+NodeOffsetNext+2] = b2
nodes[prevIndex*NodeSize+NodeOffsetNext+3] = b3
}
nodes = appendUint32s(nodes, uint32(node.Kind), utf16(node.Pos()), utf16(node.End()), 0, parentIndex, getNodeData(node, strs, positionMap, &extendedData, &structuredData), uint32(node.Flags))
if nodeIndexMap != nil {
if _, ok := nodeIndexMap[node]; ok {
nodeIndexMap[node] = nodeCount
}
}
saveParentIndex := parentIndex
currentIndex := nodeCount
prevIndex = 0
parentIndex = currentIndex
visitor.VisitEachChild(node)
if sourceFile != nil {
for _, jsdoc := range node.JSDoc(sourceFile) {
visitor.Visit(jsdoc)
}
}
prevIndex = currentIndex
parentIndex = saveParentIndex
return node
}
nodes = appendUint32s(nodes, 0, 0, 0, 0, 0, 0, 0)
nodeCount++
parentIndex++
nodeTable = append(nodeTable, rootNode) // index 1 = root node
sfExtendedDataOffset = len(extendedData)
nodes = appendUint32s(nodes, uint32(rootNode.Kind), utf16(rootNode.Pos()), utf16(rootNode.End()), 0, 0, getNodeData(rootNode, strs, positionMap, &extendedData, &structuredData), uint32(rootNode.Flags))
visitor.VisitEachChild(rootNode)
if sourceFile != nil {
for _, jsdoc := range rootNode.JSDoc(sourceFile) {
visitor.Visit(jsdoc)
}
}
var hash xxh3.Uint128
var parseOpts uint32
if rootNode.Kind == ast.KindSourceFile {
hash = sourceFile.Hash
parseOpts = encodeParseOptions(sourceFile.ParseOptions().ExternalModuleIndicatorOptions)
// Encode imports, moduleAugmentations, and ambientModuleNames into structured data,
// and patch the placeholder offsets in the SourceFile extended data.
sf := rootNode.AsSourceFile()
importsOffset := encodeNodeIndexArray(sf.Imports(), nodeIndexMap, &structuredData)
moduleAugmentationsOffset := encodeModuleAugmentations(sf.ModuleAugmentations, nodeIndexMap, &structuredData)
ambientModuleNamesOffset := encodeStringArray(sf.AmbientModuleNames, &structuredData)
// Patch the 3 placeholder uint32s at sfExtendedDataOffset + 32, 36, 40
binary.LittleEndian.PutUint32(extendedData[sfExtendedDataOffset+32:], importsOffset)
binary.LittleEndian.PutUint32(extendedData[sfExtendedDataOffset+36:], moduleAugmentationsOffset)
binary.LittleEndian.PutUint32(extendedData[sfExtendedDataOffset+40:], ambientModuleNamesOffset)
// Patch externalModuleIndicator node index at offset 44
var externalModuleIndicatorIndex uint32
if sf.ExternalModuleIndicator != nil {
if sf.ExternalModuleIndicator == rootNode {
externalModuleIndicatorIndex = 1 // root node index
} else {
externalModuleIndicatorIndex = nodeIndexMap[sf.ExternalModuleIndicator]
}
}
binary.LittleEndian.PutUint32(extendedData[sfExtendedDataOffset+44:], externalModuleIndicatorIndex)
}
metadata := uint32(ProtocolVersion) << 24
offsetStringTableOffsets := HeaderSize
offsetStringTableData := HeaderSize + len(strs.offsets)*4
offsetExtendedData := offsetStringTableData + strs.stringLength()
offsetStructuredData := offsetExtendedData + len(extendedData)
offsetNodes := offsetStructuredData + len(structuredData)
header := []uint32{
metadata,
uint32(hash.Lo), uint32(hash.Lo >> 32),
uint32(hash.Hi), uint32(hash.Hi >> 32),
parseOpts,
uint32(offsetStringTableOffsets),
uint32(offsetStringTableData),
uint32(offsetExtendedData),
uint32(offsetStructuredData),
uint32(offsetNodes),
}
var headerBytes, strsBytes []byte
headerBytes = appendUint32s(nil, header...)
strsBytes = strs.encode()
return slices.Concat(
headerBytes,
strsBytes,
extendedData,
structuredData,
nodes,
), &NodeIndexTable{Nodes: nodeTable}, nil
}
func appendUint32s(buf []byte, values ...uint32) []byte {
for _, value := range values {
buf = binary.LittleEndian.AppendUint32(buf, value)
}
return buf
}
func getNodeData(node *ast.Node, strs *stringTable, positionMap *ast.PositionMap, extendedData *[]byte, structuredData *[]byte) uint32 {
t := getNodeDataType(node)
switch t {
case NodeDataTypeChildren:
return t | getNodeCommonData(node) | uint32(getChildrenPropertyMask(node))
case NodeDataTypeString:
return t | getNodeCommonData(node) | recordNodeStrings(node, strs)
case NodeDataTypeExtendedData:
return t | getNodeCommonData(node) | recordExtendedData(node, strs, positionMap, extendedData, structuredData)
default:
panic("unreachable")
}
}
const noStructuredData = 0xFFFFFFFF
func recordExtendedData_SourceFile(node *ast.Node, strs *stringTable, positionMap *ast.PositionMap, extendedData *[]byte, structuredData *[]byte) {
sf := node.AsSourceFile()
textIndex := strs.add(sf.Text(), sf.Kind, sf.Pos(), sf.End())
fileNameIndex := strs.add(sf.FileName(), 0, 0, 0)
pathIndex := strs.add(string(sf.Path()), 0, 0, 0)
referencedFilesOffset := encodeFileReferences(sf.ReferencedFiles, positionMap, structuredData)
typeRefDirectivesOffset := encodeFileReferences(sf.TypeReferenceDirectives, positionMap, structuredData)
libRefDirectivesOffset := encodeFileReferences(sf.LibReferenceDirectives, positionMap, structuredData)
// imports, moduleAugmentations, ambientModuleNames offsets are placeholders;
// they will be patched after the tree walk when node indices are known.
*extendedData = appendUint32s(*extendedData, textIndex, fileNameIndex, pathIndex, uint32(sf.LanguageVariant), uint32(sf.ScriptKind), referencedFilesOffset, typeRefDirectivesOffset, libRefDirectivesOffset, noStructuredData, noStructuredData, noStructuredData, 0)
}
func recordExtendedData_TemplateHead(node *ast.Node, strs *stringTable, positionMap *ast.PositionMap, extendedData *[]byte, structuredData *[]byte) {
n := node.AsTemplateHead()
textIndex := strs.add(n.Text, node.Kind, node.Pos(), node.End())
rawTextIndex := strs.add(n.RawText, node.Kind, node.Pos(), node.End())
*extendedData = appendUint32s(*extendedData, textIndex, rawTextIndex, uint32(n.TemplateFlags))
}
func recordExtendedData_TemplateMiddle(node *ast.Node, strs *stringTable, positionMap *ast.PositionMap, extendedData *[]byte, structuredData *[]byte) {
n := node.AsTemplateMiddle()
textIndex := strs.add(n.Text, node.Kind, node.Pos(), node.End())
rawTextIndex := strs.add(n.RawText, node.Kind, node.Pos(), node.End())
*extendedData = appendUint32s(*extendedData, textIndex, rawTextIndex, uint32(n.TemplateFlags))
}
func recordExtendedData_TemplateTail(node *ast.Node, strs *stringTable, positionMap *ast.PositionMap, extendedData *[]byte, structuredData *[]byte) {
n := node.AsTemplateTail()
textIndex := strs.add(n.Text, node.Kind, node.Pos(), node.End())
rawTextIndex := strs.add(n.RawText, node.Kind, node.Pos(), node.End())
*extendedData = appendUint32s(*extendedData, textIndex, rawTextIndex, uint32(n.TemplateFlags))
}
func boolToByte(b bool) byte {
if b {
return 1
}
return 0
}
// hasModifiers returns true if the modifier list is non-nil and has at least one modifier.
func hasModifiers(modifiers *ast.ModifierList) bool {
return modifiers != nil && len(modifiers.Nodes) > 0
}
// encodeFileReferences encodes a slice of FileReferences as a msgpack array of tuples
// into the structured data buffer. Returns the byte offset into the buffer, or
// noStructuredData (0xFFFFFFFF) if the slice is empty.
func encodeFileReferences(refs []*ast.FileReference, positionMap *ast.PositionMap, buf *[]byte) uint32 {
if len(refs) == 0 {
return noStructuredData
}
offset := uint32(len(*buf))
*buf = msgpackWriteArrayHeader(*buf, len(refs))
for _, ref := range refs {
// Each entry is a 5-element tuple: [pos, end, fileName, resolutionMode, preserve]
*buf = msgpackWriteArrayHeader(*buf, 5)
*buf = msgpackWriteUint(*buf, uint32(positionMap.UTF8ToUTF16(ref.Pos())))
*buf = msgpackWriteUint(*buf, uint32(positionMap.UTF8ToUTF16(ref.End())))
*buf = msgpackWriteString(*buf, ref.FileName)
*buf = msgpackWriteUint(*buf, uint32(ref.ResolutionMode))
*buf = msgpackWriteBool(*buf, ref.Preserve)
}
return offset
}
// encodeNodeIndexArray encodes a slice of LiteralLikeNodes as a msgpack array of
// uint node indices. Returns the byte offset into the buffer, or noStructuredData
// if the slice is empty.
func encodeNodeIndexArray(nodes []*ast.LiteralLikeNode, indexMap map[*ast.Node]uint32, buf *[]byte) uint32 {
if len(nodes) == 0 {
return noStructuredData
}
offset := uint32(len(*buf))
*buf = msgpackWriteArrayHeader(*buf, len(nodes))
for _, node := range nodes {
*buf = msgpackWriteUint(*buf, indexMap[node.AsNode()])
}
return offset
}
// encodeModuleAugmentations encodes a slice of ModuleName nodes as a msgpack array
// of uint node indices. Returns the byte offset into the buffer, or noStructuredData
// if the slice is empty.
func encodeModuleAugmentations(nodes []*ast.ModuleName, indexMap map[*ast.Node]uint32, buf *[]byte) uint32 {
if len(nodes) == 0 {
return noStructuredData
}
offset := uint32(len(*buf))
*buf = msgpackWriteArrayHeader(*buf, len(nodes))
for _, node := range nodes {
*buf = msgpackWriteUint(*buf, indexMap[node.AsNode()])
}
return offset
}
// encodeStringArray encodes a slice of strings as a msgpack array of strings.
// Returns the byte offset into the buffer, or noStructuredData if the slice is empty.
func encodeStringArray(strs []string, buf *[]byte) uint32 {
if len(strs) == 0 {
return noStructuredData
}
offset := uint32(len(*buf))
*buf = msgpackWriteArrayHeader(*buf, len(strs))
for _, s := range strs {
*buf = msgpackWriteString(*buf, s)
}
return offset
}
// Minimal msgpack writers for the structured data section.
func msgpackWriteArrayHeader(buf []byte, length int) []byte {
if length <= 0x0f {
return append(buf, byte(0x90|length))
}
if length <= 0xffff {
return append(buf, 0xdc, byte(length>>8), byte(length))
}
return append(buf, 0xdd, byte(length>>24), byte(length>>16), byte(length>>8), byte(length))
}
func msgpackWriteUint(buf []byte, value uint32) []byte {
if value <= 0x7f {
return append(buf, byte(value))
}
if value <= 0xff {
return append(buf, 0xcc, byte(value))
}
if value <= 0xffff {
return append(buf, 0xcd, byte(value>>8), byte(value))
}
return append(buf, 0xce, byte(value>>24), byte(value>>16), byte(value>>8), byte(value))
}
func msgpackWriteString(buf []byte, s string) []byte {
n := len(s)
if n <= 0x1f {
buf = append(buf, byte(0xa0|n))
} else if n <= 0xff {
buf = append(buf, 0xd9, byte(n))
} else if n <= 0xffff {
buf = append(buf, 0xda, byte(n>>8), byte(n))
} else {
buf = append(buf, 0xdb, byte(n>>24), byte(n>>16), byte(n>>8), byte(n))
}
return append(buf, s...)
}
func msgpackWriteBool(buf []byte, value bool) []byte {
if value {
return append(buf, 0xc3)
}
return append(buf, 0xc2)
}
// Hand-written commonData encoding functions for nodes whose non-bool data
// members cannot be automatically encoded by the generator. Each function
// packs relevant fields into the 6-bit commonData area (bits 24-29) of the
// 32-bit node data word.
func getNodeCommonData_SyntheticExpression(_ *ast.Node) uint32 {
// SyntheticExpression is an internal compiler node that is never part of a parsed AST.
// It should never be encoded.
panic("SyntheticExpression should never be encoded")
}
// Hand-written extended data encoding functions for literal nodes that were
// previously string-type but whose TokenFlags/TemplateFlags cannot fit in 6 bits.
func recordExtendedData_StringLiteral(node *ast.Node, strs *stringTable, _ *ast.PositionMap, extendedData *[]byte, _ *[]byte) {
n := node.AsStringLiteral()
textIndex := strs.add(n.Text, node.Kind, node.Pos(), node.End())
*extendedData = appendUint32s(*extendedData, textIndex, uint32(n.TokenFlags))
}
func recordExtendedData_NumericLiteral(node *ast.Node, strs *stringTable, _ *ast.PositionMap, extendedData *[]byte, _ *[]byte) {
n := node.AsNumericLiteral()
textIndex := strs.add(n.Text, node.Kind, node.Pos(), node.End())
*extendedData = appendUint32s(*extendedData, textIndex, uint32(n.TokenFlags))
}
func recordExtendedData_BigIntLiteral(node *ast.Node, strs *stringTable, _ *ast.PositionMap, extendedData *[]byte, _ *[]byte) {
n := node.AsBigIntLiteral()
textIndex := strs.add(n.Text, node.Kind, node.Pos(), node.End())
*extendedData = appendUint32s(*extendedData, textIndex, uint32(n.TokenFlags))
}
func recordExtendedData_RegularExpressionLiteral(node *ast.Node, strs *stringTable, _ *ast.PositionMap, extendedData *[]byte, _ *[]byte) {
n := node.AsRegularExpressionLiteral()
textIndex := strs.add(n.Text, node.Kind, node.Pos(), node.End())
*extendedData = appendUint32s(*extendedData, textIndex, uint32(n.TokenFlags))
}
func recordExtendedData_NoSubstitutionTemplateLiteral(node *ast.Node, strs *stringTable, _ *ast.PositionMap, extendedData *[]byte, _ *[]byte) {
n := node.AsNoSubstitutionTemplateLiteral()
textIndex := strs.add(n.Text, node.Kind, node.Pos(), node.End())
*extendedData = appendUint32s(*extendedData, textIndex, uint32(n.TemplateFlags))
}

View File

@@ -0,0 +1,707 @@
// Code generated by _scripts/generate-encoder.ts. DO NOT EDIT.
package encoder
import (
"fmt"
"github.com/microsoft/typescript-go/internal/ast"
)
func getNodeDataType(node *ast.Node) uint32 {
switch node.Kind {
case ast.KindIdentifier,
ast.KindPrivateIdentifier,
ast.KindJsxText,
ast.KindJSDocText,
ast.KindJSDocLink,
ast.KindJSDocLinkPlain,
ast.KindJSDocLinkCode:
return NodeDataTypeString
case ast.KindStringLiteral,
ast.KindNumericLiteral,
ast.KindBigIntLiteral,
ast.KindRegularExpressionLiteral,
ast.KindNoSubstitutionTemplateLiteral,
ast.KindTemplateHead,
ast.KindTemplateMiddle,
ast.KindTemplateTail,
ast.KindSourceFile:
return NodeDataTypeExtendedData
default:
return NodeDataTypeChildren
}
}
func getChildrenPropertyMask(node *ast.Node) uint8 {
switch node.Kind {
case ast.KindQualifiedName:
n := node.AsQualifiedName()
return (boolToByte(n.Left != nil) << 0) | (boolToByte(n.Right != nil) << 1)
case ast.KindComputedPropertyName:
n := node.AsComputedPropertyName()
return (boolToByte(n.Expression != nil) << 0)
case ast.KindDecorator:
n := node.AsDecorator()
return (boolToByte(n.Expression != nil) << 0)
case ast.KindIfStatement:
n := node.AsIfStatement()
return (boolToByte(n.Expression != nil) << 0) | (boolToByte(n.ThenStatement != nil) << 1) | (boolToByte(n.ElseStatement != nil) << 2)
case ast.KindDoStatement:
n := node.AsDoStatement()
return (boolToByte(n.Statement != nil) << 0) | (boolToByte(n.Expression != nil) << 1)
case ast.KindWhileStatement:
n := node.AsWhileStatement()
return (boolToByte(n.Expression != nil) << 0) | (boolToByte(n.Statement != nil) << 1)
case ast.KindForStatement:
n := node.AsForStatement()
return (boolToByte(n.Initializer != nil) << 0) | (boolToByte(n.Condition != nil) << 1) | (boolToByte(n.Incrementor != nil) << 2) | (boolToByte(n.Statement != nil) << 3)
case ast.KindForInStatement, ast.KindForOfStatement:
n := node.AsForInOrOfStatement()
return (boolToByte(n.AwaitModifier != nil) << 0) | (boolToByte(n.Initializer != nil) << 1) | (boolToByte(n.Expression != nil) << 2) | (boolToByte(n.Statement != nil) << 3)
case ast.KindBreakStatement:
n := node.AsBreakStatement()
return (boolToByte(n.Label != nil) << 0)
case ast.KindContinueStatement:
n := node.AsContinueStatement()
return (boolToByte(n.Label != nil) << 0)
case ast.KindReturnStatement:
n := node.AsReturnStatement()
return (boolToByte(n.Expression != nil) << 0)
case ast.KindWithStatement:
n := node.AsWithStatement()
return (boolToByte(n.Expression != nil) << 0) | (boolToByte(n.Statement != nil) << 1)
case ast.KindSwitchStatement:
n := node.AsSwitchStatement()
return (boolToByte(n.Expression != nil) << 0) | (boolToByte(n.CaseBlock != nil) << 1)
case ast.KindCaseBlock:
n := node.AsCaseBlock()
return (boolToByte(n.Clauses != nil) << 0)
case ast.KindCaseClause, ast.KindDefaultClause:
n := node.AsCaseOrDefaultClause()
return (boolToByte(n.Expression != nil) << 0) | (boolToByte(n.Statements != nil) << 1)
case ast.KindThrowStatement:
n := node.AsThrowStatement()
return (boolToByte(n.Expression != nil) << 0)
case ast.KindTryStatement:
n := node.AsTryStatement()
return (boolToByte(n.TryBlock != nil) << 0) | (boolToByte(n.CatchClause != nil) << 1) | (boolToByte(n.FinallyBlock != nil) << 2)
case ast.KindCatchClause:
n := node.AsCatchClause()
return (boolToByte(n.VariableDeclaration != nil) << 0) | (boolToByte(n.Block != nil) << 1)
case ast.KindLabeledStatement:
n := node.AsLabeledStatement()
return (boolToByte(n.Label != nil) << 0) | (boolToByte(n.Statement != nil) << 1)
case ast.KindExpressionStatement:
n := node.AsExpressionStatement()
return (boolToByte(n.Expression != nil) << 0)
case ast.KindBlock:
n := node.AsBlock()
return (boolToByte(n.Statements != nil) << 0)
case ast.KindVariableStatement:
n := node.AsVariableStatement()
return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.DeclarationList != nil) << 1)
case ast.KindVariableDeclaration:
n := node.AsVariableDeclaration()
return (boolToByte(n.Name() != nil) << 0) | (boolToByte(n.ExclamationToken != nil) << 1) | (boolToByte(n.Type != nil) << 2) | (boolToByte(n.Initializer != nil) << 3)
case ast.KindVariableDeclarationList:
n := node.AsVariableDeclarationList()
return (boolToByte(n.Declarations != nil) << 0)
case ast.KindObjectBindingPattern, ast.KindArrayBindingPattern:
n := node.AsBindingPattern()
return (boolToByte(n.Elements != nil) << 0)
case ast.KindParameter:
n := node.AsParameterDeclaration()
return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.DotDotDotToken != nil) << 1) | (boolToByte(n.Name() != nil) << 2) | (boolToByte(n.QuestionToken != nil) << 3) | (boolToByte(n.Type != nil) << 4) | (boolToByte(n.Initializer != nil) << 5)
case ast.KindBindingElement:
n := node.AsBindingElement()
return (boolToByte(n.DotDotDotToken != nil) << 0) | (boolToByte(n.PropertyName != nil) << 1) | (boolToByte(n.Name() != nil) << 2) | (boolToByte(n.Initializer != nil) << 3)
case ast.KindMissingDeclaration:
n := node.AsMissingDeclaration()
return (boolToByte(hasModifiers(n.Modifiers())) << 0)
case ast.KindFunctionDeclaration:
n := node.AsFunctionDeclaration()
return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.AsteriskToken != nil) << 1) | (boolToByte(n.Name() != nil) << 2) | (boolToByte(n.TypeParameters != nil) << 3) | (boolToByte(n.Parameters != nil) << 4) | (boolToByte(n.Type != nil) << 5) | (boolToByte(n.Body != nil) << 6)
case ast.KindClassDeclaration:
n := node.AsClassDeclaration()
return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.Name() != nil) << 1) | (boolToByte(n.TypeParameters != nil) << 2) | (boolToByte(n.HeritageClauses != nil) << 3) | (boolToByte(n.Members != nil) << 4)
case ast.KindClassExpression:
n := node.AsClassExpression()
return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.Name() != nil) << 1) | (boolToByte(n.TypeParameters != nil) << 2) | (boolToByte(n.HeritageClauses != nil) << 3) | (boolToByte(n.Members != nil) << 4)
case ast.KindHeritageClause:
n := node.AsHeritageClause()
return (boolToByte(n.Types != nil) << 0)
case ast.KindInterfaceDeclaration:
n := node.AsInterfaceDeclaration()
return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.Name() != nil) << 1) | (boolToByte(n.TypeParameters != nil) << 2) | (boolToByte(n.HeritageClauses != nil) << 3) | (boolToByte(n.Members != nil) << 4)
case ast.KindTypeAliasDeclaration, ast.KindJSTypeAliasDeclaration:
n := node.AsTypeAliasDeclaration()
return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.Name() != nil) << 1) | (boolToByte(n.TypeParameters != nil) << 2) | (boolToByte(n.Type != nil) << 3)
case ast.KindEnumMember:
n := node.AsEnumMember()
return (boolToByte(n.Name() != nil) << 0) | (boolToByte(n.Initializer != nil) << 1)
case ast.KindEnumDeclaration:
n := node.AsEnumDeclaration()
return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.Name() != nil) << 1) | (boolToByte(n.Members != nil) << 2)
case ast.KindModuleBlock:
n := node.AsModuleBlock()
return (boolToByte(n.Statements != nil) << 0)
case ast.KindImportDeclaration, ast.KindJSImportDeclaration:
n := node.AsImportDeclaration()
return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.ImportClause != nil) << 1) | (boolToByte(n.ModuleSpecifier != nil) << 2) | (boolToByte(n.Attributes != nil) << 3)
case ast.KindExternalModuleReference:
n := node.AsExternalModuleReference()
return (boolToByte(n.Expression != nil) << 0)
case ast.KindNamespaceImport:
n := node.AsNamespaceImport()
return (boolToByte(n.Name() != nil) << 0)
case ast.KindNamedImports:
n := node.AsNamedImports()
return (boolToByte(n.Elements != nil) << 0)
case ast.KindExportAssignment:
n := node.AsExportAssignment()
return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.Type != nil) << 1) | (boolToByte(n.Expression != nil) << 2)
case ast.KindNamespaceExportDeclaration:
n := node.AsNamespaceExportDeclaration()
return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.Name() != nil) << 1)
case ast.KindNamespaceExport:
n := node.AsNamespaceExport()
return (boolToByte(n.Name() != nil) << 0)
case ast.KindNamedExports:
n := node.AsNamedExports()
return (boolToByte(n.Elements != nil) << 0)
case ast.KindExportSpecifier:
n := node.AsExportSpecifier()
return (boolToByte(n.PropertyName != nil) << 0) | (boolToByte(n.Name() != nil) << 1)
case ast.KindCallSignature:
n := node.AsCallSignatureDeclaration()
return (boolToByte(n.TypeParameters != nil) << 0) | (boolToByte(n.Parameters != nil) << 1) | (boolToByte(n.Type != nil) << 2)
case ast.KindConstructSignature:
n := node.AsConstructSignatureDeclaration()
return (boolToByte(n.TypeParameters != nil) << 0) | (boolToByte(n.Parameters != nil) << 1) | (boolToByte(n.Type != nil) << 2)
case ast.KindConstructor:
n := node.AsConstructorDeclaration()
return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.TypeParameters != nil) << 1) | (boolToByte(n.Parameters != nil) << 2) | (boolToByte(n.Type != nil) << 3) | (boolToByte(n.Body != nil) << 4)
case ast.KindGetAccessor:
n := node.AsGetAccessorDeclaration()
return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.Name() != nil) << 1) | (boolToByte(n.TypeParameters != nil) << 2) | (boolToByte(n.Parameters != nil) << 3) | (boolToByte(n.Type != nil) << 4) | (boolToByte(n.Body != nil) << 5)
case ast.KindSetAccessor:
n := node.AsSetAccessorDeclaration()
return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.Name() != nil) << 1) | (boolToByte(n.TypeParameters != nil) << 2) | (boolToByte(n.Parameters != nil) << 3) | (boolToByte(n.Type != nil) << 4) | (boolToByte(n.Body != nil) << 5)
case ast.KindIndexSignature:
n := node.AsIndexSignatureDeclaration()
return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.Parameters != nil) << 1) | (boolToByte(n.Type != nil) << 2)
case ast.KindMethodSignature:
n := node.AsMethodSignatureDeclaration()
return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.Name() != nil) << 1) | (boolToByte(n.PostfixToken != nil) << 2) | (boolToByte(n.TypeParameters != nil) << 3) | (boolToByte(n.Parameters != nil) << 4) | (boolToByte(n.Type != nil) << 5)
case ast.KindMethodDeclaration:
n := node.AsMethodDeclaration()
return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.AsteriskToken != nil) << 1) | (boolToByte(n.Name() != nil) << 2) | (boolToByte(n.PostfixToken != nil) << 3) | (boolToByte(n.TypeParameters != nil) << 4) | (boolToByte(n.Parameters != nil) << 5) | (boolToByte(n.Type != nil) << 6) | (boolToByte(n.Body != nil) << 7)
case ast.KindPropertySignature:
n := node.AsPropertySignatureDeclaration()
return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.Name() != nil) << 1) | (boolToByte(n.PostfixToken != nil) << 2) | (boolToByte(n.Type != nil) << 3) | (boolToByte(n.Initializer != nil) << 4)
case ast.KindPropertyDeclaration:
n := node.AsPropertyDeclaration()
return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.Name() != nil) << 1) | (boolToByte(n.PostfixToken != nil) << 2) | (boolToByte(n.Type != nil) << 3) | (boolToByte(n.Initializer != nil) << 4)
case ast.KindClassStaticBlockDeclaration:
n := node.AsClassStaticBlockDeclaration()
return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.Body != nil) << 1)
case ast.KindBinaryExpression:
n := node.AsBinaryExpression()
return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.Left != nil) << 1) | (boolToByte(n.Type != nil) << 2) | (boolToByte(n.OperatorToken != nil) << 3) | (boolToByte(n.Right != nil) << 4)
case ast.KindPrefixUnaryExpression:
n := node.AsPrefixUnaryExpression()
return (boolToByte(n.Operand != nil) << 0)
case ast.KindPostfixUnaryExpression:
n := node.AsPostfixUnaryExpression()
return (boolToByte(n.Operand != nil) << 0)
case ast.KindYieldExpression:
n := node.AsYieldExpression()
return (boolToByte(n.AsteriskToken != nil) << 0) | (boolToByte(n.Expression != nil) << 1)
case ast.KindArrowFunction:
n := node.AsArrowFunction()
return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.TypeParameters != nil) << 1) | (boolToByte(n.Parameters != nil) << 2) | (boolToByte(n.Type != nil) << 3) | (boolToByte(n.EqualsGreaterThanToken != nil) << 4) | (boolToByte(n.Body != nil) << 5)
case ast.KindFunctionExpression:
n := node.AsFunctionExpression()
return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.AsteriskToken != nil) << 1) | (boolToByte(n.Name() != nil) << 2) | (boolToByte(n.TypeParameters != nil) << 3) | (boolToByte(n.Parameters != nil) << 4) | (boolToByte(n.Type != nil) << 5) | (boolToByte(n.Body != nil) << 6)
case ast.KindAsExpression:
n := node.AsAsExpression()
return (boolToByte(n.Expression != nil) << 0) | (boolToByte(n.Type != nil) << 1)
case ast.KindSatisfiesExpression:
n := node.AsSatisfiesExpression()
return (boolToByte(n.Expression != nil) << 0) | (boolToByte(n.Type != nil) << 1)
case ast.KindConditionalExpression:
n := node.AsConditionalExpression()
return (boolToByte(n.Condition != nil) << 0) | (boolToByte(n.QuestionToken != nil) << 1) | (boolToByte(n.WhenTrue != nil) << 2) | (boolToByte(n.ColonToken != nil) << 3) | (boolToByte(n.WhenFalse != nil) << 4)
case ast.KindPropertyAccessExpression:
n := node.AsPropertyAccessExpression()
return (boolToByte(n.Expression != nil) << 0) | (boolToByte(n.QuestionDotToken != nil) << 1) | (boolToByte(n.Name() != nil) << 2)
case ast.KindElementAccessExpression:
n := node.AsElementAccessExpression()
return (boolToByte(n.Expression != nil) << 0) | (boolToByte(n.QuestionDotToken != nil) << 1) | (boolToByte(n.ArgumentExpression != nil) << 2)
case ast.KindCallExpression:
n := node.AsCallExpression()
return (boolToByte(n.Expression != nil) << 0) | (boolToByte(n.QuestionDotToken != nil) << 1) | (boolToByte(n.TypeArguments != nil) << 2) | (boolToByte(n.Arguments != nil) << 3)
case ast.KindNewExpression:
n := node.AsNewExpression()
return (boolToByte(n.Expression != nil) << 0) | (boolToByte(n.TypeArguments != nil) << 1) | (boolToByte(n.Arguments != nil) << 2)
case ast.KindMetaProperty:
n := node.AsMetaProperty()
return (boolToByte(n.Name() != nil) << 0)
case ast.KindNonNullExpression:
n := node.AsNonNullExpression()
return (boolToByte(n.Expression != nil) << 0)
case ast.KindSpreadElement:
n := node.AsSpreadElement()
return (boolToByte(n.Expression != nil) << 0)
case ast.KindTemplateExpression:
n := node.AsTemplateExpression()
return (boolToByte(n.Head != nil) << 0) | (boolToByte(n.TemplateSpans != nil) << 1)
case ast.KindTemplateSpan:
n := node.AsTemplateSpan()
return (boolToByte(n.Expression != nil) << 0) | (boolToByte(n.Literal != nil) << 1)
case ast.KindTaggedTemplateExpression:
n := node.AsTaggedTemplateExpression()
return (boolToByte(n.Tag != nil) << 0) | (boolToByte(n.QuestionDotToken != nil) << 1) | (boolToByte(n.TypeArguments != nil) << 2) | (boolToByte(n.Template != nil) << 3)
case ast.KindParenthesizedExpression:
n := node.AsParenthesizedExpression()
return (boolToByte(n.Expression != nil) << 0)
case ast.KindArrayLiteralExpression:
n := node.AsArrayLiteralExpression()
return (boolToByte(n.Elements != nil) << 0)
case ast.KindObjectLiteralExpression:
n := node.AsObjectLiteralExpression()
return (boolToByte(n.Properties != nil) << 0)
case ast.KindSpreadAssignment:
n := node.AsSpreadAssignment()
return (boolToByte(n.Expression != nil) << 0)
case ast.KindPropertyAssignment:
n := node.AsPropertyAssignment()
return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.Name() != nil) << 1) | (boolToByte(n.PostfixToken != nil) << 2) | (boolToByte(n.Type != nil) << 3) | (boolToByte(n.Initializer != nil) << 4)
case ast.KindShorthandPropertyAssignment:
n := node.AsShorthandPropertyAssignment()
return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.Name() != nil) << 1) | (boolToByte(n.PostfixToken != nil) << 2) | (boolToByte(n.Type != nil) << 3) | (boolToByte(n.EqualsToken != nil) << 4) | (boolToByte(n.ObjectAssignmentInitializer != nil) << 5)
case ast.KindDeleteExpression:
n := node.AsDeleteExpression()
return (boolToByte(n.Expression != nil) << 0)
case ast.KindTypeOfExpression:
n := node.AsTypeOfExpression()
return (boolToByte(n.Expression != nil) << 0)
case ast.KindVoidExpression:
n := node.AsVoidExpression()
return (boolToByte(n.Expression != nil) << 0)
case ast.KindAwaitExpression:
n := node.AsAwaitExpression()
return (boolToByte(n.Expression != nil) << 0)
case ast.KindTypeAssertionExpression:
n := node.AsTypeAssertion()
return (boolToByte(n.Type != nil) << 0) | (boolToByte(n.Expression != nil) << 1)
case ast.KindUnionType:
n := node.AsUnionTypeNode()
return (boolToByte(n.Types != nil) << 0)
case ast.KindIntersectionType:
n := node.AsIntersectionTypeNode()
return (boolToByte(n.Types != nil) << 0)
case ast.KindConditionalType:
n := node.AsConditionalTypeNode()
return (boolToByte(n.CheckType != nil) << 0) | (boolToByte(n.ExtendsType != nil) << 1) | (boolToByte(n.TrueType != nil) << 2) | (boolToByte(n.FalseType != nil) << 3)
case ast.KindTypeOperator:
n := node.AsTypeOperatorNode()
return (boolToByte(n.Type != nil) << 0)
case ast.KindInferType:
n := node.AsInferTypeNode()
return (boolToByte(n.TypeParameter != nil) << 0)
case ast.KindArrayType:
n := node.AsArrayTypeNode()
return (boolToByte(n.ElementType != nil) << 0)
case ast.KindIndexedAccessType:
n := node.AsIndexedAccessTypeNode()
return (boolToByte(n.ObjectType != nil) << 0) | (boolToByte(n.IndexType != nil) << 1)
case ast.KindTypeReference:
n := node.AsTypeReferenceNode()
return (boolToByte(n.TypeName != nil) << 0) | (boolToByte(n.TypeArguments != nil) << 1)
case ast.KindExpressionWithTypeArguments:
n := node.AsExpressionWithTypeArguments()
return (boolToByte(n.Expression != nil) << 0) | (boolToByte(n.TypeArguments != nil) << 1)
case ast.KindLiteralType:
n := node.AsLiteralTypeNode()
return (boolToByte(n.Literal != nil) << 0)
case ast.KindTypePredicate:
n := node.AsTypePredicateNode()
return (boolToByte(n.AssertsModifier != nil) << 0) | (boolToByte(n.ParameterName != nil) << 1) | (boolToByte(n.Type != nil) << 2)
case ast.KindImportAttribute:
n := node.AsImportAttribute()
return (boolToByte(n.Name() != nil) << 0) | (boolToByte(n.Value != nil) << 1)
case ast.KindImportAttributes:
n := node.AsImportAttributes()
return (boolToByte(n.Attributes != nil) << 0)
case ast.KindTypeQuery:
n := node.AsTypeQueryNode()
return (boolToByte(n.ExprName != nil) << 0) | (boolToByte(n.TypeArguments != nil) << 1)
case ast.KindMappedType:
n := node.AsMappedTypeNode()
return (boolToByte(n.ReadonlyToken != nil) << 0) | (boolToByte(n.TypeParameter != nil) << 1) | (boolToByte(n.NameType != nil) << 2) | (boolToByte(n.QuestionToken != nil) << 3) | (boolToByte(n.Type != nil) << 4) | (boolToByte(n.Members != nil) << 5)
case ast.KindTypeLiteral:
n := node.AsTypeLiteralNode()
return (boolToByte(n.Members != nil) << 0)
case ast.KindTupleType:
n := node.AsTupleTypeNode()
return (boolToByte(n.Elements != nil) << 0)
case ast.KindNamedTupleMember:
n := node.AsNamedTupleMember()
return (boolToByte(n.DotDotDotToken != nil) << 0) | (boolToByte(n.Name() != nil) << 1) | (boolToByte(n.QuestionToken != nil) << 2) | (boolToByte(n.Type != nil) << 3)
case ast.KindOptionalType:
n := node.AsOptionalTypeNode()
return (boolToByte(n.Type != nil) << 0)
case ast.KindRestType:
n := node.AsRestTypeNode()
return (boolToByte(n.Type != nil) << 0)
case ast.KindParenthesizedType:
n := node.AsParenthesizedTypeNode()
return (boolToByte(n.Type != nil) << 0)
case ast.KindFunctionType:
n := node.AsFunctionTypeNode()
return (boolToByte(n.TypeParameters != nil) << 0) | (boolToByte(n.Parameters != nil) << 1) | (boolToByte(n.Type != nil) << 2)
case ast.KindConstructorType:
n := node.AsConstructorTypeNode()
return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.TypeParameters != nil) << 1) | (boolToByte(n.Parameters != nil) << 2) | (boolToByte(n.Type != nil) << 3)
case ast.KindTemplateLiteralType:
n := node.AsTemplateLiteralTypeNode()
return (boolToByte(n.Head != nil) << 0) | (boolToByte(n.TemplateSpans != nil) << 1)
case ast.KindTemplateLiteralTypeSpan:
n := node.AsTemplateLiteralTypeSpan()
return (boolToByte(n.Type != nil) << 0) | (boolToByte(n.Literal != nil) << 1)
case ast.KindSyntheticExpression:
n := node.AsSyntheticExpression()
return (boolToByte(n.TupleNameSource != nil) << 0)
case ast.KindPartiallyEmittedExpression:
n := node.AsPartiallyEmittedExpression()
return (boolToByte(n.Expression != nil) << 0)
case ast.KindJsxElement:
n := node.AsJsxElement()
return (boolToByte(n.OpeningElement != nil) << 0) | (boolToByte(n.Children != nil) << 1) | (boolToByte(n.ClosingElement != nil) << 2)
case ast.KindJsxAttributes:
n := node.AsJsxAttributes()
return (boolToByte(n.Properties != nil) << 0)
case ast.KindJsxNamespacedName:
n := node.AsJsxNamespacedName()
return (boolToByte(n.Namespace != nil) << 0) | (boolToByte(n.Name() != nil) << 1)
case ast.KindJsxOpeningElement:
n := node.AsJsxOpeningElement()
return (boolToByte(n.TagName != nil) << 0) | (boolToByte(n.TypeArguments != nil) << 1) | (boolToByte(n.Attributes != nil) << 2)
case ast.KindJsxSelfClosingElement:
n := node.AsJsxSelfClosingElement()
return (boolToByte(n.TagName != nil) << 0) | (boolToByte(n.TypeArguments != nil) << 1) | (boolToByte(n.Attributes != nil) << 2)
case ast.KindJsxFragment:
n := node.AsJsxFragment()
return (boolToByte(n.OpeningFragment != nil) << 0) | (boolToByte(n.Children != nil) << 1) | (boolToByte(n.ClosingFragment != nil) << 2)
case ast.KindJsxAttribute:
n := node.AsJsxAttribute()
return (boolToByte(n.Name() != nil) << 0) | (boolToByte(n.Initializer != nil) << 1)
case ast.KindJsxSpreadAttribute:
n := node.AsJsxSpreadAttribute()
return (boolToByte(n.Expression != nil) << 0)
case ast.KindJsxClosingElement:
n := node.AsJsxClosingElement()
return (boolToByte(n.TagName != nil) << 0)
case ast.KindJsxExpression:
n := node.AsJsxExpression()
return (boolToByte(n.DotDotDotToken != nil) << 0) | (boolToByte(n.Expression != nil) << 1)
case ast.KindSyntaxList:
n := node.AsSyntaxList()
return (boolToByte(len(n.Children) > 0) << 0)
case ast.KindJSDoc:
n := node.AsJSDoc()
return (boolToByte(n.Comment != nil) << 0) | (boolToByte(n.Tags != nil) << 1)
case ast.KindJSDocTypeExpression:
n := node.AsJSDocTypeExpression()
return (boolToByte(n.Type != nil) << 0)
case ast.KindJSDocNonNullableType:
n := node.AsJSDocNonNullableType()
return (boolToByte(n.Type != nil) << 0)
case ast.KindJSDocNullableType:
n := node.AsJSDocNullableType()
return (boolToByte(n.Type != nil) << 0)
case ast.KindJSDocVariadicType:
n := node.AsJSDocVariadicType()
return (boolToByte(n.Type != nil) << 0)
case ast.KindJSDocOptionalType:
n := node.AsJSDocOptionalType()
return (boolToByte(n.Type != nil) << 0)
case ast.KindJSDocTypeTag:
n := node.AsJSDocTypeTag()
return (boolToByte(n.TagName != nil) << 0) | (boolToByte(n.TypeExpression != nil) << 1) | (boolToByte(n.Comment != nil) << 2)
case ast.KindJSDocUnknownTag:
n := node.AsJSDocUnknownTag()
return (boolToByte(n.TagName != nil) << 0) | (boolToByte(n.Comment != nil) << 1)
case ast.KindJSDocTemplateTag:
n := node.AsJSDocTemplateTag()
return (boolToByte(n.TagName != nil) << 0) | (boolToByte(n.Constraint != nil) << 1) | (boolToByte(n.TypeParameters != nil) << 2) | (boolToByte(n.Comment != nil) << 3)
case ast.KindJSDocReturnTag:
n := node.AsJSDocReturnTag()
return (boolToByte(n.TagName != nil) << 0) | (boolToByte(n.TypeExpression != nil) << 1) | (boolToByte(n.Comment != nil) << 2)
case ast.KindJSDocPublicTag:
n := node.AsJSDocPublicTag()
return (boolToByte(n.TagName != nil) << 0) | (boolToByte(n.Comment != nil) << 1)
case ast.KindJSDocPrivateTag:
n := node.AsJSDocPrivateTag()
return (boolToByte(n.TagName != nil) << 0) | (boolToByte(n.Comment != nil) << 1)
case ast.KindJSDocProtectedTag:
n := node.AsJSDocProtectedTag()
return (boolToByte(n.TagName != nil) << 0) | (boolToByte(n.Comment != nil) << 1)
case ast.KindJSDocReadonlyTag:
n := node.AsJSDocReadonlyTag()
return (boolToByte(n.TagName != nil) << 0) | (boolToByte(n.Comment != nil) << 1)
case ast.KindJSDocOverrideTag:
n := node.AsJSDocOverrideTag()
return (boolToByte(n.TagName != nil) << 0) | (boolToByte(n.Comment != nil) << 1)
case ast.KindJSDocDeprecatedTag:
n := node.AsJSDocDeprecatedTag()
return (boolToByte(n.TagName != nil) << 0) | (boolToByte(n.Comment != nil) << 1)
case ast.KindJSDocSeeTag:
n := node.AsJSDocSeeTag()
return (boolToByte(n.TagName != nil) << 0) | (boolToByte(n.NameExpression != nil) << 1) | (boolToByte(n.Comment != nil) << 2)
case ast.KindJSDocImplementsTag:
n := node.AsJSDocImplementsTag()
return (boolToByte(n.TagName != nil) << 0) | (boolToByte(n.ClassName != nil) << 1) | (boolToByte(n.Comment != nil) << 2)
case ast.KindJSDocAugmentsTag:
n := node.AsJSDocAugmentsTag()
return (boolToByte(n.TagName != nil) << 0) | (boolToByte(n.ClassName != nil) << 1) | (boolToByte(n.Comment != nil) << 2)
case ast.KindJSDocSatisfiesTag:
n := node.AsJSDocSatisfiesTag()
return (boolToByte(n.TagName != nil) << 0) | (boolToByte(n.TypeExpression != nil) << 1) | (boolToByte(n.Comment != nil) << 2)
case ast.KindJSDocThrowsTag:
n := node.AsJSDocThrowsTag()
return (boolToByte(n.TagName != nil) << 0) | (boolToByte(n.TypeExpression != nil) << 1) | (boolToByte(n.Comment != nil) << 2)
case ast.KindJSDocThisTag:
n := node.AsJSDocThisTag()
return (boolToByte(n.TagName != nil) << 0) | (boolToByte(n.TypeExpression != nil) << 1) | (boolToByte(n.Comment != nil) << 2)
case ast.KindJSDocImportTag:
n := node.AsJSDocImportTag()
return (boolToByte(n.TagName != nil) << 0) | (boolToByte(n.ImportClause != nil) << 1) | (boolToByte(n.ModuleSpecifier != nil) << 2) | (boolToByte(n.Attributes != nil) << 3) | (boolToByte(n.Comment != nil) << 4)
case ast.KindJSDocCallbackTag:
n := node.AsJSDocCallbackTag()
return (boolToByte(n.TagName != nil) << 0) | (boolToByte(n.TypeExpression != nil) << 1) | (boolToByte(n.Name() != nil) << 2) | (boolToByte(n.Comment != nil) << 3)
case ast.KindJSDocOverloadTag:
n := node.AsJSDocOverloadTag()
return (boolToByte(n.TagName != nil) << 0) | (boolToByte(n.TypeExpression != nil) << 1) | (boolToByte(n.Comment != nil) << 2)
case ast.KindJSDocTypedefTag:
n := node.AsJSDocTypedefTag()
return (boolToByte(n.TagName != nil) << 0) | (boolToByte(n.TypeExpression != nil) << 1) | (boolToByte(n.Name() != nil) << 2) | (boolToByte(n.Comment != nil) << 3)
case ast.KindJSDocSignature:
n := node.AsJSDocSignature()
return (boolToByte(n.TypeParameters != nil) << 0) | (boolToByte(n.Parameters != nil) << 1) | (boolToByte(n.Type != nil) << 2)
case ast.KindJSDocNameReference:
n := node.AsJSDocNameReference()
return (boolToByte(n.Name() != nil) << 0)
case ast.KindModuleDeclaration:
n := node.AsModuleDeclaration()
return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.Name() != nil) << 1) | (boolToByte(n.Body != nil) << 2)
case ast.KindImportEqualsDeclaration:
n := node.AsImportEqualsDeclaration()
return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.Name() != nil) << 1) | (boolToByte(n.ModuleReference != nil) << 2)
case ast.KindExportDeclaration:
n := node.AsExportDeclaration()
return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.ExportClause != nil) << 1) | (boolToByte(n.ModuleSpecifier != nil) << 2) | (boolToByte(n.Attributes != nil) << 3)
case ast.KindImportType:
n := node.AsImportTypeNode()
return (boolToByte(n.Argument != nil) << 0) | (boolToByte(n.Attributes != nil) << 1) | (boolToByte(n.Qualifier != nil) << 2) | (boolToByte(n.TypeArguments != nil) << 3)
case ast.KindImportClause:
n := node.AsImportClause()
return (boolToByte(n.Name() != nil) << 0) | (boolToByte(n.NamedBindings != nil) << 1)
case ast.KindImportSpecifier:
n := node.AsImportSpecifier()
return (boolToByte(n.PropertyName != nil) << 0) | (boolToByte(n.Name() != nil) << 1)
case ast.KindJSDocLink:
n := node.AsJSDocLink()
return (boolToByte(n.Name() != nil) << 0)
case ast.KindJSDocLinkPlain:
n := node.AsJSDocLinkPlain()
return (boolToByte(n.Name() != nil) << 0)
case ast.KindJSDocLinkCode:
n := node.AsJSDocLinkCode()
return (boolToByte(n.Name() != nil) << 0)
case ast.KindTypeParameter:
n := node.AsTypeParameterDeclaration()
return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.Name() != nil) << 1) | (boolToByte(n.Constraint != nil) << 2) | (boolToByte(n.Expression != nil) << 3) | (boolToByte(n.DefaultType != nil) << 4)
case ast.KindSyntheticReferenceExpression:
n := node.AsSyntheticReferenceExpression()
return (boolToByte(n.Expression != nil) << 0) | (boolToByte(n.ThisArg != nil) << 1)
case ast.KindJSDocTypeLiteral:
n := node.AsJSDocTypeLiteral()
return (boolToByte(len(n.JSDocPropertyTags) > 0) << 0)
case ast.KindJSDocParameterTag, ast.KindJSDocPropertyTag:
n := node.AsJSDocParameterOrPropertyTag()
return (boolToByte(n.TagName != nil) << 0) | (boolToByte(n.Name() != nil) << 1) | (boolToByte(n.TypeExpression != nil) << 2) | (boolToByte(n.Comment != nil) << 3)
default:
return 0
}
}
func getNodeCommonData(node *ast.Node) uint32 {
switch node.Kind {
case ast.KindBlock:
n := node.AsBlock()
return uint32(boolToByte(n.MultiLine)) << 24
case ast.KindHeritageClause:
n := node.AsHeritageClause()
var tokenIdx uint32
switch n.Token {
case ast.KindImplementsKeyword:
tokenIdx = 1
}
return tokenIdx << 24
case ast.KindExportAssignment:
n := node.AsExportAssignment()
return uint32(boolToByte(n.IsExportEquals)) << 24
case ast.KindExportSpecifier:
n := node.AsExportSpecifier()
return uint32(boolToByte(n.IsTypeOnly)) << 24
case ast.KindPrefixUnaryExpression:
n := node.AsPrefixUnaryExpression()
var operatorIdx uint32
switch n.Operator {
case ast.KindMinusToken:
operatorIdx = 1
case ast.KindTildeToken:
operatorIdx = 2
case ast.KindExclamationToken:
operatorIdx = 3
case ast.KindPlusPlusToken:
operatorIdx = 4
case ast.KindMinusMinusToken:
operatorIdx = 5
}
return operatorIdx << 24
case ast.KindPostfixUnaryExpression:
n := node.AsPostfixUnaryExpression()
var operatorIdx uint32
switch n.Operator {
case ast.KindMinusMinusToken:
operatorIdx = 1
}
return operatorIdx << 24
case ast.KindMetaProperty:
n := node.AsMetaProperty()
var keywordTokenIdx uint32
switch n.KeywordToken {
case ast.KindNewKeyword:
keywordTokenIdx = 1
}
return keywordTokenIdx << 24
case ast.KindArrayLiteralExpression:
n := node.AsArrayLiteralExpression()
return uint32(boolToByte(n.MultiLine)) << 24
case ast.KindObjectLiteralExpression:
n := node.AsObjectLiteralExpression()
return uint32(boolToByte(n.MultiLine)) << 24
case ast.KindTypeOperator:
n := node.AsTypeOperatorNode()
var operatorIdx uint32
switch n.Operator {
case ast.KindReadonlyKeyword:
operatorIdx = 1
case ast.KindUniqueKeyword:
operatorIdx = 2
}
return operatorIdx << 24
case ast.KindImportAttributes:
n := node.AsImportAttributes()
var tokenIdx uint32
switch n.Token {
case ast.KindAssertKeyword:
tokenIdx = 1
}
return uint32(boolToByte(n.MultiLine))<<24 | tokenIdx<<25
case ast.KindSyntheticExpression:
return getNodeCommonData_SyntheticExpression(node)
case ast.KindJsxText:
n := node.AsJsxText()
return uint32(boolToByte(n.ContainsOnlyTriviaWhiteSpaces)) << 24
case ast.KindModuleDeclaration:
n := node.AsModuleDeclaration()
var keywordIdx uint32
switch n.Keyword {
case ast.KindNamespaceKeyword:
keywordIdx = 1
}
return keywordIdx << 24
case ast.KindImportEqualsDeclaration:
n := node.AsImportEqualsDeclaration()
return uint32(boolToByte(n.IsTypeOnly)) << 24
case ast.KindExportDeclaration:
n := node.AsExportDeclaration()
return uint32(boolToByte(n.IsTypeOnly)) << 24
case ast.KindImportType:
n := node.AsImportTypeNode()
return uint32(boolToByte(n.IsTypeOf)) << 24
case ast.KindImportClause:
n := node.AsImportClause()
var phaseModifierIdx uint32
switch n.PhaseModifier {
case ast.KindTypeKeyword:
phaseModifierIdx = 1
case ast.KindDeferKeyword:
phaseModifierIdx = 2
}
return phaseModifierIdx << 24
case ast.KindImportSpecifier:
n := node.AsImportSpecifier()
return uint32(boolToByte(n.IsTypeOnly)) << 24
case ast.KindJSDocTypeLiteral:
n := node.AsJSDocTypeLiteral()
return uint32(boolToByte(n.IsArrayType)) << 24
case ast.KindJSDocParameterTag, ast.KindJSDocPropertyTag:
n := node.AsJSDocParameterOrPropertyTag()
return uint32(boolToByte(n.IsBracketed))<<24 | uint32(boolToByte(n.IsNameFirst))<<25
}
return 0
}
func recordNodeStrings(node *ast.Node, strs *stringTable) uint32 {
switch node.Kind {
case ast.KindIdentifier:
return strs.add(node.AsIdentifier().Text, node.Kind, node.Pos(), node.End())
case ast.KindPrivateIdentifier:
return strs.add(node.AsPrivateIdentifier().Text, node.Kind, node.Pos(), node.End())
case ast.KindJsxText:
return strs.add(node.AsJsxText().Text, node.Kind, node.Pos(), node.End())
case ast.KindJSDocText:
return strs.add(node.AsJSDocText().Text(), node.Kind, node.Pos(), node.End())
case ast.KindJSDocLink:
return strs.add(node.AsJSDocLink().Text(), node.Kind, node.Pos(), node.End())
case ast.KindJSDocLinkPlain:
return strs.add(node.AsJSDocLinkPlain().Text(), node.Kind, node.Pos(), node.End())
case ast.KindJSDocLinkCode:
return strs.add(node.AsJSDocLinkCode().Text(), node.Kind, node.Pos(), node.End())
default:
panic(fmt.Sprintf("Unexpected node kind %v", node.Kind))
}
}
func recordExtendedData(node *ast.Node, strs *stringTable, positionMap *ast.PositionMap, extendedData *[]byte, structuredData *[]byte) uint32 {
offset := uint32(len(*extendedData))
switch node.Kind {
case ast.KindStringLiteral:
recordExtendedData_StringLiteral(node, strs, positionMap, extendedData, structuredData)
case ast.KindNumericLiteral:
recordExtendedData_NumericLiteral(node, strs, positionMap, extendedData, structuredData)
case ast.KindBigIntLiteral:
recordExtendedData_BigIntLiteral(node, strs, positionMap, extendedData, structuredData)
case ast.KindRegularExpressionLiteral:
recordExtendedData_RegularExpressionLiteral(node, strs, positionMap, extendedData, structuredData)
case ast.KindNoSubstitutionTemplateLiteral:
recordExtendedData_NoSubstitutionTemplateLiteral(node, strs, positionMap, extendedData, structuredData)
case ast.KindTemplateHead:
recordExtendedData_TemplateHead(node, strs, positionMap, extendedData, structuredData)
case ast.KindTemplateMiddle:
recordExtendedData_TemplateMiddle(node, strs, positionMap, extendedData, structuredData)
case ast.KindTemplateTail:
recordExtendedData_TemplateTail(node, strs, positionMap, extendedData, structuredData)
case ast.KindSourceFile:
recordExtendedData_SourceFile(node, strs, positionMap, extendedData, structuredData)
default:
panic(fmt.Sprintf("unknown extended data node kind %v", node.Kind))
}
return offset
}

View File

@@ -0,0 +1,161 @@
package encoder_test
import (
"encoding/binary"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"github.com/microsoft/typescript-go/internal/api/encoder"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/parser"
"github.com/microsoft/typescript-go/internal/repo"
"github.com/microsoft/typescript-go/internal/testutil/baseline"
"gotest.tools/v3/assert"
)
func TestEncodeSourceFile(t *testing.T) {
t.Parallel()
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
FileName: "/test.ts",
Path: "/test.ts",
}, "import { bar } from \"bar\";\nexport function foo<T, U>(a: string, b: string): any {}\nfoo();", core.ScriptKindTS)
t.Run("baseline", func(t *testing.T) {
t.Parallel()
buf, _, err := encoder.EncodeSourceFile(sourceFile)
assert.NilError(t, err)
str := formatEncodedSourceFile(buf)
baseline.Run(t, "encodeSourceFile.txt", str, baseline.Options{
Subfolder: "api",
})
})
}
func TestEncodeSourceFileWithUnicodeEscapes(t *testing.T) {
t.Parallel()
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
FileName: "/test.ts",
Path: "/test.ts",
}, `let a = "😃"; let b = "\ud83d\ude03"; let c = "\udc00\ud83d\ude03"; let d = "\ud83d\ud83d\ude03"`, core.ScriptKindTS)
t.Run("baseline", func(t *testing.T) {
t.Parallel()
buf, _, err := encoder.EncodeSourceFile(sourceFile)
assert.NilError(t, err)
str := formatEncodedSourceFile(buf)
baseline.Run(t, "encodeSourceFileWithUnicodeEscapes.txt", str, baseline.Options{
Subfolder: "api",
})
})
}
func TestBuildNodeIndexTableMatchesEncode(t *testing.T) {
t.Parallel()
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
FileName: "/test.ts",
Path: "/test.ts",
}, "import { bar } from \"bar\";\nexport function foo<T, U>(a: string, b: string): any {}\nfoo();", core.ScriptKindTS)
_, encodeTable, err := encoder.EncodeSourceFile(sourceFile)
assert.NilError(t, err)
buildTable := encoder.BuildNodeIndexTable(sourceFile)
// Both tables should produce identical Nodes slices
assert.Equal(t, len(buildTable.Nodes), len(encodeTable.Nodes), "Nodes slice length mismatch")
// Every index should map to the same node
for i := range encodeTable.Nodes {
assert.Equal(t, buildTable.Nodes[i], encodeTable.Nodes[i], "node mismatch at index %d", i)
}
// GetIndex on both tables should agree for every non-nil node
for i, node := range encodeTable.Nodes {
if node == nil {
continue
}
encIdx := encodeTable.GetIndex(node)
buildIdx := buildTable.GetIndex(node)
assert.Equal(t, encIdx, uint32(i), "encodeTable.GetIndex mismatch at index %d, node kind=%s", i, node.Kind.String())
assert.Equal(t, buildIdx, encIdx, "buildTable.GetIndex mismatch for node kind=%s", node.Kind.String())
}
}
func BenchmarkEncodeSourceFile(b *testing.B) {
repo.SkipIfNoTypeScriptSubmodule(b)
filePath := filepath.Join(repo.TypeScriptSubmodulePath(), "src/compiler/checker.ts")
fileContent, err := os.ReadFile(filePath)
assert.NilError(b, err)
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
FileName: "/checker.ts",
Path: "/checker.ts",
}, string(fileContent), core.ScriptKindTS)
for b.Loop() {
_, _, err := encoder.EncodeSourceFile(sourceFile)
assert.NilError(b, err)
}
}
func BenchmarkBuildNodeIndexTable(b *testing.B) {
repo.SkipIfNoTypeScriptSubmodule(b)
filePath := filepath.Join(repo.TypeScriptSubmodulePath(), "src/compiler/checker.ts")
fileContent, err := os.ReadFile(filePath)
assert.NilError(b, err)
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
FileName: "/checker.ts",
Path: "/checker.ts",
}, string(fileContent), core.ScriptKindTS)
for b.Loop() {
encoder.BuildNodeIndexTable(sourceFile)
}
}
func readUint32(buf []byte, offset int) uint32 {
return binary.LittleEndian.Uint32(buf[offset : offset+4])
}
func formatEncodedSourceFile(encoded []byte) string {
var result strings.Builder
var getIndent func(parentIndex uint32) string
offsetNodes := readUint32(encoded, encoder.HeaderOffsetNodes)
offsetStringOffsets := readUint32(encoded, encoder.HeaderOffsetStringOffsets)
offsetStrings := readUint32(encoded, encoder.HeaderOffsetStringData)
getIndent = func(parentIndex uint32) string {
if parentIndex == 0 {
return ""
}
return " " + getIndent(readUint32(encoded, int(offsetNodes)+int(parentIndex)*encoder.NodeSize+encoder.NodeOffsetParent))
}
j := 1
for i := int(offsetNodes) + encoder.NodeSize; i < len(encoded); i += encoder.NodeSize {
kind := readUint32(encoded, i+encoder.NodeOffsetKind)
pos := readUint32(encoded, i+encoder.NodeOffsetPos)
end := readUint32(encoded, i+encoder.NodeOffsetEnd)
parentIndex := readUint32(encoded, i+encoder.NodeOffsetParent)
result.WriteString(getIndent(parentIndex))
if kind == encoder.SyntaxKindNodeList {
result.WriteString("NodeList")
} else {
result.WriteString(ast.Kind(kind).String())
}
data := readUint32(encoded, i+encoder.NodeOffsetData)
dataType := data & encoder.NodeDataTypeMask
if ast.Kind(kind) == ast.KindIdentifier || (dataType == encoder.NodeDataTypeString) {
stringIndex := data & encoder.NodeDataStringIndexMask
strStart := readUint32(encoded, int(offsetStringOffsets+stringIndex*4))
strEnd := readUint32(encoded, int(offsetStringOffsets+stringIndex*4)+4)
str := string(encoded[offsetStrings+strStart : offsetStrings+strEnd])
result.WriteString(fmt.Sprintf(" \"%s\"", str))
}
fmt.Fprintf(&result, " [%d, %d), i=%d, next=%d", pos, end, j, encoded[i+encoder.NodeOffsetNext])
result.WriteString("\n")
j++
}
return result.String()
}

View File

@@ -0,0 +1,68 @@
package encoder
import (
"strings"
"github.com/microsoft/typescript-go/internal/ast"
)
type stringTable struct {
fileText string
otherStrings *strings.Builder
// offsets are pos/end pairs
offsets []uint32
}
func newStringTable(fileText string, stringCount int) *stringTable {
builder := &strings.Builder{}
return &stringTable{
fileText: fileText,
otherStrings: builder,
offsets: make([]uint32, 0, stringCount*2),
}
}
func (t *stringTable) add(text string, kind ast.Kind, pos int, end int) uint32 {
index := uint32(len(t.offsets))
if kind == ast.KindSourceFile {
t.offsets = append(t.offsets, uint32(pos), uint32(end))
return index
}
length := len(text)
if end-pos > 0 && end <= len(t.fileText) {
// pos includes leading trivia, but we can usually infer the actual start of the
// string from the kind and end
endOffset := 0
if kind == ast.KindStringLiteral || kind == ast.KindTemplateTail || kind == ast.KindNoSubstitutionTemplateLiteral {
endOffset = 1
}
end = end - endOffset
start := end - length
fileSlice := t.fileText[start:end]
if fileSlice == text {
t.offsets = append(t.offsets, uint32(start), uint32(end))
return index
}
}
// no exact match, so we need to add it to the string table
offset := len(t.fileText) + t.otherStrings.Len()
t.otherStrings.WriteString(text)
t.offsets = append(t.offsets, uint32(offset), uint32(offset+length))
return index
}
func (t *stringTable) encode() []byte {
result := make([]byte, 0, t.encodedLength())
result = appendUint32s(result, t.offsets...)
result = append(result, t.fileText...)
result = append(result, t.otherStrings.String()...)
return result
}
func (t *stringTable) stringLength() int {
return len(t.fileText) + t.otherStrings.Len()
}
func (t *stringTable) encodedLength() int {
return len(t.offsets)*4 + len(t.fileText) + t.otherStrings.Len()
}

View File

@@ -0,0 +1,14 @@
package encoder_test
import (
"testing"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/testutil/baseline"
)
func TestMain(m *testing.M) {
core.ApplyDebugStackLimit()
defer baseline.Track()()
m.Run()
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,83 @@
package api_test
import (
"strings"
"testing"
"github.com/microsoft/typescript-go/internal/api"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/json"
"github.com/microsoft/typescript-go/internal/parser"
"gotest.tools/v3/assert"
)
func TestDocumentIdentifierUnmarshalJSON(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
fileName string
uri string
err string
}{
{
name: "plain string",
input: `"foo.ts"`,
fileName: "foo.ts",
},
{
name: "uri object",
input: `{"uri":"file:///foo.ts"}`,
uri: "file:///foo.ts",
},
{
name: "uri object with unknown fields",
input: `{"uri":"file:///foo.ts","extra":true}`,
uri: "file:///foo.ts",
},
{
name: "empty object",
input: `{}`,
},
{
name: "invalid type",
input: `42`,
err: "expected string or object, got number",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var d api.DocumentIdentifier
err := json.Unmarshal([]byte(tt.input), &d)
if tt.err != "" {
assert.ErrorContains(t, err, tt.err)
return
}
assert.NilError(t, err)
assert.Equal(t, d.FileName, tt.fileName)
assert.Equal(t, string(d.URI), tt.uri)
})
}
}
func TestNewDiagnosticResponseUsesUTF16Offsets(t *testing.T) {
t.Parallel()
text := "const 💩 = 1;"
file := parser.ParseSourceFile(ast.SourceFileParseOptions{FileName: "/unicode.ts"}, text, core.ScriptKindTS)
pos := strings.Index(text, "=")
assert.Assert(t, pos > 0)
end := pos + len("=")
diag := ast.NewDiagnostic(file, core.NewTextRange(pos, end), diagnostics.Expression_expected)
resp := api.NewDiagnosticResponse(diag)
assert.Equal(t, resp.Pos, 9)
assert.Equal(t, resp.End, 10)
assert.Equal(t, resp.Pos, file.GetPositionMap().UTF8ToUTF16(pos))
assert.Equal(t, resp.End, file.GetPositionMap().UTF8ToUTF16(end))
}

View File

@@ -0,0 +1,22 @@
package api
import (
"github.com/microsoft/typescript-go/internal/jsonrpc"
)
// Message is an alias for jsonrpc.Message for convenience.
type Message = jsonrpc.Message
// Protocol defines the interface for reading and writing API messages.
type Protocol interface {
// ReadMessage reads the next message from the connection.
ReadMessage() (*Message, error)
// WriteRequest writes a request message.
WriteRequest(id *jsonrpc.ID, method string, params any) error
// WriteNotification writes a notification message (no ID).
WriteNotification(method string, params any) error
// WriteResponse writes a successful response.
WriteResponse(id *jsonrpc.ID, result any) error
// WriteError writes an error response.
WriteError(id *jsonrpc.ID, err *jsonrpc.ResponseError) error
}

View File

@@ -0,0 +1,96 @@
package api
import (
"io"
"github.com/microsoft/typescript-go/internal/json"
"github.com/microsoft/typescript-go/internal/jsonrpc"
)
// JSONRPCProtocol implements the Protocol interface using JSON-RPC 2.0
// with the LSP base protocol framing (Content-Length headers).
type JSONRPCProtocol struct {
reader *jsonrpc.Reader
writer *jsonrpc.Writer
}
var _ Protocol = (*JSONRPCProtocol)(nil)
// NewJSONRPCProtocol creates a new JSON-RPC protocol handler.
func NewJSONRPCProtocol(rw io.ReadWriter) *JSONRPCProtocol {
return &JSONRPCProtocol{
reader: jsonrpc.NewReader(rw),
writer: jsonrpc.NewWriter(rw),
}
}
// ReadMessage implements Protocol.
func (p *JSONRPCProtocol) ReadMessage() (*Message, error) {
data, err := p.reader.Read()
if err != nil {
return nil, err
}
var msg Message
if err := json.Unmarshal(data, &msg); err != nil {
return nil, err
}
return &msg, nil
}
// WriteRequest implements Protocol.
func (p *JSONRPCProtocol) WriteRequest(id *jsonrpc.ID, method string, params any) error {
msg := jsonrpc.RequestMessage{
ID: id,
Method: method,
Params: params,
}
data, err := json.Marshal(msg)
if err != nil {
return err
}
return p.writer.Write(data)
}
// WriteNotification implements Protocol.
func (p *JSONRPCProtocol) WriteNotification(method string, params any) error {
msg := jsonrpc.RequestMessage{
Method: method,
Params: params,
}
data, err := json.Marshal(msg)
if err != nil {
return err
}
return p.writer.Write(data)
}
// WriteResponse implements Protocol.
func (p *JSONRPCProtocol) WriteResponse(id *jsonrpc.ID, result any) error {
if result == nil {
result = json.Value("null")
}
msg := jsonrpc.ResponseMessage{
ID: id,
Result: result,
}
data, err := json.Marshal(msg)
if err != nil {
return err
}
return p.writer.Write(data)
}
// WriteError implements Protocol.
func (p *JSONRPCProtocol) WriteError(id *jsonrpc.ID, respErr *jsonrpc.ResponseError) error {
msg := jsonrpc.ResponseMessage{
ID: id,
Error: respErr,
}
data, err := json.Marshal(msg)
if err != nil {
return err
}
return p.writer.Write(data)
}

View File

@@ -0,0 +1,280 @@
package api
import (
"bufio"
"encoding/binary"
"fmt"
"io"
"github.com/microsoft/typescript-go/internal/json"
"github.com/microsoft/typescript-go/internal/jsonrpc"
)
// MessageType represents the type of message in the msgpack protocol.
type MessageType uint8
const (
MessageTypeUnknown MessageType = iota
MessageTypeRequest
MessageTypeCallResponse
MessageTypeCallError
MessageTypeResponse
MessageTypeError
MessageTypeCall
)
func (m MessageType) IsValid() bool {
return m >= MessageTypeRequest && m <= MessageTypeCall
}
// MessagePack format constants
const (
msgpackFixedArray3 byte = 0x93
msgpackBin8 byte = 0xC4
msgpackBin16 byte = 0xC5
msgpackBin32 byte = 0xC6
msgpackU8 byte = 0xCC
)
// MessagePackProtocol implements the Protocol interface using a custom
// msgpack-based tuple format: [MessageType, method, payload].
type MessagePackProtocol struct {
r *bufio.Reader
w *bufio.Writer
}
var _ Protocol = (*MessagePackProtocol)(nil)
// NewMessagePackProtocol creates a new msgpack protocol handler.
func NewMessagePackProtocol(rw io.ReadWriter) *MessagePackProtocol {
return &MessagePackProtocol{
r: bufio.NewReader(rw),
w: bufio.NewWriter(rw),
}
}
// ReadMessage implements Protocol.
func (p *MessagePackProtocol) ReadMessage() (*Message, error) {
msgType, method, payload, err := p.readTuple()
if err != nil {
return nil, err
}
// Convert msgpack message type to JSON-RPC message
msg := &Message{}
switch msgType {
case MessageTypeRequest:
// Client request - needs an ID for response
// We use the method as a pseudo-ID since this protocol doesn't have explicit IDs
id := jsonrpc.NewIDString(method)
msg.ID = id
msg.Method = method
msg.Params = payload
case MessageTypeCallResponse:
// Response to our Call - use method as ID
// Note: Method must be empty for IsResponse() to return true
id := jsonrpc.NewIDString(method)
msg.ID = id
msg.Result = payload
case MessageTypeCallError:
// Error response to our Call
// Note: Method must be empty for IsResponse() to return true
id := jsonrpc.NewIDString(method)
msg.ID = id
msg.Error = &jsonrpc.ResponseError{
Code: jsonrpc.CodeInternalError,
Message: string(payload),
}
default:
return nil, fmt.Errorf("unexpected message type: %d", msgType)
}
return msg, nil
}
func (p *MessagePackProtocol) readTuple() (MessageType, string, []byte, error) {
// Read fixed array marker (0x93 = 3-element array)
t, err := p.r.ReadByte()
if err != nil {
return 0, "", nil, err
}
if t != msgpackFixedArray3 {
return 0, "", nil, fmt.Errorf("%w: expected fixed 3-element array (0x93), received: 0x%02x", ErrInvalidRequest, t)
}
// Read message type - can be positive fixint (0x00-0x7F) or uint8 (0xCC + value)
t, err = p.r.ReadByte()
if err != nil {
return 0, "", nil, err
}
var rawType byte
if t <= 0x7F {
// Positive fixint - the byte IS the value
rawType = t
} else if t == msgpackU8 {
// uint8 marker - next byte is the value
rawType, err = p.r.ReadByte()
if err != nil {
return 0, "", nil, err
}
} else {
return 0, "", nil, fmt.Errorf("%w: expected positive fixint or uint8 marker, received: 0x%02x", ErrInvalidRequest, t)
}
msgType := MessageType(rawType)
if !msgType.IsValid() {
return 0, "", nil, fmt.Errorf("%w: unknown message type: %d", ErrInvalidRequest, msgType)
}
// Read method (binary)
methodBytes, err := p.readBin()
if err != nil {
return 0, "", nil, err
}
method := string(methodBytes)
// Read payload (binary)
payload, err := p.readBin()
if err != nil {
return 0, "", nil, err
}
return msgType, method, payload, nil
}
func (p *MessagePackProtocol) readBin() ([]byte, error) {
t, err := p.r.ReadByte()
if err != nil {
return nil, err
}
var size uint
switch t {
case msgpackBin8:
var size8 uint8
if err = binary.Read(p.r, binary.BigEndian, &size8); err != nil {
return nil, err
}
size = uint(size8)
case msgpackBin16:
var size16 uint16
if err = binary.Read(p.r, binary.BigEndian, &size16); err != nil {
return nil, err
}
size = uint(size16)
case msgpackBin32:
var size32 uint32
if err = binary.Read(p.r, binary.BigEndian, &size32); err != nil {
return nil, err
}
size = uint(size32)
default:
return nil, fmt.Errorf("%w: expected binary data (0xc4-0xc6), received: 0x%02x", ErrInvalidRequest, t)
}
payload := make([]byte, size)
if _, err := io.ReadFull(p.r, payload); err != nil {
return nil, err
}
return payload, nil
}
// WriteRequest implements Protocol.
func (p *MessagePackProtocol) WriteRequest(id *jsonrpc.ID, method string, params any) error {
// For msgpack protocol, requests from server are "Call" type
payload, err := json.Marshal(params)
if err != nil {
return err
}
return p.writeTuple(MessageTypeCall, method, payload)
}
// WriteNotification implements Protocol.
func (p *MessagePackProtocol) WriteNotification(method string, params any) error {
// Msgpack protocol doesn't distinguish notifications from calls
return p.WriteRequest(nil, method, params)
}
// WriteResponse implements Protocol.
func (p *MessagePackProtocol) WriteResponse(id *jsonrpc.ID, result any) error {
method := ""
if id != nil {
method = id.String()
}
var payload []byte
var err error
// Check if result is raw binary (for efficient binary transport)
if raw, ok := result.(RawBinary); ok {
payload = []byte(raw)
} else {
payload, err = json.Marshal(result)
if err != nil {
return err
}
}
return p.writeTuple(MessageTypeResponse, method, payload)
}
// WriteError implements Protocol.
func (p *MessagePackProtocol) WriteError(id *jsonrpc.ID, respErr *jsonrpc.ResponseError) error {
method := ""
if id != nil {
method = id.String()
}
return p.writeTuple(MessageTypeError, method, []byte(respErr.Message))
}
func (p *MessagePackProtocol) writeTuple(msgType MessageType, method string, payload []byte) error {
// Write fixed array marker
if err := p.w.WriteByte(msgpackFixedArray3); err != nil {
return err
}
// Write message type as positive fixint (values 0-127 are written directly)
if err := p.w.WriteByte(byte(msgType)); err != nil {
return err
}
// Write method
if err := p.writeBin([]byte(method)); err != nil {
return err
}
// Write payload
if err := p.writeBin(payload); err != nil {
return err
}
return p.w.Flush()
}
func (p *MessagePackProtocol) writeBin(data []byte) error {
length := len(data)
if length < 256 {
if err := p.w.WriteByte(msgpackBin8); err != nil {
return err
}
if err := p.w.WriteByte(byte(length)); err != nil {
return err
}
} else if length < 1<<16 {
if err := p.w.WriteByte(msgpackBin16); err != nil {
return err
}
if err := binary.Write(p.w, binary.BigEndian, uint16(length)); err != nil {
return err
}
} else {
if err := p.w.WriteByte(msgpackBin32); err != nil {
return err
}
if err := binary.Write(p.w, binary.BigEndian, uint32(length)); err != nil {
return err
}
}
_, err := p.w.Write(data)
return err
}
// RawBinary is a marker type for binary data that should be written
// directly by MessagePackProtocol instead of being JSON-encoded.
type RawBinary []byte

View File

@@ -0,0 +1,124 @@
package api
import (
"context"
"fmt"
"io"
"github.com/microsoft/typescript-go/internal/bundled"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/project"
"github.com/microsoft/typescript-go/internal/vfs/osvfs"
)
// StdioServerOptions configures the STDIO-based API server.
type StdioServerOptions struct {
In io.ReadCloser
Out io.WriteCloser
Err io.Writer
Cwd string
DefaultLibraryPath string
// PipePath, if set, listens on a named pipe (Windows) or Unix domain
// socket instead of using In/Out for communication.
PipePath string
// Callbacks specifies which filesystem operations should be delegated
// to the client (e.g., "readFile", "fileExists"). Empty means no callbacks.
Callbacks []string
// Async enables JSON-RPC protocol with async connection handling.
// When false (default), uses MessagePack protocol with sync connection.
Async bool
// CollectTiming enables per-request server processing-time measurement.
// When enabled, the server accumulates each request's processing time into
// running totals and a recent-request ring buffer. Response messages are
// left unchanged; the client folds this data into its own timing snapshot
// on demand via getServerTiming / resetServerTiming requests.
CollectTiming bool
}
// StdioServer runs an API session over STDIO using MessagePack protocol.
// This is the entry point for the synchronous STDIO-based API used by
// native TypeScript tooling integration.
type StdioServer struct {
options *StdioServerOptions
}
// NewStdioServer creates a new STDIO-based API server.
func NewStdioServer(options *StdioServerOptions) *StdioServer {
if options.Cwd == "" {
panic("StdioServerOptions.Cwd is required")
}
return &StdioServer{
options: options,
}
}
// Run starts the server and blocks until the connection closes.
func (s *StdioServer) Run(ctx context.Context) error {
var transport Transport
if s.options.PipePath != "" {
t, err := NewPipeTransport(s.options.PipePath)
if err != nil {
return fmt.Errorf("failed to create pipe transport: %w", err)
}
defer t.Close()
transport = t
} else {
t := NewStdioTransport(s.options.In, s.options.Out)
defer t.Close()
transport = t
}
fs := bundled.WrapFS(osvfs.FS())
// Wrap the base FS with callbackFS if callbacks are requested
var callbackFS *callbackFS
if len(s.options.Callbacks) > 0 {
callbackFS = newCallbackFS(fs, s.options.Callbacks)
fs = callbackFS
}
projectSession := project.NewSession(&project.SessionInit{
BackgroundCtx: ctx,
Logger: nil, // TODO: Add logging support
FS: fs,
Options: &project.SessionOptions{
CurrentDirectory: s.options.Cwd,
DefaultLibraryPath: s.options.DefaultLibraryPath,
PositionEncoding: lsproto.PositionEncodingKindUTF8,
LoggingEnabled: false,
},
})
session := NewSession(projectSession, &SessionOptions{
UseBinaryResponses: !s.options.Async, // Only msgpack uses binary responses
})
defer session.Close()
// Accept connection from transport
rwc, err := transport.Accept()
if err != nil {
return fmt.Errorf("failed to accept connection: %w", err)
}
// Create protocol and connection based on async mode
var conn Conn
if s.options.Async {
protocol := NewJSONRPCProtocol(rwc)
asyncConn := NewAsyncConnWithProtocol(rwc, protocol, session)
asyncConn.SetCollectTiming(s.options.CollectTiming)
conn = asyncConn
} else {
protocol := NewMessagePackProtocol(rwc)
syncConn := NewSyncConn(rwc, protocol, session)
syncConn.SetCollectTiming(s.options.CollectTiming)
conn = syncConn
}
// If callbacks are enabled, set the connection on the FS
if callbackFS != nil {
callbackFS.SetConnection(ctx, conn)
}
return conn.Run(ctx)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,255 @@
package api
import (
"context"
"testing"
"github.com/microsoft/typescript-go/internal/bundled"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/testutil/projecttestutil"
"github.com/microsoft/typescript-go/internal/tspath"
"gotest.tools/v3/assert"
)
// TestSessionTracksAndReleasesAPIRefs verifies that an API session holds at most
// one ref per opened project/file (opens are idempotent) and releases exactly
// those refs when the session is closed, so it never leaks or over-releases refs
// in the underlying (potentially shared) project session.
func TestSessionTracksAndReleasesAPIRefs(t *testing.T) {
t.Parallel()
if !bundled.Embedded {
t.Skip("bundled files are not embedded")
}
t.Run("project opens are idempotent and released on close", func(t *testing.T) {
t.Parallel()
const configFileName = "/home/projects/p/tsconfig.json"
files := map[string]any{
configFileName: `{ "compilerOptions": { "strict": true } }`,
"/home/projects/p/src/index.ts": `export const x = 1;`,
}
projectSession, _ := projecttestutil.Setup(files)
defer projectSession.Close()
session := NewSession(projectSession, nil)
_, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{
OpenProjects: []DocumentIdentifier{{FileName: configFileName}},
})
assert.NilError(t, err)
assert.Equal(t, session.openProjects.Len(), 1)
// Opening the same project again must not take an additional ref.
_, err = session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{
OpenProjects: []DocumentIdentifier{{FileName: configFileName}},
})
assert.NilError(t, err)
assert.Equal(t, session.openProjects.Len(), 1)
assert.Assert(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path(configFileName)) != nil)
// Closing the session releases the single API ref, so the project is no
// longer kept loaded.
session.Close()
assert.Equal(t, session.openProjects.Len(), 0)
assert.Assert(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path(configFileName)) == nil)
})
t.Run("explicit close releases the project ref", func(t *testing.T) {
t.Parallel()
const configFileName = "/home/projects/p/tsconfig.json"
files := map[string]any{
configFileName: `{ "compilerOptions": { "strict": true } }`,
"/home/projects/p/src/index.ts": `export const x = 1;`,
}
projectSession, _ := projecttestutil.Setup(files)
defer projectSession.Close()
session := NewSession(projectSession, nil)
defer session.Close()
_, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{
OpenProjects: []DocumentIdentifier{{FileName: configFileName}},
})
assert.NilError(t, err)
assert.Equal(t, session.openProjects.Len(), 1)
assert.Assert(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path(configFileName)) != nil)
// Closing a project we hold releases the ref and unloads the project.
_, err = session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{
CloseProjects: []DocumentIdentifier{{FileName: configFileName}},
})
assert.NilError(t, err)
assert.Equal(t, session.openProjects.Len(), 0)
assert.Assert(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path(configFileName)) == nil)
// Closing a project we don't hold is a no-op (never over-releases).
_, err = session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{
CloseProjects: []DocumentIdentifier{{FileName: configFileName}},
})
assert.NilError(t, err)
assert.Equal(t, session.openProjects.Len(), 0)
})
t.Run("file opens are idempotent and released on close", func(t *testing.T) {
t.Parallel()
const fileName = "/home/projects/p/src/index.ts"
files := map[string]any{
"/home/projects/p/tsconfig.json": `{ "compilerOptions": { "strict": true } }`,
fileName: `export const x = 1;`,
}
projectSession, _ := projecttestutil.Setup(files)
defer projectSession.Close()
session := NewSession(projectSession, nil)
_, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{
OpenFiles: []DocumentIdentifier{{FileName: fileName}},
})
assert.NilError(t, err)
assert.Equal(t, session.openFiles.Len(), 1)
// Re-opening the same file must not take an additional ref.
_, err = session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{
OpenFiles: []DocumentIdentifier{{FileName: fileName}},
})
assert.NilError(t, err)
assert.Equal(t, session.openFiles.Len(), 1)
// The file should resolve to the configured project via ancestor search.
assert.Assert(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/p/tsconfig.json")) != nil)
// Closing a file we don't hold is a no-op (never over-releases).
_, err = session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{
CloseFiles: []DocumentIdentifier{{FileName: "/home/projects/p/other.ts"}},
})
assert.NilError(t, err)
assert.Equal(t, session.openFiles.Len(), 1)
// Explicitly closing the held file releases the ref.
_, err = session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{
CloseFiles: []DocumentIdentifier{{FileName: fileName}},
})
assert.NilError(t, err)
assert.Equal(t, session.openFiles.Len(), 0)
// Closing the file also tears down the configured project that was
// auto-loaded to serve it, instead of leaking it.
assert.Assert(t,
projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/p/tsconfig.json")) == nil,
"configured project auto-loaded for the API-opened file should be unloaded after close",
)
session.Close()
assert.Equal(t, session.openFiles.Len(), 0)
})
t.Run("relative file paths normalize consistently for open and close", func(t *testing.T) {
t.Parallel()
// The project session's current directory is "/", so a relative path
// resolves to the corresponding absolute path.
files := map[string]any{
"/src/tsconfig.json": `{ "compilerOptions": { "strict": true } }`,
"/src/index.ts": `export const x = 1;`,
}
projectSession, _ := projecttestutil.Setup(files)
defer projectSession.Close()
session := NewSession(projectSession, nil)
defer session.Close()
// Open via a relative path; it should be tracked under the absolute path
// and resolve to the containing configured project.
openResp, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{
OpenFiles: []DocumentIdentifier{{FileName: "src/index.ts"}},
})
assert.NilError(t, err)
assert.Equal(t, session.openFiles.Len(), 1)
assert.Assert(t, session.openFiles.Has(tspath.Path("/src/index.ts")))
assert.Assert(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path("/src/tsconfig.json")) != nil)
// getDefaultProjectForFile must also resolve a relative path to the same
// configured project (it builds a URI from the identifier internally).
proj, err := session.handleGetDefaultProjectForFile(context.Background(), &GetDefaultProjectForFileParams{
Snapshot: openResp.Snapshot,
File: DocumentIdentifier{FileName: "src/index.ts"},
})
assert.NilError(t, err)
assert.Assert(t, proj != nil, "relative path should resolve to a default project")
assert.Equal(t, proj.ConfigFileName, "/src/tsconfig.json")
// Re-opening via the absolute path must match the relative open (no new ref).
_, err = session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{
OpenFiles: []DocumentIdentifier{{FileName: "/src/index.ts"}},
})
assert.NilError(t, err)
assert.Equal(t, session.openFiles.Len(), 1)
// Closing via a relative path must match the path stored when opening.
_, err = session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{
CloseFiles: []DocumentIdentifier{{FileName: "src/index.ts"}},
})
assert.NilError(t, err)
assert.Equal(t, session.openFiles.Len(), 0)
assert.Assert(t,
projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path("/src/tsconfig.json")) == nil,
"configured project should be unloaded after closing the relatively-pathed file",
)
})
}
// TestUpdateSnapshotResponseSkipsUnloadedAncestorProject verifies that API
// updateSnapshot does not report unloaded ancestor project placeholders. This
// covers the case where opening a file loads its nearest configured project
// while solution search discovers an ancestor tsconfig placeholder whose command
// line is still nil.
func TestUpdateSnapshotResponseSkipsUnloadedAncestorProject(t *testing.T) {
t.Parallel()
if !bundled.Embedded {
t.Skip("bundled files are not embedded")
}
const (
nestedConfigFileName = "/repo/packages/app/tsconfig.json"
ancestorConfigFileName = "/repo/packages/tsconfig.json"
fileName = "/repo/packages/app/src/index.ts"
)
files := map[string]any{
ancestorConfigFileName: `{ "files": [] }`,
nestedConfigFileName: `{
"compilerOptions": { "composite": true },
"include": ["**/*"]
}`,
fileName: `let s: string = 1234;`,
}
projectSession, _ := projecttestutil.Setup(files)
defer projectSession.Close()
projectSession.DidOpenFile(context.Background(), lsproto.DocumentUri("file://"+fileName), 1, files[fileName].(string), lsproto.LanguageKindTypeScript)
snapshot := projectSession.Snapshot()
nestedProject := snapshot.ProjectCollection.ConfiguredProject(tspath.Path(nestedConfigFileName))
assert.Assert(t, nestedProject != nil)
assert.Assert(t, nestedProject.CommandLine != nil)
ancestorProject := snapshot.ProjectCollection.ConfiguredProject(tspath.Path(ancestorConfigFileName))
assert.Assert(t, ancestorProject != nil)
assert.Assert(t, ancestorProject.CommandLine == nil)
session := NewSession(projectSession, nil)
defer session.Close()
response, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{
OpenProjects: []DocumentIdentifier{{FileName: nestedConfigFileName}},
})
assert.NilError(t, err)
var foundNestedProject bool
var foundAncestorProject bool
for _, project := range response.Projects {
switch project.ConfigFileName {
case nestedConfigFileName:
foundNestedProject = true
assert.Assert(t, project.RootFiles != nil)
assert.Assert(t, project.CompilerOptions != nil)
case ancestorConfigFileName:
foundAncestorProject = true
}
}
assert.Assert(t, foundNestedProject)
assert.Assert(t, !foundAncestorProject)
}

View File

@@ -0,0 +1,141 @@
package api
import (
"context"
"testing"
"github.com/microsoft/typescript-go/internal/bundled"
"github.com/microsoft/typescript-go/internal/testutil/projecttestutil"
"gotest.tools/v3/assert"
)
// TestCompletionSymbolTypeIsResolvable reproduces a crash where requesting the
// type of a completion-provided symbol panicked with a nil pointer dereference.
//
// Completion ran on an ephemeral query checker (default lifetime), so members of
// a generic type such as `string[]` (= Array<string>) were returned as
// *instantiated* symbols whose per-checker instantiation links live only on that
// query checker. GetTypeOfSymbol runs on the persistent API checker — a
// different instance — where those links are absent, so getTypeOfInstantiatedSymbol
// dereferenced a nil target and brought down the connection.
//
// The fix pins symbol-producing completion to the API checker, so the returned
// handles resolve on the same checker the client re-queries.
func TestCompletionSymbolTypeIsResolvable(t *testing.T) {
t.Parallel()
if !bundled.Embedded {
t.Skip("bundled files are not embedded")
}
const fileName = "/home/projects/p/src/index.ts"
// The caret sits right after `people.`, requesting members of `string[]`.
const content = "declare const people: string[];\npeople."
files := map[string]any{
"/home/projects/p/tsconfig.json": `{ "compilerOptions": { "strict": true } }`,
fileName: content,
}
projectSession, _ := projecttestutil.Setup(files)
defer projectSession.Close()
session := NewSession(projectSession, nil)
defer session.Close()
ctx := context.Background()
snapshotResp, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{
OpenFiles: []DocumentIdentifier{{FileName: fileName}},
})
assert.NilError(t, err)
proj, err := session.handleGetDefaultProjectForFile(ctx, &GetDefaultProjectForFileParams{
Snapshot: snapshotResp.Snapshot,
File: DocumentIdentifier{FileName: fileName},
})
assert.NilError(t, err)
assert.Assert(t, proj != nil, "file should resolve to a default project")
// content is pure ASCII, so the UTF-16 caret offset equals the byte length.
completions, err := session.handleGetCompletionsAtPosition(ctx, &GetCompletionsAtPositionParams{
Snapshot: snapshotResp.Snapshot,
Project: proj.Id,
File: DocumentIdentifier{FileName: fileName},
Position: uint32(len(content)),
IncludeSymbol: true,
})
assert.NilError(t, err)
assert.Assert(t, completions != nil, "expected a completion list for array members")
// Resolving the type of every completion symbol must not panic, and known
// members like `push` must produce a concrete type.
var sawSymbol, sawPush bool
for _, entry := range completions.Entries {
if entry.Symbol == nil {
continue
}
sawSymbol = true
typeResp, err := session.handleGetTypeOfSymbol(ctx, &GetTypeOfSymbolParams{
Snapshot: snapshotResp.Snapshot,
Project: proj.Id,
Symbol: entry.Symbol.Id,
})
assert.NilError(t, err)
assert.Assert(t, typeResp != nil, "type of completion symbol %q should resolve", entry.Name)
if entry.Name == "push" {
sawPush = true
}
}
assert.Assert(t, sawSymbol, "completion entries should include resolvable symbols")
assert.Assert(t, sawPush, "array member completions should include `push`")
}
// TestCompletionOnInferredProject reproduces a crash where requesting completions
// for a loose file — one not part of any tsconfig.json, so it resolves to an
// inferred project — panicked with "ConfigFilePath called on non-configured
// project".
//
// setupLanguageService called Project.ConfigFilePath(), which is only valid for
// configured projects and panics for inferred ones. The fix uses Project.ID(),
// which returns the project's path for both configured and inferred projects without panicking.
func TestCompletionOnInferredProject(t *testing.T) {
t.Parallel()
if !bundled.Embedded {
t.Skip("bundled files are not embedded")
}
// No tsconfig.json anywhere, so this file belongs to an inferred project.
const fileName = "/home/projects/p/src/index.ts"
const content = "declare const people: string[];\npeople."
files := map[string]any{
fileName: content,
}
projectSession, _ := projecttestutil.Setup(files)
defer projectSession.Close()
session := NewSession(projectSession, nil)
defer session.Close()
ctx := context.Background()
snapshotResp, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{
OpenFiles: []DocumentIdentifier{{FileName: fileName}},
})
assert.NilError(t, err)
proj, err := session.handleGetDefaultProjectForFile(ctx, &GetDefaultProjectForFileParams{
Snapshot: snapshotResp.Snapshot,
File: DocumentIdentifier{FileName: fileName},
})
assert.NilError(t, err)
assert.Assert(t, proj != nil, "file should resolve to an inferred default project")
// This request previously panicked in setupLanguageService.
// content is pure ASCII, so the UTF-16 caret offset equals the byte length.
completions, err := session.handleGetCompletionsAtPosition(ctx, &GetCompletionsAtPositionParams{
Snapshot: snapshotResp.Snapshot,
Project: proj.Id,
File: DocumentIdentifier{FileName: fileName},
Position: uint32(len(content)),
})
assert.NilError(t, err)
assert.Assert(t, completions != nil, "expected a completion list for array members")
}

View File

@@ -0,0 +1,30 @@
// Code generated by "stringer -type=MessageType -output=stringer_generated.go"; DO NOT EDIT.
package api
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[MessageTypeUnknown-0]
_ = x[MessageTypeRequest-1]
_ = x[MessageTypeCallResponse-2]
_ = x[MessageTypeCallError-3]
_ = x[MessageTypeResponse-4]
_ = x[MessageTypeError-5]
_ = x[MessageTypeCall-6]
}
const _MessageType_name = "MessageTypeUnknownMessageTypeRequestMessageTypeCallResponseMessageTypeCallErrorMessageTypeResponseMessageTypeErrorMessageTypeCall"
var _MessageType_index = [...]uint8{0, 18, 36, 59, 79, 98, 114, 129}
func (i MessageType) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_MessageType_index)-1 {
return "MessageType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _MessageType_name[_MessageType_index[idx]:_MessageType_index[idx+1]]
}

View File

@@ -0,0 +1,136 @@
package api
import (
"sync"
"time"
)
// serverRecentRequestCapacity is the number of most-recent requests retained in
// the server-side timing ring buffer.
const serverRecentRequestCapacity = 5
// serverRequestTiming is a single server-side request's processing-time sample.
type serverRequestTiming struct {
// Method is the API method that was handled.
Method string `json:"method"`
// ProcessingTimeMs is the wall-clock time the server spent handling the
// request, in milliseconds.
ProcessingTimeMs float64 `json:"processingTimeMs"`
// Timestamp is the Unix time in milliseconds when the request completed.
Timestamp int64 `json:"timestamp"`
}
// serverTimingTotals holds running totals accumulated across every handled request.
type serverTimingTotals struct {
// RequestCount is the total number of requests measured.
RequestCount uint64 `json:"requestCount"`
// TotalProcessingTimeMs is the sum of server processing time, in milliseconds.
TotalProcessingTimeMs float64 `json:"totalProcessingTimeMs"`
}
// serverTimingInfo is a point-in-time snapshot of collected server timing,
// returned to clients in response to a getServerTiming request.
type serverTimingInfo struct {
// Enabled reports whether server-side timing collection is active.
Enabled bool `json:"enabled"`
// Totals are the running totals across every handled request.
Totals serverTimingTotals `json:"totals"`
// RecentRequests are the most recent requests, oldest to newest, up to
// serverRecentRequestCapacity.
RecentRequests []serverRequestTiming `json:"recentRequests"`
}
// timingCollector accumulates per-request server processing times into running
// totals and a fixed-size ring buffer of the most recent requests. It is safe
// for concurrent use so the async connection can record from multiple request
// goroutines.
type timingCollector struct {
mu sync.Mutex
totals serverTimingTotals
// ring holds up to serverRecentRequestCapacity entries; once full, head
// marks the oldest entry.
ring []serverRequestTiming
head int
}
func newTimingCollector() *timingCollector {
return &timingCollector{}
}
// record adds a single request's processing time to the totals and ring buffer.
func (c *timingCollector) record(method string, d time.Duration) {
processingMs := durationToMillis(d)
c.mu.Lock()
defer c.mu.Unlock()
c.totals.RequestCount++
c.totals.TotalProcessingTimeMs += processingMs
entry := serverRequestTiming{
Method: method,
ProcessingTimeMs: processingMs,
Timestamp: time.Now().UnixMilli(),
}
if len(c.ring) < serverRecentRequestCapacity {
c.ring = append(c.ring, entry)
} else {
c.ring[c.head] = entry
c.head = (c.head + 1) % serverRecentRequestCapacity
}
}
// snapshot returns a copy of the currently collected timing information, with
// recent requests ordered from oldest to newest.
func (c *timingCollector) snapshot() serverTimingInfo {
c.mu.Lock()
defer c.mu.Unlock()
recent := make([]serverRequestTiming, 0, len(c.ring))
for i := range c.ring {
recent = append(recent, c.ring[(c.head+i)%len(c.ring)])
}
return serverTimingInfo{
Enabled: true,
Totals: c.totals,
RecentRequests: recent,
}
}
// reset clears all accumulated totals and recent-request history.
func (c *timingCollector) reset() {
c.mu.Lock()
defer c.mu.Unlock()
c.totals = serverTimingTotals{}
c.ring = nil
c.head = 0
}
// serverTimingSnapshot returns the collector's snapshot, or a disabled snapshot
// when timing collection is not enabled (collector is nil).
func serverTimingSnapshot(c *timingCollector) serverTimingInfo {
if c == nil {
return disabledServerTimingInfo()
}
return c.snapshot()
}
// disabledServerTimingInfo is the snapshot returned when timing collection is
// not enabled.
func disabledServerTimingInfo() serverTimingInfo {
return serverTimingInfo{
Enabled: false,
RecentRequests: []serverRequestTiming{},
}
}
// durationToMillis converts a duration to fractional milliseconds, clamped to be
// non-negative. It preserves sub-microsecond precision by converting from the
// full nanosecond duration.
func durationToMillis(d time.Duration) float64 {
if d < 0 {
return 0
}
return float64(d) / float64(time.Millisecond)
}

View File

@@ -0,0 +1,95 @@
package api
import (
"testing"
"time"
"gotest.tools/v3/assert"
)
func TestTimingCollector(t *testing.T) {
t.Parallel()
t.Run("accumulates totals and records recent requests", func(t *testing.T) {
t.Parallel()
c := newTimingCollector()
c.record("getSourceFile", 2*time.Millisecond)
c.record("getSymbolAtPosition", 500*time.Microsecond)
snap := c.snapshot()
assert.Equal(t, snap.Enabled, true)
assert.Equal(t, snap.Totals.RequestCount, uint64(2))
assert.Equal(t, snap.Totals.TotalProcessingTimeMs, 2.5)
assert.Equal(t, len(snap.RecentRequests), 2)
assert.Equal(t, snap.RecentRequests[0].Method, "getSourceFile")
assert.Equal(t, snap.RecentRequests[0].ProcessingTimeMs, 2.0)
assert.Equal(t, snap.RecentRequests[1].Method, "getSymbolAtPosition")
assert.Equal(t, snap.RecentRequests[1].ProcessingTimeMs, 0.5)
})
t.Run("ring buffer retains only the most recent requests, oldest to newest", func(t *testing.T) {
t.Parallel()
c := newTimingCollector()
methods := []string{"a", "b", "c", "d", "e", "f", "g"}
for _, m := range methods {
c.record(m, time.Millisecond)
}
snap := c.snapshot()
assert.Equal(t, snap.Totals.RequestCount, uint64(7))
assert.Equal(t, len(snap.RecentRequests), serverRecentRequestCapacity)
// Expect the last 5 methods, oldest to newest.
want := methods[len(methods)-serverRecentRequestCapacity:]
for i, w := range want {
assert.Equal(t, snap.RecentRequests[i].Method, w)
}
})
t.Run("negative durations clamp to zero", func(t *testing.T) {
t.Parallel()
c := newTimingCollector()
c.record("x", -5*time.Second)
snap := c.snapshot()
assert.Equal(t, snap.Totals.TotalProcessingTimeMs, 0.0)
assert.Equal(t, snap.RecentRequests[0].ProcessingTimeMs, 0.0)
})
}
func TestServerTimingSnapshotDisabled(t *testing.T) {
t.Parallel()
snap := serverTimingSnapshot(nil)
assert.Equal(t, snap.Enabled, false)
assert.Equal(t, snap.Totals.RequestCount, uint64(0))
assert.Equal(t, len(snap.RecentRequests), 0)
}
func TestTimingCollectorReset(t *testing.T) {
t.Parallel()
c := newTimingCollector()
c.record("a", time.Millisecond)
c.record("b", time.Millisecond)
c.reset()
snap := c.snapshot()
assert.Equal(t, snap.Enabled, true)
assert.Equal(t, snap.Totals.RequestCount, uint64(0))
assert.Equal(t, snap.Totals.TotalProcessingTimeMs, 0.0)
assert.Equal(t, len(snap.RecentRequests), 0)
// The collector remains usable after a reset.
c.record("c", 2*time.Millisecond)
snap = c.snapshot()
assert.Equal(t, snap.Totals.RequestCount, uint64(1))
assert.Equal(t, snap.RecentRequests[0].Method, "c")
}
func TestDurationToMillis(t *testing.T) {
t.Parallel()
assert.Equal(t, durationToMillis(1500*time.Microsecond), 1.5)
assert.Equal(t, durationToMillis(0), 0.0)
assert.Equal(t, durationToMillis(-5*time.Second), 0.0)
// Sub-microsecond durations retain precision rather than truncating to 0.
assert.Equal(t, durationToMillis(500*time.Nanosecond), 0.0005)
assert.Equal(t, durationToMillis(1234*time.Nanosecond), 0.001234)
}

View File

@@ -0,0 +1,95 @@
package api
import (
"io"
"net"
)
// Transport is an interface for accepting connections from API clients.
type Transport interface {
// Accept waits for and returns the next connection.
Accept() (io.ReadWriteCloser, error)
// Close stops the transport from accepting new connections.
Close() error
}
// PipeTransport accepts connections on a Unix domain socket or Windows named pipe.
type PipeTransport struct {
listener net.Listener
}
// NewPipeTransport creates a new transport listening on the given path.
// On Unix, this creates a Unix domain socket. On Windows, this creates a named pipe.
func NewPipeTransport(path string) (*PipeTransport, error) {
listener, err := newPipeListener(path)
if err != nil {
return nil, err
}
return &PipeTransport{listener: listener}, nil
}
// Accept implements Transport.
func (t *PipeTransport) Accept() (io.ReadWriteCloser, error) {
return t.listener.Accept()
}
// Close implements Transport.
func (t *PipeTransport) Close() error {
return t.listener.Close()
}
// Path returns the path of the pipe/socket.
func (t *PipeTransport) Path() string {
return t.listener.Addr().String()
}
// StdioTransport wraps stdin/stdout as a single connection transport.
// It only accepts one connection.
type StdioTransport struct {
stdin io.ReadCloser
stdout io.WriteCloser
used bool
}
// NewStdioTransport creates a transport using the given stdin/stdout.
func NewStdioTransport(stdin io.ReadCloser, stdout io.WriteCloser) *StdioTransport {
return &StdioTransport{
stdin: stdin,
stdout: stdout,
}
}
// Accept implements Transport.
func (t *StdioTransport) Accept() (io.ReadWriteCloser, error) {
if t.used {
return nil, io.EOF
}
t.used = true
return &stdioConn{
Reader: t.stdin,
Writer: t.stdout,
stdin: t.stdin,
stdout: t.stdout,
}, nil
}
// Close implements Transport.
func (t *StdioTransport) Close() error {
return nil
}
type stdioConn struct {
io.Reader
io.Writer
stdin io.ReadCloser
stdout io.WriteCloser
}
func (c *stdioConn) Close() error {
err1 := c.stdin.Close()
err2 := c.stdout.Close()
if err1 != nil {
return err1
}
return err2
}

View File

@@ -0,0 +1,22 @@
//go:build !windows
package api
import (
"net"
"os"
"path"
)
// newPipeListener creates a Unix domain socket listener.
func newPipeListener(path string) (net.Listener, error) {
// Remove any existing socket file
_ = os.Remove(path) //nolint:forbidigo
return net.Listen("unix", path)
}
// GeneratePipePath returns a platform-appropriate pipe path for the given name.
func GeneratePipePath(name string) string {
//nolint:forbidigo
return path.Join(os.TempDir(), name)
}

View File

@@ -0,0 +1,19 @@
//go:build windows
package api
import (
"net"
"github.com/Microsoft/go-winio"
)
// newPipeListener creates a Windows named pipe listener.
func newPipeListener(path string) (net.Listener, error) {
return winio.ListenPipe(path, nil)
}
// GeneratePipePath returns a platform-appropriate pipe path for the given name.
func GeneratePipePath(name string) string {
return `\\.\pipe\` + name
}