1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 *7 * @flow8 */910import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';11import type {HydrationDiffNode} from 'react-reconciler/src/ReactFiberHydrationDiffs';1213import {14 current,15 runWithFiberInDEV,16} from 'react-reconciler/src/ReactCurrentFiber';17import {18 HostComponent,19 HostHoistable,20 HostSingleton,21 HostText,22} from 'react-reconciler/src/ReactWorkTags';2324import {describeDiff} from 'react-reconciler/src/ReactFiberHydrationDiffs';2526function describeAncestors(27 ancestor: Fiber,28 child: Fiber,29 props: null | {children: null},30): string {31 let fiber: null | Fiber = child;32 let node: null | HydrationDiffNode = null;33 let distanceFromLeaf = 0;34 while (fiber) {35 if (fiber === ancestor) {36 distanceFromLeaf = 0;37 }38 node = {39 fiber: fiber,40 children: node !== null ? [node] : [],41 serverProps:42 fiber === child ? props : fiber === ancestor ? null : undefined,43 serverTail: [],44 distanceFromLeaf: distanceFromLeaf,45 };46 distanceFromLeaf++;47 fiber = fiber.return;48 }49 if (node !== null) {50 // Describe the node using the hydration diff logic.51 // Replace + with - to mark ancestor and child. It's kind of arbitrary.52 return describeDiff(node).replaceAll(/^[+-]/gm, '>');53 }54 return '';55}5657type Info = {tag: string};58export type AncestorInfoDev = {59 current: ?Info,6061 formTag: ?Info,62 aTagInScope: ?Info,63 buttonTagInScope: ?Info,64 nobrTagInScope: ?Info,65 pTagInButtonScope: ?Info,6667 listItemTagAutoclosing: ?Info,68 dlItemTagAutoclosing: ?Info,6970 // <head> or <body>71 containerTagInScope: ?Info,72 implicitRootScope: boolean,73};7475// This validation code was written based on the HTML5 parsing spec:76// https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-scope77//78// Note: this does not catch all invalid nesting, nor does it try to (as it's79// not clear what practical benefit doing so provides); instead, we warn only80// for cases where the parser will give a parse tree differing from what React81// intended. For example, <b><div></div></b> is invalid but we don't warn82// because it still parses correctly; we do warn for other cases like nested83// <p> tags where the beginning of the second element implicitly closes the84// first, causing a confusing mess.8586// https://html.spec.whatwg.org/multipage/syntax.html#special87const specialTags = [88 'address',89 'applet',90 'area',91 'article',92 'aside',93 'base',94 'basefont',95 'bgsound',96 'blockquote',97 'body',98 'br',99 'button',100 'caption',101 'center',102 'col',103 'colgroup',104 'dd',105 'details',106 'dir',107 'div',108 'dl',109 'dt',110 'embed',111 'fieldset',112 'figcaption',113 'figure',114 'footer',115 'form',116 'frame',117 'frameset',118 'h1',119 'h2',120 'h3',121 'h4',122 'h5',123 'h6',124 'head',125 'header',126 'hgroup',127 'hr',128 'html',129 'iframe',130 'img',131 'input',132 'isindex',133 'li',134 'link',135 'listing',136 'main',137 'marquee',138 'menu',139 'menuitem',140 'meta',141 'nav',142 'noembed',143 'noframes',144 'noscript',145 'object',146 'ol',147 'p',148 'param',149 'plaintext',150 'pre',151 'script',152 'section',153 'select',154 'source',155 'style',156 'summary',157 'table',158 'tbody',159 'td',160 'template',161 'textarea',162 'tfoot',163 'th',164 'thead',165 'title',166 'tr',167 'track',168 'ul',169 'wbr',170 'xmp',171];172173// https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-scope174const inScopeTags = [175 'applet',176 'caption',177 'html',178 'table',179 'td',180 'th',181 'marquee',182 'object',183 'template',184185 // https://html.spec.whatwg.org/multipage/syntax.html#html-integration-point186 // TODO: Distinguish by namespace here -- for <title>, including it here187 // errs on the side of fewer warnings188 'foreignObject',189 'desc',190 'title',191];192193// https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-button-scope194const buttonScopeTags = __DEV__ ? inScopeTags.concat(['button']) : [];195196// https://html.spec.whatwg.org/multipage/syntax.html#generate-implied-end-tags197const impliedEndTags = [198 'dd',199 'dt',200 'li',201 'option',202 'optgroup',203 'p',204 'rp',205 'rt',206];207208const emptyAncestorInfoDev: AncestorInfoDev = {209 current: null,210211 formTag: null,212 aTagInScope: null,213 buttonTagInScope: null,214 nobrTagInScope: null,215 pTagInButtonScope: null,216217 listItemTagAutoclosing: null,218 dlItemTagAutoclosing: null,219220 containerTagInScope: null,221 implicitRootScope: false,222};223224function updatedAncestorInfoDev(225 oldInfo: null | AncestorInfoDev,226 tag: string,227): AncestorInfoDev {228 if (__DEV__) {229 const ancestorInfo = {...(oldInfo || emptyAncestorInfoDev)};230 const info = {tag};231232 if (inScopeTags.indexOf(tag) !== -1) {233 ancestorInfo.aTagInScope = null;234 ancestorInfo.buttonTagInScope = null;235 ancestorInfo.nobrTagInScope = null;236 }237 if (buttonScopeTags.indexOf(tag) !== -1) {238 ancestorInfo.pTagInButtonScope = null;239 }240241 if (242 specialTags.indexOf(tag) !== -1 &&243 tag !== 'address' &&244 tag !== 'div' &&245 tag !== 'p'246 ) {247 // See rules for 'li', 'dd', 'dt' start tags in248 // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody249 ancestorInfo.listItemTagAutoclosing = null;250 ancestorInfo.dlItemTagAutoclosing = null;251 }252253 ancestorInfo.current = info;254255 if (tag === 'form') {256 ancestorInfo.formTag = info;257 }258 if (tag === 'a') {259 ancestorInfo.aTagInScope = info;260 }261 if (tag === 'button') {262 ancestorInfo.buttonTagInScope = info;263 }264 if (tag === 'nobr') {265 ancestorInfo.nobrTagInScope = info;266 }267 if (tag === 'p') {268 ancestorInfo.pTagInButtonScope = info;269 }270 if (tag === 'li') {271 ancestorInfo.listItemTagAutoclosing = info;272 }273 if (tag === 'dd' || tag === 'dt') {274 ancestorInfo.dlItemTagAutoclosing = info;275 }276 if (tag === '#document' || tag === 'html') {277 ancestorInfo.containerTagInScope = null;278 } else if (!ancestorInfo.containerTagInScope) {279 ancestorInfo.containerTagInScope = info;280 }281282 if (283 oldInfo === null &&284 (tag === '#document' || tag === 'html' || tag === 'body')285 ) {286 // While <head> is also a singleton we don't want to support semantics where287 // you can escape the head by rendering a body singleton so we treat it like a normal scope288 ancestorInfo.implicitRootScope = true;289 } else if (ancestorInfo.implicitRootScope === true) {290 ancestorInfo.implicitRootScope = false;291 }292293 return ancestorInfo;294 } else {295 return null as any;296 }297}298299/**300 * Returns whether301 */302function isTagValidWithParent(303 tag: string,304 parentTag: ?string,305 implicitRootScope: boolean,306): boolean {307 // First, let's check if we're in an unusual parsing mode...308 switch (parentTag) {309 // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inselect310 case 'select':311 return (312 tag === 'hr' ||313 tag === 'option' ||314 tag === 'optgroup' ||315 tag === 'script' ||316 tag === 'template' ||317 tag === '#text'318 );319 case 'optgroup':320 return tag === 'option' || tag === '#text';321 // Strictly speaking, seeing an <option> doesn't mean we're in a <select>322 // but323 case 'option':324 return tag === '#text';325 // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intd326 // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incaption327 // No special behavior since these rules fall back to "in body" mode for328 // all except special table nodes which cause bad parsing behavior anyway.329330 // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intr331 case 'tr':332 return (333 tag === 'th' ||334 tag === 'td' ||335 tag === 'style' ||336 tag === 'script' ||337 tag === 'template'338 );339 // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intbody340 case 'tbody':341 case 'thead':342 case 'tfoot':343 return (344 tag === 'tr' ||345 tag === 'style' ||346 tag === 'script' ||347 tag === 'template'348 );349 // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incolgroup350 case 'colgroup':351 return tag === 'col' || tag === 'template';352 // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intable353 case 'table':354 return (355 tag === 'caption' ||356 tag === 'colgroup' ||357 tag === 'tbody' ||358 tag === 'tfoot' ||359 tag === 'thead' ||360 tag === 'style' ||361 tag === 'script' ||362 tag === 'template'363 );364 // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inhead365 case 'head':366 return (367 tag === 'base' ||368 tag === 'basefont' ||369 tag === 'bgsound' ||370 tag === 'link' ||371 tag === 'meta' ||372 tag === 'title' ||373 tag === 'noscript' ||374 tag === 'noframes' ||375 tag === 'style' ||376 tag === 'script' ||377 tag === 'template'378 );379 // https://html.spec.whatwg.org/multipage/semantics.html#the-html-element380 case 'html':381 if (implicitRootScope) {382 // When our parent tag is html and we're in the root scope we will actually383 // insert most tags into the body so we need to fall through to validating384 // the specific tag with "in body" parsing mode below385 break;386 }387 return tag === 'head' || tag === 'body' || tag === 'frameset';388 case 'frameset':389 return tag === 'frame';390 case '#document':391 if (implicitRootScope) {392 // When our parent is the Document and we're in the root scope we will actually393 // insert most tags into the body so we need to fall through to validating394 // the specific tag with "in body" parsing mode below395 break;396 }397 return tag === 'html';398 }399400 // Probably in the "in body" parsing mode, so we outlaw only tag combos401 // where the parsing rules cause implicit opens or closes to be added.402 // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody403 switch (tag) {404 case 'h1':405 case 'h2':406 case 'h3':407 case 'h4':408 case 'h5':409 case 'h6':410 return (411 parentTag !== 'h1' &&412 parentTag !== 'h2' &&413 parentTag !== 'h3' &&414 parentTag !== 'h4' &&415 parentTag !== 'h5' &&416 parentTag !== 'h6'417 );418419 case 'rp':420 case 'rt':421 // $FlowFixMe[incompatible-type]422 return impliedEndTags.indexOf(parentTag) === -1;423424 case 'caption':425 case 'col':426 case 'colgroup':427 case 'frameset':428 case 'frame':429 case 'tbody':430 case 'td':431 case 'tfoot':432 case 'th':433 case 'thead':434 case 'tr':435 // These tags are only valid with a few parents that have special child436 // parsing rules -- if we're down here, then none of those matched and437 // so we allow it only if we don't know what the parent is, as all other438 // cases are invalid.439 return parentTag == null;440 case 'head':441 // We support rendering <head> in the root when the container is442 // #document, <html>, or <body>.443 return implicitRootScope || parentTag === null;444 case 'html':445 // We support rendering <html> in the root when the container is446 // #document447 return (448 (implicitRootScope && parentTag === '#document') || parentTag === null449 );450 case 'body':451 // We support rendering <body> in the root when the container is452 // #document or <html>453 return (454 (implicitRootScope &&455 (parentTag === '#document' || parentTag === 'html')) ||456 parentTag === null457 );458 }459460 return true;461}462463/**464 * Returns whether465 */466function findInvalidAncestorForTag(467 tag: string,468 ancestorInfo: AncestorInfoDev,469): ?Info {470 switch (tag) {471 case 'address':472 case 'article':473 case 'aside':474 case 'blockquote':475 case 'center':476 case 'details':477 case 'dialog':478 case 'dir':479 case 'div':480 case 'dl':481 case 'fieldset':482 case 'figcaption':483 case 'figure':484 case 'footer':485 case 'header':486 case 'hgroup':487 case 'main':488 case 'menu':489 case 'nav':490 case 'ol':491 case 'p':492 case 'section':493 case 'summary':494 case 'ul':495 case 'pre':496 case 'listing':497 case 'table':498 case 'hr':499 case 'xmp':500 case 'h1':501 case 'h2':502 case 'h3':503 case 'h4':504 case 'h5':505 case 'h6':506 return ancestorInfo.pTagInButtonScope;507508 case 'form':509 return ancestorInfo.formTag || ancestorInfo.pTagInButtonScope;510511 case 'li':512 return ancestorInfo.listItemTagAutoclosing;513514 case 'dd':515 case 'dt':516 return ancestorInfo.dlItemTagAutoclosing;517518 case 'button':519 return ancestorInfo.buttonTagInScope;520521 case 'a':522 // Spec says something about storing a list of markers, but it sounds523 // equivalent to this check.524 return ancestorInfo.aTagInScope;525526 case 'nobr':527 return ancestorInfo.nobrTagInScope;528 }529530 return null;531}532533const didWarn: {[string]: boolean} = {};534535function findAncestor(parent: null | Fiber, tagName: string): null | Fiber {536 while (parent) {537 switch (parent.tag) {538 case HostComponent:539 case HostHoistable:540 case HostSingleton:541 if (parent.type === tagName) {542 return parent;543 }544 }545 parent = parent.return;546 }547 return null;548}549550function validateDOMNesting(551 childTag: string,552 ancestorInfo: AncestorInfoDev,553): boolean {554 if (__DEV__) {555 ancestorInfo = ancestorInfo || emptyAncestorInfoDev;556 const parentInfo = ancestorInfo.current;557 const parentTag = parentInfo && parentInfo.tag;558559 const invalidParent = isTagValidWithParent(560 childTag,561 parentTag,562 ancestorInfo.implicitRootScope,563 )564 ? null565 : parentInfo;566 const invalidAncestor = invalidParent567 ? null568 : findInvalidAncestorForTag(childTag, ancestorInfo);569 const invalidParentOrAncestor = invalidParent || invalidAncestor;570 if (!invalidParentOrAncestor) {571 return true;572 }573574 const ancestorTag = invalidParentOrAncestor.tag;575576 const warnKey =577 // eslint-disable-next-line react-internal/safe-string-coercion578 String(!!invalidParent) + '|' + childTag + '|' + ancestorTag;579 if (didWarn[warnKey]) {580 return false;581 }582 didWarn[warnKey] = true;583584 const child = current;585 const ancestor = child ? findAncestor(child.return, ancestorTag) : null;586587 const ancestorDescription =588 child !== null && ancestor !== null589 ? describeAncestors(ancestor, child, null)590 : '';591592 const tagDisplayName = '<' + childTag + '>';593 if (invalidParent) {594 let info = '';595 if (ancestorTag === 'table' && childTag === 'tr') {596 info +=597 ' Add a <tbody>, <thead> or <tfoot> to your code to match the DOM tree generated by ' +598 'the browser.';599 }600 console.error(601 'In HTML, %s cannot be a child of <%s>.%s\n' +602 'This will cause a hydration error.%s',603 tagDisplayName,604 ancestorTag,605 info,606 ancestorDescription,607 );608 } else {609 console.error(610 'In HTML, %s cannot be a descendant of <%s>.\n' +611 'This will cause a hydration error.%s',612 tagDisplayName,613 ancestorTag,614 ancestorDescription,615 );616 }617 if (child) {618 // For debugging purposes find the nearest ancestor that caused the issue.619 // The stack trace of this ancestor can be useful to find the cause.620 // If the parent is a direct parent in the same owner, we don't bother.621 const parent = child.return;622 if (623 ancestor !== null &&624 parent !== null &&625 (ancestor !== parent || parent._debugOwner !== child._debugOwner)626 ) {627 runWithFiberInDEV(ancestor, () => {628 console.error(629 // We repeat some context because this log might be taken out of context630 // such as in React DevTools or grouped server logs.631 '<%s> cannot contain a nested %s.\n' +632 'See this log for the ancestor stack trace.',633 ancestorTag,634 tagDisplayName,635 );636 });637 }638 }639 return false;640 }641 return true;642}643644function validateTextNesting(645 childText: string,646 parentTag: string,647 implicitRootScope: boolean,648): boolean {649 if (__DEV__) {650 if (implicitRootScope || isTagValidWithParent('#text', parentTag, false)) {651 return true;652 }653654 const warnKey = '#text|' + parentTag;655 if (didWarn[warnKey]) {656 return false;657 }658 didWarn[warnKey] = true;659660 const child = current;661 const ancestor = child ? findAncestor(child, parentTag) : null;662663 const ancestorDescription =664 child !== null && ancestor !== null665 ? describeAncestors(666 ancestor,667 child,668 child.tag !== HostText ? {children: null} : null,669 )670 : '';671672 if (/\S/.test(childText)) {673 console.error(674 'In HTML, text nodes cannot be a child of <%s>.\n' +675 'This will cause a hydration error.%s',676 parentTag,677 ancestorDescription,678 );679 } else {680 console.error(681 'In HTML, whitespace text nodes cannot be a child of <%s>. ' +682 "Make sure you don't have any extra whitespace between tags on " +683 'each line of your source code.\n' +684 'This will cause a hydration error.%s',685 parentTag,686 ancestorDescription,687 );688 }689 return false;690 }691 return true;692}693694export {updatedAncestorInfoDev, validateDOMNesting, validateTextNesting};
Findings
✓ No findings reported for this file.