Skip to content

docs(website): add preview of scope manager to playground #4312

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 17 commits into from
Dec 23, 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
9 changes: 8 additions & 1 deletion packages/website-eslint/src/linter/linter.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@ export function loadLinter() {
const linter = new Linter();
let storedAST;
let storedTsAST;
let storedScope;

linter.defineParser(PARSER_NAME, {
parseForESLint(code, options) {
const toParse = parseForESLint(code, options);
storedAST = toParse.ast;
storedTsAST = toParse.tsAst;
storedScope = toParse.scopeManager;
return toParse;
}, // parse(code: string, options: ParserOptions): ParseForESLintResult['ast'] {
},
// parse(code: string, options: ParserOptions): ParseForESLintResult['ast'] {
// const toParse = parseForESLint(code, options);
// storedAST = toParse.ast;
// return toParse.ast;
Expand All @@ -37,6 +40,10 @@ export function loadLinter() {
return {
ruleNames: ruleNames,

getScope() {
return storedScope;
},

getAst() {
return storedAST;
},
Expand Down
13 changes: 6 additions & 7 deletions packages/website-eslint/types/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import type { TSESLint } from '@typescript-eslint/experimental-utils';
import type { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';
import type { ParserOptions } from '@typescript-eslint/types';
import type { SourceFile } from 'typescript';

export type LintMessage = TSESLint.Linter.LintMessage;
export type RuleFix = TSESLint.RuleFix;
export type RulesRecord = TSESLint.Linter.RulesRecord;
export type RuleEntry = TSESLint.Linter.RuleEntry;
export type ParseForESLintResult = TSESLint.Linter.ESLintParseResult;
export type ESLintAST = ParseForESLintResult['ast'];

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

getAst(): ESLintAST;
getTsAst(): Record<string, unknown>;
getAst(): TSESTree.Program;
getTsAst(): SourceFile;
getScope(): Record<string, unknown>;

lint(
code: string,
Expand All @@ -25,11 +25,10 @@ export interface LinterLoader {
loadLinter(): WebLinter;
}

export type { TSESTree } from '@typescript-eslint/types';

export type {
DebugLevel,
EcmaVersion,
ParserOptions,
SourceType,
TSESTree,
} from '@typescript-eslint/types';
64 changes: 22 additions & 42 deletions packages/website/src/components/ASTViewerESTree.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,32 @@
import React, { useCallback } from 'react';
import React, { useEffect, useState } from 'react';

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

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

export const propsToFilter = ['parent', 'comments', 'tokens'];
export default function ASTViewerESTree({
value,
position,
onSelectNode,
}: ASTESTreeViewerProps): JSX.Element {
const [model, setModel] = useState<string | ASTViewerModelMap>('');

export default function ASTViewerESTree(
props: ASTViewerBaseProps,
): JSX.Element {
const filterProps = useCallback(
(item: [string, unknown]): boolean =>
!propsToFilter.includes(item[0]) &&
!item[0].startsWith('_') &&
item[1] !== undefined,
[],
);

const getRange = useCallback(
(value: unknown): SelectedRange | undefined =>
isESTreeNode(value)
? {
start: value.loc.start,
end: value.loc.end,
}
: undefined,
[],
);

const getNodeName = useCallback(
(value: unknown): string | undefined =>
isESTreeNode(value) ? String(value.type) : undefined,
[],
);
useEffect(() => {
if (typeof value === 'string') {
setModel(value);
} else {
const astSerializer = createESTreeSerializer();
setModel(serialize(value, astSerializer));
}
}, [value]);

return (
<ASTViewer
filterProps={filterProps}
getRange={getRange}
getNodeName={getNodeName}
{...props}
/>
<ASTViewer value={model} position={position} onSelectNode={onSelectNode} />
);
}
29 changes: 29 additions & 0 deletions packages/website/src/components/ASTViewerScope.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React, { useEffect, useState } from 'react';

import ASTViewer from './ast/ASTViewer';
import type { ASTViewerBaseProps, ASTViewerModelMap } from './ast/types';

import { serialize } from './ast/serializer/serializer';
import { createScopeSerializer } from './ast/serializer/serializerScope';

export interface ASTScopeViewerProps extends ASTViewerBaseProps {
readonly value: Record<string, unknown> | string;
}

export default function ASTViewerScope({
value,
onSelectNode,
}: ASTScopeViewerProps): JSX.Element {
const [model, setModel] = useState<string | ASTViewerModelMap>('');

useEffect(() => {
if (typeof value === 'string') {
setModel(value);
} else {
const scopeSerializer = createScopeSerializer();
setModel(serialize(value, scopeSerializer));
}
}, [value]);

return <ASTViewer value={model} onSelectNode={onSelectNode} />;
}
151 changes: 50 additions & 101 deletions packages/website/src/components/ASTViewerTS.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import React, { useCallback, useEffect, useState } from 'react';

import ASTViewer from './ast/ASTViewer';
import { isRecord } from './ast/utils';
import type {
ASTViewerBaseProps,
SelectedRange,
SelectedPosition,
} from './ast/types';
import type { Node, SourceFile } from 'typescript';
import type { ASTViewerBaseProps, ASTViewerModelMap } from './ast/types';
import type { SourceFile } from 'typescript';
import { serialize } from './ast/serializer/serializer';
import { createTsSerializer } from './ast/serializer/serializerTS';

export interface ASTTsViewerProps extends ASTViewerBaseProps {
readonly version: string;
readonly value: SourceFile | string;
}

function extractEnum(
Expand All @@ -28,10 +25,6 @@ function extractEnum(
return result;
}

function isTsNode(value: unknown): value is Node {
return isRecord(value) && typeof value.kind === 'number';
}

function getFlagNamesFromEnum(
allFlags: Record<number, string>,
flags: number,
Expand All @@ -42,108 +35,64 @@ function getFlagNamesFromEnum(
.map(([_, name]) => `${prefix}.${name}`);
}

export function getLineAndCharacterFor(
pos: number,
ast: SourceFile,
): SelectedPosition {
const loc = ast.getLineAndCharacterOfPosition(pos);
return {
line: loc.line + 1,
column: loc.character,
};
}

export function getLocFor(
start: number,
end: number,
ast: SourceFile,
): SelectedRange {
return {
start: getLineAndCharacterFor(start, ast),
end: getLineAndCharacterFor(end, ast),
};
}

export const propsToFilter = [
'parent',
'jsDoc',
'lineMap',
'externalModuleIndicator',
'bindDiagnostics',
'transformFlags',
'resolvedModules',
'imports',
];

export default function ASTViewerTS(props: ASTTsViewerProps): JSX.Element {
const [syntaxKind, setSyntaxKind] = useState<Record<number, string>>({});
const [nodeFlags, setNodeFlags] = useState<Record<number, string>>({});
const [tokenFlags, setTokenFlags] = useState<Record<number, string>>({});
const [modifierFlags, setModifierFlags] = useState<Record<number, string>>(
{},
);
export default function ASTViewerTS({
value,
position,
onSelectNode,
}: ASTTsViewerProps): JSX.Element {
const [model, setModel] = useState<string | ASTViewerModelMap>('');
const [syntaxKind] = useState(() => extractEnum(window.ts.SyntaxKind));
const [nodeFlags] = useState(() => extractEnum(window.ts.NodeFlags));
const [tokenFlags] = useState(() => extractEnum(window.ts.TokenFlags));
const [modifierFlags] = useState(() => extractEnum(window.ts.ModifierFlags));

useEffect(() => {
setSyntaxKind(extractEnum(window.ts.SyntaxKind));
setNodeFlags(extractEnum(window.ts.NodeFlags));
setTokenFlags(extractEnum(window.ts.TokenFlags));
setModifierFlags(extractEnum(window.ts.ModifierFlags));
}, [props.version]);
if (typeof value === 'string') {
setModel(value);
} else {
const scopeSerializer = createTsSerializer(value, syntaxKind);
setModel(serialize(value, scopeSerializer));
}
}, [value, syntaxKind]);

// TODO: move this to serializer
const getTooltip = useCallback(
(key: string, value: unknown): string | undefined => {
if (key === 'flags' && typeof value === 'number') {
return getFlagNamesFromEnum(nodeFlags, value, 'NodeFlags').join('\n');
} else if (key === 'numericLiteralFlags' && typeof value === 'number') {
return getFlagNamesFromEnum(tokenFlags, value, 'TokenFlags').join('\n');
} else if (key === 'modifierFlagsCache' && typeof value === 'number') {
return getFlagNamesFromEnum(modifierFlags, value, 'ModifierFlags').join(
'\n',
);
} else if (key === 'kind' && typeof value === 'number') {
return `SyntaxKind.${syntaxKind[value]}`;
(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],
);

const getNodeName = useCallback(
(value: unknown): string | undefined =>
isTsNode(value) ? syntaxKind[value.kind] : undefined,
[syntaxKind],
);

const filterProps = useCallback(
(item: [string, unknown]): boolean =>
!propsToFilter.includes(item[0]) &&
!item[0].startsWith('_') &&
item[1] !== undefined,
[],
);

const getRange = useCallback(
(value: unknown): SelectedRange | undefined => {
if (props.value && isTsNode(value)) {
return getLocFor(
value.pos,
value.end,
// @ts-expect-error: unsafe cast
props.value as SourceFile,
);
}
return undefined;
},
[props.value],
);

return (
<ASTViewer
filterProps={filterProps}
getRange={getRange}
getTooltip={getTooltip}
getNodeName={getNodeName}
{...props}
position={position}
onSelectNode={onSelectNode}
value={model}
/>
);
}
1 change: 1 addition & 0 deletions packages/website/src/components/OptionsSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const ASTOptions = [
{ value: false, label: 'Disabled' },
{ value: 'es', label: 'ESTree' },
{ value: 'ts', label: 'TypeScript' },
{ value: 'scope', label: 'Scope' },
] as const;

function OptionsSelector({
Expand Down
Loading