From e59dbb1315b8338cf5244b1669f0625242e50f94 Mon Sep 17 00:00:00 2001 From: Werner Badenhorst Date: Thu, 7 Nov 2019 05:28:10 -0800 Subject: [PATCH] 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 --- .../leak_canary/{index.js => index.tsx} | 52 ++++--- src/plugins/leak_canary/package.json | 11 +- ...essLeakString.js => processLeakString.tsx} | 133 +++++++++++------- src/plugins/leak_canary/yarn.lock | 3 - 4 files changed, 120 insertions(+), 79 deletions(-) rename src/plugins/leak_canary/{index.js => index.tsx} (86%) rename src/plugins/leak_canary/{processLeakString.js => processLeakString.tsx} (63%) diff --git a/src/plugins/leak_canary/index.js b/src/plugins/leak_canary/index.tsx similarity index 86% rename from src/plugins/leak_canary/index.js rename to src/plugins/leak_canary/index.tsx index 342a90927..d378eb077 100644 --- a/src/plugins/leak_canary/index.js +++ b/src/plugins/leak_canary/index.tsx @@ -7,6 +7,7 @@ * @format */ +import React from 'react'; import { Panel, FlexRow, @@ -20,29 +21,30 @@ import { Button, styled, } from 'flipper'; -import type {ElementID, Element} from 'flipper'; +import {Element} from 'flipper'; import {processLeaks} from './processLeakString'; type State = { - leaks: Leak[], - selectedIdx: ?number, - selectedEid: ?string, - showFullClassPaths: boolean, - leaksCount: number, + leaks: Leak[]; + selectedIdx: number | null; + selectedEid: string | null; + showFullClassPaths: boolean; + leaksCount: number; }; type LeakReport = { - leaks: string[], + leaks: string[]; }; +export type Fields = {[key: string]: string}; export type Leak = { - title: string, - root: string, - elements: {[key: ElementID]: Element}, - elementsSimple: {[key: ElementID]: Element}, - instanceFields: {}, - staticFields: {}, - retainedSize: string, + title: string; + root: string; + elements: {[key: string]: Element}; + elementsSimple: {[key: string]: Element}; + instanceFields: {[key: string]: Fields}; + staticFields: {[key: string]: Fields}; + retainedSize: string; }; const Window = styled(FlexRow)({ @@ -55,8 +57,12 @@ const ToolbarItem = styled(FlexRow)({ marginLeft: '8px', }); -export default class LeakCanary extends FlipperPlugin { - state = { +export default class LeakCanary extends FlipperPlugin< + State, + {type: 'LeakCanary'}, + PersistedState +> { + state: State = { leaks: [], selectedIdx: null, selectedEid: null, @@ -101,13 +107,15 @@ export default class LeakCanary extends FlipperPlugin { }; _toggleElement = (leakIdx: number, eid: string) => { - const leaks = this.state.leaks; + const {leaks} = this.state; const leak = leaks[leakIdx]; const element = leak.elements[eid]; - element.expanded = !element.expanded; - const elementSimple = leak.elementsSimple[eid]; + if (!element || !elementSimple) { + return; + } + element.expanded = !element.expanded; elementSimple.expanded = !elementSimple.expanded; this.setState({ @@ -121,8 +129,8 @@ export default class LeakCanary extends FlipperPlugin { */ _extractValue( value: any, - depth: number, - ): {|mutable: boolean, type: string, value: any|} { + _: number, // depth + ): {mutable: boolean; type: string; value: any} { if (!isNaN(value)) { return {mutable: false, type: 'number', value: value}; } else if (value == 'true' || value == 'false') { @@ -189,7 +197,7 @@ export default class LeakCanary extends FlipperPlugin { this._selectElement(idx, eid); }} onElementHovered={() => {}} - onElementExpanded={(eid, deep) => { + onElementExpanded={(eid /*, deep*/) => { this._toggleElement(idx, eid); }} onValueChanged={() => {}} diff --git a/src/plugins/leak_canary/package.json b/src/plugins/leak_canary/package.json index f06e64c72..6535a3146 100644 --- a/src/plugins/leak_canary/package.json +++ b/src/plugins/leak_canary/package.json @@ -1,12 +1,13 @@ { "name": "LeakCanary", "version": "1.0.0", - "main": "index.js", + "main": "index.tsx", "license": "MIT", - "keywords": ["flipper-plugin"], - "dependencies": { - "lodash": "^4.17.5" - }, + "keywords": [ + "flipper-plugin" + ], + "dependencies": {}, + "devDependencies": {}, "icon": "bird", "title": "LeakCanary", "bugs": { diff --git a/src/plugins/leak_canary/processLeakString.js b/src/plugins/leak_canary/processLeakString.tsx similarity index 63% rename from src/plugins/leak_canary/processLeakString.js rename to src/plugins/leak_canary/processLeakString.tsx index e0b5c8bfa..43f03d08b 100644 --- a/src/plugins/leak_canary/processLeakString.js +++ b/src/plugins/leak_canary/processLeakString.tsx @@ -8,8 +8,39 @@ * @flow */ -import type {Leak} from './index.js'; -import type {Element} from 'flipper'; +import {Leak} from './index'; +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, +) { + const element = elements.get(elementId); + if (element && element.children) { + element.children.push(childElementId); + } +} + +function toObjectMap( + dict: Map, + 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 @@ -25,8 +56,8 @@ function getElementSimple(str: string, id: string): Element { name = match[5]; } return { - id: id, - name: name, + id, + name, expanded: true, children: [], attributes: [], @@ -54,17 +85,21 @@ const RETAINED_SIZE_INDICATOR = '* Retaining: '; function generateFieldsList( lines: string[], i: number, -): {|staticFields: {}, instanceFields: {}, packages: {}|} { - const staticFields = {}; - const instanceFields = {}; +): { + staticFields: Map>; + instanceFields: Map>; + packages: Map; +} { + const staticFields = new Map>(); + const instanceFields = new Map>(); - let staticValues = {}; - let instanceValues = {}; + let staticValues = new Map(); + let instanceValues = new Map(); let elementId = -1; - let elementIdStr = String(-1); + let elementIdStr = ''; - const packages = {}; + const packages = new Map(); // Process everything between Details and Excluded Refs while ( @@ -74,10 +109,10 @@ function generateFieldsList( const line = lines[i]; if (line.startsWith('*')) { if (elementId != -1) { - staticFields[elementIdStr] = staticValues; - instanceFields[elementIdStr] = instanceValues; - staticValues = {}; - instanceValues = {}; + staticFields.set(elementIdStr, staticValues); + instanceFields.set(elementIdStr, instanceValues); + staticValues = new Map(); + instanceValues = new Map(); } elementId++; elementIdStr = String(elementId); @@ -88,7 +123,7 @@ function generateFieldsList( if (match) { pkg = match[3]; } - packages[elementIdStr] = pkg; + packages.set(elementIdStr, pkg); } else { // Field/value pairs represented in input lines as // | fieldName = value @@ -99,22 +134,18 @@ function generateFieldsList( if (fieldName.startsWith(STATIC_PREFIX)) { const strippedFieldName = fieldName.substr(7); - staticValues[strippedFieldName] = fieldValue; + staticValues.set(strippedFieldName, fieldValue); } else { - instanceValues[fieldName] = fieldValue; + instanceValues.set(fieldName, fieldValue); } } } i++; } - staticFields[elementIdStr] = staticValues; - instanceFields[elementIdStr] = instanceValues; + staticFields.set(elementIdStr, staticValues); + instanceFields.set(elementIdStr, instanceValues); - return { - staticFields: staticFields, - instanceFields: instanceFields, - packages: packages, - }; + return {staticFields, instanceFields, packages}; } /** @@ -129,8 +160,8 @@ function processLeak(output: Leak[], leakInfo: string): Leak[] { // Elements shows a Object's classname and package, wheras elementsSimple shows // just its classname - const elements = {}; - const elementsSimple = {}; + const elements = new Map(); + const elementsSimple = new Map(); let rootElementId = ''; @@ -145,27 +176,29 @@ function processLeak(output: Leak[], leakInfo: string): Leak[] { } let elementId = 0; - let elementIdStr = ''; + let elementIdStr = String(elementId); + // Last element is leaked object + let leakedObjName = ''; while (i < lines.length && lines[i].startsWith('*')) { const line = lines[i]; - elementIdStr = String(elementId); - const prevIdStr = String(elementId - 1); + const prevElementIdStr = String(elementId - 1); if (elementId !== 0) { // Add element to previous element's children - elements[prevIdStr].children.push(elementIdStr); - elementsSimple[prevIdStr].children.push(elementIdStr); + safeAddChildElementId(elementIdStr, prevElementIdStr, elements); + safeAddChildElementId(elementIdStr, prevElementIdStr, elementsSimple); } else { rootElementId = elementIdStr; } - elements[elementIdStr] = getElementSimple(line, elementIdStr); - elementsSimple[elementIdStr] = getElementSimple(line, elementIdStr); + const element = getElementSimple(line, elementIdStr); + leakedObjName = element.name; + elements.set(elementIdStr, element); + elementsSimple.set(elementIdStr, element); i++; elementId++; + elementIdStr = String(elementId); } - // Last element is leaked object - const leakedObjName = elements[elementIdStr].name; while ( 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 // inserted, in order to enable 'Show full class path' - Object.keys(packages).forEach(elementId => { - const pkg = packages[elementId]; - const simpleName = elements[elementId].name; - // Gets everything before the field name, which is replaced by the package - const match = simpleName.match(/([^\. ]*)(.*)/); - if (match) { - elements[elementId].name = pkg + match[2]; + for (const [elementId, pkg] of packages.entries()) { + const element = elements.get(elementId); + if (!element) { + continue; } - }); + // 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({ - root: rootElementId, - elements: elements, - elementsSimple: elementsSimple, - staticFields: staticFields, - instanceFields: instanceFields, title: leakedObjName, + root: rootElementId, + elements: toObjectMap(elements), + elementsSimple: toObjectMap(elementsSimple), + staticFields: toObjectMap(staticFields, true), + instanceFields: toObjectMap(instanceFields, true), retainedSize: retainedSize, }); return output; diff --git a/src/plugins/leak_canary/yarn.lock b/src/plugins/leak_canary/yarn.lock index 560de8e50..fb57ccd13 100644 --- a/src/plugins/leak_canary/yarn.lock +++ b/src/plugins/leak_canary/yarn.lock @@ -2,6 +2,3 @@ # yarn lockfile v1 -lodash@^4.17.5: - version "4.17.5" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"