Skip to content

Commit 1ba8ef6

Browse files
avpfacebook-github-bot
authored andcommitted
Store raw values for string literals as JSX attributes
Summary: Attributes have string literals which are lexed differently than JSXText or ordinary string literals. They can contain newlines, they don't support non-HTML escapes. In order to generate proper JS from these, we store the raw values like we do for template literals. This requires updating the hermes-parser WASM package, which uses StringLiterals, to also convert JSXStringLiterals. Reviewed By: bradzacher Differential Revision: D32900222 fbshipit-source-id: 9c05a226a07bd18f71783bfd4b2af8f60ee67d1b
1 parent 46faa15 commit 1ba8ef6

File tree

15 files changed

+139
-24
lines changed

15 files changed

+139
-24
lines changed

include/hermes/AST/ESTree.def

+4
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,10 @@ ESTREE_NODE_2_ARGS(
645645
ESTREE_NODE_1_ARGS(
646646
JSXSpreadAttribute, Base,
647647
NodePtr, argument, false)
648+
ESTREE_NODE_2_ARGS(
649+
JSXStringLiteral, Base,
650+
NodeString, value, false,
651+
NodeLabel, raw, false)
648652

649653
ESTREE_NODE_2_ARGS(
650654
JSXText, Base,

include/hermes/Parser/JSLexer.h

+10
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,10 @@ class Token {
163163
assert(getKind() == TokenKind::string_literal);
164164
return stringLiteral_;
165165
}
166+
UniqueString *getStringLiteralRawValue() const {
167+
assert(getKind() == TokenKind::string_literal);
168+
return rawString_;
169+
}
166170
bool getStringLiteralContainsEscapes() const {
167171
assert(getKind() == TokenKind::string_literal);
168172
return stringLiteralContainsEscapes_;
@@ -246,6 +250,12 @@ class Token {
246250
stringLiteral_ = literal;
247251
stringLiteralContainsEscapes_ = containsEscapes;
248252
}
253+
void setJSXStringLiteral(UniqueString *literal, UniqueString *raw) {
254+
kind_ = TokenKind::string_literal;
255+
stringLiteral_ = literal;
256+
rawString_ = raw;
257+
stringLiteralContainsEscapes_ = false;
258+
}
249259
void setRegExpLiteral(RegExpLiteral *literal) {
250260
kind_ = TokenKind::regexp_literal;
251261
regExpLiteral_ = literal;

lib/Parser/JSParserImpl-jsx.cpp

+3-1
Original file line numberDiff line numberDiff line change
@@ -329,10 +329,12 @@ Optional<ESTree::Node *> JSParserImpl::parseJSXAttribute() {
329329
// ^
330330
ESTree::Node *value = nullptr;
331331
if (check(TokenKind::string_literal)) {
332+
UniqueString *raw = lexer_.getStringLiteral(tok_->inputStr());
332333
value = setLocation(
333334
tok_,
334335
tok_,
335-
new (context_) ESTree::StringLiteralNode(tok_->getStringLiteral()));
336+
new (context_)
337+
ESTree::JSXStringLiteralNode(tok_->getStringLiteral(), raw));
336338
advance(JSLexer::GrammarContext::AllowJSXIdentifier);
337339
} else {
338340
// { AssignmentExpression }

test/Parser/jsx-entities.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@
3131
// CHECK-NEXT: "name": "foo"
3232
// CHECK-NEXT: },
3333
// CHECK-NEXT: "value": {
34-
// CHECK-NEXT: "type": "StringLiteral",
35-
// CHECK-NEXT: "value": "A & B"
34+
// CHECK-NEXT: "type": "JSXStringLiteral",
35+
// CHECK-NEXT: "value": "A & B",
36+
// CHECK-NEXT: "raw": "\"A &amp; B\""
3637
// CHECK-NEXT: }
3738
// CHECK-NEXT: }
3839
// CHECK-NEXT: ],

test/Parser/jsx.js

+30-20
Original file line numberDiff line numberDiff line change
@@ -265,8 +265,9 @@
265265
// CHECK-NEXT: "name": "b"
266266
// CHECK-NEXT: },
267267
// CHECK-NEXT: "value": {
268-
// CHECK-NEXT: "type": "StringLiteral",
269-
// CHECK-NEXT: "value": "1"
268+
// CHECK-NEXT: "type": "JSXStringLiteral",
269+
// CHECK-NEXT: "value": "1",
270+
// CHECK-NEXT: "raw": "'1'"
270271
// CHECK-NEXT: }
271272
// CHECK-NEXT: },
272273
// CHECK-NEXT: {
@@ -276,8 +277,9 @@
276277
// CHECK-NEXT: "name": "c"
277278
// CHECK-NEXT: },
278279
// CHECK-NEXT: "value": {
279-
// CHECK-NEXT: "type": "StringLiteral",
280-
// CHECK-NEXT: "value": "2"
280+
// CHECK-NEXT: "type": "JSXStringLiteral",
281+
// CHECK-NEXT: "value": "2",
282+
// CHECK-NEXT: "raw": "\"2\""
281283
// CHECK-NEXT: }
282284
// CHECK-NEXT: },
283285
// CHECK-NEXT: {
@@ -535,8 +537,9 @@
535537
// CHECK-NEXT: "name": "c-d"
536538
// CHECK-NEXT: },
537539
// CHECK-NEXT: "value": {
538-
// CHECK-NEXT: "type": "StringLiteral",
539-
// CHECK-NEXT: "value": "hello"
540+
// CHECK-NEXT: "type": "JSXStringLiteral",
541+
// CHECK-NEXT: "value": "hello",
542+
// CHECK-NEXT: "raw": "\"hello\""
540543
// CHECK-NEXT: }
541544
// CHECK-NEXT: },
542545
// CHECK-NEXT: {
@@ -717,8 +720,9 @@
717720
// CHECK-NEXT: "name": "a-b"
718721
// CHECK-NEXT: },
719722
// CHECK-NEXT: "value": {
720-
// CHECK-NEXT: "type": "StringLiteral",
721-
// CHECK-NEXT: "value": "foo"
723+
// CHECK-NEXT: "type": "JSXStringLiteral",
724+
// CHECK-NEXT: "value": "foo",
725+
// CHECK-NEXT: "raw": "\"foo\""
722726
// CHECK-NEXT: }
723727
// CHECK-NEXT: }
724728
// CHECK-NEXT: ],
@@ -750,8 +754,9 @@
750754
// CHECK-NEXT: "name": "foo"
751755
// CHECK-NEXT: },
752756
// CHECK-NEXT: "value": {
753-
// CHECK-NEXT: "type": "StringLiteral",
754-
// CHECK-NEXT: "value": "abc\n def"
757+
// CHECK-NEXT: "type": "JSXStringLiteral",
758+
// CHECK-NEXT: "value": "abc\n def",
759+
// CHECK-NEXT: "raw": "\"abc\n def\""
755760
// CHECK-NEXT: }
756761
// CHECK-NEXT: }
757762
// CHECK-NEXT: ],
@@ -927,8 +932,9 @@
927932
// CHECK-NEXT: "name": "foo"
928933
// CHECK-NEXT: },
929934
// CHECK-NEXT: "value": {
930-
// CHECK-NEXT: "type": "StringLiteral",
931-
// CHECK-NEXT: "value": "\\"
935+
// CHECK-NEXT: "type": "JSXStringLiteral",
936+
// CHECK-NEXT: "value": "\\",
937+
// CHECK-NEXT: "raw": "\"\\\""
932938
// CHECK-NEXT: }
933939
// CHECK-NEXT: },
934940
// CHECK-NEXT: {
@@ -938,8 +944,9 @@
938944
// CHECK-NEXT: "name": "bar"
939945
// CHECK-NEXT: },
940946
// CHECK-NEXT: "value": {
941-
// CHECK-NEXT: "type": "StringLiteral",
942-
// CHECK-NEXT: "value": "\\'"
947+
// CHECK-NEXT: "type": "JSXStringLiteral",
948+
// CHECK-NEXT: "value": "\\'",
949+
// CHECK-NEXT: "raw": "\"\\'\""
943950
// CHECK-NEXT: }
944951
// CHECK-NEXT: },
945952
// CHECK-NEXT: {
@@ -949,8 +956,9 @@
949956
// CHECK-NEXT: "name": "baz"
950957
// CHECK-NEXT: },
951958
// CHECK-NEXT: "value": {
952-
// CHECK-NEXT: "type": "StringLiteral",
953-
// CHECK-NEXT: "value": "\\t\\n"
959+
// CHECK-NEXT: "type": "JSXStringLiteral",
960+
// CHECK-NEXT: "value": "\\t\\n",
961+
// CHECK-NEXT: "raw": "\"\\t\\n\""
954962
// CHECK-NEXT: }
955963
// CHECK-NEXT: }
956964
// CHECK-NEXT: ],
@@ -981,8 +989,9 @@
981989
// CHECK-NEXT: "name": "foo"
982990
// CHECK-NEXT: },
983991
// CHECK-NEXT: "value": {
984-
// CHECK-NEXT: "type": "StringLiteral",
985-
// CHECK-NEXT: "value": "\\"
992+
// CHECK-NEXT: "type": "JSXStringLiteral",
993+
// CHECK-NEXT: "value": "\\",
994+
// CHECK-NEXT: "raw": "'\\'"
986995
// CHECK-NEXT: }
987996
// CHECK-NEXT: },
988997
// CHECK-NEXT: {
@@ -992,8 +1001,9 @@
9921001
// CHECK-NEXT: "name": "bar"
9931002
// CHECK-NEXT: },
9941003
// CHECK-NEXT: "value": {
995-
// CHECK-NEXT: "type": "StringLiteral",
996-
// CHECK-NEXT: "value": "\\\""
1004+
// CHECK-NEXT: "type": "JSXStringLiteral",
1005+
// CHECK-NEXT: "value": "\\\"",
1006+
// CHECK-NEXT: "raw": "'\\\"'"
9971007
// CHECK-NEXT: }
9981008
// CHECK-NEXT: }
9991009
// CHECK-NEXT: ],

tools/hermes-parser/js/hermes-parser/__tests__/HermesParser-test.js

+27
Original file line numberDiff line numberDiff line change
@@ -1610,6 +1610,33 @@ return 1
16101610
});
16111611
});
16121612

1613+
test('Allow JSX String literals', () => {
1614+
expect(parse(`<foo a="abc &amp; def" />;`)).toMatchObject({
1615+
type: 'Program',
1616+
body: [
1617+
{
1618+
type: 'ExpressionStatement',
1619+
expression: {
1620+
type: 'JSXElement',
1621+
openingElement: {
1622+
type: 'JSXOpeningElement',
1623+
attributes: [
1624+
{
1625+
type: 'JSXAttribute',
1626+
value: {
1627+
type: 'Literal',
1628+
value: 'abc & def',
1629+
raw: '"abc &amp; def"',
1630+
},
1631+
},
1632+
],
1633+
},
1634+
},
1635+
},
1636+
],
1637+
});
1638+
});
1639+
16131640
describe('This type annotations', () => {
16141641
test('Removed in Babel mode', () => {
16151642
const params = [

tools/hermes-parser/js/hermes-parser/src/HermesToBabelAdapter.js

+15
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ export default class HermesToBabelAdapter extends HermesASTAdapter {
7373
return this.mapRestElement(node);
7474
case 'ImportExpression':
7575
return this.mapImportExpression(node);
76+
case 'JSXStringLiteral':
77+
return this.mapJSXStringLiteral(node);
7678
case 'PrivateName':
7779
case 'ClassPrivateProperty':
7880
return this.mapPrivateProperty(node);
@@ -343,6 +345,19 @@ export default class HermesToBabelAdapter extends HermesASTAdapter {
343345
};
344346
}
345347

348+
mapJSXStringLiteral(node: HermesNode): HermesNode {
349+
// Babel expects StringLiterals in JSX,
350+
// but Hermes uses JSXStringLiteral to attach the raw value without
351+
// having to internally attach it to every single string literal.
352+
return {
353+
type: 'StringLiteral',
354+
loc: node.loc,
355+
start: node.start,
356+
end: node.end,
357+
value: node.value,
358+
};
359+
}
360+
346361
mapFunction(node: HermesNode): HermesNode {
347362
// Remove the first parameter if it is a this-type annotation,
348363
// which is not recognized by Babel.

tools/hermes-parser/js/hermes-parser/src/HermesToESTreeAdapter.js

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export default class HermesToESTreeAdapter extends HermesASTAdapter {
5555
case 'BooleanLiteral':
5656
case 'StringLiteral':
5757
case 'NumericLiteral':
58+
case 'JSXStringLiteral':
5859
return this.mapSimpleLiteral(node);
5960
case 'BigIntLiteral':
6061
return this.mapBigIntLiteral(node);

tools/hermes-parser/js/scripts/genESTreeJSON.js

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ const NODES_TO_REMOVE = new Set([
3232
'DirectiveLiteral',
3333
// used by hermes to represent an "empty" array destructuring element (`let [,,] = [1,2,3]`)
3434
'Empty',
35+
// used by hermes to avoid adding 'raw' field to actual StringLiteral
36+
'JSXStringLiteral',
3537
// don't know what this is for...
3638
'Metadata',
3739
]);

unsupported/juno/crates/hermes/src/parser/generated_ffi.rs

+4
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ pub enum NodeKind {
118118
JSXClosingElement,
119119
JSXAttribute,
120120
JSXSpreadAttribute,
121+
JSXStringLiteral,
121122
JSXText,
122123
JSXElement,
123124
JSXFragment,
@@ -540,6 +541,9 @@ extern "C" {
540541
pub fn hermes_get_JSXAttribute_value(node: NodePtr) -> NodePtrOpt;
541542
// JSXSpreadAttribute
542543
pub fn hermes_get_JSXSpreadAttribute_argument(node: NodePtr) -> NodePtr;
544+
// JSXStringLiteral
545+
pub fn hermes_get_JSXStringLiteral_value(node: NodePtr) -> NodeString;
546+
pub fn hermes_get_JSXStringLiteral_raw(node: NodePtr) -> NodeLabel;
543547
// JSXText
544548
pub fn hermes_get_JSXText_value(node: NodePtr) -> NodeString;
545549
pub fn hermes_get_JSXText_raw(node: NodePtr) -> NodeLabel;

unsupported/juno/crates/juno/src/gen_js.rs

+14
Original file line numberDiff line numberDiff line change
@@ -1923,6 +1923,20 @@ impl<W: Write> GenJS<W> {
19231923
argument.visit(ctx, self, Some(Path::new(node, NodeField::argument)));
19241924
out!(self, "}}");
19251925
}
1926+
Node::JSXStringLiteral(JSXStringLiteral {
1927+
metadata: _,
1928+
value: _,
1929+
raw,
1930+
}) => {
1931+
let mut buf = [0u8; 4];
1932+
for char in ctx.str(*raw).chars() {
1933+
if char == '\n' {
1934+
self.force_newline_without_indent();
1935+
continue;
1936+
}
1937+
self.write_char(char, &mut buf);
1938+
}
1939+
}
19261940
Node::JSXText(JSXText {
19271941
metadata: _,
19281942
value: _,

unsupported/juno/crates/juno/src/hparser/generated_cvt.rs

+11
Original file line numberDiff line numberDiff line change
@@ -1107,6 +1107,17 @@ pub unsafe fn cvt_node_ptr<'parser, 'gc>(
11071107
template.metadata.range.end = cvt.cvt_smloc(nr.source_range.end.pred());
11081108
ast::builder::JSXSpreadAttribute::build_template(gc, template)
11091109
}
1110+
NodeKind::JSXStringLiteral => {
1111+
let value = cvt_string(hermes_get_JSXStringLiteral_value(n));
1112+
let raw = cvt.cvt_label(gc, hermes_get_JSXStringLiteral_raw(n));
1113+
let mut template = ast::template::JSXStringLiteral {
1114+
metadata: ast::TemplateMetadata {range, ..Default::default()},
1115+
value,
1116+
raw,
1117+
};
1118+
template.metadata.range.end = cvt.cvt_smloc(nr.source_range.end.pred());
1119+
ast::builder::JSXStringLiteral::build_template(gc, template)
1120+
}
11101121
NodeKind::JSXText => {
11111122
let value = cvt_string(hermes_get_JSXText_value(n));
11121123
let raw = cvt.cvt_label(gc, hermes_get_JSXText_raw(n));

unsupported/juno/crates/juno/tests/gen_js/mod.rs

+9
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,15 @@ fn test_jsx() {
504504
test_roundtrip_jsx("<foo />");
505505
test_roundtrip_jsx("<foo></foo>");
506506
test_roundtrip_jsx("<foo>abc</foo>");
507+
test_roundtrip_jsx(
508+
r#"
509+
<asdf desc="foo
510+
bar"
511+
prop2='foo """ bar'>
512+
body
513+
</asdf>
514+
"#,
515+
);
507516
test_roundtrip_jsx("<></>");
508517
test_roundtrip_jsx(
509518
"

unsupported/juno/crates/juno_ast/src/def.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -423,11 +423,15 @@ macro_rules! nodekind_defs {
423423
},
424424
JSXAttribute {
425425
name: &'a Node<'a>[JSXIdentifier, JSXMemberExpression, JSXNamespacedName],
426-
value: Option<&'a Node<'a>>[JSXExpressionContainer, StringLiteral],
426+
value: Option<&'a Node<'a>>[JSXExpressionContainer, JSXStringLiteral],
427427
},
428428
JSXSpreadAttribute {
429429
argument: &'a Node<'a>[Expression],
430430
},
431+
JSXStringLiteral {
432+
value: NodeString,
433+
raw: NodeLabel,
434+
},
431435
JSXText[JSXChild] {
432436
value: NodeString,
433437
raw: NodeLabel,

utils/testsuite/esprima_test_runner.py

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class TestStatus(enum.IntEnum):
2626
"StringLiteral",
2727
"NumericLiteral",
2828
"RegExpLiteral",
29+
"JSXStringLiteral",
2930
}
3031

3132
# These are the keys in the JSON ASTs that should be omitted during diffing.

0 commit comments

Comments
 (0)