Files
2026-07-09 16:50:43 -04:00

730 lines
29 KiB
Go

package pseudochecker
import (
"slices"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/debug"
)
func (ch *PseudoChecker) GetReturnTypeOfSignature(signatureNode *ast.Node) *PseudoType {
switch signatureNode.Kind {
case ast.KindGetAccessor:
return ch.GetTypeOfAccessor(signatureNode)
case ast.KindMethodDeclaration, ast.KindFunctionDeclaration, ast.KindConstructor,
ast.KindMethodSignature, ast.KindCallSignature, ast.KindConstructSignature,
ast.KindSetAccessor, ast.KindIndexSignature, ast.KindFunctionType, ast.KindConstructorType,
ast.KindFunctionExpression, ast.KindArrowFunction, ast.KindJSDocSignature:
return ch.createReturnFromSignature(signatureNode)
default:
debug.FailBadSyntaxKind(signatureNode, "Node needs to be an inferrable node")
return nil
}
}
func (ch *PseudoChecker) GetTypeOfAccessor(accessor *ast.Node) *PseudoType {
return ch.typeFromAccessor(accessor)
}
func (ch *PseudoChecker) GetTypeOfExpression(node *ast.Node) *PseudoType {
return ch.typeFromExpression(node)
}
func (ch *PseudoChecker) GetTypeOfDeclaration(node *ast.Node) *PseudoType {
switch node.Kind {
case ast.KindParameter:
return ch.typeFromParameter(node.AsParameterDeclaration())
case ast.KindVariableDeclaration:
return ch.typeFromVariable(node.AsVariableDeclaration())
case ast.KindPropertySignature, ast.KindPropertyDeclaration, ast.KindJSDocPropertyTag:
return ch.typeFromProperty(node)
case ast.KindBindingElement:
return NewPseudoTypeNoResult(node)
case ast.KindExportAssignment:
return ch.typeFromExpression(node.AsExportAssignment().Expression)
case ast.KindPropertyAccessExpression, ast.KindElementAccessExpression, ast.KindBinaryExpression:
return ch.typeFromExpandoProperty(node)
case ast.KindPropertyAssignment, ast.KindShorthandPropertyAssignment:
return ch.typeFromPropertyAssignment(node)
case ast.KindCallExpression:
switch ast.GetAssignmentDeclarationKind(node) {
// TODO: How much of the checker's getTypeFromPropertyDescriptor is worth trying to emulate over ASTs?
case ast.JSDeclarationKindObjectDefinePropertyValue:
{
// !!!
}
case ast.JSDeclarationKindObjectDefinePropertyExports:
{
// !!!
}
}
return NewPseudoTypeNoResult(node)
default:
debug.FailBadSyntaxKind(node, "node needs to be an inferrable node")
return nil
}
}
func (ch *PseudoChecker) typeFromPropertyAssignment(node *ast.Node) *PseudoType {
annotation := node.Type()
if annotation != nil {
return NewPseudoTypeDirect(annotation)
}
if node.Kind == ast.KindPropertyAssignment {
init := node.Initializer()
if init != nil {
expr := ch.typeFromExpression(init)
if expr != nil && (expr.Kind != PseudoTypeKindInferred || len(expr.AsPseudoTypeInferred().ErrorNodes) > 0) {
return expr
}
// fallback to NoResult if PseudoTypeKindInferred without error nodes
}
}
return NewPseudoTypeNoResult(node)
}
// This is _not_ redundant with the reparser; see how expandoFunctionSymbolProperty.ts and similar behaves
func (ch *PseudoChecker) typeFromExpandoProperty(node *ast.Node) *PseudoType {
declaredType := node.Type()
if declaredType != nil {
return NewPseudoTypeDirect(declaredType)
}
// While `node` is an expression, as an expando, it should also always be a
// declaration with a `.Symbol()` which requires declaration fallback handling
return NewPseudoTypeNoResult(node)
}
func (ch *PseudoChecker) typeFromProperty(node *ast.Node) *PseudoType {
t := node.Type()
if t != nil {
return NewPseudoTypeDirect(t)
}
if ast.IsPropertyDeclaration(node) {
init := node.Initializer()
if init != nil && !isContextuallyTyped(node) {
// explicit fail on readonly template literals to allow for literal freshness in the future
if ast.HasModifier(node, ast.ModifierFlagsReadonly) && ast.IsTemplateExpression(init) {
return NewPseudoTypeNoResult(node)
}
expr := ch.typeFromExpression(init)
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 without error nodes
}
}
return NewPseudoTypeNoResult(node)
}
func (ch *PseudoChecker) typeFromVariable(declaration *ast.VariableDeclaration) *PseudoType {
t := declaration.Type
if t != nil {
return NewPseudoTypeDirect(t)
}
init := declaration.Initializer
if init != nil && (len(declaration.Symbol.Declarations) == 1 || core.CountWhere(declaration.Symbol.Declarations, ast.IsVariableDeclaration) == 1) {
if !isContextuallyTyped(declaration.AsNode()) { // TODO: also should bail on expando declarations; reuse syntactic expando check used in declaration emit
// TODO: Strada forces an inference fallback on `const` variables with template expression initializers, to leave space for template literal freshness in the future
if ast.IsVarConst(declaration.AsNode()) && ast.IsTemplateExpression(init) {
return NewPseudoTypeNoResult(declaration.AsNode())
}
expr := ch.typeFromExpression(init)
if expr != nil && (expr.Kind != PseudoTypeKindInferred || len(expr.AsPseudoTypeInferred().ErrorNodes) > 0) {
return expr
}
// fallback to NoResult if PseudoTypeKindInferred without error nodes
}
}
return NewPseudoTypeNoResult(declaration.AsNode())
}
func (ch *PseudoChecker) typeFromAccessor(accessor *ast.Node) *PseudoType {
accessorDeclarations := ast.GetAllAccessorDeclarationsForDeclaration(accessor, accessor.DeclarationData().Symbol.Declarations)
accessorType := ch.getTypeAnnotationFromAllAccessorDeclarations(accessor, accessorDeclarations)
if accessorType != nil && !ast.IsTypePredicateNode(accessorType) {
return NewPseudoTypeDirect(accessorType)
}
if accessorDeclarations.GetAccessor != nil {
res := ch.createReturnFromSignature(accessorDeclarations.GetAccessor.AsNode())
if res.Kind == PseudoTypeKindInferred && len(res.AsPseudoTypeInferred().ErrorNodes) == 0 {
errorNodes := []*ast.Node{accessorDeclarations.GetAccessor.AsNode()}
if accessorDeclarations.SetAccessor != nil {
errorNodes = append(errorNodes, accessorDeclarations.SetAccessor.AsNode())
}
res = NewPseudoTypeInferredWithErrors(res.AsPseudoTypeInferred().Expression, res.AsPseudoTypeInferred().IsSignatureReturn, errorNodes) // Move error up to the accessor
}
return res
}
return NewPseudoTypeNoResult(accessor)
}
func (ch *PseudoChecker) getTypeAnnotationFromAllAccessorDeclarations(node *ast.Node, accessors ast.AllAccessorDeclarations) *ast.Node {
accessorType := ch.getTypeAnnotationFromAccessor(node)
if accessorType == nil && node != accessors.FirstAccessor {
accessorType = ch.getTypeAnnotationFromAccessor(accessors.FirstAccessor)
}
if accessorType == nil && accessors.SecondAccessor != nil && node != accessors.SecondAccessor {
accessorType = ch.getTypeAnnotationFromAccessor(accessors.SecondAccessor)
}
return accessorType
}
func (ch *PseudoChecker) getTypeAnnotationFromAccessor(node *ast.Node) *ast.Node {
if node == nil {
return nil
}
// !!! TODO: support ripping return type off of .FullSignature
if node.Kind == ast.KindGetAccessor {
return node.AsGetAccessorDeclaration().Type
}
set := node.AsSetAccessorDeclaration()
if set.Parameters == nil || len(set.Parameters.Nodes) < 1 {
return nil
}
p := set.Parameters.Nodes[0]
if !ast.IsParameterDeclaration(p) {
return nil
}
return p.AsParameterDeclaration().Type
}
func isValueSignatureDeclaration(node *ast.Node) bool {
return ast.IsFunctionExpression(node) || ast.IsArrowFunction(node) || ast.IsMethodDeclaration(node) || ast.IsAccessor(node) || ast.IsFunctionDeclaration(node) || ast.IsConstructorDeclaration(node)
}
// does not return `nil`, returns a `NoResult` pseudotype instead
func (ch *PseudoChecker) createReturnFromSignature(fn *ast.Node) *PseudoType {
if ast.IsFunctionLike(fn) {
d := fn.FunctionLikeData()
// !!! TODO: support ripping return type off of .FullSignature
r := d.Type
if r != nil {
return NewPseudoTypeDirect(r)
}
}
if isValueSignatureDeclaration(fn) {
return ch.typeFromSingleReturnExpression(fn)
}
return NewPseudoTypeNoResult(fn)
}
func (ch *PseudoChecker) typeFromSingleReturnExpression(fn *ast.Node) *PseudoType {
var candidateExpr *ast.Node
if fn != nil && !ast.NodeIsMissing(fn.Body()) {
flags := ast.GetFunctionFlags(fn)
if flags&ast.FunctionFlagsAsyncGenerator != 0 {
return NewPseudoTypeInferred(fn, true)
}
body := fn.Body()
if ast.IsBlock(body) {
ast.ForEachReturnStatement(body, func(stmt *ast.Node) bool {
if stmt.Parent != body { // Why bail on nested return statements?
candidateExpr = nil
return true
}
if candidateExpr == nil {
candidateExpr = stmt.AsReturnStatement().Expression
} else {
candidateExpr = nil
return true
}
return false
})
} else {
candidateExpr = body
}
}
if candidateExpr != nil {
if isContextuallyTyped(candidateExpr) {
var t *ast.Node
if candidateExpr.Kind == ast.KindTypeAssertionExpression {
t = candidateExpr.AsTypeAssertion().Type
} else if candidateExpr.Kind == ast.KindAsExpression {
t = candidateExpr.AsAsExpression().Type
}
if t != nil && !ast.IsConstTypeReference(t) {
return NewPseudoTypeDirect(t)
}
} else {
return ch.typeFromExpression(candidateExpr)
}
}
return NewPseudoTypeInferred(fn, true)
}
// This is basically `checkExpression` for pseudotypes
func (ch *PseudoChecker) typeFromExpression(node *ast.Node) *PseudoType {
switch node.Kind {
case ast.KindOmittedExpression:
return PseudoTypeUndefined
case ast.KindParenthesizedExpression:
// assertions transformed on reparse, just unwrap
return ch.typeFromExpression(node.AsParenthesizedExpression().Expression)
case ast.KindIdentifier:
// !!! TODO: in strada, this uses symbol information to ensure `node` refers to the global `undefined` symbol instead
// we should probably import `resolveName` and use it here to check for the same; but we have to setup some barebones pseudoglobals for that to work!
if node.AsIdentifier().Text == "undefined" {
return PseudoTypeUndefined
}
case ast.KindNullKeyword:
return PseudoTypeNull
case ast.KindArrowFunction, ast.KindFunctionExpression:
return ch.typeFromFunctionLikeExpression(node)
case ast.KindTypeAssertionExpression:
return ch.typeFromTypeAssertion(node.AsTypeAssertion().Expression, node.AsTypeAssertion().Type)
case ast.KindAsExpression:
return ch.typeFromTypeAssertion(node.AsAsExpression().Expression, node.AsAsExpression().Type)
case ast.KindPrefixUnaryExpression:
if ast.IsPrimitiveLiteralValue(node, true) {
return ch.typeFromPrimitiveLiteralPrefix(node.AsPrefixUnaryExpression())
}
case ast.KindArrayLiteralExpression:
return ch.typeFromArrayLiteral(node.AsArrayLiteralExpression())
case ast.KindObjectLiteralExpression:
return ch.typeFromObjectLiteral(node.AsObjectLiteralExpression())
case ast.KindClassExpression:
return NewPseudoTypeInferredWithErrors(node, false, []*ast.Node{node}) // No possible annotation/directly mappable syntax
case ast.KindTemplateExpression:
// templateLitWithHoles as const, not supported
if IsInConstContext(node) {
return NewPseudoTypeInferred(node, false)
}
return NewPseudoTypeMaybeConstLocation(node, NewPseudoTypeInferred(node, false), PseudoTypeString)
case ast.KindNumericLiteral:
return NewPseudoTypeMaybeConstLocation(node, NewPseudoTypeNumericLiteral(node), PseudoTypeNumber)
case ast.KindNoSubstitutionTemplateLiteral:
return NewPseudoTypeMaybeConstLocation(node, NewPseudoTypeStringLiteral(node), PseudoTypeString)
case ast.KindStringLiteral:
return NewPseudoTypeMaybeConstLocation(node, NewPseudoTypeStringLiteral(node), PseudoTypeString)
case ast.KindBigIntLiteral:
return NewPseudoTypeMaybeConstLocation(node, NewPseudoTypeBigIntLiteral(node), PseudoTypeBigInt)
case ast.KindTrueKeyword:
return NewPseudoTypeMaybeConstLocation(node, PseudoTypeTrue, PseudoTypeBoolean)
case ast.KindFalseKeyword:
return NewPseudoTypeMaybeConstLocation(node, PseudoTypeFalse, PseudoTypeBoolean)
}
return NewPseudoTypeInferred(node, false)
}
func (ch *PseudoChecker) typeFromObjectLiteral(node *ast.ObjectLiteralExpression) *PseudoType {
if errorNodes := ch.canGetTypeFromObjectLiteral(node); errorNodes != nil {
return NewPseudoTypeInferredWithErrors(node.AsNode(), false, 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 {
return NewPseudoTypeObjectLiteral(nil)
}
results := make([]*PseudoObjectElement, 0, len(node.Properties.Nodes))
for _, e := range node.Properties.Nodes {
switch e.Kind {
case ast.KindMethodDeclaration:
optional := e.AsMethodDeclaration().PostfixToken != nil && e.AsMethodDeclaration().PostfixToken.Kind == ast.KindQuestionToken
if e.FunctionLikeData().FullSignature != nil {
results = append(results, NewPseudoPropertyAssignment(
false,
e.Name(),
optional,
NewPseudoTypeDirect(e.FunctionLikeData().FullSignature),
))
} else {
results = append(results, NewPseudoObjectMethod(
e,
e.Name(),
optional,
ch.cloneTypeParameters(e.AsMethodDeclaration().TypeParameters),
ch.cloneParameters(e.ParameterList()),
ch.createReturnFromSignature(e),
))
}
case ast.KindPropertyAssignment:
results = append(results, NewPseudoPropertyAssignment(
false,
e.Name(),
e.AsPropertyAssignment().PostfixToken != nil && e.AsPropertyAssignment().PostfixToken.Kind == ast.KindQuestionToken,
ch.typeFromExpression(e.Initializer()),
))
case ast.KindSetAccessor, ast.KindGetAccessor:
member := ch.getAccessorMember(e, e.Name())
if member != nil {
results = append(results, member)
}
}
}
return NewPseudoTypeObjectLiteral(results)
}
// roughly analogous to typeFromObjectLiteralAccessor in strada
func (ch *PseudoChecker) getAccessorMember(accessor *ast.Node, name *ast.Node) *PseudoObjectElement {
allAccessors := ast.GetAllAccessorDeclarationsForDeclaration(accessor, accessor.Symbol().Declarations) // TODO: node preservation for late-bound accessor pairs?
// TODO: handle pseudo-annotations from get accessor return positions?
if allAccessors.GetAccessor != nil && allAccessors.GetAccessor.Type != nil &&
allAccessors.SetAccessor != nil && len(allAccessors.SetAccessor.Parameters.Nodes) > 0 && allAccessors.SetAccessor.Parameters.Nodes[0].AsParameterDeclaration().Type != nil {
// We have possible types for both accessors, we can't know if they are the same type so we keep both accessors
if ast.IsGetAccessorDeclaration(accessor) {
return NewPseudoGetAccessor(
accessor,
name,
false,
ch.typeFromAccessor(accessor),
)
} else {
return NewPseudoSetAccessor(
accessor,
name,
false,
ch.cloneParameters(accessor.AsSetAccessorDeclaration().Parameters)[0],
)
}
}
if accessor == allAccessors.FirstAccessor {
// only one annotated accessor; output a property - `readonly` for a single `get` accessor
accessorType := ch.typeFromAccessor(accessor)
readonly := ast.IsGetAccessorDeclaration(accessor) && allAccessors.SecondAccessor == nil
return NewPseudoPropertyAssignment(
readonly,
name,
false,
accessorType,
)
}
return nil
}
// 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 nil // empty object, ok
}
var errorNodes []*ast.Node
for _, e := range node.Properties.Nodes {
if e.Flags&ast.NodeFlagsThisNodeHasError != 0 {
errorNodes = append(errorNodes, e)
continue
}
if e.Kind == ast.KindShorthandPropertyAssignment || e.Kind == ast.KindSpreadAssignment {
errorNodes = append(errorNodes, e)
continue
}
if e.Name().Flags&ast.NodeFlagsThisNodeHasError != 0 {
errorNodes = append(errorNodes, e.Name())
continue
}
if e.Name().Kind == ast.KindPrivateIdentifier {
errorNodes = append(errorNodes, e)
continue
}
if e.Name().Kind == ast.KindComputedPropertyName {
expression := e.Name().Expression()
if !ast.IsPrimitiveLiteralValue(expression, false) {
errorNodes = append(errorNodes, e.Name())
}
}
}
return errorNodes
}
func (ch *PseudoChecker) typeFromArrayLiteral(node *ast.ArrayLiteralExpression) *PseudoType {
if errorNodes := ch.canGetTypeFromArrayLiteral(node); errorNodes != nil {
return NewPseudoTypeInferredWithErrors(node.AsNode(), false, errorNodes)
}
if IsInConstContext(node.AsNode()) && isContextuallyTyped(node.AsNode()) {
return NewPseudoTypeInferred(node.AsNode(), false) // expr in an as const cast with a contextual type has variable readonly state, bail
}
// we are in a const context producing a tuple type, there are no spread elements
results := make([]*PseudoType, 0, len(node.Elements.Nodes))
for _, e := range node.Elements.Nodes {
results = append(results, ch.typeFromExpression(e))
}
return NewPseudoTypeTuple(results)
}
// 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 []*ast.Node{node.AsNode()}
}
for _, e := range node.Elements.Nodes {
if e.Kind == ast.KindSpreadElement {
return []*ast.Node{e}
}
}
return nil
}
// See `isConstContext` in `checker.go` - this is basically any node kind mentioned in that
func isConstContextPropagatingKind(kind ast.Kind) bool {
switch kind {
case ast.KindArrayLiteralExpression, ast.KindObjectLiteralExpression,
ast.KindParenthesizedExpression, ast.KindSpreadElement, ast.KindPropertyAssignment,
ast.KindShorthandPropertyAssignment, ast.KindTemplateSpan, ast.KindPrefixUnaryExpression:
return true
}
return false
}
// IsInConstContext traverses up the parent chain to determine if the node is within a const context without needing any
// persistent traversal scope tracking (which could be unreliable in the presence of `typeof` queries anyway!)
func IsInConstContext(node *ast.Node) bool {
// An expression is in a const context if an ancestor is a const type maybeAssertion expression
maybeAssertion := ast.FindAncestor(
node.Parent,
func(n *ast.Node) bool {
// stop traversing at assertions or anything not an array/object literal, since only those create or transfer const-ness
return ast.IsAssertionExpression(n) || !isConstContextPropagatingKind(n.Kind)
},
)
return ast.IsConstAssertion(maybeAssertion)
}
func (ch *PseudoChecker) typeFromPrimitiveLiteralPrefix(node *ast.PrefixUnaryExpression) *PseudoType {
expr := node.AsNode()
if node.Operator == ast.KindPlusToken {
expr = node.Operand
}
inner := node.Operand
if inner.Kind == ast.KindBigIntLiteral {
return NewPseudoTypeMaybeConstLocation(node.AsNode(), NewPseudoTypeBigIntLiteral(expr.AsNode()), PseudoTypeBigInt)
}
if inner.Kind == ast.KindNumericLiteral {
return NewPseudoTypeMaybeConstLocation(node.AsNode(), NewPseudoTypeNumericLiteral(expr.AsNode()), PseudoTypeNumber)
}
debug.FailBadSyntaxKind(inner)
return nil
}
func (ch *PseudoChecker) typeFromTypeAssertion(expression *ast.Node, typeNode *ast.Node) *PseudoType {
if ast.IsConstTypeReference(typeNode) {
return ch.typeFromExpression(expression)
}
return NewPseudoTypeDirect(typeNode)
}
func (ch *PseudoChecker) typeFromFunctionLikeExpression(node *ast.Node) *PseudoType {
if node.FunctionLikeData().FullSignature != nil {
return NewPseudoTypeDirect(node.FunctionLikeData().FullSignature)
}
returnType := ch.createReturnFromSignature(node)
typeParameters := ch.cloneTypeParameters(node.FunctionLikeData().TypeParameters)
parameters := ch.cloneParameters(node.FunctionLikeData().Parameters)
return NewPseudoTypeSingleCallSignature(
node,
parameters,
typeParameters,
returnType,
)
}
func (ch *PseudoChecker) cloneTypeParameters(nodes *ast.NodeList) []*ast.TypeParameterDeclaration {
if nodes == nil {
return nil
}
if len(nodes.Nodes) == 0 {
return nil
}
result := make([]*ast.TypeParameterDeclaration, 0, len(nodes.Nodes))
for _, e := range nodes.Nodes {
result = append(result, e.AsTypeParameterDeclaration())
}
return result
}
func isUndefinedPseudoType(t *PseudoType) bool {
return t.Kind == PseudoTypeKindUndefined || (t.Kind == PseudoTypeKindMaybeConstLocation && isUndefinedPseudoType(t.AsPseudoTypeMaybeConstLocation().ConstType))
}
func typeNodeCouldReferToUndefined(node *ast.Node) bool {
for node.Kind == ast.KindParenthesizedType {
node = node.AsParenthesizedTypeNode().Type
}
switch node.Kind {
// these types require symbolic/type resolution to know if they definitely do or do not refer to `undefined`, so might (or definitely do)
case ast.KindTypeReference, ast.KindIndexedAccessType, ast.KindTypeQuery, ast.KindOptionalType, ast.KindRestType, ast.KindImportType:
return true
case ast.KindIntersectionType:
// TODO: why is this not `core.Every`? strada treated unions and intersections the same, but logically every intersection member needs to contain a possible `undefined`
// for the result type to contain `undefined`. Likely a bug persisting from strada.
return core.Some(node.AsIntersectionTypeNode().Types.Nodes, typeNodeCouldReferToUndefined)
case ast.KindUnionType:
return core.Some(node.AsUnionTypeNode().Types.Nodes, typeNodeCouldReferToUndefined)
case ast.KindConditionalType: // suspect - should be treated as a union of both branches instead, likely a bug persisted from strada
return true
case ast.KindTypeOperator: // suspect - always refers to a subset of `string | number | symbol` for `keyof` or `symbol` for `unique`
return true
case ast.KindTypePredicate: // suspect - always refers to `never` or `boolean`, depending on kind - considered possibly-`undefined` referencing for strada compat
return true
case ast.KindUndefinedKeyword:
return true
default: // all other keywords, literal types, function-y types, array/tuple types, type literals, template types, this types
return false
}
}
// see this as the inverse of `canAddUndefined` in `expressionToTypeNode` in strada
func CouldAlreadyReferToUndefinedType(t *PseudoType) bool {
if t.Kind == PseudoTypeKindNoResult || t.Kind == PseudoTypeKindInferred || isUndefinedPseudoType(t) {
return true
}
if t.Kind == PseudoTypeKindMaybeConstLocation {
mc := t.AsPseudoTypeMaybeConstLocation()
return CouldAlreadyReferToUndefinedType(mc.RegularType) // if we're even asking this question, it's not a `const` location
}
if t.Kind == PseudoTypeKindDirect {
// inspect the direct type node
node := t.AsPseudoTypeDirect().TypeNode
return typeNodeCouldReferToUndefined(node)
}
if t.Kind == PseudoTypeKindUnion {
return core.Some(t.AsPseudoTypeUnion().Types, CouldAlreadyReferToUndefinedType)
}
return false
}
func isOptionalInitializedOrRestParameter(node *ast.ParameterDeclarationNode) bool {
p := node.AsParameterDeclaration()
if p.DotDotDotToken != nil || p.Initializer != nil || p.QuestionToken != nil {
return true
}
return false
}
// lastRequiredParamIndex returns the index just past the last required parameter
// in the list. A parameter is "required" if it has no question token, no initializer,
// and no rest token. This is computed in a single reverse pass so callers can
// determine "has required parameter after index i" with `i+1 < lastRequired`
// (equivalently, `i < lastRequired-1`) in O(1).
func lastRequiredParamIndex(params []*ast.Node) int {
for i := len(params) - 1; i >= 0; i-- {
if !isOptionalInitializedOrRestParameter(params[i]) {
return i + 1
}
}
return 0
}
func addUndefinedIfDefinitelyRequired(expr *PseudoType) *PseudoType {
// If `expr` doesn't already contain `| undefined` or a direct/inferred type that may contain `undefined`, add `| undefined`
// in Strada, this reached into the checker to see if `undefined` was necessary, using `isRequiredOptionalParameter` from the emit resolver,
// but that's not required on top of the syntactic checks to get the same behavior. (If we get the type wrong, it'll mismatch later and be discarded
// for an inference error since corsa actually validates that pseudotypes semantically match the inferred type the checker produces)
if CouldAlreadyReferToUndefinedType(expr) {
return expr // will just error later, more like than not, unless the `undefined` is explicit in the pseudo
}
// Explicitly add an `| undefined`
return NewPseudoTypeUnion([]*PseudoType{expr, PseudoTypeUndefined})
}
func (ch *PseudoChecker) typeFromParameter(node *ast.ParameterDeclaration) *PseudoType {
parent := node.Parent
if parent.Kind == ast.KindSetAccessor {
return ch.GetTypeOfAccessor(parent)
}
// Fast path: no initializer means we never need parameter position info.
if node.Initializer == nil {
if node.Type != nil {
return NewPseudoTypeDirect(node.Type)
}
return NewPseudoTypeNoResult(node.AsNode())
}
p := parent.Parameters()
selfIdx := slices.Index(p, node.AsNode())
lastRequired := lastRequiredParamIndex(p)
return ch.typeFromParameterWorker(node, selfIdx, lastRequired)
}
func (ch *PseudoChecker) typeFromParameterWorker(node *ast.ParameterDeclaration, selfIdx int, lastRequired int) *PseudoType {
parent := node.Parent
if parent.Kind == ast.KindSetAccessor {
return ch.GetTypeOfAccessor(parent)
}
hasRequiredAfter := selfIdx < lastRequired-1
declaredType := node.Type
if declaredType != nil {
result := NewPseudoTypeDirect(declaredType)
// When the parameter has an initializer and strict null checks are enabled,
// check if `| undefined` needs to be added because there are required parameters after this one.
// This mirrors the checker's getTypeOfParameter which adds optionality for initialized parameters.
if ch.strictNullChecks && node.Initializer != nil && hasRequiredAfter {
return addUndefinedIfDefinitelyRequired(result)
}
return result
}
if node.Initializer != nil && ast.IsIdentifier(node.Name()) && !isContextuallyTyped(node.AsNode()) {
expr := ch.typeFromExpression(node.Initializer)
if expr != nil && (expr.Kind == PseudoTypeKindInferred && len(expr.AsPseudoTypeInferred().ErrorNodes) == 0) {
expr = NewPseudoTypeInferredWithErrors(expr.AsPseudoTypeInferred().Expression, false, []*ast.Node{node.AsNode()}) // Move error up to the parameter
}
if !ch.strictNullChecks {
return expr
}
if !hasRequiredAfter {
return expr
}
// if there is a non-optional parameter after this one, a `| undefined` will need to explicitly be emitted on this parameter, if it's not already there
return addUndefinedIfDefinitelyRequired(expr)
}
// TODO: In strada, the ID checker doesn't infer a parameter type from binding pattern names, but the real checker _does_!
// This means ID won't let you write, say, `({elem}) => false` without an annotation, even though it's trivially of type
// `(p0: {elem: any}) => boolean` and error-free under `noImplicitAny: false`!
// That limitation is retained here.
return NewPseudoTypeNoResult(node.AsNode())
}
func (ch *PseudoChecker) cloneParameters(nodes *ast.NodeList) []*PseudoParameter {
if nodes == nil {
return nil
}
if len(nodes.Nodes) == 0 {
return nil
}
lastRequired := lastRequiredParamIndex(nodes.Nodes)
result := make([]*PseudoParameter, 0, len(nodes.Nodes))
for i, e := range nodes.Nodes {
p := e.AsParameterDeclaration()
optional := p.QuestionToken != nil
if !optional && p.Initializer != nil {
// A parameter with an initializer is optional only if all subsequent
// parameters are also optional/have initializers/are rest parameters.
// This matches the checker's isOptionalParameter semantics.
optional = i >= lastRequired-1
}
result = append(result, NewPseudoParameter(
p.DotDotDotToken != nil,
e.Name(),
optional,
ch.typeFromParameterWorker(p, i, lastRequired),
))
}
return result
}
func isContextuallyTyped(node *ast.Node) bool {
return ast.FindAncestor(node.Parent, func(n *ast.Node) bool {
// Functions calls or parent type annotations (but not the return type of a function expression) may impact the inferred type and local inference is unreliable
if ast.IsCallExpression(n) {
return true
}
if ast.IsSatisfiesExpression(n) {
return true
}
if (ast.IsVariableParameterOrProperty(n) || ast.IsAssertionExpression(n)) && n.Type() != nil && !ast.IsConstAssertion(n) {
return true
}
return ast.IsJsxElement(n) || ast.IsJsxExpression(n)
}) != nil
}