Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 44 additions & 4 deletions tsc/internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,7 @@ type Checker struct {
structuredTypeExtraArena core.Arena[structuredTypeExtra]
freeTypeSets *temporaryTypeSet
freePropertyCounts *temporaryPropertyCounts
symbolSortKeys []symbolSortKey
mergedSymbols map[*ast.Symbol]*ast.Symbol
mergedExportsChecked collections.Set[*ast.Symbol]
factory ast.NodeFactory
Expand Down Expand Up @@ -13695,11 +13696,16 @@ func (c *Checker) getSpreadType(left *Type, right *Type, symbol *ast.Symbol, obj
} else {
indexInfos = c.getUnionIndexInfos([]*Type{left, right})
}
// Both property lists are usually sorted already, so keep the members in
// their order (left properties first) to make sorting them cheap.
var leftOrder, rightOrder []*ast.Symbol
for _, rightProp := range c.getPropertiesOfType(right) {
if getDeclarationModifierFlagsFromSymbol(rightProp)&(ast.ModifierFlagsPrivate|ast.ModifierFlagsProtected) != 0 {
skippedPrivateMembers.Add(rightProp.Name())
} else if c.isSpreadableProperty(rightProp) {
members[rightProp.Name()] = c.getSpreadSymbol(rightProp, readonly)
spreadProp := c.getSpreadSymbol(rightProp, readonly)
members[rightProp.Name()] = spreadProp
rightOrder = append(rightOrder, spreadProp)
}
}

Expand Down Expand Up @@ -13732,15 +13738,25 @@ func (c *Checker) getSpreadType(left *Type, right *Type, symbol *ast.Symbol, obj
result.SetDeclarations(declarations)
links.setNameType(c, c.valueSymbolLinks.Get(leftProp).getNameType())
members[leftProp.Name()] = result
leftOrder = append(leftOrder, result)
}
} else {
members[leftProp.Name()] = c.getSpreadSymbol(leftProp, readonly)
spreadProp := c.getSpreadSymbol(leftProp, readonly)
members[leftProp.Name()] = spreadProp
leftOrder = append(leftOrder, spreadProp)
}
}
order := leftOrder
for _, prop := range rightOrder {
if members[prop.Name()] == prop {
order = append(order, prop)
}
}
spreadIndexInfos := core.SameMap(indexInfos, func(info *IndexInfo) *IndexInfo {
return c.getIndexInfoWithReadonly(info, readonly)
})
spread := c.newAnonymousType(symbol, members, nil, nil, spreadIndexInfos)
spread := c.newObjectType(ObjectFlagsAnonymous, symbol)
c.setStructuredTypeMembersInOrder(spread, members, order, nil, nil, spreadIndexInfos)
spread.objectFlags |= ObjectFlagsObjectLiteral | ObjectFlagsContainsObjectOrArrayLiteral | ObjectFlagsContainsSpread | objectFlags
return spread
}
Expand Down Expand Up @@ -22544,9 +22560,29 @@ func (c *Checker) getDefaultOrUnknownFromTypeParameter(t *Type) *Type {
}

func (c *Checker) getNamedMembers(members ast.SymbolTable, container *ast.Symbol) []*ast.Symbol {
return c.getNamedMembersInOrder(members, nil, container)
}

// getNamedMembersInOrder is getNamedMembers where order, if not nil, holds the
// symbols of members keyed by their names. The result does not depend on the
// order, but sorting is much cheaper when order is mostly sorted already.
func (c *Checker) getNamedMembersInOrder(members ast.SymbolTable, order []*ast.Symbol, container *ast.Symbol) []*ast.Symbol {
if len(members) == 0 {
return nil
}
if order != nil {
debug.Assert(len(order) == len(members))
if container == nil || container.Flags()&(ast.SymbolFlagsClass|ast.SymbolFlagsInterface) == 0 {
result := make([]*ast.Symbol, 0, len(order))
for _, symbol := range order {
if c.isNamedMember(symbol, symbol.Name()) {
result = append(result, symbol)
}
}
c.sortSymbols(result)
return result
}
}
// For classes and interfaces, we store explicitly declared members ahead of inherited members. This ensures we process
// explicitly declared members first in type relations, which is beneficial because explicitly declared members are more
// likely to contain discriminating differences. See for example https://github.com/microsoft/TypeScript/tsc/issues/1968.
Expand Down Expand Up @@ -25723,10 +25759,14 @@ func (c *Checker) cloneTypeReference(source *Type) *Type {
}

func (c *Checker) setStructuredTypeMembers(t *Type, members ast.SymbolTable, callSignatures []*Signature, constructSignatures []*Signature, indexInfos []*IndexInfo) {
c.setStructuredTypeMembersInOrder(t, members, nil, callSignatures, constructSignatures, indexInfos)
}

func (c *Checker) setStructuredTypeMembersInOrder(t *Type, members ast.SymbolTable, order []*ast.Symbol, callSignatures []*Signature, constructSignatures []*Signature, indexInfos []*IndexInfo) {
t.objectFlags |= ObjectFlagsMembersResolved
data := t.AsStructuredType()
data.members = members
data.properties = c.getNamedMembers(members, t.symbol)
data.properties = c.getNamedMembersInOrder(members, order, t.symbol)
c.setStructuredTypeSignatures(data, callSignatures, constructSignatures, indexInfos)
}

Expand Down
45 changes: 45 additions & 0 deletions tsc/internal/checker/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package checker

import (
"encoding/binary"
"fmt"
"math/rand/v2"
"runtime"
"slices"
"strconv"
Expand All @@ -10,7 +12,11 @@ import (
"unsafe"

"github.com/microsoft/TypeScript/tsc/internal/ast"
"github.com/microsoft/TypeScript/tsc/internal/binder"
"github.com/microsoft/TypeScript/tsc/internal/core"
"github.com/microsoft/TypeScript/tsc/internal/parser"
"github.com/microsoft/TypeScript/tsc/internal/testutil/race"
"github.com/microsoft/TypeScript/tsc/internal/tspath"
"github.com/zeebo/xxh3"
"gotest.tools/v3/assert"
)
Expand Down Expand Up @@ -345,6 +351,45 @@ func TestTemporaryPropertyCounts(t *testing.T) {
assert.Assert(t, other.getTemporaryPropertyCounts() != outer)
}

func TestSortSymbolsMatchesCompareSymbols(t *testing.T) {
t.Parallel()
c := &Checker{fileIndexMap: make(map[*ast.SourceFile]int)}
c.compareSymbols = c.compareSymbolsWorker
var symbols []*ast.Symbol
for i, name := range []string{"/b.ts", "/a.ts"} {
var text strings.Builder
text.WriteString("interface I {")
for j := range 40 {
fmt.Fprintf(&text, " p%d: any;", (j*7)%40)
}
text.WriteString(" }")
file := parser.ParseSourceFile(ast.SourceFileParseOptions{FileName: tspath.RootedFilePathFromAbsolute(name), PathKey: tspath.PathKeyFromCanonical(name)}, text.String(), core.ScriptKindTS)
binder.BindSourceFile(file)
c.fileIndexMap[file] = i
for _, member := range file.Locals["I"].Members() {
symbols = append(symbols, member)
}
}
for i := range 6 {
symbols = append(symbols, c.newSymbol(ast.SymbolFlagsProperty, fmt.Sprintf("x%d", i%3)))
}
expected := slices.Clone(symbols)
slices.SortFunc(expected, c.compareSymbols)
half := slices.Clone(expected[:len(expected)/2])
twoRuns := append(slices.Clone(expected[len(expected)/2:]), half...)
inputs := [][]*ast.Symbol{slices.Clone(expected), twoRuns}
rng := rand.New(rand.NewPCG(1, 2))
for range 20 {
shuffled := slices.Clone(expected)
rng.Shuffle(len(shuffled), func(i, j int) { shuffled[i], shuffled[j] = shuffled[j], shuffled[i] })
inputs = append(inputs, shuffled)
}
for _, input := range inputs {
c.sortSymbols(input)
assert.Assert(t, slices.Equal(input, expected))
}
}

func TestIntersectionReductionReentrancy(t *testing.T) {
t.Parallel()
c, _ := newCompositeTestChecker()
Expand Down
110 changes: 109 additions & 1 deletion tsc/internal/checker/utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,8 +359,116 @@
return result
}

type symbolSortKey struct {
symbol *ast.Symbol
file *ast.SourceFile
fileIndex int
pos int
}

// sortSymbols sorts symbols in compareSymbols order. Comparing declarations
// finds each node's source file by walking its parents, so the keys are
// computed once per symbol rather than once per comparison.
func (c *Checker) sortSymbols(symbols []*ast.Symbol) {
slices.SortFunc(symbols, c.compareSymbols)
if len(symbols) < 2 {
return
}
keys := c.symbolSortKeys[:0]
for _, s := range symbols {
key := symbolSortKey{symbol: s, fileIndex: -1}
if s != nil {
if decls := s.Declarations(); len(decls) != 0 && decls[0] != nil {
key.file = ast.GetSourceFileOfNode(decls[0])
key.fileIndex = c.fileIndexMap[key.file]
key.pos = decls[0].Pos()
}
}
keys = append(keys, key)
}
// Symbol lists are often already sorted, or two sorted runs.
descents, split := 0, 0
for i := 1; i < len(keys) && descents < 2; i++ {
if c.compareSymbolSortKeys(keys[i-1], keys[i]) > 0 {
descents++
split = i
}
}
switch descents {
case 0:

Check failure on line 397 in tsc/internal/checker/utilities.go

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

casebody: this case block is empty and will do nothing (customlint)
case 1:
// The keys hold the symbols, so the runs merge straight into symbols.
left, right := keys[:split], keys[split:]
out := symbols[:0]
for len(left) != 0 && len(right) != 0 {
if c.compareSymbolSortKeys(left[0], right[0]) <= 0 {
out = append(out, left[0].symbol)
left = left[1:]
} else {
out = append(out, right[0].symbol)
right = right[1:]
}
}
for _, k := range left {
out = append(out, k.symbol)
}
for _, k := range right {
out = append(out, k.symbol)
}
default:
slices.SortFunc(keys, c.compareSymbolSortKeys)
for i := range keys {
symbols[i] = keys[i].symbol
}
}
clear(keys)
if cap(keys) <= 4096 {
c.symbolSortKeys = keys[:0]
}
}

// compareSymbolSortKeys matches compareSymbolsWorker.
func (c *Checker) compareSymbolSortKeys(k1, k2 symbolSortKey) int {
s1, s2 := k1.symbol, k2.symbol
if s1 == s2 {
return 0
}
if s1 == nil {
return 1
}
if s2 == nil {
return -1
}
if len(s1.Declarations()) != 0 && len(s2.Declarations()) != 0 {
if r := compareSymbolSortKeyDeclarations(k1, k2); r != 0 {
return r
}
} else if len(s1.Declarations()) != 0 {
return -1
} else if len(s2.Declarations()) != 0 {
return 1
}
if r := strings.Compare(s1.Name(), s2.Name()); r != 0 {
return r
}
return cmp.Compare(ast.GetSymbolId(s1), ast.GetSymbolId(s2))
}

// compareSymbolSortKeyDeclarations matches compareNodes on the first declarations.
func compareSymbolSortKeyDeclarations(k1, k2 symbolSortKey) int {
d1, d2 := k1.symbol.Declarations()[0], k2.symbol.Declarations()[0]
if d1 == d2 {
return 0
}
if d1 == nil {
return 1
}
if d2 == nil {
return -1
}
if k1.file != k2.file {
return k1.fileIndex - k2.fileIndex
}
return k1.pos - k2.pos
}

func (c *Checker) compareSymbolsWorker(s1, s2 *ast.Symbol) int {
Expand Down
Loading