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 }