Skip to content

Commit

Permalink
docs(website): correct potential infinite loop in ts ast viewer (#4354)
Browse files Browse the repository at this point in the history
* docs(website): correct potential infinite loop in ts ast viewer

* docs(website): add guard against infinite loop to ts ast viewer

* docs(website): correct linting

* docs(website): correct tooltip generation for Symbol, Type and FlowNode
  • Loading branch information
armano2 committed Dec 28, 2021
1 parent 76167bc commit 4bb55a2
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 95 deletions.
67 changes: 17 additions & 50 deletions 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';
Expand All @@ -25,16 +25,6 @@ function extractEnum(
return result;
}

function getFlagNamesFromEnum(
allFlags: Record<number, string>,
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,
Expand All @@ -45,54 +35,31 @@ 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));
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':
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');
case 'kind':
return `SyntaxKind.${syntaxKind[Number(data.model.value)]}`;
}
}
return undefined;
},
[nodeFlags, tokenFlags, syntaxKind],
);

return (
<ASTViewer
getTooltip={getTooltip}
position={position}
onSelectNode={onSelectNode}
value={model}
/>
<ASTViewer position={position} onSelectNode={onSelectNode} value={model} />
);
}
2 changes: 0 additions & 2 deletions packages/website/src/components/ast/ASTViewer.tsx
Expand Up @@ -8,7 +8,6 @@ import { ElementItem } from './Elements';
function ASTViewer({
position,
value,
getTooltip,
onSelectNode,
}: ASTViewerProps): JSX.Element {
const [selection, setSelection] = useState<SelectedPosition | null>(null);
Expand All @@ -29,7 +28,6 @@ function ASTViewer({
) : (
<div className={styles.list}>
<ElementItem
getTooltip={getTooltip}
data={value}
level="ast"
selection={selection}
Expand Down
5 changes: 0 additions & 5 deletions packages/website/src/components/ast/Elements.tsx
Expand Up @@ -20,7 +20,6 @@ export function ComplexItem({
onSelectNode,
level,
selection,
getTooltip,
}: GenericParams<ASTViewerModelMapComplex>): JSX.Element {
const [isExpanded, setIsExpanded] = useState<boolean>(() => level === 'ast');
const [isSelected, setIsSelected] = useState<boolean>(false);
Expand Down Expand Up @@ -69,7 +68,6 @@ export function ComplexItem({
<ElementItem
level={`${level}_${item.key}[${index}]`}
key={`${level}_${item.key}[${index}]`}
getTooltip={getTooltip}
selection={selection}
data={item}
onSelectNode={onSelectNode}
Expand All @@ -90,7 +88,6 @@ export function ComplexItem({

export function ElementItem({
level,
getTooltip,
selection,
data,
onSelectNode,
Expand All @@ -99,7 +96,6 @@ export function ElementItem({
return (
<ComplexItem
level={level}
getTooltip={getTooltip}
selection={selection}
onSelectNode={onSelectNode}
data={data as ASTViewerModelMapComplex}
Expand All @@ -108,7 +104,6 @@ export function ElementItem({
} else {
return (
<SimpleItem
getTooltip={getTooltip}
data={data as ASTViewerModelMapSimple}
onSelectNode={onSelectNode}
/>
Expand Down
20 changes: 4 additions & 16 deletions 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<string | undefined>();

useEffect(() => {
setTooltip(getTooltip?.(data));
}, [getTooltip, data]);

const onHover = useCallback(
(state: boolean) => {
if (onSelectNode && data.model.range) {
Expand All @@ -37,8 +25,8 @@ export function SimpleItem({

return (
<ItemGroup data={data} onHover={data.model.range && onHover}>
{tooltip ? (
<Tooltip hover={true} position="right" text={tooltip}>
{data.model.tooltip ? (
<Tooltip hover={true} position="right" text={data.model.tooltip}>
<PropertyValue value={data} />
</Tooltip>
) : (
Expand Down
14 changes: 12 additions & 2 deletions packages/website/src/components/ast/serializer/serializer.ts
Expand Up @@ -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 {
Expand Down
125 changes: 109 additions & 16 deletions 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(
Expand All @@ -15,7 +15,9 @@ export function getLineAndCharacterFor(

export const propsToFilter = [
'parent',
'nextContainer',
'jsDoc',
'jsDocComment',
'lineMap',
'externalModuleIndicator',
'bindDiagnostics',
Expand All @@ -28,29 +30,120 @@ 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;
}

function expandFlags(
allFlags: [string, Record<number, string>],
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>): [string, unknown][] {
return Object.entries(data).filter(item => !propsToFilter.includes(item[0]));
}

export function createTsSerializer(
root: SourceFile,
syntaxKind: Record<number, string>,
nodeFlags: [string, Record<number, string>],
tokenFlags: [string, Record<number, string>],
modifierFlags: [string, Record<number, string>],
objectFlags: [string, Record<number, string>],
symbolFlags: [string, Record<number, string>],
flowFlags: [string, Record<number, string>],
typeFlags: [string, Record<number, string>],
): Serializer {
const SEEN_THINGS = new WeakMap<Record<string, unknown>, 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(
Object.entries(data).filter(item => !propsToFilter.includes(item[0])),
),
};
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(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;
} 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: '[FlowNode]',
value: processValue(prepareValue(data), item => {
if (item.model.type === 'number' && item.key === 'flags') {
return expandFlags(flowFlags, Number(item.model.value));
}
return undefined;
}),
};
}
}
return undefined;
};
Expand Down

0 comments on commit 4bb55a2

Please sign in to comment.