Thanks to visit codestin.com
Credit goes to github.com

Skip to content

docs(website): correct potential infinite loop in ts ast viewer #4354

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Dec 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 17 additions & 50 deletions packages/website/src/components/ASTViewerTS.tsx
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Loading