Skip to content

Commit

Permalink
feat(7411): JSX namespaced attribute syntax not supported (#47356)
Browse files Browse the repository at this point in the history
  • Loading branch information
a-tarasyuk authored Apr 14, 2023
1 parent f306e4e commit 0c5be02
Show file tree
Hide file tree
Showing 52 changed files with 828 additions and 376 deletions.
43 changes: 16 additions & 27 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ import {
getEntityNameFromTypeNode,
getErrorSpanForNode,
getEscapedTextOfIdentifierOrLiteral,
getEscapedTextOfJsxAttributeName,
getESModuleInterop,
getExpandoInitializer,
getExportAssignmentExpression,
Expand Down Expand Up @@ -350,6 +351,7 @@ import {
getSymbolNameForPrivateIdentifier,
getTextOfIdentifierOrLiteral,
getTextOfJSDocComment,
getTextOfJsxAttributeName,
getTextOfNode,
getTextOfPropertyName,
getThisContainer,
Expand Down Expand Up @@ -593,6 +595,7 @@ import {
isJsxAttributeLike,
isJsxAttributes,
isJsxElement,
isJsxNamespacedName,
isJsxOpeningElement,
isJsxOpeningFragment,
isJsxOpeningLikeElement,
Expand Down Expand Up @@ -807,7 +810,6 @@ import {
MappedTypeNode,
MatchingKeys,
maybeBind,
MemberName,
MemberOverrideStatus,
memoize,
MetaProperty,
Expand Down Expand Up @@ -13517,7 +13519,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
function isTypeInvalidDueToUnionDiscriminant(contextualType: Type, obj: ObjectLiteralExpression | JsxAttributes): boolean {
const list = obj.properties as NodeArray<ObjectLiteralElementLike | JsxAttributeLike>;
return list.some(property => {
const nameType = property.name && getLiteralTypeFromPropertyName(property.name);
const nameType = property.name && (isJsxNamespacedName(property.name) ? getStringLiteralType(getTextOfJsxAttributeName(property.name)) : getLiteralTypeFromPropertyName(property.name));
const name = nameType && isTypeUsableAsPropertyName(nameType) ? getPropertyNameFromType(nameType) : undefined;
const expected = name === undefined ? undefined : getTypeOfPropertyOfType(contextualType, name);
return !!expected && isLiteralType(expected) && !isTypeAssignableTo(getTypeOfNode(property), expected);
Expand Down Expand Up @@ -19590,8 +19592,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
function *generateJsxAttributes(node: JsxAttributes): ElaborationIterator {
if (!length(node.properties)) return;
for (const prop of node.properties) {
if (isJsxSpreadAttribute(prop) || isHyphenatedJsxName(idText(prop.name))) continue;
yield { errorNode: prop.name, innerExpression: prop.initializer, nameType: getStringLiteralType(idText(prop.name)) };
if (isJsxSpreadAttribute(prop) || isHyphenatedJsxName(getTextOfJsxAttributeName(prop.name))) continue;
yield { errorNode: prop.name, innerExpression: prop.initializer, nameType: getStringLiteralType(getTextOfJsxAttributeName(prop.name)) };
}
}

Expand Down Expand Up @@ -29266,7 +29268,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (!attributesType || isTypeAny(attributesType)) {
return undefined;
}
return getTypeOfPropertyOfContextualType(attributesType, attribute.name.escapedText);
return getTypeOfPropertyOfContextualType(attributesType, getEscapedTextOfJsxAttributeName(attribute.name));
}
else {
return getContextualType(attribute.parent, contextFlags);
Expand Down Expand Up @@ -30400,12 +30402,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
attributeSymbol.links.target = member;
attributesTable.set(attributeSymbol.escapedName, attributeSymbol);
allAttributesTable?.set(attributeSymbol.escapedName, attributeSymbol);
if (attributeDecl.name.escapedText === jsxChildrenPropertyName) {
if (getEscapedTextOfJsxAttributeName(attributeDecl.name) === jsxChildrenPropertyName) {
explicitlySpecifyChildrenAttribute = true;
}
if (contextualType) {
const prop = getPropertyOfType(contextualType, member.escapedName);
if (prop && prop.declarations && isDeprecatedSymbol(prop)) {
if (prop && prop.declarations && isDeprecatedSymbol(prop) && isIdentifier(attributeDecl.name)) {
addDeprecatedSuggestion(attributeDecl.name, prop.declarations, attributeDecl.name.escapedText as string);
}
}
Expand Down Expand Up @@ -47852,8 +47854,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

const { name, initializer } = attr;
if (!seen.get(name.escapedText)) {
seen.set(name.escapedText, true);
const escapedText = getEscapedTextOfJsxAttributeName(name);
if (!seen.get(escapedText)) {
seen.set(escapedText, true);
}
else {
return grammarErrorOnNode(name, Diagnostics.JSX_elements_cannot_have_multiple_attributes_with_the_same_name);
Expand All @@ -47866,25 +47869,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

function checkGrammarJsxName(node: JsxTagNameExpression) {
if (isPropertyAccessExpression(node)) {
let propName: JsxTagNameExpression = node;
do {
const check = checkGrammarJsxNestedIdentifier(propName.name);
if (check) {
return check;
}
propName = propName.expression;
} while (isPropertyAccessExpression(propName));
const check = checkGrammarJsxNestedIdentifier(propName);
if (check) {
return check;
}
if (isPropertyAccessExpression(node) && isJsxNamespacedName(node.expression)) {
return grammarErrorOnNode(node.expression, Diagnostics.JSX_property_access_expressions_cannot_include_JSX_namespace_names);
}

function checkGrammarJsxNestedIdentifier(name: MemberName | ThisExpression) {
if (isIdentifier(name) && idText(name).indexOf(":") !== -1) {
return grammarErrorOnNode(name, Diagnostics.JSX_property_access_expressions_cannot_include_JSX_namespace_names);
}
if (isJsxNamespacedName(node) && getJSXTransformEnabled(compilerOptions) && !isIntrinsicJsxName(node.namespace.escapedText)) {
return grammarErrorOnNode(node, Diagnostics.React_components_cannot_include_JSX_namespace_names);
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -2893,6 +2893,10 @@
"category": "Error",
"code": 2638
},
"React components cannot include JSX namespace names": {
"category": "Error",
"code": 2639
},

"Cannot augment module '{0}' with value exports because it resolves to a non-module entity.": {
"category": "Error",
Expand Down
9 changes: 9 additions & 0 deletions src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ import {
JsxEmit,
JsxExpression,
JsxFragment,
JsxNamespacedName,
JsxOpeningElement,
JsxOpeningFragment,
JsxSelfClosingElement,
Expand Down Expand Up @@ -2283,6 +2284,8 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri
return emitJsxSelfClosingElement(node as JsxSelfClosingElement);
case SyntaxKind.JsxFragment:
return emitJsxFragment(node as JsxFragment);
case SyntaxKind.JsxNamespacedName:
return emitJsxNamespacedName(node as JsxNamespacedName);

// Synthesized list
case SyntaxKind.SyntaxList:
Expand Down Expand Up @@ -4225,6 +4228,12 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri
}
}

function emitJsxNamespacedName(node: JsxNamespacedName) {
emitIdentifierName(node.namespace);
writePunctuation(":");
emitIdentifierName(node.name);
}

function emitJsxTagName(node: JsxTagNameExpression) {
if (node.kind === SyntaxKind.Identifier) {
emitExpression(node);
Expand Down
28 changes: 26 additions & 2 deletions src/compiler/factory/nodeFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ import {
JSDocVariadicType,
JsxAttribute,
JsxAttributeLike,
JsxAttributeName,
JsxAttributes,
JsxAttributeValue,
JsxChild,
Expand All @@ -275,6 +276,7 @@ import {
JsxElement,
JsxExpression,
JsxFragment,
JsxNamespacedName,
JsxOpeningElement,
JsxOpeningFragment,
JsxSelfClosingElement,
Expand Down Expand Up @@ -908,6 +910,8 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
updateJsxSpreadAttribute,
createJsxExpression,
updateJsxExpression,
createJsxNamespacedName,
updateJsxNamespacedName,
createCaseClause,
updateCaseClause,
createDefaultClause,
Expand Down Expand Up @@ -5582,7 +5586,7 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
}

// @api
function createJsxAttribute(name: Identifier, initializer: JsxAttributeValue | undefined) {
function createJsxAttribute(name: JsxAttributeName, initializer: JsxAttributeValue | undefined) {
const node = createBaseDeclaration<JsxAttribute>(SyntaxKind.JsxAttribute);
node.name = name;
node.initializer = initializer;
Expand All @@ -5594,7 +5598,7 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
}

// @api
function updateJsxAttribute(node: JsxAttribute, name: Identifier, initializer: JsxAttributeValue | undefined) {
function updateJsxAttribute(node: JsxAttribute, name: JsxAttributeName, initializer: JsxAttributeValue | undefined) {
return node.name !== name
|| node.initializer !== initializer
? update(createJsxAttribute(name, initializer), node)
Expand Down Expand Up @@ -5654,6 +5658,26 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
: node;
}

// @api
function createJsxNamespacedName(namespace: Identifier, name: Identifier) {
const node = createBaseNode<JsxNamespacedName>(SyntaxKind.JsxNamespacedName);
node.namespace = namespace;
node.name = name;
node.transformFlags |=
propagateChildFlags(node.namespace) |
propagateChildFlags(node.name) |
TransformFlags.ContainsJsx;
return node;
}

// @api
function updateJsxNamespacedName(node: JsxNamespacedName, namespace: Identifier, name: Identifier) {
return node.namespace !== namespace
|| node.name !== name
? update(createJsxNamespacedName(namespace, name), node)
: node;
}

//
// Clauses
//
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 @@ -128,6 +128,7 @@ import {
JsxElement,
JsxExpression,
JsxFragment,
JsxNamespacedName,
JsxOpeningElement,
JsxOpeningFragment,
JsxSelfClosingElement,
Expand Down Expand Up @@ -963,6 +964,10 @@ export function isJsxExpression(node: Node): node is JsxExpression {
return node.kind === SyntaxKind.JsxExpression;
}

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

// Clauses

export function isCaseClause(node: Node): node is CaseClause {
Expand Down
42 changes: 37 additions & 5 deletions src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ import {
JsxElement,
JsxExpression,
JsxFragment,
JsxNamespacedName,
JsxOpeningElement,
JsxOpeningFragment,
JsxOpeningLikeElement,
Expand Down Expand Up @@ -1030,6 +1031,10 @@ const forEachChildTable: ForEachChildTable = {
[SyntaxKind.JsxClosingElement]: function forEachChildInJsxClosingElement<T>(node: JsxClosingElement, cbNode: (node: Node) => T | undefined, _cbNodes?: (nodes: NodeArray<Node>) => T | undefined): T | undefined {
return visitNode(cbNode, node.tagName);
},
[SyntaxKind.JsxNamespacedName]: function forEachChildInJsxNamespacedName<T>(node: JsxNamespacedName, cbNode: (node: Node) => T | undefined, _cbNodes?: (nodes: NodeArray<Node>) => T | undefined): T | undefined {
return visitNode(cbNode, node.namespace) ||
visitNode(cbNode, node.name);
},
[SyntaxKind.OptionalType]: forEachChildInOptionalRestOrJSDocParameterModifier,
[SyntaxKind.RestType]: forEachChildInOptionalRestOrJSDocParameterModifier,
[SyntaxKind.JSDocTypeExpression]: forEachChildInOptionalRestOrJSDocParameterModifier,
Expand Down Expand Up @@ -6102,20 +6107,31 @@ namespace Parser {

function parseJsxElementName(): JsxTagNameExpression {
const pos = getNodePos();
scanJsxIdentifier();
// JsxElement can have name in the form of
// propertyAccessExpression
// primaryExpression in the form of an identifier and "this" keyword
// We can't just simply use parseLeftHandSideExpressionOrHigher because then we will start consider class,function etc as a keyword
// We only want to consider "this" as a primaryExpression
let expression: JsxTagNameExpression = token() === SyntaxKind.ThisKeyword ?
parseTokenNode<ThisExpression>() : parseIdentifierName();
let expression: JsxTagNameExpression = parseJsxTagName();
while (parseOptional(SyntaxKind.DotToken)) {
expression = finishNode(factoryCreatePropertyAccessExpression(expression, parseRightSideOfDot(/*allowIdentifierNames*/ true, /*allowPrivateIdentifiers*/ false)), pos) as JsxTagNamePropertyAccess;
}
return expression;
}

function parseJsxTagName(): Identifier | JsxNamespacedName | ThisExpression {
const pos = getNodePos();
scanJsxIdentifier();

const isThis = token() === SyntaxKind.ThisKeyword;
const tagName = parseIdentifierName();
if (parseOptional(SyntaxKind.ColonToken)) {
scanJsxIdentifier();
return finishNode(factory.createJsxNamespacedName(tagName, parseIdentifierName()), pos);
}
return isThis ? finishNode(factory.createToken(SyntaxKind.ThisKeyword), pos) : tagName;
}

function parseJsxExpression(inExpressionContext: boolean): JsxExpression | undefined {
const pos = getNodePos();
if (!parseExpected(SyntaxKind.OpenBraceToken)) {
Expand Down Expand Up @@ -6148,9 +6164,8 @@ namespace Parser {
return parseJsxSpreadAttribute();
}

scanJsxIdentifier();
const pos = getNodePos();
return finishNode(factory.createJsxAttribute(parseIdentifierName(), parseJsxAttributeValue()), pos);
return finishNode(factory.createJsxAttribute(parseJsxAttributeName(), parseJsxAttributeValue()), pos);
}

function parseJsxAttributeValue(): JsxAttributeValue | undefined {
Expand All @@ -6169,6 +6184,18 @@ namespace Parser {
return undefined;
}

function parseJsxAttributeName() {
const pos = getNodePos();
scanJsxIdentifier();

const attrName = parseIdentifierName();
if (parseOptional(SyntaxKind.ColonToken)) {
scanJsxIdentifier();
return finishNode(factory.createJsxNamespacedName(attrName, parseIdentifierName()), pos);
}
return attrName;
}

function parseJsxSpreadAttribute(): JsxSpreadAttribute {
const pos = getNodePos();
parseExpected(SyntaxKind.OpenBraceToken);
Expand Down Expand Up @@ -10425,6 +10452,11 @@ export function tagNamesAreEquivalent(lhs: JsxTagNameExpression, rhs: JsxTagName
return true;
}

if (lhs.kind === SyntaxKind.JsxNamespacedName) {
return lhs.namespace.escapedText === (rhs as JsxNamespacedName).namespace.escapedText &&
lhs.name.escapedText === (rhs as JsxNamespacedName).name.escapedText;
}

// If we are at this statement then we must have PropertyAccessExpression and because tag name in Jsx element can only
// take forms of JsxTagNameExpression which includes an identifier, "this" expression, or another propertyAccessExpression
// it is safe to case the expression property as such. See parseJsxElementName for how we parse tag name in Jsx element
Expand Down
13 changes: 0 additions & 13 deletions src/compiler/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2543,32 +2543,19 @@ export function createScanner(languageVersion: ScriptTarget,
// everything after it to the token
// Do note that this means that `scanJsxIdentifier` effectively _mutates_ the visible token without advancing to a new token
// Any caller should be expecting this behavior and should only read the pos or token value after calling it.
let namespaceSeparator = false;
while (pos < end) {
const ch = text.charCodeAt(pos);
if (ch === CharacterCodes.minus) {
tokenValue += "-";
pos++;
continue;
}
else if (ch === CharacterCodes.colon && !namespaceSeparator) {
tokenValue += ":";
pos++;
namespaceSeparator = true;
token = SyntaxKind.Identifier; // swap from keyword kind to identifier kind
continue;
}
const oldPos = pos;
tokenValue += scanIdentifierParts(); // reuse `scanIdentifierParts` so unicode escapes are handled
if (pos === oldPos) {
break;
}
}
// Do not include a trailing namespace separator in the token, since this is against the spec.
if (tokenValue.slice(-1) === ":") {
tokenValue = tokenValue.slice(0, -1);
pos--;
}
return getIdentifierToken();
}
return token;
Expand Down
Loading

0 comments on commit 0c5be02

Please sign in to comment.