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

Skip to content

Commit 4bb55a2

Browse files
authored
docs(website): correct potential infinite loop in ts ast viewer (typescript-eslint#4354)
* 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
1 parent 76167bc commit 4bb55a2

File tree

7 files changed

+147
-95
lines changed

7 files changed

+147
-95
lines changed
Lines changed: 17 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback, useEffect, useState } from 'react';
1+
import React, { useEffect, useState } from 'react';
22

33
import ASTViewer from './ast/ASTViewer';
44
import type { ASTViewerBaseProps, ASTViewerModelMap } from './ast/types';
@@ -25,16 +25,6 @@ function extractEnum(
2525
return result;
2626
}
2727

28-
function getFlagNamesFromEnum(
29-
allFlags: Record<number, string>,
30-
flags: number,
31-
prefix: string,
32-
): string[] {
33-
return Object.entries(allFlags)
34-
.filter(([f, _]) => (Number(f) & flags) !== 0)
35-
.map(([_, name]) => `${prefix}.${name}`);
36-
}
37-
3828
export default function ASTViewerTS({
3929
value,
4030
position,
@@ -45,54 +35,31 @@ export default function ASTViewerTS({
4535
const [nodeFlags] = useState(() => extractEnum(window.ts.NodeFlags));
4636
const [tokenFlags] = useState(() => extractEnum(window.ts.TokenFlags));
4737
const [modifierFlags] = useState(() => extractEnum(window.ts.ModifierFlags));
38+
const [objectFlags] = useState(() => extractEnum(window.ts.ObjectFlags));
39+
const [symbolFlags] = useState(() => extractEnum(window.ts.SymbolFlags));
40+
const [flowFlags] = useState(() => extractEnum(window.ts.FlowFlags));
41+
const [typeFlags] = useState(() => extractEnum(window.ts.TypeFlags));
4842

4943
useEffect(() => {
5044
if (typeof value === 'string') {
5145
setModel(value);
5246
} else {
53-
const scopeSerializer = createTsSerializer(value, syntaxKind);
47+
const scopeSerializer = createTsSerializer(
48+
value,
49+
syntaxKind,
50+
['NodeFlags', nodeFlags],
51+
['TokenFlags', tokenFlags],
52+
['ModifierFlags', modifierFlags],
53+
['ObjectFlags', objectFlags],
54+
['SymbolFlags', symbolFlags],
55+
['FlowFlags', flowFlags],
56+
['TypeFlags', typeFlags],
57+
);
5458
setModel(serialize(value, scopeSerializer));
5559
}
5660
}, [value, syntaxKind]);
5761

58-
// TODO: move this to serializer
59-
const getTooltip = useCallback(
60-
(data: ASTViewerModelMap): string | undefined => {
61-
if (data.model.type === 'number') {
62-
switch (data.key) {
63-
case 'flags':
64-
return getFlagNamesFromEnum(
65-
nodeFlags,
66-
Number(data.model.value),
67-
'NodeFlags',
68-
).join('\n');
69-
case 'numericLiteralFlags':
70-
return getFlagNamesFromEnum(
71-
tokenFlags,
72-
Number(data.model.value),
73-
'TokenFlags',
74-
).join('\n');
75-
case 'modifierFlagsCache':
76-
return getFlagNamesFromEnum(
77-
modifierFlags,
78-
Number(data.model.value),
79-
'ModifierFlags',
80-
).join('\n');
81-
case 'kind':
82-
return `SyntaxKind.${syntaxKind[Number(data.model.value)]}`;
83-
}
84-
}
85-
return undefined;
86-
},
87-
[nodeFlags, tokenFlags, syntaxKind],
88-
);
89-
9062
return (
91-
<ASTViewer
92-
getTooltip={getTooltip}
93-
position={position}
94-
onSelectNode={onSelectNode}
95-
value={model}
96-
/>
63+
<ASTViewer position={position} onSelectNode={onSelectNode} value={model} />
9764
);
9865
}

packages/website/src/components/ast/ASTViewer.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { ElementItem } from './Elements';
88
function ASTViewer({
99
position,
1010
value,
11-
getTooltip,
1211
onSelectNode,
1312
}: ASTViewerProps): JSX.Element {
1413
const [selection, setSelection] = useState<SelectedPosition | null>(null);
@@ -29,7 +28,6 @@ function ASTViewer({
2928
) : (
3029
<div className={styles.list}>
3130
<ElementItem
32-
getTooltip={getTooltip}
3331
data={value}
3432
level="ast"
3533
selection={selection}

packages/website/src/components/ast/Elements.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ export function ComplexItem({
2020
onSelectNode,
2121
level,
2222
selection,
23-
getTooltip,
2423
}: GenericParams<ASTViewerModelMapComplex>): JSX.Element {
2524
const [isExpanded, setIsExpanded] = useState<boolean>(() => level === 'ast');
2625
const [isSelected, setIsSelected] = useState<boolean>(false);
@@ -69,7 +68,6 @@ export function ComplexItem({
6968
<ElementItem
7069
level={`${level}_${item.key}[${index}]`}
7170
key={`${level}_${item.key}[${index}]`}
72-
getTooltip={getTooltip}
7371
selection={selection}
7472
data={item}
7573
onSelectNode={onSelectNode}
@@ -90,7 +88,6 @@ export function ComplexItem({
9088

9189
export function ElementItem({
9290
level,
93-
getTooltip,
9491
selection,
9592
data,
9693
onSelectNode,
@@ -99,7 +96,6 @@ export function ElementItem({
9996
return (
10097
<ComplexItem
10198
level={level}
102-
getTooltip={getTooltip}
10399
selection={selection}
104100
onSelectNode={onSelectNode}
105101
data={data as ASTViewerModelMapComplex}
@@ -108,7 +104,6 @@ export function ElementItem({
108104
} else {
109105
return (
110106
<SimpleItem
111-
getTooltip={getTooltip}
112107
data={data as ASTViewerModelMapSimple}
113108
onSelectNode={onSelectNode}
114109
/>

packages/website/src/components/ast/SimpleItem.tsx

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,19 @@
1-
import React, { useCallback, useEffect, useState } from 'react';
1+
import React, { useCallback } from 'react';
22
import ItemGroup from './ItemGroup';
33
import Tooltip from '@site/src/components/inputs/Tooltip';
44
import PropertyValue from './PropertyValue';
55

6-
import type {
7-
ASTViewerModelMapSimple,
8-
GetTooltipFn,
9-
OnSelectNodeFn,
10-
} from './types';
6+
import type { ASTViewerModelMapSimple, OnSelectNodeFn } from './types';
117

128
export interface SimpleItemProps {
13-
readonly getTooltip?: GetTooltipFn;
149
readonly data: ASTViewerModelMapSimple;
1510
readonly onSelectNode?: OnSelectNodeFn;
1611
}
1712

1813
export function SimpleItem({
19-
getTooltip,
2014
data,
2115
onSelectNode,
2216
}: SimpleItemProps): JSX.Element {
23-
const [tooltip, setTooltip] = useState<string | undefined>();
24-
25-
useEffect(() => {
26-
setTooltip(getTooltip?.(data));
27-
}, [getTooltip, data]);
28-
2917
const onHover = useCallback(
3018
(state: boolean) => {
3119
if (onSelectNode && data.model.range) {
@@ -37,8 +25,8 @@ export function SimpleItem({
3725

3826
return (
3927
<ItemGroup data={data} onHover={data.model.range && onHover}>
40-
{tooltip ? (
41-
<Tooltip hover={true} position="right" text={tooltip}>
28+
{data.model.tooltip ? (
29+
<Tooltip hover={true} position="right" text={data.model.tooltip}>
4230
<PropertyValue value={data} />
4331
</Tooltip>
4432
) : (

packages/website/src/components/ast/serializer/serializer.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,20 @@ export function serialize(
4747
data: unknown,
4848
serializer?: Serializer,
4949
): ASTViewerModelMap {
50-
function processValue(data: [string, unknown][]): ASTViewerModelMap[] {
51-
return data
50+
function processValue(
51+
data: [string, unknown][],
52+
tooltip?: (data: ASTViewerModelMap) => string | undefined,
53+
): ASTViewerModelMap[] {
54+
let result = data
5255
.filter(item => !item[0].startsWith('_') && item[1] !== undefined)
5356
.map(item => _serialize(item[1], item[0]));
57+
if (tooltip) {
58+
result = result.map(item => {
59+
item.model.tooltip = tooltip(item);
60+
return item;
61+
});
62+
}
63+
return result;
5464
}
5565

5666
function _serialize(data: unknown, key?: string): ASTViewerModelMap {

packages/website/src/components/ast/serializer/serializerTS.ts

Lines changed: 109 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ASTViewerModel, Serializer, SelectedPosition } from '../types';
2-
import type { SourceFile, Node } from 'typescript';
2+
import type { SourceFile, Node, Type, Symbol as TSSymbol } from 'typescript';
33
import { isRecord } from '../utils';
44

55
export function getLineAndCharacterFor(
@@ -15,7 +15,9 @@ export function getLineAndCharacterFor(
1515

1616
export const propsToFilter = [
1717
'parent',
18+
'nextContainer',
1819
'jsDoc',
20+
'jsDocComment',
1921
'lineMap',
2022
'externalModuleIndicator',
2123
'bindDiagnostics',
@@ -28,29 +30,120 @@ function isTsNode(value: unknown): value is Node {
2830
return isRecord(value) && typeof value.kind === 'number';
2931
}
3032

33+
function isTsType(value: unknown): value is Type {
34+
return isRecord(value) && value.getBaseTypes != null;
35+
}
36+
37+
function isTsSymbol(value: unknown): value is TSSymbol {
38+
return isRecord(value) && value.getDeclarations != null;
39+
}
40+
41+
function expandFlags(
42+
allFlags: [string, Record<number, string>],
43+
flags: number,
44+
): string {
45+
return Object.entries(allFlags[1])
46+
.filter(([f, _]) => (Number(f) & flags) !== 0)
47+
.map(([_, name]) => `${allFlags[0]}.${name}`)
48+
.join('\n');
49+
}
50+
51+
function prepareValue(data: Record<string, unknown>): [string, unknown][] {
52+
return Object.entries(data).filter(item => !propsToFilter.includes(item[0]));
53+
}
54+
3155
export function createTsSerializer(
3256
root: SourceFile,
3357
syntaxKind: Record<number, string>,
58+
nodeFlags: [string, Record<number, string>],
59+
tokenFlags: [string, Record<number, string>],
60+
modifierFlags: [string, Record<number, string>],
61+
objectFlags: [string, Record<number, string>],
62+
symbolFlags: [string, Record<number, string>],
63+
flowFlags: [string, Record<number, string>],
64+
typeFlags: [string, Record<number, string>],
3465
): Serializer {
66+
const SEEN_THINGS = new WeakMap<Record<string, unknown>, ASTViewerModel>();
67+
3568
return function serializer(
3669
data,
37-
_key,
70+
key,
3871
processValue,
3972
): ASTViewerModel | undefined {
40-
if (root && isTsNode(data)) {
41-
const nodeName = syntaxKind[data.kind];
42-
43-
return {
44-
range: {
45-
start: getLineAndCharacterFor(data.pos, root),
46-
end: getLineAndCharacterFor(data.end, root),
47-
},
48-
type: 'object',
49-
name: nodeName,
50-
value: processValue(
51-
Object.entries(data).filter(item => !propsToFilter.includes(item[0])),
52-
),
53-
};
73+
if (root) {
74+
if (isTsNode(data)) {
75+
if (SEEN_THINGS.has(data)) {
76+
return SEEN_THINGS.get(data);
77+
}
78+
79+
const nodeName = syntaxKind[data.kind];
80+
81+
const result: ASTViewerModel = {
82+
range: {
83+
start: getLineAndCharacterFor(data.pos, root),
84+
end: getLineAndCharacterFor(data.end, root),
85+
},
86+
type: 'object',
87+
name: nodeName,
88+
value: [],
89+
};
90+
91+
SEEN_THINGS.set(data, result);
92+
93+
result.value = processValue(prepareValue(data), item => {
94+
if (item.model.type === 'number') {
95+
switch (item.key) {
96+
case 'flags':
97+
return expandFlags(nodeFlags, Number(item.model.value));
98+
case 'numericLiteralFlags':
99+
return expandFlags(tokenFlags, Number(item.model.value));
100+
case 'modifierFlagsCache':
101+
return expandFlags(modifierFlags, Number(item.model.value));
102+
case 'kind':
103+
return `SyntaxKind.${syntaxKind[Number(item.model.value)]}`;
104+
}
105+
}
106+
return undefined;
107+
});
108+
return result;
109+
} else if (isTsType(data)) {
110+
return {
111+
type: 'object',
112+
name: '[Type]',
113+
value: processValue(prepareValue(data), item => {
114+
if (item.model.type === 'number') {
115+
if (item.key === 'objectFlags') {
116+
return expandFlags(objectFlags, Number(item.model.value));
117+
} else if (item.key === 'flags') {
118+
return expandFlags(typeFlags, Number(item.model.value));
119+
}
120+
}
121+
return undefined;
122+
}),
123+
};
124+
} else if (isTsSymbol(data)) {
125+
return {
126+
type: 'object',
127+
name: '[Symbol]',
128+
value: processValue(prepareValue(data), item => {
129+
if (item.model.type === 'number' && item.key === 'flags') {
130+
return expandFlags(symbolFlags, Number(item.model.value));
131+
}
132+
return undefined;
133+
}),
134+
};
135+
} else if (key === 'flowNode' || key === 'endFlowNode') {
136+
return {
137+
type: 'object',
138+
name: '[FlowNode]',
139+
value: processValue(prepareValue(data), item => {
140+
if (item.model.type === 'number' && item.key === 'flags') {
141+
return expandFlags(flowFlags, Number(item.model.value));
142+
}
143+
return undefined;
144+
}),
145+
};
146+
}
54147
}
55148
return undefined;
56149
};

0 commit comments

Comments
 (0)