From e893bcdc6dbe56a66a4e00dfdcae962f1006d82f Mon Sep 17 00:00:00 2001 From: Armano Date: Mon, 27 Dec 2021 11:19:08 +0100 Subject: [PATCH 1/4] docs(website): correct potential infinite loop in ts ast viewer --- packages/website/src/components/ast/serializer/serializerTS.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/website/src/components/ast/serializer/serializerTS.ts b/packages/website/src/components/ast/serializer/serializerTS.ts index e1a86f054a75..c1dccbe8f672 100644 --- a/packages/website/src/components/ast/serializer/serializerTS.ts +++ b/packages/website/src/components/ast/serializer/serializerTS.ts @@ -15,7 +15,9 @@ export function getLineAndCharacterFor( export const propsToFilter = [ 'parent', + 'nextContainer', 'jsDoc', + 'jsDocComment', 'lineMap', 'externalModuleIndicator', 'bindDiagnostics', From 0ada18934f77a64bb990d9a7f3a41f04ecde4987 Mon Sep 17 00:00:00 2001 From: Armano Date: Mon, 27 Dec 2021 12:04:29 +0100 Subject: [PATCH 2/4] docs(website): add guard against infinite loop to ts ast viewer --- .../website/src/components/ASTViewerTS.tsx | 9 +++ .../components/ast/serializer/serializerTS.ts | 65 ++++++++++++++----- 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/packages/website/src/components/ASTViewerTS.tsx b/packages/website/src/components/ASTViewerTS.tsx index 6cd080262960..97ee2cd30ff3 100644 --- a/packages/website/src/components/ASTViewerTS.tsx +++ b/packages/website/src/components/ASTViewerTS.tsx @@ -45,6 +45,7 @@ export default function ASTViewerTS({ const [nodeFlags] = useState(() => extractEnum(window.ts.NodeFlags)); const [tokenFlags] = useState(() => extractEnum(window.ts.TokenFlags)); const [modifierFlags] = useState(() => extractEnum(window.ts.ModifierFlags)); + const [objectFlags] = useState(() => extractEnum(window.ts.ObjectFlags)); useEffect(() => { if (typeof value === 'string') { @@ -61,6 +62,7 @@ export default function ASTViewerTS({ if (data.model.type === 'number') { switch (data.key) { case 'flags': + // TODO: use TypeFlags for Type, SymbolFlags for Symbol and FlowFlags for Flow return getFlagNamesFromEnum( nodeFlags, Number(data.model.value), @@ -78,6 +80,13 @@ export default function ASTViewerTS({ Number(data.model.value), 'ModifierFlags', ).join('\n'); + // used for Type + case 'objectFlags': + return getFlagNamesFromEnum( + objectFlags, + Number(data.model.value), + 'ObjectFlags', + ).join('\n'); case 'kind': return `SyntaxKind.${syntaxKind[Number(data.model.value)]}`; } diff --git a/packages/website/src/components/ast/serializer/serializerTS.ts b/packages/website/src/components/ast/serializer/serializerTS.ts index c1dccbe8f672..d6539c05e073 100644 --- a/packages/website/src/components/ast/serializer/serializerTS.ts +++ b/packages/website/src/components/ast/serializer/serializerTS.ts @@ -1,5 +1,5 @@ import type { ASTViewerModel, Serializer, SelectedPosition } from '../types'; -import type { SourceFile, Node } from 'typescript'; +import type { SourceFile, Node, Type, Symbol as TSSymbol } from 'typescript'; import { isRecord } from '../utils'; export function getLineAndCharacterFor( @@ -30,29 +30,64 @@ function isTsNode(value: unknown): value is Node { return isRecord(value) && typeof value.kind === 'number'; } +function isTsType(value: unknown): value is Type { + return isRecord(value) && value.getBaseTypes != null; +} + +function isTsSymbol(value: unknown): value is TSSymbol { + return isRecord(value) && value.getDeclarations != null; +} + export function createTsSerializer( root: SourceFile, syntaxKind: Record, ): Serializer { + // eslint-disable-next-line @typescript-eslint/ban-types + const SEEN_THINGS = new WeakMap<{}, ASTViewerModel>(); + return function serializer( data, - _key, + key, processValue, ): ASTViewerModel | undefined { - if (root && isTsNode(data)) { - const nodeName = syntaxKind[data.kind]; - - return { - range: { - start: getLineAndCharacterFor(data.pos, root), - end: getLineAndCharacterFor(data.end, root), - }, - type: 'object', - name: nodeName, - value: processValue( + if (root) { + if (isTsNode(data)) { + if (SEEN_THINGS.has(data)) { + return SEEN_THINGS.get(data); + } + + const nodeName = syntaxKind[data.kind]; + + const result: ASTViewerModel = { + range: { + start: getLineAndCharacterFor(data.pos, root), + end: getLineAndCharacterFor(data.end, root), + }, + type: 'object', + name: nodeName, + value: [], + }; + + SEEN_THINGS.set(data, result); + + result.value = processValue( Object.entries(data).filter(item => !propsToFilter.includes(item[0])), - ), - }; + ); + return result; + } + const tsType = isTsType(data); + const tsSymbol = isTsSymbol(data); + if (tsType || tsSymbol || key === 'flowNode') { + return { + type: 'object', + name: tsSymbol ? '[Symbol]' : tsType ? '[Type]' : '[FlowNode]', + value: processValue( + Object.entries(data).filter( + item => !propsToFilter.includes(item[0]), + ), + ), + }; + } } return undefined; }; From e22d47451de08e25ec05415b5d2bdf293f8b84ff Mon Sep 17 00:00:00 2001 From: Armano Date: Mon, 27 Dec 2021 12:16:06 +0100 Subject: [PATCH 3/4] docs(website): correct linting --- packages/website/src/components/ast/serializer/serializerTS.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/website/src/components/ast/serializer/serializerTS.ts b/packages/website/src/components/ast/serializer/serializerTS.ts index d6539c05e073..f1311d70bfe7 100644 --- a/packages/website/src/components/ast/serializer/serializerTS.ts +++ b/packages/website/src/components/ast/serializer/serializerTS.ts @@ -42,8 +42,7 @@ export function createTsSerializer( root: SourceFile, syntaxKind: Record, ): Serializer { - // eslint-disable-next-line @typescript-eslint/ban-types - const SEEN_THINGS = new WeakMap<{}, ASTViewerModel>(); + const SEEN_THINGS = new WeakMap, ASTViewerModel>(); return function serializer( data, From db82d712db73299e52e7bd4326352513581d229e Mon Sep 17 00:00:00 2001 From: Armano Date: Mon, 27 Dec 2021 13:04:25 +0100 Subject: [PATCH 4/4] docs(website): correct tooltip generation for Symbol, Type and FlowNode --- .../website/src/components/ASTViewerTS.tsx | 74 ++++------------- .../website/src/components/ast/ASTViewer.tsx | 2 - .../website/src/components/ast/Elements.tsx | 5 -- .../website/src/components/ast/SimpleItem.tsx | 20 +---- .../components/ast/serializer/serializer.ts | 14 +++- .../components/ast/serializer/serializerTS.ts | 83 ++++++++++++++++--- packages/website/src/components/ast/types.ts | 9 +- 7 files changed, 107 insertions(+), 100 deletions(-) diff --git a/packages/website/src/components/ASTViewerTS.tsx b/packages/website/src/components/ASTViewerTS.tsx index 97ee2cd30ff3..d33e524366aa 100644 --- a/packages/website/src/components/ASTViewerTS.tsx +++ b/packages/website/src/components/ASTViewerTS.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import ASTViewer from './ast/ASTViewer'; import type { ASTViewerBaseProps, ASTViewerModelMap } from './ast/types'; @@ -25,16 +25,6 @@ function extractEnum( return result; } -function getFlagNamesFromEnum( - allFlags: Record, - flags: number, - prefix: string, -): string[] { - return Object.entries(allFlags) - .filter(([f, _]) => (Number(f) & flags) !== 0) - .map(([_, name]) => `${prefix}.${name}`); -} - export default function ASTViewerTS({ value, position, @@ -46,62 +36,30 @@ export default function ASTViewerTS({ const [tokenFlags] = useState(() => extractEnum(window.ts.TokenFlags)); const [modifierFlags] = useState(() => extractEnum(window.ts.ModifierFlags)); const [objectFlags] = useState(() => extractEnum(window.ts.ObjectFlags)); + const [symbolFlags] = useState(() => extractEnum(window.ts.SymbolFlags)); + const [flowFlags] = useState(() => extractEnum(window.ts.FlowFlags)); + const [typeFlags] = useState(() => extractEnum(window.ts.TypeFlags)); useEffect(() => { if (typeof value === 'string') { setModel(value); } else { - const scopeSerializer = createTsSerializer(value, syntaxKind); + const scopeSerializer = createTsSerializer( + value, + syntaxKind, + ['NodeFlags', nodeFlags], + ['TokenFlags', tokenFlags], + ['ModifierFlags', modifierFlags], + ['ObjectFlags', objectFlags], + ['SymbolFlags', symbolFlags], + ['FlowFlags', flowFlags], + ['TypeFlags', typeFlags], + ); setModel(serialize(value, scopeSerializer)); } }, [value, syntaxKind]); - // TODO: move this to serializer - const getTooltip = useCallback( - (data: ASTViewerModelMap): string | undefined => { - if (data.model.type === 'number') { - switch (data.key) { - case 'flags': - // TODO: use TypeFlags for Type, SymbolFlags for Symbol and FlowFlags for Flow - return getFlagNamesFromEnum( - nodeFlags, - Number(data.model.value), - 'NodeFlags', - ).join('\n'); - case 'numericLiteralFlags': - return getFlagNamesFromEnum( - tokenFlags, - Number(data.model.value), - 'TokenFlags', - ).join('\n'); - case 'modifierFlagsCache': - return getFlagNamesFromEnum( - modifierFlags, - Number(data.model.value), - 'ModifierFlags', - ).join('\n'); - // used for Type - case 'objectFlags': - return getFlagNamesFromEnum( - objectFlags, - Number(data.model.value), - 'ObjectFlags', - ).join('\n'); - case 'kind': - return `SyntaxKind.${syntaxKind[Number(data.model.value)]}`; - } - } - return undefined; - }, - [nodeFlags, tokenFlags, syntaxKind], - ); - return ( - + ); } diff --git a/packages/website/src/components/ast/ASTViewer.tsx b/packages/website/src/components/ast/ASTViewer.tsx index 8b54f3420d58..65edd757b7db 100644 --- a/packages/website/src/components/ast/ASTViewer.tsx +++ b/packages/website/src/components/ast/ASTViewer.tsx @@ -8,7 +8,6 @@ import { ElementItem } from './Elements'; function ASTViewer({ position, value, - getTooltip, onSelectNode, }: ASTViewerProps): JSX.Element { const [selection, setSelection] = useState(null); @@ -29,7 +28,6 @@ function ASTViewer({ ) : (
): JSX.Element { const [isExpanded, setIsExpanded] = useState(() => level === 'ast'); const [isSelected, setIsSelected] = useState(false); @@ -69,7 +68,6 @@ export function ComplexItem({ diff --git a/packages/website/src/components/ast/SimpleItem.tsx b/packages/website/src/components/ast/SimpleItem.tsx index cb20dc0ccbb8..c6e597dc8e9c 100644 --- a/packages/website/src/components/ast/SimpleItem.tsx +++ b/packages/website/src/components/ast/SimpleItem.tsx @@ -1,31 +1,19 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback } from 'react'; import ItemGroup from './ItemGroup'; import Tooltip from '@site/src/components/inputs/Tooltip'; import PropertyValue from './PropertyValue'; -import type { - ASTViewerModelMapSimple, - GetTooltipFn, - OnSelectNodeFn, -} from './types'; +import type { ASTViewerModelMapSimple, OnSelectNodeFn } from './types'; export interface SimpleItemProps { - readonly getTooltip?: GetTooltipFn; readonly data: ASTViewerModelMapSimple; readonly onSelectNode?: OnSelectNodeFn; } export function SimpleItem({ - getTooltip, data, onSelectNode, }: SimpleItemProps): JSX.Element { - const [tooltip, setTooltip] = useState(); - - useEffect(() => { - setTooltip(getTooltip?.(data)); - }, [getTooltip, data]); - const onHover = useCallback( (state: boolean) => { if (onSelectNode && data.model.range) { @@ -37,8 +25,8 @@ export function SimpleItem({ return ( - {tooltip ? ( - + {data.model.tooltip ? ( + ) : ( diff --git a/packages/website/src/components/ast/serializer/serializer.ts b/packages/website/src/components/ast/serializer/serializer.ts index 3cbea9174a10..697dba624fb4 100644 --- a/packages/website/src/components/ast/serializer/serializer.ts +++ b/packages/website/src/components/ast/serializer/serializer.ts @@ -47,10 +47,20 @@ export function serialize( data: unknown, serializer?: Serializer, ): ASTViewerModelMap { - function processValue(data: [string, unknown][]): ASTViewerModelMap[] { - return data + function processValue( + data: [string, unknown][], + tooltip?: (data: ASTViewerModelMap) => string | undefined, + ): ASTViewerModelMap[] { + let result = data .filter(item => !item[0].startsWith('_') && item[1] !== undefined) .map(item => _serialize(item[1], item[0])); + if (tooltip) { + result = result.map(item => { + item.model.tooltip = tooltip(item); + return item; + }); + } + return result; } function _serialize(data: unknown, key?: string): ASTViewerModelMap { diff --git a/packages/website/src/components/ast/serializer/serializerTS.ts b/packages/website/src/components/ast/serializer/serializerTS.ts index f1311d70bfe7..fd03d983ba76 100644 --- a/packages/website/src/components/ast/serializer/serializerTS.ts +++ b/packages/website/src/components/ast/serializer/serializerTS.ts @@ -38,9 +38,30 @@ function isTsSymbol(value: unknown): value is TSSymbol { return isRecord(value) && value.getDeclarations != null; } +function expandFlags( + allFlags: [string, Record], + flags: number, +): string { + return Object.entries(allFlags[1]) + .filter(([f, _]) => (Number(f) & flags) !== 0) + .map(([_, name]) => `${allFlags[0]}.${name}`) + .join('\n'); +} + +function prepareValue(data: Record): [string, unknown][] { + return Object.entries(data).filter(item => !propsToFilter.includes(item[0])); +} + export function createTsSerializer( root: SourceFile, syntaxKind: Record, + nodeFlags: [string, Record], + tokenFlags: [string, Record], + modifierFlags: [string, Record], + objectFlags: [string, Record], + symbolFlags: [string, Record], + flowFlags: [string, Record], + typeFlags: [string, Record], ): Serializer { const SEEN_THINGS = new WeakMap, ASTViewerModel>(); @@ -69,22 +90,58 @@ export function createTsSerializer( SEEN_THINGS.set(data, result); - result.value = processValue( - Object.entries(data).filter(item => !propsToFilter.includes(item[0])), - ); + result.value = processValue(prepareValue(data), item => { + if (item.model.type === 'number') { + switch (item.key) { + case 'flags': + return expandFlags(nodeFlags, Number(item.model.value)); + case 'numericLiteralFlags': + return expandFlags(tokenFlags, Number(item.model.value)); + case 'modifierFlagsCache': + return expandFlags(modifierFlags, Number(item.model.value)); + case 'kind': + return `SyntaxKind.${syntaxKind[Number(item.model.value)]}`; + } + } + return undefined; + }); return result; - } - const tsType = isTsType(data); - const tsSymbol = isTsSymbol(data); - if (tsType || tsSymbol || key === 'flowNode') { + } else if (isTsType(data)) { + return { + type: 'object', + name: '[Type]', + value: processValue(prepareValue(data), item => { + if (item.model.type === 'number') { + if (item.key === 'objectFlags') { + return expandFlags(objectFlags, Number(item.model.value)); + } else if (item.key === 'flags') { + return expandFlags(typeFlags, Number(item.model.value)); + } + } + return undefined; + }), + }; + } else if (isTsSymbol(data)) { + return { + type: 'object', + name: '[Symbol]', + value: processValue(prepareValue(data), item => { + if (item.model.type === 'number' && item.key === 'flags') { + return expandFlags(symbolFlags, Number(item.model.value)); + } + return undefined; + }), + }; + } else if (key === 'flowNode' || key === 'endFlowNode') { return { type: 'object', - name: tsSymbol ? '[Symbol]' : tsType ? '[Type]' : '[FlowNode]', - value: processValue( - Object.entries(data).filter( - item => !propsToFilter.includes(item[0]), - ), - ), + name: '[FlowNode]', + value: processValue(prepareValue(data), item => { + if (item.model.type === 'number' && item.key === 'flags') { + return expandFlags(flowFlags, Number(item.model.value)); + } + return undefined; + }), }; } } diff --git a/packages/website/src/components/ast/types.ts b/packages/website/src/components/ast/types.ts index 1d5976424d2d..76f5164d07b6 100644 --- a/packages/website/src/components/ast/types.ts +++ b/packages/website/src/components/ast/types.ts @@ -1,7 +1,6 @@ import type { SelectedPosition, SelectedRange } from '../types'; import Monaco from 'monaco-editor'; -export type GetTooltipFn = (data: ASTViewerModelMap) => string | undefined; export type OnSelectNodeFn = (node: SelectedRange | null) => void; export type ASTViewerModelTypeSimple = @@ -19,6 +18,7 @@ export type ASTViewerModelTypeComplex = 'object' | 'array'; export interface ASTViewerModelBase { name?: string; range?: SelectedRange; + tooltip?: string; } export interface ASTViewerModelSimple extends ASTViewerModelBase { @@ -46,7 +46,6 @@ export interface GenericParams { readonly level: string; readonly selection?: SelectedPosition | null; readonly onSelectNode?: OnSelectNodeFn; - readonly getTooltip?: GetTooltipFn; } export interface ASTViewerBaseProps { @@ -55,14 +54,16 @@ export interface ASTViewerBaseProps { } export interface ASTViewerProps extends ASTViewerBaseProps { - readonly getTooltip?: GetTooltipFn; readonly value: ASTViewerModelMap | string; } export type Serializer = ( data: Record, key: string | undefined, - processValue: (data: [string, unknown][]) => ASTViewerModelMap[], + processValue: ( + data: [string, unknown][], + tooltip?: (data: ASTViewerModelMap) => string | undefined, + ) => ASTViewerModelMap[], ) => ASTViewerModel | undefined; export type { SelectedPosition, SelectedRange };