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

Parse and check type arguments on JSX opening and self-closing tags #22415

Merged
merged 2 commits into from
Mar 22, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
86 changes: 67 additions & 19 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14636,7 +14636,7 @@ namespace ts {
return mapType(valueType, t => getJsxSignaturesParameterTypes(t, isJs, node));
}

function getJsxSignaturesParameterTypes(valueType: Type, isJs: boolean, context: Node) {
function getJsxSignaturesParameterTypes(valueType: Type, isJs: boolean, context: JsxOpeningLikeElement) {
// If the elemType is a string type, we have to return anyType to prevent an error downstream as we will try to find construct or call signature of the type
if (valueType.flags & TypeFlags.String) {
return anyType;
Expand Down Expand Up @@ -14674,6 +14674,10 @@ namespace ts {
}
}

if (context.typeArguments) {
signatures = mapDefined(signatures, s => getJsxSignatureTypeArgumentInstantiation(s, context, isJs));
}

return getUnionType(map(signatures, ctor ? t => getJsxPropsTypeFromConstructSignature(t, isJs, context) : t => getJsxPropsTypeFromCallSignature(t, context)), UnionReduction.None);
}

Expand Down Expand Up @@ -15475,21 +15479,57 @@ namespace ts {

// Instantiate in context of source type
const instantiatedSignatures = [];
let candidateForTypeArgumentError: Signature;
let hasTypeArgumentError: boolean = !!node.typeArguments;
for (const signature of signatures) {
if (signature.typeParameters) {
const isJavascript = isInJavaScriptFile(node);
const inferenceContext = createInferenceContext(signature.typeParameters, signature, /*flags*/ isJavascript ? InferenceFlags.AnyDefault : InferenceFlags.None);
const typeArguments = inferJsxTypeArguments(signature, node, inferenceContext);
instantiatedSignatures.push(getSignatureInstantiation(signature, typeArguments, isJavascript));
const typeArgumentInstantiated = getJsxSignatureTypeArgumentInstantiation(signature, node, isJavascript, /*reportErrors*/ false);
if (typeArgumentInstantiated) {
hasTypeArgumentError = false;
instantiatedSignatures.push(typeArgumentInstantiated);
}
else {
if (node.typeArguments && hasCorrectTypeArgumentArity(signature, node.typeArguments)) {
candidateForTypeArgumentError = signature;
}
const inferenceContext = createInferenceContext(signature.typeParameters, signature, /*flags*/ isJavascript ? InferenceFlags.AnyDefault : InferenceFlags.None);
const typeArguments = inferJsxTypeArguments(signature, node, inferenceContext);
instantiatedSignatures.push(getSignatureInstantiation(signature, typeArguments, isJavascript));
}
}
else {
instantiatedSignatures.push(signature);
}
}

if (node.typeArguments && hasTypeArgumentError) {
if (candidateForTypeArgumentError) {
checkTypeArguments(candidateForTypeArgumentError, node.typeArguments, /*reportErrors*/ true);
}
// Length check to avoid issuing an arity error on length=0, the "Type argument list cannot be empty" grammar error alone is fine
else if (node.typeArguments.length !== 0) {
diagnostics.add(getTypeArgumentArityError(node, signatures, node.typeArguments));
}
}

return getUnionType(map(instantiatedSignatures, getReturnTypeOfSignature), UnionReduction.Subtype);
}

function getJsxSignatureTypeArgumentInstantiation(signature: Signature, node: JsxOpeningLikeElement, isJavascript: boolean, reportErrors?: boolean) {
if (!node.typeArguments) {
return;
}
if (!hasCorrectTypeArgumentArity(signature, node.typeArguments)) {
return;
}
const args = checkTypeArguments(signature, node.typeArguments, reportErrors);
if (!args) {
return;
}
return getSignatureInstantiation(signature, args, isJavascript);
}

function getJsxNamespaceAt(location: Node) {
const namespaceName = getJsxNamespace(location);
const resolvedNamespace = resolveName(location, namespaceName, SymbolFlags.Namespace, /*diagnosticMessage*/ undefined, namespaceName, /*isUse*/ false);
Expand Down Expand Up @@ -16750,13 +16790,7 @@ namespace ts {
spreadArgIndex = getSpreadArgumentIndex(args);
}

// If the user supplied type arguments, but the number of type arguments does not match
// the declared number of type parameters, the call has an incorrect arity.
const numTypeParameters = length(signature.typeParameters);
const minTypeArgumentCount = getMinTypeArgumentCount(signature.typeParameters);
const hasRightNumberOfTypeArgs = !typeArguments ||
(typeArguments.length >= minTypeArgumentCount && typeArguments.length <= numTypeParameters);
if (!hasRightNumberOfTypeArgs) {
if (!hasCorrectTypeArgumentArity(signature, typeArguments)) {
return false;
}

Expand All @@ -16776,6 +16810,15 @@ namespace ts {
return callIsIncomplete || hasEnoughArguments;
}

function hasCorrectTypeArgumentArity(signature: Signature, typeArguments: NodeArray<TypeNode> | undefined) {
// If the user supplied type arguments, but the number of type arguments does not match
// the declared number of type parameters, the call has an incorrect arity.
const numTypeParameters = length(signature.typeParameters);
const minTypeArgumentCount = getMinTypeArgumentCount(signature.typeParameters);
return !typeArguments ||
(typeArguments.length >= minTypeArgumentCount && typeArguments.length <= numTypeParameters);
}

// If type has a single call signature and no other members, return that signature. Otherwise, return undefined.
function getSingleCallSignature(type: Type): Signature {
if (type.flags & TypeFlags.Object) {
Expand Down Expand Up @@ -17337,6 +17380,17 @@ namespace ts {
}
}

function getTypeArgumentArityError(node: Node, signatures: Signature[], typeArguments: NodeArray<TypeNode>) {
let min = Number.POSITIVE_INFINITY;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why the original author didn't just use Infinity / -Infinity ?

let max = Number.NEGATIVE_INFINITY;
for (const sig of signatures) {
min = Math.min(min, getMinTypeArgumentCount(sig.typeParameters));
max = Math.max(max, length(sig.typeParameters));
}
const paramCount = min < max ? min + "-" + max : min;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would find min === max ? min : min + "-" + max clearer

return createDiagnosticForNodeArray(getSourceFileOfNode(node), typeArguments, Diagnostics.Expected_0_type_arguments_but_got_1, paramCount, typeArguments.length);
}

function resolveCall(node: CallLikeExpression, signatures: Signature[], candidatesOutArray: Signature[], fallbackError?: DiagnosticMessage): Signature {
const isTaggedTemplate = node.kind === SyntaxKind.TaggedTemplateExpression;
const isDecorator = node.kind === SyntaxKind.Decorator;
Expand Down Expand Up @@ -17464,14 +17518,7 @@ namespace ts {
checkTypeArguments(candidateForTypeArgumentError, (node as CallExpression).typeArguments, /*reportErrors*/ true, fallbackError);
}
else if (typeArguments && every(signatures, sig => length(sig.typeParameters) !== typeArguments.length)) {
let min = Number.POSITIVE_INFINITY;
let max = Number.NEGATIVE_INFINITY;
for (const sig of signatures) {
min = Math.min(min, getMinTypeArgumentCount(sig.typeParameters));
max = Math.max(max, length(sig.typeParameters));
}
const paramCount = min < max ? min + "-" + max : min;
diagnostics.add(createDiagnosticForNodeArray(getSourceFileOfNode(node), typeArguments, Diagnostics.Expected_0_type_arguments_but_got_1, paramCount, typeArguments.length));
diagnostics.add(getTypeArgumentArityError(node, signatures, typeArguments));
}
else if (args) {
let min = Number.POSITIVE_INFINITY;
Expand Down Expand Up @@ -26678,6 +26725,7 @@ namespace ts {
}

function checkGrammarJsxElement(node: JsxOpeningLikeElement) {
checkGrammarTypeArguments(node, node.typeArguments);
const seen = createUnderscoreEscapedMap<boolean>();

for (const attr of node.attributes.properties) {
Expand Down
16 changes: 10 additions & 6 deletions src/compiler/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2126,31 +2126,35 @@ namespace ts {
: node;
}

export function createJsxSelfClosingElement(tagName: JsxTagNameExpression, attributes: JsxAttributes) {
export function createJsxSelfClosingElement(tagName: JsxTagNameExpression, typeArguments: ReadonlyArray<TypeNode> | undefined, attributes: JsxAttributes) {
const node = <JsxSelfClosingElement>createSynthesizedNode(SyntaxKind.JsxSelfClosingElement);
node.tagName = tagName;
node.typeArguments = typeArguments && createNodeArray(typeArguments);
node.attributes = attributes;
return node;
}

export function updateJsxSelfClosingElement(node: JsxSelfClosingElement, tagName: JsxTagNameExpression, attributes: JsxAttributes) {
export function updateJsxSelfClosingElement(node: JsxSelfClosingElement, tagName: JsxTagNameExpression, typeArguments: ReadonlyArray<TypeNode> | undefined, attributes: JsxAttributes) {
return node.tagName !== tagName
|| node.typeArguments !== typeArguments
|| node.attributes !== attributes
? updateNode(createJsxSelfClosingElement(tagName, attributes), node)
? updateNode(createJsxSelfClosingElement(tagName, typeArguments, attributes), node)
: node;
}

export function createJsxOpeningElement(tagName: JsxTagNameExpression, attributes: JsxAttributes) {
export function createJsxOpeningElement(tagName: JsxTagNameExpression, typeArguments: ReadonlyArray<TypeNode> | undefined, attributes: JsxAttributes) {
const node = <JsxOpeningElement>createSynthesizedNode(SyntaxKind.JsxOpeningElement);
node.tagName = tagName;
node.typeArguments = typeArguments && createNodeArray(typeArguments);
node.attributes = attributes;
return node;
}

export function updateJsxOpeningElement(node: JsxOpeningElement, tagName: JsxTagNameExpression, attributes: JsxAttributes) {
export function updateJsxOpeningElement(node: JsxOpeningElement, tagName: JsxTagNameExpression, typeArguments: ReadonlyArray<TypeNode> | undefined, attributes: JsxAttributes) {
return node.tagName !== tagName
|| node.typeArguments !== typeArguments
|| node.attributes !== attributes
? updateNode(createJsxOpeningElement(tagName, attributes), node)
? updateNode(createJsxOpeningElement(tagName, typeArguments, attributes), node)
: node;
}

Expand Down
3 changes: 3 additions & 0 deletions src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,7 @@ namespace ts {
case SyntaxKind.JsxSelfClosingElement:
case SyntaxKind.JsxOpeningElement:
return visitNode(cbNode, (<JsxOpeningLikeElement>node).tagName) ||
visitNodes(cbNode, cbNodes, (<JsxOpeningLikeElement>node).typeArguments) ||
visitNode(cbNode, (<JsxOpeningLikeElement>node).attributes);
case SyntaxKind.JsxAttributes:
return visitNodes(cbNode, cbNodes, (<JsxAttributes>node).properties);
Expand Down Expand Up @@ -4189,6 +4190,7 @@ namespace ts {
}

const tagName = parseJsxElementName();
const typeArguments = tryParseTypeArguments();
const attributes = parseJsxAttributes();

let node: JsxOpeningLikeElement;
Expand All @@ -4213,6 +4215,7 @@ namespace ts {
}

node.tagName = tagName;
node.typeArguments = typeArguments;
node.attributes = attributes;

return finishNode(node);
Expand Down
4 changes: 3 additions & 1 deletion src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1458,8 +1458,10 @@ namespace ts {
case SyntaxKind.CallExpression:
case SyntaxKind.NewExpression:
case SyntaxKind.ExpressionWithTypeArguments:
case SyntaxKind.JsxSelfClosingElement:
case SyntaxKind.JsxOpeningElement:
// Check type arguments
if (nodes === (<CallExpression | NewExpression | ExpressionWithTypeArguments>parent).typeArguments) {
if (nodes === (<CallExpression | NewExpression | ExpressionWithTypeArguments | JsxOpeningLikeElement>parent).typeArguments) {
diagnostics.push(createDiagnosticForNodeArray(nodes, Diagnostics.type_arguments_can_only_be_used_in_a_ts_file));
return;
}
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1763,13 +1763,15 @@ namespace ts {
kind: SyntaxKind.JsxOpeningElement;
parent?: JsxElement;
tagName: JsxTagNameExpression;
typeArguments?: NodeArray<TypeNode>;
attributes: JsxAttributes;
}

/// A JSX expression of the form <TagName attrs />
export interface JsxSelfClosingElement extends PrimaryExpression {
kind: SyntaxKind.JsxSelfClosingElement;
tagName: JsxTagNameExpression;
typeArguments?: NodeArray<TypeNode>;
attributes: JsxAttributes;
}

Expand Down
2 changes: 2 additions & 0 deletions src/compiler/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -821,11 +821,13 @@ namespace ts {
case SyntaxKind.JsxSelfClosingElement:
return updateJsxSelfClosingElement(<JsxSelfClosingElement>node,
visitNode((<JsxSelfClosingElement>node).tagName, visitor, isJsxTagNameExpression),
nodesVisitor((<JsxSelfClosingElement>node).typeArguments, visitor, isTypeNode),
visitNode((<JsxSelfClosingElement>node).attributes, visitor, isJsxAttributes));

case SyntaxKind.JsxOpeningElement:
return updateJsxOpeningElement(<JsxOpeningElement>node,
visitNode((<JsxOpeningElement>node).tagName, visitor, isJsxTagNameExpression),
nodesVisitor((<JsxSelfClosingElement>node).typeArguments, visitor, isTypeNode),
visitNode((<JsxOpeningElement>node).attributes, visitor, isJsxAttributes));

case SyntaxKind.JsxClosingElement:
Expand Down
10 changes: 6 additions & 4 deletions tests/baselines/reference/api/tsserverlibrary.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1078,11 +1078,13 @@ declare namespace ts {
kind: SyntaxKind.JsxOpeningElement;
parent?: JsxElement;
tagName: JsxTagNameExpression;
typeArguments?: NodeArray<TypeNode>;
attributes: JsxAttributes;
}
interface JsxSelfClosingElement extends PrimaryExpression {
kind: SyntaxKind.JsxSelfClosingElement;
tagName: JsxTagNameExpression;
typeArguments?: NodeArray<TypeNode>;
attributes: JsxAttributes;
}
interface JsxFragment extends PrimaryExpression {
Expand Down Expand Up @@ -3665,10 +3667,10 @@ declare namespace ts {
function updateExternalModuleReference(node: ExternalModuleReference, expression: Expression): ExternalModuleReference;
function createJsxElement(openingElement: JsxOpeningElement, children: ReadonlyArray<JsxChild>, closingElement: JsxClosingElement): JsxElement;
function updateJsxElement(node: JsxElement, openingElement: JsxOpeningElement, children: ReadonlyArray<JsxChild>, closingElement: JsxClosingElement): JsxElement;
function createJsxSelfClosingElement(tagName: JsxTagNameExpression, attributes: JsxAttributes): JsxSelfClosingElement;
function updateJsxSelfClosingElement(node: JsxSelfClosingElement, tagName: JsxTagNameExpression, attributes: JsxAttributes): JsxSelfClosingElement;
function createJsxOpeningElement(tagName: JsxTagNameExpression, attributes: JsxAttributes): JsxOpeningElement;
function updateJsxOpeningElement(node: JsxOpeningElement, tagName: JsxTagNameExpression, attributes: JsxAttributes): JsxOpeningElement;
function createJsxSelfClosingElement(tagName: JsxTagNameExpression, typeArguments: ReadonlyArray<TypeNode> | undefined, attributes: JsxAttributes): JsxSelfClosingElement;
function updateJsxSelfClosingElement(node: JsxSelfClosingElement, tagName: JsxTagNameExpression, typeArguments: ReadonlyArray<TypeNode> | undefined, attributes: JsxAttributes): JsxSelfClosingElement;
function createJsxOpeningElement(tagName: JsxTagNameExpression, typeArguments: ReadonlyArray<TypeNode> | undefined, attributes: JsxAttributes): JsxOpeningElement;
function updateJsxOpeningElement(node: JsxOpeningElement, tagName: JsxTagNameExpression, typeArguments: ReadonlyArray<TypeNode> | undefined, attributes: JsxAttributes): JsxOpeningElement;
function createJsxClosingElement(tagName: JsxTagNameExpression): JsxClosingElement;
function updateJsxClosingElement(node: JsxClosingElement, tagName: JsxTagNameExpression): JsxClosingElement;
function createJsxFragment(openingFragment: JsxOpeningFragment, children: ReadonlyArray<JsxChild>, closingFragment: JsxClosingFragment): JsxFragment;
Expand Down
Loading