diff --git a/tools/customlint/plugin.go b/tools/customlint/plugin.go index fd9c5b9e7dfce..677f1a0d9012a 100644 --- a/tools/customlint/plugin.go +++ b/tools/customlint/plugin.go @@ -22,6 +22,7 @@ func (f *plugin) BuildAnalyzers() ([]*analysis.Analyzer, error) { forbidParentAccessAnalyzer, shadowAnalyzer, typedPathsAnalyzer, + unsafeLayoutAnalyzer, unexportedAPIAnalyzer, }, nil } diff --git a/tools/customlint/testdata/unsafelayout/consumer/consumer.go b/tools/customlint/testdata/unsafelayout/consumer/consumer.go new file mode 100644 index 0000000000000..e7117fb7492a6 --- /dev/null +++ b/tools/customlint/testdata/unsafelayout/consumer/consumer.go @@ -0,0 +1,110 @@ +package consumer + +import ( + "unsafe" + + "testdata/unsafelayout/internal/ast" + "testdata/unsafelayout/internal/checker" +) + +type BadNode ast.Node + +type NodeAlias = ast.Node + +type BorrowedNode struct { + *ast.Node +} + +func uses(node *ast.Node, declaration *ast.ImportDeclaration, kind ast.Kind, typ *checker.Type) { + node.Kind = ast.KindIdentifier + declaration.Kind = ast.KindImportDeclaration + declaration.Kind = ast.KindJSImportDeclaration + node.AsImportDeclaration().Kind = ast.KindImportDeclaration + declaration.Kind = ast.KindIdentifier + declaration.Kind = kind + declaration.Kind++ + _ = &node.Kind + _ = &ast.Node{Kind: ast.KindIdentifier} + _ = &ast.Identifier{} + _ = new(ast.Node) + _ = new(ast.Identifier) + _ = new(NodeAlias) + _ = unsafe.Sizeof(ast.Node{}) + _ = unsafe.Offsetof(ast.Identifier{}.Node) + _ = (*ast.Identifier)(unsafe.Pointer(node)) + _ = *node + _ = *declaration + _ = *typ + copy := *node + _ = copy + pointer := unsafe.Pointer(node) + _ = pointer + borrowed := BorrowedNode{Node: node} + borrowed.Kind = ast.KindIdentifier + _ = &borrowed.Kind +} + +type Pointer unsafe.Pointer + +type PointerAlias = unsafe.Pointer + +func zeroValues(node *ast.Node) { + var identifier ast.Identifier + _ = &identifier + var identifiers [2]ast.Identifier + _ = &identifiers[0] + var holder struct{ identifier ast.Identifier } + _ = &holder.identifier + var pointers [2]*ast.Identifier + _ = pointers + _ = new([2]ast.Identifier) + _ = make([]ast.Identifier, 1) + _ = make([]*ast.Identifier, 1) + _ = [2]ast.Identifier{} + _ = (*ast.Identifier)(Pointer(node)) + _ = (*ast.Identifier)(PointerAlias(node)) +} + +func retag(node *ast.Node, kinds []ast.Kind) { + for _, node.Kind = range kinds { + } +} + +func zeroResult() (n ast.Identifier) { + return +} + +func byValueParam(identifier ast.Identifier, identifiers [2]ast.Identifier) {} + +func copies(records map[string]ast.Identifier, channel chan ast.Identifier, get func() ast.Identifier, slice []ast.Identifier) *ast.Identifier { + record := records["key"] + _ = <-channel + _ = get() + _ = slice[0] + _ = &slice[0] + _ = slice[0].Text + _ = len(slice[:1]) + return &record +} + +func rangeCopies(records []ast.Identifier, byKey map[string]ast.Identifier, channel chan ast.Identifier) { + for _, record := range records { + _ = record.Text + } + for _, record := range byKey { + _ = record.Text + } + for record := range channel { + _ = record.Text + } + for i := range records { + _ = &records[i] + } +} + +func bulkCopies(dst, src []ast.Identifier, pointers []*ast.Identifier) []ast.Identifier { + copy(dst, src) + pointers = append(pointers, pointers...) + copy(pointers, pointers) + return append(dst, src...) +} diff --git a/tools/customlint/testdata/unsafelayout/consumer/consumer.go.golden b/tools/customlint/testdata/unsafelayout/consumer/consumer.go.golden new file mode 100644 index 0000000000000..b09ccabe852b6 --- /dev/null +++ b/tools/customlint/testdata/unsafelayout/consumer/consumer.go.golden @@ -0,0 +1,217 @@ + package consumer + + import ( + "unsafe" + + "testdata/unsafelayout/internal/ast" + "testdata/unsafelayout/internal/checker" + ) + + type BadNode ast.Node + ~ +!!! unsafelayout: defining a new compiler-record type bypasses checked construction; use a type alias + + type NodeAlias = ast.Node + + type BorrowedNode struct { + *ast.Node + } + + func uses(node *ast.Node, declaration *ast.ImportDeclaration, kind ast.Kind, typ *checker.Type) { + node.Kind = ast.KindIdentifier + ~ +!!! unsafelayout: AST kind is immutable outside checked construction; create a new node + declaration.Kind = ast.KindImportDeclaration + ~ +!!! unsafelayout: AST kind is immutable outside checked construction; create a new node + declaration.Kind = ast.KindJSImportDeclaration + ~ +!!! unsafelayout: AST kind is immutable outside checked construction; create a new node + node.AsImportDeclaration().Kind = ast.KindImportDeclaration + ~ +!!! unsafelayout: AST kind is immutable outside checked construction; create a new node + declaration.Kind = ast.KindIdentifier + ~ +!!! unsafelayout: AST kind is immutable outside checked construction; create a new node + declaration.Kind = kind + ~ +!!! unsafelayout: AST kind is immutable outside checked construction; create a new node + declaration.Kind++ + ~ +!!! unsafelayout: incrementing a compiler tag bypasses checked tag assignment + _ = &node.Kind + ~ +!!! unsafelayout: taking a compiler tag's address bypasses checked tag assignment + _ = &ast.Node{Kind: ast.KindIdentifier} + ~ +!!! unsafelayout: a bare compiler header has no concrete payload; use a concrete factory + ~ +!!! unsafelayout: construct compiler records through their owning factories, not struct literals + ~ +!!! unsafelayout: compiler tags must be assigned by checked construction, not struct literals + _ = &ast.Identifier{} + ~ +!!! unsafelayout: construct compiler records through their owning factories, not struct literals + _ = new(ast.Node) + ~ +!!! unsafelayout: a bare compiler header has no concrete payload; use a concrete factory + ~ +!!! unsafelayout: allocate compiler records through their owning factories + _ = new(ast.Identifier) + ~ +!!! unsafelayout: allocate compiler records through their owning factories + _ = new(NodeAlias) + ~ +!!! unsafelayout: a bare compiler header has no concrete payload; use a concrete factory + ~ +!!! unsafelayout: allocate compiler records through their owning factories + _ = unsafe.Sizeof(ast.Node{}) + _ = unsafe.Offsetof(ast.Identifier{}.Node) + _ = (*ast.Identifier)(unsafe.Pointer(node)) + ~ +!!! unsafelayout: reinterpret compiler records only through checked casts or generated AST dispatch + ~ +!!! unsafelayout: reinterpret compiler records only through checked casts or generated AST dispatch + _ = *node + ~ +!!! unsafelayout: copying a compiler record by value can detach its tagged header; keep a pointer + _ = *declaration + ~ +!!! unsafelayout: copying a compiler record by value can detach its tagged header; keep a pointer + _ = *typ + ~ +!!! unsafelayout: copying a compiler record by value can detach its tagged header; keep a pointer + copy := *node + ~ +!!! unsafelayout: declaring a compiler record by value copies or detaches its tagged header; use a pointer + ~ +!!! unsafelayout: copying a compiler record by value can detach its tagged header; keep a pointer + _ = copy + ~ +!!! unsafelayout: copying a compiler record by value can detach its tagged header; keep a pointer + pointer := unsafe.Pointer(node) + ~ +!!! unsafelayout: reinterpret compiler records only through checked casts or generated AST dispatch + _ = pointer + borrowed := BorrowedNode{Node: node} + borrowed.Kind = ast.KindIdentifier + ~ +!!! unsafelayout: AST kind is immutable outside checked construction; create a new node + _ = &borrowed.Kind + ~ +!!! unsafelayout: taking a compiler tag's address bypasses checked tag assignment + } + + type Pointer unsafe.Pointer + + type PointerAlias = unsafe.Pointer + + func zeroValues(node *ast.Node) { + var identifier ast.Identifier + ~ +!!! unsafelayout: a zero-value compiler record bypasses checked construction; use its owning factory + _ = &identifier + var identifiers [2]ast.Identifier + ~ +!!! unsafelayout: a zero-value compiler record bypasses checked construction; use its owning factory + _ = &identifiers[0] + var holder struct{ identifier ast.Identifier } + ~ +!!! unsafelayout: a zero-value compiler record bypasses checked construction; use its owning factory + _ = &holder.identifier + var pointers [2]*ast.Identifier + _ = pointers + _ = new([2]ast.Identifier) + ~ +!!! unsafelayout: a zero-value compiler record bypasses checked construction; use its owning factory + _ = make([]ast.Identifier, 1) + ~ +!!! unsafelayout: a zero-value compiler record bypasses checked construction; use its owning factory + _ = make([]*ast.Identifier, 1) + _ = [2]ast.Identifier{} + ~ +!!! unsafelayout: a zero-value compiler record bypasses checked construction; use its owning factory + _ = (*ast.Identifier)(Pointer(node)) + ~ +!!! unsafelayout: reinterpret compiler records only through checked casts or generated AST dispatch + ~ +!!! unsafelayout: reinterpret compiler records only through checked casts or generated AST dispatch + _ = (*ast.Identifier)(PointerAlias(node)) + ~ +!!! unsafelayout: reinterpret compiler records only through checked casts or generated AST dispatch + ~ +!!! unsafelayout: reinterpret compiler records only through checked casts or generated AST dispatch + } + + func retag(node *ast.Node, kinds []ast.Kind) { + for _, node.Kind = range kinds { + ~ +!!! unsafelayout: range assignment to a compiler tag bypasses checked tag assignment + } + } + + func zeroResult() (n ast.Identifier) { + ~ +!!! unsafelayout: declaring a compiler record by value copies or detaches its tagged header; use a pointer + return + } + + func byValueParam(identifier ast.Identifier, identifiers [2]ast.Identifier) {} + ~ +!!! unsafelayout: declaring a compiler record by value copies or detaches its tagged header; use a pointer + ~ +!!! unsafelayout: declaring a compiler record by value copies or detaches its tagged header; use a pointer + + func copies(records map[string]ast.Identifier, channel chan ast.Identifier, get func() ast.Identifier, slice []ast.Identifier) *ast.Identifier { + record := records["key"] + ~ +!!! unsafelayout: declaring a compiler record by value copies or detaches its tagged header; use a pointer + ~ +!!! unsafelayout: copying a compiler record by value can detach its tagged header; keep a pointer + _ = <-channel + ~ +!!! unsafelayout: copying a compiler record by value can detach its tagged header; keep a pointer + _ = get() + ~ +!!! unsafelayout: copying a compiler record by value can detach its tagged header; keep a pointer + _ = slice[0] + ~ +!!! unsafelayout: copying a compiler record by value can detach its tagged header; keep a pointer + _ = &slice[0] + _ = slice[0].Text + _ = len(slice[:1]) + return &record + } + + func rangeCopies(records []ast.Identifier, byKey map[string]ast.Identifier, channel chan ast.Identifier) { + for _, record := range records { + ~ +!!! unsafelayout: declaring a compiler record by value copies or detaches its tagged header; use a pointer + _ = record.Text + } + for _, record := range byKey { + ~ +!!! unsafelayout: declaring a compiler record by value copies or detaches its tagged header; use a pointer + _ = record.Text + } + for record := range channel { + ~ +!!! unsafelayout: declaring a compiler record by value copies or detaches its tagged header; use a pointer + _ = record.Text + } + for i := range records { + _ = &records[i] + } + } + + func bulkCopies(dst, src []ast.Identifier, pointers []*ast.Identifier) []ast.Identifier { + copy(dst, src) + ~ +!!! unsafelayout: copying a compiler record by value can detach its tagged header; keep a pointer + pointers = append(pointers, pointers...) + copy(pointers, pointers) + return append(dst, src...) + ~ +!!! unsafelayout: copying a compiler record by value can detach its tagged header; keep a pointer + } + diff --git a/tools/customlint/testdata/unsafelayout/internal/ast/ast.go b/tools/customlint/testdata/unsafelayout/internal/ast/ast.go new file mode 100644 index 0000000000000..d0ffd3677cc2a --- /dev/null +++ b/tools/customlint/testdata/unsafelayout/internal/ast/ast.go @@ -0,0 +1,103 @@ +package ast + +type Kind int16 + +const ( + KindIdentifier Kind = iota + KindImportDeclaration + KindJSImportDeclaration +) + +type Node struct { + Kind Kind +} + +type Identifier struct { + Node + Text string +} + +type ImportDeclaration struct { + Node +} + +func (n *Node) AsNode() *Node { return n } + +func (n *Node) AsIdentifier() *Identifier { + if n.Kind != KindIdentifier { + panic("Invalid node cast") + } + return nil +} + +func (n *Node) AsImportDeclaration() *ImportDeclaration { + switch n.Kind { + case KindImportDeclaration, KindJSImportDeclaration: + return nil + default: + panic("Invalid node cast") + } +} + +func newNode(kind Kind, node *Node) *Node { + node.Kind = kind + return node +} + +func NewIdentifier(text string) *Node { + data := &Identifier{Text: text} + return newNode(KindIdentifier, data.AsNode()) +} + +func badFactory() *Node { + data := &ImportDeclaration{} + return newNode(KindIdentifier, data.AsNode()) +} + +func escapes(kind Kind) *Node { + data := &Identifier{} + _ = newNode + return newNode(kind, data.AsNode()) +} + +type PretendIdentifier struct { + Identifier +} + +func (n *PretendIdentifier) AsNode() *Node { return &n.Node } + +func shadowedUpcast() *Node { + data := &PretendIdentifier{} + return newNode(KindIdentifier, data.AsNode()) +} + +func illegalWrites(node *Node, identifier *Identifier, declaration *ImportDeclaration, kind Kind) { + node.Kind = KindIdentifier + identifier.Kind = KindIdentifier + identifier.Kind = KindImportDeclaration + declaration.Kind = KindImportDeclaration + declaration.Kind = KindJSImportDeclaration + declaration.Kind = kind + declaration.Kind += KindImportDeclaration + node.Kind++ + _ = &node.Kind + (node.Kind) = KindIdentifier + (node.Kind)++ + _ = &(node.Kind) +} + +func freshNodes() *Node { + tagged := &Identifier{} + tagged.Text = "x" + _ = newNode(KindIdentifier, tagged.AsNode()) + return (&Identifier{}).AsNode() +} + +func zeroValueNode() *Identifier { + var n Identifier + return &n +} + +func (n Identifier) ValueReceiver() string { + return n.Text +} diff --git a/tools/customlint/testdata/unsafelayout/internal/ast/ast.go.golden b/tools/customlint/testdata/unsafelayout/internal/ast/ast.go.golden new file mode 100644 index 0000000000000..49b61d58be57d --- /dev/null +++ b/tools/customlint/testdata/unsafelayout/internal/ast/ast.go.golden @@ -0,0 +1,142 @@ + package ast + + type Kind int16 + + const ( + KindIdentifier Kind = iota + KindImportDeclaration + KindJSImportDeclaration + ) + + type Node struct { + Kind Kind + } + + type Identifier struct { + Node + Text string + } + + type ImportDeclaration struct { + Node + } + + func (n *Node) AsNode() *Node { return n } + + func (n *Node) AsIdentifier() *Identifier { + if n.Kind != KindIdentifier { + panic("Invalid node cast") + } + return nil + } + + func (n *Node) AsImportDeclaration() *ImportDeclaration { + switch n.Kind { + case KindImportDeclaration, KindJSImportDeclaration: + return nil + default: + panic("Invalid node cast") + } + } + + func newNode(kind Kind, node *Node) *Node { + node.Kind = kind + return node + } + + func NewIdentifier(text string) *Node { + data := &Identifier{Text: text} + return newNode(KindIdentifier, data.AsNode()) + } + + func badFactory() *Node { + data := &ImportDeclaration{} + return newNode(KindIdentifier, data.AsNode()) + ~ +!!! unsafelayout: newNode must pair a concrete AsNode() receiver with a compatible kind + } + + func escapes(kind Kind) *Node { + data := &Identifier{} + _ = newNode + ~ +!!! unsafelayout: checked construction must be called directly so its allocation/tag pairing can be verified + return newNode(kind, data.AsNode()) + ~ +!!! unsafelayout: newNode must pair a concrete AsNode() receiver with a compatible kind + } + + type PretendIdentifier struct { + Identifier + } + + func (n *PretendIdentifier) AsNode() *Node { return &n.Node } + + func shadowedUpcast() *Node { + data := &PretendIdentifier{} + return newNode(KindIdentifier, data.AsNode()) + ~ +!!! unsafelayout: newNode must pair a concrete AsNode() receiver with a compatible kind + } + + func illegalWrites(node *Node, identifier *Identifier, declaration *ImportDeclaration, kind Kind) { + node.Kind = KindIdentifier + ~ +!!! unsafelayout: AST kind is immutable outside checked construction; create a new node + identifier.Kind = KindIdentifier + ~ +!!! unsafelayout: AST kind is immutable outside checked construction; create a new node + identifier.Kind = KindImportDeclaration + ~ +!!! unsafelayout: AST kind is immutable outside checked construction; create a new node + declaration.Kind = KindImportDeclaration + ~ +!!! unsafelayout: AST kind is immutable outside checked construction; create a new node + declaration.Kind = KindJSImportDeclaration + ~ +!!! unsafelayout: AST kind is immutable outside checked construction; create a new node + declaration.Kind = kind + ~ +!!! unsafelayout: AST kind is immutable outside checked construction; create a new node + declaration.Kind += KindImportDeclaration + ~ +!!! unsafelayout: AST kind is immutable outside checked construction; create a new node + node.Kind++ + ~ +!!! unsafelayout: incrementing a compiler tag bypasses checked tag assignment + _ = &node.Kind + ~ +!!! unsafelayout: taking a compiler tag's address bypasses checked tag assignment + (node.Kind) = KindIdentifier + ~ +!!! unsafelayout: AST kind is immutable outside checked construction; create a new node + (node.Kind)++ + ~ +!!! unsafelayout: incrementing a compiler tag bypasses checked tag assignment + _ = &(node.Kind) + ~ +!!! unsafelayout: taking a compiler tag's address bypasses checked tag assignment + } + + func freshNodes() *Node { + tagged := &Identifier{} + tagged.Text = "x" + _ = newNode(KindIdentifier, tagged.AsNode()) + return (&Identifier{}).AsNode() + ~ +!!! unsafelayout: a fresh compiler record must be checked-constructed before it escapes + } + + func zeroValueNode() *Identifier { + var n Identifier + ~ +!!! unsafelayout: a zero-value compiler record bypasses checked construction; use its owning factory + return &n + } + + func (n Identifier) ValueReceiver() string { + ~ +!!! unsafelayout: declaring a compiler record by value copies or detaches its tagged header; use a pointer + return n.Text + } + diff --git a/tools/customlint/testdata/unsafelayout/internal/checker/checker.go b/tools/customlint/testdata/unsafelayout/internal/checker/checker.go new file mode 100644 index 0000000000000..220982c78358d --- /dev/null +++ b/tools/customlint/testdata/unsafelayout/internal/checker/checker.go @@ -0,0 +1,176 @@ +package checker + +import "unsafe" + +type typeKind uint8 + +const ( + typeKindLiteral typeKind = iota + typeKindObject +) + +type Type struct { + kind typeKind +} + +type LiteralType struct { + TypeBase +} + +type ObjectType struct { + TypeBase +} + +type TypeBase struct { + Type +} + +func (t *TypeBase) AsType() *Type { return &t.Type } + +type Checker struct{} + +func (c *Checker) newType(flags, objectFlags int, typ *Type, kind typeKind) *Type { + typ.kind = kind + if flags != 0 { + kind := typeKindObject + typ.kind = kind + } + return typ +} + +func castType[T any](t *Type, kind typeKind) *T { + if t.kind != kind { + panic("Invalid type cast") + } + return (*T)(unsafe.Pointer(t)) +} + +func calls(c *Checker, literal *LiteralType, typ *Type, kind typeKind) { + c.newType(0, 0, literal.AsType(), typeKindLiteral) + c.newType(0, 0, literal.AsType(), typeKindObject) + c.newType(0, 0, literal.AsType(), kind) + c.newType(0, 0, typ, typeKindLiteral) + _ = c.newType + _ = castType[LiteralType](typ, typeKindLiteral) + _ = castType[LiteralType](typ, typeKindObject) + _ = castType[ObjectType](typ, typeKindLiteral) + _ = castType[LiteralType](typ, kind) + _ = castType[*LiteralType](typ, typeKindLiteral) + _ = castType[LiteralType] + typ.kind = typeKindLiteral + typ.kind++ + _ = &typ.kind + _ = (*LiteralType)(unsafe.Pointer(typ)) +} + +type PretendLiteralType struct { + LiteralType +} + +func (t *PretendLiteralType) AsType() *Type { return &t.Type } + +func shadowedUpcast(c *Checker) { + data := &PretendLiteralType{} + c.newType(0, 0, data.AsType(), typeKindLiteral) +} + +type TypeMapperKind uint8 + +const ( + TypeMapperKindSimple TypeMapperKind = iota + TypeMapperKindArray +) + +type TypeMapper struct { + kind TypeMapperKind +} + +type SimpleTypeMapper struct { + TypeMapper +} + +type ArrayTypeMapper struct { + TypeMapper +} + +func castTypeMapper[T any](m *TypeMapper, kind TypeMapperKind) *T { + if m.kind != kind { + panic("Invalid type mapper cast") + } + return (*T)(unsafe.Pointer(m)) +} + +func mapperCalls(simple *SimpleTypeMapper, header *TypeMapper) { + simple.kind = TypeMapperKindSimple + simple.kind = TypeMapperKindArray + header.kind = TypeMapperKindSimple + _ = castTypeMapper[SimpleTypeMapper](header, TypeMapperKindSimple) + _ = castTypeMapper[SimpleTypeMapper](header, TypeMapperKindArray) + _ = (*SimpleTypeMapper)(unsafe.Pointer(header)) +} + +func freshRecords(c *Checker) *Type { + tagged := &LiteralType{} + c.newType(0, 0, tagged.AsType(), typeKindLiteral) + _ = (&LiteralType{}).AsType() + escaped := &LiteralType{} + c.newType(0, 0, escaped.AsType(), typeKindLiteral) + _ = escaped.AsType() + header := &ObjectType{} + _ = &header.TypeBase + c.newType(0, 0, header.AsType(), typeKindObject) + unused := &ObjectType{} + _ = unused + value := LiteralType{} + _ = value + return new(LiteralType).AsType() +} + +func freshMappers() (*TypeMapper, *TypeMapper) { + tagged := &SimpleTypeMapper{} + tagged.kind = TypeMapperKindSimple + untagged := &ArrayTypeMapper{} + return &tagged.TypeMapper, &untagged.TypeMapper +} + +type arena[T any] struct{} + +func (a *arena[T]) New() *T { return new(T) } + +func arenaRecords(c *Checker, literals *arena[LiteralType]) *LiteralType { + c.newType(0, 0, literals.New().AsType(), typeKindLiteral) + data := literals.New() + c.newType(0, 0, data.AsType(), typeKindLiteral) + return literals.New() +} + +func zeroValueRecords() (*LiteralType, *SimpleTypeMapper) { + var literal LiteralType + var literals [2]LiteralType + _ = literals + var mappers [2]SimpleTypeMapper + mappers[0].kind = TypeMapperKindSimple + return &literal, &mappers[0] +} + +func (m *SimpleTypeMapper) Map(t *Type) *Type { return t } + +func initSimpleTypeMapper(m *SimpleTypeMapper) *TypeMapper { + m.kind = TypeMapperKindSimple + return &m.TypeMapper +} + +type mapperOwner struct { + mapperStorage [2]SimpleTypeMapper +} + +func coallocatedMappers(owner *mapperOwner, t *Type) (*TypeMapper, *TypeMapper, *Type) { + tagged := initSimpleTypeMapper(&owner.mapperStorage[0]) + escaped := &owner.mapperStorage[1].TypeMapper + return tagged, escaped, owner.mapperStorage[1].Map(t) +} + +func zeroValueMapper() *TypeMapper { + var m SimpleTypeMapper + return &m.TypeMapper +} diff --git a/tools/customlint/testdata/unsafelayout/internal/checker/checker.go.golden b/tools/customlint/testdata/unsafelayout/internal/checker/checker.go.golden new file mode 100644 index 0000000000000..423cada86003d --- /dev/null +++ b/tools/customlint/testdata/unsafelayout/internal/checker/checker.go.golden @@ -0,0 +1,257 @@ + package checker + + import "unsafe" + + type typeKind uint8 + + const ( + typeKindLiteral typeKind = iota + typeKindObject + ) + + type Type struct { + kind typeKind + } + + type LiteralType struct { + TypeBase + } + + type ObjectType struct { + TypeBase + } + + type TypeBase struct { + Type + } + + func (t *TypeBase) AsType() *Type { return &t.Type } + + type Checker struct{} + + func (c *Checker) newType(flags, objectFlags int, typ *Type, kind typeKind) *Type { + typ.kind = kind + if flags != 0 { + kind := typeKindObject + typ.kind = kind + ~ +!!! unsafelayout: checked construction must initialize the tag from its kind parameter + } + return typ + } + + func castType[T any](t *Type, kind typeKind) *T { + if t.kind != kind { + panic("Invalid type cast") + } + return (*T)(unsafe.Pointer(t)) + } + + func calls(c *Checker, literal *LiteralType, typ *Type, kind typeKind) { + c.newType(0, 0, literal.AsType(), typeKindLiteral) + c.newType(0, 0, literal.AsType(), typeKindObject) + ~ +!!! unsafelayout: newType must directly pair a concrete AsType() receiver with its matching constant kind + c.newType(0, 0, literal.AsType(), kind) + ~ +!!! unsafelayout: newType must directly pair a concrete AsType() receiver with its matching constant kind + c.newType(0, 0, typ, typeKindLiteral) + ~ +!!! unsafelayout: newType must directly pair a concrete AsType() receiver with its matching constant kind + _ = c.newType + ~ +!!! unsafelayout: checked construction must be called directly so its allocation/tag pairing can be verified + _ = castType[LiteralType](typ, typeKindLiteral) + _ = castType[LiteralType](typ, typeKindObject) + ~ +!!! unsafelayout: checked cast must directly pair its concrete target type with its matching constant kind + _ = castType[ObjectType](typ, typeKindLiteral) + ~ +!!! unsafelayout: checked cast must directly pair its concrete target type with its matching constant kind + _ = castType[LiteralType](typ, kind) + ~ +!!! unsafelayout: checked cast must directly pair its concrete target type with its matching constant kind + _ = castType[*LiteralType](typ, typeKindLiteral) + ~ +!!! unsafelayout: checked cast must directly pair its concrete target type with its matching constant kind + _ = castType[LiteralType] + ~ +!!! unsafelayout: checked cast must directly pair its concrete target type with its matching constant kind + typ.kind = typeKindLiteral + ~ +!!! unsafelayout: type kind is immutable outside checked construction + typ.kind++ + ~ +!!! unsafelayout: incrementing a compiler tag bypasses checked tag assignment + _ = &typ.kind + ~ +!!! unsafelayout: taking a compiler tag's address bypasses checked tag assignment + _ = (*LiteralType)(unsafe.Pointer(typ)) + ~ +!!! unsafelayout: reinterpret compiler records only through checked casts or generated AST dispatch + ~ +!!! unsafelayout: reinterpret compiler records only through checked casts or generated AST dispatch + } + + type PretendLiteralType struct { + LiteralType + } + + func (t *PretendLiteralType) AsType() *Type { return &t.Type } + + func shadowedUpcast(c *Checker) { + data := &PretendLiteralType{} + c.newType(0, 0, data.AsType(), typeKindLiteral) + ~ +!!! unsafelayout: newType must directly pair a concrete AsType() receiver with its matching constant kind + } + + type TypeMapperKind uint8 + + const ( + TypeMapperKindSimple TypeMapperKind = iota + TypeMapperKindArray + ) + + type TypeMapper struct { + kind TypeMapperKind + } + + type SimpleTypeMapper struct { + TypeMapper + } + + type ArrayTypeMapper struct { + TypeMapper + } + + func castTypeMapper[T any](m *TypeMapper, kind TypeMapperKind) *T { + if m.kind != kind { + panic("Invalid type mapper cast") + } + return (*T)(unsafe.Pointer(m)) + } + + func mapperCalls(simple *SimpleTypeMapper, header *TypeMapper) { + simple.kind = TypeMapperKindSimple + simple.kind = TypeMapperKindArray + ~ +!!! unsafelayout: mapper tag must match the concrete mapper allocation + header.kind = TypeMapperKindSimple + ~ +!!! unsafelayout: mapper tag must match the concrete mapper allocation + _ = castTypeMapper[SimpleTypeMapper](header, TypeMapperKindSimple) + _ = castTypeMapper[SimpleTypeMapper](header, TypeMapperKindArray) + ~ +!!! unsafelayout: checked cast must directly pair its concrete target type with its matching constant kind + _ = (*SimpleTypeMapper)(unsafe.Pointer(header)) + ~ +!!! unsafelayout: reinterpret compiler records only through checked casts or generated AST dispatch + ~ +!!! unsafelayout: reinterpret compiler records only through checked casts or generated AST dispatch + } + + func freshRecords(c *Checker) *Type { + tagged := &LiteralType{} + c.newType(0, 0, tagged.AsType(), typeKindLiteral) + _ = (&LiteralType{}).AsType() + ~ +!!! unsafelayout: a fresh compiler record must be checked-constructed before it escapes + escaped := &LiteralType{} + ~ +!!! unsafelayout: a fresh compiler record must be checked-constructed before it escapes + c.newType(0, 0, escaped.AsType(), typeKindLiteral) + _ = escaped.AsType() + header := &ObjectType{} + ~ +!!! unsafelayout: a fresh compiler record must be checked-constructed before it escapes + _ = &header.TypeBase + c.newType(0, 0, header.AsType(), typeKindObject) + unused := &ObjectType{} + ~ +!!! unsafelayout: a fresh compiler record must be checked-constructed before it escapes + _ = unused + value := LiteralType{} + ~ +!!! unsafelayout: declaring a compiler record by value copies or detaches its tagged header; use a pointer + ~ +!!! unsafelayout: allocate compiler records by pointer so checked construction can tag them + _ = value + ~ +!!! unsafelayout: copying a compiler record by value can detach its tagged header; keep a pointer + return new(LiteralType).AsType() + ~ +!!! unsafelayout: a fresh compiler record must be checked-constructed before it escapes + } + + func freshMappers() (*TypeMapper, *TypeMapper) { + tagged := &SimpleTypeMapper{} + tagged.kind = TypeMapperKindSimple + untagged := &ArrayTypeMapper{} + ~ +!!! unsafelayout: a fresh mapper must be tagged by the statement after its allocation + return &tagged.TypeMapper, &untagged.TypeMapper + } + + type arena[T any] struct{} + + func (a *arena[T]) New() *T { return new(T) } + + func arenaRecords(c *Checker, literals *arena[LiteralType]) *LiteralType { + c.newType(0, 0, literals.New().AsType(), typeKindLiteral) + data := literals.New() + c.newType(0, 0, data.AsType(), typeKindLiteral) + return literals.New() + ~ +!!! unsafelayout: a fresh compiler record must be checked-constructed before it escapes + } + + func zeroValueRecords() (*LiteralType, *SimpleTypeMapper) { + var literal LiteralType + ~ +!!! unsafelayout: a zero-value compiler record bypasses checked construction; use its owning factory + var literals [2]LiteralType + ~ +!!! unsafelayout: a zero-value compiler record bypasses checked construction; use its owning factory + _ = literals + ~ +!!! unsafelayout: copying a compiler record by value can detach its tagged header; keep a pointer + var mappers [2]SimpleTypeMapper + ~ +!!! unsafelayout: a zero-value compiler record bypasses checked construction; use its owning factory + mappers[0].kind = TypeMapperKindSimple + return &literal, &mappers[0] + ~ +!!! unsafelayout: mapper storage held by value is untagged until an init*TypeMapper function tags it; pass its address there first + } + + func (m *SimpleTypeMapper) Map(t *Type) *Type { return t } + + func initSimpleTypeMapper(m *SimpleTypeMapper) *TypeMapper { + m.kind = TypeMapperKindSimple + return &m.TypeMapper + } + + type mapperOwner struct { + mapperStorage [2]SimpleTypeMapper + } + + func coallocatedMappers(owner *mapperOwner, t *Type) (*TypeMapper, *TypeMapper, *Type) { + tagged := initSimpleTypeMapper(&owner.mapperStorage[0]) + escaped := &owner.mapperStorage[1].TypeMapper + ~ +!!! unsafelayout: mapper storage held by value is untagged until an init*TypeMapper function tags it; pass its address there first + return tagged, escaped, owner.mapperStorage[1].Map(t) + ~ +!!! unsafelayout: mapper storage held by value is untagged until an init*TypeMapper function tags it; pass its address there first + } + + func zeroValueMapper() *TypeMapper { + var m SimpleTypeMapper + ~ +!!! unsafelayout: a zero-value compiler record bypasses checked construction; use its owning factory + return &m.TypeMapper + ~ +!!! unsafelayout: mapper storage held by value is untagged until an init*TypeMapper function tags it; pass its address there first + } + diff --git a/tools/customlint/unsafelayout.go b/tools/customlint/unsafelayout.go new file mode 100644 index 0000000000000..da7a994ec4faa --- /dev/null +++ b/tools/customlint/unsafelayout.go @@ -0,0 +1,823 @@ +package customlint + +import ( + "go/ast" + "go/constant" + "go/token" + "go/types" + "path/filepath" + "slices" + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" +) + +var unsafeLayoutAnalyzer = &analysis.Analyzer{ + Name: "unsafelayout", + Doc: "enforces construction, immutable tags, and checked casts for compiler records", + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: func(pass *analysis.Pass) (any, error) { + in := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + p := unsafeLayoutPass{pass: pass} + p.collectNodeLayouts() + for c := range in.Root().Preorder() { + p.check(c) + if expr, ok := c.Node().(ast.Expr); ok { + p.checkCopy(c, expr) + } + if name, ok := c.Node().(*ast.Ident); ok { + p.checkDeclaration(c, name) + } + } + return nil, nil + }, +} + +type unsafeLayoutPass struct { + pass *analysis.Pass + nodeLayouts map[*types.TypeName][]int64 +} + +func (p *unsafeLayoutPass) collectNodeLayouts() { + if !strings.HasSuffix(p.pass.Pkg.Path(), "/internal/ast") { + return + } + p.nodeLayouts = make(map[*types.TypeName][]int64) + for _, file := range p.pass.Files { + for _, declaration := range file.Decls { + fn, ok := declaration.(*ast.FuncDecl) + if !ok || fn.Recv == nil || !strings.HasPrefix(fn.Name.Name, "As") { + continue + } + function, _ := p.pass.TypesInfo.Defs[fn.Name].(*types.Func) + if function == nil { + continue + } + signature := function.Type().(*types.Signature) + if signature.Results().Len() != 1 { + continue + } + target := layoutNamedType(signature.Results().At(0).Type()) + if target == nil || layoutFamily(target) != "Node" || fn.Name.Name != "As"+target.Obj().Name() { + continue + } + var kinds []int64 + ast.Inspect(fn.Body, func(node ast.Node) bool { + if expr, isIdentifier := node.(*ast.Ident); isIdentifier { + if value, isConstant := p.pass.TypesInfo.Uses[expr].(*types.Const); isConstant && + value.Pkg() == p.pass.Pkg && layoutNamedType(value.Type()) != nil && + layoutNamedType(value.Type()).Obj().Name() == "Kind" { + if kind, isInteger := constant.Int64Val(value.Val()); isInteger { + kinds = append(kinds, kind) + } + } + } + return true + }) + if len(kinds) != 0 { + p.nodeLayouts[target.Obj()] = kinds + } + } + } +} + +func (p *unsafeLayoutPass) matchingNodeKind(typ types.Type, expr ast.Expr) bool { + named := layoutNamedType(typ) + if named == nil { + return false + } + value := p.pass.TypesInfo.Types[expr].Value + if value == nil || value.Kind() != constant.Int { + return false + } + kind, ok := constant.Int64Val(value) + if !ok { + return false + } + return slices.Contains(p.nodeLayouts[named.Obj()], kind) +} + +func layoutNamedType(typ types.Type) *types.Named { + if typ == nil { + return nil + } + if pointer, ok := types.Unalias(typ).(*types.Pointer); ok { + typ = pointer.Elem() + } + return namedType(typ) +} + +func layoutFamily(typ types.Type) string { + named := layoutNamedType(typ) + if named == nil || named.Obj().Pkg() == nil { + return "" + } + path := named.Obj().Pkg().Path() + if strings.HasSuffix(path, "/internal/ast") && (named.Obj().Name() == "Node" || named.Obj().Name() == "MutableNode") { + return "Node" + } + if strings.HasSuffix(path, "/internal/checker") { + switch named.Obj().Name() { + case "Type", "TypeMapper": + return named.Obj().Name() + } + } + if structure, ok := named.Underlying().(*types.Struct); ok && structure.NumFields() != 0 && structure.Field(0).Embedded() { + if _, pointer := types.Unalias(structure.Field(0).Type()).(*types.Pointer); !pointer { + return layoutFamily(structure.Field(0).Type()) + } + } + return "" +} + +// Reports whether typ holds a compiler record by value, directly or inside +// arrays or structs, so declaring it yields untagged zero-value records. +// Mappers coallocated as struct fields are exempt; checkMapperAddress makes +// sure their storage only escapes through a tagging init function. +func containsLayoutRecord(typ types.Type) bool { + return containsRecord(typ, false) +} + +func containsRecord(typ types.Type, field bool) bool { + if family := layoutFamily(typ); family != "" { + _, pointer := types.Unalias(typ).(*types.Pointer) + return !pointer && (family != "TypeMapper" || !field) + } + switch underlying := types.Unalias(typ).Underlying().(type) { + case *types.Array: + return containsRecord(underlying.Elem(), field) + case *types.Struct: + for f := range underlying.Fields() { + if containsRecord(f.Type(), true) { + return true + } + } + } + return false +} + +func isMapperValue(typ types.Type) bool { + if typ == nil { + return false + } + _, pointer := types.Unalias(typ).(*types.Pointer) + return !pointer && layoutFamily(typ) == "TypeMapper" +} + +// Mapper storage held by value starts with a zero tag, so its address may +// only escape as an argument to a checker init*TypeMapper function, which +// tags it. Selecting the embedded base of a mapper reached through a pointer +// is an ordinary upcast of an already-tagged record. +func (p *unsafeLayoutPass) checkMapperAddress(c inspector.Cursor, operand ast.Expr, pos token.Pos, what string) { + operand = ast.Unparen(operand) + if _, ok := operand.(*ast.CompositeLit); ok || !isMapperValue(p.pass.TypesInfo.TypeOf(operand)) { + return + } + if selector, ok := operand.(*ast.SelectorExpr); ok { + if _, pointer := types.Unalias(p.pass.TypesInfo.TypeOf(selector.X)).(*types.Pointer); pointer && + layoutFamily(p.pass.TypesInfo.TypeOf(selector.X)) == "TypeMapper" { + return + } + } + parent := c.Parent() + for { + if _, ok := parent.Node().(*ast.ParenExpr); !ok { + break + } + parent = parent.Parent() + } + if call, ok := parent.Node().(*ast.CallExpr); ok && what == "address" { + var object *types.Func + switch fun := ast.Unparen(call.Fun).(type) { + case *ast.SelectorExpr: + object, _ = p.pass.TypesInfo.Uses[fun.Sel].(*types.Func) + case *ast.Ident: + object, _ = p.pass.TypesInfo.Uses[fun].(*types.Func) + } + if object != nil && object.Pkg() != nil && strings.HasSuffix(object.Pkg().Path(), "/internal/checker") && + strings.HasPrefix(object.Name(), "init") && strings.HasSuffix(object.Name(), "TypeMapper") { + return + } + } + p.pass.Reportf(pos, "mapper storage held by value is untagged until an init*TypeMapper function tags it; pass its address there first") +} + +func layoutKind(typ types.Type) *types.Const { + named := layoutNamedType(typ) + if named == nil || named.Obj().Pkg() == nil { + return nil + } + var name string + switch layoutFamily(typ) { + case "Type": + name = "typeKind" + strings.TrimSuffix(named.Obj().Name(), "Type") + case "TypeMapper": + name = "TypeMapperKind" + strings.TrimSuffix(named.Obj().Name(), "TypeMapper") + default: + return nil + } + kind, _ := named.Obj().Pkg().Scope().Lookup(name).(*types.Const) + return kind +} + +func (p *unsafeLayoutPass) matchingKind(typ types.Type, expr ast.Expr) bool { + kind := layoutKind(typ) + value := p.pass.TypesInfo.Types[expr].Value + return kind != nil && value != nil && types.Identical(kind.Type(), p.pass.TypesInfo.TypeOf(expr)) && + constant.Compare(kind.Val(), token.EQL, value) +} + +func layoutFunction(c inspector.Cursor) *ast.FuncDecl { + for parent := c.Parent(); parent.Node() != nil; parent = parent.Parent() { + switch node := parent.Node().(type) { + case *ast.FuncLit: + return nil + case *ast.FuncDecl: + return node + } + } + return nil +} + +func (p *unsafeLayoutPass) ownerFunction(c inspector.Cursor, family, name string) bool { + fn := layoutFunction(c) + if fn == nil || fn.Name.Name != name { + return false + } + pkg := "/internal/checker" + if family == "Node" { + pkg = "/internal/ast" + } + if !strings.HasSuffix(p.pass.Pkg.Path(), pkg) { + return false + } + function, _ := p.pass.TypesInfo.Defs[fn.Name].(*types.Func) + if function == nil { + return false + } + signature := function.Type().(*types.Signature) + switch name { + case "castType", "castTypeMapper": + return signature.Recv() == nil + case "newNode": + return signature.Recv() == nil || + layoutNamedType(signature.Recv().Type()).Obj().Name() == "NodeFactory" + case "newType": + return signature.Recv() != nil && + layoutNamedType(signature.Recv().Type()).Obj().Name() == "Checker" + default: + return signature.Recv() != nil && layoutNamedType(signature.Recv().Type()).Obj().Name() == family + } +} + +func (p *unsafeLayoutPass) check(c inspector.Cursor) { + node := c.Node() + // Tests use detached metadata stubs; checked-cast pairing still applies. + test := strings.HasSuffix(p.pass.Fset.Position(node.Pos()).Filename, "_test.go") + switch node := node.(type) { + case *ast.TypeSpec: + if !node.Assign.IsValid() && layoutFamily(p.pass.TypesInfo.TypeOf(node.Type)) != "" { + // MutableNode changes the method set without changing the header layout. + if node.Name.Name != "MutableNode" || !strings.HasSuffix(p.pass.Pkg.Path(), "/internal/ast") { + p.pass.Reportf(node.Pos(), "defining a new compiler-record type bypasses checked construction; use a type alias") + } + } + case *ast.ValueSpec: + if test || len(node.Values) != 0 || p.layoutConstant(c) { + return + } + for _, name := range node.Names { + if variable := p.pass.TypesInfo.Defs[name]; variable != nil && containsLayoutRecord(variable.Type()) { + p.pass.Reportf(name.Pos(), "a zero-value compiler record bypasses checked construction; use its owning factory") + } + } + case *ast.IndexExpr: + if function, ok := node.X.(*ast.Ident); ok { + object, _ := p.pass.TypesInfo.Uses[function].(*types.Func) + if object != nil && strings.HasSuffix(object.Pkg().Path(), "/internal/checker") && + (object.Name() == "castType" || object.Name() == "castTypeMapper") { + call, ok := c.Parent().Node().(*ast.CallExpr) + if !ok || call.Fun != node || len(call.Args) != 2 || namedType(p.pass.TypesInfo.TypeOf(node.Index)) == nil || + !p.matchingKind(p.pass.TypesInfo.TypeOf(node.Index), call.Args[1]) { + p.pass.Reportf(node.Pos(), "checked cast must directly pair its concrete target type with its matching constant kind") + } + } + } + case *ast.AssignStmt: + if test { + return + } + for _, lhs := range node.Lhs { + if selector, ok := ast.Unparen(lhs).(*ast.SelectorExpr); ok { + p.checkTagWrite(c, selector, node) + } + } + case *ast.IncDecStmt: + if selector, ok := ast.Unparen(node.X).(*ast.SelectorExpr); ok && p.protectedTag(selector) && !test { + p.pass.Reportf(node.Pos(), "incrementing a compiler tag bypasses checked tag assignment") + } + case *ast.UnaryExpr: + if node.Op == token.AND { + if !test { + p.checkMapperAddress(c, node.X, node.Pos(), "address") + } + if selector, ok := ast.Unparen(node.X).(*ast.SelectorExpr); ok && p.protectedTag(selector) && !test { + p.pass.Reportf(node.Pos(), "taking a compiler tag's address bypasses checked tag assignment") + } + } + case *ast.CompositeLit: + if test || p.layoutConstant(c) { + return + } + if typ := p.pass.TypesInfo.TypeOf(node); layoutFamily(typ) == "" { + if _, slice := typ.Underlying().(*types.Slice); !slice && containsLayoutRecord(typ) { + p.pass.Reportf(node.Pos(), "a zero-value compiler record bypasses checked construction; use its owning factory") + } + return + } + family := layoutFamily(p.pass.TypesInfo.TypeOf(node)) + named := layoutNamedType(p.pass.TypesInfo.TypeOf(node)) + if named.Obj().Name() == family || named.Obj().Name() == "MutableNode" { + p.pass.Reportf(node.Pos(), "a bare compiler header has no concrete payload; use a concrete factory") + } + owner := "/internal/checker" + if family == "Node" { + owner = "/internal/ast" + } + if !strings.HasSuffix(p.pass.Pkg.Path(), owner) { + p.pass.Reportf(node.Pos(), "construct compiler records through their owning factories, not struct literals") + } else if unary, ok := c.Parent().Node().(*ast.UnaryExpr); ok && unary.Op == token.AND { + p.checkFreshRecord(c.Parent(), family) + } else { + p.pass.Reportf(node.Pos(), "allocate compiler records by pointer so checked construction can tag them") + } + for _, element := range node.Elts { + if field, ok := element.(*ast.KeyValueExpr); ok { + if key, ok := field.Key.(*ast.Ident); ok && (key.Name == "kind" || key.Name == "Kind") { + p.pass.Reportf(field.Pos(), "compiler tags must be assigned by checked construction, not struct literals") + } + } + } + case *ast.CallExpr: + p.checkCall(c, node, test) + case *ast.StarExpr, *ast.SelectorExpr, *ast.Ident: + if selector, ok := node.(*ast.SelectorExpr); ok && !test { + if selection := p.pass.TypesInfo.Selections[selector]; selection != nil && selection.Kind() == types.MethodVal { + if signature, ok := selection.Obj().Type().(*types.Signature); ok && signature.Recv() != nil { + if _, pointer := types.Unalias(signature.Recv().Type()).(*types.Pointer); pointer { + p.checkMapperAddress(c, selector.X, selector.Pos(), "method") + } + } + } + } + var object *types.Func + switch expr := node.(type) { + case *ast.SelectorExpr: + object, _ = p.pass.TypesInfo.Uses[expr.Sel].(*types.Func) + case *ast.Ident: + object, _ = p.pass.TypesInfo.Uses[expr].(*types.Func) + } + if object != nil && object.Pkg() != nil && + ((strings.HasSuffix(object.Pkg().Path(), "/internal/checker") && object.Name() == "newType") || + (strings.HasSuffix(object.Pkg().Path(), "/internal/ast") && object.Name() == "newNode")) { + call, ok := c.Parent().Node().(*ast.CallExpr) + if !ok || call.Fun != node { + // The selector's identifier is visited separately. + if selector, ok := c.Parent().Node().(*ast.SelectorExpr); !ok || selector.Sel != node { + p.pass.Reportf(node.Pos(), "checked construction must be called directly so its allocation/tag pairing can be verified") + } + } + } + case *ast.RangeStmt: + if node.Tok == token.ASSIGN && !test { + for _, target := range []ast.Expr{node.Key, node.Value} { + if selector, ok := ast.Unparen(target).(*ast.SelectorExpr); ok && p.protectedTag(selector) { + p.pass.Reportf(selector.Pos(), "range assignment to a compiler tag bypasses checked tag assignment") + } + } + } + } +} + +// Covers receivers, parameters, results, and := and range variables; plain +// var declarations are reported by the zero-value rule. +func (p *unsafeLayoutPass) checkDeclaration(c inspector.Cursor, name *ast.Ident) { + variable, ok := p.pass.TypesInfo.Defs[name].(*types.Var) + if !ok || variable.IsField() || !holdsRecordByValue(variable.Type()) || + strings.HasSuffix(p.pass.Fset.Position(name.Pos()).Filename, "_test.go") { + return + } + if _, ok := c.Parent().Node().(*ast.ValueSpec); ok { + return + } + p.pass.Reportf(name.Pos(), "declaring a compiler record by value copies or detaches its tagged header; use a pointer") +} + +// Reports whether typ is or contains a compiler record by value, including mappers. +func holdsRecordByValue(typ types.Type) bool { + if _, pointer := types.Unalias(typ).(*types.Pointer); pointer { + return false + } + return layoutFamily(typ) != "" || containsLayoutRecord(typ) +} + +// Every value-producing expression holding a record by value is a copy unless it +// is only addressed, has a field or method selected, or is an indexed array. +func (p *unsafeLayoutPass) checkCopy(c inspector.Cursor, expr ast.Expr) { + switch expr.(type) { + case *ast.CompositeLit, *ast.ParenExpr, *ast.FuncLit, *ast.KeyValueExpr: + return + } + if strings.HasSuffix(p.pass.Fset.Position(expr.Pos()).Filename, "_test.go") { + return + } + value, ok := p.pass.TypesInfo.Types[expr] + if !ok || value.IsType() || value.Type == nil || !holdsRecordByValue(value.Type) || p.layoutConstant(c) { + return + } + parent := c.Parent() + for { + if _, ok := parent.Node().(*ast.ParenExpr); !ok { + break + } + parent = parent.Parent() + } + switch parent := parent.Node().(type) { + case *ast.SelectorExpr: + if ast.Unparen(parent.X) == expr { + return + } + case *ast.UnaryExpr: + if parent.Op == token.AND { + return + } + case *ast.IndexExpr: + if _, array := value.Type.Underlying().(*types.Array); array && ast.Unparen(parent.X) == expr { + return + } + case *ast.SliceExpr: + if ast.Unparen(parent.X) == expr { + return + } + } + p.pass.Reportf(expr.Pos(), "copying a compiler record by value can detach its tagged header; keep a pointer") +} + +func (p *unsafeLayoutPass) protectedTag(selector *ast.SelectorExpr) bool { + return p.tagFamily(selector) != "" +} + +func (p *unsafeLayoutPass) tagFamily(selector *ast.SelectorExpr) string { + selection := p.pass.TypesInfo.Selections[selector] + if selection == nil || selection.Obj().Pkg() == nil { + return "" + } + field := selection.Obj() + if field.Name() != "Kind" && field.Name() != "kind" { + return "" + } + for _, family := range []string{"Node", "Type", "TypeMapper"} { + object := field.Pkg().Scope().Lookup(family) + if object == nil || layoutFamily(object.Type()) != family { + continue + } + structure := object.Type().Underlying().(*types.Struct) + for variable := range structure.Fields() { + if variable == field { + return family + } + } + } + return "" +} + +func (p *unsafeLayoutPass) checkTagWrite(c inspector.Cursor, selector *ast.SelectorExpr, assignment *ast.AssignStmt) { + if !p.protectedTag(selector) { + return + } + family := p.tagFamily(selector) + switch family { + case "Node": + if p.ownerFunction(c, family, "newNode") && layoutFunction(c).Recv == nil { + p.checkInitializer(c, selector, assignment) + return + } + p.pass.Reportf(selector.Pos(), "AST kind is immutable outside checked construction; create a new node") + case "Type": + if p.ownerFunction(c, family, "newType") { + p.checkInitializer(c, selector, assignment) + return + } + p.pass.Reportf(selector.Pos(), "type kind is immutable outside checked construction") + case "TypeMapper": + if assignment.Tok == token.ASSIGN && len(assignment.Lhs) == 1 && len(assignment.Rhs) == 1 && + p.matchingKind(p.pass.TypesInfo.TypeOf(selector.X), assignment.Rhs[0]) { + return + } + p.pass.Reportf(selector.Pos(), "mapper tag must match the concrete mapper allocation") + } +} + +// The tag must come from the constructor's own kind parameter, not from a +// shadowing local that happens to share its name. +func (p *unsafeLayoutPass) checkInitializer(c inspector.Cursor, selector *ast.SelectorExpr, assignment *ast.AssignStmt) { + if assignment.Tok == token.ASSIGN && len(assignment.Lhs) == 1 && len(assignment.Rhs) == 1 { + if kind, ok := ast.Unparen(assignment.Rhs[0]).(*ast.Ident); ok { + if function := layoutFunction(c); function != nil && function.Type.Params != nil { + for _, field := range function.Type.Params.List { + for _, name := range field.Names { + if name.Name == "kind" && p.pass.TypesInfo.Uses[kind] == p.pass.TypesInfo.Defs[name] { + return + } + } + } + } + } + } + p.pass.Reportf(selector.Pos(), "checked construction must initialize the tag from its kind parameter") +} + +func (p *unsafeLayoutPass) layoutConstant(c inspector.Cursor) bool { + for parent := c.Parent(); parent.Node() != nil; parent = parent.Parent() { + if call, ok := parent.Node().(*ast.CallExpr); ok { + if selector, ok := call.Fun.(*ast.SelectorExpr); ok { + object := p.pass.TypesInfo.Uses[selector.Sel] + if object != nil && object.Pkg() != nil && object.Pkg().Path() == "unsafe" && + (object.Name() == "Offsetof" || object.Name() == "Sizeof") { + return true + } + } + } + } + return false +} + +func (p *unsafeLayoutPass) checkCall(c inspector.Cursor, call *ast.CallExpr, test bool) { + var function *types.Func + switch expr := call.Fun.(type) { + case *ast.SelectorExpr: + function, _ = p.pass.TypesInfo.Uses[expr.Sel].(*types.Func) + case *ast.Ident: + function, _ = p.pass.TypesInfo.Uses[expr].(*types.Func) + } + if function != nil && function.Pkg() != nil && !test { + if strings.HasSuffix(function.Pkg().Path(), "/internal/checker") && function.Name() == "newType" { + valid := len(call.Args) == 4 && p.matchingUpcast(call.Args[2], call.Args[3], "Type", false) + if !valid { + p.pass.Reportf(call.Pos(), "newType must directly pair a concrete AsType() receiver with its matching constant kind") + } + } + if strings.HasSuffix(function.Pkg().Path(), "/internal/ast") && function.Name() == "newNode" && + !p.ownerFunction(c, "Node", "newNode") { + valid := len(call.Args) >= 2 && p.matchingUpcast(call.Args[1], call.Args[0], "Node", p.generatedAST(c)) + if !valid { + p.pass.Reportf(call.Pos(), "newNode must pair a concrete AsNode() receiver with a compatible kind") + } + } + } + if selector, ok := call.Fun.(*ast.SelectorExpr); ok && selector.Sel.Name == "New" && len(call.Args) == 0 && !test { + if family := layoutFamily(p.pass.TypesInfo.TypeOf(call)); family != "" { + p.checkFreshRecord(c, family) + } + } + if builtin, ok := call.Fun.(*ast.Ident); ok && (builtin.Name == "new" || builtin.Name == "make") && !test && len(call.Args) != 0 { + allocated := p.pass.TypesInfo.TypeOf(call.Args[0]) + // new(T) of a record itself is reported with the allocation rules below. + direct := builtin.Name == "new" && layoutFamily(allocated) != "" + if slice, ok := types.Unalias(allocated).(*types.Slice); ok && builtin.Name == "make" { + allocated = slice.Elem() + } + if allocated != nil && !direct && containsLayoutRecord(allocated) { + p.pass.Reportf(call.Pos(), "a zero-value compiler record bypasses checked construction; use its owning factory") + } + } + if builtin, ok := ast.Unparen(call.Fun).(*ast.Ident); ok && !test && len(call.Args) != 0 { + if _, ok := p.pass.TypesInfo.Uses[builtin].(*types.Builtin); ok && + (builtin.Name == "copy" || builtin.Name == "append" && call.Ellipsis.IsValid()) { + // Element-wise copies are invisible to checkCopy because the + // arguments are slices. + if slice, ok := types.Unalias(p.pass.TypesInfo.TypeOf(call.Args[0])).Underlying().(*types.Slice); ok && + holdsRecordByValue(slice.Elem()) { + p.pass.Reportf(call.Pos(), "copying a compiler record by value can detach its tagged header; keep a pointer") + } + } + } + if len(call.Args) != 1 { + return + } + if builtin, ok := call.Fun.(*ast.Ident); ok && builtin.Name == "new" { + family := layoutFamily(p.pass.TypesInfo.TypeOf(call)) + if family != "" && !test { + named := layoutNamedType(p.pass.TypesInfo.TypeOf(call)) + if named.Obj().Name() == family || named.Obj().Name() == "MutableNode" { + p.pass.Reportf(call.Pos(), "a bare compiler header has no concrete payload; use a concrete factory") + } + owner := "/internal/checker" + if family == "Node" { + owner = "/internal/ast" + } + if !strings.HasSuffix(p.pass.Pkg.Path(), owner) { + p.pass.Reportf(call.Pos(), "allocate compiler records through their owning factories") + } else { + p.checkFreshRecord(c, family) + } + } + } + source := p.pass.TypesInfo.TypeOf(call.Args[0]) + target := p.pass.TypesInfo.TypeOf(call) + isUnsafePointer := func(typ types.Type) bool { + if typ == nil { + return false + } + // Aliases and defined types share unsafe.Pointer's conversion rules. + basic, ok := typ.Underlying().(*types.Basic) + return ok && basic.Kind() == types.UnsafePointer + } + if !isUnsafePointer(source) && !isUnsafePointer(target) { + return + } + family := layoutFamily(target) + if family == "" { + family = layoutFamily(source) + } + if family == "" { + if pointer, ok := call.Args[0].(*ast.CallExpr); ok && len(pointer.Args) == 1 { + family = layoutFamily(p.pass.TypesInfo.TypeOf(pointer.Args[0])) + } + } + if family == "" { + return + } + allowed := false + switch family { + case "Node": + allowed = p.generatedAST(c) || p.ownerFunction(c, family, "AsFlowSwitchClauseData") || + p.ownerFunction(c, family, "AsFlowReduceLabelData") + case "Type": + allowed = p.ownerFunction(c, family, "castType") + for _, base := range []string{"ConstrainedType", "StructuredType", "ObjectType", "TypeReference", "InterfaceType", "UnionOrIntersectionType"} { + allowed = allowed || p.ownerFunction(c, family, "As"+base) + } + case "TypeMapper": + allowed = p.ownerFunction(c, family, "castTypeMapper") + } + if !allowed { + p.pass.Reportf(call.Pos(), "reinterpret compiler records only through checked casts or generated AST dispatch") + } +} + +func (p *unsafeLayoutPass) generatedAST(c inspector.Cursor) bool { + for parent := c; parent.Node() != nil; parent = parent.Parent() { + if file, ok := parent.Node().(*ast.File); ok { + return filepath.Base(p.pass.Fset.Position(file.Pos()).Filename) == "ast_generated.go" && ast.IsGenerated(file) && + strings.HasSuffix(p.pass.Pkg.Path(), "/internal/ast") + } + } + return false +} + +func (p *unsafeLayoutPass) matchingUpcast(expr, kind ast.Expr, family string, generated bool) bool { + call, ok := expr.(*ast.CallExpr) + if !ok || len(call.Args) != 0 { + return false + } + method, ok := call.Fun.(*ast.SelectorExpr) + if !ok || method.Sel.Name != "As"+family { + return false + } + selection := p.pass.TypesInfo.Selections[method] + if selection == nil { + return false + } + signature := selection.Obj().Type().(*types.Signature) + receiver := layoutNamedType(signature.Recv().Type()) + if receiver == nil || layoutFamily(receiver) != family || + (receiver.Obj().Name() != family && !(family == "Type" && receiver.Obj().Name() == "TypeBase")) { + return false + } + if family == "Node" { + return p.matchingNodeKind(p.pass.TypesInfo.TypeOf(method.X), kind) || + (generated && p.pass.TypesInfo.Types[kind].Value == nil) + } + return p.matchingKind(p.pass.TypesInfo.TypeOf(method.X), kind) +} + +// A fresh record has a zero tag until checked construction assigns one, so it +// must not escape before then. Types and nodes must be upcast straight into +// newType or newNode, or held in a local that is only used through its own +// fields until that upcast. Mappers must be tagged by the next statement. +func (p *unsafeLayoutPass) checkFreshRecord(alloc inspector.Cursor, family string) { + parent := alloc.Parent() + for { + if _, ok := parent.Node().(*ast.ParenExpr); !ok { + break + } + alloc, parent = parent, parent.Parent() + } + if p.constructorUpcast(alloc, family) { + return + } + assignment, ok := parent.Node().(*ast.AssignStmt) + if !ok || assignment.Tok != token.DEFINE || len(assignment.Lhs) != 1 || len(assignment.Rhs) != 1 { + p.pass.Reportf(alloc.Node().Pos(), "a fresh compiler record must be checked-constructed before it escapes") + return + } + name, _ := assignment.Lhs[0].(*ast.Ident) + variable := p.pass.TypesInfo.Defs[name] + if variable == nil { + p.pass.Reportf(alloc.Node().Pos(), "a fresh compiler record must be checked-constructed before it escapes") + return + } + if family == "TypeMapper" { + if next, ok := parent.NextSibling(); ok { + if tag, ok := next.Node().(*ast.AssignStmt); ok && len(tag.Lhs) == 1 { + if selector, ok := tag.Lhs[0].(*ast.SelectorExpr); ok && p.protectedTag(selector) { + if x, ok := selector.X.(*ast.Ident); ok && p.pass.TypesInfo.Uses[x] == variable { + return + } + } + } + } + p.pass.Reportf(alloc.Node().Pos(), "a fresh mapper must be tagged by the statement after its allocation") + return + } + scope := parent.Parent() + for scope.Node() != nil { + if _, ok := scope.Node().(*ast.BlockStmt); ok { + if _, ok := scope.Parent().Node().(*ast.FuncDecl); ok { + break + } + if _, ok := scope.Parent().Node().(*ast.FuncLit); ok { + break + } + } + scope = scope.Parent() + } + constructed := false + escaped := false + for use := range scope.Preorder((*ast.Ident)(nil)) { + if p.pass.TypesInfo.Uses[use.Node().(*ast.Ident)] != variable { + continue + } + selector, ok := use.Parent().Node().(*ast.SelectorExpr) + if !ok || selector.X != use.Node() { + escaped = true + continue + } + selection := p.pass.TypesInfo.Selections[selector] + switch { + case selection == nil: + escaped = true + case selection.Kind() == types.FieldVal: + if field := selection.Obj().(*types.Var); field.Embedded() { + escaped = true + } + case p.constructorUpcast(use, family): + constructed = true + default: + escaped = true + } + } + if escaped || !constructed { + p.pass.Reportf(alloc.Node().Pos(), "a fresh compiler record must be checked-constructed before it escapes") + } +} + +// Reports whether receiver is upcast directly into the record argument of +// newType or newNode. checkCall separately verifies the kind pairing. +func (p *unsafeLayoutPass) constructorUpcast(receiver inspector.Cursor, family string) bool { + method, ok := receiver.Parent().Node().(*ast.SelectorExpr) + if !ok || method.X != receiver.Node() || method.Sel.Name != "As"+family { + return false + } + upcast, ok := receiver.Parent().Parent().Node().(*ast.CallExpr) + if !ok || upcast.Fun != method { + return false + } + constructor, ok := receiver.Parent().Parent().Parent().Node().(*ast.CallExpr) + if !ok { + return false + } + var name *ast.Ident + switch fun := constructor.Fun.(type) { + case *ast.SelectorExpr: + name = fun.Sel + case *ast.Ident: + name = fun + } + function, _ := p.pass.TypesInfo.Uses[name].(*types.Func) + if function == nil || function.Pkg() == nil { + return false + } + switch family { + case "Type": + return strings.HasSuffix(function.Pkg().Path(), "/internal/checker") && function.Name() == "newType" && + len(constructor.Args) == 4 && constructor.Args[2] == upcast + case "Node": + return strings.HasSuffix(function.Pkg().Path(), "/internal/ast") && function.Name() == "newNode" && + len(constructor.Args) >= 2 && constructor.Args[1] == upcast + } + return false +} diff --git a/tsc/internal/ast/ast.go b/tsc/internal/ast/ast.go index 48ff1a8d63d56..e996287c91792 100644 --- a/tsc/internal/ast/ast.go +++ b/tsc/internal/ast/ast.go @@ -174,6 +174,8 @@ func (list *ModifierList) Clone(f *NodeFactory) *ModifierList { // Every concrete AST node embeds Node at offset zero. Kind identifies the // enclosing allocation, which generated accessors recover without a data pointer. +// Initialize Kind only in newNode; transformations must construct a new node +// rather than retagging one. The unsafelayout linter enforces this boundary. type Node struct { Kind Kind diff --git a/tsc/internal/ast/nodeaccessors_test.go b/tsc/internal/ast/nodeaccessors_test.go index c18342076dbdb..f842ed1fc2f89 100644 --- a/tsc/internal/ast/nodeaccessors_test.go +++ b/tsc/internal/ast/nodeaccessors_test.go @@ -81,11 +81,11 @@ func TestNodeAccessorsSharedKinds(t *testing.T) { factory := ast.NewNodeFactory(ast.NodeFactoryHooks{}) name := factory.NewIdentifier("T") modifiers := factory.NewModifierList([]*ast.Node{factory.NewToken(ast.KindExportKeyword)}) - node := factory.NewTypeAliasDeclaration(modifiers, name, nil, nil) - data := node.AsTypeAliasDeclaration() - - for _, kind := range []ast.Kind{ast.KindTypeAliasDeclaration, ast.KindJSTypeAliasDeclaration} { - node.Kind = kind + for _, node := range []*ast.Node{ + factory.NewTypeAliasDeclaration(modifiers, name, nil, nil), + factory.NewJSTypeAliasDeclaration(modifiers, name, nil, nil), + } { + data := node.AsTypeAliasDeclaration() assert.Equal(t, node.Name(), name) assert.Equal(t, node.Modifiers(), modifiers) assert.Equal(t, node.DeclarationData(), &data.DeclarationBase) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 144c82f02c93a..a4c1366c52387 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -674,6 +674,7 @@ type Checker struct { symbolWithDataArena core.Arena[ast.SymbolWithData] symbolExtraArena ast.SymbolExtraArena signatureArena core.Arena[Signature] + signatureExtraArena core.Arena[signatureExtra] indexInfoArena core.Arena[IndexInfo] literalTypeArena core.Arena[LiteralType] objectTypeArena core.Arena[ObjectType] @@ -686,16 +687,21 @@ type Checker struct { indexedAccessTypeArena core.Arena[IndexedAccessType] conditionalTypeArena core.Arena[ConditionalType] typeListArena core.Arena[*Type] + outerTypeParameterListArena core.Arena[[]*Type] valueSymbolExtraArena core.Arena[valueSymbolExtra] structuredTypeExtraArena core.Arena[structuredTypeExtra] + memberIndexArena core.Arena[memberIndex] + memberIndexSlotsArena core.Arena[uint32] freeTypeSets *temporaryTypeSet + freePropertyCounts *temporaryPropertyCounts + freeNameSets []map[string]struct{} mergedSymbols map[*ast.Symbol]*ast.Symbol mergedExportsChecked collections.Set[*ast.Symbol] factory ast.NodeFactory nodeLinks core.LinkStore[*ast.Node, NodeLinks] - signatureLinks core.LinkStore[*ast.Node, SignatureLinks] + signatureLinks nodeLinkStore[SignatureLinks] symbolNodeLinks nodeLinkStore[SymbolNodeLinks] - typeNodeLinks core.LinkStore[*ast.Node, TypeNodeLinks] + typeNodeLinks nodeLinkStore[TypeNodeLinks] enumMemberLinks core.LinkStore[*ast.Node, EnumMemberLinks] assertionLinks core.LinkStore[*ast.Node, AssertionLinks] arrayLiteralLinks core.LinkStore[*ast.Node, ArrayLiteralLinks] @@ -4388,7 +4394,7 @@ func (c *Checker) checkClassLikeDeclaration(node *ast.Node) { if len(baseTypeNode.TypeArguments()) != 0 { c.checkSourceElements(baseTypeNode.TypeArguments()) for _, constructor := range c.getConstructorsForTypeArguments(staticBaseType, baseTypeNode.TypeArguments(), baseTypeNode) { - if !c.checkTypeArgumentConstraints(baseTypeNode, constructor.typeParameters) { + if !c.checkTypeArgumentConstraints(baseTypeNode, constructor.TypeParameters()) { break } } @@ -7607,7 +7613,7 @@ func (c *Checker) getQuickTypeOfExpression(node *ast.Node) *Type { func (c *Checker) getReturnTypeOfSingleNonGenericSignature(funcType *Type, kind SignatureKind) *Type { signature := c.getSingleSignature(funcType, kind, true /*allowMembers*/) - if signature != nil && len(signature.typeParameters) == 0 { + if signature != nil && len(signature.TypeParameters()) == 0 { return c.getReturnTypeOfSignature(signature) } return nil @@ -7824,7 +7830,7 @@ func (c *Checker) instantiateTypeWithSingleGenericCallSignature(node *ast.Node, callSignature := c.getSingleSignature(t, SignatureKindCall, true /*allowMembers*/) constructSignature := c.getSingleSignature(t, SignatureKindConstruct, true /*allowMembers*/) signature := core.OrElse(callSignature, constructSignature) - if signature == nil || len(signature.typeParameters) == 0 { + if signature == nil || len(signature.TypeParameters()) == 0 { return t } contextualType := c.getApparentTypeOfContextualType(node, ContextFlagsNoConstraints) @@ -7832,7 +7838,7 @@ func (c *Checker) instantiateTypeWithSingleGenericCallSignature(node *ast.Node, return t } contextualSignature := c.getSingleSignature(c.GetNonNullableType(contextualType), core.IfElse(callSignature != nil, SignatureKindCall, SignatureKindConstruct), false /*allowMembers*/) - if contextualSignature == nil || len(contextualSignature.typeParameters) != 0 { + if contextualSignature == nil || len(contextualSignature.TypeParameters()) != 0 { return t } if checkMode&CheckModeSkipGenericFunctions != 0 { @@ -7853,10 +7859,10 @@ func (c *Checker) instantiateTypeWithSingleGenericCallSignature(node *ast.Node, returnSignature = c.getSingleCallOrConstructSignature(returnType) } } - if returnSignature != nil && len(returnSignature.typeParameters) == 0 && !core.Every(context.inferences, hasInferenceCandidates) { + if returnSignature != nil && len(returnSignature.TypeParameters()) == 0 && !core.Every(context.inferences, hasInferenceCandidates) { // Instantiate the signature with its own type parameters as type arguments, possibly // renaming the type parameters to ensure they have unique names. - uniqueTypeParameters := c.getUniqueTypeParameters(context, signature.typeParameters) + uniqueTypeParameters := c.getUniqueTypeParameters(context, signature.TypeParameters()) instantiatedSignature := c.getSignatureInstantiationWithoutFillingInTypeArguments(signature, uniqueTypeParameters) // Infer from the parameters of the instantiated signature to the parameters of the // contextual signature starting with an empty set of inference candidates. @@ -8946,7 +8952,7 @@ func (c *Checker) typeHasProtectedAccessibleBase(target *ast.Symbol, t *Type) bo func someSignature(signatures []*Signature, f func(s *Signature) bool) bool { for _, sig := range signatures { - if sig.composite != nil && sig.composite.isUnion && core.Some(sig.composite.signatures, f) || sig.composite == nil && f(sig) { + if sig.getComposite() != nil && sig.getComposite().isUnion && core.Some(sig.getComposite().signatures, f) || sig.getComposite() == nil && f(sig) { return true } } @@ -9117,7 +9123,7 @@ func (c *Checker) resolveCall(node *ast.Node, signatures []*Signature, candidate // // For a decorator, no arguments are susceptible to contextual typing due to the fact // decorators are applied to a declaration by the emitter, and not to an expression. - s.isSingleNonGenericCandidate = len(s.candidates) == 1 && len(s.candidates[0].typeParameters) == 0 + s.isSingleNonGenericCandidate = len(s.candidates) == 1 && len(s.candidates[0].TypeParameters()) == 0 if !isDecorator && !s.isSingleNonGenericCandidate && core.Some(s.args, c.isContextSensitive) { s.argCheckMode = CheckModeSkipContextSensitive } else { @@ -9284,7 +9290,7 @@ func (c *Checker) chooseOverload(s *CallState, relation *Relation) *Signature { } var checkCandidate *Signature var inferenceContext *InferenceContext - if len(candidate.typeParameters) != 0 { + if len(candidate.TypeParameters()) != 0 { var typeArgumentTypes []*Type if len(s.typeArguments) != 0 { typeArgumentTypes = c.checkTypeArguments(candidate, s.typeArguments, false /*reportErrors*/, nil) @@ -9297,7 +9303,7 @@ func (c *Checker) chooseOverload(s *CallState, relation *Relation) *Signature { // type inference to avoid circularity errors. For example, see #64192. inferenceFlags := core.IfElse(s.recursiveResolution && len(s.candidates) == 1, InferenceFlagsNoConstraintChecks, InferenceFlagsNone) | core.IfElse(ast.IsInJSFile(s.node), InferenceFlagsAnyDefault, InferenceFlagsNone) - inferenceContext = c.newInferenceContext(candidate.typeParameters, candidate, inferenceFlags /*flags*/, nil) + inferenceContext = c.newInferenceContext(candidate.TypeParameters(), candidate, inferenceFlags /*flags*/, nil) typeArgumentTypes = c.inferTypeArguments(s.node, candidate, s.args, s.argCheckMode|CheckModeSkipGenericFunctions, inferenceContext) if inferenceContext.flags&InferenceFlagsSkippedGenericFunction != 0 { s.argCheckMode |= CheckModeSkipGenericFunctions @@ -9459,14 +9465,14 @@ func (c *Checker) getLegacyDecoratorArgumentCount(node *ast.Node, signature *Sig func (c *Checker) hasCorrectTypeArgumentArity(signature *Signature, typeArguments []*ast.Node) bool { // If the user supplied type arguments, but the number of type arguments does not match // the declared number of type parameters, the call has an incorrect arity. - numTypeParameters := len(signature.typeParameters) - minTypeArgumentCount := c.getMinTypeArgumentCount(signature.typeParameters) + numTypeParameters := len(signature.TypeParameters()) + minTypeArgumentCount := c.getMinTypeArgumentCount(signature.TypeParameters()) return len(typeArguments) == 0 || len(typeArguments) >= minTypeArgumentCount && len(typeArguments) <= numTypeParameters } func (c *Checker) checkTypeArguments(signature *Signature, typeArgumentNodes []*ast.Node, reportErrors bool, headMessage *diagnostics.Message) []*Type { isJavaScript := ast.IsInJSFile(signature.declaration) - typeParameters := signature.typeParameters + typeParameters := signature.TypeParameters() typeArgumentTypes := c.fillMissingTypeArguments(core.Map(typeArgumentNodes, c.getTypeFromTypeNode), typeParameters, c.getMinTypeArgumentCount(typeParameters), isJavaScript) var mapper *TypeMapper for i := range typeArgumentNodes { @@ -9641,7 +9647,7 @@ func (c *Checker) inferTypeArguments(node *ast.Node, signature *Signature, args // 'let f: (x: string) => number = wrap(s => s.length)', we infer from the declared type of 'f' to the // return type of 'wrap'. if !ast.IsDecorator(node) && !ast.IsBinaryExpression(node) { - skipBindingPatterns := core.Every(signature.typeParameters, func(p *Type) bool { return c.getDefaultFromTypeParameter(p) != nil }) + skipBindingPatterns := core.Every(signature.TypeParameters(), func(p *Type) bool { return c.getDefaultFromTypeParameter(p) != nil }) contextualType := c.getContextualType(node, core.IfElse(skipBindingPatterns, ContextFlagsSkipBindingPatterns, ContextFlagsNone)) if contextualType != nil { inferenceTargetType := c.getReturnTypeOfSignature(signature) @@ -9673,8 +9679,8 @@ func (c *Checker) inferTypeArguments(node *ast.Node, signature *Signature, args // Above, the type of the 'value' parameter is inferred to be 'A'. contextualSignature := c.getSingleCallSignature(instantiatedType) var inferenceSourceType *Type - if contextualSignature != nil && len(contextualSignature.typeParameters) != 0 { - inferenceSourceType = c.getOrCreateTypeFromSignature(c.getSignatureInstantiationWithoutFillingInTypeArguments(contextualSignature, contextualSignature.typeParameters)) + if contextualSignature != nil && len(contextualSignature.TypeParameters()) != 0 { + inferenceSourceType = c.getOrCreateTypeFromSignature(c.getSignatureInstantiationWithoutFillingInTypeArguments(contextualSignature, contextualSignature.TypeParameters())) } else { inferenceSourceType = instantiatedType } @@ -9689,7 +9695,7 @@ func (c *Checker) inferTypeArguments(node *ast.Node, signature *Signature, args // replaced with inferences produced from the outer return type or preceding outer arguments. // This protects against circular inferences, i.e. avoiding situations where inferences reference // type parameters for which the inferences are being made. - returnContext := c.newInferenceContext(signature.typeParameters, signature, context.flags, nil) + returnContext := c.newInferenceContext(signature.TypeParameters(), signature, context.flags, nil) var outerReturnMapper *TypeMapper if outerContext != nil { outerReturnMapper = c.createOuterReturnMapper(outerContext) @@ -9746,7 +9752,7 @@ func (c *Checker) getCandidateForOverloadFailure(node *ast.Node, candidates []*S // Normally we will combine overloads. Skip this if they have type parameters since that's hard to combine. // Don't do this if there is a `candidatesOutArray`, // because then we want the chosen best candidate to be one of the overloads, not a combination. - if hasCandidatesOutArray || len(candidates) == 1 || core.Some(candidates, func(s *Signature) bool { return len(s.typeParameters) != 0 }) { + if hasCandidatesOutArray || len(candidates) == 1 || core.Some(candidates, func(s *Signature) bool { return len(s.TypeParameters()) != 0 }) { return c.pickLongestCandidateSignature(node, candidates, args, checkMode) } return c.createUnionOfSignaturesForOverloadFailure(candidates) @@ -9765,7 +9771,7 @@ func (c *Checker) pickLongestCandidateSignature(node *ast.Node, candidates []*Si } bestIndex := c.getLongestCandidateIndex(candidates, argCount) candidate := candidates[bestIndex] - typeParameters := candidate.typeParameters + typeParameters := candidate.TypeParameters() if len(typeParameters) == 0 { return candidate } @@ -9824,7 +9830,7 @@ func (c *Checker) inferSignatureInstantiationForOverloadFailure(node *ast.Node, } func (c *Checker) createUnionOfSignaturesForOverloadFailure(candidates []*Signature) *Signature { - thisParameters := core.MapNonNil(candidates, func(c *Signature) *ast.Symbol { return c.thisParameter }) + thisParameters := core.MapNonNil(candidates, func(c *Signature) *ast.Symbol { return c.ThisParameter() }) var thisParameter *ast.Symbol if len(thisParameters) != 0 { thisParameter = c.createCombinedSymbolFromTypes(thisParameters, core.Map(thisParameters, c.getTypeOfParameter)) @@ -9938,7 +9944,7 @@ func (c *Checker) addImplementationSuccessElaboration(s *CallState, failed *Sign candidate := c.getSignatureFromDeclaration(implementation) localState := *s localState.candidates = []*Signature{candidate} - localState.isSingleNonGenericCandidate = len(candidate.typeParameters) == 0 + localState.isSingleNonGenericCandidate = len(candidate.TypeParameters()) == 0 if c.chooseOverload(&localState, c.assignableRelation) != nil { diagnostic.AddRelatedInfo(NewDiagnosticForNode(implementation, diagnostics.The_call_would_have_succeeded_against_this_implementation_but_implementation_signatures_of_overloads_are_not_externally_visible)) } @@ -10020,7 +10026,7 @@ func (c *Checker) getArgumentArityError(node *ast.Node, signatures []*Signature, } var parameter *ast.Node if closestSignature != nil && closestSignature.declaration != nil { - parameter = core.ElementOrNil(closestSignature.declaration.Parameters(), len(args)+core.IfElse(closestSignature.thisParameter != nil, 1, 0)) + parameter = core.ElementOrNil(closestSignature.declaration.Parameters(), len(args)+core.IfElse(closestSignature.ThisParameter() != nil, 1, 0)) } if parameter != nil { var related *ast.Diagnostic @@ -10104,8 +10110,8 @@ func (c *Checker) getTypeArgumentArityError(node *ast.Node, signatures []*Signat if len(signatures) == 1 { // No overloads exist sig := signatures[0] - minCount := c.getMinTypeArgumentCount(sig.typeParameters) - maxCount := len(sig.typeParameters) + minCount := c.getMinTypeArgumentCount(sig.TypeParameters()) + maxCount := len(sig.TypeParameters()) expected := strconv.Itoa(minCount) if minCount < maxCount { expected = expected + "-" + strconv.Itoa(maxCount) @@ -10116,8 +10122,8 @@ func (c *Checker) getTypeArgumentArityError(node *ast.Node, signatures []*Signat belowArgCount := math.MinInt aboveArgCount := math.MaxInt for _, sig := range signatures { - minCount := c.getMinTypeArgumentCount(sig.typeParameters) - maxCount := len(sig.typeParameters) + minCount := c.getMinTypeArgumentCount(sig.TypeParameters()) + maxCount := len(sig.TypeParameters()) if minCount > argCount { aboveArgCount = min(aboveArgCount, minCount) } else if maxCount < argCount { @@ -10264,7 +10270,7 @@ func (c *Checker) invocationErrorRecovery(apparentType *Type, kind SignatureKind } func (c *Checker) isGenericFunctionReturningFunction(signature *Signature) bool { - return len(signature.typeParameters) != 0 && c.isFunctionType(c.getReturnTypeOfSignature(signature)) + return len(signature.TypeParameters()) != 0 && c.isFunctionType(c.getReturnTypeOfSignature(signature)) } func (c *Checker) skippedGenericFunction(node *ast.Node, checkMode CheckMode) { @@ -10545,7 +10551,7 @@ func (c *Checker) getContextualSignature(node *ast.Node) *Signature { func (c *Checker) createUnionSignature(sig *Signature, unionSignatures []*Signature) *Signature { result := c.cloneSignature(sig) - result.composite = &CompositeSignature{isUnion: true, signatures: unionSignatures} + result.setComposite(c, &CompositeSignature{isUnion: true, signatures: unionSignatures}) result.target = nil result.mapper = nil return result @@ -10571,7 +10577,7 @@ func (c *Checker) getIntersectedSignatures(signatures []*Signature) *Signature { switch { case combined == sig || combined == nil: combined = sig - case c.compareTypeParametersIdentical(combined.typeParameters, sig.typeParameters): + case c.compareTypeParametersIdentical(combined.TypeParameters(), sig.TypeParameters()): combined = c.combineUnionOrIntersectionMemberSignatures(combined, sig, false /*isUnion*/) default: return nil @@ -10598,20 +10604,20 @@ func (c *Checker) isAritySmaller(signature *Signature, target *ast.Node) bool { } func (c *Checker) assignContextualParameterTypes(sig *Signature, context *Signature) { - if len(context.typeParameters) != 0 { - if len(sig.typeParameters) != 0 { + if len(context.TypeParameters()) != 0 { + if len(sig.TypeParameters()) != 0 { // This signature has already has a contextual inference performed and cached on it return } - sig.typeParameters = context.typeParameters + sig.setTypeParameters(c, context.TypeParameters()) } - if context.thisParameter != nil { - parameter := sig.thisParameter + if context.ThisParameter() != nil { + parameter := sig.ThisParameter() if parameter == nil || parameter.ValueDeclaration() != nil && parameter.ValueDeclaration().Type() == nil { if parameter == nil { - sig.thisParameter = c.createSymbolWithType(context.thisParameter, nil /*type*/) + sig.setThisParameter(c, c.createSymbolWithType(context.ThisParameter(), nil /*type*/)) } - c.assignParameterType(sig.thisParameter, c.getTypeOfSymbol(context.thisParameter)) + c.assignParameterType(sig.ThisParameter(), c.getTypeOfSymbol(context.ThisParameter())) } } length := len(sig.parameters) - core.IfElse(signatureHasRestParameter(sig), 1, 0) @@ -10644,8 +10650,8 @@ func (c *Checker) assignContextualParameterTypes(sig *Signature, context *Signat } func (c *Checker) assignNonContextualParameterTypes(signature *Signature) { - if signature.thisParameter != nil { - c.assignParameterType(signature.thisParameter, nil) + if signature.ThisParameter() != nil { + c.assignParameterType(signature.ThisParameter(), nil) } for _, parameter := range signature.parameters { c.assignParameterType(parameter, nil) @@ -10921,7 +10927,7 @@ func (c *Checker) getInstantiationExpressionType(exprType *Type, node *ast.Node) var nonApplicableType *Type getInstantiatedSignatures := func(signatures []*Signature) []*Signature { applicableSignatures := core.Filter(signatures, func(sig *Signature) bool { - return len(sig.typeParameters) != 0 && c.hasCorrectTypeArgumentArity(sig, typeArguments.Nodes) + return len(sig.TypeParameters()) != 0 && c.hasCorrectTypeArgumentArity(sig, typeArguments.Nodes) }) return core.SameMap(applicableSignatures, func(sig *Signature) *Signature { typeArgumentTypes := c.checkTypeArguments(sig, typeArguments.Nodes, true /*reportErrors*/, nil) @@ -10948,7 +10954,7 @@ func (c *Checker) getInstantiationExpressionType(exprType *Type, node *ast.Node) debug.Assert(t.symbol != nil, "Instantiation expression source type must have a symbol") symbol.SetDeclarations(t.symbol.Declarations()) result := c.newObjectType(ObjectFlagsAnonymous|ObjectFlagsInstantiationExpressionType, symbol) - c.setStructuredTypeMembers(result, resolved.members, callSignatures, constructSignatures, resolved.getIndexInfos()) + c.setStructuredTypeMembers(result, resolved.getMembers(), callSignatures, constructSignatures, resolved.getIndexInfos()) result.AsInstantiationExpressionType().node = node return result } @@ -12284,7 +12290,7 @@ func (c *Checker) getContextualThisParameterType(fn *ast.Node) *Type { if c.isContextSensitiveFunctionOrObjectLiteralMethod(fn) { contextualSignature := c.getContextualSignature(fn) if contextualSignature != nil { - thisParameter := contextualSignature.thisParameter + thisParameter := contextualSignature.ThisParameter() if thisParameter != nil { return c.getTypeOfSymbol(thisParameter) } @@ -16160,7 +16166,7 @@ func (c *Checker) cloneTypeAsModuleType(symbol *ast.Symbol, moduleType *Type, re links.target = symbol links.originatingImport = referenceParent resolvedModuleType := c.resolveStructuredTypeMembers(moduleType) - c.valueSymbolLinks.Get(result).resolvedType = c.newAnonymousType(result, resolvedModuleType.members, nil, nil, resolvedModuleType.getIndexInfos()) + c.valueSymbolLinks.Get(result).resolvedType = c.newAnonymousType(result, resolvedModuleType.getMembers(), nil, nil, resolvedModuleType.getIndexInfos()) return result } @@ -17155,7 +17161,7 @@ func (c *Checker) getTypeForVariableLikeDeclaration(declaration *ast.Node, inclu if thisParameter != nil && declaration == thisParameter { // Use the type from the *getter* debug.Assert(thisParameter.Type() == nil) - return c.getTypeOfSymbol(getterSignature.thisParameter) + return c.getTypeOfSymbol(getterSignature.ThisParameter()) } return c.getReturnTypeOfSignature(getterSignature) } @@ -17459,7 +17465,7 @@ func (c *Checker) isMixinConstructorType(t *Type) bool { signatures := c.getSignaturesOfType(t, SignatureKindConstruct) if len(signatures) == 1 { s := signatures[0] - if len(s.typeParameters) == 0 && len(s.parameters) == 1 && signatureHasRestParameter(s) { + if len(s.TypeParameters()) == 0 && len(s.parameters) == 1 && signatureHasRestParameter(s) { paramType := c.getTypeOfParameter(s.parameters[0]) return IsTypeAny(paramType) || c.getElementTypeOfArrayType(paramType) == c.anyType } @@ -18065,7 +18071,7 @@ func getConditionalTypeKey(typeArguments []*Type, alias *TypeAlias, forConstrain return b.hash() } -func getRelationKey(source *Type, target *Type, intersectionState IntersectionState, isIdentity bool, ignoreConstraints bool) (CacheHashKey, bool) { +func getRelationKey(source *Type, target *Type, intersectionState IntersectionState, isIdentity bool, ignoreConstraints bool) (relationKey, bool) { if isIdentity && source.id > target.id { source, target = target, source } @@ -18075,12 +18081,15 @@ func getRelationKey(source *Type, target *Type, intersectionState IntersectionSt b.writeByte('g') constrained = b.writeGenericTypeReferences(source, target, ignoreConstraints) } else { - b.writeByte('s') - b.writeType(source) - b.writeType(target) + // Ordinary relationships need only the two exact IDs. Intersection + // states select separate maps, leaving generic hashes unmodified. + return relationKey{value: CacheHashKey{ + Lo: uint64(source.id)<<32 | uint64(target.id), + Hi: uint64(intersectionState), + }}, false } b.writeUint32(uint32(intersectionState)) - return b.hash(), constrained + return relationKey{value: b.hash(), generic: true}, constrained } func getNodeListKey(nodes []*ast.Node) CacheHashKey { @@ -19320,12 +19329,13 @@ func (c *Checker) getPropertiesOfObjectType(t *Type) []*ast.Symbol { func (c *Checker) getPropertiesOfUnionOrIntersectionType(t *Type) []*ast.Symbol { d := t.AsUnionOrIntersectionType() if d.resolvedProperties == nil { - var checked collections.Set[string] + checked := c.getTemporaryNameSet() + defer c.releaseTemporaryNameSet(checked) props := []*ast.Symbol{} for _, current := range d.types { for _, prop := range c.getPropertiesOfType(current) { - if !checked.Has(prop.Name()) { - checked.Add(prop.Name()) + if _, ok := checked[prop.Name()]; !ok { + checked[prop.Name()] = struct{}{} combinedProp := c.getPropertyOfUnionOrIntersectionType(t, prop.Name(), t.flags&TypeFlagsIntersection != 0 /*skipObjectFunctionPropertyAugment*/) if combinedProp != nil { props = append(props, combinedProp) @@ -19360,7 +19370,7 @@ func (c *Checker) getPropertyOfTypeEx(t *Type, name string, skipObjectFunctionPr switch { case t.flags&TypeFlagsObject != 0: resolved := c.resolveStructuredTypeMembers(t) - symbol := resolved.members[name] + symbol := resolved.getMember(name) if symbol != nil { if !includeTypeOnlyMembers && t.symbol != nil && t.symbol.Flags()&ast.SymbolFlagsValueModule != 0 && c.moduleSymbolLinks.Get(t.symbol).typeOnlyExportStarMap[name] != nil { // If this is the type of a module, `resolved.members.get(name)` might have effectively skipped over @@ -19568,6 +19578,7 @@ func (c *Checker) resolveTypeReferenceMembers(t *Type) { func (c *Checker) resolveObjectTypeMembers(t *Type, source *Type, typeParameters []*Type, typeArguments []*Type) { var mapper *TypeMapper var members ast.SymbolTable + var properties []*ast.Symbol var callSignatures []*Signature var constructSignatures []*Signature var indexInfos []*IndexInfo @@ -19581,7 +19592,14 @@ func (c *Checker) resolveObjectTypeMembers(t *Type, source *Type, typeParameters } else { instantiated = true mapper = newTypeMapper(typeParameters, typeArguments) - members = c.instantiateSymbolTable(resolved.declaredMembers, mapper) + if symbolTableKeysMatchNames(resolved.declaredMembers) { + properties = c.getNamedMembers(resolved.declaredMembers, t.symbol) + for i, symbol := range properties { + properties[i] = c.instantiateSymbol(symbol, mapper) + } + } else { + members = c.instantiateSymbolTable(resolved.declaredMembers, mapper) + } callSignatures = c.instantiateSignatures(resolved.declaredCallSignatures, mapper) constructSignatures = c.instantiateSignatures(resolved.declaredConstructSignatures, mapper) indexInfos = c.instantiateIndexInfos(resolved.declaredIndexInfos, mapper) @@ -19590,6 +19608,11 @@ func (c *Checker) resolveObjectTypeMembers(t *Type, source *Type, typeParameters if len(baseTypes) != 0 { if !instantiated { members = maps.Clone(members) + } else if members == nil { + members = make(ast.SymbolTable, len(properties)) + for _, symbol := range properties { + members[symbol.Name()] = symbol + } } thisArgument := core.LastOrNil(typeArguments) for _, baseType := range baseTypes { @@ -19612,6 +19635,11 @@ func (c *Checker) resolveObjectTypeMembers(t *Type, source *Type, typeParameters } } c.setStructuredTypeMembers(t, members, callSignatures, constructSignatures, indexInfos) + if instantiated && len(baseTypes) == 0 && members == nil { + data := t.AsStructuredType() + data.properties = properties + data.members = c.newPropertyIndex(properties, nil) + } } func findIndexInfo(indexInfos []*IndexInfo, keyType *Type) *IndexInfo { @@ -19735,7 +19763,7 @@ func (c *Checker) getInstantiatedConstructorsForTypeArguments(t *Type, typeArgum signatures := c.getConstructorsForTypeArguments(t, typeArgumentNodes, location) typeArguments := core.Map(typeArgumentNodes, c.getTypeFromTypeNode) return core.SameMap(signatures, func(sig *Signature) *Signature { - if len(sig.typeParameters) != 0 { + if len(sig.TypeParameters()) != 0 { return c.getSignatureInstantiation(sig, typeArguments, ast.IsInJSFile(location), nil) } return sig @@ -19745,17 +19773,17 @@ func (c *Checker) getInstantiatedConstructorsForTypeArguments(t *Type, typeArgum func (c *Checker) getConstructorsForTypeArguments(t *Type, typeArgumentNodes []*ast.Node, location *ast.Node) []*Signature { typeArgCount := len(typeArgumentNodes) return core.Filter(c.getSignaturesOfType(t, SignatureKindConstruct), func(sig *Signature) bool { - return typeArgCount >= c.getMinTypeArgumentCount(sig.typeParameters) && typeArgCount <= len(sig.typeParameters) + return typeArgCount >= c.getMinTypeArgumentCount(sig.TypeParameters()) && typeArgCount <= len(sig.TypeParameters()) }) } func (c *Checker) getSignatureInstantiation(sig *Signature, typeArguments []*Type, isJavaScript bool, inferredTypeParameters []*Type) *Signature { - instantiatedSignature := c.getSignatureInstantiationWithoutFillingInTypeArguments(sig, c.fillMissingTypeArguments(typeArguments, sig.typeParameters, c.getMinTypeArgumentCount(sig.typeParameters), isJavaScript)) + instantiatedSignature := c.getSignatureInstantiationWithoutFillingInTypeArguments(sig, c.fillMissingTypeArguments(typeArguments, sig.TypeParameters(), c.getMinTypeArgumentCount(sig.TypeParameters()), isJavaScript)) if len(inferredTypeParameters) != 0 { returnSignature := c.getSingleCallOrConstructSignature(c.getReturnTypeOfSignature(instantiatedSignature)) if returnSignature != nil { newReturnSignature := c.cloneSignature(returnSignature) - newReturnSignature.typeParameters = inferredTypeParameters + newReturnSignature.setTypeParameters(c, inferredTypeParameters) newReturnType := c.getOrCreateTypeFromSignature(newReturnSignature) newReturnType.AsObjectType().mapper = instantiatedSignature.mapper newInstantiatedSignature := c.cloneSignature(instantiatedSignature) @@ -19767,10 +19795,10 @@ func (c *Checker) getSignatureInstantiation(sig *Signature, typeArguments []*Typ } func (c *Checker) cloneSignature(sig *Signature) *Signature { - result := c.newSignature(sig.flags&SignatureFlagsPropagatingFlags, sig.declaration, sig.typeParameters, sig.thisParameter, sig.parameters, nil, nil, int(sig.minArgumentCount)) + result := c.newSignature(sig.flags&SignatureFlagsPropagatingFlags, sig.declaration, sig.TypeParameters(), sig.ThisParameter(), sig.parameters, nil, nil, int(sig.minArgumentCount)) result.target = sig.target result.mapper = sig.mapper - result.composite = sig.composite + result.setComposite(c, sig.getComposite()) return result } @@ -19793,7 +19821,7 @@ func (c *Checker) createSignatureTypeMapper(sig *Signature, typeArguments []*Typ } func (c *Checker) getTypeParametersForMapper(sig *Signature) []*Type { - return core.SameMap(sig.typeParameters, func(tp *Type) *Type { return c.instantiateType(tp, tp.Mapper()) }) + return core.SameMap(sig.TypeParameters(), func(tp *Type) *Type { return c.instantiateType(tp, tp.Mapper()) }) } // If type has a single call signature and no other members, return that signature. Otherwise, return nil. @@ -19829,7 +19857,7 @@ func (c *Checker) getOrCreateTypeFromSignature(sig *Signature) *Type { // using the constructor keyword, and the other is declaring a bare construct signature in an // object type literal or interface (using the new keyword). Each way of declaring a constructor // will result in a different declaration kind. - if sig.isolatedSignatureType == nil { + if sig.getIsolatedSignatureType() == nil { var kind ast.Kind if sig.declaration != nil { kind = sig.declaration.Kind @@ -19847,26 +19875,26 @@ func (c *Checker) getOrCreateTypeFromSignature(sig *Signature) *Type { } else { c.setStructuredTypeMembers(t, nil, []*Signature{sig}, nil, nil) } - sig.isolatedSignatureType = t + sig.ensureExtra(c).isolatedSignatureType = t } - return sig.isolatedSignatureType + return sig.getIsolatedSignatureType() } func (c *Checker) getErasedSignature(signature *Signature) *Signature { - if len(signature.typeParameters) == 0 { + if len(signature.TypeParameters()) == 0 { return signature } key := CachedSignatureKey{sig: signature, key: SignatureKeyErased} erased := c.cachedSignatures[key] if erased == nil { - erased = c.instantiateSignatureEx(signature, newArrayToSingleTypeMapper(signature.typeParameters, c.anyType), true /*eraseTypeParameters*/) + erased = c.instantiateSignatureEx(signature, newArrayToSingleTypeMapper(signature.TypeParameters(), c.anyType), true /*eraseTypeParameters*/) c.cachedSignatures[key] = erased } return erased } func (c *Checker) getCanonicalSignature(signature *Signature) *Signature { - if len(signature.typeParameters) == 0 { + if len(signature.TypeParameters()) == 0 { return signature } key := CachedSignatureKey{sig: signature, key: SignatureKeyCanonical} @@ -19885,7 +19913,7 @@ func (c *Checker) createCanonicalSignature(signature *Signature) *Signature { // where different generations of the same type parameter are in scope). This leads to a lot of new type // identities, and potentially a lot of work comparing those identities, so here we create an instantiation // that uses the original type identities for all unconstrained type parameters. - typeArguments := core.Map(signature.typeParameters, func(tp *Type) *Type { + typeArguments := core.Map(signature.TypeParameters(), func(tp *Type) *Type { if tp.Target() != nil && c.getConstraintOfTypeParameter(tp.Target()) == nil { return tp.Target() } @@ -19895,7 +19923,7 @@ func (c *Checker) createCanonicalSignature(signature *Signature) *Signature { } func (c *Checker) getBaseSignature(signature *Signature) *Signature { - typeParameters := signature.typeParameters + typeParameters := signature.TypeParameters() if len(typeParameters) == 0 { return signature } @@ -20468,8 +20496,8 @@ func (c *Checker) getReturnTypeOfSignature(sig *Signature) *Type { switch { case sig.target != nil: t = c.instantiateType(c.getReturnTypeOfSignature(sig.target), sig.mapper) - case sig.composite != nil: - t = c.instantiateType(c.getUnionOrIntersectionType(core.Map(sig.composite.signatures, c.getReturnTypeOfSignature), sig.composite.isUnion, UnionReductionSubtype), sig.mapper) + case sig.getComposite() != nil: + t = c.instantiateType(c.getUnionOrIntersectionType(core.Map(sig.getComposite().signatures, c.getReturnTypeOfSignature), sig.getComposite().isUnion, UnionReductionSubtype), sig.mapper) default: t = c.getReturnTypeFromAnnotation(sig.declaration) if t == nil { @@ -21077,12 +21105,12 @@ func (c *Checker) instantiateSignature(sig *Signature, m *TypeMapper) *Signature func (c *Checker) instantiateSignatureEx(sig *Signature, m *TypeMapper, eraseTypeParameters bool) *Signature { var freshTypeParameters []*Type - if len(sig.typeParameters) != 0 && !eraseTypeParameters { + if len(sig.TypeParameters()) != 0 && !eraseTypeParameters { // First create a fresh set of type parameters, then include a mapping from the old to the // new type parameters in the mapper function. Finally store this mapper in the new type // parameters such that we can use it when instantiating constraints. - freshTypeParameters = core.Map(sig.typeParameters, c.cloneTypeParameter) - m = c.combineTypeMappers(newTypeMapper(sig.typeParameters, freshTypeParameters), m) + freshTypeParameters = core.Map(sig.TypeParameters(), c.cloneTypeParameter) + m = c.combineTypeMappers(newTypeMapper(sig.TypeParameters(), freshTypeParameters), m) for _, tp := range freshTypeParameters { tp.AsTypeParameter().mapper = m } @@ -21091,7 +21119,7 @@ func (c *Checker) instantiateSignatureEx(sig *Signature, m *TypeMapper, eraseTyp // because using `mapper` now could trigger inferences to become fixed. (See `createInferenceContext`.) // See GH#17600. result := c.newSignature(sig.flags&SignatureFlagsPropagatingFlags, sig.declaration, freshTypeParameters, - c.instantiateSymbol(sig.thisParameter, m), c.instantiateSymbols(sig.parameters, m), + c.instantiateSymbol(sig.ThisParameter(), m), c.instantiateSymbols(sig.parameters, m), nil /*resolvedReturnType*/, nil /*resolvedTypePredicate*/, int(sig.minArgumentCount)) result.target = sig result.mapper = m @@ -21333,16 +21361,16 @@ func (c *Checker) getDefaultConstructSignatures(classType *Type) []*Signature { typeArgCount := len(typeArguments) var result []*Signature for _, baseSig := range baseSignatures { - minTypeArgumentCount := c.getMinTypeArgumentCount(baseSig.typeParameters) - typeParamCount := len(baseSig.typeParameters) + minTypeArgumentCount := c.getMinTypeArgumentCount(baseSig.TypeParameters()) + typeParamCount := len(baseSig.TypeParameters()) if isJavaScript || typeArgCount >= minTypeArgumentCount && typeArgCount <= typeParamCount { var sig *Signature if typeParamCount != 0 { - sig = c.createSignatureInstantiation(baseSig, c.fillMissingTypeArguments(typeArguments, baseSig.typeParameters, minTypeArgumentCount, isJavaScript)) + sig = c.createSignatureInstantiation(baseSig, c.fillMissingTypeArguments(typeArguments, baseSig.TypeParameters(), minTypeArgumentCount, isJavaScript)) } else { sig = c.cloneSignature(baseSig) } - sig.typeParameters = classType.AsInterfaceType().LocalTypeParameters() + sig.setTypeParameters(c, classType.AsInterfaceType().LocalTypeParameters()) sig.resolvedReturnType = classType if isAbstract { sig.flags |= SignatureFlagsAbstract @@ -21593,21 +21621,21 @@ func (c *Checker) getUnionSignatures(signatureLists [][]*Signature) []*Signature s := signature // Union the result types when more than one signature matches if len(unionSignatures) > 1 { - thisParameter := signature.thisParameter + thisParameter := signature.ThisParameter() firstThisParameterOfUnionSignatures := core.FirstNonNil(unionSignatures, func(sig *Signature) *ast.Symbol { - return sig.thisParameter + return sig.ThisParameter() }) if firstThisParameterOfUnionSignatures != nil { thisType := c.getIntersectionType(core.MapNonNil(unionSignatures, func(sig *Signature) *Type { - if sig.thisParameter != nil { - return c.getTypeOfSymbol(sig.thisParameter) + if sig.ThisParameter() != nil { + return c.getTypeOfSymbol(sig.ThisParameter()) } return nil })) thisParameter = c.createSymbolWithType(firstThisParameterOfUnionSignatures, thisType) } s = c.createUnionSignature(signature, unionSignatures) - s.thisParameter = thisParameter + s.setThisParameter(c, thisParameter) } result = append(result, s) } @@ -21625,8 +21653,8 @@ func (c *Checker) getUnionSignatures(signatureLists [][]*Signature) []*Signature if !core.Same(signatures, masterList) { signature := signatures[0] debug.Assert(signature != nil, "getUnionSignatures bails early on empty signature lists and should not have empty lists on second pass") - if len(signature.typeParameters) != 0 && core.Some(results, func(s *Signature) bool { - return len(s.typeParameters) != 0 && !c.compareTypeParametersIdentical(signature.typeParameters, s.typeParameters) + if len(signature.TypeParameters()) != 0 && core.Some(results, func(s *Signature) bool { + return len(s.TypeParameters()) != 0 && !c.compareTypeParametersIdentical(signature.TypeParameters(), s.TypeParameters()) }) { results = nil } else { @@ -21645,14 +21673,14 @@ func (c *Checker) getUnionSignatures(signatureLists [][]*Signature) []*Signature } func (c *Checker) combineUnionOrIntersectionMemberSignatures(left *Signature, right *Signature, isUnion bool) *Signature { - typeParams := left.typeParameters + typeParams := left.TypeParameters() if len(typeParams) == 0 { - typeParams = right.typeParameters + typeParams = right.TypeParameters() } var paramMapper *TypeMapper - if len(left.typeParameters) != 0 && len(right.typeParameters) != 0 { + if len(left.TypeParameters()) != 0 && len(right.TypeParameters()) != 0 { // We just use the type parameter defaults from the first signature - paramMapper = newTypeMapper(right.typeParameters, left.typeParameters) + paramMapper = newTypeMapper(right.TypeParameters(), left.TypeParameters()) } flags := (left.flags | right.flags) & (SignatureFlagsPropagatingFlags & ^SignatureFlagsHasRestParameter) declaration := left.declaration @@ -21661,23 +21689,23 @@ func (c *Checker) combineUnionOrIntersectionMemberSignatures(left *Signature, ri if lastParam != nil && lastParam.CheckFlags()&ast.CheckFlagsRestParameter != 0 { flags |= SignatureFlagsHasRestParameter } - thisParam := c.combineUnionOrIntersectionThisParam(left.thisParameter, right.thisParameter, paramMapper, isUnion) + thisParam := c.combineUnionOrIntersectionThisParam(left.ThisParameter(), right.ThisParameter(), paramMapper, isUnion) minArgCount := int(max(left.minArgumentCount, right.minArgumentCount)) result := c.newSignature(flags, declaration, typeParams, thisParam, params, nil, nil, minArgCount) var leftSignatures []*Signature - if left.composite != nil && left.composite.isUnion { - leftSignatures = left.composite.signatures + if left.getComposite() != nil && left.getComposite().isUnion { + leftSignatures = left.getComposite().signatures } else { leftSignatures = []*Signature{left} } - result.composite = &CompositeSignature{isUnion: isUnion, signatures: append(leftSignatures, right)} + result.setComposite(c, &CompositeSignature{isUnion: isUnion, signatures: append(leftSignatures, right)}) if paramMapper != nil { - if left.composite != nil && left.composite.isUnion == isUnion && left.mapper != nil { + if left.getComposite() != nil && left.getComposite().isUnion == isUnion && left.mapper != nil { result.mapper = c.combineTypeMappers(left.mapper, paramMapper) } else { result.mapper = paramMapper } - } else if left.composite != nil && left.composite.isUnion == isUnion { + } else if left.getComposite() != nil && left.getComposite().isUnion == isUnion { result.mapper = left.mapper } return result @@ -21867,7 +21895,7 @@ func (c *Checker) includeMixinType(t *Type, types []*Type, mixinFlags []bool, in func (c *Checker) getPropertyOfObjectType(t *Type, name string) *ast.Symbol { if t.flags&TypeFlagsObject != 0 { resolved := c.resolveStructuredTypeMembers(t) - symbol := resolved.members[name] + symbol := resolved.getMember(name) if symbol != nil && c.symbolIsValue(symbol) { return symbol } @@ -21890,38 +21918,94 @@ func (c *Checker) getPropertyOfUnionOrIntersectionType(t *Type, name string, ski // these partial properties when identifying discriminant properties, but otherwise they are filtered out // and do not appear to be present in the union type. func (c *Checker) getUnionOrIntersectionProperty(t *Type, name string, skipObjectFunctionPropertyAugment bool) *ast.Symbol { - var cache ast.SymbolTable + data := t.AsUnionOrIntersectionType() if skipObjectFunctionPropertyAugment { - cache = ast.GetSymbolTable(&t.AsUnionOrIntersectionType().propertyCacheWithoutFunctionPropertyAugment) + if prop := data.propertyCacheWithoutFunctionPropertyAugment[name]; prop != nil { + return prop + } } else { - cache = ast.GetSymbolTable(&t.AsUnionOrIntersectionType().propertyCache) - } - if prop := cache[name]; prop != nil { - return prop + if data.propertyCache != nil { + if prop := data.propertyCache[name]; prop != nil { + return prop + } + } + // Complete non-augmented properties are also valid for augmented + // lookup. Share the entry instead of retaining it in both tables. + if prop := data.propertyCacheWithoutFunctionPropertyAugment[name]; prop != nil && prop.CheckFlags()&ast.CheckFlagsPartial == 0 { + return prop + } } prop := c.createUnionOrIntersectionProperty(t, name, skipObjectFunctionPropertyAugment) if prop != nil { - cache[name] = prop - // Propagate an entry from the non-augmented cache to the augmented cache unless the property is partial. - if skipObjectFunctionPropertyAugment && prop.CheckFlags()&ast.CheckFlagsPartial == 0 { - augmentedCache := ast.GetSymbolTable(&t.AsUnionOrIntersectionType().propertyCache) - if augmentedCache[name] == nil { - augmentedCache[name] = prop + if skipObjectFunctionPropertyAugment { + ast.GetSymbolTable(&data.propertyCacheWithoutFunctionPropertyAugment)[name] = prop + if prop.CheckFlags()&ast.CheckFlagsPartial == 0 && data.propertyCache[name] == prop { + delete(data.propertyCache, name) + if len(data.propertyCache) == 0 { + data.propertyCache = nil + } } + } else { + ast.GetSymbolTable(&data.propertyCache)[name] = prop } } return prop } -func (c *Checker) createUnionOrIntersectionProperty(containingType *Type, name string, skipObjectFunctionPropertyAugment bool) *ast.Symbol { +// unionOrIntersectionPropertySources describes the constituent properties from +// which a union or intersection property would be synthesized. +type unionOrIntersectionPropertySources struct { + singleProp *ast.Symbol + // props holds distinct constituent properties in first-seen order, or is + // empty when every constituent shares singleProp. propsByKey indexes it + // only once it is large enough for hashing to beat a linear scan. + props []*ast.Symbol + propsByKey map[*ast.Symbol]struct{} + indexTypes []*Type + propFlags ast.SymbolFlags + optionalFlag ast.SymbolFlags + checkFlags ast.CheckFlags + syntheticFlag ast.CheckFlags + mergedInstantiations bool +} + +// isSynthetic reports whether the sources require a new synthetic symbol rather +// than reusing (or cloning) singleProp. +func (s *unionOrIntersectionPropertySources) isSynthetic() bool { + return len(s.props) != 0 || s.checkFlags&ast.CheckFlagsReadPartial != 0 || len(s.indexTypes) != 0 +} + +// addProp adds prop to props if it is not already present, preserving +// first-insertion order. +func (s *unionOrIntersectionPropertySources) addProp(prop *ast.Symbol) { + if s.propsByKey == nil { + if slices.Contains(s.props, prop) { + return + } + s.props = append(s.props, prop) + if len(s.props) > orderedSetMapThreshold { + s.propsByKey = make(map[*ast.Symbol]struct{}, len(s.props)) + for _, p := range s.props { + s.propsByKey[p] = struct{}{} + } + } + return + } + if _, ok := s.propsByKey[prop]; ok { + return + } + s.props = append(s.props, prop) + s.propsByKey[prop] = struct{}{} +} + +func (c *Checker) getUnionOrIntersectionPropertySources(containingType *Type, name string, skipObjectFunctionPropertyAugment bool, sources *unionOrIntersectionPropertySources) bool { propFlags := ast.SymbolFlagsNone - var singleProp *ast.Symbol - var propSet collections.OrderedSet[*ast.Symbol] - var indexTypes []*Type isUnion := containingType.flags&TypeFlagsUnion != 0 // Flags we want to propagate to the result if they exist in all source symbols var checkFlags ast.CheckFlags var optionalFlag ast.SymbolFlags + var singleProp *ast.Symbol + var indexTypes []*Type if !isUnion { checkFlags = ast.CheckFlagsReadonly optionalFlag = ast.SymbolFlagsOptional @@ -21957,10 +22041,10 @@ func (c *Checker) createUnionOrIntersectionProperty(containingType *Type, name s // back and not `Array.length` when we're looking at a `.length` access on a `string[] | number[]` mergedInstantiations = singleProp.Parent() != nil && len(c.getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(singleProp.Parent())) != 0 } else { - if propSet.Size() == 0 { - propSet.Add(singleProp) + if len(sources.props) == 0 { + sources.addProp(singleProp) } - propSet.Add(prop) + sources.addProp(prop) } // classes created by mixins are represented as intersections // and overriding a property in a derived class redefines it completely at runtime @@ -22023,17 +22107,17 @@ func (c *Checker) createUnionOrIntersectionProperty(containingType *Type, name s } if singleProp == nil { // No property was found - return nil + return false } if isUnion && - (propSet.Size() != 0 || checkFlags&ast.CheckFlagsPartial != 0) && + (len(sources.props) != 0 || checkFlags&ast.CheckFlagsPartial != 0) && checkFlags&(ast.CheckFlagsContainsPrivate|ast.CheckFlagsContainsProtected|ast.CheckFlagsContainsWritePrivate|ast.CheckFlagsContainsWriteProtected) != 0 && - !(propSet.Size() != 0 && c.hasCommonDeclaration(&propSet)) { + !(len(sources.props) != 0 && c.hasCommonDeclaration(sources.props)) { // A property in a union has a private or protected declaration in one constituent, but is missing // or has a different declaration in another constituent. If the private or protected declaration is // for reading, we don't create a property. if checkFlags&(ast.CheckFlagsContainsPrivate|ast.CheckFlagsContainsProtected) != 0 { - return nil + return false } // Otherwise, if the private or protected declaration is for writing, reduce accessibility to that of // the most restricted constituent. @@ -22043,8 +22127,30 @@ func (c *Checker) createUnionOrIntersectionProperty(containingType *Type, name s checkFlags &^= ast.CheckFlagsContainsWritePublic } } - if propSet.Size() == 0 && checkFlags&ast.CheckFlagsReadPartial == 0 && len(indexTypes) == 0 { - if !mergedInstantiations { + sources.singleProp = singleProp + sources.indexTypes = indexTypes + sources.propFlags = propFlags + sources.optionalFlag = optionalFlag + sources.checkFlags = checkFlags + sources.syntheticFlag = syntheticFlag + sources.mergedInstantiations = mergedInstantiations + return true +} + +func (c *Checker) createUnionOrIntersectionProperty(containingType *Type, name string, skipObjectFunctionPropertyAugment bool) *ast.Symbol { + var sources unionOrIntersectionPropertySources + if !c.getUnionOrIntersectionPropertySources(containingType, name, skipObjectFunctionPropertyAugment, &sources) { + return nil + } + singleProp := sources.singleProp + indexTypes := sources.indexTypes + propFlags := sources.propFlags + optionalFlag := sources.optionalFlag + checkFlags := sources.checkFlags + syntheticFlag := sources.syntheticFlag + isUnion := containingType.flags&TypeFlagsUnion != 0 + if !sources.isSynthetic() { + if !sources.mergedInstantiations { return singleProp } // No symbol from a union/intersection should have a `.parent` set (since unions/intersections don't act as symbol parents) @@ -22067,8 +22173,8 @@ func (c *Checker) createUnionOrIntersectionProperty(containingType *Type, name s links.setWriteType(c, c.getWriteTypeOfSymbol(singleProp)) return clone } - if propSet.Size() == 0 { - propSet.Add(singleProp) + if len(sources.props) == 0 { + sources.props = []*ast.Symbol{singleProp} } var declarations []*ast.Node var firstType *Type @@ -22077,7 +22183,7 @@ func (c *Checker) createUnionOrIntersectionProperty(containingType *Type, name s var writeTypes []*Type var firstValueDeclaration *ast.Node var hasNonUniformValueDeclaration bool - for prop := range propSet.Values() { + for _, prop := range sources.props { if firstValueDeclaration == nil { firstValueDeclaration = prop.ValueDeclaration() } else if prop.ValueDeclaration() != nil && prop.ValueDeclaration() != firstValueDeclaration { @@ -22161,9 +22267,9 @@ func isPrototypeProperty(symbol *ast.Symbol) bool { return symbol.Flags()&ast.SymbolFlagsMethod != 0 || symbol.CheckFlags()&ast.CheckFlagsSyntheticMethod != 0 } -func (c *Checker) hasCommonDeclaration(symbols *collections.OrderedSet[*ast.Symbol]) bool { +func (c *Checker) hasCommonDeclaration(symbols []*ast.Symbol) bool { var commonDeclarations collections.Set[*ast.Node] - for symbol := range symbols.Values() { + for _, symbol := range symbols { if len(symbol.Declarations()) == 0 { return false } @@ -22342,7 +22448,8 @@ func (c *Checker) isMappingOfSameObjectType(types []*Type) bool { func (c *Checker) somePropertyReducesToNever(t *Type) bool { // Collect declaration counts for each property across all constituent types of the intersection. - var counts collections.OrderedMap[string, int] + counts := c.getTemporaryPropertyCounts() + defer c.releaseTemporaryPropertyCounts(counts) for _, t := range t.Types() { for _, prop := range c.getPropertiesOfType(t) { counts.Set(prop.Name(), counts.GetOrZero(prop.Name())+1) @@ -22352,7 +22459,7 @@ func (c *Checker) somePropertyReducesToNever(t *Type) bool { // Go in the order the properties were found so the combined properties are created in the same order every time. for propName, count := range counts.Entries() { if count > 1 { - if prop := c.getPropertyOfUnionOrIntersectionType(t, propName, true /*skipObjectFunctionPropertyAugment*/); prop != nil && c.isNeverReducedProperty(prop) { + if c.intersectionPropertyReducesToNever(t, propName) { return true } } @@ -22360,6 +22467,64 @@ func (c *Checker) somePropertyReducesToNever(t *Type) bool { return false } +// intersectionPropertyReducesToNever reports whether the combined property named +// name would satisfy isNeverReducedProperty, without creating and caching that +// property. Most such properties are never otherwise looked up, so creating +// them only to inspect their flags retains a large number of symbols. +func (c *Checker) intersectionPropertyReducesToNever(t *Type, name string) bool { + if prop := t.AsUnionOrIntersectionType().propertyCacheWithoutFunctionPropertyAugment[name]; prop != nil { + return c.isNeverReducedProperty(prop) + } + var sources unionOrIntersectionPropertySources + if !c.getUnionOrIntersectionPropertySources(t, name, true /*skipObjectFunctionPropertyAugment*/, &sources) { + return false + } + if !sources.isSynthetic() { + if !sources.mergedInstantiations { + return c.isNeverReducedProperty(sources.singleProp) + } + prop := c.getUnionOrIntersectionProperty(t, name, true /*skipObjectFunctionPropertyAugment*/) + return prop != nil && c.isNeverReducedProperty(prop) + } + if len(sources.props) == 0 { + sources.props = []*ast.Symbol{sources.singleProp} + } + checkFlags := sources.checkFlags + var firstType *Type + var firstValueDeclaration *ast.Node + hasNonUniformValueDeclaration := false + propTypes := make([]*Type, 0, len(sources.props)+len(sources.indexTypes)) + for _, prop := range sources.props { + if firstValueDeclaration == nil { + firstValueDeclaration = prop.ValueDeclaration() + } else if prop.ValueDeclaration() != nil && prop.ValueDeclaration() != firstValueDeclaration { + hasNonUniformValueDeclaration = true + } + propType := c.getTypeOfSymbol(prop) + if firstType == nil { + firstType = propType + } + if propType != firstType { + checkFlags |= ast.CheckFlagsHasNonUniformType + } + if isLiteralType(propType) || c.isPatternLiteralType(propType) { + checkFlags |= ast.CheckFlagsHasLiteralType + } + if propType.flags&TypeFlagsNever != 0 && propType != c.uniqueLiteralType { + checkFlags |= ast.CheckFlagsHasNeverType + } + propTypes = append(propTypes, propType) + } + propTypes = append(propTypes, sources.indexTypes...) + hasValueDeclaration := !hasNonUniformValueDeclaration && firstValueDeclaration != nil + if !hasValueDeclaration && checkFlags&ast.CheckFlagsContainsPrivate != 0 { + return true + } + return (sources.propFlags|sources.optionalFlag)&ast.SymbolFlagsOptional == 0 && + checkFlags&(ast.CheckFlagsNonUniformAndLiteral|ast.CheckFlagsHasNeverType) == ast.CheckFlagsNonUniformAndLiteral && + c.getIntersectionType(propTypes).flags&TypeFlagsNever != 0 +} + func (c *Checker) getReducedUnionType(unionType *Type) *Type { reducedTypes := core.SameMap(unionType.Types(), c.getReducedType) if core.Same(reducedTypes, unionType.Types()) { @@ -22870,7 +23035,7 @@ func (c *Checker) getObjectTypeInstantiation(t *Type, m *TypeMapper, alias *Type default: target = t } - typeParameters = links.outerTypeParameters + typeParameters = links.getOuterTypeParameters() if typeParameters == nil { // The first time an anonymous type is instantiated we compute and store a list of the type // parameters that are in scope (and therefore potentially referenced). For type literals that @@ -22893,7 +23058,7 @@ func (c *Checker) getObjectTypeInstantiation(t *Type, m *TypeMapper, alias *Type if typeParameters == nil { typeParameters = []*Type{} } - links.outerTypeParameters = typeParameters + links.setOuterTypeParameters(c, typeParameters) } if len(typeParameters) == 0 { return t @@ -24361,8 +24526,8 @@ func (c *Checker) getOuterTypeParameters(node *ast.Node, includeThisTypes bool) outerTypeParameters := c.getOuterTypeParameters(node, includeThisTypes) if (kind == ast.KindFunctionExpression || kind == ast.KindArrowFunction || ast.IsObjectLiteralMethod(node)) && c.isContextSensitive(node) { signature := core.FirstOrNil(c.getSignaturesOfType(c.getTypeOfSymbol(c.getSymbolOfDeclaration(node)), SignatureKindCall)) - if signature != nil && len(signature.typeParameters) != 0 { - return append(outerTypeParameters, signature.typeParameters...) + if signature != nil && len(signature.TypeParameters()) != 0 { + return append(outerTypeParameters, signature.TypeParameters()...) } } if kind == ast.KindMappedType { @@ -25655,36 +25820,26 @@ func (c *Checker) newUniqueESSymbolType(symbol *ast.Symbol, name string) *Type { func (c *Checker) newObjectType(objectFlags ObjectFlags, symbol *ast.Symbol) *Type { var t *Type - var kind typeKind switch { case objectFlags&ObjectFlagsClassOrInterface != 0: - t = c.interfaceTypeArena.New().AsType() - kind = typeKindInterface + t = c.newType(TypeFlagsObject, objectFlags, c.interfaceTypeArena.New().AsType(), typeKindInterface) case objectFlags&ObjectFlagsTuple != 0: - t = (&TupleType{}).AsType() - kind = typeKindTuple + t = c.newType(TypeFlagsObject, objectFlags, (&TupleType{}).AsType(), typeKindTuple) case objectFlags&ObjectFlagsReference != 0: - t = c.typeReferenceArena.New().AsType() - kind = typeKindTypeReference + t = c.newType(TypeFlagsObject, objectFlags, c.typeReferenceArena.New().AsType(), typeKindTypeReference) case objectFlags&ObjectFlagsMapped != 0: - t = c.mappedTypeArena.New().AsType() - kind = typeKindMapped + t = c.newType(TypeFlagsObject, objectFlags, c.mappedTypeArena.New().AsType(), typeKindMapped) case objectFlags&ObjectFlagsReverseMapped != 0: - t = (&ReverseMappedType{}).AsType() - kind = typeKindReverseMapped + t = c.newType(TypeFlagsObject, objectFlags, (&ReverseMappedType{}).AsType(), typeKindReverseMapped) case objectFlags&ObjectFlagsEvolvingArray != 0: - t = (&EvolvingArrayType{}).AsType() - kind = typeKindEvolvingArray + t = c.newType(TypeFlagsObject, objectFlags, (&EvolvingArrayType{}).AsType(), typeKindEvolvingArray) case objectFlags&ObjectFlagsInstantiationExpressionType != 0: - t = (&InstantiationExpressionType{}).AsType() - kind = typeKindInstantiationExpression + t = c.newType(TypeFlagsObject, objectFlags, (&InstantiationExpressionType{}).AsType(), typeKindInstantiationExpression) case objectFlags&ObjectFlagsAnonymous != 0: - t = c.objectTypeArena.New().AsType() - kind = typeKindObject + t = c.newType(TypeFlagsObject, objectFlags, c.objectTypeArena.New().AsType(), typeKindObject) default: panic("Unhandled case in newObjectType") } - c.newType(TypeFlagsObject, objectFlags, t, kind) t.symbol = symbol return t } @@ -25747,8 +25902,18 @@ func (c *Checker) cloneTypeReference(source *Type) *Type { func (c *Checker) setStructuredTypeMembers(t *Type, members ast.SymbolTable, callSignatures []*Signature, constructSignatures []*Signature, indexInfos []*IndexInfo) { t.objectFlags |= ObjectFlagsMembersResolved data := t.AsStructuredType() - data.members = members + // Named-member filtering may recursively query this type before it finishes. + data.members = nil + if members != nil { + data.members = c.memberIndexArena.New() + data.members.fallback = members + } data.properties = c.getNamedMembers(members, t.symbol) + if data.members != nil && t.symbol != nil && t.symbol.Flags()&ast.SymbolFlagsValueModule != 0 { + data.members.fallback = members + } else { + data.members = c.newMemberIndex(members, data.properties, data.members) + } c.setStructuredTypeSignatures(data, callSignatures, constructSignatures, indexInfos) } @@ -25860,11 +26025,11 @@ func (c *Checker) newSignature(flags SignatureFlags, declaration *ast.Node, type sig.id = SignatureId(c.SignatureCount) sig.flags = flags sig.declaration = declaration - sig.typeParameters = typeParameters + sig.setTypeParameters(c, typeParameters) sig.parameters = parameters - sig.thisParameter = thisParameter + sig.setThisParameter(c, thisParameter) sig.resolvedReturnType = resolvedReturnType - sig.resolvedTypePredicate = resolvedTypePredicate + sig.setResolvedTypePredicate(c, resolvedTypePredicate) sig.minArgumentCount = int32(minArgumentCount) sig.resolvedMinArgumentCount = -1 return sig @@ -31810,7 +31975,7 @@ func (c *Checker) isFunctionObjectType(t *Type) bool { // We do a quick check for a "bind" property before performing the more expensive subtype // check. This gives us a quicker out in the common case where an object type is not a function. resolved := c.resolveStructuredTypeMembers(t) - return len(resolved.getSignatures()) != 0 || resolved.members["bind"] != nil && c.isTypeSubtypeOf(t, c.globalFunctionType) + return len(resolved.getSignatures()) != 0 || resolved.getMember("bind") != nil && c.isTypeSubtypeOf(t, c.globalFunctionType) } func (c *Checker) getTypeWithFacts(t *Type, include TypeFacts) *Type { @@ -32306,7 +32471,7 @@ func (c *Checker) getSymbolAtLocation(node *ast.Node, ignoreErrors bool) *ast.Sy // member should more exactly be the kind of (declarationless) symbol we want. // (See #44364 and #45031 for relevant implementation PRs) if metaProp.KeywordToken == ast.KindImportKeyword && node.Text() == "meta" { - return c.getGlobalImportMetaExpressionType().AsObjectType().members["meta"] + return c.getGlobalImportMetaExpressionType().AsObjectType().getMember("meta") } // no other meta properties are valid syntax, thus no others should have symbols return nil @@ -32331,8 +32496,8 @@ func (c *Checker) getSymbolAtLocation(node *ast.Node, ignoreErrors bool) *ast.Sy container := c.getThisContainer(node, false /*includeArrowFunctions*/, false /*includeClassComputedPropertyName*/) if ast.IsFunctionLike(container) { sig := c.getSignatureFromDeclaration(container) - if sig.thisParameter != nil { - return sig.thisParameter + if sig.ThisParameter() != nil { + return sig.ThisParameter() } } if ast.IsInExpressionContext(node) { diff --git a/tsc/internal/checker/checker_test.go b/tsc/internal/checker/checker_test.go index 31cea19751525..fdb95f6a70a08 100644 --- a/tsc/internal/checker/checker_test.go +++ b/tsc/internal/checker/checker_test.go @@ -3,6 +3,7 @@ package checker_test import ( "fmt" "path/filepath" + "strconv" "strings" "testing" @@ -117,6 +118,51 @@ export type E = D;`, assert.Equal(t, defaultClause, defaultReference) } +func TestUnionPropertyFunctionAugmentation(t *testing.T) { + t.Parallel() + for _, lookupFirst := range []bool{false, true} { + t.Run(strconv.FormatBool(lookupFirst), func(t *testing.T) { + t.Parallel() + fs := bundled.WrapFS(vfstest.FromMap(map[string]string{ + "/main.ts": `type Callable = { (): string; shared: string }; +type Other = { call: number; shared: number }; +declare const instance: Callable | Other; +instance;`, + "/function.d.ts": `interface Function { + call(thisArg: any, ...args: any[]): any; +} +interface CallableFunction extends Function {}`, + "/tsconfig.json": `{"compilerOptions":{"strict":true,"noLib":true},"files":["main.ts","function.d.ts"]}`, + }, tspath.CaseInsensitive)) + host := compiler.NewCompilerHost(fs, bundled.LibPath(), nil, nil, nil) + parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile("/tsconfig.json", &core.CompilerOptions{}, nil, fs, nil) + assert.Equal(t, len(errors), 0) + program := compiler.NewProgram(compiler.ProgramOptions{Config: parsed, Host: host}) + program.BindSourceFiles() + c, done := program.GetTypeChecker(t.Context()) + defer done() + file := program.GetSourceFile("/main.ts") + typ := c.GetTypeAtLocation(file.Statements.Nodes[3].Expression()) + var call *ast.Symbol + if lookupFirst { + call = c.GetPropertyOfType(typ, "call") + assert.Assert(t, call != nil) + } + properties := c.GetPropertiesOfType(typ) + assert.Equal(t, len(properties), 1) + assert.Equal(t, properties[0].Name(), "shared") + if call == nil { + call = c.GetPropertyOfType(typ, "call") + assert.Assert(t, call != nil) + } + assert.Equal(t, c.GetPropertyOfType(typ, "call"), call) + assert.Equal(t, c.GetPropertyOfType(typ, "shared"), properties[0]) + assert.Equal(t, len(c.GetPropertiesOfType(typ)), 1) + assert.Equal(t, len(c.GetDiagnostics(t.Context(), file)), 0) + }) + } +} + func BenchmarkNewChecker(b *testing.B) { fs := bundled.WrapFS(osvfs.FS()) rootPath := tspath.RootedDirectoryPathFromAbsolute(filepath.Join(repo.TestDataPath(), "fixtures/compiler")) diff --git a/tsc/internal/checker/flow.go b/tsc/internal/checker/flow.go index 24d2f4f48b566..d2831231772fe 100644 --- a/tsc/internal/checker/flow.go +++ b/tsc/internal/checker/flow.go @@ -2071,7 +2071,7 @@ func (c *Checker) getEffectsSignature(node *ast.Node) *Signature { } signatures := c.getSignaturesOfType(core.OrElse(apparentType, c.unknownType), SignatureKindCall) switch { - case len(signatures) == 1 && len(signatures[0].typeParameters) == 0: + case len(signatures) == 1 && len(signatures[0].TypeParameters()) == 0: signature = signatures[0] case core.Some(signatures, c.hasTypePredicateOrNeverReturnType): signature = c.getResolvedSignature(node, nil, CheckModeNormal) @@ -2216,8 +2216,8 @@ func (c *Checker) getExplicitThisType(node *ast.Node) *Type { container := ast.GetThisContainer(node, false /*includeArrowFunctions*/, false /*includeClassComputedPropertyName*/) if ast.IsFunctionLike(container) { signature := c.getSignatureFromDeclaration(container) - if signature.thisParameter != nil { - return c.getExplicitTypeOfSymbol(signature.thisParameter, nil) + if signature.ThisParameter() != nil { + return c.getExplicitTypeOfSymbol(signature.ThisParameter(), nil) } } if container.Parent != nil && ast.IsClassLike(container.Parent) { diff --git a/tsc/internal/checker/jsx.go b/tsc/internal/checker/jsx.go index 4e7dc902d23c6..7f35f796957cf 100644 --- a/tsc/internal/checker/jsx.go +++ b/tsc/internal/checker/jsx.go @@ -987,14 +987,14 @@ func (c *Checker) getJsxPropsTypeFromClassType(sig *Signature, context *ast.Node } func (c *Checker) getJsxPropsTypeForSignatureFromMember(sig *Signature, forcedLookupLocation string) *Type { - if sig.composite != nil { + if sig.getComposite() != nil { // JSX Elements using the legacy `props`-field based lookup (eg, react class components) need to treat the `props` member as an input // instead of an output position when resolving the signature. We need to go back to the input signatures of the composite signature, // get the type of `props` on each return type individually, and then _intersect them_, rather than union them (as would normally occur // for a union signature). It's an unfortunate quirk of looking in the output of the signature for the type we want to use for the input. // The default behavior of `getTypeOfFirstParameterOfSignatureWithFallback` when no `props` member name is defined is much more sane. var results []*Type - for _, signature := range sig.composite.signatures { + for _, signature := range sig.getComposite().signatures { instance := c.getReturnTypeOfSignature(signature) if IsTypeAny(instance) { return instance diff --git a/tsc/internal/checker/links.go b/tsc/internal/checker/links.go index c21981cae9d80..c8a3e718e7b1c 100644 --- a/tsc/internal/checker/links.go +++ b/tsc/internal/checker/links.go @@ -28,7 +28,11 @@ func (s *nodeLinkStore[V]) Get(node *ast.Node) *V { } func (s *nodeLinkStore[V]) Has(node *ast.Node) bool { - return s.TryGet(node) != nil + id := uint64(s.gen.GetNodeId(node)) + if id >= ast.BlockIdOffset && id < ast.BlockIdOffset+maxPageLinkCount { + return s.pages.Has(id - ast.BlockIdOffset) + } + return s.links.Has(id) } func (s *nodeLinkStore[V]) TryGet(node *ast.Node) *V { @@ -54,7 +58,11 @@ func (s *symbolLinkStore[V]) Get(symbol *ast.Symbol) *V { } func (s *symbolLinkStore[V]) Has(symbol *ast.Symbol) bool { - return s.TryGet(symbol) != nil + id := uint64(s.gen.GetSymbolId(symbol)) + if id >= ast.BlockIdOffset && id < ast.BlockIdOffset+maxPageLinkCount { + return s.pages.Has(id - ast.BlockIdOffset) + } + return s.links.Has(id) } func (s *symbolLinkStore[V]) TryGet(symbol *ast.Symbol) *V { diff --git a/tsc/internal/checker/nodebuilderimpl.go b/tsc/internal/checker/nodebuilderimpl.go index 8e4b0c63f4d2a..d5f9f9a203864 100644 --- a/tsc/internal/checker/nodebuilderimpl.go +++ b/tsc/internal/checker/nodebuilderimpl.go @@ -576,7 +576,7 @@ func (b *NodeBuilderImpl) getResolvedTypeWithoutAbstractConstructSignatures(t *S t.extra.objectTypeWithoutAbstractConstructSignatures = t.AsType() return t.AsType() } - typeCopy := b.ch.newAnonymousType(t.symbol, t.members, t.CallSignatures(), core.IfElse(len(constructSignatures) > 0, constructSignatures, []*Signature{}), t.getIndexInfos()) + typeCopy := b.ch.newAnonymousType(t.symbol, t.getMembers(), t.CallSignatures(), core.IfElse(len(constructSignatures) > 0, constructSignatures, []*Signature{}), t.getIndexInfos()) t.extra.objectTypeWithoutAbstractConstructSignatures = typeCopy typeCopy.AsStructuredType().ensureExtra().objectTypeWithoutAbstractConstructSignatures = typeCopy return typeCopy @@ -1882,12 +1882,12 @@ func (b *NodeBuilderImpl) signatureToSignatureDeclarationHelper(signature *Signa b.ctx.approximateLength += 3 // Usually a signature contributes a few more characters than this, but 3 is the minimum - if b.ctx.flags&nodebuilder.FlagsWriteTypeArgumentsOfSignature != 0 && signature.target != nil && signature.mapper != nil && len(signature.target.typeParameters) != 0 { - for _, parameter := range signature.target.typeParameters { + if b.ctx.flags&nodebuilder.FlagsWriteTypeArgumentsOfSignature != 0 && signature.target != nil && signature.mapper != nil && len(signature.target.TypeParameters()) != 0 { + for _, parameter := range signature.target.TypeParameters() { typeParameters = append(typeParameters, b.typeToTypeNode(b.ch.instantiateType(parameter, signature.mapper))) } } else { - for _, parameter := range signature.typeParameters { + for _, parameter := range signature.TypeParameters() { typeParameters = append(typeParameters, b.typeParameterToDeclaration(parameter)) } } @@ -2084,8 +2084,8 @@ func (c *Checker) getExpandedParameters(sig *Signature, skipUnionExpanding bool) } func (b *NodeBuilderImpl) tryGetThisParameterDeclaration(signature *Signature) *ast.Node { - if signature.thisParameter != nil { - return b.symbolToParameterDeclaration(signature.thisParameter, false) + if signature.ThisParameter() != nil { + return b.symbolToParameterDeclaration(signature.ThisParameter(), false) } if signature.declaration != nil && ast.IsInJSFile(signature.declaration) { // !!! JSDoc Support diff --git a/tsc/internal/checker/nodebuilderscopes.go b/tsc/internal/checker/nodebuilderscopes.go index 244d598682c59..05f033bf05b7d 100644 --- a/tsc/internal/checker/nodebuilderscopes.go +++ b/tsc/internal/checker/nodebuilderscopes.go @@ -52,7 +52,7 @@ func (b *NodeBuilderImpl) addSymbolTypeToContext(symbol *ast.Symbol, t *Type) fu func (b *NodeBuilderImpl) enterSignatureScope(signature *Signature) (expandedParams []*ast.Symbol, cleanup func()) { expandedParams = b.ch.getExpandedParameters(signature, true /*skipUnionExpanding*/)[0] - cleanup = b.enterNewScope(signature.declaration, expandedParams, signature.typeParameters, signature.parameters, signature.mapper) + cleanup = b.enterNewScope(signature.declaration, expandedParams, signature.TypeParameters(), signature.parameters, signature.mapper) return expandedParams, cleanup } diff --git a/tsc/internal/checker/nodecopy.go b/tsc/internal/checker/nodecopy.go index b3d2f82c435c1..71ed761dcbdc8 100644 --- a/tsc/internal/checker/nodecopy.go +++ b/tsc/internal/checker/nodecopy.go @@ -842,7 +842,7 @@ func getExistingNodeTreeVisitor(b *NodeBuilderImpl, bound *recoveryBoundary) *as if ast.IsFunctionLike(node) { sig := b.ch.getSignatureFromDeclaration(node) params = sig.parameters - typeParams = sig.typeParameters + typeParams = sig.TypeParameters() } else if ast.IsConditionalTypeNode(node) { // !!! TODO: impossible in combination with the scope start check??? typeParams = b.ch.getInferTypeParameters(node) } else if ast.IsMappedTypeNode(node) { diff --git a/tsc/internal/checker/pseudotypenodebuilder.go b/tsc/internal/checker/pseudotypenodebuilder.go index 40ca59da0ebec..8f1e2bf1700e1 100644 --- a/tsc/internal/checker/pseudotypenodebuilder.go +++ b/tsc/internal/checker/pseudotypenodebuilder.go @@ -178,7 +178,7 @@ func (b *NodeBuilderImpl) pseudoTypeToNode(t *pseudochecker.PseudoType) *ast.Nod d := t.AsPseudoTypeSingleCallSignature() signature := b.ch.getSignatureFromDeclaration(d.Signature) expandedParams := b.ch.getExpandedParameters(signature, true /*skipUnionExpanding*/)[0] - cleanup := b.enterNewScope(d.Signature, expandedParams, signature.typeParameters, signature.parameters, signature.mapper) + cleanup := b.enterNewScope(d.Signature, expandedParams, signature.TypeParameters(), signature.parameters, signature.mapper) defer cleanup() var typeParams *ast.NodeList if len(d.TypeParameters) > 0 { @@ -233,7 +233,7 @@ func (b *NodeBuilderImpl) pseudoTypeToNode(t *pseudochecker.PseudoType) *ast.Nod if e.Kind != pseudochecker.PseudoObjectElementKindPropertyAssignment { signature := b.ch.getSignatureFromDeclaration(e.Signature()) expandedParams := b.ch.getExpandedParameters(signature, true /*skipUnionExpanding*/)[0] - cleanup = b.enterNewScope(e.Signature(), expandedParams, signature.typeParameters, signature.parameters, signature.mapper) + cleanup = b.enterNewScope(e.Signature(), expandedParams, signature.TypeParameters(), signature.parameters, signature.mapper) } var newProp *ast.Node switch e.Kind { @@ -549,7 +549,7 @@ func (b *NodeBuilderImpl) pseudoTypeEquivalentToType(t *pseudochecker.PseudoType return false } pt := t.AsPseudoTypeSingleCallSignature() - if len(targetSig.typeParameters) != len(pt.TypeParameters) { + if len(targetSig.TypeParameters()) != len(pt.TypeParameters) { if reportErrors { b.ctx.tracker.ReportInferenceFallback(pt.Signature) } @@ -583,13 +583,13 @@ func (b *NodeBuilderImpl) pseudoTypeEquivalentToType(t *pseudochecker.PseudoType } func (b *NodeBuilderImpl) pseudoParametersEquivalentToParameters(params []*pseudochecker.PseudoParameter, targetSig *Signature, reportErrors bool, nonParamErrorLocation *ast.Node) bool { - if targetSig.thisParameter != nil && len(params) == 0 { + if targetSig.ThisParameter() != nil && len(params) == 0 { if reportErrors { b.ctx.tracker.ReportInferenceFallback(nonParamErrorLocation) // missing `this` param } return false - } else if targetSig.thisParameter != nil && ast.IsThisIdentifier(params[0].Name) { - targetParam := targetSig.thisParameter + } else if targetSig.ThisParameter() != nil && ast.IsThisIdentifier(params[0].Name) { + targetParam := targetSig.ThisParameter() paramType := b.ch.getTypeOfParameter(targetParam) if !b.pseudoTypeEquivalentToType(params[0].Type, paramType, params[0].Optional, false) { if reportErrors { @@ -598,7 +598,7 @@ func (b *NodeBuilderImpl) pseudoParametersEquivalentToParameters(params []*pseud return false } params = params[1:] - } else if targetSig.thisParameter != nil { + } else if targetSig.ThisParameter() != nil { if reportErrors { b.ctx.tracker.ReportInferenceFallback(nonParamErrorLocation) } diff --git a/tsc/internal/checker/relater.go b/tsc/internal/checker/relater.go index 8bf177bfd9e3a..a47dfbcb8a971 100644 --- a/tsc/internal/checker/relater.go +++ b/tsc/internal/checker/relater.go @@ -96,22 +96,43 @@ func asRecursionId[T *ast.Node | *ast.Symbol | *Type](value T) RecursionId { } type Relation struct { - results map[CacheHashKey]RelationComparisonResult + genericResults map[CacheHashKey]RelationComparisonResult + simpleResults [4]map[uint64]RelationComparisonResult } -func (r *Relation) get(key CacheHashKey) RelationComparisonResult { - return r.results[key] +type relationKey struct { + value CacheHashKey + generic bool } -func (r *Relation) set(key CacheHashKey, result RelationComparisonResult) { - if r.results == nil { - r.results = make(map[CacheHashKey]RelationComparisonResult) +func (r *Relation) get(key relationKey) RelationComparisonResult { + if key.generic { + return r.genericResults[key.value] + } + return r.simpleResults[key.value.Hi][key.value.Lo] +} + +func (r *Relation) set(key relationKey, result RelationComparisonResult) { + if key.generic { + if r.genericResults == nil { + r.genericResults = make(map[CacheHashKey]RelationComparisonResult) + } + r.genericResults[key.value] = result + } else { + results := &r.simpleResults[key.value.Hi] + if *results == nil { + *results = make(map[uint64]RelationComparisonResult) + } + (*results)[key.value.Lo] = result } - r.results[key] = result } func (r *Relation) size() int { - return len(r.results) + size := len(r.genericResults) + for _, results := range r.simpleResults { + size += len(results) + } + return size } func (c *Checker) isTypeIdenticalTo(source *Type, target *Type) bool { @@ -1515,7 +1536,7 @@ func (c *Checker) compareSignaturesRelated(source *Signature, target *Signature, } return TernaryFalse } - if len(source.typeParameters) != 0 && !core.Same(source.typeParameters, target.typeParameters) { + if len(source.TypeParameters()) != 0 && !core.Same(source.TypeParameters(), target.TypeParameters()) { target = c.getCanonicalSignature(target) source = c.instantiateSignatureInContextOf(source, target /*inferenceContext*/, nil, compareTypes) } @@ -1706,7 +1727,7 @@ func (c *Checker) compareTypePredicateRelatedTo(source *TypePredicate, target *T // Returns true if `s` is `(...args: A) => R` where `A` is `any`, `any[]`, `never`, or `never[]`, and `R` is `any` or `unknown`. func (c *Checker) isTopSignature(s *Signature) bool { - if len(s.typeParameters) == 0 && (s.thisParameter == nil || IsTypeAny(c.getTypeOfParameter(s.thisParameter))) && len(s.parameters) == 1 && signatureHasRestParameter(s) { + if len(s.TypeParameters()) == 0 && (s.ThisParameter() == nil || IsTypeAny(c.getTypeOfParameter(s.ThisParameter()))) && len(s.parameters) == 1 && signatureHasRestParameter(s) { paramType := c.getTypeOfParameter(s.parameters[0]) var restType *Type if c.isArrayType(paramType) { @@ -1945,8 +1966,8 @@ func (c *Checker) getRestArrayTypeOfTupleType(t *Type) *Type { } func (c *Checker) getThisTypeOfSignature(signature *Signature) *Type { - if signature.thisParameter != nil { - return c.getTypeOfSymbol(signature.thisParameter) + if signature.ThisParameter() != nil { + return c.getTypeOfSymbol(signature.ThisParameter()) } return nil } @@ -2047,37 +2068,38 @@ func (c *Checker) getTupleElementLabelFromBindingElement(node *ast.Node, index i } func (c *Checker) getTypePredicateOfSignature(sig *Signature) *TypePredicate { - if sig.resolvedTypePredicate == nil { + if sig.getResolvedTypePredicate(c) == nil { switch { case sig.target != nil: targetTypePredicate := c.getTypePredicateOfSignature(sig.target) if targetTypePredicate != nil { - sig.resolvedTypePredicate = c.instantiateTypePredicate(targetTypePredicate, sig.mapper) + sig.setResolvedTypePredicate(c, c.instantiateTypePredicate(targetTypePredicate, sig.mapper)) } - case sig.composite != nil: - sig.resolvedTypePredicate = c.getUnionOrIntersectionTypePredicate(sig.composite.signatures, sig.composite.isUnion) + case sig.getComposite() != nil: + sig.setResolvedTypePredicate(c, c.getUnionOrIntersectionTypePredicate(sig.getComposite().signatures, sig.getComposite().isUnion)) default: if sig.declaration != nil { typeNode := sig.declaration.Type() switch { case typeNode != nil: if ast.IsTypePredicateNode(typeNode) { - sig.resolvedTypePredicate = c.createTypePredicateFromTypePredicateNode(typeNode, sig) + sig.setResolvedTypePredicate(c, c.createTypePredicateFromTypePredicateNode(typeNode, sig)) } case ast.IsFunctionLikeDeclaration(sig.declaration) && (sig.resolvedReturnType == nil || sig.resolvedReturnType.flags&TypeFlagsBoolean != 0) && c.getParameterCount(sig) > 0: - sig.resolvedTypePredicate = c.noTypePredicate // avoid infinite loop - sig.resolvedTypePredicate = c.getTypePredicateFromBody(sig.declaration) + sig.setResolvedTypePredicate(c, c.noTypePredicate) // avoid infinite loop + sig.setResolvedTypePredicate(c, c.getTypePredicateFromBody(sig.declaration)) } } } - if sig.resolvedTypePredicate == nil { - sig.resolvedTypePredicate = c.noTypePredicate + if sig.getResolvedTypePredicate(c) == nil { + sig.setResolvedTypePredicate(c, c.noTypePredicate) } } - if sig.resolvedTypePredicate == c.noTypePredicate { + predicate := sig.getResolvedTypePredicate(c) + if predicate == c.noTypePredicate { return nil } - return sig.resolvedTypePredicate + return predicate } func (c *Checker) getUnionOrIntersectionTypePredicate(signatures []*Signature, isUnion bool) *TypePredicate { @@ -2143,14 +2165,14 @@ func (c *Checker) newTypePredicate(kind TypePredicateKind, parameterName string, } func (c *Checker) isResolvingReturnTypeOfSignature(signature *Signature) bool { - if signature.composite != nil && core.Some(signature.composite.signatures, c.isResolvingReturnTypeOfSignature) { + if signature.getComposite() != nil && core.Some(signature.getComposite().signatures, c.isResolvingReturnTypeOfSignature) { return true } return signature.resolvedReturnType == nil && c.findResolutionCycleStartIndex(signature, TypeSystemPropertyNameResolvedReturnType) >= 0 } func (c *Checker) findMatchingSignatures(signatureLists [][]*Signature, signature *Signature, listIndex int) []*Signature { - if len(signature.typeParameters) != 0 { + if len(signature.TypeParameters()) != 0 { // We require an exact match for generic signatures, so we only return signatures from the first // signature list and only if they have exact matches in the other signature lists. if listIndex > 0 { @@ -2205,16 +2227,16 @@ func (c *Checker) compareSignaturesIdentical(source *Signature, target *Signatur return TernaryFalse } // Check that the two signatures have the same number of type parameters. - if len(source.typeParameters) != len(target.typeParameters) { + if len(source.TypeParameters()) != len(target.TypeParameters()) { return TernaryFalse } // Check that type parameter constraints and defaults match. If they do, instantiate the source // signature with the type parameters of the target signature and continue the comparison. - if len(target.typeParameters) != 0 { - mapper := newTypeMapper(source.typeParameters, target.typeParameters) - for i := range len(target.typeParameters) { - s := source.typeParameters[i] - t := target.typeParameters[i] + if len(target.TypeParameters()) != 0 { + mapper := newTypeMapper(source.TypeParameters(), target.TypeParameters()) + for i := range len(target.TypeParameters()) { + s := source.TypeParameters()[i] + t := target.TypeParameters()[i] if !(s == t || compareTypes(c.instantiateType(c.getConstraintOrUnknownFromTypeParameter(s), mapper), c.getConstraintOrUnknownFromTypeParameter(t)) != TernaryFalse && compareTypes(c.instantiateType(c.getDefaultOrUnknownFromTypeParameter(s), mapper), c.getDefaultOrUnknownFromTypeParameter(t)) != TernaryFalse) { return TernaryFalse @@ -2583,8 +2605,8 @@ type Relater struct { errorNode *ast.Node errorChain *ErrorChain relatedInfo []*ast.Diagnostic - maybeKeys []CacheHashKey - maybeKeysSet collections.Set[CacheHashKey] + maybeKeys []relationKey + maybeKeysSet collections.Set[relationKey] sourceStack []*Type targetStack []*Type maybeCount int diff --git a/tsc/internal/checker/services.go b/tsc/internal/checker/services.go index 5f4d894355844..4926d142bbde5 100644 --- a/tsc/internal/checker/services.go +++ b/tsc/internal/checker/services.go @@ -154,10 +154,17 @@ func (c *Checker) ForEachExportAndPropertyOfModule(moduleSymbol *ast.Symbol, cb if reducedType.flags&TypeFlagsStructuredType == 0 { return } - for name, symbol := range c.resolveStructuredTypeMembers(reducedType).members { - if c.isNamedMember(symbol, name) { - cb(symbol, name) + resolved := c.resolveStructuredTypeMembers(reducedType) + if resolved.members != nil && resolved.members.fallback != nil { + for name, symbol := range resolved.members.fallback { + if c.isNamedMember(symbol, name) { + cb(symbol, name) + } } + return + } + for _, symbol := range resolved.properties { + cb(symbol, symbol.Name()) } } @@ -726,10 +733,10 @@ func (c *Checker) getUninstantiatedSignatures(node *ast.Node) []*Signature { func (c *Checker) getTypeParameterConstraintForPositionAcrossSignatures(signatures []*Signature, position int) *Type { var relevantConstraints []*Type for _, signature := range signatures { - if position >= len(signature.typeParameters) { + if position >= len(signature.TypeParameters()) { continue } - relevantTypeParameter := signature.typeParameters[position] + relevantTypeParameter := signature.TypeParameters()[position] relevantConstraint := c.getConstraintOfTypeParameter(relevantTypeParameter) if relevantConstraint != nil { relevantConstraints = append(relevantConstraints, relevantConstraint) diff --git a/tsc/internal/checker/types.go b/tsc/internal/checker/types.go index 9266430f3a715..0da6652adeb40 100644 --- a/tsc/internal/checker/types.go +++ b/tsc/internal/checker/types.go @@ -10,6 +10,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/collections" "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/evaluator" + "github.com/zeebo/xxh3" ) //go:generate npx hereby generate:checker @@ -438,8 +439,26 @@ type SymbolNodeLinks struct { } type TypeNodeLinks struct { - resolvedType *Type // Resolved type associated with node - outerTypeParameters []*Type // Outer type parameters of anonymous object type + resolvedType *Type // Resolved type associated with node + outerTypeParameters *[]*Type // Outer type parameters of anonymous object type +} + +func (links *TypeNodeLinks) getOuterTypeParameters() []*Type { + if links.outerTypeParameters == nil { + return nil + } + return *links.outerTypeParameters +} + +func (links *TypeNodeLinks) setOuterTypeParameters(c *Checker, types []*Type) { + if types == nil { + links.outerTypeParameters = nil + return + } + if links.outerTypeParameters == nil { + links.outerTypeParameters = c.outerTypeParameterListArena.New() + } + *links.outerTypeParameters = types } type ComputedNameNodeLinks struct { @@ -1096,7 +1115,7 @@ func (t *ConstrainedType) AsConstrainedType() *ConstrainedType { return t } type StructuredType struct { ConstrainedType - members ast.SymbolTable + members *memberIndex properties []*ast.Symbol extra *structuredTypeExtra } @@ -1150,6 +1169,107 @@ func (t *StructuredType) Properties() []*ast.Symbol { return t.properties } +type memberIndex struct { + slots []uint32 + fallback ast.SymbolTable +} + +// Property order is semantic. Keep it unchanged and index it with pointer-free +// slots instead of retaining a second map of names and symbol pointers. +func (c *Checker) newMemberIndex(members ast.SymbolTable, properties []*ast.Symbol, index *memberIndex) *memberIndex { + if len(members) != len(properties) || !symbolTableKeysMatchNames(members) { + if index == nil { + index = c.memberIndexArena.New() + } + index.fallback = members + return index + } + if index != nil { + index.fallback = nil + } + return c.newPropertyIndex(properties, index) +} + +// Reports whether every symbol is stored under its own name. Checking the +// table's pairs, rather than looking each property up by name, also rejects a +// symbol aliased under a second key, which a name-keyed index would drop. +func symbolTableKeysMatchNames(members ast.SymbolTable) bool { + for name, symbol := range members { + if symbol.Name() != name { + return false + } + } + return true +} + +func (c *Checker) newPropertyIndex(properties []*ast.Symbol, index *memberIndex) *memberIndex { + if len(properties) <= 8 { + return nil + } + if uint64(len(properties)) > uint64(^uint32(0)) { + panic("Member index exhausted") + } + if index == nil { + index = c.memberIndexArena.New() + } + size := 1 << bits.Len(uint(len(properties))*2-1) + index.slots = c.memberIndexSlotsArena.NewSlice(size) + mask := uint64(size - 1) + for i, symbol := range properties { + slot := xxh3.HashString(symbol.Name()) & mask + for index.slots[slot] != 0 { + slot = (slot + 1) & mask + } + index.slots[slot] = uint32(i + 1) + } + return index +} + +func (t *StructuredType) getMember(name string) *ast.Symbol { + if t.members == nil { + for _, symbol := range t.properties { + if symbol.Name() == name { + return symbol + } + } + return nil + } + if t.members.fallback != nil { + return t.members.fallback[name] + } + slots := t.members.slots + if len(slots) == 0 { + return nil + } + mask := uint64(len(slots) - 1) + slot := xxh3.HashString(name) & mask + for { + index := slots[slot] + if index == 0 { + return nil + } + symbol := t.properties[index-1] + if symbol.Name() == name { + return symbol + } + slot = (slot + 1) & mask + } +} + +func (t *StructuredType) getMembers() ast.SymbolTable { + if t.members != nil && t.members.fallback != nil { + return t.members.fallback + } + if len(t.properties) == 0 { + return nil + } + members := make(ast.SymbolTable, len(t.properties)) + for _, symbol := range t.properties { + members[symbol.Name()] = symbol + } + return members +} + // Except for tuple type references and reverse mapped types, all object types have an associated symbol. // Possible object type instances are listed in the following. @@ -1344,7 +1464,7 @@ type EvolvingArrayType struct { type UnionOrIntersectionType struct { StructuredType types []*Type - propertyCache ast.SymbolTable + propertyCache ast.SymbolTable // Augmented entries not shared with the non-augmented cache. propertyCacheWithoutFunctionPropertyAugment ast.SymbolTable resolvedProperties []*ast.Symbol } @@ -1492,6 +1612,7 @@ const ( SignatureFlagsIsUntypedSignatureInJSFile SignatureFlags = 1 << 6 // Indicates signature is from a js file and has no types SignatureFlagsIsNonInferrable SignatureFlags = 1 << 7 // Indicates signature comes from a non-inferrable type SignatureFlagsIsSignatureCandidateForOverloadFailure SignatureFlags = 1 << 8 + signatureFlagsNoTypePredicate SignatureFlags = 1 << 9 // We do not propagate `IsInnerCallChain` or `IsOuterCallChain` to instantiated signatures, as that would result in us // attempting to add `| undefined` on each recursive call to `getReturnTypeOfSignature` when // instantiating the return type. @@ -1507,15 +1628,79 @@ type Signature struct { minArgumentCount int32 resolvedMinArgumentCount int32 declaration *ast.Node - typeParameters []*Type parameters []*ast.Symbol - thisParameter *ast.Symbol resolvedReturnType *Type - resolvedTypePredicate *TypePredicate target *Signature mapper *TypeMapper - isolatedSignatureType *Type - composite *CompositeSignature + extra *signatureExtra +} + +type signatureExtra struct { + typeParameters []*Type + thisParameter *ast.Symbol + resolvedTypePredicate *TypePredicate + isolatedSignatureType *Type + composite *CompositeSignature +} + +func (s *Signature) ensureExtra(c *Checker) *signatureExtra { + if s.extra == nil { + s.extra = c.signatureExtraArena.New() + } + return s.extra +} + +func (s *Signature) setTypeParameters(c *Checker, types []*Type) { + if types != nil || s.extra != nil { + s.ensureExtra(c).typeParameters = types + } +} + +func (s *Signature) setThisParameter(c *Checker, parameter *ast.Symbol) { + if parameter != nil || s.extra != nil { + s.ensureExtra(c).thisParameter = parameter + } +} + +func (s *Signature) getResolvedTypePredicate(c *Checker) *TypePredicate { + if s.flags&signatureFlagsNoTypePredicate != 0 { + return c.noTypePredicate + } + if s.extra == nil { + return nil + } + return s.extra.resolvedTypePredicate +} + +func (s *Signature) setResolvedTypePredicate(c *Checker, predicate *TypePredicate) { + s.flags &^= signatureFlagsNoTypePredicate + if predicate != nil && predicate == c.noTypePredicate { + s.flags |= signatureFlagsNoTypePredicate + predicate = nil + } + if predicate != nil || s.extra != nil { + s.ensureExtra(c).resolvedTypePredicate = predicate + } +} + +func (s *Signature) getComposite() *CompositeSignature { + if s.extra == nil { + return nil + } + return s.extra.composite +} + +func (s *Signature) setComposite(c *Checker, composite *CompositeSignature) { + if composite != nil || s.extra != nil { + s.ensureExtra(c).composite = composite + } +} + +func (s *Signature) getIsolatedSignatureType() *Type { + if s.extra == nil { + return nil + } + return s.extra.isolatedSignatureType } func (s *Signature) Id() SignatureId { @@ -1523,11 +1708,14 @@ func (s *Signature) Id() SignatureId { } func (s *Signature) Flags() SignatureFlags { - return s.flags + return s.flags &^ signatureFlagsNoTypePredicate } func (s *Signature) TypeParameters() []*Type { - return s.typeParameters + if s.extra == nil { + return nil + } + return s.extra.typeParameters } func (s *Signature) Declaration() *ast.Node { @@ -1539,7 +1727,10 @@ func (s *Signature) Target() *Signature { } func (s *Signature) ThisParameter() *ast.Symbol { - return s.thisParameter + if s.extra == nil { + return nil + } + return s.extra.thisParameter } func (s *Signature) Parameters() []*ast.Symbol { diff --git a/tsc/internal/checker/types_test.go b/tsc/internal/checker/types_test.go index d08d1803e2035..dc422eed2b8cf 100644 --- a/tsc/internal/checker/types_test.go +++ b/tsc/internal/checker/types_test.go @@ -9,11 +9,44 @@ import ( "testing" "unsafe" + "github.com/microsoft/TypeScript/tsc/internal/ast" "github.com/microsoft/TypeScript/tsc/internal/testutil/race" "github.com/zeebo/xxh3" "gotest.tools/v3/assert" ) +func TestCheckerNodeLinkStores(t *testing.T) { + t.Parallel() + f := ast.NewNodeFactory(ast.NodeFactoryHooks{}) + first := &Checker{} + second := &Checker{} + nodes := make([]*ast.Node, 4096) + for i := range nodes { + node := f.NewIdentifier(strconv.Itoa(i)) + if i%2 == 0 { + ast.GetNodeId(node) + } + nodes[i] = node + assert.Assert(t, !first.typeNodeLinks.Has(node)) + assert.Assert(t, !first.signatureLinks.Has(node)) + first.typeNodeLinks.Get(node).resolvedType = &Type{id: TypeId(i + 1)} + first.signatureLinks.Get(node).resolvedSignature = &Signature{id: SignatureId(i + 1)} + second.typeNodeLinks.Get(node).resolvedType = &Type{id: TypeId(i + 2)} + second.signatureLinks.Get(node).resolvedSignature = &Signature{id: SignatureId(i + 2)} + } + f.ReleaseArenas() + runtime.GC() + for i, node := range nodes { + assert.Equal(t, first.typeNodeLinks.Get(node).resolvedType.id, TypeId(i+1)) + assert.Equal(t, first.signatureLinks.Get(node).resolvedSignature.id, SignatureId(i+1)) + assert.Equal(t, second.typeNodeLinks.Get(node).resolvedType.id, TypeId(i+2)) + assert.Equal(t, second.signatureLinks.Get(node).resolvedSignature.id, SignatureId(i+2)) + } + clone := nodes[0].Clone(f) + assert.Assert(t, !first.typeNodeLinks.Has(clone)) + assert.Assert(t, !first.signatureLinks.Has(clone)) +} + func TestTypeCasts(t *testing.T) { t.Parallel() type bases interface { @@ -65,7 +98,18 @@ func TestTypeCasts(t *testing.T) { typ.flags = TypeFlagsNever typ.objectFlags = ObjectFlagsReference assert.Equal(t, test.cast(typ), any(test.data)) - assert.Assert(t, didPanic(func() { typ.AsLiteralType() }) == (test.kind != typeKindLiteral)) + for _, target := range tests { + switch target.kind { + case typeKindObject: + assert.Equal(t, target.cast(typ), any(test.data.AsObjectType())) + case typeKindTypeReference: + assert.Equal(t, target.cast(typ), any(test.data.AsTypeReference())) + case typeKindInterface: + assert.Equal(t, target.cast(typ), any(test.data.AsInterfaceType())) + default: + assert.Assert(t, didPanic(func() { target.cast(typ) }) == (test.kind != target.kind)) + } + } } } @@ -109,6 +153,79 @@ func TestKeyBuilder(t *testing.T) { } } +func TestRelationKeys(t *testing.T) { + t.Parallel() + source := &Type{id: 1} + target := &Type{id: ^TypeId(0)} + var relation Relation + assert.Equal(t, relation.size(), 0) + for state := IntersectionStateNone; state <= IntersectionStateSource|IntersectionStateTarget; state++ { + key, constrained := getRelationKey(source, target, state, false, false) + assert.Assert(t, !constrained) + assert.Equal(t, relation.get(key), RelationComparisonResultNone) + relation.set(key, RelationComparisonResultSucceeded) + reverse, _ := getRelationKey(target, source, state, false, false) + assert.Assert(t, key != reverse) + assert.Equal(t, relation.get(reverse), RelationComparisonResultNone) + identity, _ := getRelationKey(target, source, state, true, false) + assert.Equal(t, key, identity) + relation.set(key, RelationComparisonResultFailed|RelationComparisonResultReportsUnreliable) + assert.Equal(t, relation.get(key), RelationComparisonResultFailed|RelationComparisonResultReportsUnreliable) + } + assert.Equal(t, relation.size(), 4) + for state := IntersectionStateNone; state <= IntersectionStateSource|IntersectionStateTarget; state++ { + key, _ := getRelationKey(source, target, state, false, false) + assert.Equal(t, relation.get(key), RelationComparisonResultFailed|RelationComparisonResultReportsUnreliable) + } +} + +func TestRelationKeyNamespaces(t *testing.T) { + t.Parallel() + simple, _ := getRelationKey(&Type{id: 1}, &Type{id: 2}, IntersectionStateNone, false, false) + generic := relationKey{value: simple.value, generic: true} + var relation Relation + relation.set(simple, RelationComparisonResultSucceeded) + assert.Equal(t, relation.get(generic), RelationComparisonResultNone) + relation.set(generic, RelationComparisonResultFailed) + assert.Equal(t, relation.get(simple), RelationComparisonResultSucceeded) + assert.Equal(t, relation.get(generic), RelationComparisonResultFailed) + assert.Equal(t, relation.size(), 2) +} + +func TestGenericRelationKeys(t *testing.T) { + t.Parallel() + c, types := newCompositeTestChecker() + c.noConstraintType = c.newIntrinsicType(TypeFlagsNever, "no constraint") + root := types[0] + newParameter := func(constraint *Type) *Type { + parameter := &TypeParameter{constraint: constraint} + return c.newType(TypeFlagsTypeParameter, ObjectFlagsNone, parameter.AsType(), typeKindTypeParameter) + } + newReference := func(argument *Type) *Type { + reference := &TypeReference{resolvedTypeArguments: []*Type{argument}} + reference.target = root + return c.newType(TypeFlagsObject, ObjectFlagsReference, reference.AsType(), typeKindTypeReference) + } + left := newReference(newParameter(c.noConstraintType)) + right := newReference(newParameter(c.noConstraintType)) + equivalentLeft := newReference(newParameter(c.noConstraintType)) + equivalentRight := newReference(newParameter(c.noConstraintType)) + key, constrained := getRelationKey(left, right, IntersectionStateNone, false, false) + assert.Assert(t, !constrained) + equivalent, _ := getRelationKey(equivalentLeft, equivalentRight, IntersectionStateNone, false, false) + assert.Equal(t, key, equivalent) + var relation Relation + relation.set(key, RelationComparisonResultSucceeded) + assert.Equal(t, relation.get(equivalent), RelationComparisonResultSucceeded) + constrainedLeft := newReference(newParameter(types[1])) + constrainedKey, constrained := getRelationKey(constrainedLeft, right, IntersectionStateNone, false, false) + assert.Assert(t, constrained) + assert.Assert(t, constrainedKey != key) + broadest, constrained := getRelationKey(constrainedLeft, right, IntersectionStateNone, false, true) + assert.Assert(t, !constrained) + assert.Equal(t, broadest, key) +} + func TestTypeListKeyAllocations(t *testing.T) { //nolint:paralleltest if race.Enabled { t.Skip("race instrumentation changes hashing allocations") @@ -253,6 +370,88 @@ func TestTemporaryTypeSetReuse(t *testing.T) { assert.Equal(t, c.getIntersectionType([]*Type{left, right}), primitives[1]) } +func TestIntersectionReductionAllocations(t *testing.T) { //nolint:paralleltest + if race.Enabled { + t.Skip("race instrumentation changes hashing allocations") + } + c, _ := newCompositeTestChecker() + members := make(ast.SymbolTable) + for i := range 128 { + name := strconv.Itoa(i) + members[name] = c.newSymbol(ast.SymbolFlagsProperty, name) + } + object := c.newAnonymousType(nil, members, nil, nil, nil) + intersection := c.newIntersectionType(ObjectFlagsNone, []*Type{object, object}) + assert.Assert(t, !c.somePropertyReducesToNever(intersection)) + assert.Equal(t, testing.AllocsPerRun(10, func() { + assert.Assert(t, !c.somePropertyReducesToNever(intersection)) + }), float64(0)) +} + +func TestTemporaryPropertyCounts(t *testing.T) { + t.Parallel() + c := &Checker{} + outer := c.getTemporaryPropertyCounts() + outer.Set("second", 1) + outer.Set("first", 1) + outer.Set("second", 2) + assert.Assert(t, slices.Equal(slices.Collect(outer.Keys()), []string{"second", "first"})) + inner := c.getTemporaryPropertyCounts() + assert.Assert(t, inner != outer) + inner.Set("first", 4) + assert.Equal(t, outer.GetOrZero("first"), 1) + assert.Equal(t, outer.GetOrZero("second"), 2) + c.releaseTemporaryPropertyCounts(inner) + assert.Equal(t, inner.Size(), 0) + assert.Assert(t, !inner.Has("first")) + reused := c.getTemporaryPropertyCounts() + assert.Equal(t, reused, inner) + reused.Set("second", 3) + reused.Set("first", 5) + assert.Assert(t, slices.Equal(slices.Collect(reused.Keys()), []string{"second", "first"})) + assert.Equal(t, outer.GetOrZero("first"), 1) + c.releaseTemporaryPropertyCounts(reused) + c.releaseTemporaryPropertyCounts(outer) + runtime.GC() + reused = c.getTemporaryPropertyCounts() + assert.Equal(t, reused.Size(), 0) + assert.Assert(t, !reused.Has("second")) + for i := range 4097 { + reused.Set(strconv.Itoa(i), i) + } + c.releaseTemporaryPropertyCounts(reused) + for counts := c.freePropertyCounts; counts != nil; counts = counts.next { + assert.Assert(t, counts != reused) + assert.Equal(t, counts.Size(), 0) + } + assert.Assert(t, c.getTemporaryPropertyCounts() != reused) + other := &Checker{} + assert.Assert(t, other.getTemporaryPropertyCounts() != outer) +} + +func TestIntersectionReductionReentrancy(t *testing.T) { + t.Parallel() + c, _ := newCompositeTestChecker() + property := c.newSymbol(ast.SymbolFlagsProperty, "value") + object := c.newAnonymousType(nil, ast.SymbolTable{"value": property}, nil, nil, nil) + inner := c.newIntersectionType(ObjectFlagsNone, []*Type{object, object}) + outer := c.newIntersectionType(ObjectFlagsNone, []*Type{inner, object}) + assert.Equal(t, c.getReducedType(outer), outer) + assert.Equal(t, c.getReducedType(inner), inner) + assert.Equal(t, c.getPropertyOfUnionOrIntersectionType(outer, "value", true), property) + assert.Assert(t, c.freePropertyCounts != nil) + assert.Assert(t, c.freePropertyCounts.next != nil) + workspace := c.freePropertyCounts + conflicting := c.newSymbolEx(ast.SymbolFlagsProperty, "value", ast.CheckFlagsContainsPrivate) + outer.AsUnionOrIntersectionType().propertyCacheWithoutFunctionPropertyAugment["value"] = conflicting + assert.Assert(t, c.somePropertyReducesToNever(outer)) + assert.Equal(t, c.freePropertyCounts, workspace) + for counts := c.freePropertyCounts; counts != nil; counts = counts.next { + assert.Equal(t, counts.Size(), 0) + assert.Assert(t, !counts.Has("value")) + } +} + func BenchmarkCompositeTypeCacheHit(b *testing.B) { c, types := newCompositeTestChecker() union := c.getUnionType(types) @@ -326,10 +525,30 @@ func TestCompactTypeMapperLayout(t *testing.T) { c, types := newCompositeTestChecker() mapper := newSimpleTypeMapper(types[0], types[1]) assert.Assert(t, castTypeMapper[SimpleTypeMapper](mapper, TypeMapperKindSimple).source == types[0]) - func() { - defer func() { assert.Assert(t, recover() != nil) }() - castTypeMapper[ArrayTypeMapper](mapper, TypeMapperKindArray) - }() + context := c.newInferenceContextWorker(nil, nil, InferenceFlagsNone, nil) + tests := []struct { + kind TypeMapperKind + mapper *TypeMapper + cast func(*TypeMapper) any + }{ + {TypeMapperKindSimple, mapper, func(m *TypeMapper) any { return castTypeMapper[SimpleTypeMapper](m, TypeMapperKindSimple) }}, + {TypeMapperKindArray, newArrayTypeMapper(nil, nil), func(m *TypeMapper) any { return castTypeMapper[ArrayTypeMapper](m, TypeMapperKindArray) }}, + {TypeMapperKindArrayToSingle, newArrayToSingleTypeMapper(nil, nil), func(m *TypeMapper) any { + return castTypeMapper[ArrayToSingleTypeMapper](m, TypeMapperKindArrayToSingle) + }}, + {TypeMapperKindDeferred, newDeferredTypeMapper(nil, nil), func(m *TypeMapper) any { return castTypeMapper[DeferredTypeMapper](m, TypeMapperKindDeferred) }}, + {TypeMapperKindFunction, newFunctionTypeMapper(func(t *Type) *Type { return t }), func(m *TypeMapper) any { return castTypeMapper[FunctionTypeMapper](m, TypeMapperKindFunction) }}, + {TypeMapperKindMerged, newMergedTypeMapper(mapper, mapper), func(m *TypeMapper) any { return castTypeMapper[MergedTypeMapper](m, TypeMapperKindMerged) }}, + {TypeMapperKindComposite, newCompositeTypeMapper(c, mapper, mapper), func(m *TypeMapper) any { return castTypeMapper[CompositeTypeMapper](m, TypeMapperKindComposite) }}, + {TypeMapperKindInference, context.mapper, func(m *TypeMapper) any { return castTypeMapper[InferenceTypeMapper](m, TypeMapperKindInference) }}, + } + runtime.GC() + for _, test := range tests { + assert.Equal(t, test.mapper.kind, test.kind) + for _, target := range tests { + assert.Assert(t, didPanic(func() { target.cast(test.mapper) }) == (test.kind != target.kind)) + } + } runtime.GC() assert.Equal(t, mapper.Map(types[0]), types[1]) runtime.KeepAlive(c) @@ -392,6 +611,133 @@ func TestCompactValueSymbolLinksLayout(t *testing.T) { assert.Equal(t, links.getContainingType(), types[2]) } +func TestStructuredMembersLookup(t *testing.T) { + t.Parallel() + c, _ := newCompositeTestChecker() + for _, count := range []int{0, 1, 8, 20, 300} { + members := make(ast.SymbolTable, count) + for i := range count { + name := strconv.Itoa(i) + members[name] = c.newSymbol(ast.SymbolFlagsProperty, name) + } + typ := c.newAnonymousType(nil, members, nil, nil, nil) + resolved := typ.AsStructuredType() + assert.Equal(t, len(resolved.properties), count) + for name, symbol := range members { + assert.Equal(t, resolved.getMember(name), symbol) + } + assert.Assert(t, resolved.getMember("missing") == nil) + other := c.newAnonymousType(nil, resolved.getMembers(), nil, nil, nil) + for name, symbol := range members { + assert.Equal(t, other.AsStructuredType().getMember(name), symbol) + } + members["new"] = c.newSymbol(ast.SymbolFlagsProperty, "new") + assert.Assert(t, resolved.getMember("new") == nil) + members[ast.InternalSymbolNameCall] = c.newSymbol(ast.SymbolFlagsSignature, ast.InternalSymbolNameCall) + withReserved := c.newAnonymousType(nil, members, nil, nil, nil).AsStructuredType() + assert.Equal(t, withReserved.getMember(ast.InternalSymbolNameCall), members[ast.InternalSymbolNameCall]) + for range 300 { + c.newMemberIndex(members, withReserved.properties, nil) + } + runtime.GC() + for name, symbol := range members { + assert.Equal(t, withReserved.getMember(name), symbol) + } + } + renamed := c.newSymbol(ast.SymbolFlagsProperty, "original") + member := c.newAnonymousType(nil, ast.SymbolTable{"different": renamed}, nil, nil, nil).AsStructuredType() + assert.Equal(t, member.getMember("different"), renamed) + assert.Assert(t, member.getMember("original") == nil) + aliased := c.newSymbol(ast.SymbolFlagsProperty, "original") + aliasedMembers := ast.SymbolTable{"original": aliased, "different": aliased} + for i := range 10 { + name := "p" + strconv.Itoa(i) + aliasedMembers[name] = c.newSymbol(ast.SymbolFlagsProperty, name) + } + aliasedType := c.newAnonymousType(nil, aliasedMembers, nil, nil, nil).AsStructuredType() + for name, symbol := range aliasedMembers { + assert.Equal(t, aliasedType.getMember(name), symbol) + } +} + +func TestUnionIntersectionPropertyCacheSharing(t *testing.T) { + t.Parallel() + for _, intersection := range []bool{false, true} { + for _, augmentedFirst := range []bool{false, true} { + t.Run(strconv.FormatBool(intersection)+"/"+strconv.FormatBool(augmentedFirst), func(t *testing.T) { + t.Parallel() + c, _ := newCompositeTestChecker() + members := make(ast.SymbolTable) + for i := range 300 { + name := strconv.Itoa(i) + members[name] = c.newSymbol(ast.SymbolFlagsProperty, name) + } + members[""] = c.newSymbol(ast.SymbolFlagsProperty, "") + object := c.newAnonymousType(nil, members, nil, nil, nil) + var typ *Type + if intersection { + typ = c.newIntersectionType(ObjectFlagsNone, []*Type{object, object}) + } else { + typ = c.newUnionType(ObjectFlagsNone, []*Type{object, object}) + } + data := typ.AsUnionOrIntersectionType() + for name, symbol := range members { + assert.Equal(t, c.getUnionOrIntersectionProperty(typ, name, !augmentedFirst), symbol) + assert.Equal(t, c.getUnionOrIntersectionProperty(typ, name, augmentedFirst), symbol) + } + assert.Equal(t, len(data.propertyCacheWithoutFunctionPropertyAugment), len(members)) + assert.Assert(t, data.propertyCache == nil) + runtime.GC() + for name, symbol := range members { + assert.Equal(t, c.getUnionOrIntersectionProperty(typ, name, true), symbol) + assert.Equal(t, c.getUnionOrIntersectionProperty(typ, name, false), symbol) + } + runtime.KeepAlive(c) + }) + } + } +} + +func TestUnionIntersectionPropertyCacheModes(t *testing.T) { + t.Parallel() + for _, intersection := range []bool{false, true} { + t.Run(strconv.FormatBool(intersection), func(t *testing.T) { + t.Parallel() + c, _ := newCompositeTestChecker() + var typ *Type + if intersection { + typ = c.newIntersectionType(ObjectFlagsNone, nil) + } else { + typ = c.newUnionType(ObjectFlagsNone, nil) + } + data := typ.AsUnionOrIntersectionType() + assert.Assert(t, c.getUnionOrIntersectionProperty(typ, "missing", true) == nil) + assert.Assert(t, c.getUnionOrIntersectionProperty(typ, "missing", false) == nil) + assert.Assert(t, data.propertyCache == nil) + assert.Assert(t, data.propertyCacheWithoutFunctionPropertyAugment == nil) + data.propertyCacheWithoutFunctionPropertyAugment = make(ast.SymbolTable) + data.propertyCache = make(ast.SymbolTable) + for _, flags := range []ast.CheckFlags{ast.CheckFlagsReadPartial, ast.CheckFlagsWritePartial} { + name := strconv.Itoa(int(flags)) + partial := c.newSymbolEx(ast.SymbolFlagsProperty, name, flags) + complete := c.newSymbol(ast.SymbolFlagsProperty, name) + data.propertyCacheWithoutFunctionPropertyAugment[name] = partial + assert.Equal(t, c.getUnionOrIntersectionProperty(typ, name, true), partial) + assert.Assert(t, c.getUnionOrIntersectionProperty(typ, name, false) == nil) + assert.Equal(t, c.getPropertyOfUnionOrIntersectionType(typ, name, true) == nil, flags&ast.CheckFlagsReadPartial != 0) + data.propertyCache[name] = complete + assert.Equal(t, c.getUnionOrIntersectionProperty(typ, name, false), complete) + } + nonAugmented := c.newSymbol(ast.SymbolFlagsProperty, "different") + augmented := c.newSymbol(ast.SymbolFlagsProperty, "different") + data.propertyCacheWithoutFunctionPropertyAugment["different"] = nonAugmented + data.propertyCache["different"] = augmented + assert.Equal(t, c.getUnionOrIntersectionProperty(typ, "different", true), nonAugmented) + assert.Equal(t, c.getUnionOrIntersectionProperty(typ, "different", false), augmented) + }) + } +} + func TestCompactStructuredTypeLayout(t *testing.T) { t.Parallel() assert.Equal(t, unsafe.Sizeof(StructuredType{}), unsafe.Sizeof(ConstrainedType{})+5*unsafe.Sizeof(uintptr(0))) @@ -441,3 +787,113 @@ func TestStructuredTypeExtra(t *testing.T) { assert.Assert(t, data.getIndexInfos() != nil) runtime.KeepAlive(c) } + +func TestCompactTypeNodeLinksLayout(t *testing.T) { + t.Parallel() + assert.Equal(t, unsafe.Sizeof(TypeNodeLinks{}), 2*unsafe.Sizeof(uintptr(0))) + c, types := newCompositeTestChecker() + var links TypeNodeLinks + links.resolvedType = types[0] + assert.Assert(t, links.getOuterTypeParameters() == nil) + links.setOuterTypeParameters(c, nil) + assert.Assert(t, links.outerTypeParameters == nil) + links.setOuterTypeParameters(c, []*Type{}) + assert.Assert(t, links.getOuterTypeParameters() != nil) + assert.Equal(t, len(links.getOuterTypeParameters()), 0) + entry := links.outerTypeParameters + list := slices.Clone(types) + links.setOuterTypeParameters(c, list) + assert.Equal(t, links.outerTypeParameters, entry) + assert.Equal(t, links.getOuterTypeParameters()[0], types[0]) + list[0] = types[1] + assert.Equal(t, links.getOuterTypeParameters()[0], types[1]) + for range 1024 { + var other TypeNodeLinks + other.setOuterTypeParameters(c, types) + } + runtime.GC() + assert.Equal(t, links.getOuterTypeParameters()[0], types[1]) + assert.Equal(t, links.resolvedType, types[0]) + links.setOuterTypeParameters(c, nil) + assert.Assert(t, links.getOuterTypeParameters() == nil) + assert.Equal(t, links.resolvedType, types[0]) + runtime.KeepAlive(c) +} + +func TestCompactSignatureLayout(t *testing.T) { + t.Parallel() + assert.Equal(t, unsafe.Sizeof(Signature{}), 16+8*unsafe.Sizeof(uintptr(0))) + assert.Equal(t, unsafe.Sizeof(signatureExtra{}), 7*unsafe.Sizeof(uintptr(0))) +} + +func TestSignatureExtra(t *testing.T) { + t.Parallel() + c, types := newCompositeTestChecker() + c.noTypePredicate = &TypePredicate{} + sig := c.newSignature(SignatureFlagsHasRestParameter, nil, nil, nil, nil, types[0], nil, 1) + assert.Assert(t, sig.extra == nil) + assert.Assert(t, sig.TypeParameters() == nil) + assert.Assert(t, sig.ThisParameter() == nil) + assert.Assert(t, sig.getComposite() == nil) + assert.Assert(t, sig.getIsolatedSignatureType() == nil) + assert.Assert(t, sig.getResolvedTypePredicate(c) == nil) + assert.Equal(t, sig.resolvedMinArgumentCount, int32(-1)) + sig.setTypeParameters(c, nil) + sig.setThisParameter(c, nil) + sig.setComposite(c, nil) + sig.setResolvedTypePredicate(c, nil) + assert.Assert(t, sig.extra == nil) + assert.Assert(t, c.getTypePredicateOfSignature(sig) == nil) + assert.Equal(t, sig.getResolvedTypePredicate(c), c.noTypePredicate) + assert.Assert(t, sig.extra == nil) + assert.Assert(t, sig.flags&SignatureFlagsHasRestParameter != 0) + assert.Equal(t, sig.Flags(), SignatureFlagsHasRestParameter) + sig.setResolvedTypePredicate(c, nil) + assert.Assert(t, sig.getResolvedTypePredicate(c) == nil) + sig.setTypeParameters(c, []*Type{}) + assert.Assert(t, sig.TypeParameters() != nil) + entry := sig.extra + list := slices.Clone(types) + sig.setTypeParameters(c, list) + list[0] = types[1] + assert.Equal(t, sig.TypeParameters()[0], types[1]) + parameter := &ast.Symbol{} + sig.setThisParameter(c, parameter) + predicate := c.newTypePredicate(TypePredicateKindIdentifier, "p", 0, types[0]) + sig.setResolvedTypePredicate(c, predicate) + assert.Equal(t, c.getTypePredicateOfSignature(sig), predicate) + composite := &CompositeSignature{isUnion: true, signatures: []*Signature{sig}} + sig.setComposite(c, composite) + sig.ensureExtra(c).isolatedSignatureType = types[2] + clone := c.cloneSignature(sig) + assert.Assert(t, clone.extra != entry) + assert.Equal(t, clone.getComposite(), composite) + assert.Equal(t, clone.ThisParameter(), parameter) + assert.Equal(t, clone.TypeParameters()[0], types[1]) + assert.Assert(t, clone.getResolvedTypePredicate(c) == nil) + assert.Assert(t, clone.getIsolatedSignatureType() == nil) + clone.setThisParameter(c, nil) + assert.Equal(t, sig.ThisParameter(), parameter) + sig.setResolvedTypePredicate(c, c.noTypePredicate) + assert.Assert(t, c.getTypePredicateOfSignature(sig) == nil) + assert.Assert(t, entry.resolvedTypePredicate == nil) + sig.setResolvedTypePredicate(c, predicate) + for range 1024 { + other := c.newSignature(SignatureFlagsNone, nil, types, parameter, nil, nil, predicate, 0) + other.setComposite(c, composite) + } + runtime.GC() + assert.Equal(t, sig.extra, entry) + assert.Equal(t, sig.getResolvedTypePredicate(c), predicate) + assert.Equal(t, sig.Flags(), SignatureFlagsHasRestParameter) + assert.Equal(t, sig.getIsolatedSignatureType(), types[2]) + sig.setTypeParameters(c, nil) + sig.setThisParameter(c, nil) + sig.setComposite(c, nil) + assert.Equal(t, sig.extra, entry) + assert.Assert(t, sig.TypeParameters() == nil) + assert.Equal(t, sig.resolvedReturnType, types[0]) + assert.Equal(t, sig.minArgumentCount, int32(1)) + assert.Equal(t, sig.getResolvedTypePredicate(c), predicate) + runtime.KeepAlive(c) +} diff --git a/tsc/internal/checker/utilities.go b/tsc/internal/checker/utilities.go index c8b82e3bcff24..2c0c4055ce114 100644 --- a/tsc/internal/checker/utilities.go +++ b/tsc/internal/checker/utilities.go @@ -8,6 +8,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/ast" "github.com/microsoft/TypeScript/tsc/internal/binder" + "github.com/microsoft/TypeScript/tsc/internal/collections" "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/debug" "github.com/microsoft/TypeScript/tsc/internal/diagnostics" @@ -929,6 +930,53 @@ func (c *Checker) releaseTemporaryTypeSet(set *temporaryTypeSet) { c.freeTypeSets = set } +type temporaryPropertyCounts struct { + collections.OrderedMap[string, int] + next *temporaryPropertyCounts +} + +func (c *Checker) getTemporaryPropertyCounts() *temporaryPropertyCounts { + counts := c.freePropertyCounts + if counts == nil { + return &temporaryPropertyCounts{} + } + c.freePropertyCounts = counts.next + counts.next = nil + return counts +} + +func (c *Checker) releaseTemporaryPropertyCounts(counts *temporaryPropertyCounts) { + size := counts.Size() + counts.Clear() + // Recursive reductions borrow separate maps. Do not retain unusually + // large property tables or references to their names in the free list. + if size > 4096 { + return + } + counts.next = c.freePropertyCounts + c.freePropertyCounts = counts +} + +// Property enumeration of a combined type may recursively enumerate other +// combined types, so each call borrows a separate name set. +func (c *Checker) getTemporaryNameSet() map[string]struct{} { + if n := len(c.freeNameSets); n != 0 { + set := c.freeNameSets[n-1] + c.freeNameSets = c.freeNameSets[:n-1] + return set + } + return make(map[string]struct{}) +} + +func (c *Checker) releaseTemporaryNameSet(set map[string]struct{}) { + // Do not retain unusually large tables in the free list. + if len(set) > 4096 { + return + } + clear(set) + c.freeNameSets = append(c.freeNameSets, set) +} + func (s *orderedSet[T]) contains(value T) bool { if s.valuesByKey == nil { return slices.Contains(s.values, value) diff --git a/tsc/internal/core/arena.go b/tsc/internal/core/arena.go index 87e231f70c781..ed795b7eca0f4 100644 --- a/tsc/internal/core/arena.go +++ b/tsc/internal/core/arena.go @@ -1,6 +1,10 @@ package core -import "slices" +import ( + "math/bits" + "slices" + "unsafe" +) // Arena allocator @@ -8,11 +12,11 @@ type Arena[T any] struct { data []T } -// Allocate a single element in the arena and return a pointer to the element. If the arena is at capacity, -// a new arena of the next size up is allocated. +// Allocate a single element at a stable address. Small chunks bound the unused +// capacity and unrelated values retained by a surviving pointer into a chunk. func (a *Arena[T]) New() *T { if len(a.data) == cap(a.data) { - nextSize := nextArenaSize(len(a.data)) + nextSize := nextArenaSize[T](len(a.data)) // Use the same trick as slices.Concat; Grow rounds up to the next size class. a.data = slices.Grow[[]T](nil, nextSize) } @@ -30,7 +34,7 @@ func (a *Arena[T]) NewSlice(size int) []T { return nil } if len(a.data)+size > cap(a.data) { - nextSize := nextArenaSize(len(a.data)) + nextSize := nextArenaSize[T](len(a.data)) if size > nextSize { return make([]T, size) } @@ -58,9 +62,49 @@ func (a *Arena[T]) Clone(t []T) []T { return slice } -func nextArenaSize(size int) int { - // This compiles down branch-free. +func nextArenaSize[T any](size int) int { + // Target 512 bytes before allocator size-class rounding, with at least one + // value. A surviving interior pointer retains and scans the whole chunk. + maxSize := max(512/max(int(unsafe.Sizeof(*new(T))), 1), 1) size = max(size, 1) - size = min(size*2, 256) + size = min(size*2, 256, maxSize) return size } + +// IndexedArena keeps values at stable addresses and references them with uint32 +// indexes instead of Go pointers. Index zero is nil. Unlike Arena, it retains all +// blocks so that an index alone keeps its value reachable through the arena. +type IndexedArena[T any] struct { + blocks [][]T + size uint32 +} + +const indexedArenaPageShift = 8 + +func (a *IndexedArena[T]) New() (uint32, *T) { + if a.size == ^uint32(0) { + panic("IndexedArena exhausted") + } + if len(a.blocks) == 0 || len(a.blocks[len(a.blocks)-1]) == cap(a.blocks[len(a.blocks)-1]) { + size := 1 << min(len(a.blocks), indexedArenaPageShift) + a.blocks = append(a.blocks, make([]T, 0, size)) + } + block := &a.blocks[len(a.blocks)-1] + *block = (*block)[:len(*block)+1] + a.size++ + return a.size, &(*block)[len(*block)-1] +} + +func (a *IndexedArena[T]) Get(index uint32) *T { + if index == 0 { + return nil + } + // The initial blocks have sizes 1, 2, 4, ..., 128. Subsequent + // blocks have 256 entries, avoiding geometric over-allocation. + if index < 1<>indexedArenaPageShift)][index&((1<> LinkPageShift + return int(pageIndex) < len(s.pages) && s.pages[pageIndex] != nil && s.pages[pageIndex][key&LinkPageMask] != 0 } func (s *PagedLinkStore[V]) TryGet(key uint64) *V { pageIndex := key >> LinkPageShift if int(pageIndex) < len(s.pages) { if page := s.pages[pageIndex]; page != nil { - return page[key&LinkPageMask] + return s.arena.Get(page[key&LinkPageMask]) } } return nil diff --git a/tsc/internal/core/linkstore_test.go b/tsc/internal/core/linkstore_test.go new file mode 100644 index 0000000000000..af785a980a7f8 --- /dev/null +++ b/tsc/internal/core/linkstore_test.go @@ -0,0 +1,145 @@ +package core_test + +import ( + "runtime" + "strconv" + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/core" + "gotest.tools/v3/assert" +) + +func TestLinkStore(t *testing.T) { + t.Parallel() + type value struct { + number int + text *string + } + var store core.LinkStore[int, value] + assert.Assert(t, !store.Has(0)) + assert.Assert(t, store.TryGet(0) == nil) + + values := make([]*value, 4096) + for i := range values { + v := store.Get(i) + assert.Equal(t, *v, value{}) + v.number = i + v.text = new("retained") + values[i] = v + } + runtime.GC() + for i, v := range values { + assert.Assert(t, store.Has(i)) + assert.Equal(t, store.Get(i), v) + assert.Equal(t, store.TryGet(i), v) + assert.Equal(t, v.number, i) + assert.Equal(t, *v.text, "retained") + } + assert.Assert(t, !store.Has(len(values))) + assert.Assert(t, store.TryGet(len(values)) == nil) +} + +func TestPagedLinkStore(t *testing.T) { + t.Parallel() + var store core.PagedLinkStore[int] + keys := []uint64{0, 255, 256, 65535, 16777215} + values := make([]*int, len(keys)) + for _, key := range keys { + assert.Assert(t, !store.Has(key)) + } + for i, key := range keys { + v := store.Get(key) + assert.Equal(t, *v, 0) + *v = i + 1 + values[i] = v + } + runtime.GC() + for i, key := range keys { + assert.Assert(t, store.Has(key)) + assert.Equal(t, store.Get(key), values[i]) + assert.Equal(t, *store.TryGet(key), i+1) + } + assert.Assert(t, !store.Has(1)) + assert.Assert(t, store.TryGet(1) == nil) +} + +func BenchmarkLinkStore(b *testing.B) { + type value struct { + typ *int + flags uint32 + } + b.ReportAllocs() + for b.Loop() { + var store core.LinkStore[int, value] + for i := range 10000 { + store.Get(i).flags = uint32(i) + } + for i := range 10000 { + if store.Get(i).flags != uint32(i) { + b.Fatal("link changed during arena growth") + } + } + + } +} + +func TestIndexedArena(t *testing.T) { + t.Parallel() + type value struct { + number int + text *string + } + var arena core.IndexedArena[value] + assert.Assert(t, arena.Get(0) == nil) + values := make([]*value, 4096) + for i := range values { + index, v := arena.New() + assert.Equal(t, index, uint32(i+1)) + assert.Equal(t, *v, value{}) + v.number = i + v.text = new("retained") + values[i] = v + } + runtime.GC() + for i, v := range values { + assert.Equal(t, arena.Get(uint32(i+1)), v) + assert.Equal(t, v.number, i) + assert.Equal(t, *v.text, "retained") + } +} + +func TestIndexedArenaReachability(t *testing.T) { + t.Parallel() + var arena core.IndexedArena[*string] + for i := range 4096 { + _, value := arena.New() + *value = new(strconv.Itoa(i)) + } + runtime.GC() + for i := range 4096 { + assert.Equal(t, **arena.Get(uint32(i + 1)), strconv.Itoa(i)) + } +} + +func BenchmarkPagedLinkStore(b *testing.B) { + for _, stride := range []uint64{1, 16, 256} { + b.Run(strconv.FormatUint(stride, 10), func(b *testing.B) { + type value struct { + typ *int + flags uint32 + } + b.ReportAllocs() + for b.Loop() { + var store core.PagedLinkStore[value] + for i := range 10000 { + store.Get(uint64(i) * stride).flags = uint32(i) + } + for i := range 10000 { + if store.Get(uint64(i)*stride).flags != uint32(i) { + b.Fatal("link changed during arena growth") + } + } + } + }) + } +} diff --git a/tsc/internal/transformers/declarations/transform.go b/tsc/internal/transformers/declarations/transform.go index 9333392d53fb8..1eb9d3cf63838 100644 --- a/tsc/internal/transformers/declarations/transform.go +++ b/tsc/internal/transformers/declarations/transform.go @@ -1738,9 +1738,12 @@ func (tx *DeclarationTransformer) transformTopLevelDeclaration(input *ast.Node) if input.Kind == ast.KindImportDeclaration || input.Kind == ast.KindJSImportDeclaration { res := tx.transformImportDeclaration(input.AsImportDeclaration()) if res != nil && res.Kind != ast.KindImportDeclaration { - res := res.Clone(tx.Factory()) - res.Kind = ast.KindImportDeclaration - return res + decl := res.AsImportDeclaration() + replacement := tx.Factory().NewImportDeclaration(decl.Modifiers(), decl.ImportClause, decl.ModuleSpecifier, decl.Attributes) + replacement.Flags = res.Flags + replacement.Loc = res.Loc + tx.EmitContext().SetOriginal(replacement, res) + return replacement } return res } diff --git a/tsc/testdata/baselines/reference/conformance/importTag5.js b/tsc/testdata/baselines/reference/conformance/importTag5.js index 3d2361c1ad8fb..b8856f6c0ba42 100644 --- a/tsc/testdata/baselines/reference/conformance/importTag5.js +++ b/tsc/testdata/baselines/reference/conformance/importTag5.js @@ -4,6 +4,9 @@ export interface Foo { a: number; } +export interface Unused { + unused: true; +} //// [foo.js] /** @@ -15,6 +18,26 @@ export interface Foo { */ function f(foo) {} +//// [imports.js] +/** + * @import { Foo, Unused } from "./types" + */ +/** + * @param {Foo} foo + */ +export function filtered(foo) { + return foo; +} +/** + * @import * as types from "./types" + */ +/** + * @param {types.Foo} foo + */ +export function namespace(foo) { + return foo; +} + @@ -22,7 +45,10 @@ function f(foo) {} export interface Foo { a: number; } -//// [foo.d.ts] +export interface Unused { + unused: true; +} +//# sourceMappingURL=types.d.ts.map//// [foo.d.ts] /** * @import { Foo } from "./types" */ @@ -31,3 +57,21 @@ import type { Foo } from "./types"; * @param { Foo } foo */ declare function f(foo: Foo): void; +//# sourceMappingURL=foo.d.ts.map//// [imports.d.ts] +import type { Foo } from "./types"; +/** + * @import { Foo, Unused } from "./types" + */ +/** + * @param {Foo} foo + */ +export declare function filtered(foo: Foo): Foo; +import type * as types from "./types"; +/** + * @import * as types from "./types" + */ +/** + * @param {types.Foo} foo + */ +export declare function namespace(foo: types.Foo): Foo; +//# sourceMappingURL=imports.d.ts.map \ No newline at end of file diff --git a/tsc/testdata/baselines/reference/conformance/importTag5.js.map b/tsc/testdata/baselines/reference/conformance/importTag5.js.map new file mode 100644 index 0000000000000..e26abef78ca3b --- /dev/null +++ b/tsc/testdata/baselines/reference/conformance/importTag5.js.map @@ -0,0 +1,11 @@ +//// [foo.d.ts.map] +{"version":3,"file":"foo.d.ts","sources":["foo.js"],"names":[],"mappings":"AAAA;;GAEG;AADA,OAAQ,KAAA,EAAE,GAAG,EAAE,MAAM,SAAS,CAAA;AAGjC;;GAEG;AACH,iBAAS,CAAC,CAAC,GAAG,EAFF,GAEE,QAAI"} +//// https://sokra.github.io/source-map-visualization#base64,LyoqDQogKiBAaW1wb3J0IHsgRm9vIH0gZnJvbSAiLi90eXBlcyINCiAqLw0KaW1wb3J0IHR5cGUgeyBGb28gfSBmcm9tICIuL3R5cGVzIjsNCi8qKg0KICogQHBhcmFtIHsgRm9vIH0gZm9vDQogKi8NCmRlY2xhcmUgZnVuY3Rpb24gZihmb286IEZvbyk6IHZvaWQ7DQovLyMgc291cmNlTWFwcGluZ1VSTD1mb28uZC50cy5tYXA=,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZm9vLmQudHMiLCJzb3VyY2VzIjpbImZvby5qcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7R0FFRztBQURBLE9BQVEsS0FBQSxFQUFFLEdBQUcsRUFBRSxNQUFNLFNBQVMsQ0FBQTtBQUdqQzs7R0FFRztBQUNILGlCQUFTLENBQUMsQ0FBQyxHQUFHLEVBRkYsR0FFRSxRQUFJIn0=,LyoqCiAqIEBpbXBvcnQgeyBGb28gfSBmcm9tICIuL3R5cGVzIgogKi8KCi8qKgogKiBAcGFyYW0geyBGb28gfSBmb28KICovCmZ1bmN0aW9uIGYoZm9vKSB7fQo= + +//// [imports.d.ts.map] +{"version":3,"file":"imports.d.ts","sources":["imports.js"],"names":[],"mappings":"AACG,OAAQ,KAAA,EAAE,GAAG,EAAU,MAAM,SAAS,CAAA;AADzC;;GAEG;AACH;;GAEG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAFjB,GAEiB,OAE3B;AAEE,OAAQ,KAAA,KAAK,KAAK,MAAM,SAAS,CAAA;AADpC;;GAEG;AACH;;GAEG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAFlB,KAAK,CAAC,GAEY,OAE5B"} +//// https://sokra.github.io/source-map-visualization#base64,aW1wb3J0IHR5cGUgeyBGb28gfSBmcm9tICIuL3R5cGVzIjsNCi8qKg0KICogQGltcG9ydCB7IEZvbywgVW51c2VkIH0gZnJvbSAiLi90eXBlcyINCiAqLw0KLyoqDQogKiBAcGFyYW0ge0Zvb30gZm9vDQogKi8NCmV4cG9ydCBkZWNsYXJlIGZ1bmN0aW9uIGZpbHRlcmVkKGZvbzogRm9vKTogRm9vOw0KaW1wb3J0IHR5cGUgKiBhcyB0eXBlcyBmcm9tICIuL3R5cGVzIjsNCi8qKg0KICogQGltcG9ydCAqIGFzIHR5cGVzIGZyb20gIi4vdHlwZXMiDQogKi8NCi8qKg0KICogQHBhcmFtIHt0eXBlcy5Gb299IGZvbw0KICovDQpleHBvcnQgZGVjbGFyZSBmdW5jdGlvbiBuYW1lc3BhY2UoZm9vOiB0eXBlcy5Gb28pOiBGb287DQovLyMgc291cmNlTWFwcGluZ1VSTD1pbXBvcnRzLmQudHMubWFw,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW1wb3J0cy5kLnRzIiwic291cmNlcyI6WyJpbXBvcnRzLmpzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUNHLE9BQVEsS0FBQSxFQUFFLEdBQUcsRUFBVSxNQUFNLFNBQVMsQ0FBQTtBQUR6Qzs7R0FFRztBQUNIOztHQUVHO0FBQ0gsd0JBQWdCLFFBQVEsQ0FBQyxHQUFHLEVBRmpCLEdBRWlCLE9BRTNCO0FBRUUsT0FBUSxLQUFBLEtBQUssS0FBSyxNQUFNLFNBQVMsQ0FBQTtBQURwQzs7R0FFRztBQUNIOztHQUVHO0FBQ0gsd0JBQWdCLFNBQVMsQ0FBQyxHQUFHLEVBRmxCLEtBQUssQ0FBQyxHQUVZLE9BRTVCIn0=,LyoqCiAqIEBpbXBvcnQgeyBGb28sIFVudXNlZCB9IGZyb20gIi4vdHlwZXMiCiAqLwovKioKICogQHBhcmFtIHtGb299IGZvbwogKi8KZXhwb3J0IGZ1bmN0aW9uIGZpbHRlcmVkKGZvbykgewogICAgcmV0dXJuIGZvbzsKfQovKioKICogQGltcG9ydCAqIGFzIHR5cGVzIGZyb20gIi4vdHlwZXMiCiAqLwovKioKICogQHBhcmFtIHt0eXBlcy5Gb299IGZvbwogKi8KZXhwb3J0IGZ1bmN0aW9uIG5hbWVzcGFjZShmb28pIHsKICAgIHJldHVybiBmb287Cn0K + +//// [types.d.ts.map] +{"version":3,"file":"types.d.ts","sources":["types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,GAAG;IAChB,CAAC,EAAE,MAAM,CAAC;CACb;AACD,MAAM,WAAW,MAAM;IACnB,MAAM,EAAE,IAAI,CAAC;CAChB"} +//// https://sokra.github.io/source-map-visualization#base64,ZXhwb3J0IGludGVyZmFjZSBGb28gew0KICAgIGE6IG51bWJlcjsNCn0NCmV4cG9ydCBpbnRlcmZhY2UgVW51c2VkIHsNCiAgICB1bnVzZWQ6IHRydWU7DQp9DQovLyMgc291cmNlTWFwcGluZ1VSTD10eXBlcy5kLnRzLm1hcA==,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHlwZXMuZC50cyIsInNvdXJjZXMiOlsidHlwZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsTUFBTSxXQUFXLEdBQUc7SUFDaEIsQ0FBQyxFQUFFLE1BQU0sQ0FBQztDQUNiO0FBQ0QsTUFBTSxXQUFXLE1BQU07SUFDbkIsTUFBTSxFQUFFLElBQUksQ0FBQztDQUNoQiJ9,ZXhwb3J0IGludGVyZmFjZSBGb28gewogICAgYTogbnVtYmVyOwp9CmV4cG9ydCBpbnRlcmZhY2UgVW51c2VkIHsKICAgIHVudXNlZDogdHJ1ZTsKfQo= diff --git a/tsc/testdata/baselines/reference/conformance/importTag5.sourcemap.txt b/tsc/testdata/baselines/reference/conformance/importTag5.sourcemap.txt new file mode 100644 index 0000000000000..5afb9fdff1454 --- /dev/null +++ b/tsc/testdata/baselines/reference/conformance/importTag5.sourcemap.txt @@ -0,0 +1,389 @@ +=================================================================== +JsFile: types.d.ts +mapUrl: types.d.ts.map +sourceRoot: +sources: types.ts +=================================================================== +------------------------------------------------------------------- +emittedFile:/types.d.ts +sourceFile:types.ts +------------------------------------------------------------------- +>>>export interface Foo { +1 > +2 >^^^^^^ +3 > ^^^^^^^^^^^ +4 > ^^^ +1 > +2 >export +3 > interface +4 > Foo +1 >Emitted(1, 1) Source(1, 1) + SourceIndex(0) +2 >Emitted(1, 7) Source(1, 7) + SourceIndex(0) +3 >Emitted(1, 18) Source(1, 18) + SourceIndex(0) +4 >Emitted(1, 21) Source(1, 21) + SourceIndex(0) +--- +>>> a: number; +1 >^^^^ +2 > ^ +3 > ^^ +4 > ^^^^^^ +5 > ^ +1 > { + > +2 > a +3 > : +4 > number +5 > ; +1 >Emitted(2, 5) Source(2, 5) + SourceIndex(0) +2 >Emitted(2, 6) Source(2, 6) + SourceIndex(0) +3 >Emitted(2, 8) Source(2, 8) + SourceIndex(0) +4 >Emitted(2, 14) Source(2, 14) + SourceIndex(0) +5 >Emitted(2, 15) Source(2, 15) + SourceIndex(0) +--- +>>>} +1 >^ +2 > ^^^^^^^^^^^^^^^^^^^^^^^^^-> +1 > + >} +1 >Emitted(3, 2) Source(3, 2) + SourceIndex(0) +--- +>>>export interface Unused { +1-> +2 >^^^^^^ +3 > ^^^^^^^^^^^ +4 > ^^^^^^ +1-> + > +2 >export +3 > interface +4 > Unused +1->Emitted(4, 1) Source(4, 1) + SourceIndex(0) +2 >Emitted(4, 7) Source(4, 7) + SourceIndex(0) +3 >Emitted(4, 18) Source(4, 18) + SourceIndex(0) +4 >Emitted(4, 24) Source(4, 24) + SourceIndex(0) +--- +>>> unused: true; +1 >^^^^ +2 > ^^^^^^ +3 > ^^ +4 > ^^^^ +5 > ^ +1 > { + > +2 > unused +3 > : +4 > true +5 > ; +1 >Emitted(5, 5) Source(5, 5) + SourceIndex(0) +2 >Emitted(5, 11) Source(5, 11) + SourceIndex(0) +3 >Emitted(5, 13) Source(5, 13) + SourceIndex(0) +4 >Emitted(5, 17) Source(5, 17) + SourceIndex(0) +5 >Emitted(5, 18) Source(5, 18) + SourceIndex(0) +--- +>>>} +1 >^ +2 > ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^-> +1 > + >} +1 >Emitted(6, 2) Source(6, 2) + SourceIndex(0) +--- +>>>//# sourceMappingURL=types.d.ts.map=================================================================== +JsFile: foo.d.ts +mapUrl: foo.d.ts.map +sourceRoot: +sources: foo.js +=================================================================== +------------------------------------------------------------------- +emittedFile:/foo.d.ts +sourceFile:foo.js +------------------------------------------------------------------- +>>>/** +1 > +2 >^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^-> +1 > +1 >Emitted(1, 1) Source(1, 1) + SourceIndex(0) +--- +>>> * @import { Foo } from "./types" +>>> */ +1->^^^ +2 > ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^-> +1->/** + > * @import { Foo } from "./types" + > */ +1->Emitted(3, 4) Source(3, 4) + SourceIndex(0) +--- +>>>import type { Foo } from "./types"; +1-> +2 >^^^^^^^ +3 > ^^^^^ +4 > ^^ +5 > ^^^ +6 > ^^ +7 > ^^^^^^ +8 > ^^^^^^^^^ +9 > ^ +1-> +2 >@import +3 > +4 > { +5 > Foo +6 > } +7 > from +8 > "./types" +9 > +1->Emitted(4, 1) Source(2, 4) + SourceIndex(0) +2 >Emitted(4, 8) Source(2, 12) + SourceIndex(0) +3 >Emitted(4, 13) Source(2, 12) + SourceIndex(0) +4 >Emitted(4, 15) Source(2, 14) + SourceIndex(0) +5 >Emitted(4, 18) Source(2, 17) + SourceIndex(0) +6 >Emitted(4, 20) Source(2, 19) + SourceIndex(0) +7 >Emitted(4, 26) Source(2, 25) + SourceIndex(0) +8 >Emitted(4, 35) Source(2, 34) + SourceIndex(0) +9 >Emitted(4, 36) Source(2, 34) + SourceIndex(0) +--- +>>>/** +1 > +2 >^^^^^^^^^^^^^^^^^^^^^^-> +1 > + > */ + > + > +1 >Emitted(5, 1) Source(5, 1) + SourceIndex(0) +--- +>>> * @param { Foo } foo +>>> */ +1->^^^ +2 > ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^-> +1->/** + > * @param { Foo } foo + > */ +1->Emitted(7, 4) Source(7, 4) + SourceIndex(0) +--- +>>>declare function f(foo: Foo): void; +1-> +2 >^^^^^^^^^^^^^^^^^ +3 > ^ +4 > ^ +5 > ^^^ +6 > ^^ +7 > ^^^ +8 > ^^^^^^^^ +1-> + > +2 >function +3 > f +4 > ( +5 > foo +6 > +7 > Foo } foo + > */ + > function f(foo +8 > ) {} +1->Emitted(8, 1) Source(8, 1) + SourceIndex(0) +2 >Emitted(8, 18) Source(8, 10) + SourceIndex(0) +3 >Emitted(8, 19) Source(8, 11) + SourceIndex(0) +4 >Emitted(8, 20) Source(8, 12) + SourceIndex(0) +5 >Emitted(8, 23) Source(8, 15) + SourceIndex(0) +6 >Emitted(8, 25) Source(6, 13) + SourceIndex(0) +7 >Emitted(8, 28) Source(8, 15) + SourceIndex(0) +8 >Emitted(8, 36) Source(8, 19) + SourceIndex(0) +--- +>>>//# sourceMappingURL=foo.d.ts.map=================================================================== +JsFile: imports.d.ts +mapUrl: imports.d.ts.map +sourceRoot: +sources: imports.js +=================================================================== +------------------------------------------------------------------- +emittedFile:/imports.d.ts +sourceFile:imports.js +------------------------------------------------------------------- +>>>import type { Foo } from "./types"; +1 > +2 >^^^^^^^ +3 > ^^^^^ +4 > ^^ +5 > ^^^ +6 > ^^ +7 > ^^^^^^ +8 > ^^^^^^^^^ +9 > ^ +1 >/** + > * +2 >@import +3 > +4 > { +5 > Foo +6 > , Unused } +7 > from +8 > "./types" +9 > +1 >Emitted(1, 1) Source(2, 4) + SourceIndex(0) +2 >Emitted(1, 8) Source(2, 12) + SourceIndex(0) +3 >Emitted(1, 13) Source(2, 12) + SourceIndex(0) +4 >Emitted(1, 15) Source(2, 14) + SourceIndex(0) +5 >Emitted(1, 18) Source(2, 17) + SourceIndex(0) +6 >Emitted(1, 20) Source(2, 27) + SourceIndex(0) +7 >Emitted(1, 26) Source(2, 33) + SourceIndex(0) +8 >Emitted(1, 35) Source(2, 42) + SourceIndex(0) +9 >Emitted(1, 36) Source(2, 42) + SourceIndex(0) +--- +>>>/** +1 > +2 >^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^-> +1 > +1 >Emitted(2, 1) Source(1, 1) + SourceIndex(0) +--- +>>> * @import { Foo, Unused } from "./types" +>>> */ +1->^^^ +2 > ^-> +1->/** + > * @import { Foo, Unused } from "./types" + > */ +1->Emitted(4, 4) Source(3, 4) + SourceIndex(0) +--- +>>>/** +1-> +2 >^^^^^^^^^^^^^^^^^^^^-> +1-> + > +1->Emitted(5, 1) Source(4, 1) + SourceIndex(0) +--- +>>> * @param {Foo} foo +>>> */ +1->^^^ +2 > ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^-> +1->/** + > * @param {Foo} foo + > */ +1->Emitted(7, 4) Source(6, 4) + SourceIndex(0) +--- +>>>export declare function filtered(foo: Foo): Foo; +1-> +2 >^^^^^^^^^^^^^^^^^^^^^^^^ +3 > ^^^^^^^^ +4 > ^ +5 > ^^^ +6 > ^^ +7 > ^^^ +8 > ^^^^^^^ +1-> + > +2 >export function +3 > filtered +4 > ( +5 > foo +6 > +7 > Foo} foo + > */ + > export function filtered(foo +8 > ) { + > return foo; + > } +1->Emitted(8, 1) Source(7, 1) + SourceIndex(0) +2 >Emitted(8, 25) Source(7, 17) + SourceIndex(0) +3 >Emitted(8, 33) Source(7, 25) + SourceIndex(0) +4 >Emitted(8, 34) Source(7, 26) + SourceIndex(0) +5 >Emitted(8, 37) Source(7, 29) + SourceIndex(0) +6 >Emitted(8, 39) Source(5, 12) + SourceIndex(0) +7 >Emitted(8, 42) Source(7, 29) + SourceIndex(0) +8 >Emitted(8, 49) Source(9, 2) + SourceIndex(0) +--- +>>>import type * as types from "./types"; +1 > +2 >^^^^^^^ +3 > ^^^^^ +4 > ^^^^^ +5 > ^^^^^ +6 > ^^^^^^ +7 > ^^^^^^^^^ +8 > ^ +1 > + >/** + > * +2 >@import +3 > +4 > * as +5 > types +6 > from +7 > "./types" +8 > +1 >Emitted(9, 1) Source(11, 4) + SourceIndex(0) +2 >Emitted(9, 8) Source(11, 12) + SourceIndex(0) +3 >Emitted(9, 13) Source(11, 12) + SourceIndex(0) +4 >Emitted(9, 18) Source(11, 17) + SourceIndex(0) +5 >Emitted(9, 23) Source(11, 22) + SourceIndex(0) +6 >Emitted(9, 29) Source(11, 28) + SourceIndex(0) +7 >Emitted(9, 38) Source(11, 37) + SourceIndex(0) +8 >Emitted(9, 39) Source(11, 37) + SourceIndex(0) +--- +>>>/** +1 > +2 >^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^-> +1 > +1 >Emitted(10, 1) Source(10, 1) + SourceIndex(0) +--- +>>> * @import * as types from "./types" +>>> */ +1->^^^ +2 > ^-> +1->/** + > * @import * as types from "./types" + > */ +1->Emitted(12, 4) Source(12, 4) + SourceIndex(0) +--- +>>>/** +1-> +2 >^^^^^^^^^^^^^^^^^^^^^^^^^^-> +1-> + > +1->Emitted(13, 1) Source(13, 1) + SourceIndex(0) +--- +>>> * @param {types.Foo} foo +>>> */ +1->^^^ +2 > ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^-> +1->/** + > * @param {types.Foo} foo + > */ +1->Emitted(15, 4) Source(15, 4) + SourceIndex(0) +--- +>>>export declare function namespace(foo: types.Foo): Foo; +1-> +2 >^^^^^^^^^^^^^^^^^^^^^^^^ +3 > ^^^^^^^^^ +4 > ^ +5 > ^^^ +6 > ^^ +7 > ^^^^^ +8 > ^ +9 > ^^^ +10> ^^^^^^^ +1-> + > +2 >export function +3 > namespace +4 > ( +5 > foo +6 > +7 > types +8 > . +9 > Foo} foo + > */ + > export function namespace(foo +10> ) { + > return foo; + > } +1->Emitted(16, 1) Source(16, 1) + SourceIndex(0) +2 >Emitted(16, 25) Source(16, 17) + SourceIndex(0) +3 >Emitted(16, 34) Source(16, 26) + SourceIndex(0) +4 >Emitted(16, 35) Source(16, 27) + SourceIndex(0) +5 >Emitted(16, 38) Source(16, 30) + SourceIndex(0) +6 >Emitted(16, 40) Source(14, 12) + SourceIndex(0) +7 >Emitted(16, 45) Source(14, 17) + SourceIndex(0) +8 >Emitted(16, 46) Source(14, 18) + SourceIndex(0) +9 >Emitted(16, 49) Source(16, 30) + SourceIndex(0) +10>Emitted(16, 56) Source(18, 2) + SourceIndex(0) +--- +>>>//# sourceMappingURL=imports.d.ts.map \ No newline at end of file diff --git a/tsc/testdata/baselines/reference/conformance/importTag5.symbols b/tsc/testdata/baselines/reference/conformance/importTag5.symbols index 35242e6c8988e..9775067933615 100644 --- a/tsc/testdata/baselines/reference/conformance/importTag5.symbols +++ b/tsc/testdata/baselines/reference/conformance/importTag5.symbols @@ -7,6 +7,12 @@ export interface Foo { a: number; >a : Symbol(Foo.a, Decl(types.ts, 0, 22)) } +export interface Unused { +>Unused : Symbol(Unused, Decl(types.ts, 2, 1)) + + unused: true; +>unused : Symbol(Unused.unused, Decl(types.ts, 3, 25)) +} === /foo.js === /** @@ -20,3 +26,31 @@ function f(foo) {} >f : Symbol(f, Decl(foo.js, 0, 0)) >foo : Symbol(foo, Decl(foo.js, 7, 11)) +=== /imports.js === +/** + * @import { Foo, Unused } from "./types" + */ +/** + * @param {Foo} foo + */ +export function filtered(foo) { +>filtered : Symbol(filtered, Decl(imports.js, 0, 0)) +>foo : Symbol(foo, Decl(imports.js, 6, 25)) + + return foo; +>foo : Symbol(foo, Decl(imports.js, 6, 25)) +} +/** + * @import * as types from "./types" + */ +/** + * @param {types.Foo} foo + */ +export function namespace(foo) { +>namespace : Symbol(namespace, Decl(imports.js, 8, 1)) +>foo : Symbol(foo, Decl(imports.js, 15, 26)) + + return foo; +>foo : Symbol(foo, Decl(imports.js, 15, 26)) +} + diff --git a/tsc/testdata/baselines/reference/conformance/importTag5.types b/tsc/testdata/baselines/reference/conformance/importTag5.types index 7d9641be55328..c2ff4244e0694 100644 --- a/tsc/testdata/baselines/reference/conformance/importTag5.types +++ b/tsc/testdata/baselines/reference/conformance/importTag5.types @@ -5,6 +5,11 @@ export interface Foo { a: number; >a : number } +export interface Unused { + unused: true; +>unused : true +>true : true +} === /foo.js === /** @@ -18,3 +23,31 @@ function f(foo) {} >f : (foo: Foo) => void >foo : Foo +=== /imports.js === +/** + * @import { Foo, Unused } from "./types" + */ +/** + * @param {Foo} foo + */ +export function filtered(foo) { +>filtered : (foo: Foo) => Foo +>foo : Foo + + return foo; +>foo : Foo +} +/** + * @import * as types from "./types" + */ +/** + * @param {types.Foo} foo + */ +export function namespace(foo) { +>namespace : (foo: types.Foo) => Foo +>foo : Foo + + return foo; +>foo : Foo +} + diff --git a/tsc/testdata/tests/cases/conformance/jsdoc/importTag5.ts b/tsc/testdata/tests/cases/conformance/jsdoc/importTag5.ts index eb934954c4dba..f8f7836a2eb9c 100644 --- a/tsc/testdata/tests/cases/conformance/jsdoc/importTag5.ts +++ b/tsc/testdata/tests/cases/conformance/jsdoc/importTag5.ts @@ -3,11 +3,15 @@ // @allowJs: true // @declaration: true // @emitDeclarationOnly: true +// @declarationMap: true // @filename: /types.ts export interface Foo { a: number; } +export interface Unused { + unused: true; +} // @filename: /foo.js /** @@ -18,3 +22,23 @@ export interface Foo { * @param { Foo } foo */ function f(foo) {} + +// @filename: /imports.js +/** + * @import { Foo, Unused } from "./types" + */ +/** + * @param {Foo} foo + */ +export function filtered(foo) { + return foo; +} +/** + * @import * as types from "./types" + */ +/** + * @param {types.Foo} foo + */ +export function namespace(foo) { + return foo; +}