@samitouri / QOS-React-2 / commits / 64b1f05e75

Add more context to DOM nesting warning

Better version of #3578.

Ben Alpert committed Apr 21, 2015 at 21:55 UTC 64b1f05e7518e46c13ca9dbe8d2792054dd9a515
6 files changed +327 -141
src/browser/__tests__/validateDOMNesting-test.js
+10 -4
@@ -11,7 +11,7 @@
11
12 'use strict';
13
14 -var isTagValidInContext;
14 +var validateDOMNesting;
15
16 // https://html.spec.whatwg.org/multipage/syntax.html#special
17 var specialTags = [
@@ -34,10 +34,13 @@ var formattingTags = [
34 ];
35
36 function isTagStackValid(stack) {
37 + var ancestorInfo = null;
38 for (var i = 0; i < stack.length; i++) {
38 - if (!isTagValidInContext(stack[i], stack.slice(0, i))) {
39 + if (!validateDOMNesting.isTagValidInContext(stack[i], ancestorInfo)) {
40 return false;
41 }
42 + ancestorInfo =
43 + validateDOMNesting.updatedAncestorInfo(ancestorInfo, stack[i], null);
44 }
45 return true;
46 }
@@ -46,14 +49,14 @@ describe('ReactContextValidator', function() {
49 beforeEach(function() {
50 require('mock-modules').dumpCache();
51
49 - isTagValidInContext = require('validateDOMNesting').isTagValidInContext;
52 + validateDOMNesting = require('validateDOMNesting');
53 });
54
55 it('allows any tag with no context', function() {
56 // With renderToString (for example), we don't know where we're mounting the
57 // tag so we must err on the side of leniency.
58 specialTags.concat(formattingTags, ['mysterytag']).forEach(function(tag) {
56 - expect(isTagValidInContext(tag, [])).toBe(true);
59 + expect(validateDOMNesting.isTagValidInContext(tag, null)).toBe(true);
60 });
61 });
62
@@ -67,6 +70,8 @@ describe('ReactContextValidator', function() {
70 // Invalid, but not changed by browser parsing so we allow them
71 expect(isTagStackValid(['div', 'ul', 'ul', 'li'])).toBe(true);
72 expect(isTagStackValid(['div', 'label', 'div'])).toBe(true);
73 + expect(isTagStackValid(['div', 'ul', 'li', 'section', 'li'])).toBe(true);
74 + expect(isTagStackValid(['div', 'ul', 'li', 'dd', 'li'])).toBe(true);
75 });
76
77 it('prevents problematic nestings', function() {
@@ -74,5 +79,6 @@ describe('ReactContextValidator', function() {
79 expect(isTagStackValid(['form', 'form'])).toBe(false);
80 expect(isTagStackValid(['p', 'p'])).toBe(false);
81 expect(isTagStackValid(['table', 'tr'])).toBe(false);
82 + expect(isTagStackValid(['div', 'ul', 'li', 'div', 'li'])).toBe(false);
83 });
84 });
src/browser/ui/ReactDOMComponent.js
+9 -8
@@ -222,12 +222,13 @@ function validateDangerousTag(tag) {
222 }
223 }
224
225 -function processChildContext(context, tagName) {
225 +function processChildContext(context, inst) {
226 if (__DEV__) {
227 // Pass down our tag name to child components for validation purposes
228 context = assign({}, context);
229 - var stack = context[validateDOMNesting.tagStackContextKey] || [];
230 - context[validateDOMNesting.tagStackContextKey] = stack.concat([tagName]);
229 + var info = context[validateDOMNesting.ancestorInfoContextKey];
230 + context[validateDOMNesting.ancestorInfoContextKey] =
231 + validateDOMNesting.updatedAncestorInfo(info, inst._tag, inst);
232 }
233 return context;
234 }
@@ -278,11 +279,11 @@ ReactDOMComponent.Mixin = {
279
280 assertValidProps(this, this._currentElement.props);
281 if (__DEV__) {
281 - if (context[validateDOMNesting.tagStackContextKey]) {
282 + if (context[validateDOMNesting.ancestorInfoContextKey]) {
283 validateDOMNesting(
283 - context[validateDOMNesting.tagStackContextKey],
284 this._tag,
285 - this._currentElement
285 + this,
286 + context[validateDOMNesting.ancestorInfoContextKey]
287 );
288 }
289 }
@@ -379,7 +380,7 @@ ReactDOMComponent.Mixin = {
380 var mountImages = this.mountChildren(
381 childrenToUse,
382 transaction,
382 - processChildContext(context, this._tag)
383 + processChildContext(context, this)
384 );
385 ret = mountImages.join('');
386 }
@@ -431,7 +432,7 @@ ReactDOMComponent.Mixin = {
432 this._updateDOMChildren(
433 prevElement.props,
434 transaction,
434 - processChildContext(context, this._tag)
435 + processChildContext(context, this)
436 );
437 },
438
src/browser/ui/ReactDOMTextComponent.js
+3 -3
@@ -67,11 +67,11 @@ assign(ReactDOMTextComponent.prototype, {
67 */
68 mountComponent: function(rootID, transaction, context) {
69 if (__DEV__) {
70 - if (context[validateDOMNesting.tagStackContextKey]) {
70 + if (context[validateDOMNesting.ancestorInfoContextKey]) {
71 validateDOMNesting(
72 - context[validateDOMNesting.tagStackContextKey],
72 'span',
74 - null
73 + null,
74 + context[validateDOMNesting.ancestorInfoContextKey]
75 );
76 }
77 }
src/browser/ui/ReactMount.js
+3 -2
@@ -268,8 +268,9 @@ function mountComponentIntoNode(
268 if (context === emptyObject) {
269 context = {};
270 }
271 - context[validateDOMNesting.tagStackContextKey] =
272 - [container.nodeName.toLowerCase()];
271 + var tag = container.nodeName.toLowerCase();
272 + context[validateDOMNesting.ancestorInfoContextKey] =
273 + validateDOMNesting.updatedAncestorInfo(null, tag, null);
274 }
275 var markup = ReactReconciler.mountComponent(
276 componentInstance, rootID, transaction, context
src/browser/ui/__tests__/ReactDOMComponent-test.js
+77 -6
@@ -740,7 +740,7 @@ describe('ReactDOMComponent', function() {
740 expect(console.error.calls.length).toBe(1);
741 expect(console.error.calls[0].args[0]).toBe(
742 'Warning: validateDOMNesting(...): <tr> cannot appear as a child of ' +
743 - '<div> in this context (div > div).'
743 + '<div>. See div > tr.'
744 );
745 });
746
@@ -752,15 +752,20 @@ describe('ReactDOMComponent', function() {
752 expect(console.error.calls.length).toBe(1);
753 expect(console.error.calls[0].args[0]).toBe(
754 'Warning: validateDOMNesting(...): <tr> cannot appear as a child of ' +
755 - '<p> in this context (p).'
755 + '<p>. See p > tr.'
756 );
757 });
758
759 it('warns nicely for table rows', () => {
760 spyOn(console, 'error');
761 + var Row = React.createClass({
762 + render: function() {
763 + return <tr />;
764 + }
765 + });
766 var Foo = React.createClass({
767 render: function() {
763 - return <table><tr /></table>;
768 + return <table><Row /></table>;
769 }
770 });
771 ReactTestUtils.renderIntoDocument(<Foo />);
@@ -768,9 +773,75 @@ describe('ReactDOMComponent', function() {
773 expect(console.error.calls.length).toBe(1);
774 expect(console.error.calls[0].args[0]).toBe(
775 'Warning: validateDOMNesting(...): <tr> cannot appear as a child of ' +
771 - '<table> in this context (div > table). Add a <tbody> to your code ' +
772 - 'to match the DOM tree generated by the browser. Check the render ' +
773 - 'method of `Foo`.'
776 + '<table>. See Foo > table > Row > tr. Add a <tbody> to your code to ' +
777 + 'match the DOM tree generated by the browser.'
778 + );
779 + });
780 +
781 + it('gives useful context in warnings', () => {
782 + spyOn(console, 'error');
783 + var Row = React.createClass({
784 + render: () => <tr />
785 + });
786 + var FancyRow = React.createClass({
787 + render: () => <Row />
788 + });
789 + var Table = React.createClass({
790 + render: function() { return <table>{this.props.children}</table>; }
791 + });
792 + var FancyTable = React.createClass({
793 + render: function() { return <Table>{this.props.children}</Table>; }
794 + });
795 +
796 + var Viz1 = React.createClass({
797 + render: () => <table><FancyRow /></table>
798 + });
799 + var App1 = React.createClass({
800 + render: () => <Viz1 />
801 + });
802 + ReactTestUtils.renderIntoDocument(<App1 />);
803 + expect(console.error.calls.length).toBe(1);
804 + expect(console.error.calls[0].args[0]).toContain(
805 + 'See Viz1 > table > FancyRow > Row > tr.'
806 + );
807 +
808 + var Viz2 = React.createClass({
809 + render: () => <FancyTable><FancyRow /></FancyTable>
810 + });
811 + var App2 = React.createClass({
812 + render: () => <Viz2 />
813 + });
814 + ReactTestUtils.renderIntoDocument(<App2 />);
815 + expect(console.error.calls.length).toBe(2);
816 + expect(console.error.calls[1].args[0]).toContain(
817 + 'See Viz2 > FancyTable > Table > table > FancyRow > Row > tr.'
818 + );
819 +
820 + ReactTestUtils.renderIntoDocument(<FancyTable><FancyRow /></FancyTable>);
821 + expect(console.error.calls.length).toBe(3);
822 + expect(console.error.calls[2].args[0]).toContain(
823 + 'See FancyTable > Table > table > FancyRow > Row > tr.'
824 + );
825 +
826 + ReactTestUtils.renderIntoDocument(<table><FancyRow /></table>);
827 + expect(console.error.calls.length).toBe(4);
828 + expect(console.error.calls[3].args[0]).toContain(
829 + 'See table > FancyRow > Row > tr.'
830 + );
831 +
832 + ReactTestUtils.renderIntoDocument(<FancyTable><tr /></FancyTable>);
833 + expect(console.error.calls.length).toBe(5);
834 + expect(console.error.calls[4].args[0]).toContain(
835 + 'See FancyTable > Table > table > tr.'
836 + );
837 +
838 + var Link = React.createClass({
839 + render: function() { return <a>{this.props.children}</a>; }
840 + });
841 + ReactTestUtils.renderIntoDocument(<Link><div><Link /></div></Link>);
842 + expect(console.error.calls.length).toBe(6);
843 + expect(console.error.calls[5].args[0]).toContain(
844 + 'See Link > a > ... > Link > a.'
845 );
846 });
847 });
src/browser/validateDOMNesting.js
+225 -118
@@ -11,6 +11,7 @@
11
12 'use strict';
13
14 +var assign = require('Object.assign');
15 var emptyFunction = require('emptyFunction');
16 var warning = require('warning');
17
@@ -43,88 +44,93 @@ if (__DEV__) {
44 'xmp'
45 ];
46
46 - /**
47 - * Return whether `stack` contains `tag` and the last occurrence of `tag` is
48 - * deeper than any element in the `scope` array.
49 - *
50 - * https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-the-specific-scope
51 - *
52 - * Examples:
53 - * stackHasTagInSpecificScope(['p', 'quote'], 'p', ['button']) is true
54 - * stackHasTagInSpecificScope(['p', 'button'], 'p', ['button']) is false
55 - *
56 - * @param {Array<string>} stack
57 - * @param {string} tag
58 - * @param {Array<string>} scope
59 - */
60 - var stackHasTagInSpecificScope = function(stack, tag, scope) {
61 - for (var i = stack.length - 1; i >= 0; i--) {
62 - if (stack[i] === tag) {
63 - return true;
64 - }
65 - if (scope.indexOf(stack[i]) !== -1) {
66 - return false;
67 - }
68 - }
69 - return false;
70 - };
71 -
47 // https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-scope
48 var inScopeTags = [
49 'applet', 'caption', 'html', 'table', 'td', 'th', 'marquee', 'object',
50 'template',
51
52 // https://html.spec.whatwg.org/multipage/syntax.html#html-integration-point
78 - // TODO: Distinguish by namespace here
53 + // TODO: Distinguish by namespace here -- for <title>, including it here
54 + // errs on the side of fewer warnings
55 'foreignObject', 'desc', 'title'
56 ];
81 - var stackHasTagInScope = function(stack, tag) {
82 - return stackHasTagInSpecificScope(stack, tag, inScopeTags);
83 - };
57
58 // https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-button-scope
59 var buttonScopeTags = inScopeTags.concat(['button']);
87 - var stackHasTagInButtonScope = function(stack, tag) {
88 - return stackHasTagInSpecificScope(stack, tag, buttonScopeTags);
89 - };
90 -
91 - // See rules for 'li', 'dd', 'dt' start tags in
92 - // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody
93 - var listItemTagAllowed = function(tags, stack) {
94 - // tags is ['li'] or ['dd, 'dt']
95 - for (var i = stack.length - 1; i >= 0; i--) {
96 - if (tags.indexOf(stack[i]) !== -1) {
97 - return false;
98 - } else if (
99 - specialTags.indexOf(stack[i]) !== -1 &&
100 - stack[i] !== 'address' && stack[i] !== 'div' && stack[i] !== 'p'
101 - ) {
102 - return true;
103 - }
104 - }
105 - return true;
106 - };
60
61 // https://html.spec.whatwg.org/multipage/syntax.html#generate-implied-end-tags
62 var impliedEndTags =
63 ['dd', 'dt', 'li', 'option', 'optgroup', 'p', 'rp', 'rt'];
64
65 + var emptyAncestorInfo = {
66 + parentTag: null,
67 +
68 + formTag: null,
69 + aTagInScope: null,
70 + buttonTagInScope: null,
71 + nobrTagInScope: null,
72 + pTagInButtonScope: null,
73 +
74 + listItemTagAutoclosing: null,
75 + dlItemTagAutoclosing: null
76 + };
77 +
78 + var updatedAncestorInfo = function(oldInfo, tag, instance) {
79 + var ancestorInfo = assign({}, oldInfo || emptyAncestorInfo);
80 + var info = {tag: tag, instance: instance};
81 +
82 + if (inScopeTags.indexOf(tag) !== -1) {
83 + ancestorInfo.aTagInScope = null;
84 + ancestorInfo.buttonTagInScope = null;
85 + ancestorInfo.nobrTagInScope = null;
86 + }
87 + if (buttonScopeTags.indexOf(tag) !== -1) {
88 + ancestorInfo.pTagInButtonScope = null;
89 + }
90 +
91 + // See rules for 'li', 'dd', 'dt' start tags in
92 + // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody
93 + if (
94 + specialTags.indexOf(tag) !== -1 &&
95 + tag !== 'address' && tag !== 'div' && tag !== 'p'
96 + ) {
97 + ancestorInfo.listItemTagAutoclosing = null;
98 + ancestorInfo.dlItemTagAutoclosing = null;
99 + }
100 +
101 + ancestorInfo.parentTag = info;
102 +
103 + if (tag === 'form') {
104 + ancestorInfo.formTag = info;
105 + }
106 + if (tag === 'a') {
107 + ancestorInfo.aTagInScope = info;
108 + }
109 + if (tag === 'button') {
110 + ancestorInfo.buttonTagInScope = info;
111 + }
112 + if (tag === 'nobr') {
113 + ancestorInfo.nobrTagInScope = info;
114 + }
115 + if (tag === 'p') {
116 + ancestorInfo.pTagInButtonScope = info;
117 + }
118 + if (tag === 'li') {
119 + ancestorInfo.listItemTagAutoclosing = info;
120 + }
121 + if (tag === 'dd' || tag === 'dt') {
122 + ancestorInfo.dlItemTagAutoclosing = info;
123 + }
124 +
125 + return ancestorInfo;
126 + };
127 +
128 /**
113 - * Returns whether we allow putting `tag` in the document if the current stack
114 - * of open tags is `openTagStack`.
115 - *
116 - * Examples:
117 - * isTagValidInContext('tr', [..., 'table', 'tbody']) is true
118 - * isTagValidInContext('tr', [..., 'table']) is false
119 - *
120 - * @param {string} tag Lowercase HTML tag name or node name like '#text'
121 - * @param {Array<string>} openTagStack
129 + * Returns whether
130 */
123 - var isTagValidInContext = function(tag, openTagStack) {
124 - var currentTag = openTagStack[openTagStack.length - 1];
125 -
131 + var isTagValidWithParent = function(tag, parentTag) {
132 // First, let's check if we're in an unusual parsing mode...
127 - switch (currentTag) {
133 + switch (parentTag) {
134 // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inselect
135 case 'select':
136 return tag === 'option' || tag === 'optgroup' || tag === '#text';
@@ -185,6 +191,47 @@ if (__DEV__) {
191 // Probably in the "in body" parsing mode, so we outlaw only tag combos
192 // where the parsing rules cause implicit opens or closes to be added.
193 // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody
194 + switch (tag) {
195 + case 'h1':
196 + case 'h2':
197 + case 'h3':
198 + case 'h4':
199 + case 'h5':
200 + case 'h6':
201 + return (
202 + parentTag !== 'h1' && parentTag !== 'h2' && parentTag !== 'h3' &&
203 + parentTag !== 'h4' && parentTag !== 'h5' && parentTag !== 'h6'
204 + );
205 +
206 + case 'rp':
207 + case 'rt':
208 + return impliedEndTags.indexOf(parentTag) === -1;
209 +
210 + case 'caption':
211 + case 'col':
212 + case 'colgroup':
213 + case 'frame':
214 + case 'head':
215 + case 'tbody':
216 + case 'td':
217 + case 'tfoot':
218 + case 'th':
219 + case 'thead':
220 + case 'tr':
221 + // These tags are only valid with a few parents that have special child
222 + // parsing rules -- if we're down here, then none of those matched and
223 + // so we allow it only if we don't know what the parent is, as all other
224 + // cases are invalid.
225 + return parentTag == null;
226 + }
227 +
228 + return true;
229 + };
230 +
231 + /**
232 + * Returns whether
233 + */
234 + var findInvalidAncestorForTag = function(tag, ancestorInfo) {
235 switch (tag) {
236 case 'address':
237 case 'article':
@@ -219,7 +266,6 @@ if (__DEV__) {
266 case 'hr':
267
268 case 'xmp':
222 - return !stackHasTagInButtonScope(openTagStack, 'p');
269
270 case 'h1':
271 case 'h2':
@@ -227,90 +273,151 @@ if (__DEV__) {
273 case 'h4':
274 case 'h5':
275 case 'h6':
230 - return (
231 - !stackHasTagInButtonScope(openTagStack, 'p') &&
232 - currentTag !== 'h1' && currentTag !== 'h2' && currentTag !== 'h3' &&
233 - currentTag !== 'h4' && currentTag !== 'h5' && currentTag !== 'h6'
234 - );
276 + return ancestorInfo.pTagInButtonScope;
277
278 case 'form':
237 - return (
238 - openTagStack.indexOf('form') === -1 &&
239 - !stackHasTagInButtonScope(openTagStack, 'p')
240 - );
279 + return ancestorInfo.formTag || ancestorInfo.pTagInButtonScope;
280
281 case 'li':
243 - return listItemTagAllowed(['li'], openTagStack);
282 + return ancestorInfo.listItemTagAutoclosing;
283
284 case 'dd':
285 case 'dt':
247 - return listItemTagAllowed(['dd', 'dt'], openTagStack);
286 + return ancestorInfo.dlItemTagAutoclosing;
287
288 case 'button':
250 - return !stackHasTagInScope(openTagStack, 'button');
289 + return ancestorInfo.buttonTagInScope;
290
291 case 'a':
292 // Spec says something about storing a list of markers, but it sounds
293 // equivalent to this check.
255 - return !stackHasTagInScope(openTagStack, 'a');
294 + return ancestorInfo.aTagInScope;
295
296 case 'nobr':
258 - return !stackHasTagInScope(openTagStack, 'nobr');
297 + return ancestorInfo.nobrTagInScope;
298 + }
299
260 - case 'rp':
261 - case 'rt':
262 - return impliedEndTags.indexOf(currentTag) === -1;
300 + return null;
301 + };
302
264 - case 'caption':
265 - case 'col':
266 - case 'colgroup':
267 - case 'frame':
268 - case 'head':
269 - case 'tbody':
270 - case 'td':
271 - case 'tfoot':
272 - case 'th':
273 - case 'thead':
274 - case 'tr':
275 - return currentTag === undefined;
303 + /**
304 + * Given a ReactCompositeComponent instance, return a list of its recursive
305 + * owners, starting at the root and ending with the instance itself.
306 + */
307 + var findOwnerStack = function(instance) {
308 + if (!instance) {
309 + return [];
310 }
311
278 - return true;
312 + var stack = [];
313 + /*eslint-disable space-after-keywords */
314 + do {
315 + /*eslint-enable space-after-keywords */
316 + stack.push(instance);
317 + } while ((instance = instance._currentElement._owner));
318 + stack.reverse();
319 + return stack;
320 };
321
281 - validateDOMNesting = function(parentStack, childTag, element) {
282 - if (!isTagValidInContext(childTag, parentStack)) {
283 - var info = '';
284 - var parentTag = parentStack[parentStack.length - 1];
285 - if (parentTag === 'table' && childTag === 'tr') {
286 - info +=
287 - ' Add a <tbody> to your code to match the DOM tree generated by ' +
288 - 'the browser.';
289 - }
290 - if (element && element._owner) {
291 - var name = element._owner.getName();
292 - if (name) {
293 - info += ` Check the render method of \`${name}\`.`;
322 + validateDOMNesting = function(childTag, childInstance, ancestorInfo) {
323 + ancestorInfo = ancestorInfo || emptyAncestorInfo;
324 + var parentInfo = ancestorInfo.parentTag;
325 + var parentTag = parentInfo && parentInfo.tag;
326 +
327 + var invalidParent =
328 + isTagValidWithParent(childTag, parentTag) ? null : parentInfo;
329 + var invalidAncestor =
330 + invalidParent ? null : findInvalidAncestorForTag(childTag, ancestorInfo);
331 + var problematic = invalidParent || invalidAncestor;
332 +
333 + if (problematic) {
334 + var ancestorTag = problematic.tag;
335 + var ancestorInstance = problematic.instance;
336 +
337 + var childOwner = childInstance && childInstance._currentElement._owner;
338 + var ancestorOwner =
339 + ancestorInstance && ancestorInstance._currentElement._owner;
340 +
341 + var childOwners = findOwnerStack(childOwner);
342 + var ancestorOwners = findOwnerStack(ancestorOwner);
343 +
344 + var minStackLen = Math.min(childOwners.length, ancestorOwners.length);
345 + var i;
346 +
347 + var deepestCommon = -1;
348 + for (i = 0; i < minStackLen; i++) {
349 + if (childOwners[i] === ancestorOwners[i]) {
350 + deepestCommon = i;
351 + } else {
352 + break;
353 }
354 }
355
297 - warning(
298 - false,
299 - 'validateDOMNesting(...): <%s> cannot appear as a child of <%s> ' +
300 - 'in this context (%s).%s',
301 - childTag,
302 - parentTag,
303 - parentStack.join(' > '),
304 - info
356 + var UNKNOWN = '(unknown)';
357 + var childOwnerNames = childOwners.slice(deepestCommon + 1).map(
358 + (inst) => inst.getName() || UNKNOWN
359 + );
360 + var ancestorOwnerNames = ancestorOwners.slice(deepestCommon + 1).map(
361 + (inst) => inst.getName() || UNKNOWN
362 );
363 + var ownerInfo = [].concat(
364 + // If the parent and child instances have a common owner ancestor, start
365 + // with that -- otherwise we just start with the parent's owners.
366 + deepestCommon !== -1 ?
367 + childOwners[deepestCommon].getName() || UNKNOWN :
368 + [],
369 + ancestorOwnerNames,
370 + ancestorTag,
371 + // If we're warning about an invalid (non-parent) ancestry, add '...'
372 + invalidAncestor ? ['...'] : [],
373 + childOwnerNames,
374 + childTag
375 + ).join(' > ');
376 +
377 + if (invalidParent) {
378 + var info = '';
379 + if (ancestorTag === 'table' && childTag === 'tr') {
380 + info +=
381 + ' Add a <tbody> to your code to match the DOM tree generated by ' +
382 + 'the browser.';
383 + }
384 + warning(
385 + false,
386 + 'validateDOMNesting(...): <%s> cannot appear as a child of <%s>. ' +
387 + 'See %s.%s',
388 + childTag,
389 + ancestorTag,
390 + ownerInfo,
391 + info
392 + );
393 + } else {
394 + warning(
395 + false,
396 + 'validateDOMNesting(...): <%s> cannot appear as a descendant of ' +
397 + '<%s>. See %s.',
398 + childTag,
399 + ancestorTag,
400 + ownerInfo
401 + );
402 + }
403 }
404 };
405
309 - validateDOMNesting.tagStackContextKey =
310 - '__validateDOMNesting_tagStack$' + Math.random().toString(36).slice(2);
406 + validateDOMNesting.ancestorInfoContextKey =
407 + '__validateDOMNesting_ancestorInfo$' + Math.random().toString(36).slice(2);
408 +
409 + validateDOMNesting.updatedAncestorInfo = updatedAncestorInfo;
410
411 // For testing
313 - validateDOMNesting.isTagValidInContext = isTagValidInContext;
412 + validateDOMNesting.isTagValidInContext = function(tag, ancestorInfo) {
413 + ancestorInfo = ancestorInfo || emptyAncestorInfo;
414 + var parentInfo = ancestorInfo.parentTag;
415 + var parentTag = parentInfo && parentInfo.tag;
416 + return (
417 + isTagValidWithParent(tag, parentTag) &&
418 + !findInvalidAncestorForTag(tag, ancestorInfo)
419 + );
420 + };
421 }
422
423 module.exports = validateDOMNesting;