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 e58e96048e94b..efe984f219a4f 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -690,6 +690,8 @@ type Checker struct { outerTypeParameterListArena core.Arena[[]*Type] valueSymbolExtraArena core.Arena[valueSymbolExtra] structuredTypeExtraArena core.Arena[structuredTypeExtra] + memberIndexArena core.Arena[memberIndex] + memberIndexSlotsArena core.Arena[uint32] freeTypeSets *temporaryTypeSet freePropertyCounts *temporaryPropertyCounts mergedSymbols map[*ast.Symbol]*ast.Symbol @@ -10951,7 +10953,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 } @@ -16163,7 +16165,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 } @@ -18068,7 +18070,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 } @@ -18078,12 +18080,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 { @@ -19363,7 +19368,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 @@ -19571,6 +19576,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 @@ -19584,7 +19590,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) @@ -19593,6 +19606,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 { @@ -19615,6 +19633,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 { @@ -21870,7 +21893,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 } @@ -21893,38 +21916,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 @@ -21960,10 +22039,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 @@ -22026,17 +22105,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. @@ -22046,8 +22125,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) @@ -22070,8 +22171,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 @@ -22080,7 +22181,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 { @@ -22164,9 +22265,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 } @@ -22356,7 +22457,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 } } @@ -22364,6 +22465,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()) { @@ -25659,36 +25818,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 } @@ -25751,8 +25900,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) } @@ -31814,7 +31973,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 { @@ -32310,7 +32469,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 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/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 3d84ff3bd4094..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 diff --git a/tsc/internal/checker/relater.go b/tsc/internal/checker/relater.go index d3b6c86014498..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 { @@ -2584,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 90f3f2f901844..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()) } } diff --git a/tsc/internal/checker/types.go b/tsc/internal/checker/types.go index a3cfe28c2e735..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 @@ -1114,7 +1115,7 @@ func (t *ConstrainedType) AsConstrainedType() *ConstrainedType { return t } type StructuredType struct { ConstrainedType - members ast.SymbolTable + members *memberIndex properties []*ast.Symbol extra *structuredTypeExtra } @@ -1168,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. @@ -1362,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 } diff --git a/tsc/internal/checker/types_test.go b/tsc/internal/checker/types_test.go index 86de2b089e06e..dc422eed2b8cf 100644 --- a/tsc/internal/checker/types_test.go +++ b/tsc/internal/checker/types_test.go @@ -98,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)) + } + } } } @@ -142,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") @@ -441,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) @@ -507,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))) diff --git a/tsc/internal/core/arena.go b/tsc/internal/core/arena.go index bbe802464138c..ed795b7eca0f4 100644 --- a/tsc/internal/core/arena.go +++ b/tsc/internal/core/arena.go @@ -1,6 +1,7 @@ package core import ( + "math/bits" "slices" "unsafe" ) @@ -69,3 +70,41 @@ func nextArenaSize[T any](size int) int { 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; +}