730 lines
29 KiB
Go
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
|
|
}
|