Skip to content

Add nested call and new expressions as potential intra expression inference sites #54183

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
63 changes: 36 additions & 27 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1756,7 +1756,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const node = getParseTreeNode(nodeIn, isJsxAttributeLike);
return node && getContextualTypeForJsxAttribute(node, /*contextFlags*/ undefined);
},
isContextSensitive,
containsContextSensitive,
getTypeOfPropertyOfContextualType,
getFullyQualifiedName,
getResolvedSignature: (node, candidatesOutArray, argumentCount) => getResolvedSignatureWorker(node, candidatesOutArray, argumentCount, CheckMode.Normal),
Expand Down Expand Up @@ -6136,7 +6136,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

function symbolValueDeclarationIsContextSensitive(symbol: Symbol): boolean {
return symbol && !!symbol.valueDeclaration && isExpression(symbol.valueDeclaration) && !isContextSensitive(symbol.valueDeclaration);
return symbol && !!symbol.valueDeclaration && isExpression(symbol.valueDeclaration) && !containsContextSensitive(symbol.valueDeclaration);
}

function toNodeBuilderFlags(flags = TypeFormatFlags.None): NodeBuilderFlags {
Expand Down Expand Up @@ -12875,7 +12875,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
case SyntaxKind.MappedType:
case SyntaxKind.ConditionalType: {
const outerTypeParameters = getOuterTypeParameters(node, includeThisTypes);
if ((kind === SyntaxKind.FunctionExpression || kind === SyntaxKind.ArrowFunction || isObjectLiteralMethod(node)) && isContextSensitive(node as Expression | MethodDeclaration)) {
if ((kind === SyntaxKind.FunctionExpression || kind === SyntaxKind.ArrowFunction || isObjectLiteralMethod(node)) && containsContextSensitive(node as Expression | MethodDeclaration)) {
const signature = firstOrUndefined(getSignaturesOfType(getTypeOfSymbol(getSymbolOfDeclaration(node as FunctionLikeDeclaration)), SignatureKind.Call));
if (signature && signature.typeParameters) {
return [...(outerTypeParameters || emptyArray), ...signature.typeParameters];
Expand Down Expand Up @@ -20969,47 +20969,56 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return createIndexInfo(info.keyType, instantiateType(info.type, mapper), info.isReadonly, info.declaration, info.components);
}

// Returns true if the given expression contains (at any level of nesting) a function or arrow expression
// that is subject to contextual typing.
function isContextSensitive(node: Expression | MethodDeclaration | ObjectLiteralElementLike | JsxAttributeLike | JsxChild): boolean {
function containsContextRelatedNode(node: Expression | MethodDeclaration | ObjectLiteralElementLike | JsxAttributeLike | JsxChild, predicate: (node: Node) => boolean): boolean {
Debug.assert(node.kind !== SyntaxKind.MethodDeclaration || isObjectLiteralMethod(node));
switch (node.kind) {
case SyntaxKind.FunctionExpression:
case SyntaxKind.ArrowFunction:
case SyntaxKind.MethodDeclaration:
case SyntaxKind.FunctionDeclaration: // Function declarations can have context when annotated with a jsdoc @type
return isContextSensitiveFunctionLikeDeclaration(node as FunctionExpression | ArrowFunction | MethodDeclaration);
case SyntaxKind.FunctionDeclaration:
case SyntaxKind.CallExpression:
case SyntaxKind.NewExpression:
return predicate(node);
case SyntaxKind.ObjectLiteralExpression:
return some((node as ObjectLiteralExpression).properties, isContextSensitive);
return some((node as ObjectLiteralExpression).properties, p => containsContextRelatedNode(p, predicate));
case SyntaxKind.ArrayLiteralExpression:
return some((node as ArrayLiteralExpression).elements, isContextSensitive);
return some((node as ArrayLiteralExpression).elements, e => containsContextRelatedNode(e, predicate));
case SyntaxKind.ConditionalExpression:
return isContextSensitive((node as ConditionalExpression).whenTrue) ||
isContextSensitive((node as ConditionalExpression).whenFalse);
return containsContextRelatedNode((node as ConditionalExpression).whenTrue, predicate) ||
containsContextRelatedNode((node as ConditionalExpression).whenFalse, predicate);
case SyntaxKind.BinaryExpression:
return ((node as BinaryExpression).operatorToken.kind === SyntaxKind.BarBarToken || (node as BinaryExpression).operatorToken.kind === SyntaxKind.QuestionQuestionToken) &&
(isContextSensitive((node as BinaryExpression).left) || isContextSensitive((node as BinaryExpression).right));
(containsContextRelatedNode((node as BinaryExpression).left, predicate) || containsContextRelatedNode((node as BinaryExpression).right, predicate));
case SyntaxKind.PropertyAssignment:
return isContextSensitive((node as PropertyAssignment).initializer);
return containsContextRelatedNode((node as PropertyAssignment).initializer, predicate);
case SyntaxKind.ParenthesizedExpression:
return isContextSensitive((node as ParenthesizedExpression).expression);
return containsContextRelatedNode((node as ParenthesizedExpression).expression, predicate);
case SyntaxKind.JsxAttributes:
return some((node as JsxAttributes).properties, isContextSensitive) || isJsxOpeningElement(node.parent) && some(node.parent.parent.children, isContextSensitive);
return some((node as JsxAttributes).properties, p => containsContextRelatedNode(p, predicate)) || isJsxOpeningElement(node.parent) && some(node.parent.parent.children, c => containsContextRelatedNode(c, predicate));
case SyntaxKind.JsxAttribute: {
// If there is no initializer, JSX attribute has a boolean value of true which is not context sensitive.
const { initializer } = node as JsxAttribute;
return !!initializer && isContextSensitive(initializer);
return !!initializer && containsContextRelatedNode(initializer, predicate);
}
case SyntaxKind.JsxExpression: {
// It is possible to that node.expression is undefined (e.g <div x={} />)
const { expression } = node as JsxExpression;
return !!expression && isContextSensitive(expression);
return !!expression && containsContextRelatedNode(expression, predicate);
}
}

return false;
}

// Returns true if the given expression contains (at any level of nesting) a function or arrow expression
// that is subject to contextual typing.
function containsContextSensitive(node: Expression | MethodDeclaration | ObjectLiteralElementLike | JsxAttributeLike | JsxChild): boolean {
return containsContextRelatedNode(node, isContextSensitiveFunctionOrObjectLiteralMethod);
}

function containsContextSensitiveOrCallOrNewExpression(node: Expression | MethodDeclaration | ObjectLiteralElementLike | JsxAttributeLike | JsxChild): boolean {
return containsContextRelatedNode(node, n => isContextSensitiveFunctionOrObjectLiteralMethod(n) || isCallOrNewExpression(n));
}

function isContextSensitiveFunctionLikeDeclaration(node: FunctionLikeDeclaration): boolean {
return hasContextSensitiveParameters(node) || hasContextSensitiveReturnExpression(node);
}
Expand All @@ -21019,9 +21028,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return false;
}
if (node.body.kind !== SyntaxKind.Block) {
return isContextSensitive(node.body);
return containsContextSensitive(node.body);
}
return !!forEachReturnStatement(node.body as Block, statement => !!statement.expression && isContextSensitive(statement.expression));
return !!forEachReturnStatement(node.body as Block, statement => !!statement.expression && containsContextSensitive(statement.expression));
}

function isContextSensitiveFunctionOrObjectLiteralMethod(func: Node): func is FunctionExpression | ArrowFunction | MethodDeclaration {
Expand Down Expand Up @@ -33091,7 +33100,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const type = checkExpressionForMutableLocation(e, checkMode, forceTuple);
elementTypes.push(addOptionality(type, /*isProperty*/ true, hasOmittedExpression));
elementFlags.push(hasOmittedExpression ? ElementFlags.Optional : ElementFlags.Required);
if (inTupleContext && checkMode && checkMode & CheckMode.Inferential && !(checkMode & CheckMode.SkipContextSensitive) && isContextSensitive(e)) {
if (inTupleContext && checkMode && checkMode & CheckMode.Inferential && !(checkMode & CheckMode.SkipContextSensitive) && containsContextSensitiveOrCallOrNewExpression(e)) {
const inferenceContext = getInferenceContext(node);
Debug.assert(inferenceContext); // In CheckMode.Inferential we should always have an inference context
addIntraExpressionInferenceSite(inferenceContext, e, type);
Expand Down Expand Up @@ -33332,7 +33341,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {

if (
contextualType && checkMode & CheckMode.Inferential && !(checkMode & CheckMode.SkipContextSensitive) &&
(memberDecl.kind === SyntaxKind.PropertyAssignment || memberDecl.kind === SyntaxKind.MethodDeclaration) && isContextSensitive(memberDecl)
(memberDecl.kind === SyntaxKind.PropertyAssignment || memberDecl.kind === SyntaxKind.MethodDeclaration) && containsContextSensitiveOrCallOrNewExpression(memberDecl)
) {
const inferenceContext = getInferenceContext(node);
Debug.assert(inferenceContext); // In CheckMode.Inferential we should always have an inference context
Expand Down Expand Up @@ -33571,7 +33580,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
addDeprecatedSuggestion(attributeDecl.name, prop.declarations, attributeDecl.name.escapedText as string);
}
}
if (contextualType && checkMode & CheckMode.Inferential && !(checkMode & CheckMode.SkipContextSensitive) && isContextSensitive(attributeDecl)) {
if (contextualType && checkMode & CheckMode.Inferential && !(checkMode & CheckMode.SkipContextSensitive) && containsContextSensitiveOrCallOrNewExpression(attributeDecl)) {
const inferenceContext = getInferenceContext(attributes);
Debug.assert(inferenceContext); // In CheckMode.Inferential we should always have an inference context
const inferenceNode = (attributeDecl.initializer as JsxExpression).expression!;
Expand Down Expand Up @@ -36329,7 +36338,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// For a decorator, no arguments are susceptible to contextual typing due to the fact
// decorators are applied to a declaration by the emitter, and not to an expression.
const isSingleNonGenericCandidate = candidates.length === 1 && !candidates[0].typeParameters;
if (!isDecorator && !isSingleNonGenericCandidate && some(args, isContextSensitive)) {
if (!isDecorator && !isSingleNonGenericCandidate && some(args, containsContextSensitive)) {
argCheckMode = CheckMode.SkipContextSensitive;
}

Expand Down Expand Up @@ -39318,7 +39327,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

// The identityMapper object is used to indicate that function expressions are wildcards
if (checkMode && checkMode & CheckMode.SkipContextSensitive && isContextSensitive(node)) {
if (checkMode && checkMode & CheckMode.SkipContextSensitive && containsContextSensitive(node)) {
// Skip parameters, return signature with return type that retains noncontextual parts so inferences can still be drawn in an early stage
if (!getEffectiveReturnTypeNode(node) && !hasContextSensitiveParameters(node)) {
// Return plain anyFunctionType if there is no possibility we'll make inferences from the return type
Expand Down Expand Up @@ -39363,7 +39372,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (!signature) {
return;
}
if (isContextSensitive(node)) {
if (containsContextSensitive(node)) {
if (contextualSignature) {
const inferenceContext = getInferenceContext(node);
let instantiatedContextualSignature: Signature | undefined;
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5216,7 +5216,7 @@ export interface TypeChecker {
/** @internal */ getContextualTypeForObjectLiteralElement(element: ObjectLiteralElementLike): Type | undefined;
/** @internal */ getContextualTypeForArgumentAtIndex(call: CallLikeExpression, argIndex: number): Type | undefined;
/** @internal */ getContextualTypeForJsxAttribute(attribute: JsxAttribute | JsxSpreadAttribute): Type | undefined;
/** @internal */ isContextSensitive(node: Expression | MethodDeclaration | ObjectLiteralElementLike | JsxAttributeLike): boolean;
/** @internal */ containsContextSensitive(node: Expression | MethodDeclaration | ObjectLiteralElementLike | JsxAttributeLike): boolean;
/** @internal */ getTypeOfPropertyOfContextualType(type: Type, name: __String): Type | undefined;

/**
Expand Down
2 changes: 1 addition & 1 deletion src/services/refactors/extractSymbol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1379,7 +1379,7 @@ function extractConstantInScope(
const localNameText = getIdentifierForNode(node, scope, checker, file);
const isJS = isInJSFile(scope);

let variableType = isJS || !checker.isContextSensitive(node)
let variableType = isJS || !checker.containsContextSensitive(node)
? undefined
: checker.typeToTypeNode(checker.getContextualType(node)!, scope, NodeBuilderFlags.NoTruncation, InternalNodeBuilderFlags.AllowUnresolvedNames); // TODO: GH#18217

Expand Down
23 changes: 23 additions & 0 deletions tests/baselines/reference/intraExpressionInferences.errors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -344,4 +344,27 @@ intraExpressionInferences.ts(133,26): error TS2339: Property 'nonexistent' does
},
consumer: (val) => {},
});

type ErrorFn = (error: unknown) => void;

declare const genericFn: <T>(args: {
parser: (p: unknown, errorFn: ErrorFn) => T;
handler: (data: { body: T }) => unknown;
}) => T;

declare const createParser: <T>(arg: T) => (p: unknown, errorFn: ErrorFn) => T;

genericFn({
parser: createParser(1 as const),
handler: ({ body: _ }) => {},
});

declare const genericFnTuple: <T>(
args: [
parser: (p: unknown, errorFn: ErrorFn) => T,
handler: (data: { body: T }) => unknown
]
) => T;

genericFnTuple([createParser(1 as const), ({ body: _ }) => {}]);

46 changes: 46 additions & 0 deletions tests/baselines/reference/intraExpressionInferences.js
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,29 @@ const distantRes = distant({
},
consumer: (val) => {},
});

type ErrorFn = (error: unknown) => void;

declare const genericFn: <T>(args: {
parser: (p: unknown, errorFn: ErrorFn) => T;
handler: (data: { body: T }) => unknown;
}) => T;

declare const createParser: <T>(arg: T) => (p: unknown, errorFn: ErrorFn) => T;

genericFn({
parser: createParser(1 as const),
handler: ({ body: _ }) => {},
});

declare const genericFnTuple: <T>(
args: [
parser: (p: unknown, errorFn: ErrorFn) => T,
handler: (data: { body: T }) => unknown
]
) => T;

genericFnTuple([createParser(1 as const), ({ body: _ }) => {}]);


//// [intraExpressionInferences.js]
Expand Down Expand Up @@ -518,6 +541,15 @@ var distantRes = distant({
},
consumer: function (val) { },
});
genericFn({
parser: createParser(1),
handler: function (_b) {
var _ = _b.body;
},
});
genericFnTuple([createParser(1), function (_b) {
var _ = _b.body;
}]);


//// [intraExpressionInferences.d.ts]
Expand Down Expand Up @@ -648,3 +680,17 @@ declare function distant<T>(args: {
consumer: (val: T) => unknown;
}): T;
declare const distantRes: number;
type ErrorFn = (error: unknown) => void;
declare const genericFn: <T>(args: {
parser: (p: unknown, errorFn: ErrorFn) => T;
handler: (data: {
body: T;
}) => unknown;
}) => T;
declare const createParser: <T>(arg: T) => (p: unknown, errorFn: ErrorFn) => T;
declare const genericFnTuple: <T>(args: [
parser: (p: unknown, errorFn: ErrorFn) => T,
handler: (data: {
body: T;
}) => unknown
]) => T;
Loading