Skip to content

Commit

Permalink
feat(7411): add JSXNamespacedName
Browse files Browse the repository at this point in the history
  • Loading branch information
a-tarasyuk committed Apr 11, 2022
1 parent a1e77ed commit eac179e
Show file tree
Hide file tree
Showing 51 changed files with 778 additions and 369 deletions.
37 changes: 12 additions & 25 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11927,7 +11927,7 @@ namespace ts {
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 @@ -17650,8 +17650,8 @@ namespace ts {
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 @@ -26908,7 +26908,7 @@ namespace ts {
if (!attributesType || isTypeAny(attributesType)) {
return undefined;
}
return getTypeOfPropertyOfContextualType(attributesType, attribute.name.escapedText);
return getTypeOfPropertyOfContextualType(attributesType, getEscapedTextOfJsxAttributeName(attribute.name));
}
else {
return getContextualType(attribute.parent);
Expand Down Expand Up @@ -27962,7 +27962,7 @@ namespace ts {
attributeSymbol.target = member;
attributesTable.set(attributeSymbol.escapedName, attributeSymbol);
allAttributesTable?.set(attributeSymbol.escapedName, attributeSymbol);
if (attributeDecl.name.escapedText === jsxChildrenPropertyName) {
if (getEscapedTextOfJsxAttributeName(attributeDecl.name) === jsxChildrenPropertyName) {
explicitlySpecifyChildrenAttribute = true;
}
}
Expand Down Expand Up @@ -44068,8 +44068,9 @@ namespace ts {
}

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 @@ -44082,25 +44083,11 @@ namespace ts {
}

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 @@ -2747,6 +2747,10 @@
"category": "Error",
"code": 2637
},
"React components cannot include JSX namespace names": {
"category": "Error",
"code": 2638
},

"Cannot augment module '{0}' with value exports because it resolves to a non-module entity.": {
"category": "Error",
Expand Down
8 changes: 8 additions & 0 deletions src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1744,6 +1744,8 @@ namespace ts {
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 @@ -3670,6 +3672,12 @@ namespace ts {
}
}

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

function emitJsxTagName(node: JsxTagNameExpression) {
if (node.kind === SyntaxKind.Identifier) {
emitExpression(node);
Expand Down
23 changes: 23 additions & 0 deletions src/compiler/factory/nodeFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,9 @@ namespace ts {
updateJsxSpreadAttribute,
createJsxExpression,
updateJsxExpression,
createJsxNamespacedName,
updateJsxNamespacedName,

createCaseClause,
updateCaseClause,
createDefaultClause,
Expand Down Expand Up @@ -5036,6 +5039,26 @@ namespace ts {
: 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
4 changes: 4 additions & 0 deletions src/compiler/factory/nodeTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,10 @@ namespace ts {
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
41 changes: 35 additions & 6 deletions src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,9 @@ namespace ts {
visitNode(cbNode, (node as JsxExpression).expression);
case SyntaxKind.JsxClosingElement:
return visitNode(cbNode, (node as JsxClosingElement).tagName);

case SyntaxKind.JsxNamespacedName:
return visitNode(cbNode, (node as JsxNamespacedName).namespace) ||
visitNode(cbNode, (node as JsxNamespacedName).name);
case SyntaxKind.OptionalType:
case SyntaxKind.RestType:
case SyntaxKind.JSDocTypeExpression:
Expand Down Expand Up @@ -5418,20 +5420,31 @@ namespace ts {

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(factory.createPropertyAccessExpression(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 @@ -5464,11 +5477,10 @@ namespace ts {
return parseJsxSpreadAttribute();
}

scanJsxIdentifier();
const pos = getNodePos();
return finishNode(
factory.createJsxAttribute(
parseIdentifierName(),
parseJsxAttributeName(),
token() !== SyntaxKind.EqualsToken ? undefined :
scanJsxAttributeValue() === SyntaxKind.StringLiteral ? parseLiteralNode() as StringLiteral :
parseJsxExpression(/*inExpressionContext*/ true)
Expand All @@ -5477,6 +5489,18 @@ namespace ts {
);
}

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 @@ -9664,6 +9688,11 @@ namespace ts {
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 @@ -2349,32 +2349,19 @@ namespace ts {
// 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
23 changes: 12 additions & 11 deletions src/compiler/transformers/jsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ namespace ts {
if (isJsxSpreadAttribute(elem)) {
spread = true;
}
else if (spread && isJsxAttribute(elem) && elem.name.escapedText === "key") {
else if (spread && isJsxAttribute(elem) && isIdentifier(elem.name) && elem.name.escapedText === "key") {
return true;
}
}
Expand Down Expand Up @@ -518,12 +518,15 @@ namespace ts {
return getTagName(node.openingElement);
}
else {
const name = node.tagName;
if (isIdentifier(name) && isIntrinsicJsxName(name.escapedText)) {
return factory.createStringLiteral(idText(name));
const tagName = node.tagName;
if (isIdentifier(tagName) && isIntrinsicJsxName(tagName.escapedText)) {
return factory.createStringLiteral(idText(tagName));
}
else if (isJsxNamespacedName(tagName)) {
return factory.createStringLiteral(idText(tagName.namespace) + ":" + idText(tagName.name));
}
else {
return createExpressionFromEntityName(factory, name);
return createExpressionFromEntityName(factory, tagName);
}
}
}
Expand All @@ -535,13 +538,11 @@ namespace ts {
*/
function getAttributeName(node: JsxAttribute): StringLiteral | Identifier {
const name = node.name;
const text = idText(name);
if (/^[A-Za-z_]\w*$/.test(text)) {
return name;
}
else {
return factory.createStringLiteral(text);
if (isIdentifier(name)) {
const text = idText(name);
return (/^[A-Za-z_]\w*$/.test(text)) ? name : factory.createStringLiteral(text);
}
return factory.createStringLiteral(idText(name.namespace) + ":" + idText(name.name));
}

function visitJsxExpression(node: JsxExpression) {
Expand Down
Loading

0 comments on commit eac179e

Please sign in to comment.