764 lines
24 KiB
Go
764 lines
24 KiB
Go
package tracing
|
|
|
|
import (
|
|
"fmt"
|
|
"maps"
|
|
"math"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/microsoft/typescript-go/internal/ast"
|
|
"github.com/microsoft/typescript-go/internal/json"
|
|
"github.com/microsoft/typescript-go/internal/scanner"
|
|
"github.com/microsoft/typescript-go/internal/tspath"
|
|
"github.com/microsoft/typescript-go/internal/vfs"
|
|
"github.com/zeebo/xxh3"
|
|
)
|
|
|
|
// Tracer is an interface for recording types during type checking.
|
|
// Each checker should have its own Tracer instance to avoid sharing types between checkers.
|
|
type Tracer interface {
|
|
// RecordType records a type for later dumping.
|
|
RecordType(t TracedType)
|
|
// DumpTypes writes all recorded types to disk.
|
|
DumpTypes() error
|
|
}
|
|
|
|
// TracedType is an interface that represents a type that can be traced.
|
|
// This allows the tracing package to work with types from the checker package
|
|
// without creating a circular dependency.
|
|
type TracedType interface {
|
|
Id() uint32
|
|
FormatFlags() []string
|
|
IsConditional() bool
|
|
Symbol() *ast.Symbol
|
|
AliasSymbol() *ast.Symbol
|
|
AliasTypeArguments() []TracedType
|
|
|
|
// Type-specific data accessors
|
|
IntrinsicName() string
|
|
UnionTypes() []TracedType
|
|
IntersectionTypes() []TracedType
|
|
IndexType() TracedType
|
|
IndexedAccessObjectType() TracedType
|
|
IndexedAccessIndexType() TracedType
|
|
ConditionalCheckType() TracedType
|
|
ConditionalExtendsType() TracedType
|
|
ConditionalTrueType() TracedType
|
|
ConditionalFalseType() TracedType
|
|
SubstitutionBaseType() TracedType
|
|
SubstitutionConstraintType() TracedType
|
|
ReferenceTarget() TracedType
|
|
ReferenceTypeArguments() []TracedType
|
|
ReferenceNode() *ast.Node
|
|
ReverseMappedSourceType() TracedType
|
|
ReverseMappedMappedType() TracedType
|
|
ReverseMappedConstraintType() TracedType
|
|
EvolvingArrayElementType() TracedType
|
|
EvolvingArrayFinalType() TracedType
|
|
IsTuple() bool
|
|
Pattern() *ast.Node
|
|
RecursionIdentity() any
|
|
|
|
// Display is an optional string representation of the type
|
|
Display() string
|
|
}
|
|
|
|
// TraceRecord represents metadata about a single trace file
|
|
type TraceRecord struct {
|
|
ConfigFilePath string `json:"configFilePath,omitzero"`
|
|
TracePath string `json:"tracePath,omitzero"`
|
|
TypesPath string `json:"typesPath,omitzero"`
|
|
CheckerID int `json:"checkerId"`
|
|
}
|
|
|
|
type traceEvent struct {
|
|
PID int `json:"pid"`
|
|
TID int `json:"tid"`
|
|
PH string `json:"ph"`
|
|
Cat string `json:"cat"`
|
|
TS float64 `json:"ts"`
|
|
Name string `json:"name,omitzero"`
|
|
S string `json:"s,omitzero"` // scope, only set for instant events ("g" = global)
|
|
Dur *float64 `json:"dur,omitzero"`
|
|
Args map[string]any `json:"args,omitzero"`
|
|
}
|
|
|
|
// sampleInterval matches TypeScript's 10ms sampling interval.
|
|
// Events with separateBeginAndEnd=false are only recorded if their
|
|
// duration crosses a 10ms sampling boundary.
|
|
const sampleInterval = 10 * time.Millisecond
|
|
|
|
const traceFileName = "trace.json"
|
|
|
|
const (
|
|
mainThreadID = 1
|
|
firstSyntheticThreadID = 2
|
|
firstFileThreadID = 1_000_000
|
|
fileThreadIDHashRange = 1_000_000_000
|
|
)
|
|
|
|
var traceThreadArgKeys = [...]string{"path", "fileName", "containingFileName", "jsFilePath", "declarationFilePath"}
|
|
|
|
// flushThreshold is the size at which buffered trace content is flushed to disk
|
|
// via AppendFile. Keeps peak memory bounded for long-running compilations while
|
|
// avoiding a syscall per event.
|
|
const flushThreshold = 256 * 1024
|
|
|
|
// Tracing manages the overall tracing session including all checkers
|
|
type Tracing struct {
|
|
fs vfs.FS
|
|
traceDir string
|
|
tracePath string
|
|
configFilePath string
|
|
legend []TraceRecord
|
|
tracers []*typeTracer
|
|
traceContent strings.Builder
|
|
traceStarted atomic.Bool
|
|
threadIDs map[traceThreadKey]int
|
|
threadKeys map[int]traceThreadKey
|
|
metadataTS float64
|
|
deterministic bool // when true, use monotonic counter instead of real time
|
|
timestampCounter uint64 // only used in deterministic mode
|
|
startTime time.Time
|
|
mu sync.Mutex
|
|
// flushErr holds the first error encountered while appending the trace buffer
|
|
// to disk. Once set, subsequent flushes become no-ops and the error is
|
|
// surfaced from StopTracing so that transient I/O failures (disk full,
|
|
// permission denied, etc.) don't crash the compiler.
|
|
flushErr error
|
|
}
|
|
|
|
// Phase represents a tracing phase
|
|
type Phase string
|
|
|
|
const (
|
|
PhaseParse Phase = "parse"
|
|
PhaseProgram Phase = "program"
|
|
PhaseBind Phase = "bind"
|
|
PhaseCheck Phase = "check"
|
|
PhaseCheckTypes Phase = "checkTypes"
|
|
PhaseEmit Phase = "emit"
|
|
PhaseSession Phase = "session"
|
|
)
|
|
|
|
// StartTracing creates a new tracing session.
|
|
// When deterministic is true, timestamps use a monotonic counter instead of
|
|
// real wall-clock time, producing stable output for test baselines.
|
|
func StartTracing(fs vfs.FS, traceDir string, configFilePath string, deterministic bool) (*Tracing, error) {
|
|
tr := &Tracing{
|
|
fs: fs,
|
|
traceDir: traceDir,
|
|
tracePath: tspath.CombinePaths(traceDir, traceFileName),
|
|
configFilePath: configFilePath,
|
|
legend: []TraceRecord{},
|
|
tracers: []*typeTracer{},
|
|
threadIDs: make(map[traceThreadKey]int),
|
|
threadKeys: make(map[int]traceThreadKey),
|
|
deterministic: deterministic,
|
|
startTime: time.Now(),
|
|
}
|
|
tr.traceStarted.Store(true)
|
|
|
|
// Write the trace file header with metadata events
|
|
tr.traceContent.WriteString("[\n")
|
|
|
|
// Write metadata events (matching TypeScript's format)
|
|
metaTs := tr.timestamp()
|
|
tr.metadataTS = metaTs
|
|
tr.writeEvent(traceEvent{PID: 1, TID: mainThreadID, PH: "M", Cat: "__metadata", TS: metaTs, Name: "process_name", Args: map[string]any{"name": "tsgo"}})
|
|
tr.traceContent.WriteString(",\n")
|
|
tr.writeEvent(traceEvent{PID: 1, TID: mainThreadID, PH: "M", Cat: "__metadata", TS: metaTs, Name: "thread_name", Args: map[string]any{"name": "Main"}})
|
|
tr.traceContent.WriteString(",\n")
|
|
tr.writeEvent(traceEvent{PID: 1, TID: mainThreadID, PH: "M", Cat: "disabled-by-default-devtools.timeline", TS: metaTs, Name: "TracingStartedInBrowser"})
|
|
|
|
// Truncate any existing trace file with the header so subsequent AppendFile
|
|
// calls extend a clean file.
|
|
if err := tr.fs.WriteFile(tr.tracePath, tr.traceContent.String()); err != nil {
|
|
return nil, fmt.Errorf("failed to write trace file header: %w", err)
|
|
}
|
|
tr.traceContent.Reset()
|
|
|
|
return tr, nil
|
|
}
|
|
|
|
// timestamp returns the current timestamp in microseconds.
|
|
// In deterministic mode it returns a monotonically increasing counter;
|
|
// otherwise it returns the real elapsed wall-clock time since tracing started,
|
|
// matching TypeScript's 1000 * timestamp() (microseconds).
|
|
func (tr *Tracing) timestamp() float64 {
|
|
if tr.deterministic {
|
|
tr.timestampCounter++
|
|
return float64(tr.timestampCounter)
|
|
}
|
|
return float64(time.Since(tr.startTime).Nanoseconds()) / 1000.0
|
|
}
|
|
|
|
func writeEventTo(buf *strings.Builder, event traceEvent) {
|
|
if err := json.MarshalWrite(buf, event, json.Deterministic(true)); err != nil {
|
|
panic(fmt.Sprintf("failed to marshal trace event: %v", err))
|
|
}
|
|
}
|
|
|
|
func (tr *Tracing) writeEvent(event traceEvent) {
|
|
writeEventTo(&tr.traceContent, event)
|
|
}
|
|
|
|
// maybeFlushLocked appends the buffered trace content to disk if it has grown
|
|
// past the flush threshold. Caller must hold tr.mu. If a previous flush failed,
|
|
// or this flush fails, the error is recorded in tr.flushErr and subsequent
|
|
// writes become no-ops; the error is surfaced from StopTracing.
|
|
func (tr *Tracing) maybeFlushLocked() {
|
|
if tr.flushErr != nil {
|
|
tr.traceContent.Reset()
|
|
return
|
|
}
|
|
if tr.traceContent.Len() < flushThreshold {
|
|
return
|
|
}
|
|
if err := tr.fs.AppendFile(tr.tracePath, tr.traceContent.String()); err != nil {
|
|
tr.flushErr = fmt.Errorf("failed to flush trace file: %w", err)
|
|
}
|
|
tr.traceContent.Reset()
|
|
}
|
|
|
|
// Instant records an instant event in the trace.
|
|
// Safe to call on nil receiver.
|
|
func (tr *Tracing) Instant(phase Phase, name string, args map[string]any) {
|
|
if tr == nil || !tr.traceStarted.Load() {
|
|
return
|
|
}
|
|
|
|
tr.mu.Lock()
|
|
defer tr.mu.Unlock()
|
|
|
|
// Re-check under the lock: StopTracing may have run between the load above
|
|
// and acquiring the lock. Once stopped, further writes would land in a buffer
|
|
// that has already been flushed and the closing "]" written.
|
|
if !tr.traceStarted.Load() {
|
|
return
|
|
}
|
|
|
|
ts := tr.timestamp()
|
|
tid := tr.threadIDLocked(args)
|
|
tr.traceContent.WriteString(",\n")
|
|
tr.writeEvent(traceEvent{PID: 1, TID: tid, PH: "I", Cat: string(phase), TS: ts, Name: name, S: "g", Args: args})
|
|
tr.maybeFlushLocked()
|
|
}
|
|
|
|
// Push starts a trace event block on the shared trace buffer.
|
|
// Safe to call on nil receiver. Safe to call from multiple goroutines.
|
|
//
|
|
// When separateBeginAndEnd is true, a "B" (begin) event is written immediately and
|
|
// the returned function writes a matching "E" (end) event. This is used for events
|
|
// that must always appear in the trace (e.g. checkSourceFile, createProgram, emit).
|
|
//
|
|
// When separateBeginAndEnd is false (the default in TypeScript), the event is only
|
|
// recorded if its duration crosses a 10ms sampling boundary, matching TypeScript's
|
|
// behavior of sampling short-lived events to avoid trace bloat.
|
|
//
|
|
// Returns a function that should be called (typically deferred) to end the event.
|
|
// Each returned function is self-contained and does not depend on a shared stack,
|
|
// making it safe for concurrent use across goroutines.
|
|
func (tr *Tracing) Push(phase Phase, name string, args map[string]any, separateBeginAndEnd bool) func() {
|
|
if tr == nil || !tr.traceStarted.Load() {
|
|
return func() {}
|
|
}
|
|
|
|
if separateBeginAndEnd {
|
|
tr.mu.Lock()
|
|
if !tr.traceStarted.Load() {
|
|
tr.mu.Unlock()
|
|
return func() {}
|
|
}
|
|
ts := tr.timestamp()
|
|
tid := tr.threadIDLocked(args)
|
|
tr.traceContent.WriteString(",\n")
|
|
tr.writeEvent(traceEvent{PID: 1, TID: tid, PH: "B", Cat: string(phase), TS: ts, Name: name, Args: args})
|
|
tr.maybeFlushLocked()
|
|
tr.mu.Unlock()
|
|
|
|
return func() {
|
|
tr.mu.Lock()
|
|
defer tr.mu.Unlock()
|
|
if !tr.traceStarted.Load() {
|
|
return
|
|
}
|
|
endTs := tr.timestamp()
|
|
tr.traceContent.WriteString(",\n")
|
|
tr.writeEvent(traceEvent{PID: 1, TID: tid, PH: "E", Cat: string(phase), TS: endTs, Name: name, Args: args})
|
|
tr.maybeFlushLocked()
|
|
}
|
|
}
|
|
|
|
// Sampled event: only record if duration crosses a sampling boundary.
|
|
// In deterministic mode, sampled events are skipped entirely to avoid flaky baselines,
|
|
// so avoid the cost of cloning args / capturing the start time.
|
|
if tr.deterministic {
|
|
return func() {}
|
|
}
|
|
startTime := time.Now()
|
|
args = maps.Clone(args)
|
|
return func() {
|
|
dur := float64(time.Since(startTime).Nanoseconds()) / 1000.0
|
|
startMicros := float64(startTime.Sub(tr.startTime).Nanoseconds()) / 1000.0
|
|
intervalMicros := float64(sampleInterval.Nanoseconds()) / 1000.0
|
|
if intervalMicros-math.Mod(startMicros, intervalMicros) > dur {
|
|
return
|
|
}
|
|
tr.mu.Lock()
|
|
defer tr.mu.Unlock()
|
|
if !tr.traceStarted.Load() {
|
|
return
|
|
}
|
|
tid := tr.threadIDLocked(args)
|
|
tr.traceContent.WriteString(",\n")
|
|
tr.writeEvent(traceEvent{PID: 1, TID: tid, PH: "X", Cat: string(phase), TS: startMicros, Name: name, Dur: &dur, Args: args})
|
|
tr.maybeFlushLocked()
|
|
}
|
|
}
|
|
|
|
func (tr *Tracing) threadIDLocked(args map[string]any) int {
|
|
key, ok := traceThreadKeyFromArgs(args)
|
|
if !ok {
|
|
return mainThreadID
|
|
}
|
|
|
|
if tid, ok := tr.threadIDs[key]; ok {
|
|
return tid
|
|
}
|
|
|
|
tid := key.defaultThreadID()
|
|
for {
|
|
if existingKey, ok := tr.threadKeys[tid]; !ok || existingKey == key {
|
|
break
|
|
}
|
|
tid++
|
|
}
|
|
tr.threadIDs[key] = tid
|
|
tr.threadKeys[tid] = key
|
|
tr.writeThreadNameEventLocked(tid, key.displayName())
|
|
return tid
|
|
}
|
|
|
|
func (tr *Tracing) writeThreadNameEventLocked(tid int, name string) {
|
|
tr.traceContent.WriteString(",\n")
|
|
tr.writeEvent(traceEvent{PID: 1, TID: tid, PH: "M", Cat: "__metadata", TS: tr.metadataTS, Name: "thread_name", Args: map[string]any{"name": name}})
|
|
}
|
|
|
|
type traceThreadKind string
|
|
|
|
const (
|
|
traceThreadKindChecker traceThreadKind = "checker"
|
|
traceThreadKindFile traceThreadKind = "file"
|
|
)
|
|
|
|
type traceThreadKey struct {
|
|
kind traceThreadKind
|
|
text string
|
|
index int
|
|
hasIndex bool
|
|
}
|
|
|
|
func traceThreadKeyFromArgs(args map[string]any) (traceThreadKey, bool) {
|
|
if len(args) == 0 {
|
|
return traceThreadKey{}, false
|
|
}
|
|
|
|
if checkerID, ok := args["checkerId"].(int); ok {
|
|
return traceThreadKey{kind: traceThreadKindChecker, index: checkerID, hasIndex: true}, true
|
|
}
|
|
|
|
for _, key := range traceThreadArgKeys {
|
|
if value, ok := args[key]; ok {
|
|
if path, ok := value.(string); ok && path != "" {
|
|
return traceThreadKey{kind: traceThreadKindFile, text: path}, true
|
|
}
|
|
}
|
|
}
|
|
|
|
return traceThreadKey{}, false
|
|
}
|
|
|
|
func (key traceThreadKey) defaultThreadID() int {
|
|
if key.kind == traceThreadKindChecker && key.hasIndex && key.index >= 0 {
|
|
return firstSyntheticThreadID + key.index
|
|
}
|
|
return stableTraceThreadID(key)
|
|
}
|
|
|
|
func (key traceThreadKey) displayName() string {
|
|
if key.hasIndex {
|
|
return string(key.kind) + ":" + strconv.Itoa(key.index)
|
|
}
|
|
return string(key.kind) + ":" + key.text
|
|
}
|
|
|
|
func stableTraceThreadID(key traceThreadKey) int {
|
|
hash := xxh3.New()
|
|
_, _ = hash.WriteString(string(key.kind))
|
|
_, _ = hash.WriteString(":")
|
|
if key.hasIndex {
|
|
_, _ = hash.WriteString(strconv.Itoa(key.index))
|
|
} else {
|
|
_, _ = hash.WriteString(key.text)
|
|
}
|
|
return firstFileThreadID + int(hash.Sum64()%fileThreadIDHashRange)
|
|
}
|
|
|
|
// NewTypeTracer creates a new tracer for a specific checker.
|
|
// The checkerIndex is used to create unique filenames for each checker's output.
|
|
func (tr *Tracing) NewTypeTracer(checkerIndex int) Tracer {
|
|
tr.mu.Lock()
|
|
defer tr.mu.Unlock()
|
|
|
|
typesPath := tspath.CombinePaths(tr.traceDir, fmt.Sprintf("types_%d.json", checkerIndex))
|
|
tracer := &typeTracer{
|
|
fs: tr.fs,
|
|
checkerIndex: checkerIndex,
|
|
typesPath: typesPath,
|
|
types: []TracedType{},
|
|
}
|
|
tr.tracers = append(tr.tracers, tracer)
|
|
tr.legend = append(tr.legend, TraceRecord{
|
|
ConfigFilePath: tr.configFilePath,
|
|
TracePath: tr.tracePath,
|
|
TypesPath: typesPath,
|
|
CheckerID: checkerIndex,
|
|
})
|
|
return tracer
|
|
}
|
|
|
|
// StopTracing finalizes the tracing session and writes all output files
|
|
func (tr *Tracing) StopTracing() error {
|
|
// Dump types from all tracers BEFORE acquiring the lock, because
|
|
// DumpTypes → buildTypeDescriptor → Display() → TypeToString can
|
|
// re-enter the checker which calls Push/Pop (which need tr.mu).
|
|
for _, tracer := range tr.tracers {
|
|
if err := tracer.DumpTypes(); err != nil {
|
|
return fmt.Errorf("failed to dump types for checker %d: %w", tracer.checkerIndex, err)
|
|
}
|
|
}
|
|
|
|
tr.mu.Lock()
|
|
defer tr.mu.Unlock()
|
|
|
|
// Close the trace file(s)
|
|
if tr.traceStarted.Load() {
|
|
// Surface any buffered flush failure before attempting the final write.
|
|
if tr.flushErr != nil {
|
|
tr.traceContent.Reset()
|
|
tr.traceStarted.Store(false)
|
|
return tr.flushErr
|
|
}
|
|
// Flush any remaining buffered content and close the JSON array.
|
|
if err := tr.fs.AppendFile(tr.tracePath, tr.traceContent.String()+"\n]\n"); err != nil {
|
|
return fmt.Errorf("failed to write trace file: %w", err)
|
|
}
|
|
tr.traceContent.Reset()
|
|
tr.traceStarted.Store(false)
|
|
}
|
|
|
|
// Sort legend entries by typesPath for deterministic output
|
|
slices.SortFunc(tr.legend, func(a, b TraceRecord) int {
|
|
return strings.Compare(a.TypesPath, b.TypesPath)
|
|
})
|
|
|
|
// Write the legend file
|
|
legendPath := tspath.CombinePaths(tr.traceDir, "legend.json")
|
|
legendData, err := json.MarshalIndent(tr.legend, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal legend file: %w", err)
|
|
}
|
|
if err := tr.fs.WriteFile(legendPath, string(legendData)); err != nil {
|
|
return fmt.Errorf("failed to write legend file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// typeTracer is the per-checker tracer implementation
|
|
type typeTracer struct {
|
|
fs vfs.FS
|
|
checkerIndex int
|
|
typesPath string
|
|
types []TracedType
|
|
mu sync.Mutex
|
|
}
|
|
|
|
func (t *typeTracer) RecordType(typ TracedType) {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
t.types = append(t.types, typ)
|
|
}
|
|
|
|
func (t *typeTracer) DumpTypes() error {
|
|
// Copy the types slice under lock, then release so Display() calls during
|
|
// buildTypeDescriptor don't deadlock when they create new types
|
|
t.mu.Lock()
|
|
types := slices.Clone(t.types)
|
|
t.mu.Unlock()
|
|
|
|
if len(types) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var sb strings.Builder
|
|
// Write opening bracket (no newline so type ID matches line number)
|
|
sb.WriteString("[")
|
|
|
|
recursionIdentityMap := make(map[any]int)
|
|
|
|
for i, typ := range types {
|
|
descriptor := t.buildTypeDescriptor(typ, recursionIdentityMap)
|
|
|
|
if err := json.MarshalWrite(&sb, descriptor); err != nil {
|
|
return fmt.Errorf("failed to marshal type %d: %w", typ.Id(), err)
|
|
}
|
|
|
|
if i < len(types)-1 {
|
|
sb.WriteString(",\n")
|
|
}
|
|
}
|
|
|
|
sb.WriteString("]\n")
|
|
|
|
return t.fs.WriteFile(t.typesPath, sb.String())
|
|
}
|
|
|
|
// TypeDescriptor represents a type in the output JSON
|
|
type TypeDescriptor struct {
|
|
ID uint32 `json:"id"`
|
|
IntrinsicName string `json:"intrinsicName,omitzero"`
|
|
SymbolName string `json:"symbolName,omitzero"`
|
|
RecursionID *int `json:"recursionId,omitzero"`
|
|
IsTuple bool `json:"isTuple,omitzero"`
|
|
UnionTypes []uint32 `json:"unionTypes,omitzero"`
|
|
IntersectionTypes []uint32 `json:"intersectionTypes,omitzero"`
|
|
AliasTypeArguments []uint32 `json:"aliasTypeArguments,omitzero"`
|
|
KeyofType *uint32 `json:"keyofType,omitzero"`
|
|
IndexedAccessObjectType *uint32 `json:"indexedAccessObjectType,omitzero"`
|
|
IndexedAccessIndexType *uint32 `json:"indexedAccessIndexType,omitzero"`
|
|
ConditionalCheckType *uint32 `json:"conditionalCheckType,omitzero"`
|
|
ConditionalExtendsType *uint32 `json:"conditionalExtendsType,omitzero"`
|
|
// ConditionalTrueType and ConditionalFalseType are *int32 (not *uint32) because
|
|
// unresolved conditional branches are serialized as -1, matching TypeScript's behavior.
|
|
ConditionalTrueType *int32 `json:"conditionalTrueType,omitzero"`
|
|
ConditionalFalseType *int32 `json:"conditionalFalseType,omitzero"`
|
|
SubstitutionBaseType *uint32 `json:"substitutionBaseType,omitzero"`
|
|
ConstraintType *uint32 `json:"constraintType,omitzero"`
|
|
InstantiatedType *uint32 `json:"instantiatedType,omitzero"`
|
|
TypeArguments []uint32 `json:"typeArguments,omitzero"`
|
|
ReferenceLocation *Location `json:"referenceLocation,omitzero"`
|
|
ReverseMappedSourceType *uint32 `json:"reverseMappedSourceType,omitzero"`
|
|
ReverseMappedMappedType *uint32 `json:"reverseMappedMappedType,omitzero"`
|
|
ReverseMappedConstraintType *uint32 `json:"reverseMappedConstraintType,omitzero"`
|
|
EvolvingArrayElementType *uint32 `json:"evolvingArrayElementType,omitzero"`
|
|
EvolvingArrayFinalType *uint32 `json:"evolvingArrayFinalType,omitzero"`
|
|
DestructuringPattern *Location `json:"destructuringPattern,omitzero"`
|
|
FirstDeclaration *Location `json:"firstDeclaration,omitzero"`
|
|
Flags []string `json:"flags"`
|
|
Display string `json:"display,omitzero"`
|
|
}
|
|
|
|
// Location represents a source code location
|
|
type Location struct {
|
|
Path string `json:"path"`
|
|
Start *LineAndChar `json:"start,omitzero"`
|
|
End *LineAndChar `json:"end,omitzero"`
|
|
}
|
|
|
|
// LineAndChar represents a line and character position (1-indexed)
|
|
type LineAndChar struct {
|
|
Line int `json:"line"`
|
|
Character int `json:"character"`
|
|
}
|
|
|
|
func (t *typeTracer) buildTypeDescriptor(typ TracedType, recursionIdentityMap map[any]int) TypeDescriptor {
|
|
symbol := typ.Symbol()
|
|
aliasSymbol := typ.AliasSymbol()
|
|
|
|
desc := TypeDescriptor{
|
|
ID: typ.Id(),
|
|
Flags: typ.FormatFlags(),
|
|
}
|
|
|
|
// Assign a unique integer token per recursion identity, matching TypeScript's behavior.
|
|
// This lets trace analysis tools detect which types share the same recursion identity.
|
|
if identity := typ.RecursionIdentity(); identity != nil {
|
|
token, ok := recursionIdentityMap[identity]
|
|
if !ok {
|
|
token = len(recursionIdentityMap)
|
|
recursionIdentityMap[identity] = token
|
|
}
|
|
desc.RecursionID = &token
|
|
}
|
|
|
|
// Intrinsic name
|
|
if name := typ.IntrinsicName(); name != "" {
|
|
desc.IntrinsicName = name
|
|
}
|
|
|
|
// Symbol name - escape the internal symbol name prefix for valid JSON
|
|
if sym := aliasSymbol; sym != nil {
|
|
desc.SymbolName = ast.EscapeAllInternalSymbolNames(sym.Name)
|
|
} else if symbol != nil {
|
|
desc.SymbolName = ast.EscapeAllInternalSymbolNames(symbol.Name)
|
|
}
|
|
|
|
// Tuple flag
|
|
if typ.IsTuple() {
|
|
desc.IsTuple = true
|
|
}
|
|
|
|
// Union types
|
|
if types := typ.UnionTypes(); len(types) > 0 {
|
|
desc.UnionTypes = mapTypeIds(types)
|
|
}
|
|
|
|
// Intersection types
|
|
if types := typ.IntersectionTypes(); len(types) > 0 {
|
|
desc.IntersectionTypes = mapTypeIds(types)
|
|
}
|
|
|
|
// Alias type arguments
|
|
if args := typ.AliasTypeArguments(); len(args) > 0 {
|
|
desc.AliasTypeArguments = mapTypeIds(args)
|
|
}
|
|
|
|
// Index type (keyof)
|
|
if indexType := typ.IndexType(); indexType != nil {
|
|
desc.KeyofType = new(indexType.Id())
|
|
}
|
|
|
|
// Indexed access type
|
|
if objType := typ.IndexedAccessObjectType(); objType != nil {
|
|
desc.IndexedAccessObjectType = new(objType.Id())
|
|
}
|
|
if idxType := typ.IndexedAccessIndexType(); idxType != nil {
|
|
desc.IndexedAccessIndexType = new(idxType.Id())
|
|
}
|
|
|
|
// Conditional type
|
|
if typ.IsConditional() {
|
|
if checkType := typ.ConditionalCheckType(); checkType != nil {
|
|
desc.ConditionalCheckType = new(checkType.Id())
|
|
}
|
|
if extendsType := typ.ConditionalExtendsType(); extendsType != nil {
|
|
desc.ConditionalExtendsType = new(extendsType.Id())
|
|
}
|
|
if trueType := typ.ConditionalTrueType(); trueType != nil {
|
|
desc.ConditionalTrueType = new(int32(trueType.Id()))
|
|
} else {
|
|
desc.ConditionalTrueType = new(int32(-1))
|
|
}
|
|
if falseType := typ.ConditionalFalseType(); falseType != nil {
|
|
desc.ConditionalFalseType = new(int32(falseType.Id()))
|
|
} else {
|
|
desc.ConditionalFalseType = new(int32(-1))
|
|
}
|
|
}
|
|
|
|
// Substitution type
|
|
if baseType := typ.SubstitutionBaseType(); baseType != nil {
|
|
desc.SubstitutionBaseType = new(baseType.Id())
|
|
}
|
|
if constraint := typ.SubstitutionConstraintType(); constraint != nil {
|
|
desc.ConstraintType = new(constraint.Id())
|
|
}
|
|
|
|
// Reference type
|
|
if target := typ.ReferenceTarget(); target != nil {
|
|
desc.InstantiatedType = new(target.Id())
|
|
}
|
|
if args := typ.ReferenceTypeArguments(); len(args) > 0 {
|
|
desc.TypeArguments = mapTypeIds(args)
|
|
}
|
|
if node := typ.ReferenceNode(); node != nil {
|
|
desc.ReferenceLocation = getLocation(node)
|
|
}
|
|
|
|
// Reverse mapped type
|
|
if sourceType := typ.ReverseMappedSourceType(); sourceType != nil {
|
|
desc.ReverseMappedSourceType = new(sourceType.Id())
|
|
}
|
|
if mappedType := typ.ReverseMappedMappedType(); mappedType != nil {
|
|
desc.ReverseMappedMappedType = new(mappedType.Id())
|
|
}
|
|
if constraintType := typ.ReverseMappedConstraintType(); constraintType != nil {
|
|
desc.ReverseMappedConstraintType = new(constraintType.Id())
|
|
}
|
|
|
|
// Evolving array type
|
|
if elemType := typ.EvolvingArrayElementType(); elemType != nil {
|
|
desc.EvolvingArrayElementType = new(elemType.Id())
|
|
}
|
|
if finalType := typ.EvolvingArrayFinalType(); finalType != nil {
|
|
desc.EvolvingArrayFinalType = new(finalType.Id())
|
|
}
|
|
|
|
// Pattern (destructuring)
|
|
if pattern := typ.Pattern(); pattern != nil {
|
|
desc.DestructuringPattern = getLocation(pattern)
|
|
}
|
|
|
|
// First declaration - prefer aliasSymbol, matching TypeScript's `aliasSymbol ?? symbol`
|
|
firstDeclSymbol := aliasSymbol
|
|
if firstDeclSymbol == nil {
|
|
firstDeclSymbol = symbol
|
|
}
|
|
if firstDeclSymbol != nil && len(firstDeclSymbol.Declarations) > 0 {
|
|
desc.FirstDeclaration = getLocation(firstDeclSymbol.Declarations[0])
|
|
}
|
|
|
|
// Display text
|
|
if display := typ.Display(); display != "" {
|
|
desc.Display = display
|
|
}
|
|
|
|
return desc
|
|
}
|
|
|
|
func mapTypeIds(types []TracedType) []uint32 {
|
|
if len(types) == 0 {
|
|
return nil
|
|
}
|
|
ids := make([]uint32, len(types))
|
|
for i, t := range types {
|
|
if t != nil {
|
|
ids[i] = t.Id()
|
|
}
|
|
}
|
|
return ids
|
|
}
|
|
|
|
func getLocation(node *ast.Node) *Location {
|
|
if node == nil {
|
|
return nil
|
|
}
|
|
file := ast.GetSourceFileOfNode(node)
|
|
if file == nil {
|
|
return nil
|
|
}
|
|
|
|
startPos := scanner.GetTokenPosOfNode(node, file, false)
|
|
startLine, startChar := scanner.GetECMALineAndUTF16CharacterOfPosition(file, startPos)
|
|
endLine, endChar := scanner.GetECMALineAndUTF16CharacterOfPosition(file, node.End())
|
|
|
|
return &Location{
|
|
Path: string(tspath.ToPath(file.FileName(), "", false)),
|
|
Start: &LineAndChar{
|
|
Line: startLine + 1,
|
|
Character: int(startChar) + 1,
|
|
},
|
|
End: &LineAndChar{
|
|
Line: endLine + 1,
|
|
Character: int(endChar) + 1,
|
|
},
|
|
}
|
|
}
|