2725 lines
110 KiB
Go
2725 lines
110 KiB
Go
package checker
|
|
|
|
import (
|
|
"math"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/binder"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/diagnostics"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/evaluator"
|
|
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner"
|
|
)
|
|
|
|
type FlowType struct {
|
|
t *Type
|
|
incomplete bool
|
|
}
|
|
|
|
func (ft *FlowType) isNil() bool {
|
|
return ft.t == nil
|
|
}
|
|
|
|
func (c *Checker) newFlowType(t *Type, incomplete bool) FlowType {
|
|
if incomplete && t.flags&TypeFlagsNever != 0 {
|
|
t = c.silentNeverType
|
|
}
|
|
return FlowType{t: t, incomplete: incomplete}
|
|
}
|
|
|
|
type SharedFlow struct {
|
|
flow *ast.FlowNode
|
|
flowType FlowType
|
|
}
|
|
|
|
type FlowState struct {
|
|
reference *ast.Node
|
|
declaredType *Type
|
|
initialType *Type
|
|
flowContainer *ast.Node
|
|
refKey string
|
|
depth int
|
|
sharedFlowStart int
|
|
reduceLabels []*ast.FlowReduceLabelData
|
|
next *FlowState
|
|
}
|
|
|
|
func (c *Checker) getFlowState() *FlowState {
|
|
f := c.freeFlowState
|
|
if f == nil {
|
|
f = &FlowState{}
|
|
}
|
|
c.freeFlowState = f.next
|
|
return f
|
|
}
|
|
|
|
func (c *Checker) putFlowState(f *FlowState) {
|
|
*f = FlowState{
|
|
reduceLabels: f.reduceLabels[:0],
|
|
next: c.freeFlowState,
|
|
}
|
|
c.freeFlowState = f
|
|
}
|
|
|
|
func getFlowNodeOfNode(node *ast.Node) *ast.FlowNode {
|
|
flowNodeData := node.FlowNodeData()
|
|
if flowNodeData != nil {
|
|
return flowNodeData.FlowNode
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Checker) getFlowTypeOfReference(reference *ast.Node, declaredType *Type) *Type {
|
|
return c.getFlowTypeOfReferenceEx(reference, declaredType, declaredType, nil, nil)
|
|
}
|
|
|
|
func (c *Checker) getFlowTypeOfReferenceEx(reference *ast.Node, declaredType *Type, initialType *Type, flowContainer *ast.Node, flowNode *ast.FlowNode) *Type {
|
|
if c.flowAnalysisDisabled {
|
|
return c.errorType
|
|
}
|
|
if flowNode == nil {
|
|
flowNode = getFlowNodeOfNode(reference)
|
|
if flowNode == nil {
|
|
return declaredType
|
|
}
|
|
}
|
|
f := c.getFlowState()
|
|
f.reference = reference
|
|
f.declaredType = declaredType
|
|
f.initialType = core.Coalesce(initialType, declaredType)
|
|
f.flowContainer = flowContainer
|
|
f.sharedFlowStart = len(c.sharedFlows)
|
|
c.flowInvocationCount++
|
|
evolvedType := c.getTypeAtFlowNode(f, flowNode).t
|
|
c.sharedFlows = c.sharedFlows[:f.sharedFlowStart]
|
|
c.putFlowState(f)
|
|
// When the reference is 'x' in an 'x.length', 'x.push(value)', 'x.unshift(value)' or x[n] = value' operation,
|
|
// we give type 'any[]' to 'x' instead of using the type determined by control flow analysis such that operations
|
|
// on empty arrays are possible without implicit any errors and new element types can be inferred without
|
|
// type mismatch errors.
|
|
var resultType *Type
|
|
if evolvedType.objectFlags&ObjectFlagsEvolvingArray != 0 && c.isEvolvingArrayOperationTarget(reference) {
|
|
resultType = c.autoArrayType
|
|
} else {
|
|
resultType = c.finalizeEvolvingArrayType(evolvedType)
|
|
}
|
|
if resultType == c.unreachableNeverType || reference.Parent != nil && ast.IsNonNullExpression(reference.Parent) && resultType.flags&TypeFlagsNever == 0 && c.getTypeWithFacts(resultType, TypeFactsNEUndefinedOrNull).flags&TypeFlagsNever != 0 {
|
|
return declaredType
|
|
}
|
|
return resultType
|
|
}
|
|
|
|
func (c *Checker) getTypeAtFlowNode(f *FlowState, flow *ast.FlowNode) FlowType {
|
|
if f.depth == 2000 {
|
|
// We have made 2000 recursive invocations. To avoid overflowing the call stack we report an error
|
|
// and disable further control flow analysis in the containing function or module body.
|
|
c.flowAnalysisDisabled = true
|
|
c.reportFlowControlError(f.reference)
|
|
return FlowType{t: c.errorType}
|
|
}
|
|
f.depth++
|
|
var sharedFlow *ast.FlowNode
|
|
for {
|
|
flags := flow.Flags
|
|
if flags&ast.FlowFlagsShared != 0 {
|
|
// We cache results of flow type resolution for shared nodes that were previously visited in
|
|
// the same getFlowTypeOfReference invocation. A node is considered shared when it is the
|
|
// antecedent of more than one node.
|
|
for i := f.sharedFlowStart; i < len(c.sharedFlows); i++ {
|
|
if c.sharedFlows[i].flow == flow {
|
|
f.depth--
|
|
return c.sharedFlows[i].flowType
|
|
}
|
|
}
|
|
sharedFlow = flow
|
|
}
|
|
var t FlowType
|
|
switch {
|
|
case flags&ast.FlowFlagsAssignment != 0:
|
|
t = c.getTypeAtFlowAssignment(f, flow)
|
|
if t.isNil() {
|
|
flow = flow.Antecedent
|
|
continue
|
|
}
|
|
case flags&ast.FlowFlagsCall != 0:
|
|
t = c.getTypeAtFlowCall(f, flow)
|
|
if t.isNil() {
|
|
flow = flow.Antecedent
|
|
continue
|
|
}
|
|
case flags&ast.FlowFlagsCondition != 0:
|
|
t = c.getTypeAtFlowCondition(f, flow)
|
|
case flags&ast.FlowFlagsSwitchClause != 0:
|
|
t = c.getTypeAtSwitchClause(f, flow)
|
|
case flags&ast.FlowFlagsBranchLabel != 0:
|
|
antecedents := getBranchLabelAntecedents(flow, f.reduceLabels)
|
|
if antecedents.Next == nil {
|
|
flow = antecedents.Flow
|
|
continue
|
|
}
|
|
t = c.getTypeAtFlowBranchLabel(f, flow, antecedents)
|
|
case flags&ast.FlowFlagsLoopLabel != 0:
|
|
if flow.Antecedents.Next == nil {
|
|
flow = flow.Antecedents.Flow
|
|
continue
|
|
}
|
|
t = c.getTypeAtFlowLoopLabel(f, flow)
|
|
case flags&ast.FlowFlagsArrayMutation != 0:
|
|
t = c.getTypeAtFlowArrayMutation(f, flow)
|
|
if t.isNil() {
|
|
flow = flow.Antecedent
|
|
continue
|
|
}
|
|
case flags&ast.FlowFlagsReduceLabel != 0:
|
|
f.reduceLabels = append(f.reduceLabels, flow.Node.AsFlowReduceLabelData())
|
|
t = c.getTypeAtFlowNode(f, flow.Antecedent)
|
|
f.reduceLabels = f.reduceLabels[:len(f.reduceLabels)-1]
|
|
case flags&ast.FlowFlagsStart != 0:
|
|
// Check if we should continue with the control flow of the containing function.
|
|
container := flow.Node
|
|
if container != nil && container != f.flowContainer && !ast.IsPropertyAccessExpression(f.reference) && !ast.IsElementAccessExpression(f.reference) && !(f.reference.Kind == ast.KindThisKeyword && !ast.IsArrowFunction(container)) {
|
|
flow = container.FlowNodeData().FlowNode
|
|
continue
|
|
}
|
|
// At the top of the flow we have the initial type.
|
|
t = FlowType{t: f.initialType}
|
|
default:
|
|
// Unreachable code errors are reported in the binding phase. Here we
|
|
// simply return the non-auto declared type to reduce follow-on errors.
|
|
t = FlowType{t: c.convertAutoToAny(f.declaredType)}
|
|
}
|
|
if sharedFlow != nil {
|
|
// Record visited node and the associated type in the cache.
|
|
c.sharedFlows = append(c.sharedFlows, SharedFlow{flow: sharedFlow, flowType: t})
|
|
}
|
|
f.depth--
|
|
return t
|
|
}
|
|
}
|
|
|
|
func getBranchLabelAntecedents(flow *ast.FlowNode, reduceLabels []*ast.FlowReduceLabelData) *ast.FlowList {
|
|
i := len(reduceLabels)
|
|
for i != 0 {
|
|
i--
|
|
data := reduceLabels[i]
|
|
if data.Target == flow {
|
|
return data.Antecedents
|
|
}
|
|
}
|
|
return flow.Antecedents
|
|
}
|
|
|
|
func (c *Checker) getTypeAtFlowAssignment(f *FlowState, flow *ast.FlowNode) FlowType {
|
|
node := flow.Node
|
|
// Assignments only narrow the computed type if the declared type is a union type. Thus, we
|
|
// only need to evaluate the assigned type if the declared type is a union type.
|
|
if c.isMatchingReference(f.reference, node) {
|
|
if !c.isReachableFlowNode(flow) {
|
|
return FlowType{t: c.unreachableNeverType}
|
|
}
|
|
if getAssignmentTargetKind(node) == AssignmentKindCompound {
|
|
flowType := c.getTypeAtFlowNode(f, flow.Antecedent)
|
|
return c.newFlowType(c.getBaseTypeOfLiteralType(flowType.t), flowType.incomplete)
|
|
}
|
|
if f.declaredType == c.autoType || f.declaredType == c.autoArrayType {
|
|
if c.isEmptyArrayAssignment(node) {
|
|
return FlowType{t: c.getEvolvingArrayType(c.neverType)}
|
|
}
|
|
assignedType := c.getWidenedLiteralType(c.getInitialOrAssignedType(f, flow))
|
|
if c.isTypeAssignableTo(assignedType, f.declaredType) {
|
|
return FlowType{t: assignedType}
|
|
}
|
|
return FlowType{t: c.anyArrayType}
|
|
}
|
|
t := f.declaredType
|
|
if isInCompoundLikeAssignment(node) {
|
|
t = c.getBaseTypeOfLiteralType(t)
|
|
}
|
|
if t.flags&TypeFlagsUnion != 0 {
|
|
return FlowType{t: c.getAssignmentReducedType(t, c.getInitialOrAssignedType(f, flow))}
|
|
}
|
|
return FlowType{t: t}
|
|
}
|
|
// We didn't have a direct match. However, if the reference is a dotted name, this
|
|
// may be an assignment to a left hand part of the reference. For example, for a
|
|
// reference 'x.y.z', we may be at an assignment to 'x.y' or 'x'. In that case,
|
|
// return the declared type.
|
|
if c.containsMatchingReference(f.reference, node) {
|
|
if !c.isReachableFlowNode(flow) {
|
|
return FlowType{t: c.unreachableNeverType}
|
|
}
|
|
return FlowType{t: f.declaredType}
|
|
}
|
|
// for (const _ in ref) acts as a nonnull on ref
|
|
if ast.IsVariableDeclaration(node) && ast.IsForInStatement(node.Parent.Parent) && (c.isMatchingReference(f.reference, node.Parent.Parent.Expression()) || c.optionalChainContainsReference(node.Parent.Parent.Expression(), f.reference)) {
|
|
return FlowType{t: c.getNonNullableTypeIfNeeded(c.finalizeEvolvingArrayType(c.getTypeAtFlowNode(f, flow.Antecedent).t))}
|
|
}
|
|
// Assignment doesn't affect reference
|
|
return FlowType{}
|
|
}
|
|
|
|
func (c *Checker) getInitialOrAssignedType(f *FlowState, flow *ast.FlowNode) *Type {
|
|
if ast.IsVariableDeclaration(flow.Node) || ast.IsBindingElement(flow.Node) {
|
|
return c.getNarrowableTypeForReference(c.getInitialType(flow.Node), f.reference, CheckModeNormal)
|
|
}
|
|
return c.getNarrowableTypeForReference(c.getAssignedType(flow.Node), f.reference, CheckModeNormal)
|
|
}
|
|
|
|
func (c *Checker) isEmptyArrayAssignment(node *ast.Node) bool {
|
|
return ast.IsVariableDeclaration(node) && node.Initializer() != nil && isEmptyArrayLiteral(node.Initializer()) ||
|
|
!ast.IsBindingElement(node) && ast.IsBinaryExpression(node.Parent) && isEmptyArrayLiteral(node.Parent.AsBinaryExpression().Right)
|
|
}
|
|
|
|
func (c *Checker) getTypeAtFlowCall(f *FlowState, flow *ast.FlowNode) FlowType {
|
|
signature := c.getEffectsSignature(flow.Node)
|
|
if signature != nil {
|
|
predicate := c.getTypePredicateOfSignature(signature)
|
|
if predicate != nil && (predicate.kind == TypePredicateKindAssertsThis || predicate.kind == TypePredicateKindAssertsIdentifier) {
|
|
flowType := c.getTypeAtFlowNode(f, flow.Antecedent)
|
|
t := c.finalizeEvolvingArrayType(flowType.t)
|
|
var narrowedType *Type
|
|
switch {
|
|
case predicate.t != nil:
|
|
narrowedType = c.narrowTypeByTypePredicate(f, t, predicate, flow.Node, true /*assumeTrue*/)
|
|
case predicate.kind == TypePredicateKindAssertsIdentifier && predicate.parameterIndex >= 0 && int(predicate.parameterIndex) < len(flow.Node.Arguments()):
|
|
narrowedType = c.narrowTypeByAssertion(f, t, flow.Node.Arguments()[predicate.parameterIndex])
|
|
default:
|
|
narrowedType = t
|
|
}
|
|
if narrowedType == t {
|
|
return flowType
|
|
}
|
|
return c.newFlowType(narrowedType, flowType.incomplete)
|
|
}
|
|
if c.getReturnTypeOfSignature(signature).flags&TypeFlagsNever != 0 {
|
|
return FlowType{t: c.unreachableNeverType}
|
|
}
|
|
}
|
|
return FlowType{}
|
|
}
|
|
|
|
func (c *Checker) narrowTypeByTypePredicate(f *FlowState, t *Type, predicate *TypePredicate, callExpression *ast.Node, assumeTrue bool) *Type {
|
|
// Don't narrow from 'any' if the predicate type is exactly 'Object' or 'Function'
|
|
if predicate.t != nil && !(IsTypeAny(t) && (predicate.t == c.globalObjectType || predicate.t == c.globalFunctionType)) {
|
|
predicateArgument := c.getTypePredicateArgument(predicate, callExpression)
|
|
if predicateArgument != nil {
|
|
if c.isMatchingReference(f.reference, predicateArgument) {
|
|
return c.getNarrowedType(t, predicate.t, assumeTrue, false /*checkDerived*/)
|
|
}
|
|
if c.strictNullChecks && c.optionalChainContainsReference(predicateArgument, f.reference) && (assumeTrue && !(c.hasTypeFacts(predicate.t, TypeFactsEQUndefined)) || !assumeTrue && everyType(predicate.t, c.IsNullableType)) {
|
|
t = c.getAdjustedTypeWithFacts(t, TypeFactsNEUndefinedOrNull)
|
|
}
|
|
access := c.getDiscriminantPropertyAccess(f, predicateArgument, t)
|
|
if access != nil {
|
|
return c.narrowTypeByDiscriminant(t, access, func(t *Type) *Type {
|
|
return c.getNarrowedType(t, predicate.t, assumeTrue, false /*checkDerived*/)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return t
|
|
}
|
|
|
|
func (c *Checker) narrowTypeByAssertion(f *FlowState, t *Type, expr *ast.Node) *Type {
|
|
node := ast.SkipParentheses(expr)
|
|
if node.Kind == ast.KindFalseKeyword {
|
|
return c.unreachableNeverType
|
|
}
|
|
if node.Kind == ast.KindBinaryExpression {
|
|
if node.AsBinaryExpression().OperatorToken.Kind == ast.KindAmpersandAmpersandToken {
|
|
return c.narrowTypeByAssertion(f, c.narrowTypeByAssertion(f, t, node.AsBinaryExpression().Left), node.AsBinaryExpression().Right)
|
|
}
|
|
if node.AsBinaryExpression().OperatorToken.Kind == ast.KindBarBarToken {
|
|
return c.getUnionType([]*Type{c.narrowTypeByAssertion(f, t, node.AsBinaryExpression().Left), c.narrowTypeByAssertion(f, t, node.AsBinaryExpression().Right)})
|
|
}
|
|
}
|
|
return c.narrowType(f, t, node, true /*assumeTrue*/)
|
|
}
|
|
|
|
func (c *Checker) getTypeAtFlowCondition(f *FlowState, flow *ast.FlowNode) FlowType {
|
|
flowType := c.getTypeAtFlowNode(f, flow.Antecedent)
|
|
if flowType.t.flags&TypeFlagsNever != 0 {
|
|
return flowType
|
|
}
|
|
// If we have an antecedent type (meaning we're reachable in some way), we first
|
|
// attempt to narrow the antecedent type. If that produces the never type, and if
|
|
// the antecedent type is incomplete (i.e. a transient type in a loop), then we
|
|
// take the type guard as an indication that control *could* reach here once we
|
|
// have the complete type. We proceed by switching to the silent never type which
|
|
// doesn't report errors when operators are applied to it. Note that this is the
|
|
// *only* place a silent never type is ever generated.
|
|
assumeTrue := flow.Flags&ast.FlowFlagsTrueCondition != 0
|
|
nonEvolvingType := c.finalizeEvolvingArrayType(flowType.t)
|
|
narrowedType := c.narrowType(f, nonEvolvingType, flow.Node, assumeTrue)
|
|
if narrowedType == nonEvolvingType {
|
|
return flowType
|
|
}
|
|
return c.newFlowType(narrowedType, flowType.incomplete)
|
|
}
|
|
|
|
// Narrow the given type based on the given expression having the assumed boolean value. The returned type
|
|
// will be a subtype or the same type as the argument.
|
|
func (c *Checker) narrowType(f *FlowState, t *Type, expr *ast.Node, assumeTrue bool) *Type {
|
|
// for `a?.b`, we emulate a synthetic `a !== null && a !== undefined` condition for `a`
|
|
if ast.IsExpressionOfOptionalChainRoot(expr) || ast.IsBinaryExpression(expr.Parent) && (expr.Parent.AsBinaryExpression().OperatorToken.Kind == ast.KindQuestionQuestionToken || expr.Parent.AsBinaryExpression().OperatorToken.Kind == ast.KindQuestionQuestionEqualsToken) && expr.Parent.AsBinaryExpression().Left == expr {
|
|
return c.narrowTypeByOptionality(f, t, expr, assumeTrue)
|
|
}
|
|
switch expr.Kind {
|
|
case ast.KindIdentifier:
|
|
// When narrowing a reference to a const variable, non-assigned parameter, or readonly property, we inline
|
|
// up to five levels of aliased conditional expressions that are themselves declared as const variables.
|
|
if !c.isMatchingReference(f.reference, expr) && c.inlineLevel < 5 {
|
|
symbol := c.getResolvedSymbol(expr)
|
|
if c.isConstantVariable(symbol) {
|
|
declaration := symbol.ValueDeclaration
|
|
if declaration != nil && ast.IsVariableDeclaration(declaration) && declaration.Type() == nil && declaration.Initializer() != nil && c.isConstantReference(f.reference) {
|
|
c.inlineLevel++
|
|
result := c.narrowType(f, t, declaration.Initializer(), assumeTrue)
|
|
c.inlineLevel--
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
fallthrough
|
|
case ast.KindThisKeyword, ast.KindSuperKeyword, ast.KindPropertyAccessExpression, ast.KindElementAccessExpression:
|
|
return c.narrowTypeByTruthiness(f, t, expr, assumeTrue)
|
|
case ast.KindCallExpression:
|
|
return c.narrowTypeByCallExpression(f, t, expr, assumeTrue)
|
|
case ast.KindParenthesizedExpression, ast.KindNonNullExpression, ast.KindSatisfiesExpression:
|
|
return c.narrowType(f, t, expr.Expression(), assumeTrue)
|
|
case ast.KindBinaryExpression:
|
|
return c.narrowTypeByBinaryExpression(f, t, expr.AsBinaryExpression(), assumeTrue)
|
|
case ast.KindPrefixUnaryExpression:
|
|
if expr.AsPrefixUnaryExpression().Operator == ast.KindExclamationToken {
|
|
return c.narrowType(f, t, expr.AsPrefixUnaryExpression().Operand, !assumeTrue)
|
|
}
|
|
}
|
|
return t
|
|
}
|
|
|
|
func (c *Checker) narrowTypeByOptionality(f *FlowState, t *Type, expr *ast.Node, assumePresent bool) *Type {
|
|
if c.isMatchingReference(f.reference, expr) {
|
|
return c.getAdjustedTypeWithFacts(t, core.IfElse(assumePresent, TypeFactsNEUndefinedOrNull, TypeFactsEQUndefinedOrNull))
|
|
}
|
|
access := c.getDiscriminantPropertyAccess(f, expr, t)
|
|
if access != nil {
|
|
return c.narrowTypeByDiscriminant(t, access, func(t *Type) *Type {
|
|
return c.getTypeWithFacts(t, core.IfElse(assumePresent, TypeFactsNEUndefinedOrNull, TypeFactsEQUndefinedOrNull))
|
|
})
|
|
}
|
|
return t
|
|
}
|
|
|
|
func (c *Checker) narrowTypeByTruthiness(f *FlowState, t *Type, expr *ast.Node, assumeTrue bool) *Type {
|
|
if c.isMatchingReference(f.reference, expr) {
|
|
return c.getAdjustedTypeWithFacts(t, core.IfElse(assumeTrue, TypeFactsTruthy, TypeFactsFalsy))
|
|
}
|
|
if c.strictNullChecks && assumeTrue && c.optionalChainContainsReference(expr, f.reference) {
|
|
t = c.getAdjustedTypeWithFacts(t, TypeFactsNEUndefinedOrNull)
|
|
}
|
|
access := c.getDiscriminantPropertyAccess(f, expr, t)
|
|
if access != nil {
|
|
return c.narrowTypeByDiscriminant(t, access, func(t *Type) *Type {
|
|
return c.getTypeWithFacts(t, core.IfElse(assumeTrue, TypeFactsTruthy, TypeFactsFalsy))
|
|
})
|
|
}
|
|
return t
|
|
}
|
|
|
|
func (c *Checker) narrowTypeByCallExpression(f *FlowState, t *Type, callExpression *ast.Node, assumeTrue bool) *Type {
|
|
if c.hasMatchingArgument(callExpression, f.reference) {
|
|
var predicate *TypePredicate
|
|
if assumeTrue || !isCallChain(callExpression) {
|
|
signature := c.getEffectsSignature(callExpression)
|
|
if signature != nil {
|
|
predicate = c.getTypePredicateOfSignature(signature)
|
|
}
|
|
}
|
|
if predicate != nil && (predicate.kind == TypePredicateKindThis || predicate.kind == TypePredicateKindIdentifier) {
|
|
return c.narrowTypeByTypePredicate(f, t, predicate, callExpression, assumeTrue)
|
|
}
|
|
}
|
|
if c.containsMissingType(t) && ast.IsAccessExpression(f.reference) && ast.IsPropertyAccessExpression(callExpression.Expression()) {
|
|
callAccess := callExpression.Expression()
|
|
if c.isMatchingReference(f.reference.Expression(), c.getReferenceCandidate(callAccess.Expression())) && ast.IsIdentifier(callAccess.Name()) && callAccess.Name().Text() == "hasOwnProperty" && len(callExpression.Arguments()) == 1 {
|
|
argument := callExpression.Arguments()[0]
|
|
if accessedName, ok := c.getAccessedPropertyName(f.reference); ok && ast.IsStringLiteralLike(argument) && accessedName == argument.Text() {
|
|
return c.getTypeWithFacts(t, core.IfElse(assumeTrue, TypeFactsNEUndefined, TypeFactsEQUndefined))
|
|
}
|
|
}
|
|
}
|
|
return t
|
|
}
|
|
|
|
func (c *Checker) narrowTypeByBinaryExpression(f *FlowState, t *Type, expr *ast.BinaryExpression, assumeTrue bool) *Type {
|
|
switch expr.OperatorToken.Kind {
|
|
case ast.KindEqualsToken, ast.KindBarBarEqualsToken, ast.KindAmpersandAmpersandEqualsToken, ast.KindQuestionQuestionEqualsToken:
|
|
return c.narrowTypeByTruthiness(f, c.narrowType(f, t, expr.Right, assumeTrue), expr.Left, assumeTrue)
|
|
case ast.KindEqualsEqualsToken, ast.KindExclamationEqualsToken, ast.KindEqualsEqualsEqualsToken, ast.KindExclamationEqualsEqualsToken:
|
|
operator := expr.OperatorToken.Kind
|
|
left := c.getReferenceCandidate(expr.Left)
|
|
right := c.getReferenceCandidate(expr.Right)
|
|
if left.Kind == ast.KindTypeOfExpression && ast.IsStringLiteralLike(right) {
|
|
return c.narrowTypeByTypeof(f, t, left.AsTypeOfExpression(), operator, right, assumeTrue)
|
|
}
|
|
if right.Kind == ast.KindTypeOfExpression && ast.IsStringLiteralLike(left) {
|
|
return c.narrowTypeByTypeof(f, t, right.AsTypeOfExpression(), operator, left, assumeTrue)
|
|
}
|
|
if c.isMatchingReference(f.reference, left) {
|
|
return c.narrowTypeByEquality(t, operator, right, assumeTrue)
|
|
}
|
|
if c.isMatchingReference(f.reference, right) {
|
|
return c.narrowTypeByEquality(t, operator, left, assumeTrue)
|
|
}
|
|
if c.strictNullChecks {
|
|
if c.optionalChainContainsReference(left, f.reference) {
|
|
t = c.narrowTypeByOptionalChainContainment(f, t, operator, right, assumeTrue)
|
|
} else if c.optionalChainContainsReference(right, f.reference) {
|
|
t = c.narrowTypeByOptionalChainContainment(f, t, operator, left, assumeTrue)
|
|
}
|
|
}
|
|
leftAccess := c.getDiscriminantPropertyAccess(f, left, t)
|
|
if leftAccess != nil {
|
|
return c.narrowTypeByDiscriminantProperty(t, leftAccess, operator, right, assumeTrue)
|
|
}
|
|
rightAccess := c.getDiscriminantPropertyAccess(f, right, t)
|
|
if rightAccess != nil {
|
|
return c.narrowTypeByDiscriminantProperty(t, rightAccess, operator, left, assumeTrue)
|
|
}
|
|
if c.isMatchingConstructorReference(f, left) {
|
|
return c.narrowTypeByConstructor(t, operator, right, assumeTrue)
|
|
}
|
|
if c.isMatchingConstructorReference(f, right) {
|
|
return c.narrowTypeByConstructor(t, operator, left, assumeTrue)
|
|
}
|
|
if ast.IsBooleanLiteral(right) && !ast.IsAccessExpression(left) {
|
|
return c.narrowTypeByBooleanComparison(f, t, left, right, operator, assumeTrue)
|
|
}
|
|
if ast.IsBooleanLiteral(left) && !ast.IsAccessExpression(right) {
|
|
return c.narrowTypeByBooleanComparison(f, t, right, left, operator, assumeTrue)
|
|
}
|
|
case ast.KindInstanceOfKeyword:
|
|
return c.narrowTypeByInstanceof(f, t, expr, assumeTrue)
|
|
case ast.KindInKeyword:
|
|
if ast.IsPrivateIdentifier(expr.Left) {
|
|
return c.narrowTypeByPrivateIdentifierInInExpression(f, t, expr, assumeTrue)
|
|
}
|
|
target := c.getReferenceCandidate(expr.Right)
|
|
if c.containsMissingType(t) && ast.IsAccessExpression(f.reference) && c.isMatchingReference(f.reference.Expression(), target) {
|
|
leftType := c.getTypeOfExpression(expr.Left)
|
|
if isTypeUsableAsPropertyName(leftType) {
|
|
if accessedName, ok := c.getAccessedPropertyName(f.reference); ok && accessedName == getPropertyNameFromType(leftType) {
|
|
return c.getTypeWithFacts(t, core.IfElse(assumeTrue, TypeFactsNEUndefined, TypeFactsEQUndefined))
|
|
}
|
|
}
|
|
}
|
|
if c.isMatchingReference(f.reference, target) {
|
|
leftType := c.getTypeOfExpression(expr.Left)
|
|
if isTypeUsableAsPropertyName(leftType) {
|
|
return c.narrowTypeByInKeyword(f, t, leftType, assumeTrue)
|
|
}
|
|
}
|
|
case ast.KindCommaToken:
|
|
return c.narrowType(f, t, expr.Right, assumeTrue)
|
|
case ast.KindAmpersandAmpersandToken:
|
|
// Ordinarily we won't see && and || expressions in control flow analysis because the Binder breaks those
|
|
// expressions down to individual conditional control flows. However, we may encounter them when analyzing
|
|
// aliased conditional expressions.
|
|
if assumeTrue {
|
|
return c.narrowType(f, c.narrowType(f, t, expr.Left, true /*assumeTrue*/), expr.Right, true /*assumeTrue*/)
|
|
}
|
|
return c.getUnionType([]*Type{c.narrowType(f, t, expr.Left, false /*assumeTrue*/), c.narrowType(f, t, expr.Right, false /*assumeTrue*/)})
|
|
case ast.KindBarBarToken:
|
|
if assumeTrue {
|
|
return c.getUnionType([]*Type{c.narrowType(f, t, expr.Left, true /*assumeTrue*/), c.narrowType(f, t, expr.Right, true /*assumeTrue*/)})
|
|
}
|
|
return c.narrowType(f, c.narrowType(f, t, expr.Left, false /*assumeTrue*/), expr.Right, false /*assumeTrue*/)
|
|
}
|
|
return t
|
|
}
|
|
|
|
func (c *Checker) narrowTypeByEquality(t *Type, operator ast.Kind, value *ast.Node, assumeTrue bool) *Type {
|
|
if t.flags&TypeFlagsAny != 0 {
|
|
return t
|
|
}
|
|
if operator == ast.KindExclamationEqualsToken || operator == ast.KindExclamationEqualsEqualsToken {
|
|
assumeTrue = !assumeTrue
|
|
}
|
|
valueType := c.getTypeOfExpression(value)
|
|
doubleEquals := operator == ast.KindEqualsEqualsToken || operator == ast.KindExclamationEqualsToken
|
|
if valueType.flags&TypeFlagsNullable != 0 {
|
|
if !c.strictNullChecks {
|
|
return t
|
|
}
|
|
var facts TypeFacts
|
|
switch {
|
|
case doubleEquals:
|
|
facts = core.IfElse(assumeTrue, TypeFactsEQUndefinedOrNull, TypeFactsNEUndefinedOrNull)
|
|
case valueType.flags&TypeFlagsNull != 0:
|
|
facts = core.IfElse(assumeTrue, TypeFactsEQNull, TypeFactsNENull)
|
|
default:
|
|
facts = core.IfElse(assumeTrue, TypeFactsEQUndefined, TypeFactsNEUndefined)
|
|
}
|
|
return c.getAdjustedTypeWithFacts(t, facts)
|
|
}
|
|
if assumeTrue {
|
|
if !doubleEquals && (t.flags&TypeFlagsUnknown != 0 || someType(t, c.IsEmptyAnonymousObjectType)) {
|
|
if valueType.flags&(TypeFlagsPrimitive|TypeFlagsNonPrimitive) != 0 || c.IsEmptyAnonymousObjectType(valueType) {
|
|
return valueType
|
|
}
|
|
if valueType.flags&TypeFlagsObject != 0 {
|
|
return c.nonPrimitiveType
|
|
}
|
|
}
|
|
filteredType := c.filterType(t, func(t *Type) bool {
|
|
return c.areTypesComparable(t, valueType) || doubleEquals && isCoercibleUnderDoubleEquals(t, valueType)
|
|
})
|
|
return c.replacePrimitivesWithLiterals(filteredType, valueType)
|
|
}
|
|
if isUnitType(valueType) {
|
|
return c.filterType(t, func(t *Type) bool {
|
|
return !(c.isUnitLikeType(t) && c.areTypesComparable(t, valueType))
|
|
})
|
|
}
|
|
return t
|
|
}
|
|
|
|
func (c *Checker) narrowTypeByTypeof(f *FlowState, t *Type, typeOfExpr *ast.TypeOfExpression, operator ast.Kind, literal *ast.Node, assumeTrue bool) *Type {
|
|
// We have '==', '!=', '===', or !==' operator with 'typeof xxx' and string literal operands
|
|
if operator == ast.KindExclamationEqualsToken || operator == ast.KindExclamationEqualsEqualsToken {
|
|
assumeTrue = !assumeTrue
|
|
}
|
|
target := c.getReferenceCandidate(typeOfExpr.Expression)
|
|
if !c.isMatchingReference(f.reference, target) {
|
|
if c.strictNullChecks && c.optionalChainContainsReference(target, f.reference) && assumeTrue == (literal.Text() != "undefined") {
|
|
t = c.getAdjustedTypeWithFacts(t, TypeFactsNEUndefinedOrNull)
|
|
}
|
|
propertyAccess := c.getDiscriminantPropertyAccess(f, target, t)
|
|
if propertyAccess != nil {
|
|
return c.narrowTypeByDiscriminant(t, propertyAccess, func(t *Type) *Type {
|
|
return c.narrowTypeByLiteralExpression(t, literal, assumeTrue)
|
|
})
|
|
}
|
|
return t
|
|
}
|
|
return c.narrowTypeByLiteralExpression(t, literal, assumeTrue)
|
|
}
|
|
|
|
var typeofNEFacts = map[string]TypeFacts{
|
|
"string": TypeFactsTypeofNEString,
|
|
"number": TypeFactsTypeofNENumber,
|
|
"bigint": TypeFactsTypeofNEBigInt,
|
|
"boolean": TypeFactsTypeofNEBoolean,
|
|
"symbol": TypeFactsTypeofNESymbol,
|
|
"undefined": TypeFactsNEUndefined,
|
|
"object": TypeFactsTypeofNEObject,
|
|
"function": TypeFactsTypeofNEFunction,
|
|
}
|
|
|
|
func (c *Checker) narrowTypeByLiteralExpression(t *Type, literal *ast.LiteralExpression, assumeTrue bool) *Type {
|
|
if assumeTrue {
|
|
return c.narrowTypeByTypeName(t, literal.Text())
|
|
}
|
|
facts, ok := typeofNEFacts[literal.Text()]
|
|
if !ok {
|
|
facts = TypeFactsTypeofNEHostObject
|
|
}
|
|
return c.getAdjustedTypeWithFacts(t, facts)
|
|
}
|
|
|
|
func (c *Checker) narrowTypeByTypeName(t *Type, typeName string) *Type {
|
|
switch typeName {
|
|
case "string":
|
|
return c.narrowTypeByTypeFacts(t, c.stringType, TypeFactsTypeofEQString)
|
|
case "number":
|
|
return c.narrowTypeByTypeFacts(t, c.numberType, TypeFactsTypeofEQNumber)
|
|
case "bigint":
|
|
return c.narrowTypeByTypeFacts(t, c.bigintType, TypeFactsTypeofEQBigInt)
|
|
case "boolean":
|
|
return c.narrowTypeByTypeFacts(t, c.booleanType, TypeFactsTypeofEQBoolean)
|
|
case "symbol":
|
|
return c.narrowTypeByTypeFacts(t, c.esSymbolType, TypeFactsTypeofEQSymbol)
|
|
case "object":
|
|
if t.flags&TypeFlagsAny != 0 {
|
|
return t
|
|
}
|
|
return c.getUnionType([]*Type{c.narrowTypeByTypeFacts(t, c.nonPrimitiveType, TypeFactsTypeofEQObject), c.narrowTypeByTypeFacts(t, c.nullType, TypeFactsEQNull)})
|
|
case "function":
|
|
if t.flags&TypeFlagsAny != 0 {
|
|
return t
|
|
}
|
|
return c.narrowTypeByTypeFacts(t, c.globalFunctionType, TypeFactsTypeofEQFunction)
|
|
case "undefined":
|
|
return c.narrowTypeByTypeFacts(t, c.undefinedType, TypeFactsEQUndefined)
|
|
}
|
|
return c.narrowTypeByTypeFacts(t, c.nonPrimitiveType, TypeFactsTypeofEQHostObject)
|
|
}
|
|
|
|
func (c *Checker) narrowTypeByTypeFacts(t *Type, impliedType *Type, facts TypeFacts) *Type {
|
|
return c.mapType(t, func(t *Type) *Type {
|
|
switch {
|
|
case c.isTypeRelatedTo(t, impliedType, c.strictSubtypeRelation):
|
|
if c.hasTypeFacts(t, facts) {
|
|
return t
|
|
}
|
|
return c.neverType
|
|
case c.isTypeSubtypeOf(impliedType, t):
|
|
return impliedType
|
|
case c.hasTypeFacts(t, facts):
|
|
return c.getIntersectionType([]*Type{t, impliedType})
|
|
}
|
|
return c.neverType
|
|
})
|
|
}
|
|
|
|
func (c *Checker) narrowTypeByDiscriminantProperty(t *Type, access *ast.Node, operator ast.Kind, value *ast.Node, assumeTrue bool) *Type {
|
|
if (operator == ast.KindEqualsEqualsEqualsToken || operator == ast.KindExclamationEqualsEqualsToken) && t.flags&TypeFlagsUnion != 0 {
|
|
keyPropertyName := c.getKeyPropertyName(t)
|
|
if keyPropertyName != "" {
|
|
if accessedName, ok := c.getAccessedPropertyName(access); ok && keyPropertyName == accessedName {
|
|
candidate := c.getConstituentTypeForKeyType(t, c.getTypeOfExpression(value))
|
|
if candidate != nil {
|
|
if assumeTrue && operator == ast.KindEqualsEqualsEqualsToken || !assumeTrue && operator == ast.KindExclamationEqualsEqualsToken {
|
|
return candidate
|
|
}
|
|
if propType := c.getTypeOfPropertyOfType(candidate, keyPropertyName); propType != nil && isUnitType(propType) {
|
|
return c.removeType(t, candidate)
|
|
}
|
|
return t
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return c.narrowTypeByDiscriminant(t, access, func(t *Type) *Type {
|
|
return c.narrowTypeByEquality(t, operator, value, assumeTrue)
|
|
})
|
|
}
|
|
|
|
func (c *Checker) narrowTypeByDiscriminant(t *Type, access *ast.Node, narrowType func(t *Type) *Type) *Type {
|
|
propName, ok := c.getAccessedPropertyName(access)
|
|
if !ok {
|
|
return t
|
|
}
|
|
optionalChain := ast.IsOptionalChain(access)
|
|
removeNullable := c.strictNullChecks && (optionalChain || isNonNullAccess(access)) && c.maybeTypeOfKind(t, TypeFlagsNullable)
|
|
nonNullType := t
|
|
if removeNullable {
|
|
nonNullType = c.getTypeWithFacts(t, TypeFactsNEUndefinedOrNull)
|
|
}
|
|
propType := c.getTypeOfPropertyOfType(nonNullType, propName)
|
|
if propType == nil {
|
|
return t
|
|
}
|
|
if removeNullable && optionalChain {
|
|
propType = c.getOptionalType(propType, false)
|
|
}
|
|
narrowedPropType := narrowType(propType)
|
|
return c.filterType(t, func(t *Type) bool {
|
|
discriminantType := core.OrElse(c.getTypeOfPropertyOrIndexSignatureOfType(t, propName), c.unknownType)
|
|
return discriminantType.flags&TypeFlagsNever == 0 && narrowedPropType.flags&TypeFlagsNever == 0 && c.areTypesComparable(narrowedPropType, discriminantType)
|
|
})
|
|
}
|
|
|
|
func (c *Checker) isMatchingConstructorReference(f *FlowState, expr *ast.Node) bool {
|
|
if ast.IsAccessExpression(expr) {
|
|
if accessedName, ok := c.getAccessedPropertyName(expr); ok && accessedName == "constructor" && c.isMatchingReference(f.reference, expr.Expression()) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (c *Checker) narrowTypeByConstructor(t *Type, operator ast.Kind, identifier *ast.Node, assumeTrue bool) *Type {
|
|
// Do not narrow when checking inequality.
|
|
if assumeTrue && operator != ast.KindEqualsEqualsToken && operator != ast.KindEqualsEqualsEqualsToken || !assumeTrue && operator != ast.KindExclamationEqualsToken && operator != ast.KindExclamationEqualsEqualsToken {
|
|
return t
|
|
}
|
|
// Get the type of the constructor identifier expression, if it is not a function then do not narrow.
|
|
identifierType := c.getTypeOfExpression(identifier)
|
|
if !c.isFunctionType(identifierType) && !c.isConstructorType(identifierType) {
|
|
return t
|
|
}
|
|
// Get the prototype property of the type identifier so we can find out its type.
|
|
prototypeProperty := c.getPropertyOfType(identifierType, "prototype")
|
|
if prototypeProperty == nil {
|
|
return t
|
|
}
|
|
// Get the type of the prototype, if it is undefined, or the global `Object` or `Function` types then do not narrow.
|
|
prototypeType := c.getTypeOfSymbol(prototypeProperty)
|
|
var candidate *Type
|
|
if !IsTypeAny(prototypeType) {
|
|
candidate = prototypeType
|
|
}
|
|
if candidate == nil || candidate == c.globalObjectType || candidate == c.globalFunctionType {
|
|
return t
|
|
}
|
|
// If the type that is being narrowed is `any` then just return the `candidate` type since every type is a subtype of `any`.
|
|
if IsTypeAny(t) {
|
|
return candidate
|
|
}
|
|
// Filter out types that are not considered to be "constructed by" the `candidate` type.
|
|
return c.filterType(t, func(t *Type) bool {
|
|
return c.isConstructedBy(t, candidate)
|
|
})
|
|
}
|
|
|
|
func (c *Checker) isConstructedBy(source *Type, target *Type) bool {
|
|
// If either the source or target type are a class type then we need to check that they are the same exact type.
|
|
// This is because you may have a class `A` that defines some set of properties, and another class `B`
|
|
// that defines the same set of properties as class `A`, in that case they are structurally the same
|
|
// type, but when you do something like `instanceOfA.constructor === B` it will return false.
|
|
if source.flags&TypeFlagsObject != 0 && source.objectFlags&ObjectFlagsClass != 0 || target.flags&TypeFlagsObject != 0 && target.objectFlags&ObjectFlagsClass != 0 {
|
|
return source.symbol == target.symbol
|
|
}
|
|
// For all other types just check that the `source` type is a subtype of the `target` type.
|
|
return c.isTypeSubtypeOf(source, target)
|
|
}
|
|
|
|
func (c *Checker) narrowTypeByBooleanComparison(f *FlowState, t *Type, expr *ast.Node, boolValue *ast.Node, operator ast.Kind, assumeTrue bool) *Type {
|
|
assumeTrue = (assumeTrue != (boolValue.Kind == ast.KindTrueKeyword)) != (operator != ast.KindExclamationEqualsEqualsToken && operator != ast.KindExclamationEqualsToken)
|
|
return c.narrowType(f, t, expr, assumeTrue)
|
|
}
|
|
|
|
func (c *Checker) narrowTypeByInstanceof(f *FlowState, t *Type, expr *ast.BinaryExpression, assumeTrue bool) *Type {
|
|
left := c.getReferenceCandidate(expr.Left)
|
|
if !c.isMatchingReference(f.reference, left) {
|
|
if assumeTrue && c.strictNullChecks && c.optionalChainContainsReference(left, f.reference) {
|
|
return c.getAdjustedTypeWithFacts(t, TypeFactsNEUndefinedOrNull)
|
|
}
|
|
return t
|
|
}
|
|
right := expr.Right
|
|
rightType := c.getTypeOfExpression(right)
|
|
if !c.isTypeDerivedFrom(rightType, c.globalObjectType) {
|
|
return t
|
|
}
|
|
// if the right-hand side has an object type with a custom `[Symbol.hasInstance]` method, and that method
|
|
// has a type predicate, use the type predicate to perform narrowing. This allows normal `object` types to
|
|
// participate in `instanceof`, as per Step 2 of https://tc39.es/ecma262/#sec-instanceofoperator.
|
|
var predicate *TypePredicate
|
|
if signature := c.getEffectsSignature(expr.AsNode()); signature != nil {
|
|
predicate = c.getTypePredicateOfSignature(signature)
|
|
}
|
|
if predicate != nil && predicate.kind == TypePredicateKindIdentifier && predicate.parameterIndex == 0 {
|
|
return c.getNarrowedType(t, predicate.t, assumeTrue, true /*checkDerived*/)
|
|
}
|
|
if !c.isTypeDerivedFrom(rightType, c.globalFunctionType) {
|
|
return t
|
|
}
|
|
instanceType := c.mapType(rightType, c.getInstanceType)
|
|
// Don't narrow from `any` if the target type is exactly `Object` or `Function`, and narrow
|
|
// in the false branch only if the target is a non-empty object type.
|
|
if IsTypeAny(t) && (instanceType == c.globalObjectType || instanceType == c.globalFunctionType) || !assumeTrue && !(instanceType.flags&TypeFlagsObject != 0 && !c.IsEmptyAnonymousObjectType(instanceType)) {
|
|
return t
|
|
}
|
|
return c.getNarrowedType(t, instanceType, assumeTrue, true /*checkDerived*/)
|
|
}
|
|
|
|
func (c *Checker) getNarrowedType(t *Type, candidate *Type, assumeTrue bool, checkDerived bool) *Type {
|
|
if t.flags&TypeFlagsUnion == 0 {
|
|
return c.getNarrowedTypeWorker(t, candidate, assumeTrue, checkDerived)
|
|
}
|
|
key := NarrowedTypeKey{t, candidate, assumeTrue, checkDerived}
|
|
if narrowedType, ok := c.narrowedTypes[key]; ok {
|
|
return narrowedType
|
|
}
|
|
narrowedType := c.getNarrowedTypeWorker(t, candidate, assumeTrue, checkDerived)
|
|
c.narrowedTypes[key] = narrowedType
|
|
return narrowedType
|
|
}
|
|
|
|
func (c *Checker) getNarrowedTypeWorker(t *Type, candidate *Type, assumeTrue bool, checkDerived bool) *Type {
|
|
if !assumeTrue {
|
|
if t == candidate {
|
|
return c.neverType
|
|
}
|
|
if checkDerived {
|
|
return c.filterType(t, func(t *Type) bool {
|
|
return !c.isTypeDerivedFrom(t, candidate)
|
|
})
|
|
}
|
|
if t.flags&TypeFlagsUnknown != 0 {
|
|
t = c.unknownUnionType
|
|
}
|
|
trueType := c.getNarrowedType(t, candidate, true /*assumeTrue*/, false /*checkDerived*/)
|
|
return c.recombineUnknownType(c.filterType(t, func(t *Type) bool {
|
|
return !c.isTypeSubsetOf(t, trueType)
|
|
}))
|
|
}
|
|
if t.flags&TypeFlagsAnyOrUnknown != 0 {
|
|
return candidate
|
|
}
|
|
if t == candidate {
|
|
return candidate
|
|
}
|
|
// We first attempt to filter the current type, narrowing constituents as appropriate and removing
|
|
// constituents that are unrelated to the candidate.
|
|
var keyPropertyName string
|
|
if t.flags&TypeFlagsUnion != 0 {
|
|
keyPropertyName = c.getKeyPropertyName(t)
|
|
}
|
|
narrowedType := c.mapType(candidate, func(n *Type) *Type {
|
|
// If a discriminant property is available, use that to reduce the type.
|
|
matching := t
|
|
if keyPropertyName != "" {
|
|
if discriminant := c.getTypeOfPropertyOfType(n, keyPropertyName); discriminant != nil {
|
|
if constituent := c.getConstituentTypeForKeyType(t, discriminant); constituent != nil {
|
|
matching = constituent
|
|
}
|
|
}
|
|
}
|
|
// For each constituent t in the current type, if t and c are directly related, pick the most
|
|
// specific of the two. When t and c are related in both directions, we prefer c for type predicates
|
|
// because that is the asserted type, but t for `instanceof` because generics aren't reflected in
|
|
// prototype object types.
|
|
var mapType func(*Type) *Type
|
|
if checkDerived {
|
|
mapType = func(t *Type) *Type {
|
|
switch {
|
|
case c.isTypeDerivedFrom(t, n):
|
|
return t
|
|
case c.isTypeDerivedFrom(n, t):
|
|
return n
|
|
}
|
|
return c.neverType
|
|
}
|
|
} else {
|
|
mapType = func(t *Type) *Type {
|
|
switch {
|
|
case c.isTypeStrictSubtypeOf(t, n):
|
|
return t
|
|
case c.isTypeStrictSubtypeOf(n, t):
|
|
return n
|
|
case c.isTypeSubtypeOf(t, n):
|
|
return t
|
|
case c.isTypeSubtypeOf(n, t):
|
|
return n
|
|
}
|
|
return c.neverType
|
|
}
|
|
}
|
|
directlyRelated := c.mapType(matching, mapType)
|
|
if directlyRelated.flags&TypeFlagsNever == 0 {
|
|
return directlyRelated
|
|
}
|
|
// If no constituents are directly related, create intersections for any generic constituents that
|
|
// are related by constraint.
|
|
var isRelated func(*Type, *Type) bool
|
|
if checkDerived {
|
|
isRelated = c.isTypeDerivedFrom
|
|
} else {
|
|
isRelated = c.isTypeSubtypeOf
|
|
}
|
|
return c.mapType(t, func(t *Type) *Type {
|
|
if c.maybeTypeOfKind(t, TypeFlagsInstantiable) {
|
|
constraint := c.getBaseConstraintOfType(t)
|
|
if constraint == nil || isRelated(n, constraint) {
|
|
return c.getIntersectionType([]*Type{t, n})
|
|
}
|
|
}
|
|
return c.neverType
|
|
})
|
|
})
|
|
// If filtering produced a non-empty type, return that. Otherwise, pick the most specific of the two
|
|
// based on assignability, or as a last resort produce an intersection.
|
|
switch {
|
|
case narrowedType.flags&TypeFlagsNever == 0:
|
|
return narrowedType
|
|
case c.isTypeSubtypeOf(candidate, t):
|
|
return candidate
|
|
case c.isTypeAssignableTo(t, candidate):
|
|
return t
|
|
case c.isTypeAssignableTo(candidate, t):
|
|
return candidate
|
|
}
|
|
return c.getIntersectionType([]*Type{t, candidate})
|
|
}
|
|
|
|
func (c *Checker) getInstanceType(constructorType *Type) *Type {
|
|
prototypePropertyType := c.getTypeOfPropertyOfType(constructorType, "prototype")
|
|
if prototypePropertyType != nil && !IsTypeAny(prototypePropertyType) {
|
|
return prototypePropertyType
|
|
}
|
|
constructSignatures := c.getSignaturesOfType(constructorType, SignatureKindConstruct)
|
|
if len(constructSignatures) != 0 {
|
|
return c.getUnionType(core.Map(constructSignatures, func(signature *Signature) *Type {
|
|
return c.getReturnTypeOfSignature(c.getErasedSignature(signature))
|
|
}))
|
|
}
|
|
// We use the empty object type to indicate we don't know the type of objects created by
|
|
// this constructor function.
|
|
return c.emptyObjectType
|
|
}
|
|
|
|
func (c *Checker) narrowTypeByPrivateIdentifierInInExpression(f *FlowState, t *Type, expr *ast.BinaryExpression, assumeTrue bool) *Type {
|
|
target := c.getReferenceCandidate(expr.Right)
|
|
if !c.isMatchingReference(f.reference, target) {
|
|
return t
|
|
}
|
|
symbol := c.getSymbolForPrivateIdentifierExpression(expr.Left)
|
|
if symbol == nil {
|
|
return t
|
|
}
|
|
classSymbol := symbol.Parent
|
|
var targetType *Type
|
|
if ast.HasStaticModifier(symbol.ValueDeclaration) {
|
|
targetType = c.getTypeOfSymbol(classSymbol)
|
|
} else {
|
|
targetType = c.getDeclaredTypeOfSymbol(classSymbol)
|
|
}
|
|
return c.getNarrowedType(t, targetType, assumeTrue, true /*checkDerived*/)
|
|
}
|
|
|
|
func (c *Checker) narrowTypeByInKeyword(f *FlowState, t *Type, nameType *Type, assumeTrue bool) *Type {
|
|
name := getPropertyNameFromType(nameType)
|
|
isKnownProperty := someType(t, func(t *Type) bool {
|
|
return c.isTypePresencePossible(t, name, true /*assumeTrue*/)
|
|
})
|
|
if isKnownProperty {
|
|
// If the check is for a known property (i.e. a property declared in some constituent of
|
|
// the target type), we filter the target type by presence of absence of the property.
|
|
return c.filterType(t, func(t *Type) bool {
|
|
return c.isTypePresencePossible(t, name, assumeTrue)
|
|
})
|
|
}
|
|
if assumeTrue {
|
|
// If the check is for an unknown property, we intersect the target type with `Record<X, unknown>`,
|
|
// where X is the name of the property.
|
|
recordSymbol := c.getGlobalRecordSymbol()
|
|
if recordSymbol != nil {
|
|
return c.getIntersectionType([]*Type{t, c.getTypeAliasInstantiation(recordSymbol, []*Type{nameType, c.unknownType}, nil)})
|
|
}
|
|
}
|
|
return t
|
|
}
|
|
|
|
func (c *Checker) isTypePresencePossible(t *Type, propName string, assumeTrue bool) bool {
|
|
prop := c.getPropertyOfType(t, propName)
|
|
if prop != nil {
|
|
return prop.Flags&ast.SymbolFlagsOptional != 0 || prop.CheckFlags&ast.CheckFlagsPartial != 0 || assumeTrue
|
|
}
|
|
return c.getApplicableIndexInfoForName(t, propName) != nil || !assumeTrue
|
|
}
|
|
|
|
func (c *Checker) narrowTypeByOptionalChainContainment(f *FlowState, t *Type, operator ast.Kind, value *ast.Node, assumeTrue bool) *Type {
|
|
// We are in a branch of obj?.foo === value (or any one of the other equality operators). We narrow obj as follows:
|
|
// When operator is === and type of value excludes undefined, null and undefined is removed from type of obj in true branch.
|
|
// When operator is !== and type of value excludes undefined, null and undefined is removed from type of obj in false branch.
|
|
// When operator is == and type of value excludes null and undefined, null and undefined is removed from type of obj in true branch.
|
|
// When operator is != and type of value excludes null and undefined, null and undefined is removed from type of obj in false branch.
|
|
// When operator is === and type of value is undefined, null and undefined is removed from type of obj in false branch.
|
|
// When operator is !== and type of value is undefined, null and undefined is removed from type of obj in true branch.
|
|
// When operator is == and type of value is null or undefined, null and undefined is removed from type of obj in false branch.
|
|
// When operator is != and type of value is null or undefined, null and undefined is removed from type of obj in true branch.
|
|
equalsOperator := operator == ast.KindEqualsEqualsToken || operator == ast.KindEqualsEqualsEqualsToken
|
|
var nullableFlags TypeFlags
|
|
if operator == ast.KindEqualsEqualsToken || operator == ast.KindExclamationEqualsToken {
|
|
nullableFlags = TypeFlagsNullable
|
|
} else {
|
|
nullableFlags = TypeFlagsUndefined
|
|
}
|
|
valueType := c.getTypeOfExpression(value)
|
|
// Note that we include any and unknown in the exclusion test because their domain includes null and undefined.
|
|
removeNullable := equalsOperator != assumeTrue && everyType(valueType, func(t *Type) bool { return t.flags&nullableFlags != 0 }) ||
|
|
equalsOperator == assumeTrue && everyType(valueType, func(t *Type) bool { return t.flags&(TypeFlagsAnyOrUnknown|nullableFlags) == 0 })
|
|
if removeNullable {
|
|
return c.getAdjustedTypeWithFacts(t, TypeFactsNEUndefinedOrNull)
|
|
}
|
|
return t
|
|
}
|
|
|
|
func (c *Checker) getTypeAtSwitchClause(f *FlowState, flow *ast.FlowNode) FlowType {
|
|
data := flow.Node.AsFlowSwitchClauseData()
|
|
expr := ast.SkipParentheses(data.SwitchStatement.Expression())
|
|
flowType := c.getTypeAtFlowNode(f, flow.Antecedent)
|
|
t := flowType.t
|
|
switch {
|
|
case c.isMatchingReference(f.reference, expr):
|
|
t = c.narrowTypeBySwitchOnDiscriminant(t, data)
|
|
case expr.Kind == ast.KindTypeOfExpression && c.isMatchingReference(f.reference, expr.Expression()):
|
|
t = c.narrowTypeBySwitchOnTypeOf(t, data)
|
|
case expr.Kind == ast.KindTrueKeyword:
|
|
t = c.narrowTypeBySwitchOnTrue(f, t, data)
|
|
default:
|
|
if c.strictNullChecks {
|
|
if c.optionalChainContainsReference(expr, f.reference) {
|
|
t = c.narrowTypeBySwitchOptionalChainContainment(t, data, func(t *Type) bool {
|
|
return t.flags&(TypeFlagsUndefined|TypeFlagsNever) == 0
|
|
})
|
|
} else if ast.IsTypeOfExpression(expr) && c.optionalChainContainsReference(expr.Expression(), f.reference) {
|
|
t = c.narrowTypeBySwitchOptionalChainContainment(t, data, func(t *Type) bool {
|
|
return !(t.flags&TypeFlagsNever != 0 || t.flags&TypeFlagsStringLiteral != 0 && getStringLiteralValue(t) == "undefined")
|
|
})
|
|
}
|
|
}
|
|
access := c.getDiscriminantPropertyAccess(f, expr, t)
|
|
if access != nil {
|
|
t = c.narrowTypeBySwitchOnDiscriminantProperty(t, access, data)
|
|
}
|
|
}
|
|
return c.newFlowType(t, flowType.incomplete)
|
|
}
|
|
|
|
func (c *Checker) narrowTypeBySwitchOnDiscriminant(t *Type, data *ast.FlowSwitchClauseData) *Type {
|
|
// We only narrow if all case expressions specify
|
|
// values with unit types, except for the case where
|
|
// `type` is unknown. In this instance we map object
|
|
// types to the nonPrimitive type and narrow with that.
|
|
switchTypes := c.getSwitchClauseTypes(data.SwitchStatement)
|
|
if len(switchTypes) == 0 {
|
|
return t
|
|
}
|
|
clauseTypes := switchTypes[data.ClauseStart:data.ClauseEnd]
|
|
hasDefaultClause := data.ClauseStart == data.ClauseEnd || slices.Contains(clauseTypes, c.neverType)
|
|
if (t.flags&TypeFlagsUnknown != 0) && !hasDefaultClause {
|
|
var groundClauseTypes []*Type
|
|
for i, s := range clauseTypes {
|
|
if s.flags&(TypeFlagsPrimitive|TypeFlagsNonPrimitive) != 0 {
|
|
if groundClauseTypes != nil {
|
|
groundClauseTypes = append(groundClauseTypes, s)
|
|
}
|
|
} else if s.flags&TypeFlagsObject != 0 {
|
|
if groundClauseTypes == nil {
|
|
groundClauseTypes = clauseTypes[:i:i]
|
|
}
|
|
groundClauseTypes = append(groundClauseTypes, c.nonPrimitiveType)
|
|
} else {
|
|
return t
|
|
}
|
|
}
|
|
return c.getUnionType(core.IfElse(groundClauseTypes == nil, clauseTypes, groundClauseTypes))
|
|
}
|
|
discriminantType := c.getUnionType(clauseTypes)
|
|
var caseType *Type
|
|
if discriminantType.flags&TypeFlagsNever != 0 {
|
|
caseType = c.neverType
|
|
} else {
|
|
filtered := c.filterType(t, func(t *Type) bool { return c.areTypesComparable(discriminantType, t) })
|
|
caseType = c.replacePrimitivesWithLiterals(filtered, discriminantType)
|
|
}
|
|
if !hasDefaultClause {
|
|
return caseType
|
|
}
|
|
defaultType := c.filterType(t, func(t *Type) bool {
|
|
if !c.isUnitLikeType(t) {
|
|
return true
|
|
}
|
|
u := c.undefinedType
|
|
if t.flags&TypeFlagsUndefined == 0 {
|
|
u = c.getRegularTypeOfLiteralType(c.extractUnitType(t))
|
|
}
|
|
return !slices.Contains(switchTypes, u)
|
|
})
|
|
if caseType.flags&TypeFlagsNever != 0 {
|
|
return defaultType
|
|
}
|
|
return c.getUnionType([]*Type{caseType, defaultType})
|
|
}
|
|
|
|
func (c *Checker) narrowTypeBySwitchOnTypeOf(t *Type, data *ast.FlowSwitchClauseData) *Type {
|
|
witnesses := c.getSwitchClauseTypeOfWitnesses(data.SwitchStatement)
|
|
if witnesses == nil {
|
|
return t
|
|
}
|
|
clauses := data.SwitchStatement.AsSwitchStatement().CaseBlock.AsCaseBlock().Clauses.Nodes
|
|
// Equal start and end denotes implicit fallthrough; undefined marks explicit default clause.
|
|
defaultIndex := core.FindIndex(clauses, func(clause *ast.Node) bool {
|
|
return clause.Kind == ast.KindDefaultClause
|
|
})
|
|
clauseStart := int(data.ClauseStart)
|
|
clauseEnd := int(data.ClauseEnd)
|
|
hasDefaultClause := clauseStart == clauseEnd || (defaultIndex >= clauseStart && defaultIndex < clauseEnd)
|
|
if hasDefaultClause {
|
|
// In the default clause we filter constituents down to those that are not-equal to all handled cases.
|
|
notEqualFacts := c.getNotEqualFactsFromTypeofSwitch(clauseStart, clauseEnd, witnesses)
|
|
return c.filterType(t, func(t *Type) bool {
|
|
return c.getTypeFacts(t, notEqualFacts) == notEqualFacts
|
|
})
|
|
}
|
|
// In the non-default cause we create a union of the type narrowed by each of the listed cases.
|
|
clauseWitnesses := witnesses[clauseStart:clauseEnd]
|
|
return c.getUnionType(core.Map(clauseWitnesses, func(text string) *Type {
|
|
if text != "" {
|
|
return c.narrowTypeByTypeName(t, text)
|
|
}
|
|
return c.neverType
|
|
}))
|
|
}
|
|
|
|
func (c *Checker) narrowTypeBySwitchOnTrue(f *FlowState, t *Type, data *ast.FlowSwitchClauseData) *Type {
|
|
clauses := data.SwitchStatement.AsSwitchStatement().CaseBlock.AsCaseBlock().Clauses.Nodes
|
|
defaultIndex := core.FindIndex(clauses, func(clause *ast.Node) bool {
|
|
return clause.Kind == ast.KindDefaultClause
|
|
})
|
|
clauseStart := int(data.ClauseStart)
|
|
clauseEnd := int(data.ClauseEnd)
|
|
hasDefaultClause := clauseStart == clauseEnd || (defaultIndex >= clauseStart && defaultIndex < clauseEnd)
|
|
// First, narrow away all of the cases that preceded this set of cases.
|
|
for i := range clauseStart {
|
|
clause := clauses[i]
|
|
if clause.Kind == ast.KindCaseClause {
|
|
t = c.narrowType(f, t, clause.Expression(), false /*assumeTrue*/)
|
|
}
|
|
}
|
|
// If our current set has a default, then none the other cases were hit either.
|
|
// There's no point in narrowing by the other cases in the set, since we can
|
|
// get here through other paths.
|
|
if hasDefaultClause {
|
|
for i := clauseEnd; i < len(clauses); i++ {
|
|
clause := clauses[i]
|
|
if clause.Kind == ast.KindCaseClause {
|
|
t = c.narrowType(f, t, clause.Expression(), false /*assumeTrue*/)
|
|
}
|
|
}
|
|
return t
|
|
}
|
|
// Now, narrow based on the cases in this set.
|
|
return c.getUnionType(core.Map(clauses[clauseStart:clauseEnd], func(clause *ast.Node) *Type {
|
|
if clause.Kind == ast.KindCaseClause {
|
|
return c.narrowType(f, t, clause.Expression(), true /*assumeTrue*/)
|
|
}
|
|
return c.neverType
|
|
}))
|
|
}
|
|
|
|
func (c *Checker) narrowTypeBySwitchOptionalChainContainment(t *Type, data *ast.FlowSwitchClauseData, clauseCheck func(t *Type) bool) *Type {
|
|
everyClauseChecks := data.ClauseStart != data.ClauseEnd && core.Every(c.getSwitchClauseTypes(data.SwitchStatement)[data.ClauseStart:data.ClauseEnd], clauseCheck)
|
|
if everyClauseChecks {
|
|
return c.getTypeWithFacts(t, TypeFactsNEUndefinedOrNull)
|
|
}
|
|
return t
|
|
}
|
|
|
|
func (c *Checker) narrowTypeBySwitchOnDiscriminantProperty(t *Type, access *ast.Node, data *ast.FlowSwitchClauseData) *Type {
|
|
if data.ClauseStart < data.ClauseEnd && t.flags&TypeFlagsUnion != 0 {
|
|
accessedName, _ := c.getAccessedPropertyName(access)
|
|
if accessedName != "" && c.getKeyPropertyName(t) == accessedName {
|
|
clauseTypes := c.getSwitchClauseTypes(data.SwitchStatement)[data.ClauseStart:data.ClauseEnd]
|
|
candidate := c.getUnionType(core.Map(clauseTypes, func(s *Type) *Type {
|
|
result := c.getConstituentTypeForKeyType(t, s)
|
|
if result != nil {
|
|
return result
|
|
}
|
|
return c.unknownType
|
|
}))
|
|
if candidate != c.unknownType {
|
|
return candidate
|
|
}
|
|
}
|
|
}
|
|
return c.narrowTypeByDiscriminant(t, access, func(t *Type) *Type {
|
|
return c.narrowTypeBySwitchOnDiscriminant(t, data)
|
|
})
|
|
}
|
|
|
|
func (c *Checker) getTypeAtFlowBranchLabel(f *FlowState, flow *ast.FlowNode, antecedents *ast.FlowList) FlowType {
|
|
antecedentStart := len(c.antecedentTypes)
|
|
subtypeReduction := false
|
|
seenIncomplete := false
|
|
var bypassFlow *ast.FlowNode
|
|
for list := antecedents; list != nil; list = list.Next {
|
|
antecedent := list.Flow
|
|
if bypassFlow == nil && antecedent.Flags&ast.FlowFlagsSwitchClause != 0 && antecedent.Node.AsFlowSwitchClauseData().IsEmpty() {
|
|
// The antecedent is the bypass branch of a potentially exhaustive switch statement.
|
|
bypassFlow = antecedent
|
|
continue
|
|
}
|
|
flowType := c.getTypeAtFlowNode(f, antecedent)
|
|
// If the type at a particular antecedent path is the declared type and the
|
|
// reference is known to always be assigned (i.e. when declared and initial types
|
|
// are the same), there is no reason to process more antecedents since the only
|
|
// possible outcome is subtypes that will be removed in the final union type anyway.
|
|
if flowType.t == f.declaredType && f.declaredType == f.initialType {
|
|
c.antecedentTypes = c.antecedentTypes[:antecedentStart]
|
|
return FlowType{t: flowType.t}
|
|
}
|
|
if !slices.Contains(c.antecedentTypes[antecedentStart:], flowType.t) {
|
|
c.antecedentTypes = append(c.antecedentTypes, flowType.t)
|
|
}
|
|
// If an antecedent type is not a subset of the declared type, we need to perform
|
|
// subtype reduction. This happens when a "foreign" type is injected into the control
|
|
// flow using the instanceof operator or a user defined type predicate.
|
|
if !c.isTypeSubsetOf(flowType.t, f.initialType) {
|
|
subtypeReduction = true
|
|
}
|
|
if flowType.incomplete {
|
|
seenIncomplete = true
|
|
}
|
|
}
|
|
if bypassFlow != nil {
|
|
flowType := c.getTypeAtFlowNode(f, bypassFlow)
|
|
// If the bypass flow contributes a type we haven't seen yet and the switch statement
|
|
// isn't exhaustive, process the bypass flow type. Since exhaustiveness checks increase
|
|
// the risk of circularities, we only want to perform them when they make a difference.
|
|
if flowType.t.flags&TypeFlagsNever == 0 && !slices.Contains(c.antecedentTypes[antecedentStart:], flowType.t) && !c.isExhaustiveSwitchStatement(bypassFlow.Node.AsFlowSwitchClauseData().SwitchStatement) {
|
|
if flowType.t == f.declaredType && f.declaredType == f.initialType {
|
|
c.antecedentTypes = c.antecedentTypes[:antecedentStart]
|
|
return FlowType{t: flowType.t}
|
|
}
|
|
c.antecedentTypes = append(c.antecedentTypes, flowType.t)
|
|
if !c.isTypeSubsetOf(flowType.t, f.initialType) {
|
|
subtypeReduction = true
|
|
}
|
|
if flowType.incomplete {
|
|
seenIncomplete = true
|
|
}
|
|
}
|
|
}
|
|
result := c.newFlowType(c.getUnionOrEvolvingArrayType(f, c.antecedentTypes[antecedentStart:], core.IfElse(subtypeReduction, UnionReductionSubtype, UnionReductionLiteral)), seenIncomplete)
|
|
c.antecedentTypes = c.antecedentTypes[:antecedentStart]
|
|
return result
|
|
}
|
|
|
|
// At flow control branch or loop junctions, if the type along every antecedent code path
|
|
// is an evolving array type, we construct a combined evolving array type. Otherwise we
|
|
// finalize all evolving array types.
|
|
func (c *Checker) getUnionOrEvolvingArrayType(f *FlowState, types []*Type, subtypeReduction UnionReduction) *Type {
|
|
if isEvolvingArrayTypeList(types) {
|
|
return c.getEvolvingArrayType(c.getUnionType(core.Map(types, c.getElementTypeOfEvolvingArrayType)))
|
|
}
|
|
result := c.recombineUnknownType(c.getUnionTypeEx(core.SameMap(types, c.finalizeEvolvingArrayType), subtypeReduction, nil, nil))
|
|
if result != f.declaredType && result.flags&f.declaredType.flags&TypeFlagsUnion != 0 && slices.Equal(result.AsUnionType().types, f.declaredType.AsUnionType().types) {
|
|
return f.declaredType
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (c *Checker) getTypeAtFlowLoopLabel(f *FlowState, flow *ast.FlowNode) FlowType {
|
|
if f.refKey == "" {
|
|
f.refKey = c.getFlowReferenceKey(f)
|
|
}
|
|
if f.refKey == "?" {
|
|
// No cache key is generated when binding patterns are in unnarrowable situations
|
|
return FlowType{t: f.declaredType}
|
|
}
|
|
key := FlowLoopKey{flowNode: flow, refKey: f.refKey}
|
|
// If we have previously computed the control flow type for the reference at
|
|
// this flow loop junction, return the cached type.
|
|
if cached := c.flowLoopCache[key]; cached != nil {
|
|
return FlowType{t: cached}
|
|
}
|
|
// If this flow loop junction and reference are already being processed, return
|
|
// the union of the types computed for each branch so far, marked as incomplete.
|
|
// It is possible to see an empty array in cases where loops are nested and the
|
|
// back edge of the outer loop reaches an inner loop that is already being analyzed.
|
|
// In such cases we restart the analysis of the inner loop, which will then see
|
|
// a non-empty in-process array for the outer loop and eventually terminate because
|
|
// the first antecedent of a loop junction is always the non-looping control flow
|
|
// path that leads to the top.
|
|
for _, loopInfo := range c.flowLoopStack {
|
|
if loopInfo.key == key && len(loopInfo.types) != 0 {
|
|
return c.newFlowType(c.getUnionOrEvolvingArrayType(f, loopInfo.types, UnionReductionLiteral), true /*incomplete*/)
|
|
}
|
|
}
|
|
// Add the flow loop junction and reference to the in-process stack and analyze
|
|
// each antecedent code path.
|
|
antecedentTypes := make([]*Type, 0, 4)
|
|
subtypeReduction := false
|
|
var firstAntecedentType FlowType
|
|
for list := flow.Antecedents; list != nil; list = list.Next {
|
|
var flowType FlowType
|
|
if firstAntecedentType.isNil() {
|
|
// The first antecedent of a loop junction is always the non-looping control
|
|
// flow path that leads to the top.
|
|
firstAntecedentType = c.getTypeAtFlowNode(f, list.Flow)
|
|
flowType = firstAntecedentType
|
|
} else {
|
|
// All but the first antecedent are the looping control flow paths that lead
|
|
// back to the loop junction. We track these on the flow loop stack.
|
|
c.flowLoopStack = append(c.flowLoopStack, FlowLoopInfo{key: key, types: antecedentTypes})
|
|
saveFlowTypeCache := c.flowTypeCache
|
|
c.flowTypeCache = nil
|
|
flowType = c.getTypeAtFlowNode(f, list.Flow)
|
|
c.flowTypeCache = saveFlowTypeCache
|
|
c.flowLoopStack = c.flowLoopStack[:len(c.flowLoopStack)-1]
|
|
// If we see a value appear in the cache it is a sign that control flow analysis
|
|
// was restarted and completed by checkExpressionCached. We can simply pick up
|
|
// the resulting type and bail out.
|
|
if cached := c.flowLoopCache[key]; cached != nil {
|
|
return FlowType{t: cached}
|
|
}
|
|
}
|
|
antecedentTypes = core.AppendIfUnique(antecedentTypes, flowType.t)
|
|
// If an antecedent type is not a subset of the declared type, we need to perform
|
|
// subtype reduction. This happens when a "foreign" type is injected into the control
|
|
// flow using the instanceof operator or a user defined type predicate.
|
|
if !c.isTypeSubsetOf(flowType.t, f.initialType) {
|
|
subtypeReduction = true
|
|
}
|
|
// If the type at a particular antecedent path is the declared type there is no
|
|
// reason to process more antecedents since the only possible outcome is subtypes
|
|
// that will be removed in the final union type anyway.
|
|
if flowType.t == f.declaredType {
|
|
break
|
|
}
|
|
}
|
|
// The result is incomplete if the first antecedent (the non-looping control flow path)
|
|
// is incomplete.
|
|
result := c.getUnionOrEvolvingArrayType(f, antecedentTypes, core.IfElse(subtypeReduction, UnionReductionSubtype, UnionReductionLiteral))
|
|
if firstAntecedentType.incomplete {
|
|
return c.newFlowType(result, true /*incomplete*/)
|
|
}
|
|
c.flowLoopCache[key] = result
|
|
return FlowType{t: result}
|
|
}
|
|
|
|
func (c *Checker) getTypeAtFlowArrayMutation(f *FlowState, flow *ast.FlowNode) FlowType {
|
|
if f.declaredType == c.autoType || f.declaredType == c.autoArrayType {
|
|
node := flow.Node
|
|
var expr *ast.Node
|
|
if ast.IsCallExpression(node) {
|
|
expr = node.Expression().Expression()
|
|
} else {
|
|
expr = node.AsBinaryExpression().Left.Expression()
|
|
}
|
|
if c.isMatchingReference(f.reference, c.getReferenceCandidate(expr)) {
|
|
flowType := c.getTypeAtFlowNode(f, flow.Antecedent)
|
|
if flowType.t.objectFlags&ObjectFlagsEvolvingArray != 0 {
|
|
evolvedType := flowType.t
|
|
if ast.IsCallExpression(node) {
|
|
for _, arg := range node.Arguments() {
|
|
evolvedType = c.addEvolvingArrayElementType(evolvedType, arg)
|
|
}
|
|
} else {
|
|
// We must get the context free expression type so as to not recur in an uncached fashion on the LHS (which causes exponential blowup in compile time)
|
|
indexType := c.getContextFreeTypeOfExpression(node.AsBinaryExpression().Left.AsElementAccessExpression().ArgumentExpression)
|
|
if c.isTypeAssignableToKind(indexType, TypeFlagsNumberLike) {
|
|
evolvedType = c.addEvolvingArrayElementType(evolvedType, node.AsBinaryExpression().Right)
|
|
}
|
|
}
|
|
return c.newFlowType(evolvedType, flowType.incomplete)
|
|
}
|
|
return flowType
|
|
}
|
|
}
|
|
return FlowType{}
|
|
}
|
|
|
|
func (c *Checker) getDiscriminantPropertyAccess(f *FlowState, expr *ast.Node, computedType *Type) *ast.Node {
|
|
// As long as the computed type is a subset of the declared type, we use the full declared type to detect
|
|
// a discriminant property. In cases where the computed type isn't a subset, e.g because of a preceding type
|
|
// predicate narrowing, we use the actual computed type.
|
|
if f.declaredType.flags&TypeFlagsUnion != 0 || computedType.flags&TypeFlagsUnion != 0 {
|
|
access := c.getCandidateDiscriminantPropertyAccess(f, expr)
|
|
if access != nil {
|
|
if name, ok := c.getAccessedPropertyName(access); ok {
|
|
t := computedType
|
|
if f.declaredType.flags&TypeFlagsUnion != 0 && c.isTypeSubsetOf(computedType, f.declaredType) {
|
|
t = f.declaredType
|
|
}
|
|
if c.isDiscriminantProperty(t, name) {
|
|
return access
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Checker) getCandidateDiscriminantPropertyAccess(f *FlowState, expr *ast.Node) *ast.Node {
|
|
switch {
|
|
case ast.IsBindingPattern(f.reference) || ast.IsFunctionExpressionOrArrowFunction(f.reference) || ast.IsObjectLiteralMethod(f.reference):
|
|
// When the reference is a binding pattern or function or arrow expression, we are narrowing a pseudo-reference in
|
|
// getNarrowedTypeOfSymbol. An identifier for a destructuring variable declared in the same binding pattern or
|
|
// parameter declared in the same parameter list is a candidate.
|
|
if ast.IsIdentifier(expr) {
|
|
symbol := c.getResolvedSymbol(expr)
|
|
declaration := c.getExportSymbolOfValueSymbolIfExported(symbol).ValueDeclaration
|
|
if declaration != nil && (ast.IsBindingElement(declaration) || ast.IsParameter(declaration)) && f.reference == declaration.Parent && declaration.Initializer() == nil && !hasDotDotDotToken(declaration) {
|
|
return declaration
|
|
}
|
|
}
|
|
case ast.IsAccessExpression(expr):
|
|
// An access expression is a candidate if the reference matches the left hand expression.
|
|
if c.isMatchingReference(f.reference, expr.Expression()) {
|
|
return expr
|
|
}
|
|
case ast.IsIdentifier(expr):
|
|
symbol := c.getResolvedSymbol(expr)
|
|
if c.isConstantVariable(symbol) {
|
|
declaration := symbol.ValueDeclaration
|
|
// Given 'const x = obj.kind', allow 'x' as an alias for 'obj.kind'
|
|
if ast.IsVariableDeclaration(declaration) && declaration.Type() == nil {
|
|
if initializer := declaration.Initializer(); initializer != nil && ast.IsAccessExpression(initializer) && c.isMatchingReference(f.reference, initializer.Expression()) {
|
|
return initializer
|
|
}
|
|
}
|
|
// Given 'const { kind: x } = obj', allow 'x' as an alias for 'obj.kind'
|
|
if ast.IsBindingElement(declaration) && declaration.Initializer() == nil {
|
|
parent := declaration.Parent.Parent
|
|
if ast.IsVariableDeclaration(parent) && parent.Type() == nil {
|
|
if initializer := parent.Initializer(); initializer != nil && (ast.IsIdentifier(initializer) || ast.IsAccessExpression(initializer)) && c.isMatchingReference(f.reference, initializer) {
|
|
return declaration
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// An evolving array type tracks the element types that have so far been seen in an
|
|
// 'x.push(value)' or 'x[n] = value' operation along the control flow graph. Evolving
|
|
// array types are ultimately converted into manifest array types (using getFinalArrayType)
|
|
// and never escape the getFlowTypeOfReference function.
|
|
func (c *Checker) getEvolvingArrayType(elementType *Type) *Type {
|
|
key := CachedTypeKey{kind: CachedTypeKindEvolvingArrayType, typeId: elementType.id}
|
|
result := c.cachedTypes[key]
|
|
if result == nil {
|
|
result = c.newObjectType(ObjectFlagsEvolvingArray, nil)
|
|
result.AsEvolvingArrayType().elementType = elementType
|
|
c.cachedTypes[key] = result
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (c *Checker) getElementTypeOfEvolvingArrayType(t *Type) *Type {
|
|
if t.objectFlags&ObjectFlagsEvolvingArray != 0 {
|
|
return t.AsEvolvingArrayType().elementType
|
|
}
|
|
return c.neverType
|
|
}
|
|
|
|
func isEvolvingArrayTypeList(types []*Type) bool {
|
|
hasEvolvingArrayType := false
|
|
for _, t := range types {
|
|
if t.flags&TypeFlagsNever == 0 {
|
|
if t.objectFlags&ObjectFlagsEvolvingArray == 0 {
|
|
return false
|
|
}
|
|
hasEvolvingArrayType = true
|
|
}
|
|
}
|
|
return hasEvolvingArrayType
|
|
}
|
|
|
|
// Return true if the given node is 'x' in an 'x.length', x.push(value)', 'x.unshift(value)' or
|
|
// 'x[n] = value' operation, where 'n' is an expression of type any, undefined, or a number-like type.
|
|
func (c *Checker) isEvolvingArrayOperationTarget(node *ast.Node) bool {
|
|
root := c.getReferenceRoot(node)
|
|
parent := root.Parent
|
|
isLengthPushOrUnshift := ast.IsPropertyAccessExpression(parent) && (parent.Name().Text() == "length" ||
|
|
ast.IsCallExpression(parent.Parent) && ast.IsIdentifier(parent.Name()) && ast.IsPushOrUnshiftIdentifier(parent.Name()))
|
|
isElementAssignment := ast.IsElementAccessExpression(parent) && parent.Expression() == root &&
|
|
ast.IsBinaryExpression(parent.Parent) && parent.Parent.AsBinaryExpression().OperatorToken.Kind == ast.KindEqualsToken &&
|
|
parent.Parent.AsBinaryExpression().Left == parent && !ast.IsAssignmentTarget(parent.Parent) &&
|
|
c.isTypeAssignableToKind(c.getTypeOfExpression(parent.AsElementAccessExpression().ArgumentExpression), TypeFlagsNumberLike)
|
|
return isLengthPushOrUnshift || isElementAssignment
|
|
}
|
|
|
|
// When adding evolving array element types we do not perform subtype reduction. Instead,
|
|
// we defer subtype reduction until the evolving array type is finalized into a manifest
|
|
// array type.
|
|
func (c *Checker) addEvolvingArrayElementType(evolvingArrayType *Type, node *ast.Node) *Type {
|
|
newElementType := c.getRegularTypeOfObjectLiteral(c.getBaseTypeOfLiteralType(c.getContextFreeTypeOfExpression(node)))
|
|
elementType := evolvingArrayType.AsEvolvingArrayType().elementType
|
|
if c.isTypeSubsetOf(newElementType, elementType) {
|
|
return evolvingArrayType
|
|
}
|
|
return c.getEvolvingArrayType(c.getUnionType([]*Type{elementType, newElementType}))
|
|
}
|
|
|
|
func (c *Checker) finalizeEvolvingArrayType(t *Type) *Type {
|
|
if t.objectFlags&ObjectFlagsEvolvingArray != 0 {
|
|
return c.getFinalArrayType(t.AsEvolvingArrayType())
|
|
}
|
|
return t
|
|
}
|
|
|
|
func (c *Checker) getFinalArrayType(t *EvolvingArrayType) *Type {
|
|
if t.finalArrayType == nil {
|
|
t.finalArrayType = c.createFinalArrayType(t.elementType)
|
|
}
|
|
return t.finalArrayType
|
|
}
|
|
|
|
func (c *Checker) createFinalArrayType(elementType *Type) *Type {
|
|
switch {
|
|
case elementType.flags&TypeFlagsNever != 0:
|
|
return c.autoArrayType
|
|
case elementType.flags&TypeFlagsUnion != 0:
|
|
return c.createArrayType(c.getUnionTypeEx(elementType.Types(), UnionReductionSubtype, nil, nil))
|
|
}
|
|
return c.createArrayType(elementType)
|
|
}
|
|
|
|
func (c *Checker) reportFlowControlError(node *ast.Node) {
|
|
block := ast.FindAncestor(node, ast.IsFunctionOrModuleBlock)
|
|
sourceFile := ast.GetSourceFileOfNode(node)
|
|
span := scanner.GetRangeOfTokenAtPosition(sourceFile, ast.GetStatementsOfBlock(block).Pos())
|
|
c.diagnostics.Add(ast.NewDiagnostic(sourceFile, span, diagnostics.The_containing_function_or_module_body_is_too_large_for_control_flow_analysis))
|
|
}
|
|
|
|
func (c *Checker) isMatchingReference(source *ast.Node, target *ast.Node) bool {
|
|
switch target.Kind {
|
|
case ast.KindParenthesizedExpression, ast.KindNonNullExpression:
|
|
return c.isMatchingReference(source, target.Expression())
|
|
case ast.KindBinaryExpression:
|
|
return ast.IsAssignmentExpression(target, false) && c.isMatchingReference(source, target.AsBinaryExpression().Left) ||
|
|
ast.IsBinaryExpression(target) && target.AsBinaryExpression().OperatorToken.Kind == ast.KindCommaToken &&
|
|
c.isMatchingReference(source, target.AsBinaryExpression().Right)
|
|
}
|
|
switch source.Kind {
|
|
case ast.KindMetaProperty:
|
|
return ast.IsMetaProperty(target) && source.AsMetaProperty().KeywordToken == target.AsMetaProperty().KeywordToken && source.Name().Text() == target.Name().Text()
|
|
case ast.KindIdentifier, ast.KindPrivateIdentifier:
|
|
if ast.IsThisInTypeQuery(source) {
|
|
return target.Kind == ast.KindThisKeyword
|
|
}
|
|
return ast.IsIdentifier(target) && c.getResolvedSymbol(source) == c.getResolvedSymbol(target) ||
|
|
(ast.IsVariableDeclaration(target) || ast.IsBindingElement(target)) && c.getExportSymbolOfValueSymbolIfExported(c.getResolvedSymbol(source)) == c.getSymbolOfDeclaration(target)
|
|
case ast.KindThisKeyword:
|
|
return target.Kind == ast.KindThisKeyword
|
|
case ast.KindSuperKeyword:
|
|
return target.Kind == ast.KindSuperKeyword
|
|
case ast.KindNonNullExpression, ast.KindParenthesizedExpression, ast.KindSatisfiesExpression:
|
|
return c.isMatchingReference(source.Expression(), target)
|
|
case ast.KindPropertyAccessExpression, ast.KindElementAccessExpression:
|
|
if sourcePropertyName, ok := c.getAccessedPropertyName(source); ok {
|
|
if ast.IsAccessExpression(target) {
|
|
if targetPropertyName, ok := c.getAccessedPropertyName(target); ok {
|
|
return targetPropertyName == sourcePropertyName && c.isMatchingReference(source.Expression(), target.Expression())
|
|
}
|
|
}
|
|
}
|
|
if ast.IsElementAccessExpression(source) && ast.IsElementAccessExpression(target) {
|
|
sourceArg := source.AsElementAccessExpression().ArgumentExpression
|
|
targetArg := target.AsElementAccessExpression().ArgumentExpression
|
|
if ast.IsIdentifier(sourceArg) && ast.IsIdentifier(targetArg) {
|
|
symbol := c.getResolvedSymbol(sourceArg)
|
|
if symbol == c.getResolvedSymbol(targetArg) && (c.isConstantVariable(symbol) || c.isParameterOrMutableLocalVariable(symbol) && !c.isSymbolAssigned(symbol)) {
|
|
return c.isMatchingReference(source.Expression(), target.Expression())
|
|
}
|
|
}
|
|
}
|
|
case ast.KindQualifiedName:
|
|
if ast.IsAccessExpression(target) {
|
|
if targetPropertyName, ok := c.getAccessedPropertyName(target); ok {
|
|
return source.AsQualifiedName().Right.Text() == targetPropertyName && c.isMatchingReference(source.AsQualifiedName().Left, target.Expression())
|
|
}
|
|
}
|
|
case ast.KindBinaryExpression:
|
|
return ast.IsBinaryExpression(source) && source.AsBinaryExpression().OperatorToken.Kind == ast.KindCommaToken && c.isMatchingReference(source.AsBinaryExpression().Right, target)
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Return the flow cache key for a "dotted name" (i.e. a sequence of identifiers
|
|
// separated by dots). The key consists of the id of the symbol referenced by the
|
|
// leftmost identifier followed by zero or more property names separated by dots.
|
|
// The result is an empty string if the reference isn't a dotted name.
|
|
func (c *Checker) getFlowReferenceKey(f *FlowState) string {
|
|
var b KeyBuilder
|
|
if c.writeFlowCacheKey(&b, f.reference, f.declaredType, f.initialType, f.flowContainer) {
|
|
return b.String()
|
|
}
|
|
return "?" // Reference isn't a dotted name
|
|
}
|
|
|
|
func (c *Checker) writeFlowCacheKey(b *KeyBuilder, node *ast.Node, declaredType *Type, initialType *Type, flowContainer *ast.Node) bool {
|
|
switch node.Kind {
|
|
case ast.KindIdentifier:
|
|
if !ast.IsThisInTypeQuery(node) {
|
|
symbol := c.getResolvedSymbol(node)
|
|
if symbol == c.unknownSymbol {
|
|
return false
|
|
}
|
|
b.WriteSymbol(symbol)
|
|
}
|
|
fallthrough
|
|
case ast.KindThisKeyword:
|
|
b.WriteByte(':')
|
|
b.WriteType(declaredType)
|
|
if initialType != declaredType {
|
|
b.WriteByte('=')
|
|
b.WriteType(initialType)
|
|
}
|
|
if flowContainer != nil {
|
|
b.WriteByte('@')
|
|
b.WriteNode(flowContainer)
|
|
}
|
|
return true
|
|
case ast.KindNonNullExpression, ast.KindParenthesizedExpression:
|
|
return c.writeFlowCacheKey(b, node.Expression(), declaredType, initialType, flowContainer)
|
|
case ast.KindQualifiedName:
|
|
if !c.writeFlowCacheKey(b, node.AsQualifiedName().Left, declaredType, initialType, flowContainer) {
|
|
return false
|
|
}
|
|
b.WriteByte('.')
|
|
b.WriteString(node.AsQualifiedName().Right.Text())
|
|
return true
|
|
case ast.KindPropertyAccessExpression, ast.KindElementAccessExpression:
|
|
if propName, ok := c.getAccessedPropertyName(node); ok {
|
|
if !c.writeFlowCacheKey(b, node.Expression(), declaredType, initialType, flowContainer) {
|
|
return false
|
|
}
|
|
b.WriteByte('.')
|
|
b.WriteString(propName)
|
|
return true
|
|
}
|
|
if ast.IsElementAccessExpression(node) && ast.IsIdentifier(node.AsElementAccessExpression().ArgumentExpression) {
|
|
symbol := c.getResolvedSymbol(node.AsElementAccessExpression().ArgumentExpression)
|
|
if c.isConstantVariable(symbol) || c.isParameterOrMutableLocalVariable(symbol) && !c.isSymbolAssigned(symbol) {
|
|
if !c.writeFlowCacheKey(b, node.Expression(), declaredType, initialType, flowContainer) {
|
|
return false
|
|
}
|
|
b.WriteString(".@")
|
|
b.WriteSymbol(symbol)
|
|
return true
|
|
}
|
|
}
|
|
case ast.KindObjectBindingPattern, ast.KindArrayBindingPattern, ast.KindFunctionDeclaration,
|
|
ast.KindFunctionExpression, ast.KindArrowFunction, ast.KindMethodDeclaration:
|
|
b.WriteNode(node)
|
|
b.WriteByte('#')
|
|
b.WriteType(declaredType)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (c *Checker) getAccessedPropertyName(access *ast.Node) (string, bool) {
|
|
if ast.IsPropertyAccessExpression(access) {
|
|
return access.Name().Text(), true
|
|
}
|
|
if ast.IsElementAccessExpression(access) {
|
|
return c.tryGetElementAccessExpressionName(access.AsElementAccessExpression())
|
|
}
|
|
if ast.IsBindingElement(access) {
|
|
return c.getDestructuringPropertyName(access)
|
|
}
|
|
if ast.IsParameter(access) {
|
|
return strconv.Itoa(slices.Index(access.Parent.Parameters(), access)), true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func (c *Checker) tryGetElementAccessExpressionName(node *ast.ElementAccessExpression) (string, bool) {
|
|
switch {
|
|
case ast.IsStringOrNumericLiteralLike(node.ArgumentExpression):
|
|
return node.ArgumentExpression.Text(), true
|
|
case ast.IsEntityNameExpression(node.ArgumentExpression):
|
|
return c.tryGetNameFromEntityNameExpression(node.ArgumentExpression)
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func (c *Checker) tryGetNameFromEntityNameExpression(node *ast.Node) (string, bool) {
|
|
symbol := c.resolveEntityName(node, ast.SymbolFlagsValue, true /*ignoreErrors*/, false, nil)
|
|
if symbol == nil || !(c.isConstantVariable(symbol) || (symbol.Flags&ast.SymbolFlagsEnumMember != 0)) {
|
|
return "", false
|
|
}
|
|
declaration := symbol.ValueDeclaration
|
|
if declaration == nil {
|
|
return "", false
|
|
}
|
|
t := c.tryGetTypeFromTypeNode(declaration)
|
|
if t != nil {
|
|
if name, ok := tryGetNameFromType(t); ok {
|
|
return name, true
|
|
}
|
|
}
|
|
if hasOnlyExpressionInitializer(declaration) && c.isBlockScopedNameDeclaredBeforeUse(declaration, node) {
|
|
initializer := declaration.Initializer()
|
|
if initializer != nil {
|
|
var initializerType *Type
|
|
if ast.IsBindingPattern(declaration.Parent) {
|
|
initializerType = c.getTypeForBindingElement(declaration)
|
|
} else {
|
|
initializerType = c.getTypeOfExpression(initializer)
|
|
}
|
|
if initializerType != nil {
|
|
return tryGetNameFromType(initializerType)
|
|
}
|
|
} else if ast.IsEnumMember(declaration) {
|
|
return ast.TryGetTextOfPropertyName(declaration.Name())
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func tryGetNameFromType(t *Type) (string, bool) {
|
|
switch {
|
|
case t.flags&TypeFlagsUniqueESSymbol != 0:
|
|
return t.AsUniqueESSymbolType().name, true
|
|
case t.flags&TypeFlagsStringOrNumberLiteral != 0:
|
|
return evaluator.AnyToString(t.AsLiteralType().value), true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func (c *Checker) getDestructuringPropertyName(node *ast.Node) (string, bool) {
|
|
parent := node.Parent
|
|
if ast.IsBindingElement(node) && ast.IsObjectBindingPattern(parent) {
|
|
return c.getLiteralPropertyNameText(getBindingElementPropertyName(node))
|
|
}
|
|
if ast.IsPropertyAssignment(node) || ast.IsShorthandPropertyAssignment(node) {
|
|
return c.getLiteralPropertyNameText(node.Name())
|
|
}
|
|
if ast.IsArrayBindingPattern(parent) {
|
|
return strconv.Itoa(slices.Index(parent.AsBindingPattern().Elements.Nodes, node)), true
|
|
}
|
|
if ast.IsArrayLiteralExpression(parent) {
|
|
return strconv.Itoa(slices.Index(parent.AsArrayLiteralExpression().Elements.Nodes, node)), true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func (c *Checker) getLiteralPropertyNameText(name *ast.Node) (string, bool) {
|
|
t := c.getLiteralTypeFromPropertyName(name)
|
|
if t.flags&(TypeFlagsStringLiteral|TypeFlagsNumberLiteral) != 0 {
|
|
return evaluator.AnyToString(t.AsLiteralType().value), true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func (c *Checker) isConstantReference(node *ast.Node) bool {
|
|
switch node.Kind {
|
|
case ast.KindThisKeyword:
|
|
return true
|
|
case ast.KindIdentifier:
|
|
if !ast.IsThisInTypeQuery(node) {
|
|
symbol := c.getResolvedSymbol(node)
|
|
return c.isConstantVariable(symbol) || c.isParameterOrMutableLocalVariable(symbol) && !c.isSymbolAssigned(symbol) || symbol.ValueDeclaration != nil && ast.IsFunctionExpression(symbol.ValueDeclaration)
|
|
}
|
|
case ast.KindPropertyAccessExpression, ast.KindElementAccessExpression:
|
|
// The resolvedSymbol property is initialized by checkPropertyAccess or checkElementAccess before we get here.
|
|
if c.isConstantReference(node.Expression()) {
|
|
symbol := c.getResolvedSymbolOrNil(node)
|
|
if symbol != nil {
|
|
return c.isReadonlySymbol(symbol)
|
|
}
|
|
}
|
|
case ast.KindObjectBindingPattern, ast.KindArrayBindingPattern:
|
|
rootDeclaration := ast.GetRootDeclaration(node.Parent)
|
|
if ast.IsParameter(rootDeclaration) || ast.IsVariableDeclaration(rootDeclaration) && ast.IsCatchClause(rootDeclaration.Parent) {
|
|
return !c.isSomeSymbolAssigned(rootDeclaration)
|
|
}
|
|
return ast.IsVariableDeclaration(rootDeclaration) && c.isVarConstLike(rootDeclaration)
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (c *Checker) containsMatchingReference(source *ast.Node, target *ast.Node) bool {
|
|
for ast.IsAccessExpression(source) {
|
|
source = source.Expression()
|
|
if c.isMatchingReference(source, target) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (c *Checker) optionalChainContainsReference(source *ast.Node, target *ast.Node) bool {
|
|
for ast.IsOptionalChain(source) {
|
|
source = source.Expression()
|
|
if c.isMatchingReference(source, target) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (c *Checker) getReferenceCandidate(node *ast.Node) *ast.Node {
|
|
switch node.Kind {
|
|
case ast.KindParenthesizedExpression:
|
|
return c.getReferenceCandidate(node.Expression())
|
|
case ast.KindBinaryExpression:
|
|
switch node.AsBinaryExpression().OperatorToken.Kind {
|
|
case ast.KindEqualsToken, ast.KindBarBarEqualsToken, ast.KindAmpersandAmpersandEqualsToken, ast.KindQuestionQuestionEqualsToken:
|
|
return c.getReferenceCandidate(node.AsBinaryExpression().Left)
|
|
case ast.KindCommaToken:
|
|
return c.getReferenceCandidate(node.AsBinaryExpression().Right)
|
|
}
|
|
}
|
|
return node
|
|
}
|
|
|
|
func (c *Checker) getReferenceRoot(node *ast.Node) *ast.Node {
|
|
parent := node.Parent
|
|
if ast.IsParenthesizedExpression(parent) ||
|
|
ast.IsBinaryExpression(parent) && parent.AsBinaryExpression().OperatorToken.Kind == ast.KindEqualsToken && parent.AsBinaryExpression().Left == node ||
|
|
ast.IsBinaryExpression(parent) && parent.AsBinaryExpression().OperatorToken.Kind == ast.KindCommaToken && parent.AsBinaryExpression().Right == node {
|
|
return c.getReferenceRoot(parent)
|
|
}
|
|
return node
|
|
}
|
|
|
|
func (c *Checker) hasMatchingArgument(expression *ast.Node, reference *ast.Node) bool {
|
|
for _, argument := range expression.Arguments() {
|
|
if c.isOrContainsMatchingReference(reference, argument) || c.optionalChainContainsReference(argument, reference) {
|
|
return true
|
|
}
|
|
}
|
|
if ast.IsPropertyAccessExpression(expression.Expression()) && c.isOrContainsMatchingReference(reference, expression.Expression().Expression()) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (c *Checker) isOrContainsMatchingReference(source *ast.Node, target *ast.Node) bool {
|
|
return c.isMatchingReference(source, target) || c.containsMatchingReference(source, target)
|
|
}
|
|
|
|
// Return a new type in which occurrences of the string, number and bigint primitives and placeholder template
|
|
// literal types in typeWithPrimitives have been replaced with occurrences of compatible and more specific types
|
|
// from typeWithLiterals. This is essentially a limited form of intersection between the two types. We avoid a
|
|
// true intersection because it is more costly and, when applied to union types, generates a large number of
|
|
// types we don't actually care about.
|
|
func (c *Checker) replacePrimitivesWithLiterals(typeWithPrimitives *Type, typeWithLiterals *Type) *Type {
|
|
if c.maybeTypeOfKind(typeWithPrimitives, TypeFlagsString|TypeFlagsTemplateLiteral|TypeFlagsNumber|TypeFlagsBigInt) &&
|
|
c.maybeTypeOfKind(typeWithLiterals, TypeFlagsStringLiteral|TypeFlagsTemplateLiteral|TypeFlagsStringMapping|TypeFlagsNumberLiteral|TypeFlagsBigIntLiteral) {
|
|
return c.mapType(typeWithPrimitives, func(t *Type) *Type {
|
|
switch {
|
|
case t.flags&TypeFlagsString != 0:
|
|
return c.extractTypesOfKind(typeWithLiterals, TypeFlagsString|TypeFlagsStringLiteral|TypeFlagsTemplateLiteral|TypeFlagsStringMapping)
|
|
case c.isPatternLiteralType(t) && !c.maybeTypeOfKind(typeWithLiterals, TypeFlagsString|TypeFlagsTemplateLiteral|TypeFlagsStringMapping):
|
|
return c.extractTypesOfKind(typeWithLiterals, TypeFlagsStringLiteral)
|
|
case t.flags&TypeFlagsNumber != 0:
|
|
return c.extractTypesOfKind(typeWithLiterals, TypeFlagsNumber|TypeFlagsNumberLiteral)
|
|
case t.flags&TypeFlagsBigInt != 0:
|
|
return c.extractTypesOfKind(typeWithLiterals, TypeFlagsBigInt|TypeFlagsBigIntLiteral)
|
|
default:
|
|
return t
|
|
}
|
|
})
|
|
}
|
|
return typeWithPrimitives
|
|
}
|
|
|
|
func isCoercibleUnderDoubleEquals(source *Type, target *Type) bool {
|
|
return source.flags&(TypeFlagsNumber|TypeFlagsString|TypeFlagsBooleanLiteral) != 0 &&
|
|
target.flags&(TypeFlagsNumber|TypeFlagsString|TypeFlagsBoolean) != 0
|
|
}
|
|
|
|
func (c *Checker) isExhaustiveSwitchStatement(node *ast.Node) bool {
|
|
links := c.switchStatementLinks.Get(node)
|
|
if links.exhaustiveState == ExhaustiveStateUnknown {
|
|
// Indicate resolution is in process
|
|
links.exhaustiveState = ExhaustiveStateComputing
|
|
isExhaustive := c.computeExhaustiveSwitchStatement(node)
|
|
if links.exhaustiveState == ExhaustiveStateComputing {
|
|
links.exhaustiveState = core.IfElse(isExhaustive, ExhaustiveStateTrue, ExhaustiveStateFalse)
|
|
}
|
|
} else if links.exhaustiveState == ExhaustiveStateComputing {
|
|
// Resolve circularity to false
|
|
links.exhaustiveState = ExhaustiveStateFalse
|
|
}
|
|
return links.exhaustiveState == ExhaustiveStateTrue
|
|
}
|
|
|
|
func (c *Checker) computeExhaustiveSwitchStatement(node *ast.Node) bool {
|
|
if ast.IsTypeOfExpression(node.Expression()) {
|
|
witnesses := c.getSwitchClauseTypeOfWitnesses(node)
|
|
if witnesses == nil {
|
|
return false
|
|
}
|
|
operandConstraint := c.getBaseConstraintOrType(c.checkExpressionCached(node.Expression().Expression()))
|
|
// Get the not-equal flags for all handled cases.
|
|
notEqualFacts := c.getNotEqualFactsFromTypeofSwitch(0, 0, witnesses)
|
|
if operandConstraint.flags&TypeFlagsAnyOrUnknown != 0 {
|
|
// We special case the top types to be exhaustive when all cases are handled.
|
|
return TypeFactsAllTypeofNE¬EqualFacts == TypeFactsAllTypeofNE
|
|
}
|
|
// A missing not-equal flag indicates that the type wasn't handled by some case.
|
|
return !someType(operandConstraint, func(t *Type) bool {
|
|
return c.getTypeFacts(t, notEqualFacts) == notEqualFacts
|
|
})
|
|
}
|
|
t := c.getBaseConstraintOrType(c.checkExpressionCached(node.Expression()))
|
|
if !isLiteralType(t) {
|
|
return false
|
|
}
|
|
switchTypes := c.getSwitchClauseTypes(node)
|
|
if len(switchTypes) == 0 || core.Some(switchTypes, isNeitherUnitTypeNorNever) {
|
|
return false
|
|
}
|
|
return c.eachTypeContainedIn(c.mapType(t, c.getRegularTypeOfLiteralType), switchTypes)
|
|
}
|
|
|
|
func (c *Checker) eachTypeContainedIn(source *Type, types []*Type) bool {
|
|
if source.flags&TypeFlagsUnion != 0 {
|
|
return !core.Some(source.AsUnionType().types, func(t *Type) bool {
|
|
return !slices.Contains(types, t)
|
|
})
|
|
}
|
|
return slices.Contains(types, source)
|
|
}
|
|
|
|
// Get the type names from all cases in a switch on `typeof`. The default clause and/or duplicate type names are
|
|
// represented as empty strings. Return nil if one or more case clause expressions are not string literals.
|
|
func (c *Checker) getSwitchClauseTypeOfWitnesses(node *ast.Node) []string {
|
|
links := c.switchStatementLinks.Get(node)
|
|
if !links.witnessesComputed {
|
|
clauses := node.AsSwitchStatement().CaseBlock.AsCaseBlock().Clauses.Nodes
|
|
witnesses := make([]string, len(clauses))
|
|
for i, clause := range clauses {
|
|
if clause.Kind == ast.KindCaseClause {
|
|
var text string
|
|
if ast.IsStringLiteralLike(clause.Expression()) {
|
|
text = clause.Expression().Text()
|
|
}
|
|
if text == "" {
|
|
witnesses = nil
|
|
break
|
|
}
|
|
if !slices.Contains(witnesses, text) {
|
|
witnesses[i] = text
|
|
}
|
|
}
|
|
}
|
|
links.witnesses = witnesses
|
|
links.witnessesComputed = true
|
|
}
|
|
return links.witnesses
|
|
}
|
|
|
|
// Return the combined not-equal type facts for all cases except those between the start and end indices.
|
|
func (c *Checker) getNotEqualFactsFromTypeofSwitch(start int, end int, witnesses []string) TypeFacts {
|
|
var facts TypeFacts = TypeFactsNone
|
|
for i, witness := range witnesses {
|
|
if (i < start || i >= end) && witness != "" {
|
|
f, ok := typeofNEFacts[witness]
|
|
if !ok {
|
|
f = TypeFactsTypeofNEHostObject
|
|
}
|
|
facts |= f
|
|
}
|
|
}
|
|
return facts
|
|
}
|
|
|
|
func (c *Checker) getSwitchClauseTypes(node *ast.Node) []*Type {
|
|
links := c.switchStatementLinks.Get(node)
|
|
if !links.switchTypesComputed {
|
|
clauses := node.AsSwitchStatement().CaseBlock.AsCaseBlock().Clauses.Nodes
|
|
types := make([]*Type, len(clauses))
|
|
for i, clause := range clauses {
|
|
types[i] = c.getTypeOfSwitchClause(clause)
|
|
}
|
|
links.switchTypes = types
|
|
links.switchTypesComputed = true
|
|
}
|
|
return links.switchTypes
|
|
}
|
|
|
|
func (c *Checker) getTypeOfSwitchClause(clause *ast.Node) *Type {
|
|
if clause.Kind == ast.KindCaseClause {
|
|
return c.getRegularTypeOfLiteralType(c.getTypeOfExpression(clause.Expression()))
|
|
}
|
|
return c.neverType
|
|
}
|
|
|
|
func (c *Checker) getEffectsSignature(node *ast.Node) *Signature {
|
|
links := c.signatureLinks.Get(node)
|
|
signature := links.effectsSignature
|
|
if signature == nil {
|
|
// A call expression parented by an expression statement is a potential assertion. Other call
|
|
// expressions are potential type predicate function calls. In order to avoid triggering
|
|
// circularities in control flow analysis, we use getTypeOfDottedName when resolving the call
|
|
// target expression of an assertion.
|
|
var funcType *Type
|
|
if ast.IsBinaryExpression(node) {
|
|
rightType := c.checkNonNullExpression(node.AsBinaryExpression().Right)
|
|
funcType = c.getSymbolHasInstanceMethodOfObjectType(rightType)
|
|
} else if ast.IsExpressionStatement(node.Parent) {
|
|
funcType = c.getTypeOfDottedName(node.Expression(), nil /*diagnostic*/)
|
|
} else if node.Expression().Kind != ast.KindSuperKeyword {
|
|
if ast.IsOptionalChain(node) {
|
|
funcType = c.checkNonNullType(c.getOptionalExpressionType(c.checkExpression(node.Expression()), node.Expression()), node.Expression())
|
|
} else {
|
|
funcType = c.checkNonNullExpression(node.Expression())
|
|
}
|
|
}
|
|
var apparentType *Type
|
|
if funcType != nil {
|
|
apparentType = c.getApparentType(funcType)
|
|
}
|
|
signatures := c.getSignaturesOfType(core.OrElse(apparentType, c.unknownType), SignatureKindCall)
|
|
switch {
|
|
case len(signatures) == 1 && len(signatures[0].typeParameters) == 0:
|
|
signature = signatures[0]
|
|
case core.Some(signatures, c.hasTypePredicateOrNeverReturnType):
|
|
signature = c.getResolvedSignature(node, nil, CheckModeNormal)
|
|
}
|
|
if !(signature != nil && c.hasTypePredicateOrNeverReturnType(signature)) {
|
|
signature = c.unknownSignature
|
|
}
|
|
links.effectsSignature = signature
|
|
}
|
|
if signature == c.unknownSignature {
|
|
return nil
|
|
}
|
|
return signature
|
|
}
|
|
|
|
/**
|
|
* Get the type of the `[Symbol.hasInstance]` method of an object type.
|
|
*/
|
|
func (c *Checker) getSymbolHasInstanceMethodOfObjectType(t *Type) *Type {
|
|
hasInstancePropertyName := c.getPropertyNameForKnownSymbolName("hasInstance")
|
|
if c.allTypesAssignableToKind(t, TypeFlagsNonPrimitive) {
|
|
hasInstanceProperty := c.getPropertyOfType(t, hasInstancePropertyName)
|
|
if hasInstanceProperty != nil {
|
|
hasInstancePropertyType := c.getTypeOfSymbol(hasInstanceProperty)
|
|
if hasInstancePropertyType != nil && len(c.getSignaturesOfType(hasInstancePropertyType, SignatureKindCall)) != 0 {
|
|
return hasInstancePropertyType
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Checker) getPropertyNameForKnownSymbolName(symbolName string) string {
|
|
ctorType := c.getGlobalESSymbolConstructorSymbolOrNil()
|
|
if ctorType != nil {
|
|
uniqueType := c.getTypeOfPropertyOfType(c.getTypeOfSymbol(ctorType), symbolName)
|
|
if uniqueType != nil && isTypeUsableAsPropertyName(uniqueType) {
|
|
return getPropertyNameFromType(uniqueType)
|
|
}
|
|
}
|
|
return ast.InternalSymbolNamePrefix + "@" + symbolName
|
|
}
|
|
|
|
// We require the dotted function name in an assertion expression to be comprised of identifiers
|
|
// that reference function, method, class or value module symbols; or variable, property or
|
|
// parameter symbols with declarations that have explicit type annotations. Such references are
|
|
// resolvable with no possibility of triggering circularities in control flow analysis.
|
|
func (c *Checker) getTypeOfDottedName(node *ast.Node, diagnostic *ast.Diagnostic) *Type {
|
|
if node.Flags&ast.NodeFlagsInWithStatement == 0 {
|
|
switch node.Kind {
|
|
case ast.KindIdentifier:
|
|
symbol := c.getExportSymbolOfValueSymbolIfExported(c.getResolvedSymbol(node))
|
|
return c.getExplicitTypeOfSymbol(symbol, diagnostic)
|
|
case ast.KindThisKeyword:
|
|
return c.getExplicitThisType(node)
|
|
case ast.KindSuperKeyword:
|
|
return c.checkSuperExpression(node)
|
|
case ast.KindPropertyAccessExpression:
|
|
t := c.getTypeOfDottedName(node.AsPropertyAccessExpression().Expression, diagnostic)
|
|
if t != nil {
|
|
name := node.Name()
|
|
var prop *ast.Symbol
|
|
if ast.IsPrivateIdentifier(name) {
|
|
if t.symbol != nil {
|
|
prop = c.getPropertyOfType(t, binder.GetSymbolNameForPrivateIdentifier(t.symbol, name.Text()))
|
|
}
|
|
} else {
|
|
prop = c.getPropertyOfType(t, name.Text())
|
|
}
|
|
if prop != nil {
|
|
return c.getExplicitTypeOfSymbol(prop, diagnostic)
|
|
}
|
|
}
|
|
case ast.KindParenthesizedExpression:
|
|
return c.getTypeOfDottedName(node.AsParenthesizedExpression().Expression, diagnostic)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Checker) getExplicitTypeOfSymbol(symbol *ast.Symbol, diagnostic *ast.Diagnostic) *Type {
|
|
symbol = c.resolveSymbol(symbol)
|
|
if symbol.Flags&(ast.SymbolFlagsFunction|ast.SymbolFlagsMethod|ast.SymbolFlagsClass|ast.SymbolFlagsValueModule) != 0 {
|
|
return c.getTypeOfSymbol(symbol)
|
|
}
|
|
if symbol.Flags&(ast.SymbolFlagsVariable|ast.SymbolFlagsProperty) != 0 {
|
|
if symbol.CheckFlags&ast.CheckFlagsMapped != 0 {
|
|
origin := c.mappedSymbolLinks.Get(symbol).syntheticOrigin
|
|
if origin != nil && c.getExplicitTypeOfSymbol(origin, diagnostic) != nil {
|
|
return c.getTypeOfSymbol(symbol)
|
|
}
|
|
}
|
|
declaration := symbol.ValueDeclaration
|
|
if declaration != nil {
|
|
if c.isDeclarationWithExplicitTypeAnnotation(declaration) {
|
|
return c.getTypeOfSymbol(symbol)
|
|
}
|
|
if ast.IsVariableDeclaration(declaration) && ast.IsForOfStatement(declaration.Parent.Parent) {
|
|
statement := declaration.Parent.Parent
|
|
expressionType := c.getTypeOfDottedName(statement.Expression(), nil /*diagnostic*/)
|
|
if expressionType != nil {
|
|
var use IterationUse
|
|
if statement.AsForInOrOfStatement().AwaitModifier != nil {
|
|
use = IterationUseForAwaitOf
|
|
} else {
|
|
use = IterationUseForOf
|
|
}
|
|
return c.checkIteratedTypeOrElementType(use, expressionType, c.undefinedType, nil /*errorNode*/)
|
|
}
|
|
}
|
|
if diagnostic != nil {
|
|
diagnostic.AddRelatedInfo(createDiagnosticForNode(declaration, diagnostics.X_0_needs_an_explicit_type_annotation, c.symbolToString(symbol)))
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Checker) isDeclarationWithExplicitTypeAnnotation(node *ast.Node) bool {
|
|
return (ast.IsVariableDeclaration(node) || ast.IsPropertyDeclaration(node) || ast.IsPropertySignatureDeclaration(node) || ast.IsParameter(node)) && node.Type() != nil ||
|
|
c.isExpandoPropertyFunctionWithReturnTypeAnnotation(node)
|
|
}
|
|
|
|
func (c *Checker) isExpandoPropertyFunctionWithReturnTypeAnnotation(node *ast.Node) bool {
|
|
if ast.IsBinaryExpression(node) {
|
|
if expr := node.AsBinaryExpression().Right; ast.IsFunctionLike(expr) && expr.Type() != nil {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (c *Checker) hasTypePredicateOrNeverReturnType(sig *Signature) bool {
|
|
return c.getTypePredicateOfSignature(sig) != nil || sig.declaration != nil && core.OrElse(c.getReturnTypeFromAnnotation(sig.declaration), c.unknownType).flags&TypeFlagsNever != 0
|
|
}
|
|
|
|
func (c *Checker) getExplicitThisType(node *ast.Node) *Type {
|
|
container := ast.GetThisContainer(node, false /*includeArrowFunctions*/, false /*includeClassComputedPropertyName*/)
|
|
if ast.IsFunctionLike(container) {
|
|
signature := c.getSignatureFromDeclaration(container)
|
|
if signature.thisParameter != nil {
|
|
return c.getExplicitTypeOfSymbol(signature.thisParameter, nil)
|
|
}
|
|
}
|
|
if container.Parent != nil && ast.IsClassLike(container.Parent) {
|
|
symbol := c.getSymbolOfDeclaration(container.Parent)
|
|
if ast.IsStatic(container) {
|
|
return c.getTypeOfSymbol(symbol)
|
|
} else {
|
|
return c.getDeclaredTypeOfSymbol(symbol).AsInterfaceType().thisType
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Checker) getInitialType(node *ast.Node) *Type {
|
|
switch node.Kind {
|
|
case ast.KindVariableDeclaration:
|
|
return c.getInitialTypeOfVariableDeclaration(node)
|
|
case ast.KindBindingElement:
|
|
return c.getInitialTypeOfBindingElement(node)
|
|
}
|
|
panic("Unhandled case in getInitialType")
|
|
}
|
|
|
|
func (c *Checker) getInitialTypeOfVariableDeclaration(node *ast.Node) *Type {
|
|
if node.Initializer() != nil {
|
|
return c.getTypeOfInitializer(node.Initializer())
|
|
}
|
|
if ast.IsForInStatement(node.Parent.Parent) {
|
|
return c.stringType
|
|
}
|
|
if ast.IsForOfStatement(node.Parent.Parent) {
|
|
t := c.checkRightHandSideOfForOf(node.Parent.Parent)
|
|
if t != nil {
|
|
return t
|
|
}
|
|
}
|
|
return c.errorType
|
|
}
|
|
|
|
func (c *Checker) getTypeOfInitializer(node *ast.Node) *Type {
|
|
// Return the cached type if one is available. If the type of the variable was inferred
|
|
// from its initializer, we'll already have cached the type. Otherwise we compute it now
|
|
// without caching such that transient types are reflected.
|
|
if c.typeNodeLinks.Has(node) {
|
|
t := c.typeNodeLinks.Get(node).resolvedType
|
|
if t != nil {
|
|
return t
|
|
}
|
|
}
|
|
return c.getTypeOfExpression(node)
|
|
}
|
|
|
|
func (c *Checker) getInitialTypeOfBindingElement(node *ast.Node) *Type {
|
|
pattern := node.Parent
|
|
parentType := c.getInitialType(pattern.Parent)
|
|
var t *Type
|
|
switch {
|
|
case ast.IsObjectBindingPattern(pattern):
|
|
t = c.getTypeOfDestructuredProperty(parentType, getBindingElementPropertyName(node))
|
|
case !hasDotDotDotToken(node):
|
|
t = c.getTypeOfDestructuredArrayElement(parentType, slices.Index(pattern.AsBindingPattern().Elements.Nodes, node))
|
|
default:
|
|
t = c.getTypeOfDestructuredSpreadExpression(parentType)
|
|
}
|
|
return c.getTypeWithDefault(t, node.Initializer())
|
|
}
|
|
|
|
func (c *Checker) getAssignedType(node *ast.Node) *Type {
|
|
parent := node.Parent
|
|
switch parent.Kind {
|
|
case ast.KindForInStatement:
|
|
return c.stringType
|
|
case ast.KindForOfStatement:
|
|
t := c.checkRightHandSideOfForOf(parent)
|
|
if t != nil {
|
|
return t
|
|
}
|
|
case ast.KindBinaryExpression:
|
|
return c.getAssignedTypeOfBinaryExpression(parent)
|
|
case ast.KindDeleteExpression:
|
|
return c.undefinedType
|
|
case ast.KindArrayLiteralExpression:
|
|
return c.getAssignedTypeOfArrayLiteralElement(parent, node)
|
|
case ast.KindSpreadElement:
|
|
return c.getAssignedTypeOfSpreadExpression(parent)
|
|
case ast.KindPropertyAssignment:
|
|
return c.getAssignedTypeOfPropertyAssignment(parent)
|
|
case ast.KindShorthandPropertyAssignment:
|
|
return c.getAssignedTypeOfShorthandPropertyAssignment(parent)
|
|
}
|
|
return c.errorType
|
|
}
|
|
|
|
func (c *Checker) getAssignedTypeOfBinaryExpression(node *ast.Node) *Type {
|
|
isDestructuringDefaultAssignment := ast.IsArrayLiteralExpression(node.Parent) && c.isDestructuringAssignmentTarget(node.Parent) ||
|
|
ast.IsPropertyAssignment(node.Parent) && c.isDestructuringAssignmentTarget(node.Parent.Parent)
|
|
if isDestructuringDefaultAssignment {
|
|
return c.getTypeWithDefault(c.getAssignedType(node), node.AsBinaryExpression().Right)
|
|
}
|
|
return c.getTypeOfExpression(node.AsBinaryExpression().Right)
|
|
}
|
|
|
|
func (c *Checker) getAssignedTypeOfArrayLiteralElement(node *ast.Node, element *ast.Node) *Type {
|
|
return c.getTypeOfDestructuredArrayElement(c.getAssignedType(node), slices.Index(node.AsArrayLiteralExpression().Elements.Nodes, element))
|
|
}
|
|
|
|
func (c *Checker) getTypeOfDestructuredArrayElement(t *Type, index int) *Type {
|
|
if everyType(t, c.isTupleLikeType) {
|
|
if elementType := c.getTupleElementType(t, index); elementType != nil {
|
|
return elementType
|
|
}
|
|
}
|
|
if elementType := c.checkIteratedTypeOrElementType(IterationUseDestructuring, t, c.undefinedType, nil /*errorNode*/); elementType != nil {
|
|
return c.includeUndefinedInIndexSignature(elementType)
|
|
}
|
|
return c.errorType
|
|
}
|
|
|
|
func (c *Checker) includeUndefinedInIndexSignature(t *Type) *Type {
|
|
if c.compilerOptions.NoUncheckedIndexedAccess == core.TSTrue {
|
|
return c.getUnionType([]*Type{t, c.missingType})
|
|
}
|
|
return t
|
|
}
|
|
|
|
func (c *Checker) getAssignedTypeOfSpreadExpression(node *ast.Node) *Type {
|
|
return c.getTypeOfDestructuredSpreadExpression(c.getAssignedType(node.Parent))
|
|
}
|
|
|
|
func (c *Checker) getTypeOfDestructuredSpreadExpression(t *Type) *Type {
|
|
elementType := c.checkIteratedTypeOrElementType(IterationUseDestructuring, t, c.undefinedType, nil /*errorNode*/)
|
|
if elementType == nil {
|
|
elementType = c.errorType
|
|
}
|
|
return c.createArrayType(elementType)
|
|
}
|
|
|
|
func (c *Checker) getAssignedTypeOfPropertyAssignment(node *ast.Node) *Type {
|
|
return c.getTypeOfDestructuredProperty(c.getAssignedType(node.Parent), node.Name())
|
|
}
|
|
|
|
func (c *Checker) getTypeOfDestructuredProperty(t *Type, name *ast.Node) *Type {
|
|
nameType := c.getLiteralTypeFromPropertyName(name)
|
|
if !isTypeUsableAsPropertyName(nameType) {
|
|
return c.errorType
|
|
}
|
|
text := getPropertyNameFromType(nameType)
|
|
if propType := c.getTypeOfPropertyOfType(t, text); propType != nil {
|
|
return propType
|
|
}
|
|
if indexInfo := c.getApplicableIndexInfoForName(t, text); indexInfo != nil {
|
|
return c.includeUndefinedInIndexSignature(indexInfo.valueType)
|
|
}
|
|
return c.errorType
|
|
}
|
|
|
|
func (c *Checker) getAssignedTypeOfShorthandPropertyAssignment(node *ast.Node) *Type {
|
|
return c.getTypeWithDefault(c.getAssignedTypeOfPropertyAssignment(node), node.AsShorthandPropertyAssignment().ObjectAssignmentInitializer)
|
|
}
|
|
|
|
func (c *Checker) isDestructuringAssignmentTarget(parent *ast.Node) bool {
|
|
return ast.IsBinaryExpression(parent.Parent) && parent.Parent.AsBinaryExpression().Left == parent ||
|
|
ast.IsForOfStatement(parent.Parent) && parent.Parent.Initializer() == parent
|
|
}
|
|
|
|
func (c *Checker) getTypeWithDefault(t *Type, defaultExpression *ast.Node) *Type {
|
|
if defaultExpression != nil {
|
|
return c.getUnionType([]*Type{c.getNonUndefinedType(t), c.getTypeOfExpression(defaultExpression)})
|
|
}
|
|
return t
|
|
}
|
|
|
|
// Remove those constituent types of declaredType to which no constituent type of assignedType is assignable.
|
|
// For example, when a variable of type number | string | boolean is assigned a value of type number | boolean,
|
|
// we remove type string.
|
|
func (c *Checker) getAssignmentReducedType(declaredType *Type, assignedType *Type) *Type {
|
|
if declaredType == assignedType {
|
|
return declaredType
|
|
}
|
|
if assignedType.flags&TypeFlagsNever != 0 {
|
|
return assignedType
|
|
}
|
|
key := AssignmentReducedKey{id1: declaredType.id, id2: assignedType.id}
|
|
result := c.assignmentReducedTypes[key]
|
|
if result == nil {
|
|
result = c.getAssignmentReducedTypeWorker(declaredType, assignedType)
|
|
c.assignmentReducedTypes[key] = result
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (c *Checker) getAssignmentReducedTypeWorker(declaredType *Type, assignedType *Type) *Type {
|
|
filteredType := c.filterType(declaredType, func(t *Type) bool {
|
|
return c.typeMaybeAssignableTo(assignedType, t)
|
|
})
|
|
// Ensure that we narrow to fresh types if the assignment is a fresh boolean literal type.
|
|
reducedType := filteredType
|
|
if assignedType.flags&TypeFlagsBooleanLiteral != 0 && isFreshLiteralType(assignedType) {
|
|
reducedType = c.mapType(filteredType, c.getFreshTypeOfLiteralType)
|
|
}
|
|
// Our crude heuristic produces an invalid result in some cases: see GH#26130.
|
|
// For now, when that happens, we give up and don't narrow at all. (This also
|
|
// means we'll never narrow for erroneous assignments where the assigned type
|
|
// is not assignable to the declared type.)
|
|
if c.isTypeAssignableTo(assignedType, reducedType) {
|
|
return reducedType
|
|
}
|
|
return declaredType
|
|
}
|
|
|
|
func (c *Checker) typeMaybeAssignableTo(source *Type, target *Type) bool {
|
|
if source.flags&TypeFlagsUnion == 0 {
|
|
return c.isTypeAssignableTo(source, target)
|
|
}
|
|
for _, t := range source.AsUnionType().types {
|
|
if c.isTypeAssignableTo(t, target) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (c *Checker) getTypePredicateArgument(predicate *TypePredicate, callExpression *ast.Node) *ast.Node {
|
|
if predicate.kind == TypePredicateKindIdentifier || predicate.kind == TypePredicateKindAssertsIdentifier {
|
|
arguments := callExpression.Arguments()
|
|
if int(predicate.parameterIndex) < len(arguments) {
|
|
return arguments[predicate.parameterIndex]
|
|
}
|
|
} else {
|
|
invokedExpression := ast.SkipParentheses(callExpression.Expression())
|
|
if ast.IsAccessExpression(invokedExpression) {
|
|
return ast.SkipParentheses(invokedExpression.Expression())
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Checker) getFlowTypeInConstructor(symbol *ast.Symbol, constructor *ast.Node) *Type {
|
|
var accessName *ast.Node
|
|
if strings.HasPrefix(symbol.Name, ast.InternalSymbolNamePrefix+"#") {
|
|
accessName = c.factory.NewPrivateIdentifier(symbol.Name[strings.Index(symbol.Name, "@")+1:])
|
|
} else {
|
|
accessName = c.factory.NewIdentifier(symbol.Name)
|
|
}
|
|
reference := c.factory.NewPropertyAccessExpression(c.factory.NewKeywordExpression(ast.KindThisKeyword), nil, accessName, ast.NodeFlagsNone)
|
|
reference.Expression().Parent = reference
|
|
reference.Parent = constructor
|
|
reference.FlowNodeData().FlowNode = constructor.AsConstructorDeclaration().ReturnFlowNode
|
|
flowType := c.getFlowTypeOfProperty(reference, symbol)
|
|
if c.noImplicitAny && (flowType == c.autoType || flowType == c.autoArrayType) {
|
|
c.error(symbol.ValueDeclaration, diagnostics.Member_0_implicitly_has_an_1_type, c.symbolToString(symbol), c.TypeToString(flowType))
|
|
}
|
|
// We don't infer a type if assignments are only null or undefined.
|
|
if everyType(flowType, c.IsNullableType) {
|
|
return nil
|
|
}
|
|
return c.convertAutoToAny(flowType)
|
|
}
|
|
|
|
func (c *Checker) getFlowTypeInStaticBlocks(symbol *ast.Symbol, staticBlocks []*ast.Node) *Type {
|
|
var accessName *ast.Node
|
|
if strings.HasPrefix(symbol.Name, ast.InternalSymbolNamePrefix+"#") {
|
|
accessName = c.factory.NewPrivateIdentifier(symbol.Name[strings.Index(symbol.Name, "@")+1:])
|
|
} else {
|
|
accessName = c.factory.NewIdentifier(symbol.Name)
|
|
}
|
|
for _, staticBlock := range staticBlocks {
|
|
reference := c.factory.NewPropertyAccessExpression(c.factory.NewKeywordExpression(ast.KindThisKeyword), nil, accessName, ast.NodeFlagsNone)
|
|
reference.Expression().Parent = reference
|
|
reference.Parent = staticBlock
|
|
reference.FlowNodeData().FlowNode = staticBlock.AsClassStaticBlockDeclaration().ReturnFlowNode
|
|
flowType := c.getFlowTypeOfProperty(reference, symbol)
|
|
if c.noImplicitAny && (flowType == c.autoType || flowType == c.autoArrayType) {
|
|
c.error(symbol.ValueDeclaration, diagnostics.Member_0_implicitly_has_an_1_type, c.symbolToString(symbol), c.TypeToString(flowType))
|
|
}
|
|
// We don't infer a type if assignments are only null or undefined.
|
|
if everyType(flowType, c.IsNullableType) {
|
|
continue
|
|
}
|
|
return c.convertAutoToAny(flowType)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Checker) isReachableFlowNode(flow *ast.FlowNode) bool {
|
|
f := c.getFlowState()
|
|
result := c.isReachableFlowNodeWorker(f, flow, false /*noCacheCheck*/)
|
|
c.putFlowState(f)
|
|
c.lastFlowNode = flow
|
|
c.lastFlowNodeReachable = result
|
|
return result
|
|
}
|
|
|
|
func (c *Checker) isReachableFlowNodeWorker(f *FlowState, flow *ast.FlowNode, noCacheCheck bool) bool {
|
|
for {
|
|
if flow == c.lastFlowNode {
|
|
return c.lastFlowNodeReachable
|
|
}
|
|
flags := flow.Flags
|
|
if flags&ast.FlowFlagsShared != 0 {
|
|
if !noCacheCheck {
|
|
if reachable, ok := c.flowNodeReachable[flow]; ok {
|
|
return reachable
|
|
}
|
|
reachable := c.isReachableFlowNodeWorker(f, flow, true /*noCacheCheck*/)
|
|
c.flowNodeReachable[flow] = reachable
|
|
return reachable
|
|
}
|
|
noCacheCheck = false
|
|
}
|
|
switch {
|
|
case flags&(ast.FlowFlagsAssignment|ast.FlowFlagsCondition|ast.FlowFlagsArrayMutation) != 0:
|
|
flow = flow.Antecedent
|
|
case flags&ast.FlowFlagsCall != 0:
|
|
if signature := c.getEffectsSignature(flow.Node); signature != nil {
|
|
if predicate := c.getTypePredicateOfSignature(signature); predicate != nil && predicate.kind == TypePredicateKindAssertsIdentifier && predicate.t == nil {
|
|
if arguments := flow.Node.Arguments(); int(predicate.parameterIndex) < len(arguments) && c.isFalseExpression(arguments[predicate.parameterIndex]) {
|
|
return false
|
|
}
|
|
}
|
|
if c.getReturnTypeOfSignature(signature).flags&TypeFlagsNever != 0 {
|
|
return false
|
|
}
|
|
}
|
|
flow = flow.Antecedent
|
|
case flags&ast.FlowFlagsBranchLabel != 0:
|
|
// A branching point is reachable if any branch is reachable.
|
|
for list := getBranchLabelAntecedents(flow, f.reduceLabels); list != nil; list = list.Next {
|
|
if c.isReachableFlowNodeWorker(f, list.Flow, false /*noCacheCheck*/) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
case flags&ast.FlowFlagsLoopLabel != 0:
|
|
if flow.Antecedents == nil {
|
|
return false
|
|
}
|
|
// A loop is reachable if the control flow path that leads to the top is reachable.
|
|
flow = flow.Antecedents.Flow
|
|
case flags&ast.FlowFlagsSwitchClause != 0:
|
|
// The control flow path representing an unmatched value in a switch statement with
|
|
// no default clause is unreachable if the switch statement is exhaustive.
|
|
data := flow.Node.AsFlowSwitchClauseData()
|
|
if data.ClauseStart == data.ClauseEnd && c.isExhaustiveSwitchStatement(data.SwitchStatement) {
|
|
return false
|
|
}
|
|
flow = flow.Antecedent
|
|
case flags&ast.FlowFlagsReduceLabel != 0:
|
|
// Cache is unreliable once we start adjusting labels
|
|
c.lastFlowNode = nil
|
|
f.reduceLabels = append(f.reduceLabels, flow.Node.AsFlowReduceLabelData())
|
|
result := c.isReachableFlowNodeWorker(f, flow.Antecedent, false /*noCacheCheck*/)
|
|
f.reduceLabels = f.reduceLabels[:len(f.reduceLabels)-1]
|
|
return result
|
|
default:
|
|
return flags&ast.FlowFlagsUnreachable == 0
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Checker) isFalseExpression(expr *ast.Node) bool {
|
|
node := ast.SkipParentheses(expr)
|
|
if node.Kind == ast.KindFalseKeyword {
|
|
return true
|
|
}
|
|
if ast.IsBinaryExpression(node) {
|
|
binary := node.AsBinaryExpression()
|
|
return binary.OperatorToken.Kind == ast.KindAmpersandAmpersandToken && (c.isFalseExpression(binary.Left) || c.isFalseExpression(binary.Right)) ||
|
|
binary.OperatorToken.Kind == ast.KindBarBarToken && c.isFalseExpression(binary.Left) && c.isFalseExpression(binary.Right)
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Return true if the given flow node is preceded by a 'super(...)' call in every possible code path
|
|
// leading to the node.
|
|
func (c *Checker) isPostSuperFlowNode(flow *ast.FlowNode, noCacheCheck bool) bool {
|
|
f := c.getFlowState()
|
|
result := c.isPostSuperFlowNodeWorker(f, flow, noCacheCheck)
|
|
c.putFlowState(f)
|
|
return result
|
|
}
|
|
|
|
func (c *Checker) isPostSuperFlowNodeWorker(f *FlowState, flow *ast.FlowNode, noCacheCheck bool) bool {
|
|
for {
|
|
flags := flow.Flags
|
|
if flags&ast.FlowFlagsShared != 0 {
|
|
if !noCacheCheck {
|
|
if postSuper, ok := c.flowNodePostSuper[flow]; ok {
|
|
return postSuper
|
|
}
|
|
postSuper := c.isPostSuperFlowNodeWorker(f, flow, true /*noCacheCheck*/)
|
|
c.flowNodePostSuper[flow] = postSuper
|
|
}
|
|
noCacheCheck = false
|
|
}
|
|
switch {
|
|
case flags&(ast.FlowFlagsAssignment|ast.FlowFlagsCondition|ast.FlowFlagsArrayMutation|ast.FlowFlagsSwitchClause) != 0:
|
|
flow = flow.Antecedent
|
|
case flags&ast.FlowFlagsCall != 0:
|
|
if flow.Node.Expression().Kind == ast.KindSuperKeyword {
|
|
return true
|
|
}
|
|
flow = flow.Antecedent
|
|
case flags&ast.FlowFlagsBranchLabel != 0:
|
|
for list := getBranchLabelAntecedents(flow, f.reduceLabels); list != nil; list = list.Next {
|
|
if !c.isPostSuperFlowNodeWorker(f, list.Flow, false /*noCacheCheck*/) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
case flags&ast.FlowFlagsLoopLabel != 0:
|
|
// A loop is post-super if the control flow path that leads to the top is post-super.
|
|
flow = flow.Antecedents.Flow
|
|
case flags&ast.FlowFlagsReduceLabel != 0:
|
|
f.reduceLabels = append(f.reduceLabels, flow.Node.AsFlowReduceLabelData())
|
|
result := c.isPostSuperFlowNodeWorker(f, flow.Antecedent, false /*noCacheCheck*/)
|
|
f.reduceLabels = f.reduceLabels[:len(f.reduceLabels)-1]
|
|
return result
|
|
default:
|
|
// Unreachable nodes are considered post-super to silence errors
|
|
return flags&ast.FlowFlagsUnreachable != 0
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if a parameter, catch variable, or mutable local variable is definitely assigned anywhere
|
|
func (c *Checker) isSymbolAssignedDefinitely(symbol *ast.Symbol) bool {
|
|
c.ensureAssignmentsMarked(symbol)
|
|
return c.markedAssignmentSymbolLinks.Get(symbol).hasDefiniteAssignment
|
|
}
|
|
|
|
// Check if a parameter, catch variable, or mutable local variable is assigned anywhere
|
|
func (c *Checker) isSymbolAssigned(symbol *ast.Symbol) bool {
|
|
c.ensureAssignmentsMarked(symbol)
|
|
return c.markedAssignmentSymbolLinks.Get(symbol).lastAssignmentPos != 0
|
|
}
|
|
|
|
// Return true if there are no assignments to the given symbol or if the given location
|
|
// is past the last assignment to the symbol.
|
|
func (c *Checker) isPastLastAssignment(symbol *ast.Symbol, location *ast.Node) bool {
|
|
c.ensureAssignmentsMarked(symbol)
|
|
lastAssignmentPos := c.markedAssignmentSymbolLinks.Get(symbol).lastAssignmentPos
|
|
return lastAssignmentPos == 0 || location != nil && int(lastAssignmentPos) < location.Pos()
|
|
}
|
|
|
|
func (c *Checker) ensureAssignmentsMarked(symbol *ast.Symbol) {
|
|
if c.markedAssignmentSymbolLinks.Get(symbol).lastAssignmentPos != 0 {
|
|
return
|
|
}
|
|
parent := ast.FindAncestor(symbol.ValueDeclaration, ast.IsFunctionOrSourceFile)
|
|
if parent == nil {
|
|
return
|
|
}
|
|
links := c.nodeLinks.Get(parent)
|
|
if links.flags&NodeCheckFlagsAssignmentsMarked == 0 {
|
|
links.flags |= NodeCheckFlagsAssignmentsMarked
|
|
if !c.hasParentWithAssignmentsMarked(parent) {
|
|
c.markNodeAssignments(parent)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Checker) hasParentWithAssignmentsMarked(node *ast.Node) bool {
|
|
return ast.FindAncestor(node.Parent, func(node *ast.Node) bool {
|
|
return ast.IsFunctionOrSourceFile(node) && c.nodeLinks.Get(node).flags&NodeCheckFlagsAssignmentsMarked != 0
|
|
}) != nil
|
|
}
|
|
|
|
// For all assignments within the given root node, record the last assignment source position for all
|
|
// referenced parameters and mutable local variables. When assignments occur in nested functions or
|
|
// references occur in export specifiers, record math.MaxInt32 as the assignment position. When
|
|
// assignments occur in compound statements, record the ending source position of the compound statement
|
|
// as the assignment position (this is more conservative than full control flow analysis, but requires
|
|
// only a single walk over the AST).
|
|
func (c *Checker) markNodeAssignmentsWorker(node *ast.Node) bool {
|
|
switch node.Kind {
|
|
case ast.KindIdentifier:
|
|
assignmentKind := getAssignmentTargetKind(node)
|
|
if assignmentKind != AssignmentKindNone {
|
|
symbol := c.getResolvedSymbol(node)
|
|
if c.isParameterOrMutableLocalVariable(symbol) {
|
|
links := c.markedAssignmentSymbolLinks.Get(symbol)
|
|
if pos := links.lastAssignmentPos; pos == 0 || pos != math.MaxInt32 {
|
|
referencingFunction := ast.FindAncestor(node, ast.IsFunctionOrSourceFile)
|
|
declaringFunction := ast.FindAncestor(symbol.ValueDeclaration, ast.IsFunctionOrSourceFile)
|
|
if referencingFunction == declaringFunction {
|
|
links.lastAssignmentPos = int32(c.extendAssignmentPosition(node, symbol.ValueDeclaration))
|
|
} else {
|
|
links.lastAssignmentPos = math.MaxInt32
|
|
}
|
|
}
|
|
if assignmentKind == AssignmentKindDefinite {
|
|
links.hasDefiniteAssignment = true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
case ast.KindExportSpecifier:
|
|
exportDeclaration := node.AsExportSpecifier().Parent.Parent.AsExportDeclaration()
|
|
name := node.AsExportSpecifier().PropertyName
|
|
if name == nil {
|
|
name = node.Name()
|
|
}
|
|
if !node.AsExportSpecifier().IsTypeOnly && !exportDeclaration.IsTypeOnly && exportDeclaration.ModuleSpecifier == nil && !ast.IsStringLiteral(name) {
|
|
symbol := c.resolveEntityName(name, ast.SymbolFlagsValue, true /*ignoreErrors*/, true /*dontResolveAlias*/, nil)
|
|
if symbol != nil && c.isParameterOrMutableLocalVariable(symbol) {
|
|
links := c.markedAssignmentSymbolLinks.Get(symbol)
|
|
links.lastAssignmentPos = math.MaxInt32
|
|
}
|
|
}
|
|
return false
|
|
case ast.KindInterfaceDeclaration,
|
|
ast.KindTypeAliasDeclaration,
|
|
ast.KindJSTypeAliasDeclaration,
|
|
ast.KindEnumDeclaration:
|
|
return false
|
|
}
|
|
if ast.IsTypeNode(node) {
|
|
return false
|
|
}
|
|
return node.ForEachChild(c.markNodeAssignments)
|
|
}
|
|
|
|
// Extend the position of the given assignment target node to the end of any intervening variable statement,
|
|
// expression statement, compound statement, or class declaration occurring between the node and the given
|
|
// declaration node.
|
|
func (c *Checker) extendAssignmentPosition(node *ast.Node, declaration *ast.Node) int {
|
|
pos := node.Pos()
|
|
for node != nil && node.Pos() > declaration.Pos() {
|
|
switch node.Kind {
|
|
case ast.KindVariableStatement, ast.KindExpressionStatement, ast.KindIfStatement, ast.KindDoStatement, ast.KindWhileStatement,
|
|
ast.KindForStatement, ast.KindForInStatement, ast.KindForOfStatement, ast.KindWithStatement, ast.KindSwitchStatement,
|
|
ast.KindTryStatement, ast.KindClassDeclaration:
|
|
pos = node.End()
|
|
}
|
|
node = node.Parent
|
|
}
|
|
return pos
|
|
}
|