Coverage Report

Created: 2025-12-12 07:27

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/hermes/lib/Parser/JSParserImpl-jsx.cpp
Line
Count
Source
1
/*
2
 * Copyright (c) Meta Platforms, Inc. and affiliates.
3
 *
4
 * This source code is licensed under the MIT license found in the
5
 * LICENSE file in the root directory of this source tree.
6
 */
7
8
#include "JSParserImpl.h"
9
10
#include "llvh/Support/SaveAndRestore.h"
11
12
using llvh::cast;
13
using llvh::dyn_cast;
14
using llvh::isa;
15
16
namespace hermes {
17
namespace parser {
18
namespace detail {
19
20
#if HERMES_PARSE_JSX
21
22
0
Optional<ESTree::Node *> JSParserImpl::parseJSX() {
23
0
  assert(check(TokenKind::less));
24
0
  llvh::SaveAndRestore<uint32_t> saveDepth(jsxDepth_, 0);
25
0
  SMLoc start = advance(JSLexer::GrammarContext::AllowJSXIdentifier).Start;
26
0
  if (check(TokenKind::greater)) {
27
0
    return parseJSXFragment(start);
28
0
  }
29
0
  return parseJSXElement(start);
30
0
}
31
32
/// \return true if the opening and closing tag names match, which is needed
33
/// to define a JSXElement.
34
static bool tagNamesMatch(
35
    ESTree::JSXOpeningElementNode *opening,
36
0
    ESTree::JSXClosingElementNode *closing) {
37
  // Loop over member expressions or namespace names, stopping when both
38
  // `name1` and `name2` are the same JSXIdentifier or when they mismatch.
39
0
  ESTree::Node *name1 = opening->_name;
40
0
  ESTree::Node *name2 = closing->_name;
41
0
  for (;;) {
42
0
    if (auto *name1ID = dyn_cast<ESTree::JSXIdentifierNode>(name1)) {
43
0
      if (auto *name2ID = dyn_cast<ESTree::JSXIdentifierNode>(name2)) {
44
0
        return name1ID->_name == name2ID->_name;
45
0
      }
46
0
      return false;
47
0
    } else if (auto *name1NS = dyn_cast<ESTree::JSXNamespacedNameNode>(name1)) {
48
0
      if (auto *name2NS = dyn_cast<ESTree::JSXNamespacedNameNode>(name2)) {
49
        // ESTree spec dictates that both namespace and name are JSXIdentifier.
50
0
        auto *name1NSID = cast<ESTree::JSXIdentifierNode>(name1NS->_namespace);
51
0
        auto *name1ID = cast<ESTree::JSXIdentifierNode>(name1NS->_name);
52
0
        auto *name2NSID = cast<ESTree::JSXIdentifierNode>(name2NS->_namespace);
53
0
        auto *name2ID = cast<ESTree::JSXIdentifierNode>(name2NS->_name);
54
0
        return name1NSID->_name == name2NSID->_name &&
55
0
            name1ID->_name == name2ID->_name;
56
0
      }
57
0
      return false;
58
0
    } else {
59
0
      auto *name1ME = cast<ESTree::JSXMemberExpressionNode>(name1);
60
0
      if (auto *name2ME = dyn_cast<ESTree::JSXMemberExpressionNode>(name2)) {
61
0
        auto *name1ID = cast<ESTree::JSXIdentifierNode>(name1ME->_property);
62
0
        auto *name2ID = cast<ESTree::JSXIdentifierNode>(name2ME->_property);
63
0
        if (name1ID->_name != name2ID->_name) {
64
0
          return false;
65
0
        }
66
        // Both names are JSXMemberExpression with matching property names.
67
        // Compare the object names.
68
0
        name1 = name1ME->_object;
69
0
        name2 = name2ME->_object;
70
0
        continue;
71
0
      }
72
0
      return false;
73
0
    }
74
0
  }
75
0
}
76
77
0
Optional<ESTree::Node *> JSParserImpl::parseJSXElement(SMLoc start) {
78
0
  llvh::SaveAndRestore<uint32_t> saveDepth(jsxDepth_, jsxDepth_ + 1);
79
0
  auto optOpening = parseJSXOpeningElement(start);
80
0
  if (!optOpening)
81
0
    return None;
82
0
  if ((*optOpening)->_selfClosing) {
83
0
    return setLocation(
84
0
        start,
85
0
        *optOpening,
86
0
        new (context_) ESTree::JSXElementNode(*optOpening, {}, nullptr));
87
0
  }
88
0
  ESTree::JSXOpeningElementNode *opening = *optOpening;
89
90
  // Parse JSXChildren.
91
0
  ESTree::NodeList children{};
92
93
0
  auto optClosing = parseJSXChildren(children);
94
0
  if (!optClosing)
95
0
    return None;
96
97
  // Check that the closing is not a fragment and that the name matches.
98
0
  if (ESTree::JSXClosingElementNode *closing =
99
0
          dyn_cast<ESTree::JSXClosingElementNode>(*optClosing)) {
100
0
    if (!tagNamesMatch(opening, closing)) {
101
0
      error((*optClosing)->getSourceRange(), "Closing tag must match opening");
102
0
      sm_.note(opening->getSourceRange().Start, "location of opening");
103
0
    }
104
0
  } else {
105
0
    error(
106
0
        (*optClosing)->getSourceRange(), "Closing tag must not be a fragment");
107
0
    sm_.note(opening->getSourceRange().Start, "location of opening");
108
0
  }
109
110
0
  return setLocation(
111
0
      start,
112
0
      *optClosing,
113
0
      new (context_)
114
0
          ESTree::JSXElementNode(opening, std::move(children), *optClosing));
115
0
}
116
117
Optional<ESTree::JSXOpeningElementNode *> JSParserImpl::parseJSXOpeningElement(
118
0
    SMLoc start) {
119
0
  auto optName = parseJSXElementName(AllowJSXMemberExpression::Yes);
120
0
  if (!optName)
121
0
    return None;
122
0
  ESTree::Node *name = *optName;
123
124
0
  ESTree::Node *typeArgs = nullptr;
125
0
  if (check(TokenKind::less)) {
126
0
    auto optTypeArgs = parseTypeArgsFlow();
127
0
    if (!optTypeArgs) {
128
0
      return None;
129
0
    }
130
0
    typeArgs = *optTypeArgs;
131
0
  }
132
133
0
  ESTree::NodeList attributes{};
134
0
  while (!check(TokenKind::slash, TokenKind::greater)) {
135
0
    if (check(TokenKind::l_brace)) {
136
0
      auto optSpread = parseJSXSpreadAttribute();
137
0
      if (!optSpread)
138
0
        return None;
139
0
      attributes.push_back(**optSpread);
140
0
      continue;
141
0
    }
142
143
0
    auto optAttr = parseJSXAttribute();
144
0
    if (!optAttr)
145
0
      return None;
146
0
    attributes.push_back(**optAttr);
147
0
  }
148
149
0
  bool selfClosing = checkAndEat(TokenKind::slash);
150
151
0
  SMLoc end = tok_->getEndLoc();
152
0
  if (!need(TokenKind::greater, "at end of JSX tag", "start of tag", start))
153
0
    return None;
154
155
0
  if (selfClosing && jsxDepth_ <= 1) {
156
    // Done with JSX for now, return to standard JS mode.
157
0
    advance();
158
0
  } else {
159
    // Still in JSX, children after this element.
160
0
    lexer_.advanceInJSXChild();
161
0
  }
162
163
0
  return setLocation(
164
0
      start,
165
0
      end,
166
0
      new (context_) ESTree::JSXOpeningElementNode(
167
0
          name, std::move(attributes), selfClosing, typeArgs));
168
0
}
169
170
0
Optional<ESTree::Node *> JSParserImpl::parseJSXFragment(SMLoc start) {
171
0
  assert(check(TokenKind::greater));
172
  // JSXFragment:
173
  // < > JSXChildren[opt] < / >
174
  //   ^
175
0
  llvh::SaveAndRestore<uint32_t> saveDepth(jsxDepth_, jsxDepth_ + 1);
176
0
  ESTree::Node *opening =
177
0
      setLocation(start, tok_, new (context_) ESTree::JSXOpeningFragmentNode());
178
0
  lexer_.advanceInJSXChild();
179
180
  // Parse JSXChildren.
181
0
  ESTree::NodeList children{};
182
183
0
  auto optClosing = parseJSXChildren(children);
184
0
  if (!optClosing)
185
0
    return None;
186
187
  // Check that the closing is a fragment.
188
0
  if (!isa<ESTree::JSXClosingFragmentNode>(*optClosing)) {
189
0
    error((*optClosing)->getSourceRange(), "Closing tag must be a fragment");
190
0
    lexer_.getSourceMgr().note(
191
0
        opening->getSourceRange().Start, "location of opening");
192
0
    return None;
193
0
  }
194
195
0
  return setLocation(
196
0
      start,
197
0
      *optClosing,
198
0
      new (context_)
199
0
          ESTree::JSXFragmentNode(opening, std::move(children), *optClosing));
200
0
}
201
202
Optional<ESTree::Node *> JSParserImpl::parseJSXChildren(
203
0
    ESTree::NodeList &children) {
204
  // Keep looping until we encounter a closing element or a JSXClosingFragment.
205
0
  for (;;) {
206
0
    if (check(TokenKind::less)) {
207
      // JSXElement or closing tag.
208
0
      SMLoc start = advance(JSLexer::GrammarContext::AllowJSXIdentifier).Start;
209
0
      if (check(TokenKind::slash)) {
210
        // < /
211
        //   ^
212
        // Start of a JSXClosingElement or JSXClosingFragment.
213
0
        auto optClosing = parseJSXClosing(start);
214
0
        if (!optClosing)
215
0
          return None;
216
0
        return *optClosing;
217
0
      }
218
      // Using a JSXFragment as a child node appears to be disallowed by the
219
      // spec, but code frequently uses this pattern and all parsers appear to
220
      // support it.
221
0
      auto optElem = check(TokenKind::greater) ? parseJSXFragment(start)
222
0
                                               : parseJSXElement(start);
223
0
      if (!optElem)
224
0
        return None;
225
0
      children.push_back(**optElem);
226
0
    } else if (check(TokenKind::l_brace)) {
227
      // { JSXChildExpression[opt] }
228
      // ^
229
0
      SMRange startRange = advance();
230
0
      SMLoc start = startRange.Start;
231
0
      if (check(TokenKind::r_brace)) {
232
        // { }
233
        //   ^
234
0
        SMRange endRange = tok_->getSourceRange();
235
0
        children.push_back(*setLocation(
236
0
            start,
237
0
            endRange.End,
238
0
            new (context_) ESTree::JSXExpressionContainerNode(setLocation(
239
0
                startRange.End,
240
0
                endRange.Start,
241
0
                new (context_) ESTree::JSXEmptyExpressionNode()))));
242
0
      } else {
243
        // { JSXChildExpression }
244
        //   ^
245
0
        auto optChildExpr = parseJSXChildExpression(start);
246
0
        if (!optChildExpr)
247
0
          return None;
248
0
        if (!need(
249
0
                TokenKind::r_brace,
250
0
                "in JSX child expression",
251
0
                "start of expression",
252
0
                start))
253
0
          return None;
254
0
        children.push_back(**optChildExpr);
255
0
      }
256
0
      lexer_.advanceInJSXChild();
257
0
    } else {
258
      /// JSXText handled by the lexer.
259
0
      if (!need(TokenKind::jsx_text, "in JSX child expression", nullptr, {}))
260
0
        return None;
261
0
      children.push_back(*setLocation(
262
0
          tok_,
263
0
          tok_,
264
0
          new (context_) ESTree::JSXTextNode(
265
0
              tok_->getJSXTextValue(), tok_->getJSXTextRaw())));
266
0
      lexer_.advanceInJSXChild();
267
0
    }
268
0
  }
269
0
}
270
271
0
Optional<ESTree::Node *> JSParserImpl::parseJSXChildExpression(SMLoc start) {
272
0
  if (checkAndEat(TokenKind::dotdotdot)) {
273
0
    auto optAssign = parseAssignmentExpression();
274
0
    if (!optAssign)
275
0
      return None;
276
0
    return setLocation(
277
0
        start, tok_, new (context_) ESTree::JSXSpreadChildNode(*optAssign));
278
0
  }
279
0
  auto optAssign = parseAssignmentExpression();
280
0
  if (!optAssign)
281
0
    return None;
282
0
  return setLocation(
283
0
      start,
284
0
      tok_,
285
0
      new (context_) ESTree::JSXExpressionContainerNode(*optAssign));
286
0
}
287
288
0
Optional<ESTree::Node *> JSParserImpl::parseJSXSpreadAttribute() {
289
0
  assert(check(TokenKind::l_brace));
290
0
  SMLoc start = advance().Start;
291
292
  // { ... AssignmentExpression }
293
  //   ^
294
295
0
  if (!eat(
296
0
          TokenKind::dotdotdot,
297
0
          JSLexer::GrammarContext::AllowRegExp,
298
0
          "in JSX spread attribute",
299
0
          "location of attribute",
300
0
          start))
301
0
    return None;
302
303
0
  auto optAssign = parseAssignmentExpression();
304
0
  if (!optAssign)
305
0
    return None;
306
307
0
  SMLoc end = tok_->getEndLoc();
308
0
  if (!eat(
309
0
          TokenKind::r_brace,
310
0
          JSLexer::GrammarContext::AllowJSXIdentifier,
311
0
          "in JSX spread attribute",
312
0
          "location of attribute",
313
0
          start))
314
0
    return None;
315
316
0
  return setLocation(
317
0
      start, end, new (context_) ESTree::JSXSpreadAttributeNode(*optAssign));
318
0
}
319
320
0
Optional<ESTree::Node *> JSParserImpl::parseJSXAttribute() {
321
0
  SMLoc start = tok_->getStartLoc();
322
323
0
  auto optName = parseJSXElementName(AllowJSXMemberExpression::No);
324
0
  if (!optName)
325
0
    return None;
326
0
  ESTree::Node *name = *optName;
327
328
0
  if (!checkAndEat(
329
0
          TokenKind::equal, JSLexer::GrammarContext::AllowJSXIdentifier)) {
330
0
    return setLocation(
331
0
        *optName,
332
0
        *optName,
333
0
        new (context_) ESTree::JSXAttributeNode(*optName, nullptr));
334
0
  }
335
336
  // JSXAttributeInitializer:
337
  // = JSXAttributeValue
338
  //   ^
339
0
  ESTree::Node *value = nullptr;
340
0
  if (check(TokenKind::string_literal)) {
341
0
    UniqueString *raw = lexer_.getStringLiteral(tok_->inputStr());
342
0
    value = setLocation(
343
0
        tok_,
344
0
        tok_,
345
0
        new (context_)
346
0
            ESTree::JSXStringLiteralNode(tok_->getStringLiteral(), raw));
347
0
    advance(JSLexer::GrammarContext::AllowJSXIdentifier);
348
0
  } else {
349
    // { AssignmentExpression }
350
    // ^
351
0
    if (!need(
352
0
            TokenKind::l_brace,
353
0
            "in JSX attribute",
354
0
            "location of attribute",
355
0
            start))
356
0
      return None;
357
358
0
    SMLoc valueStart = advance().Start;
359
360
0
    auto optAssign = parseAssignmentExpression();
361
0
    if (!optAssign)
362
0
      return None;
363
364
0
    SMLoc valueEnd = tok_->getEndLoc();
365
0
    if (!eat(
366
0
            TokenKind::r_brace,
367
0
            JSLexer::GrammarContext::AllowJSXIdentifier,
368
0
            "in JSX attribute",
369
0
            "location of attribute",
370
0
            start))
371
0
      return None;
372
373
0
    value = setLocation(
374
0
        valueStart,
375
0
        valueEnd,
376
0
        new (context_) ESTree::JSXExpressionContainerNode(*optAssign));
377
0
  }
378
379
0
  assert(value && "value must be set by one of the branches");
380
381
0
  return setLocation(
382
0
      start, value, new (context_) ESTree::JSXAttributeNode(name, value));
383
0
}
384
385
0
Optional<ESTree::Node *> JSParserImpl::parseJSXClosing(SMLoc start) {
386
0
  assert(check(TokenKind::slash));
387
0
  advance(JSLexer::GrammarContext::AllowJSXIdentifier);
388
389
0
  if (check(TokenKind::greater)) {
390
0
    SMLoc end = tok_->getEndLoc();
391
0
    if (jsxDepth_ > 1) {
392
0
      lexer_.advanceInJSXChild();
393
0
    } else {
394
      // Done with JSX, advance normally.
395
0
      advance();
396
0
    }
397
0
    return setLocation(
398
0
        start, end, new (context_) ESTree::JSXClosingFragmentNode());
399
0
  }
400
401
0
  auto optName = parseJSXElementName(AllowJSXMemberExpression::Yes);
402
0
  if (!optName)
403
0
    return None;
404
405
0
  if (!need(
406
0
          TokenKind::greater,
407
0
          "at end of JSX closing tag",
408
0
          "start of tag",
409
0
          start))
410
0
    return None;
411
412
0
  SMLoc end = tok_->getEndLoc();
413
0
  if (jsxDepth_ > 1) {
414
0
    lexer_.advanceInJSXChild();
415
0
  } else {
416
    // Done with JSX, advance normally.
417
0
    advance();
418
0
  }
419
420
0
  return setLocation(
421
0
      start, end, new (context_) ESTree::JSXClosingElementNode(*optName));
422
0
}
423
424
Optional<ESTree::Node *> JSParserImpl::parseJSXElementName(
425
0
    AllowJSXMemberExpression allowJSXMemberExpression) {
426
0
  SMLoc start = tok_->getStartLoc();
427
428
0
  if (!check(TokenKind::identifier) && !tok_->isResWord()) {
429
0
    errorExpected(TokenKind::identifier, "as JSX element name", nullptr, {});
430
0
    return None;
431
0
  }
432
433
0
  ESTree::Node *name = setLocation(
434
0
      start,
435
0
      tok_,
436
0
      new (context_) ESTree::JSXIdentifierNode(tok_->getResWordOrIdentifier()));
437
0
  advance(JSLexer::GrammarContext::AllowJSXIdentifier);
438
439
0
  if (check(TokenKind::colon)) {
440
    // JSXNamespacedName:
441
    // JSXIdentifier : JSXIdentifier
442
    // ^
443
0
    advance(JSLexer::GrammarContext::AllowJSXIdentifier);
444
0
    if (!check(TokenKind::identifier) && !tok_->isResWord()) {
445
0
      errorExpected(
446
0
          TokenKind::identifier,
447
0
          "in JSX element name",
448
0
          "start of JSX element name",
449
0
          start);
450
0
      return None;
451
0
    }
452
453
0
    ESTree::Node *child = setLocation(
454
0
        tok_,
455
0
        tok_,
456
0
        new (context_)
457
0
            ESTree::JSXIdentifierNode(tok_->getResWordOrIdentifier()));
458
0
    advance(JSLexer::GrammarContext::AllowJSXIdentifier);
459
0
    return setLocation(
460
0
        start,
461
0
        child,
462
0
        new (context_) ESTree::JSXNamespacedNameNode(name, child));
463
0
  }
464
465
0
  while (check(TokenKind::period)) {
466
    // JSXNamespacedName:
467
    // JSXMemberExpression . JSXIdentifier
468
    // ^
469
0
    advance(JSLexer::GrammarContext::AllowJSXIdentifier);
470
0
    if (!check(TokenKind::identifier) && !tok_->isResWord()) {
471
0
      errorExpected(
472
0
          TokenKind::identifier,
473
0
          "in JSX element name",
474
0
          "start of JSX element name",
475
0
          start);
476
0
      return None;
477
0
    }
478
479
0
    ESTree::Node *child = setLocation(
480
0
        tok_,
481
0
        tok_,
482
0
        new (context_)
483
0
            ESTree::JSXIdentifierNode(tok_->getResWordOrIdentifier()));
484
0
    advance(JSLexer::GrammarContext::AllowJSXIdentifier);
485
486
0
    name = setLocation(
487
0
        start,
488
0
        child,
489
0
        new (context_) ESTree::JSXMemberExpressionNode(name, child));
490
0
  }
491
492
0
  if (isa<ESTree::MemberExpressionNode>(name) &&
493
0
      allowJSXMemberExpression == AllowJSXMemberExpression::No) {
494
0
    error(name->getSourceRange(), "unexpected member expression");
495
0
  }
496
497
0
  return name;
498
0
}
499
500
#endif
501
502
} // namespace detail
503
} // namespace parser
504
} // namespace hermes