Skip to content

Commit 92c7e49

Browse files
todortotevBrian Vaughn
andauthored
Don't consumer iterators while inspecting (facebook#19831)
Co-authored-by: Brian Vaughn <bvaughn@fb.com>
1 parent 81aaee5 commit 92c7e49

File tree

6 files changed

+129
-1
lines changed

6 files changed

+129
-1
lines changed

packages/react-devtools-shared/src/__tests__/__snapshots__/inspectedElementContext-test.js.snap

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,19 @@ exports[`InspectedElementContext should inspect the currently selected element:
200200
}
201201
`;
202202

203+
exports[`InspectedElementContext should not consume iterables while inspecting: 1: Inspected element 2 1`] = `
204+
{
205+
"id": 2,
206+
"owners": null,
207+
"context": null,
208+
"hooks": null,
209+
"props": {
210+
"prop": {}
211+
},
212+
"state": null
213+
}
214+
`;
215+
203216
exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 1: Initially inspect element 1`] = `
204217
{
205218
"id": 2,

packages/react-devtools-shared/src/__tests__/inspectedElementContext-test.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,57 @@ describe('InspectedElementContext', () => {
796796
done();
797797
});
798798

799+
it('should not consume iterables while inspecting', async done => {
800+
const Example = () => null;
801+
802+
function* generator() {
803+
throw Error('Should not be consumed!');
804+
}
805+
806+
const container = document.createElement('div');
807+
808+
const iterable = generator();
809+
await utils.actAsync(() =>
810+
ReactDOM.render(<Example prop={iterable} />, container),
811+
);
812+
813+
const id = ((store.getElementIDAtIndex(0): any): number);
814+
815+
let inspectedElement = null;
816+
817+
function Suspender({target}) {
818+
const {getInspectedElement} = React.useContext(InspectedElementContext);
819+
inspectedElement = getInspectedElement(id);
820+
return null;
821+
}
822+
823+
await utils.actAsync(
824+
() =>
825+
TestRenderer.create(
826+
<Contexts
827+
defaultSelectedElementID={id}
828+
defaultSelectedElementIndex={0}>
829+
<React.Suspense fallback={null}>
830+
<Suspender target={id} />
831+
</React.Suspense>
832+
</Contexts>,
833+
),
834+
false,
835+
);
836+
837+
expect(inspectedElement).not.toBeNull();
838+
expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
839+
840+
const {prop} = (inspectedElement: any).props;
841+
expect(prop[meta.inspectable]).toBe(false);
842+
expect(prop[meta.name]).toBe('Generator');
843+
expect(prop[meta.type]).toBe('opaque_iterator');
844+
expect(prop[meta.preview_long]).toBe('Generator');
845+
expect(prop[meta.preview_short]).toBe('Generator');
846+
847+
done();
848+
});
849+
799850
it('should support objects with no prototype', async done => {
800851
const Example = () => null;
801852

packages/react-devtools-shared/src/__tests__/legacy/__snapshots__/inspectElement-test.js.snap

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,23 @@ Object {
1818
}
1919
`;
2020

21+
exports[`InspectedElementContext should not consume iterables while inspecting: 1: Initial inspection 1`] = `
22+
Object {
23+
"id": 2,
24+
"type": "full-data",
25+
"value": {
26+
"id": 2,
27+
"owners": null,
28+
"context": {},
29+
"hooks": null,
30+
"props": {
31+
"iteratable": {}
32+
},
33+
"state": null
34+
},
35+
}
36+
`;
37+
2138
exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 1: Initially inspect element 1`] = `
2239
Object {
2340
"id": 2,

packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,36 @@ describe('InspectedElementContext', () => {
397397
done();
398398
});
399399

400+
it('should not consume iterables while inspecting', async done => {
401+
const Example = () => null;
402+
403+
function* generator() {
404+
yield 1;
405+
yield 2;
406+
}
407+
408+
const iteratable = generator();
409+
410+
act(() =>
411+
ReactDOM.render(
412+
<Example iteratable={iteratable} />,
413+
document.createElement('div'),
414+
),
415+
);
416+
417+
const id = ((store.getElementIDAtIndex(0): any): number);
418+
const inspectedElement = await read(id);
419+
420+
expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
421+
422+
// Inspecting should not consume the iterable.
423+
expect(iteratable.next().value).toEqual(1);
424+
expect(iteratable.next().value).toEqual(2);
425+
expect(iteratable.next().value).toBeUndefined();
426+
427+
done();
428+
});
429+
400430
it('should support custom objects with enumerable properties and getters', async done => {
401431
class CustomData {
402432
_number = 42;

packages/react-devtools-shared/src/hydration.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,16 @@ export function dehydrate(
266266
return unserializableValue;
267267
}
268268

269+
case 'opaque_iterator':
270+
cleaned.push(path);
271+
return {
272+
inspectable: false,
273+
preview_short: formatDataForPreview(data, false),
274+
preview_long: formatDataForPreview(data, true),
275+
name: data[Symbol.toStringTag],
276+
type,
277+
};
278+
269279
case 'date':
270280
cleaned.push(path);
271281
return {

packages/react-devtools-shared/src/utils.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,7 @@ export type DataType =
440440
| 'html_element'
441441
| 'infinity'
442442
| 'iterator'
443+
| 'opaque_iterator'
443444
| 'nan'
444445
| 'null'
445446
| 'number'
@@ -500,7 +501,9 @@ export function getDataType(data: Object): DataType {
500501
// but this seems kind of awkward and expensive.
501502
return 'array_buffer';
502503
} else if (typeof data[Symbol.iterator] === 'function') {
503-
return 'iterator';
504+
return data[Symbol.iterator]() === data
505+
? 'opaque_iterator'
506+
: 'iterator';
504507
} else if (data.constructor && data.constructor.name === 'RegExp') {
505508
return 'regexp';
506509
} else {
@@ -679,6 +682,7 @@ export function formatDataForPreview(
679682
}
680683
case 'iterator':
681684
const name = data.constructor.name;
685+
682686
if (showFormattedValue) {
683687
// TRICKY
684688
// Don't use [...spread] syntax for this purpose.
@@ -717,6 +721,9 @@ export function formatDataForPreview(
717721
} else {
718722
return `${name}(${data.size})`;
719723
}
724+
case 'opaque_iterator': {
725+
return data[Symbol.toStringTag];
726+
}
720727
case 'date':
721728
return data.toString();
722729
case 'object':

0 commit comments

Comments
 (0)