Skip to content
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

[Stage 1] Implements call-this operator #58294

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 119 additions & 1 deletion src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
CallExpression,
CallLikeExpression,
CallSignatureDeclaration,
CallThisExpression,
CancellationToken,
canHaveDecorators,
canHaveExportModifier,
Expand Down Expand Up @@ -489,6 +490,7 @@ import {
isCallLikeOrFunctionLikeExpression,
isCallOrNewExpression,
isCallSignatureDeclaration,
isCallThisExpression,
isCatchClause,
isCatchClauseVariableDeclaration,
isCatchClauseVariableDeclarationOrBindingElement,
Expand Down Expand Up @@ -1664,6 +1666,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
symbolToTypeParameterDeclarations: nodeBuilder.symbolToTypeParameterDeclarations,
symbolToParameterDeclaration: nodeBuilder.symbolToParameterDeclaration,
typeParameterToDeclaration: nodeBuilder.typeParameterToDeclaration,
getCallThisFunctionsForCompletion: (locationIn) => {
const location = getParseTreeNode(locationIn);
return location ? getCallThisFunctionsForCompletion(location) : [];
},
getSymbolsInScope: (locationIn, meaning) => {
const location = getParseTreeNode(locationIn);
return location ? getSymbolsInScope(location, meaning) : [];
Expand Down Expand Up @@ -35428,7 +35434,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// If the called expression is not of the form `x.f` or `x["f"]`, then sourceType = voidType
// If the signature's 'this' type is voidType, then the check is skipped -- anything is compatible.
// If the expression is a new expression or super call expression, then the check is skipped.
const thisArgumentNode = getThisArgumentOfCall(node);
const thisArgumentNode = isCallThisExpression(node) ? node.receiver : getThisArgumentOfCall(node);
const thisArgumentType = getThisArgumentType(thisArgumentNode);
const errorNode = reportErrors ? (thisArgumentNode || node) : undefined;
const headMessage = Diagnostics.The_this_context_of_type_0_is_not_assignable_to_method_s_this_of_type_1;
Expand Down Expand Up @@ -36441,6 +36447,87 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return resolveErrorCall(node);
}

function resolveCallThisExpression(node: CallThisExpression, candidatesOutArray: Signature[] | undefined, checkMode: CheckMode): Signature {
let funcType = checkExpression(node.expression);

funcType = checkNonNullTypeWithReporter(
funcType,
node.expression,
reportCannotInvokePossiblyNullOrUndefinedError,
);

if (funcType === silentNeverType) {
return silentNeverSignature;
}

const apparentType = getApparentType(funcType);
if (isErrorType(apparentType)) {
// Another error has already been reported
return resolveErrorCall(node);
}

// Technically, this signatures list may be incomplete. We are taking the apparent type,
// but we are not including call signatures that may have been added to the Object or
// Function interface, since they have none by default. This is a bit of a leap of faith
// that the user will not add any.
const callSignatures = getSignaturesOfType(apparentType, SignatureKind.Call);
const numConstructSignatures = getSignaturesOfType(apparentType, SignatureKind.Construct).length;

// TS 1.0 Spec: 4.12
// In an untyped function call no TypeArgs are permitted, Args can be any argument list, no contextual
// types are provided for the argument expressions, and the result is always of type Any.
if (isUntypedFunctionCall(funcType, apparentType, callSignatures.length, numConstructSignatures)) {
// The unknownType indicates that an error already occurred (and was reported). No
// need to report another error in this case.
if (!isErrorType(funcType) && node.typeArguments) {
error(node, Diagnostics.Untyped_function_calls_may_not_accept_type_arguments);
}
return resolveUntypedCall(node);
}
// If FuncExpr's apparent type(section 3.8.1) is a function type, the call is a typed function call.
// TypeScript employs overload resolution in typed function calls in order to support functions
// with multiple call signatures.
if (!callSignatures.length) {
if (numConstructSignatures) {
error(node, Diagnostics.Value_of_type_0_is_not_callable_Did_you_mean_to_include_new, typeToString(funcType));
}
else {
let relatedInformation: DiagnosticRelatedInformation | undefined;
if (node.arguments.length === 1) {
const text = getSourceFileOfNode(node).text;
if (isLineBreak(text.charCodeAt(skipTrivia(text, node.expression.end, /*stopAfterLineBreak*/ true) - 1))) {
relatedInformation = createDiagnosticForNode(node.expression, Diagnostics.Are_you_missing_a_semicolon);
}
}
invocationError(node.expression, apparentType, SignatureKind.Call, relatedInformation);
}
return resolveErrorCall(node);
}
// When a call to a generic function is an argument to an outer call to a generic function for which
// inference is in process, we have a choice to make. If the inner call relies on inferences made from
// its contextual type to its return type, deferring the inner call processing allows the best possible
// contextual type to accumulate. But if the outer call relies on inferences made from the return type of
// the inner call, the inner call should be processed early. There's no sure way to know which choice is
// right (only a full unification algorithm can determine that), so we resort to the following heuristic:
// If no type arguments are specified in the inner call and at least one call signature is generic and
// returns a function type, we choose to defer processing. This narrowly permits function composition
// operators to flow inferences through return types, but otherwise processes calls right away. We
// use the resolvingSignature singleton to indicate that we deferred processing. This result will be
// propagated out and eventually turned into silentNeverType (a type that is assignable to anything and
// from which we never make inferences).
if (checkMode & CheckMode.SkipGenericFunctions && !node.typeArguments && callSignatures.some(isGenericFunctionReturningFunction)) {
skippedGenericFunction(node, checkMode);
return resolvingSignature;
}
// If the function is explicitly marked with `@class`, then it must be constructed.
if (callSignatures.some(sig => isInJSFile(sig.declaration) && !!getJSDocClassTag(sig.declaration!))) {
error(node, Diagnostics.Value_of_type_0_is_not_callable_Did_you_mean_to_include_new, typeToString(funcType));
return resolveErrorCall(node);
}

return resolveCall(node, callSignatures, candidatesOutArray, checkMode, SignatureFlags.None);
}

function someSignature(signatures: Signature | readonly Signature[], f: (s: Signature) => boolean): boolean {
if (isArray(signatures)) {
return some(signatures, signature => someSignature(signature, f));
Expand Down Expand Up @@ -36829,6 +36916,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return resolveCallExpression(node, candidatesOutArray, checkMode);
case SyntaxKind.NewExpression:
return resolveNewExpression(node, candidatesOutArray, checkMode);
case SyntaxKind.CallThisExpression:
return resolveCallThisExpression(node, candidatesOutArray, checkMode);
case SyntaxKind.TaggedTemplateExpression:
return resolveTaggedTemplateExpression(node, candidatesOutArray, checkMode);
case SyntaxKind.Decorator:
Expand Down Expand Up @@ -37081,6 +37170,15 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return returnType;
}

function checkCallThisExpression(node: CallThisExpression, checkMode?: CheckMode): Type {
checkGrammarTypeArguments(node, node.typeArguments);

const signature = getResolvedSignature(node, /*candidatesOutArray*/ undefined, checkMode);
const returnType = getReturnTypeOfSignature(signature);

return returnType;
}

function checkDeprecatedSignature(signature: Signature, node: CallLikeExpression) {
if (signature.flags & SignatureFlags.IsSignatureCandidateForOverloadFailure) return;
if (signature.declaration && signature.declaration.flags & NodeFlags.Deprecated) {
Expand Down Expand Up @@ -41032,6 +41130,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// falls through
case SyntaxKind.NewExpression:
return checkCallExpression(node as CallExpression, checkMode);
case SyntaxKind.CallThisExpression:
return checkCallThisExpression(node as CallThisExpression, checkMode);
case SyntaxKind.TaggedTemplateExpression:
return checkTaggedTemplateExpression(node as TaggedTemplateExpression);
case SyntaxKind.ParenthesizedExpression:
Expand Down Expand Up @@ -48643,6 +48743,24 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {

// Language service support

function getCallThisFunctionsForCompletion(location: Node): Symbol[] {
if (!isCallThisExpression(location)) {
return [];
}

const receiverType = getTypeOfExpression(location.receiver);
const symbols = getSymbolsInScope(location, SymbolFlags.None | SymbolFlags.Value);
return filter(symbols, (symbol) => {
const decl = symbol.declarations?.find(d => isFunctionDeclaration(d));
if (!decl) {
return false;
}

const thisType = getThisTypeOfDeclaration(decl);
return thisType ? isTypeRelatedTo(receiverType, thisType, assignableRelation) : false;
})
}

function getSymbolsInScope(location: Node, meaning: SymbolFlags): Symbol[] {
if (location.flags & NodeFlags.InWithStatement) {
// We cannot answer semantic questions within a with block, do not proceed any further
Expand Down
11 changes: 11 additions & 0 deletions src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
Bundle,
CallExpression,
CallSignatureDeclaration,
CallThisExpression,
canHaveLocals,
canIncludeBindAndCheckDiagnostics,
CaseBlock,
Expand Down Expand Up @@ -1926,6 +1927,8 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri
return emitCallExpression(node as CallExpression);
case SyntaxKind.NewExpression:
return emitNewExpression(node as NewExpression);
case SyntaxKind.CallThisExpression:
return emitCallThisExpression(node as CallThisExpression);
case SyntaxKind.TaggedTemplateExpression:
return emitTaggedTemplateExpression(node as TaggedTemplateExpression);
case SyntaxKind.TypeAssertionExpression:
Expand Down Expand Up @@ -2695,6 +2698,14 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri
emitExpressionList(node, node.arguments, ListFormat.NewExpressionArguments, parenthesizer.parenthesizeExpressionForDisallowedComma);
}

function emitCallThisExpression(node: CallThisExpression) {
emitExpression(node.receiver, parenthesizer.parenthesizeLeftSideOfAccess);
emitTokenWithComment(SyntaxKind.TildeGreaterThanToken, node.receiver.end, writePunctuation, node);
emitExpression(node.expression, expr => parenthesizer.parenthesizeRightSideOfBinary(SyntaxKind.TildeGreaterThanToken, node.receiver, expr));
emitTypeArguments(node, node.typeArguments);
emitExpressionList(node, node.arguments, ListFormat.CallExpressionArguments, parenthesizer.parenthesizeExpressionForDisallowedComma);
}

function emitTaggedTemplateExpression(node: TaggedTemplateExpression) {
const indirectCall = getInternalEmitFlags(node) & InternalEmitFlags.IndirectCall;
if (indirectCall) {
Expand Down
41 changes: 41 additions & 0 deletions src/compiler/factory/nodeFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
CallChain,
CallExpression,
CallSignatureDeclaration,
CallThisExpression,
CaseBlock,
CaseClause,
CaseOrDefaultClause,
Expand Down Expand Up @@ -648,6 +649,8 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
updateCallChain,
createNewExpression,
updateNewExpression,
createCallThisExpression,
updateCallThisExpression,
createTaggedTemplateExpression,
updateTaggedTemplateExpression,
createTypeAssertion,
Expand Down Expand Up @@ -3106,6 +3109,44 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
: node;
}

function createBaseCallThisExpression(receiver: LeftHandSideExpression, expression: Expression, typeArguments: NodeArray<TypeNode> | undefined, argumentsArray: NodeArray<Expression>) {
const node = createBaseDeclaration<CallThisExpression>(SyntaxKind.CallThisExpression);
node.receiver = receiver;
node.expression = expression;
node.typeArguments = typeArguments;
node.arguments = argumentsArray;
node.transformFlags |= TransformFlags.ContainsESNext |
propagateChildFlags(node.receiver) |
propagateChildFlags(node.expression) |
propagateChildrenFlags(node.typeArguments) |
propagateChildrenFlags(node.arguments);
if (node.typeArguments) {
node.transformFlags |= TransformFlags.ContainsTypeScript;
}
return node;
}

// @api
function createCallThisExpression(receiver: Expression, expression: Expression, typeArguments: readonly TypeNode[] | undefined, argumentsArray: readonly Expression[] | undefined) {
const node = createBaseCallThisExpression(
parenthesizerRules().parenthesizeLeftSideOfAccess(receiver, /*optionalChain*/ false),
parenthesizerRules().parenthesizeRightSideOfBinary(SyntaxKind.TildeGreaterThanToken, receiver, expression),
asNodeArray(typeArguments),
parenthesizerRules().parenthesizeExpressionsOfCommaDelimitedList(createNodeArray(argumentsArray)),
);
return node;
}

// @api
function updateCallThisExpression(node: CallThisExpression, receiver: Expression, expression: Expression, typeArguments: readonly TypeNode[] | undefined, argumentsArray: readonly Expression[]) {
return node.receiver !== receiver
|| node.expression !== expression
|| node.typeArguments !== typeArguments
|| node.arguments !== argumentsArray
? update(createCallThisExpression(receiver, expression, typeArguments, argumentsArray), node)
: node;
}

// @api
function createTaggedTemplateExpression(tag: Expression, typeArguments: readonly TypeNode[] | undefined, template: TemplateLiteral) {
const node = createBaseNode<TaggedTemplateExpression>(SyntaxKind.TaggedTemplateExpression);
Expand Down
5 changes: 5 additions & 0 deletions src/compiler/factory/nodeTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
Bundle,
CallExpression,
CallSignatureDeclaration,
CallThisExpression,
CaseBlock,
CaseClause,
CaseKeyword,
Expand Down Expand Up @@ -597,6 +598,10 @@ export function isNewExpression(node: Node): node is NewExpression {
return node.kind === SyntaxKind.NewExpression;
}

export function isCallThisExpression(node: Node): node is CallThisExpression {
return node.kind === SyntaxKind.CallThisExpression;
}

export function isTaggedTemplateExpression(node: Node): node is TaggedTemplateExpression {
return node.kind === SyntaxKind.TaggedTemplateExpression;
}
Expand Down
25 changes: 25 additions & 0 deletions src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
BreakStatement,
CallExpression,
CallSignatureDeclaration,
CallThisExpression,
canHaveJSDoc,
canHaveModifiers,
CaseBlock,
Expand Down Expand Up @@ -744,6 +745,7 @@ const forEachChildTable: ForEachChildTable = {
},
[SyntaxKind.CallExpression]: forEachChildInCallOrNewExpression,
[SyntaxKind.NewExpression]: forEachChildInCallOrNewExpression,
[SyntaxKind.CallThisExpression]: forEachChildInCallThisExpression,
[SyntaxKind.TaggedTemplateExpression]: function forEachChildInTaggedTemplateExpression<T>(node: TaggedTemplateExpression, cbNode: (node: Node) => T | undefined, cbNodes?: (nodes: NodeArray<Node>) => T | undefined): T | undefined {
return visitNode(cbNode, node.tag) ||
visitNode(cbNode, node.questionDotToken) ||
Expand Down Expand Up @@ -1162,6 +1164,13 @@ function forEachChildInCallOrNewExpression<T>(node: CallExpression | NewExpressi
visitNodes(cbNode, cbNodes, node.arguments);
}

function forEachChildInCallThisExpression<T>(node: CallThisExpression, cbNode: (node: Node) => T | undefined, cbNodes?: (nodes: NodeArray<Node>) => T | undefined): T | undefined {
return visitNode(cbNode, node.receiver) ||
visitNode(cbNode, node.expression) ||
visitNodes(cbNode, cbNodes, node.typeArguments) ||
visitNodes(cbNode, cbNodes, node.arguments);
}

function forEachChildInBlock<T>(node: Block | ModuleBlock, cbNode: (node: Node) => T | undefined, cbNodes?: (nodes: NodeArray<Node>) => T | undefined): T | undefined {
return visitNodes(cbNode, cbNodes, node.statements);
}
Expand Down Expand Up @@ -1476,6 +1485,7 @@ namespace Parser {
createCallExpression: factoryCreateCallExpression,
createCallChain: factoryCreateCallChain,
createNewExpression: factoryCreateNewExpression,
createCallThisExpression: factoryCreateCallThisExpression,
createParenthesizedExpression: factoryCreateParenthesizedExpression,
createBlock: factoryCreateBlock,
createVariableStatement: factoryCreateVariableStatement,
Expand Down Expand Up @@ -6421,6 +6431,15 @@ namespace Parser {
return finishNode(indexedAccess, pos);
}

function parseCallThisExpressionRest(pos: number, expression: LeftHandSideExpression) {
const name = parseIdentifier();
const typeArguments = tryParse(parseTypeArgumentsInExpression);
const argumentList = parseArgumentList();

const callThis = factoryCreateCallThisExpression(expression, name, typeArguments, argumentList);
return finishNode(callThis, pos);
}

function parseMemberExpressionRest(pos: number, expression: LeftHandSideExpression, allowOptionalChain: boolean): MemberExpression {
while (true) {
let questionDotToken: QuestionDotToken | undefined;
Expand Down Expand Up @@ -6465,6 +6484,12 @@ namespace Parser {
}
}

const isCallThis = parseOptional(SyntaxKind.TildeGreaterThanToken);
if (isCallThis) {
expression = parseCallThisExpressionRest(pos, expression);
continue;
}

return expression as MemberExpression;
}
}
Expand Down
Loading