package core import ( "iter" "maps" "math" "os" rtdebug "runtime/debug" "slices" "sort" "strconv" "strings" "sync" "unicode" "unicode/utf16" "unicode/utf8" "github.com/microsoft/typescript-go/internal/debug" "github.com/microsoft/typescript-go/internal/json" "github.com/microsoft/typescript-go/internal/stringutil" "github.com/microsoft/typescript-go/internal/tspath" ) func ApplyDebugStackLimit() { v := os.Getenv("TS_GO_DEBUG_STACK_LIMIT") //nolint:forbidigo if v == "" { return } n, err := strconv.Atoi(v) if err != nil || n <= 0 { return } rtdebug.SetMaxStack(n) } func Filter[T any](slice []T, f func(T) bool) []T { for i, value := range slice { if !f(value) { result := slices.Clone(slice[:i]) for i++; i < len(slice); i++ { value = slice[i] if f(value) { result = append(result, value) } } return result } } return slice } func FilterSeq[T any](slice []T, f func(T) bool) iter.Seq[T] { return func(yield func(T) bool) { for _, value := range slice { if f(value) { if !yield(value) { return } } } } } func FilterIndex[T any](slice []T, f func(T, int, []T) bool) []T { for i, value := range slice { if !f(value, i, slice) { result := slices.Clone(slice[:i]) for i++; i < len(slice); i++ { value = slice[i] if f(value, i, slice) { result = append(result, value) } } return result } } return slice } func Map[T, U any](slice []T, f func(T) U) []U { if slice == nil { return nil } result := make([]U, len(slice)) for i, value := range slice { result[i] = f(value) } return result } func TryMap[T, U any](slice []T, f func(T) (U, error)) ([]U, error) { if len(slice) == 0 { return nil, nil } result := make([]U, len(slice)) for i, value := range slice { mapped, err := f(value) if err != nil { return nil, err } result[i] = mapped } return result, nil } func MapIndex[T, U any](slice []T, f func(T, int) U) []U { if slice == nil { return nil } result := make([]U, len(slice)) for i, value := range slice { result[i] = f(value, i) } return result } func MapNonNil[T any, U comparable](slice []T, f func(T) U) []U { var result []U for _, value := range slice { mapped := f(value) if mapped != *new(U) { result = append(result, mapped) } } return result } func MapFiltered[T any, U any](slice []T, f func(T) (U, bool)) []U { var result []U for _, value := range slice { mapped, ok := f(value) if !ok { continue } result = append(result, mapped) } return result } func FlatMap[T any, U any](slice []T, f func(T) []U) []U { var result []U for _, value := range slice { mapped := f(value) if len(mapped) != 0 { result = append(result, mapped...) } } return result } func SameMap[T comparable](slice []T, f func(T) T) []T { for i, value := range slice { mapped := f(value) if mapped != value { result := make([]T, len(slice)) copy(result, slice[:i]) result[i] = mapped for j := i + 1; j < len(slice); j++ { result[j] = f(slice[j]) } return result } } return slice } func SameMapIndex[T comparable](slice []T, f func(T, int) T) []T { for i, value := range slice { mapped := f(value, i) if mapped != value { result := make([]T, len(slice)) copy(result, slice[:i]) result[i] = mapped for j := i + 1; j < len(slice); j++ { result[j] = f(slice[j], j) } return result } } return slice } func Same[T any](s1 []T, s2 []T) bool { if len(s1) == len(s2) { return len(s1) == 0 || &s1[0] == &s2[0] } return false } func Some[T any](slice []T, f func(T) bool) bool { for _, value := range slice { //nolint:modernize if f(value) { return true } } return false } func Every[T any](slice []T, f func(T) bool) bool { for _, value := range slice { if !f(value) { return false } } return true } func Or[T any](funcs ...func(T) bool) func(T) bool { return func(input T) bool { for _, f := range funcs { if f(input) { return true } } return false } } func Find[T any](slice []T, f func(T) bool) T { for _, value := range slice { if f(value) { return value } } return *new(T) } func FindLast[T any](slice []T, f func(T) bool) T { for i := len(slice) - 1; i >= 0; i-- { value := slice[i] if f(value) { return value } } return *new(T) } func FindIndex[T any](slice []T, f func(T) bool) int { for i, value := range slice { if f(value) { return i } } return -1 } func FindLastIndex[T any](slice []T, f func(T) bool) int { for i := len(slice) - 1; i >= 0; i-- { value := slice[i] if f(value) { return i } } return -1 } func FirstOrNil[T any](slice []T) T { if len(slice) != 0 { return slice[0] } return *new(T) } func LastOrNil[T any](slice []T) T { if len(slice) != 0 { return slice[len(slice)-1] } return *new(T) } func ElementOrNil[T any](slice []T, index int) T { if index < len(slice) { return slice[index] } return *new(T) } func FirstOrNilSeq[T any](seq iter.Seq[T]) T { if seq != nil { for value := range seq { return value } } return *new(T) } func FirstNonNil[T any, U comparable](slice []T, f func(T) U) U { for _, value := range slice { mapped := f(value) if mapped != *new(U) { return mapped } } return *new(U) } func FirstNonZero[T comparable](values ...T) T { var zero T for _, value := range values { if value != zero { return value } } return zero } func Concatenate[T any](s1 []T, s2 []T) []T { if len(s2) == 0 { return s1 } if len(s1) == 0 { return s2 } return slices.Concat(s1, s2) } func Splice[T any](s1 []T, start int, deleteCount int, items ...T) []T { if start < 0 { start = len(s1) + start } if start < 0 { start = 0 } if start > len(s1) { start = len(s1) } if deleteCount < 0 { deleteCount = 0 } end := min(start+max(deleteCount, 0), len(s1)) if start == end && len(items) == 0 { return s1 } return slices.Concat(s1[:start], items, s1[end:]) } func CountWhere[T any](slice []T, f func(T) bool) int { count := 0 for _, value := range slice { if f(value) { count++ } } return count } func ReplaceElement[T any](slice []T, i int, t T) []T { result := slices.Clone(slice) result[i] = t return result } func InsertSorted[T any](slice []T, element T, cmp func(T, T) int) []T { i, _ := slices.BinarySearchFunc(slice, element, cmp) return slices.Insert(slice, i, element) } // MinAllFunc returns all minimum elements from xs according to the comparison function cmp. func MinAllFunc[T any](xs []T, cmp func(a, b T) int) []T { if len(xs) == 0 { return nil } m := xs[0] mins := []T{m} for _, x := range xs[1:] { c := cmp(x, m) switch { case c < 0: m = x mins = mins[:0] mins = append(mins, x) case c == 0: mins = append(mins, x) } } return mins } func AppendIfUnique[T comparable](slice []T, element T) []T { if slices.Contains(slice, element) { return slice } return append(slice, element) } func Memoize[T any](create func() T) func() T { var value T return func() T { if create != nil { value = create() create = nil } return value } } // Returns whenTrue if b is true; otherwise, returns whenFalse. IfElse should only be used when branches are either // constant or precomputed as both branches will be evaluated regardless as to the value of b. func IfElse[T any](b bool, whenTrue T, whenFalse T) T { if b { return whenTrue } return whenFalse } // Returns value if value is not the zero value of T; Otherwise, returns defaultValue. OrElse should only be used when // defaultValue is constant or precomputed as its argument will be evaluated regardless as to the content of value. func OrElse[T comparable](value T, defaultValue T) T { if value != *new(T) { return value } return defaultValue } // Returns `a` if `a` is not `nil`; Otherwise, returns `b`. Coalesce is roughly analogous to `??` in JS, except that it // non-shortcutting, so it is advised to only use a constant or precomputed value for `b` func Coalesce[T *U, U any](a T, b T) T { if a == nil { return b } else { return a } } type ECMALineStarts []TextPos func ComputeECMALineStarts(text string) ECMALineStarts { result := make([]TextPos, 0, strings.Count(text, "\n")+1) return slices.AppendSeq(result, ComputeECMALineStartsSeq(text)) } func ComputeECMALineStartsSeq(text string) iter.Seq[TextPos] { return func(yield func(TextPos) bool) { textLen := TextPos(len(text)) var pos TextPos var lineStart TextPos for pos < textLen { b := text[pos] if b < utf8.RuneSelf { pos++ switch b { case '\r': if pos < textLen && text[pos] == '\n' { pos++ } fallthrough case '\n': if !yield(lineStart) { return } lineStart = pos } } else { ch, size := utf8.DecodeRuneInString(text[pos:]) pos += TextPos(size) if stringutil.IsLineBreak(ch) { if !yield(lineStart) { return } lineStart = pos } } } yield(lineStart) } } // PositionToLineAndByteOffset returns the 0-based line and byte offset from the // start of that line for the given byte position, using the provided line starts. // The byte offset is a raw UTF-8 byte offset from the line start, not a UTF-16 code unit count. func PositionToLineAndByteOffset(position int, lineStarts []TextPos) (line int, byteOffset int) { line = max(sort.Search(len(lineStarts), func(i int) bool { return int(lineStarts[i]) > position })-1, 0) return line, position - int(lineStarts[line]) } // UTF16Offset represents a character offset measured in UTF-16 code units. type UTF16Offset int // UTF16Len returns the number of UTF-16 code units needed to // represent the given UTF-8 encoded string. func UTF16Len(s string) UTF16Offset { // Fast path: scan for non-ASCII bytes. For ASCII-only strings, // each byte is one UTF-16 code unit, so we can return len(s) directly. for i := range len(s) { if s[i] >= utf8.RuneSelf { // Found non-ASCII; count the ASCII prefix, then decode the rest. n := UTF16Offset(i) for _, r := range s[i:] { n += UTF16Offset(utf16.RuneLen(r)) } return n } } return UTF16Offset(len(s)) } func Flatten[T any](array [][]T) []T { var result []T for _, subArray := range array { result = append(result, subArray...) } return result } func Must[T any](v T, err error) T { if err != nil { panic(err) } return v } // Extracts the first value of a multi-value return. func FirstResult[T1 any](t1 T1, _ ...any) T1 { return t1 } func StringifyJson(input any, prefix string, indent string) (string, error) { output, err := json.MarshalIndent(input, prefix, indent) return string(output), err } func GetScriptKindFromFileName(fileName string) ScriptKind { dotPos := strings.LastIndex(fileName, ".") if dotPos >= 0 { switch strings.ToLower(fileName[dotPos:]) { case tspath.ExtensionJs, tspath.ExtensionCjs, tspath.ExtensionMjs: return ScriptKindJS case tspath.ExtensionJsx: return ScriptKindJSX case tspath.ExtensionTs, tspath.ExtensionCts, tspath.ExtensionMts: return ScriptKindTS case tspath.ExtensionTsx: return ScriptKindTSX case tspath.ExtensionJson: return ScriptKindJSON } } return ScriptKindUnknown } // Given a name and a list of names that are *not* equal to the name, return a spelling suggestion if there is one that is close enough. // Names less than length 3 only check for case-insensitive equality. // // find the candidate with the smallest Levenshtein distance, // // except for candidates: // * With no name // * Whose length differs from the target name by more than 0.34 of the length of the name. // * Whose levenshtein distance is more than 0.4 of the length of the name // (0.4 allows 1 substitution/transposition for every 5 characters, // and 1 insertion/deletion at 3 characters) // // @internal func GetSpellingSuggestion[T any](name string, candidates iter.Seq[T], getName func(T) string, compare func(T, T) int) T { runeName := []rune(name) maximumLengthDifference := max(2, int(float64(len(runeName))*0.34)) bestDistance := math.Floor(float64(len(runeName))*0.4) + 0.9 // If the best result is worse than this, don't bother. buffers := levenshteinBuffersPool.Get().(*levenshteinBuffers) defer levenshteinBuffersPool.Put(buffers) var bestCandidate T hasBest := false for candidate := range candidates { candidateName := getName(candidate) maxLen := max(len(candidateName), len(runeName)) minLen := min(len(candidateName), len(runeName)) if candidateName != "" && maxLen-minLen <= maximumLengthDifference { if candidateName == name { continue } // Only consider candidates less than 3 characters long when they differ by case. // Otherwise, don't bother, since a user would usually notice differences of a 2-character name. if len(candidateName) < 3 && !strings.EqualFold(candidateName, name) { continue } distance := levenshteinWithMax(buffers, runeName, []rune(candidateName), bestDistance) if distance < 0 { continue } debug.Assert(distance <= bestDistance) // Else `levenshteinWithMax` should return undefined if distance < bestDistance { bestDistance = distance bestCandidate = candidate hasBest = true } else if !hasBest || compare(candidate, bestCandidate) < 0 { bestCandidate = candidate hasBest = true } } } return bestCandidate } func GetSpellingSuggestionForStrings(name string, candidates iter.Seq[string]) string { return GetSpellingSuggestion(name, candidates, Identity, strings.Compare) } type levenshteinBuffers struct { previous []float64 current []float64 } var levenshteinBuffersPool = sync.Pool{ New: func() any { return &levenshteinBuffers{} }, } func levenshteinWithMax(buffers *levenshteinBuffers, s1 []rune, s2 []rune, maxValue float64) float64 { bufferSize := len(s2) + 1 buffers.previous = slices.Grow(buffers.previous[:0], bufferSize)[:bufferSize] buffers.current = slices.Grow(buffers.current[:0], bufferSize)[:bufferSize] previous := buffers.previous current := buffers.current big := maxValue + 0.01 for i := range previous { previous[i] = float64(i) } for i := 1; i <= len(s1); i++ { c1 := s1[i-1] minJ := max(int(math.Ceil(float64(i)-maxValue)), 1) maxJ := min(int(math.Floor(maxValue+float64(i))), len(s2)) colMin := float64(i) current[0] = colMin for j := 1; j < minJ; j++ { current[j] = big } for j := minJ; j <= maxJ; j++ { var substitutionDistance, dist float64 if unicode.ToLower(s1[i-1]) == unicode.ToLower(s2[j-1]) { substitutionDistance = previous[j-1] + 0.1 } else { substitutionDistance = previous[j-1] + 2 } if c1 == s2[j-1] { dist = previous[j-1] } else { dist = math.Min(previous[j]+1, math.Min(current[j-1]+1, substitutionDistance)) } current[j] = dist colMin = math.Min(colMin, dist) } for j := maxJ + 1; j <= len(s2); j++ { current[j] = big } if colMin > maxValue { // Give up -- everything in this column is > max and it can't get better in future columns. return -1 } previous, current = current, previous } res := previous[len(s2)] if res > maxValue { return -1 } return res } func Identity[T any](t T) T { return t } func CheckEachDefined[S any](s []*S, msg string) []*S { for _, value := range s { if value == nil { panic(msg) } } return s } func IndexAfter(s string, pattern string, startIndex int) int { matched := strings.Index(s[startIndex:], pattern) if matched == -1 { return -1 } else { return matched + startIndex } } func ShouldRewriteModuleSpecifier(specifier string, compilerOptions *CompilerOptions) bool { return compilerOptions.RewriteRelativeImportExtensions.IsTrue() && tspath.PathIsRelative(specifier) && !tspath.IsDeclarationFileName(specifier) && tspath.HasTSFileExtension(specifier) } func SingleElementSlice[T any](element *T) []*T { if element == nil { return nil } return []*T{element} } func ConcatenateSeq[T any](seqs ...iter.Seq[T]) iter.Seq[T] { return func(yield func(T) bool) { for _, seq := range seqs { if seq == nil { continue } for e := range seq { if !yield(e) { return } } } } } // Enumerate returns a sequence of (index, value) pairs from the input sequence. func Enumerate[T any](seq iter.Seq[T]) iter.Seq2[int, T] { return func(yield func(int, T) bool) { i := 0 for v := range seq { if !yield(i, v) { return } i++ } } } func comparableValuesEqual[T comparable](a, b T) bool { return a == b } // DiffMaps compares two maps m1 and m2 and calls the provided callbacks for added, removed, and changed entries. // onAdded is called for each key-value pair that is in m2 but not in m1. // onRemoved is called for each key-value pair that is in m1 but not in m2. // onChanged is called for each key where the value in m1 differs from the value in m2. func DiffMaps[K comparable, V comparable](m1 map[K]V, m2 map[K]V, onAdded func(K, V), onRemoved func(K, V), onChanged func(K, V, V)) { DiffMapsFunc(m1, m2, comparableValuesEqual, onAdded, onRemoved, onChanged) } // DiffMapsFunc compares two maps m1 and m2 and calls the provided callbacks for added, removed, and changed entries. // onAdded is called for each key-value pair that is in m2 but not in m1. // onRemoved is called for each key-value pair that is in m1 but not in m2. // onChanged is called for each key where the value in m1 differs from the value in m2. func DiffMapsFunc[K comparable, V1 any, V2 any](m1 map[K]V1, m2 map[K]V2, equalValues func(V1, V2) bool, onAdded func(K, V2), onRemoved func(K, V1), onChanged func(K, V1, V2)) { if onAdded != nil { for k, v2 := range m2 { if _, ok := m1[k]; !ok { onAdded(k, v2) } } } if onChanged == nil && onRemoved == nil { return } for k, v1 := range m1 { if v2, ok := m2[k]; ok { if onChanged != nil && !equalValues(v1, v2) { onChanged(k, v1, v2) } } else { onRemoved(k, v1) } } } // CopyMapInto is maps.Copy, unless dst is nil, in which case it clones and returns src. // Use CopyMapInto anywhere you would use maps.Copy preceded by a nil check and map initialization. func CopyMapInto[M1 ~map[K]V, M2 ~map[K]V, K comparable, V any](dst M1, src M2) map[K]V { if dst == nil { return maps.Clone(src) } maps.Copy(dst, src) return dst } // UnorderedEqual returns true if s1 and s2 contain the same elements, regardless of order. func UnorderedEqual[T comparable](s1 []T, s2 []T) bool { if len(s1) != len(s2) { return false } counts := make(map[T]int) for _, v := range s1 { counts[v]++ } for _, v := range s2 { counts[v]-- if counts[v] < 0 { return false } } return true } func Deduplicate[T comparable](slice []T) []T { if len(slice) > 1 { for i, value := range slice { if slices.Contains(slice[:i], value) { result := slices.Clone(slice[:i]) for i++; i < len(slice); i++ { value = slice[i] if !slices.Contains(result, value) { result = append(result, value) } } return result } } } return slice } func DeduplicateSorted[T any](slice []T, isEqual func(a, b T) bool) []T { if len(slice) == 0 { return slice } last := slice[0] deduplicated := slice[:1] for i := 1; i < len(slice); i++ { next := slice[i] if isEqual(last, next) { continue } deduplicated = append(deduplicated, next) last = next } return deduplicated } // CompareBooleans treats true as greater than false. func CompareBooleans(a, b bool) int { if a && !b { return 1 } else if !a && b { return -1 } return 0 }