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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions internal/checker/nodebuilderimpl.go
Original file line number Diff line number Diff line change
Expand Up @@ -1991,7 +1991,7 @@ func (b *NodeBuilderImpl) serializeReturnTypeForSignature(signature *Signature,
if pt != nil {
// !!! TODO: If annotated type node is a reference with insufficient type arguments, we should still fall back to type serialization
// see: canReuseTypeNodeAnnotation in strada for context
returnTypeNode = b.pseudoTypeToNode(pt)
returnTypeNode = b.pseudoTypeToNodeWithCheckerFallback(pt, returnType)
}
}
restore()
Expand Down Expand Up @@ -2095,11 +2095,11 @@ func (b *NodeBuilderImpl) serializeTypeForDeclaration(declaration *ast.Declarati
if ptt != nil && requiresAddingUndefined && containsNonMissingUndefinedType(b.ch, t) && !containsNonMissingUndefinedType(b.ch, ptt) {
pt = pseudochecker.NewPseudoTypeUnion([]*pseudochecker.PseudoType{pt, pseudochecker.PseudoTypeUndefined})
}
result = b.pseudoTypeToNode(pt)
result = b.pseudoTypeToNodeWithCheckerFallback(pt, t)
} else if requiresAddingUndefined {
pt = pseudochecker.NewPseudoTypeUnion([]*pseudochecker.PseudoType{pt, pseudochecker.PseudoTypeUndefined})
if b.pseudoTypeEquivalentToType(pt, t, false, !b.ctx.suppressReportInferenceFallback) {
result = b.pseudoTypeToNode(pt)
result = b.pseudoTypeToNodeWithCheckerFallback(pt, t)
}
}
remove()
Expand Down
66 changes: 58 additions & 8 deletions internal/checker/pseudotypenodebuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,48 @@ import (
"github.com/microsoft/typescript-go/internal/pseudochecker"
)

// pseudoTypeToNodeWithCheckerFallback is like pseudoTypeToNode but when the top-level pseudo type
// is PseudoTypeInferred, it reports any error nodes and then serializes from the checker's type.
// This avoids incorrect type output when PseudoTypeInferred would derive the type from the
// original declaration expression in an instantiated context.
func (b *NodeBuilderImpl) pseudoTypeToNodeWithCheckerFallback(t *pseudochecker.PseudoType, checkerType *Type) *ast.Node {
if t.Kind == pseudochecker.PseudoTypeKindInferred {
if !b.ctx.suppressReportInferenceFallback {
if errorNodes := t.AsPseudoTypeInferred().ErrorNodes; len(errorNodes) > 0 {
for _, n := range errorNodes {
b.ctx.tracker.ReportInferenceFallback(n)
}
} else {
b.ctx.tracker.ReportInferenceFallback(t.AsPseudoTypeInferred().Expression)
}
}
oldSuppress := b.ctx.suppressReportInferenceFallback
b.ctx.suppressReportInferenceFallback = true
result := b.typeToTypeNode(checkerType)
b.ctx.suppressReportInferenceFallback = oldSuppress
return result
}
return b.pseudoTypeToNode(t)
}

// Maps a pseudochecker's pseudotypes into ast nodes and reports any inference fallback errors the pseudotype structure implies
func (b *NodeBuilderImpl) pseudoTypeToNode(t *pseudochecker.PseudoType) *ast.Node {
debug.Assert(t != nil, "Attempted to serialize nil pseudotype")
switch t.Kind {
case pseudochecker.PseudoTypeKindDirect:
return b.reuseTypeNode(t.AsPseudoTypeDirect().TypeNode)
case pseudochecker.PseudoTypeKindInferred:
node := t.AsPseudoTypeInferred().Expression
b.ctx.tracker.ReportInferenceFallback(node)
inferred := t.AsPseudoTypeInferred()
node := inferred.Expression
if errorNodes := inferred.ErrorNodes; len(errorNodes) > 0 {
for _, n := range errorNodes {
b.ctx.tracker.ReportInferenceFallback(n)
}
} else if ast.IsEntityNameExpression(node) && ast.IsDeclaration(node.Parent) {
b.ctx.tracker.ReportInferenceFallback(node.Parent)
} else {
b.ctx.tracker.ReportInferenceFallback(node)
}
// use symbol type from parent declaration to automatically handle expression type widening without duplicating logic
if ast.IsReturnStatement(node.Parent) {
enclosing := ast.GetContainingFunction(node)
Expand Down Expand Up @@ -316,6 +349,17 @@ func (b *NodeBuilderImpl) pseudoTypeEquivalentToType(t *pseudochecker.PseudoType
}
// otherwise, fallback to actual pseudo/type cross-comparisons
switch t.Kind {
case pseudochecker.PseudoTypeKindInferred:
// PseudoTypeInferred with error nodes is always considered equivalent —
// the error nodes identify specific problematic children, and pseudoTypeToNodeWithCheckerFallback
// will report errors on them and fall back to the checker's real type.
if len(t.AsPseudoTypeInferred().ErrorNodes) > 0 {
return true
}
if reportErrors {
b.ctx.tracker.ReportInferenceFallback(t.AsPseudoTypeInferred().Expression)
}
return false
case pseudochecker.PseudoTypeKindObjectLiteral:
pt := t.AsPseudoTypeObjectLiteral()
if type_ == nil {
Expand Down Expand Up @@ -366,7 +410,7 @@ func (b *NodeBuilderImpl) pseudoTypeEquivalentToType(t *pseudochecker.PseudoType
case pseudochecker.PseudoObjectElementKindPropertyAssignment:
d := e.AsPseudoPropertyAssignment()
if !b.pseudoTypeEquivalentToType(d.Type, propType, e.Optional, false) {
if reportErrors {
if reportErrors && !isStructuralPseudoType(d.Type) {
b.ctx.tracker.ReportInferenceFallback(e.Name.Parent)
}
return false
Expand Down Expand Up @@ -501,16 +545,22 @@ func (b *NodeBuilderImpl) pseudoTypeEquivalentToType(t *pseudochecker.PseudoType
b.ctx.tracker.ReportInferenceFallback(t.AsPseudoTypeNoResult().Declaration)
}
return false
case pseudochecker.PseudoTypeKindInferred:
if reportErrors {
b.ctx.tracker.ReportInferenceFallback(t.AsPseudoTypeInferred().Expression)
}
return false
default:
return false
}
}

func isStructuralPseudoType(t *pseudochecker.PseudoType) bool {
switch t.Kind {
case pseudochecker.PseudoTypeKindObjectLiteral, pseudochecker.PseudoTypeKindTuple, pseudochecker.PseudoTypeKindSingleCallSignature:
return true
case pseudochecker.PseudoTypeKindMaybeConstLocation:
d := t.AsPseudoTypeMaybeConstLocation()
return isStructuralPseudoType(d.ConstType) || isStructuralPseudoType(d.RegularType)
}
return false
}

// pseudoReturnTypeMatchesPredicate checks if a pseudo return type (which should be a Direct type
// wrapping a TypePredicateNode) matches the given type predicate from the checker.
func (b *NodeBuilderImpl) pseudoReturnTypeMatchesPredicate(rt *pseudochecker.PseudoType, predicate *TypePredicate) bool {
Expand Down
61 changes: 34 additions & 27 deletions internal/pseudochecker/lookup.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@ func (ch *PseudoChecker) typeFromPropertyAssignment(node *ast.Node) *PseudoType
init := node.Initializer()
if init != nil {
expr := ch.typeFromExpression(init)
if expr != nil && expr.Kind != PseudoTypeKindInferred {
if expr != nil && (expr.Kind != PseudoTypeKindInferred || len(expr.AsPseudoTypeInferred().ErrorNodes) > 0) {
return expr
}
// fallback to NoResult if PseudoTypeKindInferred
// fallback to NoResult if PseudoTypeKindInferred without error nodes
}
}
return NewPseudoTypeNoResult(node)
Expand Down Expand Up @@ -118,14 +118,14 @@ func (ch *PseudoChecker) typeFromProperty(node *ast.Node) *PseudoType {
return NewPseudoTypeNoResult(node)
}
expr := ch.typeFromExpression(init)
if expr != nil && expr.Kind != PseudoTypeKindInferred {
if expr != nil && (expr.Kind != PseudoTypeKindInferred || len(expr.AsPseudoTypeInferred().ErrorNodes) > 0) {
if expr.Kind != PseudoTypeKindDirect && node.AsPropertyDeclaration().PostfixToken != nil && node.AsPropertyDeclaration().PostfixToken.Kind == ast.KindQuestionToken {
// type comes from the initializer expression on a property with a `?` - add `| undefined` to the type
return addUndefinedIfDefinitelyRequired(expr)
}
return expr
}
// fallback to NoResult if PseudoTypeKindInferred
// fallback to NoResult if PseudoTypeKindInferred without error nodes
}
}
return NewPseudoTypeNoResult(node)
Expand All @@ -144,10 +144,10 @@ func (ch *PseudoChecker) typeFromVariable(declaration *ast.VariableDeclaration)
return NewPseudoTypeNoResult(declaration.AsNode())
}
expr := ch.typeFromExpression(init)
if expr != nil && expr.Kind != PseudoTypeKindInferred {
if expr != nil && (expr.Kind != PseudoTypeKindInferred || len(expr.AsPseudoTypeInferred().ErrorNodes) > 0) {
return expr
}
// fallback to NoResult if PseudoTypeKindInferred
// fallback to NoResult if PseudoTypeKindInferred without error nodes
}
}
return NewPseudoTypeNoResult(declaration.AsNode())
Expand Down Expand Up @@ -322,8 +322,8 @@ func (ch *PseudoChecker) typeFromExpression(node *ast.Node) *PseudoType {
}

func (ch *PseudoChecker) typeFromObjectLiteral(node *ast.ObjectLiteralExpression) *PseudoType {
if !ch.canGetTypeFromObjectLiteral(node) {
return NewPseudoTypeInferred(node.AsNode())
if errorNodes := ch.canGetTypeFromObjectLiteral(node); errorNodes != nil {
return NewPseudoTypeInferredWithErrors(node.AsNode(), errorNodes)
}
// we are in a const context producing an object literal type, there are no shorthand or spread assignments
if node.Properties == nil || len(node.Properties.Nodes) == 0 {
Expand Down Expand Up @@ -409,41 +409,44 @@ func (ch *PseudoChecker) getAccessorMember(accessor *ast.Node, name *ast.Node) *
return nil
}

func (ch *PseudoChecker) canGetTypeFromObjectLiteral(node *ast.ObjectLiteralExpression) bool {
// canGetTypeFromObjectLiteral checks whether an object literal can be typed by the pseudochecker.
// Returns nil if the object can be typed, or a slice of error nodes (shorthand/spread properties,
// non-literal computed names) that prevent typing.
func (ch *PseudoChecker) canGetTypeFromObjectLiteral(node *ast.ObjectLiteralExpression) []*ast.Node {
if node.Properties == nil || len(node.Properties.Nodes) == 0 {
return true // empty object
return nil // empty object, ok
}
// !!! TODO: strada reports errors on multiple non-inferrable props
// via calling reportInferenceFallback multiple times here before returning.
// Does that logic need to be included in this checker? Or can it
// be kept to the `PseudoType` -> `Node` mapping logic, so this
// checker can avoid needing any error reporting logic?
var errorNodes []*ast.Node
for _, e := range node.Properties.Nodes {
if e.Flags&ast.NodeFlagsThisNodeHasError != 0 {
return false
errorNodes = append(errorNodes, e)
continue
}
if e.Kind == ast.KindShorthandPropertyAssignment || e.Kind == ast.KindSpreadAssignment {
return false
errorNodes = append(errorNodes, e)
continue
}
if e.Name().Flags&ast.NodeFlagsThisNodeHasError != 0 {
return false
errorNodes = append(errorNodes, e.Name())
continue
}
if e.Name().Kind == ast.KindPrivateIdentifier {
return false
errorNodes = append(errorNodes, e)
continue
}
if e.Name().Kind == ast.KindComputedPropertyName {
expression := e.Name().Expression()
if !ast.IsPrimitiveLiteralValue(expression, false) {
return false
errorNodes = append(errorNodes, e.Name())
}
}
}
return true
return errorNodes
}

func (ch *PseudoChecker) typeFromArrayLiteral(node *ast.ArrayLiteralExpression) *PseudoType {
if !ch.canGetTypeFromArrayLiteral(node) {
return NewPseudoTypeInferred(node.AsNode())
if errorNodes := ch.canGetTypeFromArrayLiteral(node); errorNodes != nil {
return NewPseudoTypeInferredWithErrors(node.AsNode(), errorNodes)
}
if IsInConstContext(node.AsNode()) && isContextuallyTyped(node.AsNode()) {
return NewPseudoTypeInferred(node.AsNode()) // expr in an as const cast with a contextual type has variable readonly state, bail
Expand All @@ -456,16 +459,20 @@ func (ch *PseudoChecker) typeFromArrayLiteral(node *ast.ArrayLiteralExpression)
return NewPseudoTypeTuple(results)
}

func (ch *PseudoChecker) canGetTypeFromArrayLiteral(node *ast.ArrayLiteralExpression) bool {
// canGetTypeFromArrayLiteral checks whether an array literal can be typed by the pseudochecker.
// Returns nil if the array can be typed, or a slice of error nodes that prevent typing.
// For non-const arrays, the error node is the array expression itself.
// For const arrays with spreads, the error node is the spread element.
func (ch *PseudoChecker) canGetTypeFromArrayLiteral(node *ast.ArrayLiteralExpression) []*ast.Node {
if !IsInConstContext(node.AsNode()) {
return false
return []*ast.Node{node.AsNode()}
}
for _, e := range node.Elements.Nodes {
if e.Kind == ast.KindSpreadElement {
return false
return []*ast.Node{e}
}
}
return true
return nil
}

// See `isConstContext` in `checker.go` - this is basically any node kind mentioned in that
Expand Down
9 changes: 8 additions & 1 deletion internal/pseudochecker/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,18 +94,25 @@ func (t *PseudoType) AsPseudoTypeDirect() *PseudoTypeDirect { return t.data.(*Ps
// PseudoTypeInferred directly encodes the type referred to by a given Expression
// These represent cases where the expression was too complex for the pseudochecker.
// Most of the time, these locations will produce an error under ID.
// Specific error nodes (shorthand properties, spread assignments, etc.) are stored on the
// ErrorNodes field, collected during pseudochecker construction.
type PseudoTypeInferred struct {
PseudoTypeBase
Expression *ast.Node
ErrorNodes []*ast.Node
}

func NewPseudoTypeInferred(expr *ast.Node) *PseudoType {
return newPseudoType(PseudoTypeKindInferred, &PseudoTypeInferred{Expression: expr})
}

func NewPseudoTypeInferredWithErrors(expr *ast.Node, errorNodes []*ast.Node) *PseudoType {
return newPseudoType(PseudoTypeKindInferred, &PseudoTypeInferred{Expression: expr, ErrorNodes: errorNodes})
}

func (t *PseudoType) AsPseudoTypeInferred() *PseudoTypeInferred { return t.data.(*PseudoTypeInferred) }

// PseudoTypeNoResult is anlogous to PseudoTypeInferred in that it references a case
// PseudoTypeNoResult is analogous to PseudoTypeInferred in that it references a case
// where the type was too complex for the pseudochecker. Rather than an expression, however,
// it is referring to the return type of a signature or declaration.
type PseudoTypeNoResult struct {
Expand Down
Loading
Loading