Skip to content

Commit

Permalink
Add 'extends' clause to 'infer' type (#48112)
Browse files Browse the repository at this point in the history
* Add 'extends' clause to 'infer' type

* Revise parse for infer..extends, improve parenthesizer

* More aggressive parens to match existing DT tests

* tests 'infer' constraint using outer type parameter

* Adds a test showing 'infer' cannot reference other 'infer' in same 'extends'

* Emit extends clause for synthetic infer typesduring declaration emit
  • Loading branch information
rbuckton committed Apr 5, 2022
1 parent 6e0447f commit 7da80d7
Show file tree
Hide file tree
Showing 56 changed files with 1,946 additions and 240 deletions.
41 changes: 35 additions & 6 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5009,7 +5009,20 @@ namespace ts {
if (type.flags & TypeFlags.TypeParameter || objectFlags & ObjectFlags.ClassOrInterface) {
if (type.flags & TypeFlags.TypeParameter && contains(context.inferTypeParameters, type)) {
context.approximateLength += (symbolName(type.symbol).length + 6);
return factory.createInferTypeNode(typeParameterToDeclarationWithConstraint(type as TypeParameter, context, /*constraintNode*/ undefined));
let constraintNode: TypeNode | undefined;
const constraint = getConstraintOfTypeParameter(type as TypeParameter);
if (constraint) {
// If the infer type has a constraint that is not the same as the constraint
// we would have normally inferred based on context, we emit the constraint
// using `infer T extends ?`. We omit inferred constraints from type references
// as they may be elided.
const inferredConstraint = getInferredTypeParameterConstraint(type as TypeParameter, /*omitTypeReferences*/ true);
if (!(inferredConstraint && isTypeIdenticalTo(constraint, inferredConstraint))) {
context.approximateLength += 9;
constraintNode = constraint && typeToTypeNodeHelper(constraint, context);
}
}
return factory.createInferTypeNode(typeParameterToDeclarationWithConstraint(type as TypeParameter, context, constraintNode));
}
if (context.flags & NodeBuilderFlags.GenerateNamesForShadowedTypeParams &&
type.flags & TypeFlags.TypeParameter &&
Expand Down Expand Up @@ -13257,7 +13270,7 @@ namespace ts {
return mapDefined(filter(type.symbol && type.symbol.declarations, isTypeParameterDeclaration), getEffectiveConstraintOfTypeParameter)[0];
}

function getInferredTypeParameterConstraint(typeParameter: TypeParameter) {
function getInferredTypeParameterConstraint(typeParameter: TypeParameter, omitTypeReferences?: boolean) {
let inferences: Type[] | undefined;
if (typeParameter.symbol?.declarations) {
for (const declaration of typeParameter.symbol.declarations) {
Expand All @@ -13267,7 +13280,7 @@ namespace ts {
// corresponding type parameter in 'Foo'. When multiple 'infer T' declarations are
// present, we form an intersection of the inferred constraint types.
const [childTypeParameter = declaration.parent, grandParent] = walkUpParenthesizedTypesAndGetParentAndChild(declaration.parent.parent);
if (grandParent.kind === SyntaxKind.TypeReference) {
if (grandParent.kind === SyntaxKind.TypeReference && !omitTypeReferences) {
const typeReference = grandParent as TypeReferenceNode;
const typeParameters = getTypeParametersForTypeReference(typeReference);
if (typeParameters) {
Expand Down Expand Up @@ -35614,6 +35627,22 @@ namespace ts {
grammarErrorOnNode(node, Diagnostics.infer_declarations_are_only_permitted_in_the_extends_clause_of_a_conditional_type);
}
checkSourceElement(node.typeParameter);
const symbol = getSymbolOfNode(node.typeParameter);
if (symbol.declarations && symbol.declarations.length > 1) {
const links = getSymbolLinks(symbol);
if (!links.typeParametersChecked) {
links.typeParametersChecked = true;
const typeParameter = getDeclaredTypeOfTypeParameter(symbol);
const declarations: TypeParameterDeclaration[] = getDeclarationsOfKind(symbol, SyntaxKind.TypeParameter);
if (!areTypeParametersIdentical(declarations, [typeParameter], decl => [decl])) {
// Report an error on every conflicting declaration.
const name = symbolToString(symbol);
for (const declaration of declarations) {
error(declaration.name, Diagnostics.All_declarations_of_0_must_have_identical_constraints, name);
}
}
}
}
registerForUnusedIdentifiersCheck(node);
}

Expand Down Expand Up @@ -39124,7 +39153,7 @@ namespace ts {
}

const type = getDeclaredTypeOfSymbol(symbol) as InterfaceType;
if (!areTypeParametersIdentical(declarations, type.localTypeParameters!)) {
if (!areTypeParametersIdentical(declarations, type.localTypeParameters!, getEffectiveTypeParameterDeclarations)) {
// Report an error on every conflicting declaration.
const name = symbolToString(symbol);
for (const declaration of declarations) {
Expand All @@ -39134,13 +39163,13 @@ namespace ts {
}
}

function areTypeParametersIdentical(declarations: readonly (ClassDeclaration | InterfaceDeclaration)[], targetParameters: TypeParameter[]) {
function areTypeParametersIdentical<T extends DeclarationWithTypeParameters | TypeParameterDeclaration>(declarations: readonly T[], targetParameters: TypeParameter[], getTypeParameterDeclarations: (node: T) => readonly TypeParameterDeclaration[]) {
const maxTypeArgumentCount = length(targetParameters);
const minTypeArgumentCount = getMinTypeArgumentCount(targetParameters);

for (const declaration of declarations) {
// If this declaration has too few or too many type parameters, we report an error
const sourceParameters = getEffectiveTypeParameterDeclarations(declaration);
const sourceParameters = getTypeParameterDeclarations(declaration);
const numTypeParameters = sourceParameters.length;
if (numTypeParameters < minTypeArgumentCount || numTypeParameters > maxTypeArgumentCount) {
return false;
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -3433,6 +3433,10 @@
"category": "Error",
"code": 2837
},
"All declarations of '{0}' must have identical constraints.": {
"category": "Error",
"code": 2838
},

"Import declaration '{0}' is using private name '{1}'.": {
"category": "Error",
Expand Down
68 changes: 49 additions & 19 deletions src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,9 @@ namespace ts {
let currentParenthesizerRule: ((node: Node) => Node) | undefined;
const { enter: enterComment, exit: exitComment } = performance.createTimerIf(extendedDiagnostics, "commentTime", "beforeComment", "afterComment");
const parenthesizer = factory.parenthesizer;
const typeArgumentParenthesizerRuleSelector: OrdinalParentheizerRuleSelector<Node> = {
select: index => index === 0 ? parenthesizer.parenthesizeLeadingTypeArgument : undefined
};
const emitBinaryExpression = createEmitBinaryExpression();

reset();
Expand Down Expand Up @@ -2241,7 +2244,7 @@ namespace ts {
}

function emitArrayType(node: ArrayTypeNode) {
emit(node.elementType, parenthesizer.parenthesizeElementTypeOfArrayType);
emit(node.elementType, parenthesizer.parenthesizeNonArrayTypeOfPostfixType);
writePunctuation("[");
writePunctuation("]");
}
Expand All @@ -2254,7 +2257,7 @@ namespace ts {
function emitTupleType(node: TupleTypeNode) {
emitTokenWithComment(SyntaxKind.OpenBracketToken, node.pos, writePunctuation, node);
const flags = getEmitFlags(node) & EmitFlags.SingleLine ? ListFormat.SingleLineTupleTypeElements : ListFormat.MultiLineTupleTypeElements;
emitList(node, node.elements, flags | ListFormat.NoSpaceIfEmpty);
emitList(node, node.elements, flags | ListFormat.NoSpaceIfEmpty, parenthesizer.parenthesizeElementTypeOfTupleType);
emitTokenWithComment(SyntaxKind.CloseBracketToken, node.elements.end, writePunctuation, node);
}

Expand All @@ -2268,24 +2271,24 @@ namespace ts {
}

function emitOptionalType(node: OptionalTypeNode) {
emit(node.type, parenthesizer.parenthesizeElementTypeOfArrayType);
emit(node.type, parenthesizer.parenthesizeTypeOfOptionalType);
writePunctuation("?");
}

function emitUnionType(node: UnionTypeNode) {
emitList(node, node.types, ListFormat.UnionTypeConstituents, parenthesizer.parenthesizeMemberOfElementType);
emitList(node, node.types, ListFormat.UnionTypeConstituents, parenthesizer.parenthesizeConstituentTypeOfUnionType);
}

function emitIntersectionType(node: IntersectionTypeNode) {
emitList(node, node.types, ListFormat.IntersectionTypeConstituents, parenthesizer.parenthesizeMemberOfElementType);
emitList(node, node.types, ListFormat.IntersectionTypeConstituents, parenthesizer.parenthesizeConstituentTypeOfIntersectionType);
}

function emitConditionalType(node: ConditionalTypeNode) {
emit(node.checkType, parenthesizer.parenthesizeMemberOfConditionalType);
emit(node.checkType, parenthesizer.parenthesizeCheckTypeOfConditionalType);
writeSpace();
writeKeyword("extends");
writeSpace();
emit(node.extendsType, parenthesizer.parenthesizeMemberOfConditionalType);
emit(node.extendsType, parenthesizer.parenthesizeExtendsTypeOfConditionalType);
writeSpace();
writePunctuation("?");
writeSpace();
Expand Down Expand Up @@ -2315,11 +2318,15 @@ namespace ts {
function emitTypeOperator(node: TypeOperatorNode) {
writeTokenText(node.operator, writeKeyword);
writeSpace();
emit(node.type, parenthesizer.parenthesizeMemberOfElementType);

const parenthesizerRule = node.operator === SyntaxKind.ReadonlyKeyword ?
parenthesizer.parenthesizeOperandOfReadonlyTypeOperator :
parenthesizer.parenthesizeOperandOfTypeOperator;
emit(node.type, parenthesizerRule);
}

function emitIndexedAccessType(node: IndexedAccessTypeNode) {
emit(node.objectType, parenthesizer.parenthesizeMemberOfElementType);
emit(node.objectType, parenthesizer.parenthesizeNonArrayTypeOfPostfixType);
writePunctuation("[");
emit(node.indexType);
writePunctuation("]");
Expand Down Expand Up @@ -4256,7 +4263,7 @@ namespace ts {
}

function emitTypeArguments(parentNode: Node, typeArguments: NodeArray<TypeNode> | undefined) {
emitList(parentNode, typeArguments, ListFormat.TypeArguments, parenthesizer.parenthesizeMemberOfElementType);
emitList(parentNode, typeArguments, ListFormat.TypeArguments, typeArgumentParenthesizerRuleSelector);
}

function emitTypeParameters(parentNode: SignatureDeclaration | InterfaceDeclaration | TypeAliasDeclaration | ClassDeclaration | ClassExpression, typeParameters: NodeArray<TypeParameterDeclaration> | undefined) {
Expand Down Expand Up @@ -4324,15 +4331,15 @@ namespace ts {
}
}

function emitList(parentNode: Node | undefined, children: NodeArray<Node> | undefined, format: ListFormat, parenthesizerRule?: (node: Node) => Node, start?: number, count?: number) {
function emitList(parentNode: Node | undefined, children: NodeArray<Node> | undefined, format: ListFormat, parenthesizerRule?: ParenthesizerRuleOrSelector<Node>, start?: number, count?: number) {
emitNodeList(emit, parentNode, children, format, parenthesizerRule, start, count);
}

function emitExpressionList(parentNode: Node | undefined, children: NodeArray<Node> | undefined, format: ListFormat, parenthesizerRule?: (node: Expression) => Expression, start?: number, count?: number) {
function emitExpressionList(parentNode: Node | undefined, children: NodeArray<Node> | undefined, format: ListFormat, parenthesizerRule?: ParenthesizerRuleOrSelector<Expression>, start?: number, count?: number) {
emitNodeList(emitExpression, parentNode, children, format, parenthesizerRule, start, count);
}

function emitNodeList(emit: (node: Node, parenthesizerRule?: ((node: Node) => Node) | undefined) => void, parentNode: Node | undefined, children: NodeArray<Node> | undefined, format: ListFormat, parenthesizerRule: ((node: Node) => Node) | undefined, start = 0, count = children ? children.length - start : 0) {
function emitNodeList(emit: (node: Node, parenthesizerRule?: ((node: Node) => Node) | undefined) => void, parentNode: Node | undefined, children: NodeArray<Node> | undefined, format: ListFormat, parenthesizerRule: ParenthesizerRuleOrSelector<Node> | undefined, start = 0, count = children ? children.length - start : 0) {
const isUndefined = children === undefined;
if (isUndefined && format & ListFormat.OptionalIfUndefined) {
return;
Expand Down Expand Up @@ -4388,6 +4395,8 @@ namespace ts {
increaseIndent();
}

const emitListItem = getEmitListItem(emit, parenthesizerRule);

// Emit each child.
let previousSibling: Node | undefined;
let previousSourceFileTextKind: ReturnType<typeof recordBundleFileInternalSectionStart>;
Expand Down Expand Up @@ -4443,12 +4452,7 @@ namespace ts {
}

nextListElementPos = child.pos;
if (emit.length === 1) {
emit(child);
}
else {
emit(child, parenthesizerRule);
}
emitListItem(child, emit, parenthesizerRule, i);

if (shouldDecreaseIndentAfterEmit) {
decreaseIndent();
Expand Down Expand Up @@ -5890,4 +5894,30 @@ namespace ts {
CountMask = 0x0FFFFFFF, // Temp variable counter
_i = 0x10000000, // Use/preference flag for '_i'
}

interface OrdinalParentheizerRuleSelector<T extends Node> {
select(index: number): ((node: T) => T) | undefined;
}

type ParenthesizerRule<T extends Node> = (node: T) => T;

type ParenthesizerRuleOrSelector<T extends Node> = OrdinalParentheizerRuleSelector<T> | ParenthesizerRule<T>;

function emitListItemNoParenthesizer(node: Node, emit: (node: Node, parenthesizerRule?: ((node: Node) => Node) | undefined) => void, _parenthesizerRule: ParenthesizerRuleOrSelector<Node> | undefined, _index: number) {
emit(node);
}

function emitListItemWithParenthesizerRuleSelector(node: Node, emit: (node: Node, parenthesizerRule?: ((node: Node) => Node) | undefined) => void, parenthesizerRuleSelector: OrdinalParentheizerRuleSelector<Node>, index: number) {
emit(node, parenthesizerRuleSelector.select(index));
}

function emitListItemWithParenthesizerRule(node: Node, emit: (node: Node, parenthesizerRule?: ((node: Node) => Node) | undefined) => void, parenthesizerRule: ParenthesizerRule<Node> | undefined, _index: number) {
emit(node, parenthesizerRule);
}

function getEmitListItem<T extends Node, R extends ParenthesizerRuleOrSelector<T> | undefined>(emit: (node: Node, parenthesizerRule?: ((node: Node) => Node) | undefined) => void, parenthesizerRule: R): (node: Node, emit: (node: Node, parenthesizerRule?: ((node: Node) => Node) | undefined) => void, parenthesizerRule: R, index: number) => void {
return emit.length === 1 ? emitListItemNoParenthesizer :
typeof parenthesizerRule === "object" ? emitListItemWithParenthesizerRuleSelector :
emitListItemWithParenthesizerRule;
}
}
Loading

0 comments on commit 7da80d7

Please sign in to comment.