Convert Flipper plugin "LeakCanary" to TypeScript

Summary: The Flipper codebase was recently converted to TypeScript. Plugins should be converted as well to maintain type safety.

Reviewed By: passy

Differential Revision: D18347730

fbshipit-source-id: be63e8bac677c13fa5c0fa4f964dda8e7ba6251d
This commit is contained in:
Werner Badenhorst
2019-11-07 05:28:10 -08:00
committed by Facebook Github Bot
parent 96049d43a5
commit e59dbb1315
4 changed files with 120 additions and 79 deletions

View File

@@ -7,6 +7,7 @@
* @format * @format
*/ */
import React from 'react';
import { import {
Panel, Panel,
FlexRow, FlexRow,
@@ -20,29 +21,30 @@ import {
Button, Button,
styled, styled,
} from 'flipper'; } from 'flipper';
import type {ElementID, Element} from 'flipper'; import {Element} from 'flipper';
import {processLeaks} from './processLeakString'; import {processLeaks} from './processLeakString';
type State = { type State = {
leaks: Leak[], leaks: Leak[];
selectedIdx: ?number, selectedIdx: number | null;
selectedEid: ?string, selectedEid: string | null;
showFullClassPaths: boolean, showFullClassPaths: boolean;
leaksCount: number, leaksCount: number;
}; };
type LeakReport = { type LeakReport = {
leaks: string[], leaks: string[];
}; };
export type Fields = {[key: string]: string};
export type Leak = { export type Leak = {
title: string, title: string;
root: string, root: string;
elements: {[key: ElementID]: Element}, elements: {[key: string]: Element};
elementsSimple: {[key: ElementID]: Element}, elementsSimple: {[key: string]: Element};
instanceFields: {}, instanceFields: {[key: string]: Fields};
staticFields: {}, staticFields: {[key: string]: Fields};
retainedSize: string, retainedSize: string;
}; };
const Window = styled(FlexRow)({ const Window = styled(FlexRow)({
@@ -55,8 +57,12 @@ const ToolbarItem = styled(FlexRow)({
marginLeft: '8px', marginLeft: '8px',
}); });
export default class LeakCanary extends FlipperPlugin<State> { export default class LeakCanary<PersistedState> extends FlipperPlugin<
state = { State,
{type: 'LeakCanary'},
PersistedState
> {
state: State = {
leaks: [], leaks: [],
selectedIdx: null, selectedIdx: null,
selectedEid: null, selectedEid: null,
@@ -101,13 +107,15 @@ export default class LeakCanary extends FlipperPlugin<State> {
}; };
_toggleElement = (leakIdx: number, eid: string) => { _toggleElement = (leakIdx: number, eid: string) => {
const leaks = this.state.leaks; const {leaks} = this.state;
const leak = leaks[leakIdx]; const leak = leaks[leakIdx];
const element = leak.elements[eid]; const element = leak.elements[eid];
element.expanded = !element.expanded;
const elementSimple = leak.elementsSimple[eid]; const elementSimple = leak.elementsSimple[eid];
if (!element || !elementSimple) {
return;
}
element.expanded = !element.expanded;
elementSimple.expanded = !elementSimple.expanded; elementSimple.expanded = !elementSimple.expanded;
this.setState({ this.setState({
@@ -121,8 +129,8 @@ export default class LeakCanary extends FlipperPlugin<State> {
*/ */
_extractValue( _extractValue(
value: any, value: any,
depth: number, _: number, // depth
): {|mutable: boolean, type: string, value: any|} { ): {mutable: boolean; type: string; value: any} {
if (!isNaN(value)) { if (!isNaN(value)) {
return {mutable: false, type: 'number', value: value}; return {mutable: false, type: 'number', value: value};
} else if (value == 'true' || value == 'false') { } else if (value == 'true' || value == 'false') {
@@ -189,7 +197,7 @@ export default class LeakCanary extends FlipperPlugin<State> {
this._selectElement(idx, eid); this._selectElement(idx, eid);
}} }}
onElementHovered={() => {}} onElementHovered={() => {}}
onElementExpanded={(eid, deep) => { onElementExpanded={(eid /*, deep*/) => {
this._toggleElement(idx, eid); this._toggleElement(idx, eid);
}} }}
onValueChanged={() => {}} onValueChanged={() => {}}

View File

@@ -1,12 +1,13 @@
{ {
"name": "LeakCanary", "name": "LeakCanary",
"version": "1.0.0", "version": "1.0.0",
"main": "index.js", "main": "index.tsx",
"license": "MIT", "license": "MIT",
"keywords": ["flipper-plugin"], "keywords": [
"dependencies": { "flipper-plugin"
"lodash": "^4.17.5" ],
}, "dependencies": {},
"devDependencies": {},
"icon": "bird", "icon": "bird",
"title": "LeakCanary", "title": "LeakCanary",
"bugs": { "bugs": {

View File

@@ -8,8 +8,39 @@
* @flow * @flow
*/ */
import type {Leak} from './index.js'; import {Leak} from './index';
import type {Element} from 'flipper'; import {Element} from 'flipper';
/**
* Utility Function to add a child element
* @param childElementId
* @param elementId
* @param elements
*/
function safeAddChildElementId(
childElementId: string,
elementId: string,
elements: Map<string, Element>,
) {
const element = elements.get(elementId);
if (element && element.children) {
element.children.push(childElementId);
}
}
function toObjectMap(
dict: Map<any, any>,
deep: boolean = false,
): {[key: string]: any} {
const result: {[key: string]: any} = {};
for (let [key, value] of dict.entries()) {
if (deep && value instanceof Map) {
value = toObjectMap(value, true);
}
result[String(key)] = value;
}
return result;
}
/** /**
* Creates an Element (for ElementsInspector) representing a single Object in * Creates an Element (for ElementsInspector) representing a single Object in
@@ -25,8 +56,8 @@ function getElementSimple(str: string, id: string): Element {
name = match[5]; name = match[5];
} }
return { return {
id: id, id,
name: name, name,
expanded: true, expanded: true,
children: [], children: [],
attributes: [], attributes: [],
@@ -54,17 +85,21 @@ const RETAINED_SIZE_INDICATOR = '* Retaining: ';
function generateFieldsList( function generateFieldsList(
lines: string[], lines: string[],
i: number, i: number,
): {|staticFields: {}, instanceFields: {}, packages: {}|} { ): {
const staticFields = {}; staticFields: Map<string, Map<string, string>>;
const instanceFields = {}; instanceFields: Map<string, Map<string, string>>;
packages: Map<string, string>;
} {
const staticFields = new Map<string, Map<string, string>>();
const instanceFields = new Map<string, Map<string, string>>();
let staticValues = {}; let staticValues = new Map<string, string>();
let instanceValues = {}; let instanceValues = new Map<string, string>();
let elementId = -1; let elementId = -1;
let elementIdStr = String(-1); let elementIdStr = '';
const packages = {}; const packages = new Map<string, any>();
// Process everything between Details and Excluded Refs // Process everything between Details and Excluded Refs
while ( while (
@@ -74,10 +109,10 @@ function generateFieldsList(
const line = lines[i]; const line = lines[i];
if (line.startsWith('*')) { if (line.startsWith('*')) {
if (elementId != -1) { if (elementId != -1) {
staticFields[elementIdStr] = staticValues; staticFields.set(elementIdStr, staticValues);
instanceFields[elementIdStr] = instanceValues; instanceFields.set(elementIdStr, instanceValues);
staticValues = {}; staticValues = new Map<string, string>();
instanceValues = {}; instanceValues = new Map<string, string>();
} }
elementId++; elementId++;
elementIdStr = String(elementId); elementIdStr = String(elementId);
@@ -88,7 +123,7 @@ function generateFieldsList(
if (match) { if (match) {
pkg = match[3]; pkg = match[3];
} }
packages[elementIdStr] = pkg; packages.set(elementIdStr, pkg);
} else { } else {
// Field/value pairs represented in input lines as // Field/value pairs represented in input lines as
// | fieldName = value // | fieldName = value
@@ -99,22 +134,18 @@ function generateFieldsList(
if (fieldName.startsWith(STATIC_PREFIX)) { if (fieldName.startsWith(STATIC_PREFIX)) {
const strippedFieldName = fieldName.substr(7); const strippedFieldName = fieldName.substr(7);
staticValues[strippedFieldName] = fieldValue; staticValues.set(strippedFieldName, fieldValue);
} else { } else {
instanceValues[fieldName] = fieldValue; instanceValues.set(fieldName, fieldValue);
} }
} }
} }
i++; i++;
} }
staticFields[elementIdStr] = staticValues; staticFields.set(elementIdStr, staticValues);
instanceFields[elementIdStr] = instanceValues; instanceFields.set(elementIdStr, instanceValues);
return { return {staticFields, instanceFields, packages};
staticFields: staticFields,
instanceFields: instanceFields,
packages: packages,
};
} }
/** /**
@@ -129,8 +160,8 @@ function processLeak(output: Leak[], leakInfo: string): Leak[] {
// Elements shows a Object's classname and package, wheras elementsSimple shows // Elements shows a Object's classname and package, wheras elementsSimple shows
// just its classname // just its classname
const elements = {}; const elements = new Map<string, Element>();
const elementsSimple = {}; const elementsSimple = new Map<string, Element>();
let rootElementId = ''; let rootElementId = '';
@@ -145,27 +176,29 @@ function processLeak(output: Leak[], leakInfo: string): Leak[] {
} }
let elementId = 0; let elementId = 0;
let elementIdStr = ''; let elementIdStr = String(elementId);
// Last element is leaked object
let leakedObjName = '';
while (i < lines.length && lines[i].startsWith('*')) { while (i < lines.length && lines[i].startsWith('*')) {
const line = lines[i]; const line = lines[i];
elementIdStr = String(elementId); const prevElementIdStr = String(elementId - 1);
const prevIdStr = String(elementId - 1);
if (elementId !== 0) { if (elementId !== 0) {
// Add element to previous element's children // Add element to previous element's children
elements[prevIdStr].children.push(elementIdStr); safeAddChildElementId(elementIdStr, prevElementIdStr, elements);
elementsSimple[prevIdStr].children.push(elementIdStr); safeAddChildElementId(elementIdStr, prevElementIdStr, elementsSimple);
} else { } else {
rootElementId = elementIdStr; rootElementId = elementIdStr;
} }
elements[elementIdStr] = getElementSimple(line, elementIdStr); const element = getElementSimple(line, elementIdStr);
elementsSimple[elementIdStr] = getElementSimple(line, elementIdStr); leakedObjName = element.name;
elements.set(elementIdStr, element);
elementsSimple.set(elementIdStr, element);
i++; i++;
elementId++; elementId++;
elementIdStr = String(elementId);
} }
// Last element is leaked object
const leakedObjName = elements[elementIdStr].name;
while ( while (
i < lines.length && i < lines.length &&
@@ -197,23 +230,25 @@ function processLeak(output: Leak[], leakInfo: string): Leak[] {
// While elementsSimple remains as-is, elements has the package of each class // While elementsSimple remains as-is, elements has the package of each class
// inserted, in order to enable 'Show full class path' // inserted, in order to enable 'Show full class path'
Object.keys(packages).forEach(elementId => { for (const [elementId, pkg] of packages.entries()) {
const pkg = packages[elementId]; const element = elements.get(elementId);
const simpleName = elements[elementId].name; if (!element) {
// Gets everything before the field name, which is replaced by the package continue;
const match = simpleName.match(/([^\. ]*)(.*)/);
if (match) {
elements[elementId].name = pkg + match[2];
} }
}); // Gets everything before the field name, which is replaced by the package
const match = element.name.match(/([^\. ]*)(.*)/);
if (match && match.length === 3) {
element.name = pkg + match[2];
}
}
output.push({ output.push({
root: rootElementId,
elements: elements,
elementsSimple: elementsSimple,
staticFields: staticFields,
instanceFields: instanceFields,
title: leakedObjName, title: leakedObjName,
root: rootElementId,
elements: toObjectMap(elements),
elementsSimple: toObjectMap(elementsSimple),
staticFields: toObjectMap(staticFields, true),
instanceFields: toObjectMap(instanceFields, true),
retainedSize: retainedSize, retainedSize: retainedSize,
}); });
return output; return output;

View File

@@ -2,6 +2,3 @@
# yarn lockfile v1 # yarn lockfile v1
lodash@^4.17.5:
version "4.17.5"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"