Skip to content

Commit 135f30a

Browse files
authored
docs(website): add preview of scope manager to playground (typescript-eslint#4312)
1 parent 0cd911a commit 135f30a

25 files changed

+823
-387
lines changed

packages/website-eslint/src/linter/linter.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,17 @@ export function loadLinter() {
99
const linter = new Linter();
1010
let storedAST;
1111
let storedTsAST;
12+
let storedScope;
1213

1314
linter.defineParser(PARSER_NAME, {
1415
parseForESLint(code, options) {
1516
const toParse = parseForESLint(code, options);
1617
storedAST = toParse.ast;
1718
storedTsAST = toParse.tsAst;
19+
storedScope = toParse.scopeManager;
1820
return toParse;
19-
}, // parse(code: string, options: ParserOptions): ParseForESLintResult['ast'] {
21+
},
22+
// parse(code: string, options: ParserOptions): ParseForESLintResult['ast'] {
2023
// const toParse = parseForESLint(code, options);
2124
// storedAST = toParse.ast;
2225
// return toParse.ast;
@@ -37,6 +40,10 @@ export function loadLinter() {
3740
return {
3841
ruleNames: ruleNames,
3942

43+
getScope() {
44+
return storedScope;
45+
},
46+
4047
getAst() {
4148
return storedAST;
4249
},
+6-7
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1-
import type { TSESLint } from '@typescript-eslint/experimental-utils';
1+
import type { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';
22
import type { ParserOptions } from '@typescript-eslint/types';
3+
import type { SourceFile } from 'typescript';
34

45
export type LintMessage = TSESLint.Linter.LintMessage;
56
export type RuleFix = TSESLint.RuleFix;
67
export type RulesRecord = TSESLint.Linter.RulesRecord;
78
export type RuleEntry = TSESLint.Linter.RuleEntry;
8-
export type ParseForESLintResult = TSESLint.Linter.ESLintParseResult;
9-
export type ESLintAST = ParseForESLintResult['ast'];
109

1110
export interface WebLinter {
1211
ruleNames: { name: string; description?: string }[];
1312

14-
getAst(): ESLintAST;
15-
getTsAst(): Record<string, unknown>;
13+
getAst(): TSESTree.Program;
14+
getTsAst(): SourceFile;
15+
getScope(): Record<string, unknown>;
1616

1717
lint(
1818
code: string,
@@ -25,11 +25,10 @@ export interface LinterLoader {
2525
loadLinter(): WebLinter;
2626
}
2727

28-
export type { TSESTree } from '@typescript-eslint/types';
29-
3028
export type {
3129
DebugLevel,
3230
EcmaVersion,
3331
ParserOptions,
3432
SourceType,
33+
TSESTree,
3534
} from '@typescript-eslint/types';
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,32 @@
1-
import React, { useCallback } from 'react';
1+
import React, { useEffect, useState } from 'react';
22

33
import ASTViewer from './ast/ASTViewer';
4-
import { isRecord } from './ast/utils';
5-
import type { ASTViewerBaseProps, SelectedRange } from './ast/types';
6-
import { TSESTree } from '@typescript-eslint/website-eslint';
4+
import type { ASTViewerBaseProps, ASTViewerModelMap } from './ast/types';
5+
import type { TSESTree } from '@typescript-eslint/website-eslint';
6+
import { serialize } from './ast/serializer/serializer';
7+
import { createESTreeSerializer } from './ast/serializer/serializerESTree';
78

8-
function isESTreeNode(
9-
value: unknown,
10-
): value is Record<string, unknown> & TSESTree.BaseNode {
11-
return isRecord(value) && 'type' in value && 'loc' in value;
9+
export interface ASTESTreeViewerProps extends ASTViewerBaseProps {
10+
readonly value: TSESTree.BaseNode | string;
1211
}
1312

14-
export const propsToFilter = ['parent', 'comments', 'tokens'];
13+
export default function ASTViewerESTree({
14+
value,
15+
position,
16+
onSelectNode,
17+
}: ASTESTreeViewerProps): JSX.Element {
18+
const [model, setModel] = useState<string | ASTViewerModelMap>('');
1519

16-
export default function ASTViewerESTree(
17-
props: ASTViewerBaseProps,
18-
): JSX.Element {
19-
const filterProps = useCallback(
20-
(item: [string, unknown]): boolean =>
21-
!propsToFilter.includes(item[0]) &&
22-
!item[0].startsWith('_') &&
23-
item[1] !== undefined,
24-
[],
25-
);
26-
27-
const getRange = useCallback(
28-
(value: unknown): SelectedRange | undefined =>
29-
isESTreeNode(value)
30-
? {
31-
start: value.loc.start,
32-
end: value.loc.end,
33-
}
34-
: undefined,
35-
[],
36-
);
37-
38-
const getNodeName = useCallback(
39-
(value: unknown): string | undefined =>
40-
isESTreeNode(value) ? String(value.type) : undefined,
41-
[],
42-
);
20+
useEffect(() => {
21+
if (typeof value === 'string') {
22+
setModel(value);
23+
} else {
24+
const astSerializer = createESTreeSerializer();
25+
setModel(serialize(value, astSerializer));
26+
}
27+
}, [value]);
4328

4429
return (
45-
<ASTViewer
46-
filterProps={filterProps}
47-
getRange={getRange}
48-
getNodeName={getNodeName}
49-
{...props}
50-
/>
30+
<ASTViewer value={model} position={position} onSelectNode={onSelectNode} />
5131
);
5232
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React, { useEffect, useState } from 'react';
2+
3+
import ASTViewer from './ast/ASTViewer';
4+
import type { ASTViewerBaseProps, ASTViewerModelMap } from './ast/types';
5+
6+
import { serialize } from './ast/serializer/serializer';
7+
import { createScopeSerializer } from './ast/serializer/serializerScope';
8+
9+
export interface ASTScopeViewerProps extends ASTViewerBaseProps {
10+
readonly value: Record<string, unknown> | string;
11+
}
12+
13+
export default function ASTViewerScope({
14+
value,
15+
onSelectNode,
16+
}: ASTScopeViewerProps): JSX.Element {
17+
const [model, setModel] = useState<string | ASTViewerModelMap>('');
18+
19+
useEffect(() => {
20+
if (typeof value === 'string') {
21+
setModel(value);
22+
} else {
23+
const scopeSerializer = createScopeSerializer();
24+
setModel(serialize(value, scopeSerializer));
25+
}
26+
}, [value]);
27+
28+
return <ASTViewer value={model} onSelectNode={onSelectNode} />;
29+
}
+50-101
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
import React, { useCallback, useEffect, useState } from 'react';
22

33
import ASTViewer from './ast/ASTViewer';
4-
import { isRecord } from './ast/utils';
5-
import type {
6-
ASTViewerBaseProps,
7-
SelectedRange,
8-
SelectedPosition,
9-
} from './ast/types';
10-
import type { Node, SourceFile } from 'typescript';
4+
import type { ASTViewerBaseProps, ASTViewerModelMap } from './ast/types';
5+
import type { SourceFile } from 'typescript';
6+
import { serialize } from './ast/serializer/serializer';
7+
import { createTsSerializer } from './ast/serializer/serializerTS';
118

129
export interface ASTTsViewerProps extends ASTViewerBaseProps {
13-
readonly version: string;
10+
readonly value: SourceFile | string;
1411
}
1512

1613
function extractEnum(
@@ -28,10 +25,6 @@ function extractEnum(
2825
return result;
2926
}
3027

31-
function isTsNode(value: unknown): value is Node {
32-
return isRecord(value) && typeof value.kind === 'number';
33-
}
34-
3528
function getFlagNamesFromEnum(
3629
allFlags: Record<number, string>,
3730
flags: number,
@@ -42,108 +35,64 @@ function getFlagNamesFromEnum(
4235
.map(([_, name]) => `${prefix}.${name}`);
4336
}
4437

45-
export function getLineAndCharacterFor(
46-
pos: number,
47-
ast: SourceFile,
48-
): SelectedPosition {
49-
const loc = ast.getLineAndCharacterOfPosition(pos);
50-
return {
51-
line: loc.line + 1,
52-
column: loc.character,
53-
};
54-
}
55-
56-
export function getLocFor(
57-
start: number,
58-
end: number,
59-
ast: SourceFile,
60-
): SelectedRange {
61-
return {
62-
start: getLineAndCharacterFor(start, ast),
63-
end: getLineAndCharacterFor(end, ast),
64-
};
65-
}
66-
67-
export const propsToFilter = [
68-
'parent',
69-
'jsDoc',
70-
'lineMap',
71-
'externalModuleIndicator',
72-
'bindDiagnostics',
73-
'transformFlags',
74-
'resolvedModules',
75-
'imports',
76-
];
77-
78-
export default function ASTViewerTS(props: ASTTsViewerProps): JSX.Element {
79-
const [syntaxKind, setSyntaxKind] = useState<Record<number, string>>({});
80-
const [nodeFlags, setNodeFlags] = useState<Record<number, string>>({});
81-
const [tokenFlags, setTokenFlags] = useState<Record<number, string>>({});
82-
const [modifierFlags, setModifierFlags] = useState<Record<number, string>>(
83-
{},
84-
);
38+
export default function ASTViewerTS({
39+
value,
40+
position,
41+
onSelectNode,
42+
}: ASTTsViewerProps): JSX.Element {
43+
const [model, setModel] = useState<string | ASTViewerModelMap>('');
44+
const [syntaxKind] = useState(() => extractEnum(window.ts.SyntaxKind));
45+
const [nodeFlags] = useState(() => extractEnum(window.ts.NodeFlags));
46+
const [tokenFlags] = useState(() => extractEnum(window.ts.TokenFlags));
47+
const [modifierFlags] = useState(() => extractEnum(window.ts.ModifierFlags));
8548

8649
useEffect(() => {
87-
setSyntaxKind(extractEnum(window.ts.SyntaxKind));
88-
setNodeFlags(extractEnum(window.ts.NodeFlags));
89-
setTokenFlags(extractEnum(window.ts.TokenFlags));
90-
setModifierFlags(extractEnum(window.ts.ModifierFlags));
91-
}, [props.version]);
50+
if (typeof value === 'string') {
51+
setModel(value);
52+
} else {
53+
const scopeSerializer = createTsSerializer(value, syntaxKind);
54+
setModel(serialize(value, scopeSerializer));
55+
}
56+
}, [value, syntaxKind]);
9257

58+
// TODO: move this to serializer
9359
const getTooltip = useCallback(
94-
(key: string, value: unknown): string | undefined => {
95-
if (key === 'flags' && typeof value === 'number') {
96-
return getFlagNamesFromEnum(nodeFlags, value, 'NodeFlags').join('\n');
97-
} else if (key === 'numericLiteralFlags' && typeof value === 'number') {
98-
return getFlagNamesFromEnum(tokenFlags, value, 'TokenFlags').join('\n');
99-
} else if (key === 'modifierFlagsCache' && typeof value === 'number') {
100-
return getFlagNamesFromEnum(modifierFlags, value, 'ModifierFlags').join(
101-
'\n',
102-
);
103-
} else if (key === 'kind' && typeof value === 'number') {
104-
return `SyntaxKind.${syntaxKind[value]}`;
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+
}
10584
}
10685
return undefined;
10786
},
10887
[nodeFlags, tokenFlags, syntaxKind],
10988
);
11089

111-
const getNodeName = useCallback(
112-
(value: unknown): string | undefined =>
113-
isTsNode(value) ? syntaxKind[value.kind] : undefined,
114-
[syntaxKind],
115-
);
116-
117-
const filterProps = useCallback(
118-
(item: [string, unknown]): boolean =>
119-
!propsToFilter.includes(item[0]) &&
120-
!item[0].startsWith('_') &&
121-
item[1] !== undefined,
122-
[],
123-
);
124-
125-
const getRange = useCallback(
126-
(value: unknown): SelectedRange | undefined => {
127-
if (props.value && isTsNode(value)) {
128-
return getLocFor(
129-
value.pos,
130-
value.end,
131-
// @ts-expect-error: unsafe cast
132-
props.value as SourceFile,
133-
);
134-
}
135-
return undefined;
136-
},
137-
[props.value],
138-
);
139-
14090
return (
14191
<ASTViewer
142-
filterProps={filterProps}
143-
getRange={getRange}
14492
getTooltip={getTooltip}
145-
getNodeName={getNodeName}
146-
{...props}
93+
position={position}
94+
onSelectNode={onSelectNode}
95+
value={model}
14796
/>
14897
);
14998
}

packages/website/src/components/OptionsSelector.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const ASTOptions = [
3232
{ value: false, label: 'Disabled' },
3333
{ value: 'es', label: 'ESTree' },
3434
{ value: 'ts', label: 'TypeScript' },
35+
{ value: 'scope', label: 'Scope' },
3536
] as const;
3637

3738
function OptionsSelector({

0 commit comments

Comments
 (0)