react/packages/react-devtools-shared/src/utils.js
Pavlo Tymchuk d1c2555861 [react-devtools-shared] Added string type check for object name prop in getDisplayName function (#16798)
* [react-devtools-shared] Added string type check for object name prop in getDisplayName function from utils.js file; tests included;

* Re-added empty string check to getDisplayName()

* Tweaked tests to use real functions

This more closely simulates how the utility is being used in production, and would catch cases like anonymous functions (with empty string names).
2019-09-17 13:17:04 -07:00

308 lines
8.1 KiB
JavaScript

/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import Symbol from 'es6-symbol';
import LRU from 'lru-cache';
import {
TREE_OPERATION_ADD,
TREE_OPERATION_REMOVE,
TREE_OPERATION_REORDER_CHILDREN,
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
} from './constants';
import {ElementTypeRoot} from 'react-devtools-shared/src/types';
import {
LOCAL_STORAGE_FILTER_PREFERENCES_KEY,
LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY,
} from './constants';
import {ComponentFilterElementType, ElementTypeHostComponent} from './types';
import {
ElementTypeClass,
ElementTypeForwardRef,
ElementTypeFunction,
ElementTypeMemo,
} from 'react-devtools-shared/src/types';
import {localStorageGetItem, localStorageSetItem} from './storage';
import type {ComponentFilter, ElementType} from './types';
const cachedDisplayNames: WeakMap<Function, string> = new WeakMap();
// On large trees, encoding takes significant time.
// Try to reuse the already encoded strings.
let encodedStringCache = new LRU({max: 1000});
export function getDisplayName(
type: Function,
fallbackName: string = 'Anonymous',
): string {
const nameFromCache = cachedDisplayNames.get(type);
if (nameFromCache != null) {
return nameFromCache;
}
let displayName = fallbackName;
// The displayName property is not guaranteed to be a string.
// It's only safe to use for our purposes if it's a string.
// github.com/facebook/react-devtools/issues/803
if (typeof type.displayName === 'string') {
displayName = type.displayName;
} else if (typeof type.name === 'string' && type.name !== '') {
displayName = type.name;
}
cachedDisplayNames.set(type, displayName);
return displayName;
}
let uidCounter: number = 0;
export function getUID(): number {
return ++uidCounter;
}
export function utfDecodeString(array: Array<number>): string {
return String.fromCodePoint(...array);
}
export function utfEncodeString(string: string): Array<number> {
let cached = encodedStringCache.get(string);
if (cached !== undefined) {
return cached;
}
const encoded = new Array(string.length);
for (let i = 0; i < string.length; i++) {
encoded[i] = string.codePointAt(i);
}
encodedStringCache.set(string, encoded);
return encoded;
}
export function printOperationsArray(operations: Array<number>) {
// The first two values are always rendererID and rootID
const rendererID = operations[0];
const rootID = operations[1];
const logs = [`opertions for renderer:${rendererID} and root:${rootID}`];
let i = 2;
// Reassemble the string table.
const stringTable = [
null, // ID = 0 corresponds to the null string.
];
const stringTableSize = operations[i++];
const stringTableEnd = i + stringTableSize;
while (i < stringTableEnd) {
const nextLength = operations[i++];
const nextString = utfDecodeString(
(operations.slice(i, i + nextLength): any),
);
stringTable.push(nextString);
i += nextLength;
}
while (i < operations.length) {
const operation = operations[i];
switch (operation) {
case TREE_OPERATION_ADD: {
const id = ((operations[i + 1]: any): number);
const type = ((operations[i + 2]: any): ElementType);
i += 3;
if (type === ElementTypeRoot) {
logs.push(`Add new root node ${id}`);
i++; // supportsProfiling
i++; // hasOwnerMetadata
} else {
const parentID = ((operations[i]: any): number);
i++;
i++; // ownerID
const displayNameStringID = operations[i];
const displayName = stringTable[displayNameStringID];
i++;
i++; // key
logs.push(
`Add node ${id} (${displayName || 'null'}) as child of ${parentID}`,
);
}
break;
}
case TREE_OPERATION_REMOVE: {
const removeLength = ((operations[i + 1]: any): number);
i += 2;
for (let removeIndex = 0; removeIndex < removeLength; removeIndex++) {
const id = ((operations[i]: any): number);
i += 1;
logs.push(`Remove node ${id}`);
}
break;
}
case TREE_OPERATION_REORDER_CHILDREN: {
const id = ((operations[i + 1]: any): number);
const numChildren = ((operations[i + 2]: any): number);
i += 3;
const children = operations.slice(i, i + numChildren);
i += numChildren;
logs.push(`Re-order node ${id} children ${children.join(',')}`);
break;
}
case TREE_OPERATION_UPDATE_TREE_BASE_DURATION:
// Base duration updates are only sent while profiling is in progress.
// We can ignore them at this point.
// The profiler UI uses them lazily in order to generate the tree.
i += 3;
break;
default:
throw Error(`Unsupported Bridge operation ${operation}`);
}
}
console.log(logs.join('\n '));
}
export function getDefaultComponentFilters(): Array<ComponentFilter> {
return [
{
type: ComponentFilterElementType,
value: ElementTypeHostComponent,
isEnabled: true,
},
];
}
export function getSavedComponentFilters(): Array<ComponentFilter> {
try {
const raw = localStorageGetItem(LOCAL_STORAGE_FILTER_PREFERENCES_KEY);
if (raw != null) {
return JSON.parse(raw);
}
} catch (error) {}
return getDefaultComponentFilters();
}
export function saveComponentFilters(
componentFilters: Array<ComponentFilter>,
): void {
localStorageSetItem(
LOCAL_STORAGE_FILTER_PREFERENCES_KEY,
JSON.stringify(componentFilters),
);
}
export function getAppendComponentStack(): boolean {
try {
const raw = localStorageGetItem(LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY);
if (raw != null) {
return JSON.parse(raw);
}
} catch (error) {}
return true;
}
export function setAppendComponentStack(value: boolean): void {
localStorageSetItem(
LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY,
JSON.stringify(value),
);
}
export function separateDisplayNameAndHOCs(
displayName: string | null,
type: ElementType,
): [string | null, Array<string> | null] {
if (displayName === null) {
return [null, null];
}
let hocDisplayNames = null;
switch (type) {
case ElementTypeClass:
case ElementTypeForwardRef:
case ElementTypeFunction:
case ElementTypeMemo:
if (displayName.indexOf('(') >= 0) {
const matches = displayName.match(/[^()]+/g);
if (matches != null) {
displayName = matches.pop();
hocDisplayNames = matches;
}
}
break;
default:
break;
}
return [displayName, hocDisplayNames];
}
// Pulled from react-compat
// https://github.com/developit/preact-compat/blob/7c5de00e7c85e2ffd011bf3af02899b63f699d3a/src/index.js#L349
export function shallowDiffers(prev: Object, next: Object): boolean {
for (let attribute in prev) {
if (!(attribute in next)) {
return true;
}
}
for (let attribute in next) {
if (prev[attribute] !== next[attribute]) {
return true;
}
}
return false;
}
export function getInObject(object: Object, path: Array<string | number>): any {
return path.reduce((reduced: Object, attr: any): any => {
if (reduced) {
if (hasOwnProperty.call(reduced, attr)) {
return reduced[attr];
}
if (typeof reduced[Symbol.iterator] === 'function') {
// Convert iterable to array and return array[index]
//
// TRICKY
// Don't use [...spread] syntax for this purpose.
// This project uses @babel/plugin-transform-spread in "loose" mode which only works with Array values.
// Other types (e.g. typed arrays, Sets) will not spread correctly.
return Array.from(reduced)[attr];
}
}
return null;
}, object);
}
export function setInObject(
object: Object,
path: Array<string | number>,
value: any,
) {
const length = path.length;
const last = path[length - 1];
if (object != null) {
const parent = getInObject(object, path.slice(0, length - 1));
if (parent) {
parent[last] = value;
}
}
}