vendor tsgo
This commit is contained in:
226
tools/tsgo/internal/api/callbackfs.go
Normal file
226
tools/tsgo/internal/api/callbackfs.go
Normal 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)
|
||||
}
|
||||
46
tools/tsgo/internal/api/conn.go
Normal file
46
tools/tsgo/internal/api/conn.go
Normal 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
|
||||
}
|
||||
231
tools/tsgo/internal/api/conn_async.go
Normal file
231
tools/tsgo/internal/api/conn_async.go
Normal 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)
|
||||
}
|
||||
208
tools/tsgo/internal/api/conn_sync.go
Normal file
208
tools/tsgo/internal/api/conn_sync.go
Normal 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)
|
||||
}
|
||||
381
tools/tsgo/internal/api/encoder/decoder.go
Normal file
381
tools/tsgo/internal/api/encoder/decoder.go
Normal 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
|
||||
}
|
||||
1140
tools/tsgo/internal/api/encoder/decoder_generated.go
Normal file
1140
tools/tsgo/internal/api/encoder/decoder_generated.go
Normal file
File diff suppressed because it is too large
Load Diff
450
tools/tsgo/internal/api/encoder/decoder_test.go
Normal file
450
tools/tsgo/internal/api/encoder/decoder_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
844
tools/tsgo/internal/api/encoder/encoder.go
Normal file
844
tools/tsgo/internal/api/encoder/encoder.go
Normal 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))
|
||||
}
|
||||
707
tools/tsgo/internal/api/encoder/encoder_generated.go
Normal file
707
tools/tsgo/internal/api/encoder/encoder_generated.go
Normal 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
|
||||
}
|
||||
161
tools/tsgo/internal/api/encoder/encoder_test.go
Normal file
161
tools/tsgo/internal/api/encoder/encoder_test.go
Normal 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()
|
||||
}
|
||||
68
tools/tsgo/internal/api/encoder/stringtable.go
Normal file
68
tools/tsgo/internal/api/encoder/stringtable.go
Normal 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()
|
||||
}
|
||||
14
tools/tsgo/internal/api/encoder/testmain_test.go
Normal file
14
tools/tsgo/internal/api/encoder/testmain_test.go
Normal 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()
|
||||
}
|
||||
1225
tools/tsgo/internal/api/proto.go
Normal file
1225
tools/tsgo/internal/api/proto.go
Normal file
File diff suppressed because it is too large
Load Diff
83
tools/tsgo/internal/api/proto_test.go
Normal file
83
tools/tsgo/internal/api/proto_test.go
Normal 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))
|
||||
}
|
||||
22
tools/tsgo/internal/api/protocol.go
Normal file
22
tools/tsgo/internal/api/protocol.go
Normal 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
|
||||
}
|
||||
96
tools/tsgo/internal/api/protocol_jsonrpc.go
Normal file
96
tools/tsgo/internal/api/protocol_jsonrpc.go
Normal 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)
|
||||
}
|
||||
280
tools/tsgo/internal/api/protocol_msgpack.go
Normal file
280
tools/tsgo/internal/api/protocol_msgpack.go
Normal 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
|
||||
124
tools/tsgo/internal/api/server.go
Normal file
124
tools/tsgo/internal/api/server.go
Normal 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)
|
||||
}
|
||||
3210
tools/tsgo/internal/api/session.go
Normal file
3210
tools/tsgo/internal/api/session.go
Normal file
File diff suppressed because it is too large
Load Diff
255
tools/tsgo/internal/api/session_apistate_test.go
Normal file
255
tools/tsgo/internal/api/session_apistate_test.go
Normal 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)
|
||||
}
|
||||
141
tools/tsgo/internal/api/session_completion_test.go
Normal file
141
tools/tsgo/internal/api/session_completion_test.go
Normal 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")
|
||||
}
|
||||
30
tools/tsgo/internal/api/stringer_generated.go
Normal file
30
tools/tsgo/internal/api/stringer_generated.go
Normal 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]]
|
||||
}
|
||||
136
tools/tsgo/internal/api/timing.go
Normal file
136
tools/tsgo/internal/api/timing.go
Normal 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)
|
||||
}
|
||||
95
tools/tsgo/internal/api/timing_test.go
Normal file
95
tools/tsgo/internal/api/timing_test.go
Normal 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)
|
||||
}
|
||||
95
tools/tsgo/internal/api/transport.go
Normal file
95
tools/tsgo/internal/api/transport.go
Normal 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
|
||||
}
|
||||
22
tools/tsgo/internal/api/transport_unix.go
Normal file
22
tools/tsgo/internal/api/transport_unix.go
Normal 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)
|
||||
}
|
||||
19
tools/tsgo/internal/api/transport_windows.go
Normal file
19
tools/tsgo/internal/api/transport_windows.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user