From f50f446d2034f57700f0486d5d03fedf14dc7196 Mon Sep 17 00:00:00 2001 From: Armano Date: Wed, 15 Dec 2021 12:49:24 +0100 Subject: [PATCH 01/13] docs(website): add scope manager to playground preview --- packages/website-eslint/src/linter/linter.js | 9 +++++++- packages/website-eslint/types/index.d.ts | 1 + .../website/src/components/ASTViewerScope.tsx | 22 +++++++++++++++++++ .../src/components/OptionsSelector.tsx | 1 + .../website/src/components/Playground.tsx | 10 +++++++++ .../src/components/editor/LoadedEditor.tsx | 2 ++ .../website/src/components/editor/types.ts | 1 + .../src/components/hooks/useHashState.ts | 4 +++- packages/website/src/components/types.ts | 2 +- 9 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 packages/website/src/components/ASTViewerScope.tsx diff --git a/packages/website-eslint/src/linter/linter.js b/packages/website-eslint/src/linter/linter.js index 1643dcf2562f..aae8ecc0feda 100644 --- a/packages/website-eslint/src/linter/linter.js +++ b/packages/website-eslint/src/linter/linter.js @@ -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; @@ -37,6 +40,10 @@ export function loadLinter() { return { ruleNames: ruleNames, + getScope() { + return storedScope; + }, + getAst() { return storedAST; }, diff --git a/packages/website-eslint/types/index.d.ts b/packages/website-eslint/types/index.d.ts index 86e0c02eed6c..582e5b0860fa 100644 --- a/packages/website-eslint/types/index.d.ts +++ b/packages/website-eslint/types/index.d.ts @@ -13,6 +13,7 @@ export interface WebLinter { getAst(): ESLintAST; getTsAst(): Record; + getScope(): Record; lint( code: string, diff --git a/packages/website/src/components/ASTViewerScope.tsx b/packages/website/src/components/ASTViewerScope.tsx new file mode 100644 index 000000000000..1553fbde60f2 --- /dev/null +++ b/packages/website/src/components/ASTViewerScope.tsx @@ -0,0 +1,22 @@ +import React, { useCallback } from 'react'; + +import ASTViewer from './ast/ASTViewer'; +import type { ASTViewerBaseProps } from './ast/types'; + +export default function ASTViewerScope(props: ASTViewerBaseProps): JSX.Element { + const filterProps = useCallback( + (item: [string, unknown]): boolean => + !item[0].startsWith('_') && item[1] !== undefined, + [], + ); + + return ( + undefined} + getTooltip={(): undefined => undefined} + getNodeName={(): undefined => undefined} + {...props} + /> + ); +} diff --git a/packages/website/src/components/OptionsSelector.tsx b/packages/website/src/components/OptionsSelector.tsx index ee1f14648d9d..9acc8677df7b 100644 --- a/packages/website/src/components/OptionsSelector.tsx +++ b/packages/website/src/components/OptionsSelector.tsx @@ -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({ diff --git a/packages/website/src/components/Playground.tsx b/packages/website/src/components/Playground.tsx index 844b955dd8b8..8e2971a9c5c6 100644 --- a/packages/website/src/components/Playground.tsx +++ b/packages/website/src/components/Playground.tsx @@ -18,6 +18,7 @@ import ASTViewerTS from './ASTViewerTS'; import type { RuleDetails, SelectedRange } from './types'; import type { TSESTree } from '@typescript-eslint/website-eslint'; +import ASTViewerScope from '@site/src/components/ASTViewerScope'; function Playground(): JSX.Element { const [state, setState] = useHashState({ @@ -32,6 +33,7 @@ function Playground(): JSX.Element { const { isDarkTheme } = useThemeContext(); const [esAst, setEsAst] = useState(); const [tsAst, setTsAST] = useState | string | null>(); + const [scope, setScope] = useState | string | null>(); const [ruleNames, setRuleNames] = useState([]); const [isLoading, setIsLoading] = useState(true); const [tsVersions, setTSVersion] = useState([]); @@ -85,6 +87,7 @@ function Playground(): JSX.Element { showAST={state.showAST} onEsASTChange={setEsAst} onTsASTChange={setTsAST} + onScopeChange={setScope} decoration={selectedRange} onChange={(code): void => setState({ code: code })} onLoaded={(ruleNames, tsVersions): void => { @@ -105,6 +108,13 @@ function Playground(): JSX.Element { version={state.ts} /> )) || + (state.showAST === 'scope' && scope && ( + + )) || (esAst && ( = ({ jsx, main, onEsASTChange, + onScopeChange, onTsASTChange, onChange, onSelect, @@ -65,6 +66,7 @@ export const LoadedEditor: React.FC = ({ onEsASTChange(fatalMessage ?? webLinter.getAst()); onTsASTChange(fatalMessage ?? webLinter.getTsAst()); + onScopeChange(fatalMessage ?? webLinter.getScope()); onSelect(sandboxInstance.editor.getPosition()); }, 500), [code, jsx, sandboxInstance, rules, sourceType, webLinter], diff --git a/packages/website/src/components/editor/types.ts b/packages/website/src/components/editor/types.ts index 56048321473f..e0e099c0fe86 100644 --- a/packages/website/src/components/editor/types.ts +++ b/packages/website/src/components/editor/types.ts @@ -8,5 +8,6 @@ export interface CommonEditorProps extends ConfigModel { readonly onChange: (value: string) => void; readonly onTsASTChange: (value: string | Record) => void; readonly onEsASTChange: (value: string | TSESTree.Program) => void; + readonly onScopeChange: (value: string | Record) => void; readonly onSelect: (position: Monaco.Position | null) => void; } diff --git a/packages/website/src/components/hooks/useHashState.ts b/packages/website/src/components/hooks/useHashState.ts index 9c495facc591..f8b7e1270171 100644 --- a/packages/website/src/components/hooks/useHashState.ts +++ b/packages/website/src/components/hooks/useHashState.ts @@ -15,12 +15,14 @@ function readQueryParam(value: string | null, fallback: string): string { : fallback; } -function readShowAST(value: string | null): 'ts' | 'es' | boolean { +function readShowAST(value: string | null): 'ts' | 'scope' | 'es' | boolean { switch (value) { case 'es': return 'es'; case 'ts': return 'ts'; + case 'scope': + return 'scope'; } return Boolean(value); } diff --git a/packages/website/src/components/types.ts b/packages/website/src/components/types.ts index 7830baa224c5..5e1e3a721950 100644 --- a/packages/website/src/components/types.ts +++ b/packages/website/src/components/types.ts @@ -42,7 +42,7 @@ export interface ConfigModel { tsConfig?: CompilerFlags; code: string; ts: string; - showAST?: boolean | 'ts' | 'es'; + showAST?: boolean | 'ts' | 'es' | 'scope'; } export interface SelectedPosition { From 1c587714aecd75fec161c034ee07c7d1989497cb Mon Sep 17 00:00:00 2001 From: Armano Date: Thu, 16 Dec 2021 12:48:29 +0100 Subject: [PATCH 02/13] docs(website): print node name in scope manager --- .../website/src/components/ASTViewerScope.tsx | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/packages/website/src/components/ASTViewerScope.tsx b/packages/website/src/components/ASTViewerScope.tsx index 1553fbde60f2..55300d7c1e57 100644 --- a/packages/website/src/components/ASTViewerScope.tsx +++ b/packages/website/src/components/ASTViewerScope.tsx @@ -3,10 +3,44 @@ import React, { useCallback } from 'react'; import ASTViewer from './ast/ASTViewer'; import type { ASTViewerBaseProps } from './ast/types'; +import { isRecord } from './ast/utils'; + +export const propsToFilter = [ + 'parent', + 'comments', + 'tokens', + // 'block', + 'upper', + '$id', +]; + +function isScopeNode(node: unknown): node is Record & { + constructor: { name: string }; +} { + return ( + isRecord(node) && + 'constructor' in node && + node.constructor.name !== 'Object' + ); +} + +function getNodeName(value: unknown): string | undefined { + if (isScopeNode(value)) { + const name = String(value.constructor.name).replace(/\$[0-9]+$/, ''); + if (value.$id) { + return `${name}$${String(value.$id)}`; + } + return name; + } + return undefined; +} + export default function ASTViewerScope(props: ASTViewerBaseProps): JSX.Element { const filterProps = useCallback( (item: [string, unknown]): boolean => - !item[0].startsWith('_') && item[1] !== undefined, + !propsToFilter.includes(item[0]) && + !item[0].startsWith('_') && + item[1] !== undefined, [], ); @@ -14,8 +48,7 @@ export default function ASTViewerScope(props: ASTViewerBaseProps): JSX.Element { undefined} - getTooltip={(): undefined => undefined} - getNodeName={(): undefined => undefined} + getNodeName={getNodeName} {...props} /> ); From 52efab8da4391646ee3cea681fcf664362863d46 Mon Sep 17 00:00:00 2001 From: Armano Date: Thu, 16 Dec 2021 20:25:17 +0100 Subject: [PATCH 03/13] docs(website): add serializer to ast viewer --- .../src/components/ASTViewerESTree.tsx | 58 +++---- .../website/src/components/ASTViewerScope.tsx | 63 +++----- .../website/src/components/ASTViewerTS.tsx | 72 +++------ .../website/src/components/ast/ASTViewer.tsx | 14 +- .../website/src/components/ast/Elements.tsx | 144 +++++++----------- .../website/src/components/ast/HiddenItem.tsx | 13 +- .../website/src/components/ast/ItemGroup.tsx | 11 +- .../src/components/ast/PropertyName.tsx | 16 +- .../src/components/ast/PropertyValue.tsx | 45 +++--- .../website/src/components/ast/SimpleItem.tsx | 34 +++++ .../components/ast/serializer/serializer.ts | 78 ++++++++++ .../ast/serializer/serializerESTree.ts | 34 +++++ .../ast/serializer/serializerScope.ts | 93 +++++++++++ .../components/ast/serializer/serializerTS.ts | 57 +++++++ .../src/components/ast/serializer/types.ts | 9 ++ packages/website/src/components/ast/types.ts | 49 ++++-- packages/website/tsconfig.json | 2 +- 17 files changed, 517 insertions(+), 275 deletions(-) create mode 100644 packages/website/src/components/ast/SimpleItem.tsx create mode 100644 packages/website/src/components/ast/serializer/serializer.ts create mode 100644 packages/website/src/components/ast/serializer/serializerESTree.ts create mode 100644 packages/website/src/components/ast/serializer/serializerScope.ts create mode 100644 packages/website/src/components/ast/serializer/serializerTS.ts create mode 100644 packages/website/src/components/ast/serializer/types.ts diff --git a/packages/website/src/components/ASTViewerESTree.tsx b/packages/website/src/components/ASTViewerESTree.tsx index 899daeedba9a..7bd2e1be3c69 100644 --- a/packages/website/src/components/ASTViewerESTree.tsx +++ b/packages/website/src/components/ASTViewerESTree.tsx @@ -1,52 +1,34 @@ -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, ASTViewerModel } 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 & 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( - props: ASTViewerBaseProps, + props: ASTESTreeViewerProps, ): JSX.Element { - const filterProps = useCallback( - (item: [string, unknown]): boolean => - !propsToFilter.includes(item[0]) && - !item[0].startsWith('_') && - item[1] !== undefined, - [], - ); + const [model, setModel] = useState(''); - 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 props.value === 'string') { + setModel(props.value); + } else { + const scopeSerializer = createESTreeSerializer(); + setModel(serialize(props.value, scopeSerializer)); + } + }, [props.value]); return ( ); } diff --git a/packages/website/src/components/ASTViewerScope.tsx b/packages/website/src/components/ASTViewerScope.tsx index 55300d7c1e57..7588584170e1 100644 --- a/packages/website/src/components/ASTViewerScope.tsx +++ b/packages/website/src/components/ASTViewerScope.tsx @@ -1,55 +1,34 @@ -import React, { useCallback } from 'react'; +import React, { useEffect, useState } from 'react'; import ASTViewer from './ast/ASTViewer'; -import type { ASTViewerBaseProps } from './ast/types'; +import type { ASTViewerBaseProps, ASTViewerModel } from './ast/types'; -import { isRecord } from './ast/utils'; +import { serialize } from './ast/serializer/serializer'; +import { createScopeSerializer } from './ast/serializer/serializerScope'; -export const propsToFilter = [ - 'parent', - 'comments', - 'tokens', - // 'block', - 'upper', - '$id', -]; - -function isScopeNode(node: unknown): node is Record & { - constructor: { name: string }; -} { - return ( - isRecord(node) && - 'constructor' in node && - node.constructor.name !== 'Object' - ); +export interface ASTScopeViewerProps extends ASTViewerBaseProps { + readonly value: Record | string; } -function getNodeName(value: unknown): string | undefined { - if (isScopeNode(value)) { - const name = String(value.constructor.name).replace(/\$[0-9]+$/, ''); - if (value.$id) { - return `${name}$${String(value.$id)}`; - } - return name; - } - return undefined; -} +export default function ASTViewerScope( + props: ASTScopeViewerProps, +): JSX.Element { + const [model, setModel] = useState(''); -export default function ASTViewerScope(props: ASTViewerBaseProps): JSX.Element { - const filterProps = useCallback( - (item: [string, unknown]): boolean => - !propsToFilter.includes(item[0]) && - !item[0].startsWith('_') && - item[1] !== undefined, - [], - ); + useEffect(() => { + if (typeof props.value === 'string') { + setModel(props.value); + } else { + const scopeSerializer = createScopeSerializer(); + setModel(serialize(props.value, scopeSerializer)); + } + }, [props.value]); return ( undefined} - getNodeName={getNodeName} - {...props} + value={model} + position={props.position} + onSelectNode={props.onSelectNode} /> ); } diff --git a/packages/website/src/components/ASTViewerTS.tsx b/packages/website/src/components/ASTViewerTS.tsx index f30265ba574e..fb34de3a468f 100644 --- a/packages/website/src/components/ASTViewerTS.tsx +++ b/packages/website/src/components/ASTViewerTS.tsx @@ -1,16 +1,19 @@ import React, { useCallback, useEffect, useState } from 'react'; import ASTViewer from './ast/ASTViewer'; -import { isRecord } from './ast/utils'; import type { ASTViewerBaseProps, + ASTViewerModel, SelectedRange, SelectedPosition, } from './ast/types'; -import type { Node, SourceFile } from 'typescript'; +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: Record | string; } function extractEnum( @@ -28,10 +31,6 @@ function extractEnum( return result; } -function isTsNode(value: unknown): value is Node { - return isRecord(value) && typeof value.kind === 'number'; -} - function getFlagNamesFromEnum( allFlags: Record, flags: number, @@ -64,18 +63,8 @@ export function getLocFor( }; } -export const propsToFilter = [ - 'parent', - 'jsDoc', - 'lineMap', - 'externalModuleIndicator', - 'bindDiagnostics', - 'transformFlags', - 'resolvedModules', - 'imports', -]; - export default function ASTViewerTS(props: ASTTsViewerProps): JSX.Element { + const [model, setModel] = useState(''); const [syntaxKind, setSyntaxKind] = useState>({}); const [nodeFlags, setNodeFlags] = useState>({}); const [tokenFlags, setTokenFlags] = useState>({}); @@ -90,6 +79,19 @@ export default function ASTViewerTS(props: ASTTsViewerProps): JSX.Element { setModifierFlags(extractEnum(window.ts.ModifierFlags)); }, [props.version]); + useEffect(() => { + if (typeof props.value === 'string') { + setModel(props.value); + } else { + const scopeSerializer = createTsSerializer( + // @ts-expect-error: unsafe cast + props.value as SourceFile, + syntaxKind, + ); + setModel(serialize(props.value, scopeSerializer)); + } + }, [props.value, syntaxKind]); + const getTooltip = useCallback( (key: string, value: unknown): string | undefined => { if (key === 'flags' && typeof value === 'number') { @@ -108,42 +110,12 @@ export default function ASTViewerTS(props: ASTTsViewerProps): JSX.Element { [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 ( ); } diff --git a/packages/website/src/components/ast/ASTViewer.tsx b/packages/website/src/components/ast/ASTViewer.tsx index aa1e35687ec0..46823400dbde 100644 --- a/packages/website/src/components/ast/ASTViewer.tsx +++ b/packages/website/src/components/ast/ASTViewer.tsx @@ -3,8 +3,7 @@ import styles from './ASTViewer.module.css'; import type { SelectedPosition, ASTViewerProps } from './types'; -import { ComplexItem } from './Elements'; -import { isRecord } from './utils'; +import { ElementItem } from './Elements'; function ASTViewer(props: ASTViewerProps): JSX.Element { const [selection, setSelection] = useState(null); @@ -20,21 +19,18 @@ function ASTViewer(props: ASTViewerProps): JSX.Element { ); }, [props.position]); - return isRecord(props.value) ? ( + return typeof props.value === 'string' ? ( +
{props.value}
+ ) : (
-
- ) : ( -
{props.value}
); } diff --git a/packages/website/src/components/ast/Elements.tsx b/packages/website/src/components/ast/Elements.tsx index be573770d94d..9befc4930a0e 100644 --- a/packages/website/src/components/ast/Elements.tsx +++ b/packages/website/src/components/ast/Elements.tsx @@ -1,35 +1,28 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, /* useEffect, */ useState } from 'react'; -import type { GenericParams } from './types'; +import type { GenericParams, ASTViewerModelComplex } from './types'; -import { hasChildInRange, isArrayInRange, isInRange, isRecord } from './utils'; +// import { hasChildInRange, isArrayInRange, isInRange } from './utils'; -import styles from '@site/src/components/ast/ASTViewer.module.css'; +import styles from './ASTViewer.module.css'; -import PropertyValue from '@site/src/components/ast/PropertyValue'; -import ItemGroup from '@site/src/components/ast/ItemGroup'; -import HiddenItem from '@site/src/components/ast/HiddenItem'; -import Tooltip from '@site/src/components/inputs/Tooltip'; +import ItemGroup from './ItemGroup'; +import HiddenItem from './HiddenItem'; +import { ASTViewerModel, ASTViewerModelSimple } from './types'; +import { SimpleItem } from './SimpleItem'; export function ComplexItem( - props: GenericParams | unknown[]>, + props: GenericParams, ): JSX.Element { const [isExpanded, setIsExpanded] = useState( () => props.level === 'ast', ); - const [isSelected, setIsSelected] = useState(false); - const [model, setModel] = useState<[string, unknown][]>([]); - - useEffect(() => { - setModel( - Object.entries(props.value).filter(item => props.filterProps(item)), - ); - }, [props.value, props.filterProps]); + const [isSelected /* setIsSelected */] = useState(false); const onHover = useCallback( (state: boolean) => { if (props.onSelectNode) { - const range = props.getRange(props.value); + const range = props.value.range; if (range) { props.onSelectNode(state ? range : null); } @@ -38,23 +31,23 @@ export function ComplexItem( [props.value], ); - useEffect(() => { - const selected = props.selection - ? props.isArray - ? isArrayInRange(props.selection, props.value, props.getRange) - : isInRange(props.selection, props.value, props.getRange) - : false; - - setIsSelected( - props.level !== 'ast' && - selected && - !hasChildInRange(props.selection, model, props.getRange), - ); - - if (selected && !isExpanded) { - setIsExpanded(selected); - } - }, [model, props.selection, props.value, props.isArray, props.getRange]); + // useEffect(() => { + // const selected = props.selection + // ? props.value.type === 'array' + // ? isArrayInRange(props.selection, props.value) + // : isInRange(props.selection, props.value) + // : false; + // + // setIsSelected( + // props.level !== 'ast' && + // selected && + // !hasChildInRange(props.selection, props.value), + // ); + // + // if (selected && !isExpanded) { + // setIsExpanded(selected); + // } + // }, [props.selection, props.value]); return ( setIsExpanded(!isExpanded)} > - {props.isArray ? '[' : '{'} + {props.value.type === 'array' ? '[' : '{'} {isExpanded ? (
- {model.map((item, index) => ( + {props.value.value.map((item, index) => ( ))}
) : ( - + )} - {props.isArray ? ']' : '}'} + {props.value.type === 'array' ? ']' : '}'}
); } -export function SimpleItem(props: GenericParams): JSX.Element { - const [tooltip, setTooltip] = useState(); - - useEffect(() => { - setTooltip(props.getTooltip?.(props.propName ?? '', props.value)); - }, [props.getTooltip, props.propName, props.value]); - - return ( - - {tooltip ? ( - - - - ) : ( - - )} - - ); -} - -export function ElementItem(props: GenericParams): JSX.Element { - const isArray = Array.isArray(props.value); - if (isArray || isRecord(props.value)) { +export function ElementItem({ + level, + getTooltip, + selection, + value, + onSelectNode, +}: GenericParams): JSX.Element { + if (value.type === 'array' || value.type === 'object') { return ( ); } else { - return ; + return ( + + ); } } diff --git a/packages/website/src/components/ast/HiddenItem.tsx b/packages/website/src/components/ast/HiddenItem.tsx index 26717b349567..7a12036747b3 100644 --- a/packages/website/src/components/ast/HiddenItem.tsx +++ b/packages/website/src/components/ast/HiddenItem.tsx @@ -1,9 +1,10 @@ import React, { useEffect, useState } from 'react'; import styles from './ASTViewer.module.css'; import PropertyValue from './PropertyValue'; +import type { ASTViewerModel } from './types'; export interface HiddenItemProps { - readonly value: [string, unknown][]; + readonly value: ASTViewerModel[]; readonly level: string; readonly isArray?: boolean; } @@ -14,9 +15,11 @@ export default function HiddenItem(props: HiddenItemProps): JSX.Element { useEffect(() => { if (props.isArray) { - const filtered = props.value.filter(item => !isNaN(Number(item[0]))); + const filtered = props.value.filter(item => !isNaN(Number(item.key))); setIsComplex( - !filtered.some(item => typeof item[1] !== 'object' || item[1] === null), + filtered.some( + item => typeof item.value === 'object' && item.value !== null, + ), ); setLength(filtered.length); } @@ -28,7 +31,7 @@ export default function HiddenItem(props: HiddenItemProps): JSX.Element { props.value.map((item, index) => ( {index > 0 && ', '} - + )) ) : props.isArray ? ( @@ -39,7 +42,7 @@ export default function HiddenItem(props: HiddenItemProps): JSX.Element { props.value.map((item, index) => ( {index > 0 && ', '} - {String(item[0])} + {String(item.key)} )) )} diff --git a/packages/website/src/components/ast/ItemGroup.tsx b/packages/website/src/components/ast/ItemGroup.tsx index a117c9c05f89..de49d23bf47b 100644 --- a/packages/website/src/components/ast/ItemGroup.tsx +++ b/packages/website/src/components/ast/ItemGroup.tsx @@ -4,15 +4,12 @@ import clsx from 'clsx'; import styles from './ASTViewer.module.css'; -import PropertyNameComp from './PropertyName'; -import type { GetNodeNameFn } from './types'; - -const PropertyName = React.memo(PropertyNameComp); +import PropertyName from './PropertyName'; +import type { ASTViewerModel } from './types'; export interface ItemGroupProps { readonly propName?: string; - readonly value: unknown; - readonly getNodeName: GetNodeNameFn; + readonly value: ASTViewerModel; readonly isSelected?: boolean; readonly isExpanded?: boolean; readonly canExpand?: boolean; @@ -41,7 +38,7 @@ export default function ItemGroup(props: ItemGroupProps): JSX.Element { > props.onHover?.(true)} onMouseLeave={(): void => props.onHover?.(false)} onClick={(props.canExpand && props.onClick) || undefined} diff --git a/packages/website/src/components/ast/PropertyName.tsx b/packages/website/src/components/ast/PropertyName.tsx index 36c3a6341534..e869980f34e1 100644 --- a/packages/website/src/components/ast/PropertyName.tsx +++ b/packages/website/src/components/ast/PropertyName.tsx @@ -13,12 +13,14 @@ export default function PropertyName(props: PropertyNameProps): JSX.Element { return props.onClick ? ( <> {props.propName && ( - // eslint-disable-next-line jsx-a11y/anchor-is-valid { + e.preventDefault(); + props.onClick?.(e); + }} className={styles.propName} > {props.propName} @@ -26,12 +28,14 @@ export default function PropertyName(props: PropertyNameProps): JSX.Element { )} {props.propName && : } {props.typeName && ( - // eslint-disable-next-line jsx-a11y/anchor-is-valid { + e.preventDefault(); + props.onClick?.(e); + }} className={styles.tokenName} > {props.typeName} diff --git a/packages/website/src/components/ast/PropertyValue.tsx b/packages/website/src/components/ast/PropertyValue.tsx index 4d777b3a4bf4..6d5d385552dc 100644 --- a/packages/website/src/components/ast/PropertyValue.tsx +++ b/packages/website/src/components/ast/PropertyValue.tsx @@ -1,32 +1,33 @@ import React from 'react'; import styles from './ASTViewer.module.css'; -import { objType } from './utils'; +import type { ASTViewerModel } from './types'; export interface PropertyValueProps { - readonly value: unknown; + readonly value: ASTViewerModel; } -function PropertyValue(props: PropertyValueProps): JSX.Element { - if (typeof props.value === 'string') { - return ( - {JSON.stringify(props.value)} - ); - } else if (typeof props.value === 'number') { - return {props.value}; - } else if (typeof props.value === 'bigint') { - return {String(props.value)}n; - } else if (props.value instanceof RegExp) { - return {String(props.value)}; - } else if (typeof props.value === 'undefined' || props.value === null) { - return {String(props.value)}; - } else if (typeof props.value === 'boolean') { - return ( - - {props.value ? 'true' : 'false'} - - ); +function PropertyValue({ value }: PropertyValueProps): JSX.Element { + switch (value.type) { + case 'string': + return {value.value}; + case 'bigint': + return {value.value}; + case 'number': + return {value.value}; + case 'regexp': + return {value.value}; + case 'undefined': + return {value.value}; + case 'boolean': + return {value.value}; + case 'array': + case 'object': + return {value.key}; + case 'class': + case 'ref': + default: + return {value.value}; } - return {objType(props.value)}; } export default PropertyValue; diff --git a/packages/website/src/components/ast/SimpleItem.tsx b/packages/website/src/components/ast/SimpleItem.tsx new file mode 100644 index 000000000000..b9a8d1e44f48 --- /dev/null +++ b/packages/website/src/components/ast/SimpleItem.tsx @@ -0,0 +1,34 @@ +import React, { useEffect, useState } from 'react'; +import ItemGroup from './ItemGroup'; +import Tooltip from '@site/src/components/inputs/Tooltip'; +import PropertyValue from './PropertyValue'; + +import type { ASTViewerModelSimple, GetTooltipFn } from './types'; + +export interface SimpleItemProps { + getTooltip?: GetTooltipFn; + value: ASTViewerModelSimple; +} + +export function SimpleItem({ + getTooltip, + value, +}: SimpleItemProps): JSX.Element { + const [tooltip, setTooltip] = useState(); + + useEffect(() => { + setTooltip(getTooltip?.(value.key ?? '', value)); + }, [getTooltip, value]); + + return ( + + {tooltip ? ( + + + + ) : ( + + )} + + ); +} diff --git a/packages/website/src/components/ast/serializer/serializer.ts b/packages/website/src/components/ast/serializer/serializer.ts new file mode 100644 index 000000000000..48e840a79da4 --- /dev/null +++ b/packages/website/src/components/ast/serializer/serializer.ts @@ -0,0 +1,78 @@ +import type { ASTViewerModel, Serializer } from './types'; +import { isRecord, objType } from '../utils'; + +function getSimpleModel(data: unknown): ASTViewerModel { + if (typeof data === 'string') { + return { + value: JSON.stringify(data), + type: 'string', + }; + } else if (typeof data === 'number') { + return { + value: String(data), + type: 'number', + }; + } else if (typeof data === 'bigint') { + return { + value: `${data}n`, + type: 'bigint', + }; + } else if (data instanceof RegExp) { + return { + value: String(data), + type: 'regexp', + }; + } else if (typeof data === 'undefined' || data === null) { + return { + value: String(data), + type: 'undefined', + }; + } else if (typeof data === 'boolean') { + return { + value: data ? 'true' : 'false', + type: 'boolean', + }; + } + return { + value: objType(data), + type: 'class', + }; +} + +export function serialize(data: unknown, mapper?: Serializer): ASTViewerModel { + function processValue(data: [string, unknown][]): ASTViewerModel[] { + return data + .filter(item => !item[0].startsWith('_') && item[1] !== undefined) + .map(item => _serialize(item[1], item[0])); + } + + function _serialize(data: unknown, key?: string): ASTViewerModel { + if (isRecord(data)) { + const mapped = mapper ? mapper(data, key, processValue) : undefined; + if (mapped) { + return { + key, + ...mapped, + }; + } + return { + key, + value: processValue(Object.entries(data)), + type: 'object', + }; + } else if (Array.isArray(data)) { + return { + key, + value: processValue(Object.entries(data)), + type: 'array', + }; + } + + return { + key, + ...getSimpleModel(data), + }; + } + + return _serialize(data); +} diff --git a/packages/website/src/components/ast/serializer/serializerESTree.ts b/packages/website/src/components/ast/serializer/serializerESTree.ts new file mode 100644 index 000000000000..914721afe3e9 --- /dev/null +++ b/packages/website/src/components/ast/serializer/serializerESTree.ts @@ -0,0 +1,34 @@ +import type { ASTViewerModel, Serializer } from './types'; +import { isRecord } from '../utils'; +import type { TSESTree } from '@typescript-eslint/website-eslint'; + +export const propsToFilter = ['parent', 'comments', 'tokens']; + +function isESTreeNode( + value: unknown, +): value is Record & TSESTree.BaseNode { + return isRecord(value) && 'type' in value && 'loc' in value; +} + +export function createESTreeSerializer(): Serializer { + return function serializer( + data, + _key, + processValue, + ): ASTViewerModel | undefined { + if (isESTreeNode(data)) { + return { + range: { + start: data.loc.start, + end: data.loc.end, + }, + type: 'object', + name: String(data.type), + value: processValue( + Object.entries(data).filter(item => !propsToFilter.includes(item[0])), + ), + }; + } + return undefined; + }; +} diff --git a/packages/website/src/components/ast/serializer/serializerScope.ts b/packages/website/src/components/ast/serializer/serializerScope.ts new file mode 100644 index 000000000000..b224ddde7e25 --- /dev/null +++ b/packages/website/src/components/ast/serializer/serializerScope.ts @@ -0,0 +1,93 @@ +import type { ASTViewerModel, Serializer, SelectedRange } from './types'; +import type { TSESTree } from '@typescript-eslint/website-eslint'; +import { isRecord } from '../utils'; + +function isESTreeNode( + value: unknown, +): value is Record & TSESTree.BaseNode { + return isRecord(value) && 'type' in value && 'loc' in value; +} + +export function getClassName(value: Record): string { + // eslint-disable-next-line @typescript-eslint/ban-types + return (Object.getPrototypeOf(value) as Object).constructor.name.replace( + /\$[0-9]+$/, + '', + ); +} + +export function getNodeName(data: Record): string | undefined { + const id = data.$id != null ? `$${String(data.$id)}` : ''; + + const constructorName = getClassName(data); + + if (constructorName === 'ImplicitLibVariable' && data.name === 'const') { + return 'ImplicitGlobalConstTypeVariable'; + } + + return `${constructorName}${id}`; +} + +export function getRange( + value: Record, +): SelectedRange | undefined { + if (isESTreeNode(value.block)) { + return { + start: value.block.loc.start, + end: value.block.loc.end, + }; + } + return undefined; +} + +export const propsToFilter = [ + 'parent', + 'comments', + 'tokens', + 'block', + 'upper', + '$id', +]; + +export function createScopeSerializer(): Serializer { + const SEEN_THINGS = new Set(); + + return function serializer( + data, + _key, + processValue, + ): ASTViewerModel | undefined { + const className = getClassName(data); + if (className !== 'Object') { + const nodeName = getNodeName(data); + + if (SEEN_THINGS.has(nodeName)) { + return { + type: 'ref', + value: `${nodeName}`, + }; + } + SEEN_THINGS.add(nodeName); + + const value = Object.entries(data); + + switch (nodeName) { + case '': + break; + } + + return { + range: getRange(data), + type: 'object', + name: nodeName, + value: processValue(value), + }; + } else { + return { + type: 'object', + value: [], + }; + } + // return undefined; + }; +} diff --git a/packages/website/src/components/ast/serializer/serializerTS.ts b/packages/website/src/components/ast/serializer/serializerTS.ts new file mode 100644 index 000000000000..cbca4df1c41d --- /dev/null +++ b/packages/website/src/components/ast/serializer/serializerTS.ts @@ -0,0 +1,57 @@ +import type { ASTViewerModel, Serializer, SelectedPosition } from './types'; +import type { SourceFile, Node } from 'typescript'; +import { isRecord } from '../utils'; + +export function getLineAndCharacterFor( + pos: number, + ast: SourceFile, +): SelectedPosition { + const loc = ast.getLineAndCharacterOfPosition(pos); + return { + line: loc.line + 1, + column: loc.character, + }; +} + +export const propsToFilter = [ + 'parent', + 'jsDoc', + 'lineMap', + 'externalModuleIndicator', + 'bindDiagnostics', + 'transformFlags', + 'resolvedModules', + 'imports', +]; + +function isTsNode(value: unknown): value is Node { + return isRecord(value) && typeof value.kind === 'number'; +} + +export function createTsSerializer( + root: SourceFile, + syntaxKind: Record, +): Serializer { + return function serializer( + data, + _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])), + ), + }; + } + return undefined; + }; +} diff --git a/packages/website/src/components/ast/serializer/types.ts b/packages/website/src/components/ast/serializer/types.ts new file mode 100644 index 000000000000..eb917d64a404 --- /dev/null +++ b/packages/website/src/components/ast/serializer/types.ts @@ -0,0 +1,9 @@ +import type { ASTViewerModel, SelectedRange, SelectedPosition } from '../types'; + +export type Serializer = ( + data: Record, + key: string | undefined, + processValue: (data: [string, unknown][]) => ASTViewerModel[], +) => ASTViewerModel | undefined; + +export { ASTViewerModel, SelectedRange, SelectedPosition }; diff --git a/packages/website/src/components/ast/types.ts b/packages/website/src/components/ast/types.ts index 8b27865e3428..9f13da812abb 100644 --- a/packages/website/src/components/ast/types.ts +++ b/packages/website/src/components/ast/types.ts @@ -1,12 +1,48 @@ import type { SelectedPosition, SelectedRange } from '../types'; -import { TSESTree } from '@typescript-eslint/website-eslint'; import Monaco from 'monaco-editor'; +export type FilterPropsFnData = Record | unknown[]; + export type GetNodeNameFn = (data: unknown) => string | undefined; export type GetTooltipFn = (key: string, data: unknown) => string | undefined; export type GetRangeFn = (data: unknown) => SelectedRange | undefined; export type OnSelectNodeFn = (node: SelectedRange | null) => void; -export type FilterPropsFn = (item: [string, unknown]) => boolean; +export type FilterPropsFn = (item: FilterPropsFnData) => [string, unknown][]; + +export type ASTViewerModelTypeSimple = + | 'ref' + | 'string' + | 'number' + | 'class' + | 'boolean' + | 'bigint' + | 'regexp' + | 'undefined'; + +export type ASTViewerModelTypeComplex = 'object' | 'array'; + +export type ASTViewerModelType = + | ASTViewerModelTypeComplex + | ASTViewerModelTypeSimple; + +export interface ASTViewerModelBase { + key?: string; + name?: string; + range?: SelectedRange; + tooltip?: string; +} + +export interface ASTViewerModelSimple extends ASTViewerModelBase { + type: ASTViewerModelTypeSimple; + value: string; +} + +export interface ASTViewerModelComplex extends ASTViewerModelBase { + type: ASTViewerModelTypeComplex; + value: ASTViewerModel[]; +} + +export type ASTViewerModel = ASTViewerModelSimple | ASTViewerModelComplex; export interface GenericParams { readonly propName?: string; @@ -14,24 +50,17 @@ export interface GenericParams { readonly level: string; readonly selection?: SelectedPosition | null; readonly onSelectNode?: OnSelectNodeFn; - readonly getNodeName: GetNodeNameFn; readonly getTooltip?: GetTooltipFn; - readonly filterProps: FilterPropsFn; - readonly getRange: GetRangeFn; - readonly isArray?: boolean; } export interface ASTViewerBaseProps { - readonly value: Record | TSESTree.Node | string; readonly position?: Monaco.Position | null; readonly onSelectNode?: OnSelectNodeFn; } export interface ASTViewerProps extends ASTViewerBaseProps { - readonly getNodeName: GetNodeNameFn; readonly getTooltip?: GetTooltipFn; - readonly getRange: GetRangeFn; - readonly filterProps: FilterPropsFn; + readonly value: ASTViewerModel | string; } export type { SelectedPosition, SelectedRange }; diff --git a/packages/website/tsconfig.json b/packages/website/tsconfig.json index a689b00a6f5e..3488d04f2e95 100644 --- a/packages/website/tsconfig.json +++ b/packages/website/tsconfig.json @@ -6,7 +6,7 @@ "allowJs": true, "esModuleInterop": true, "jsx": "react", - "lib": ["DOM"], + "lib": ["DOM", "ESNext"], "noEmit": true, "noImplicitAny": false, "resolveJsonModule": true, From a08607156688c8714416a3a53a7bdf170e3975ba Mon Sep 17 00:00:00 2001 From: Armano Date: Thu, 16 Dec 2021 21:47:39 +0100 Subject: [PATCH 04/13] docs(website): correct minor issues with rendering ast --- .../website/src/components/ASTViewerScope.tsx | 8 +- .../website/src/components/ASTViewerTS.tsx | 36 ++++--- .../website/src/components/ast/Elements.tsx | 48 ++++----- .../website/src/components/ast/HiddenItem.tsx | 6 +- .../website/src/components/ast/SimpleItem.tsx | 2 +- .../ast/serializer/serializerScope.ts | 98 ++++++++++++++----- packages/website/src/components/ast/types.ts | 12 +-- packages/website/src/components/ast/utils.ts | 33 +++---- 8 files changed, 143 insertions(+), 100 deletions(-) diff --git a/packages/website/src/components/ASTViewerScope.tsx b/packages/website/src/components/ASTViewerScope.tsx index 7588584170e1..0b4878527350 100644 --- a/packages/website/src/components/ASTViewerScope.tsx +++ b/packages/website/src/components/ASTViewerScope.tsx @@ -24,11 +24,5 @@ export default function ASTViewerScope( } }, [props.value]); - return ( - - ); + return ; } diff --git a/packages/website/src/components/ASTViewerTS.tsx b/packages/website/src/components/ASTViewerTS.tsx index fb34de3a468f..2ed4547c35e7 100644 --- a/packages/website/src/components/ASTViewerTS.tsx +++ b/packages/website/src/components/ASTViewerTS.tsx @@ -10,6 +10,7 @@ import type { import type { SourceFile } from 'typescript'; import { serialize } from './ast/serializer/serializer'; import { createTsSerializer } from './ast/serializer/serializerTS'; +import { ASTViewerModelSimple } from './ast/types'; export interface ASTTsViewerProps extends ASTViewerBaseProps { readonly version: string; @@ -93,17 +94,30 @@ export default function ASTViewerTS(props: ASTTsViewerProps): JSX.Element { }, [props.value, syntaxKind]); 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: ASTViewerModelSimple): string | undefined => { + if (data.type === 'number') { + switch (data.key) { + case 'flags': + return getFlagNamesFromEnum( + nodeFlags, + Number(data.value), + 'NodeFlags', + ).join('\n'); + case 'numericLiteralFlags': + return getFlagNamesFromEnum( + tokenFlags, + Number(data.value), + 'TokenFlags', + ).join('\n'); + case 'modifierFlagsCache': + return getFlagNamesFromEnum( + modifierFlags, + Number(data.value), + 'ModifierFlags', + ).join('\n'); + case 'kind': + return `SyntaxKind.${syntaxKind[Number(data.value)]}`; + } } return undefined; }, diff --git a/packages/website/src/components/ast/Elements.tsx b/packages/website/src/components/ast/Elements.tsx index 9befc4930a0e..848bb2e95313 100644 --- a/packages/website/src/components/ast/Elements.tsx +++ b/packages/website/src/components/ast/Elements.tsx @@ -1,14 +1,18 @@ -import React, { useCallback, /* useEffect, */ useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; -import type { GenericParams, ASTViewerModelComplex } from './types'; +import type { + GenericParams, + ASTViewerModelComplex, + ASTViewerModel, + ASTViewerModelSimple, +} from './types'; -// import { hasChildInRange, isArrayInRange, isInRange } from './utils'; +import { hasChildInRange, isArrayInRange, isInRange } from './utils'; import styles from './ASTViewer.module.css'; import ItemGroup from './ItemGroup'; import HiddenItem from './HiddenItem'; -import { ASTViewerModel, ASTViewerModelSimple } from './types'; import { SimpleItem } from './SimpleItem'; export function ComplexItem( @@ -17,7 +21,7 @@ export function ComplexItem( const [isExpanded, setIsExpanded] = useState( () => props.level === 'ast', ); - const [isSelected /* setIsSelected */] = useState(false); + const [isSelected, setIsSelected] = useState(false); const onHover = useCallback( (state: boolean) => { @@ -31,23 +35,23 @@ export function ComplexItem( [props.value], ); - // useEffect(() => { - // const selected = props.selection - // ? props.value.type === 'array' - // ? isArrayInRange(props.selection, props.value) - // : isInRange(props.selection, props.value) - // : false; - // - // setIsSelected( - // props.level !== 'ast' && - // selected && - // !hasChildInRange(props.selection, props.value), - // ); - // - // if (selected && !isExpanded) { - // setIsExpanded(selected); - // } - // }, [props.selection, props.value]); + useEffect(() => { + const selected = props.selection + ? props.value.type === 'array' + ? isArrayInRange(props.selection, props.value) + : isInRange(props.selection, props.value) + : false; + + setIsSelected( + props.level !== 'ast' && + selected && + !hasChildInRange(props.selection, props.value), + ); + + if (selected && !isExpanded) { + setIsExpanded(selected); + } + }, [props.selection, props.value]); return ( { if (props.isArray) { const filtered = props.value.filter(item => !isNaN(Number(item.key))); - setIsComplex( - filtered.some( - item => typeof item.value === 'object' && item.value !== null, - ), - ); + setIsComplex(filtered.some(item => item.type !== 'number')); setLength(filtered.length); } }, [props.value, props.isArray]); diff --git a/packages/website/src/components/ast/SimpleItem.tsx b/packages/website/src/components/ast/SimpleItem.tsx index b9a8d1e44f48..9f633b95f1a1 100644 --- a/packages/website/src/components/ast/SimpleItem.tsx +++ b/packages/website/src/components/ast/SimpleItem.tsx @@ -17,7 +17,7 @@ export function SimpleItem({ const [tooltip, setTooltip] = useState(); useEffect(() => { - setTooltip(getTooltip?.(value.key ?? '', value)); + setTooltip(getTooltip?.(value)); }, [getTooltip, value]); return ( diff --git a/packages/website/src/components/ast/serializer/serializerScope.ts b/packages/website/src/components/ast/serializer/serializerScope.ts index b224ddde7e25..ce8b6942cbd0 100644 --- a/packages/website/src/components/ast/serializer/serializerScope.ts +++ b/packages/website/src/components/ast/serializer/serializerScope.ts @@ -4,8 +4,8 @@ import { isRecord } from '../utils'; function isESTreeNode( value: unknown, -): value is Record & TSESTree.BaseNode { - return isRecord(value) && 'type' in value && 'loc' in value; +): value is Record & TSESTree.Node { + return Boolean(value) && isRecord(value) && 'type' in value && 'loc' in value; } export function getClassName(value: Record): string { @@ -19,13 +19,13 @@ export function getClassName(value: Record): string { export function getNodeName(data: Record): string | undefined { const id = data.$id != null ? `$${String(data.$id)}` : ''; - const constructorName = getClassName(data); + let constructorName = getClassName(data); if (constructorName === 'ImplicitLibVariable' && data.name === 'const') { - return 'ImplicitGlobalConstTypeVariable'; + constructorName = 'ImplicitGlobalConstTypeVariable'; } - return `${constructorName}${id}`; + return `${constructorName}${id}${name}`; } export function getRange( @@ -36,18 +36,54 @@ export function getRange( start: value.block.loc.start, end: value.block.loc.end, }; + } else if (isESTreeNode(value.identifier)) { + return { + start: value.identifier.loc.start, + end: value.identifier.loc.end, + }; } return undefined; } -export const propsToFilter = [ - 'parent', - 'comments', - 'tokens', - 'block', - 'upper', - '$id', -]; +export function getProps(nodeName: string | undefined): string[] | undefined { + if (nodeName) { + if (nodeName.endsWith('Scope')) { + return [ + 'block', + 'isStrict', + 'references', + 'set', + 'type', + 'upper', + 'variables', + ]; + } else if (nodeName.endsWith('Definition')) { + return ['name', 'node']; + } else if (nodeName === 'Reference') { + return [ + 'identifier', + 'init', + 'isRead', + 'isTypeReference', + 'isValueReference', + 'isWrite', + 'resolved', + 'writeExpr', + ]; + } else if (nodeName === 'Variable' || nodeName === 'ImplicitLibVariable') { + return [ + 'defs', + 'name', + 'references', + 'isValueVariable', + 'isTypeVariable', + ]; + } else if (nodeName === 'ScopeManager') { + return ['variables', 'scopes']; + } + } + return undefined; +} export function createScopeSerializer(): Serializer { const SEEN_THINGS = new Set(); @@ -63,31 +99,47 @@ export function createScopeSerializer(): Serializer { if (SEEN_THINGS.has(nodeName)) { return { + range: getRange(data), type: 'ref', - value: `${nodeName}`, + name: nodeName, + value: data.name != null ? `<"${String(data.name)}">` : '', }; } SEEN_THINGS.add(nodeName); - const value = Object.entries(data); + let values: [string, unknown][]; - switch (nodeName) { - case '': - break; + const props = getProps(className); + if (props) { + values = props.map(key => [key, data[key]]); + } else { + values = Object.entries(data); } return { range: getRange(data), type: 'object', name: nodeName, - value: processValue(value), + value: processValue(values), }; - } else { + } + + if (isESTreeNode(data)) { + if (data.type === 'Identifier') { + return { + type: 'ref', + name: data.type, + value: `<"${data.name}">`, + }; + } + return { - type: 'object', - value: [], + type: 'ref', + name: data.type, + value: '', }; } - // return undefined; + + return undefined; }; } diff --git a/packages/website/src/components/ast/types.ts b/packages/website/src/components/ast/types.ts index 9f13da812abb..8e8f3cf66efa 100644 --- a/packages/website/src/components/ast/types.ts +++ b/packages/website/src/components/ast/types.ts @@ -1,13 +1,8 @@ import type { SelectedPosition, SelectedRange } from '../types'; import Monaco from 'monaco-editor'; -export type FilterPropsFnData = Record | unknown[]; - -export type GetNodeNameFn = (data: unknown) => string | undefined; -export type GetTooltipFn = (key: string, data: unknown) => string | undefined; -export type GetRangeFn = (data: unknown) => SelectedRange | undefined; +export type GetTooltipFn = (data: ASTViewerModelSimple) => string | undefined; export type OnSelectNodeFn = (node: SelectedRange | null) => void; -export type FilterPropsFn = (item: FilterPropsFnData) => [string, unknown][]; export type ASTViewerModelTypeSimple = | 'ref' @@ -21,15 +16,10 @@ export type ASTViewerModelTypeSimple = export type ASTViewerModelTypeComplex = 'object' | 'array'; -export type ASTViewerModelType = - | ASTViewerModelTypeComplex - | ASTViewerModelTypeSimple; - export interface ASTViewerModelBase { key?: string; name?: string; range?: SelectedRange; - tooltip?: string; } export interface ASTViewerModelSimple extends ASTViewerModelBase { diff --git a/packages/website/src/components/ast/utils.ts b/packages/website/src/components/ast/utils.ts index a06436e9c356..0bc048d37fb9 100644 --- a/packages/website/src/components/ast/utils.ts +++ b/packages/website/src/components/ast/utils.ts @@ -1,5 +1,5 @@ import type { SelectedPosition, SelectedRange } from './types'; -import { GetRangeFn } from './types'; +import { ASTViewerModel, ASTViewerModelComplex } from './types'; export function isWithinRange( loc: SelectedPosition, @@ -30,42 +30,35 @@ export function isRecord(value: unknown): value is Record { export function isInRange( position: SelectedPosition | null | undefined, - value: unknown, - getRange: GetRangeFn, + value: ASTViewerModel, ): boolean { - if (!position) { + if (!position || !value.range) { return false; } - const range = getRange(value); - if (!range) { - return false; - } - return isWithinRange(position, range); + return isWithinRange(position, value.range); } export function isArrayInRange( position: SelectedPosition | null | undefined, - value: unknown, - getRange: GetRangeFn, + value: ASTViewerModelComplex, ): boolean { return Boolean( - position && - Array.isArray(value) && - value.some(item => isInRange(position, item, getRange)), + position && value.value.some(item => isInRange(position, item)), ); } export function hasChildInRange( position: SelectedPosition | null | undefined, - value: [string, unknown][], - getRange: GetRangeFn, + value: ASTViewerModelComplex, ): boolean { return Boolean( position && - value.some( - ([, item]) => - isInRange(position, item, getRange) || - isArrayInRange(position, item, getRange), + value.value.some(item => + item.type === 'object' + ? isInRange(position, item) + : item.type === 'array' + ? isArrayInRange(position, item) + : false, ), ); } From 6014379522cd35eb38528d0131b407ba40748a5b Mon Sep 17 00:00:00 2001 From: Armano Date: Thu, 16 Dec 2021 22:35:38 +0100 Subject: [PATCH 05/13] docs(website): add missing code for selection on tse nodes --- .../website/src/components/ast/Elements.tsx | 1 + .../website/src/components/ast/ItemGroup.tsx | 4 ++-- .../src/components/ast/PropertyName.tsx | 14 +++++------ .../website/src/components/ast/SimpleItem.tsx | 24 +++++++++++++++---- .../ast/serializer/serializerScope.ts | 20 +++++++++++++--- 5 files changed, 47 insertions(+), 16 deletions(-) diff --git a/packages/website/src/components/ast/Elements.tsx b/packages/website/src/components/ast/Elements.tsx index 848bb2e95313..66c239978f0d 100644 --- a/packages/website/src/components/ast/Elements.tsx +++ b/packages/website/src/components/ast/Elements.tsx @@ -112,6 +112,7 @@ export function ElementItem({ ); } diff --git a/packages/website/src/components/ast/ItemGroup.tsx b/packages/website/src/components/ast/ItemGroup.tsx index de49d23bf47b..4504373af3f0 100644 --- a/packages/website/src/components/ast/ItemGroup.tsx +++ b/packages/website/src/components/ast/ItemGroup.tsx @@ -39,8 +39,8 @@ export default function ItemGroup(props: ItemGroupProps): JSX.Element { props.onHover?.(true)} - onMouseLeave={(): void => props.onHover?.(false)} + onMouseEnter={props.onHover} + onMouseLeave={props.onHover} onClick={(props.canExpand && props.onClick) || undefined} /> {React.Children.map(props.children, child => child)} diff --git a/packages/website/src/components/ast/PropertyName.tsx b/packages/website/src/components/ast/PropertyName.tsx index e869980f34e1..58d533abc5c4 100644 --- a/packages/website/src/components/ast/PropertyName.tsx +++ b/packages/website/src/components/ast/PropertyName.tsx @@ -5,18 +5,18 @@ export interface PropertyNameProps { readonly typeName?: string; readonly propName?: string; readonly onClick?: (e: MouseEvent) => void; - readonly onMouseEnter?: (e: MouseEvent) => void; - readonly onMouseLeave?: (e: MouseEvent) => void; + readonly onMouseEnter?: (e: boolean) => void; + readonly onMouseLeave?: (e: boolean) => void; } export default function PropertyName(props: PropertyNameProps): JSX.Element { - return props.onClick ? ( + return props.onClick || props.onMouseEnter || props.onMouseLeave ? ( <> {props.propName && ( props.onMouseEnter?.(true)} + onMouseLeave={(): void => props.onMouseLeave?.(false)} onClick={(e): void => { e.preventDefault(); props.onClick?.(e); @@ -30,8 +30,8 @@ export default function PropertyName(props: PropertyNameProps): JSX.Element { {props.typeName && ( props.onMouseEnter?.(true)} + onMouseLeave={(): void => props.onMouseLeave?.(false)} onClick={(e): void => { e.preventDefault(); props.onClick?.(e); diff --git a/packages/website/src/components/ast/SimpleItem.tsx b/packages/website/src/components/ast/SimpleItem.tsx index 9f633b95f1a1..b9a892f347a5 100644 --- a/packages/website/src/components/ast/SimpleItem.tsx +++ b/packages/website/src/components/ast/SimpleItem.tsx @@ -1,18 +1,21 @@ -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import ItemGroup from './ItemGroup'; import Tooltip from '@site/src/components/inputs/Tooltip'; import PropertyValue from './PropertyValue'; import type { ASTViewerModelSimple, GetTooltipFn } from './types'; +import { OnSelectNodeFn } from './types'; export interface SimpleItemProps { - getTooltip?: GetTooltipFn; - value: ASTViewerModelSimple; + readonly getTooltip?: GetTooltipFn; + readonly value: ASTViewerModelSimple; + readonly onSelectNode?: OnSelectNodeFn; } export function SimpleItem({ getTooltip, value, + onSelectNode, }: SimpleItemProps): JSX.Element { const [tooltip, setTooltip] = useState(); @@ -20,8 +23,21 @@ export function SimpleItem({ setTooltip(getTooltip?.(value)); }, [getTooltip, value]); + const onHover = useCallback( + (state: boolean) => { + if (onSelectNode && value.range) { + onSelectNode(state ? value.range : null); + } + }, + [value], + ); + return ( - + {tooltip ? ( diff --git a/packages/website/src/components/ast/serializer/serializerScope.ts b/packages/website/src/components/ast/serializer/serializerScope.ts index ce8b6942cbd0..5eeaf2a13193 100644 --- a/packages/website/src/components/ast/serializer/serializerScope.ts +++ b/packages/website/src/components/ast/serializer/serializerScope.ts @@ -25,7 +25,7 @@ export function getNodeName(data: Record): string | undefined { constructorName = 'ImplicitGlobalConstTypeVariable'; } - return `${constructorName}${id}${name}`; + return `${constructorName}${id}`; } export function getRange( @@ -38,10 +38,20 @@ export function getRange( }; } else if (isESTreeNode(value.identifier)) { return { - start: value.identifier.loc.start, - end: value.identifier.loc.end, + start: { ...value.identifier.loc.start }, + end: { ...value.identifier.loc.end }, + }; + } else if ( + Array.isArray(value.identifiers) && + value.identifiers.length > 0 && + isESTreeNode(value.identifiers[0]) + ) { + return { + start: { ...value.identifiers[0].loc.start }, + end: { ...value.identifiers[0].loc.end }, }; } + return undefined; } @@ -129,6 +139,10 @@ export function createScopeSerializer(): Serializer { return { type: 'ref', name: data.type, + range: { + start: { ...data.loc.start }, + end: { ...data.loc.end }, + }, value: `<"${data.name}">`, }; } From 2d6eb372096189c292e59d96dacb1b0013e29ac4 Mon Sep 17 00:00:00 2001 From: Armano Date: Fri, 17 Dec 2021 06:28:26 +0100 Subject: [PATCH 06/13] docs(website): optimize ast viewer --- .../src/components/ASTViewerESTree.tsx | 22 +++--- .../website/src/components/ASTViewerScope.tsx | 17 ++--- .../website/src/components/ASTViewerTS.tsx | 71 +++++-------------- .../website/src/components/Playground.tsx | 44 ++++++------ .../website/src/components/ast/ASTViewer.tsx | 25 ++++--- .../website/src/components/ast/Elements.tsx | 63 ++++++++-------- .../website/src/components/ast/HiddenItem.tsx | 24 ++++--- .../website/src/components/ast/ItemGroup.tsx | 32 +++++---- .../src/components/ast/PropertyName.tsx | 41 ++++++----- .../ast/serializer/serializerScope.ts | 41 ++++++----- 10 files changed, 189 insertions(+), 191 deletions(-) diff --git a/packages/website/src/components/ASTViewerESTree.tsx b/packages/website/src/components/ASTViewerESTree.tsx index 7bd2e1be3c69..0af598dcb321 100644 --- a/packages/website/src/components/ASTViewerESTree.tsx +++ b/packages/website/src/components/ASTViewerESTree.tsx @@ -10,25 +10,23 @@ export interface ASTESTreeViewerProps extends ASTViewerBaseProps { readonly value: TSESTree.BaseNode | string; } -export default function ASTViewerESTree( - props: ASTESTreeViewerProps, -): JSX.Element { +export default function ASTViewerESTree({ + value, + position, + onSelectNode, +}: ASTESTreeViewerProps): JSX.Element { const [model, setModel] = useState(''); useEffect(() => { - if (typeof props.value === 'string') { - setModel(props.value); + if (typeof value === 'string') { + setModel(value); } else { const scopeSerializer = createESTreeSerializer(); - setModel(serialize(props.value, scopeSerializer)); + setModel(serialize(value, scopeSerializer)); } - }, [props.value]); + }, [value]); return ( - + ); } diff --git a/packages/website/src/components/ASTViewerScope.tsx b/packages/website/src/components/ASTViewerScope.tsx index 0b4878527350..e2a76c5f3c0d 100644 --- a/packages/website/src/components/ASTViewerScope.tsx +++ b/packages/website/src/components/ASTViewerScope.tsx @@ -10,19 +10,20 @@ export interface ASTScopeViewerProps extends ASTViewerBaseProps { readonly value: Record | string; } -export default function ASTViewerScope( - props: ASTScopeViewerProps, -): JSX.Element { +export default function ASTViewerScope({ + value, + onSelectNode, +}: ASTScopeViewerProps): JSX.Element { const [model, setModel] = useState(''); useEffect(() => { - if (typeof props.value === 'string') { - setModel(props.value); + if (typeof value === 'string') { + setModel(value); } else { const scopeSerializer = createScopeSerializer(); - setModel(serialize(props.value, scopeSerializer)); + setModel(serialize(value, scopeSerializer)); } - }, [props.value]); + }, [value]); - return ; + return ; } diff --git a/packages/website/src/components/ASTViewerTS.tsx b/packages/website/src/components/ASTViewerTS.tsx index 2ed4547c35e7..ac66f5d10557 100644 --- a/packages/website/src/components/ASTViewerTS.tsx +++ b/packages/website/src/components/ASTViewerTS.tsx @@ -4,17 +4,14 @@ import ASTViewer from './ast/ASTViewer'; import type { ASTViewerBaseProps, ASTViewerModel, - SelectedRange, - SelectedPosition, + ASTViewerModelSimple, } from './ast/types'; import type { SourceFile } from 'typescript'; import { serialize } from './ast/serializer/serializer'; import { createTsSerializer } from './ast/serializer/serializerTS'; -import { ASTViewerModelSimple } from './ast/types'; export interface ASTTsViewerProps extends ASTViewerBaseProps { - readonly version: string; - readonly value: Record | string; + readonly value: SourceFile | string; } function extractEnum( @@ -42,57 +39,27 @@ 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 default function ASTViewerTS(props: ASTTsViewerProps): JSX.Element { +export default function ASTViewerTS({ + value, + position, + onSelectNode, +}: ASTTsViewerProps): JSX.Element { const [model, setModel] = useState(''); - const [syntaxKind, setSyntaxKind] = useState>({}); - const [nodeFlags, setNodeFlags] = useState>({}); - const [tokenFlags, setTokenFlags] = useState>({}); - const [modifierFlags, setModifierFlags] = useState>( - {}, - ); - - useEffect(() => { - setSyntaxKind(extractEnum(window.ts.SyntaxKind)); - setNodeFlags(extractEnum(window.ts.NodeFlags)); - setTokenFlags(extractEnum(window.ts.TokenFlags)); - setModifierFlags(extractEnum(window.ts.ModifierFlags)); - }, [props.version]); + 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(() => { - if (typeof props.value === 'string') { - setModel(props.value); + if (typeof value === 'string') { + setModel(value); } else { - const scopeSerializer = createTsSerializer( - // @ts-expect-error: unsafe cast - props.value as SourceFile, - syntaxKind, - ); - setModel(serialize(props.value, scopeSerializer)); + const scopeSerializer = createTsSerializer(value, syntaxKind); + setModel(serialize(value, scopeSerializer)); } - }, [props.value, syntaxKind]); + }, [value, syntaxKind]); + // TODO: move this to serializer const getTooltip = useCallback( (data: ASTViewerModelSimple): string | undefined => { if (data.type === 'number') { @@ -127,8 +94,8 @@ export default function ASTViewerTS(props: ASTTsViewerProps): JSX.Element { return ( ); diff --git a/packages/website/src/components/Playground.tsx b/packages/website/src/components/Playground.tsx index 8e2971a9c5c6..fd6d66c480c4 100644 --- a/packages/website/src/components/Playground.tsx +++ b/packages/website/src/components/Playground.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from 'react'; +import React, { useReducer, useState } from 'react'; import type Monaco from 'monaco-editor'; import clsx from 'clsx'; import useThemeContext from '@theme/hooks/useThemeContext'; @@ -20,6 +20,23 @@ import type { RuleDetails, SelectedRange } from './types'; import type { TSESTree } from '@typescript-eslint/website-eslint'; import ASTViewerScope from '@site/src/components/ASTViewerScope'; +function rangeReducer( + prevState: T, + action: T, +): T { + if (prevState !== action) { + if ( + !prevState || + !action || + !shallowEqual(prevState.start, action.start) || + !shallowEqual(prevState.end, action.end) + ) { + return action; + } + } + return prevState; +} + function Playground(): JSX.Element { const [state, setState] = useHashState({ jsx: false, @@ -37,25 +54,9 @@ function Playground(): JSX.Element { const [ruleNames, setRuleNames] = useState([]); const [isLoading, setIsLoading] = useState(true); const [tsVersions, setTSVersion] = useState([]); - const [selectedRange, setSelectedRange] = useState( - null, - ); + const [selectedRange, setSelectedRange] = useReducer(rangeReducer, null); const [position, setPosition] = useState(null); - const updateSelectedNode = useCallback( - (value: SelectedRange | null) => { - if ( - !value || - !selectedRange || - !shallowEqual(selectedRange.start, value.start) || - !shallowEqual(selectedRange.end, value.end) - ) { - setSelectedRange(value); - } - }, - [selectedRange], - ); - return (
@@ -104,22 +105,21 @@ function Playground(): JSX.Element { )) || (state.showAST === 'scope' && scope && ( )) || (esAst && ( ))}
diff --git a/packages/website/src/components/ast/ASTViewer.tsx b/packages/website/src/components/ast/ASTViewer.tsx index 46823400dbde..f4892897947a 100644 --- a/packages/website/src/components/ast/ASTViewer.tsx +++ b/packages/website/src/components/ast/ASTViewer.tsx @@ -5,30 +5,35 @@ import type { SelectedPosition, ASTViewerProps } from './types'; import { ElementItem } from './Elements'; -function ASTViewer(props: ASTViewerProps): JSX.Element { +function ASTViewer({ + position, + value, + getTooltip, + onSelectNode, +}: ASTViewerProps): JSX.Element { const [selection, setSelection] = useState(null); useEffect(() => { setSelection( - props.position + position ? { - line: props.position.lineNumber, - column: props.position.column - 1, + line: position.lineNumber, + column: position.column - 1, } : null, ); - }, [props.position]); + }, [position]); - return typeof props.value === 'string' ? ( -
{props.value}
+ return typeof value === 'string' ? ( +
{value}
) : (
); diff --git a/packages/website/src/components/ast/Elements.tsx b/packages/website/src/components/ast/Elements.tsx index 66c239978f0d..e08a8ee689af 100644 --- a/packages/website/src/components/ast/Elements.tsx +++ b/packages/website/src/components/ast/Elements.tsx @@ -15,76 +15,77 @@ import ItemGroup from './ItemGroup'; import HiddenItem from './HiddenItem'; import { SimpleItem } from './SimpleItem'; -export function ComplexItem( - props: GenericParams, -): JSX.Element { - const [isExpanded, setIsExpanded] = useState( - () => props.level === 'ast', - ); +export function ComplexItem({ + value, + onSelectNode, + level, + selection, + propName, + getTooltip, +}: GenericParams): JSX.Element { + const [isExpanded, setIsExpanded] = useState(() => level === 'ast'); const [isSelected, setIsSelected] = useState(false); const onHover = useCallback( (state: boolean) => { - if (props.onSelectNode) { - const range = props.value.range; + if (onSelectNode) { + const range = value.range; if (range) { - props.onSelectNode(state ? range : null); + onSelectNode(state ? range : null); } } }, - [props.value], + [value], ); useEffect(() => { - const selected = props.selection - ? props.value.type === 'array' - ? isArrayInRange(props.selection, props.value) - : isInRange(props.selection, props.value) + const selected = selection + ? value.type === 'array' + ? isArrayInRange(selection, value) + : isInRange(selection, value) : false; setIsSelected( - props.level !== 'ast' && - selected && - !hasChildInRange(props.selection, props.value), + level !== 'ast' && selected && !hasChildInRange(selection, value), ); if (selected && !isExpanded) { setIsExpanded(selected); } - }, [props.selection, props.value]); + }, [selection, value]); return ( setIsExpanded(!isExpanded)} > - {props.value.type === 'array' ? '[' : '{'} + {value.type === 'array' ? '[' : '{'} {isExpanded ? (
- {props.value.value.map((item, index) => ( + {value.value.map((item, index) => ( ))}
) : ( )} - {props.value.type === 'array' ? ']' : '}'} + {value.type === 'array' ? ']' : '}'}
); } diff --git a/packages/website/src/components/ast/HiddenItem.tsx b/packages/website/src/components/ast/HiddenItem.tsx index 0d16dbd43411..fb6ca1fc8c72 100644 --- a/packages/website/src/components/ast/HiddenItem.tsx +++ b/packages/website/src/components/ast/HiddenItem.tsx @@ -9,34 +9,38 @@ export interface HiddenItemProps { readonly isArray?: boolean; } -export default function HiddenItem(props: HiddenItemProps): JSX.Element { +export default function HiddenItem({ + value, + level, + isArray, +}: HiddenItemProps): JSX.Element { const [isComplex, setIsComplex] = useState(true); const [length, setLength] = useState(0); useEffect(() => { - if (props.isArray) { - const filtered = props.value.filter(item => !isNaN(Number(item.key))); + if (isArray) { + const filtered = value.filter(item => !isNaN(Number(item.key))); setIsComplex(filtered.some(item => item.type !== 'number')); setLength(filtered.length); } - }, [props.value, props.isArray]); + }, [value, isArray]); return ( - {props.isArray && !isComplex ? ( - props.value.map((item, index) => ( - + {isArray && !isComplex ? ( + value.map((item, index) => ( + {index > 0 && ', '} )) - ) : props.isArray ? ( + ) : isArray ? ( <> {length} {length === 1 ? 'element' : 'elements'} ) : ( - props.value.map((item, index) => ( - + value.map((item, index) => ( + {index > 0 && ', '} {String(item.key)} diff --git a/packages/website/src/components/ast/ItemGroup.tsx b/packages/website/src/components/ast/ItemGroup.tsx index 4504373af3f0..cb2d1fd46b81 100644 --- a/packages/website/src/components/ast/ItemGroup.tsx +++ b/packages/website/src/components/ast/ItemGroup.tsx @@ -18,32 +18,40 @@ export interface ItemGroupProps { readonly children: JSX.Element | false | (JSX.Element | false)[]; } -export default function ItemGroup(props: ItemGroupProps): JSX.Element { +export default function ItemGroup({ + propName, + value, + isSelected, + isExpanded, + canExpand, + onClick, + onHover, + children, +}: ItemGroupProps): JSX.Element { const listItem = useRef(null); useEffect(() => { - if (listItem.current && props.isSelected) { + if (listItem.current && isSelected) { scrollIntoViewIfNeeded(listItem.current); } - }, [props.isSelected, listItem]); + }, [isSelected, listItem]); return (
- {React.Children.map(props.children, child => child)} + {React.Children.map(children, child => child)}
); } diff --git a/packages/website/src/components/ast/PropertyName.tsx b/packages/website/src/components/ast/PropertyName.tsx index 58d533abc5c4..8ff4b7c62406 100644 --- a/packages/website/src/components/ast/PropertyName.tsx +++ b/packages/website/src/components/ast/PropertyName.tsx @@ -1,26 +1,38 @@ -import React, { MouseEvent } from 'react'; +import React, { MouseEvent, useCallback } from 'react'; import styles from './ASTViewer.module.css'; export interface PropertyNameProps { readonly typeName?: string; readonly propName?: string; readonly onClick?: (e: MouseEvent) => void; - readonly onMouseEnter?: (e: boolean) => void; - readonly onMouseLeave?: (e: boolean) => void; + readonly onHover?: (e: boolean) => void; } export default function PropertyName(props: PropertyNameProps): JSX.Element { - return props.onClick || props.onMouseEnter || props.onMouseLeave ? ( + const onClick = useCallback( + (e: MouseEvent) => { + e.preventDefault(); + props.onClick?.(e); + }, + [props.onClick], + ); + + const onMouseEnter = useCallback(() => { + props.onHover?.(true); + }, [props.onHover]); + + const onMouseLeave = useCallback(() => { + props.onHover?.(false); + }, [props.onHover]); + + return props.onClick || props.onHover ? ( <> {props.propName && (
props.onMouseEnter?.(true)} - onMouseLeave={(): void => props.onMouseLeave?.(false)} - onClick={(e): void => { - e.preventDefault(); - props.onClick?.(e); - }} + onMouseEnter={onMouseEnter} + onMouseLeave={onMouseLeave} + onClick={onClick} className={styles.propName} > {props.propName} @@ -30,12 +42,9 @@ export default function PropertyName(props: PropertyNameProps): JSX.Element { {props.typeName && ( props.onMouseEnter?.(true)} - onMouseLeave={(): void => props.onMouseLeave?.(false)} - onClick={(e): void => { - e.preventDefault(); - props.onClick?.(e); - }} + onMouseEnter={onMouseEnter} + onMouseLeave={onMouseLeave} + onClick={onClick} className={styles.tokenName} > {props.typeName} diff --git a/packages/website/src/components/ast/serializer/serializerScope.ts b/packages/website/src/components/ast/serializer/serializerScope.ts index 5eeaf2a13193..06cfaa972790 100644 --- a/packages/website/src/components/ast/serializer/serializerScope.ts +++ b/packages/website/src/components/ast/serializer/serializerScope.ts @@ -41,6 +41,11 @@ export function getRange( start: { ...value.identifier.loc.start }, end: { ...value.identifier.loc.end }, }; + } else if (isESTreeNode(value.node)) { + return { + start: { ...value.node.loc.start }, + end: { ...value.node.loc.end }, + }; } else if ( Array.isArray(value.identifiers) && value.identifiers.length > 0 && @@ -66,9 +71,11 @@ export function getProps(nodeName: string | undefined): string[] | undefined { 'type', 'upper', 'variables', + 'variableScope', + 'functionExpressionScope', ]; } else if (nodeName.endsWith('Definition')) { - return ['name', 'node']; + return ['name', 'type', 'node']; } else if (nodeName === 'Reference') { return [ 'identifier', @@ -87,9 +94,11 @@ export function getProps(nodeName: string | undefined): string[] | undefined { 'references', 'isValueVariable', 'isTypeVariable', + 'eslintUsed', + 'identifiers', ]; } else if (nodeName === 'ScopeManager') { - return ['variables', 'scopes']; + return ['variables', 'scopes', 'references']; } } return undefined; @@ -104,18 +113,22 @@ export function createScopeSerializer(): Serializer { processValue, ): ASTViewerModel | undefined { const className = getClassName(data); + if (className !== 'Object') { const nodeName = getNodeName(data); + const value = data.name != null ? `<"${String(data.name)}">` : ''; - if (SEEN_THINGS.has(nodeName)) { + const uniqName = `${nodeName}${value}`; + + if (SEEN_THINGS.has(uniqName)) { return { range: getRange(data), type: 'ref', name: nodeName, - value: data.name != null ? `<"${String(data.name)}">` : '', + value: value, }; } - SEEN_THINGS.add(nodeName); + SEEN_THINGS.add(uniqName); let values: [string, unknown][]; @@ -135,22 +148,14 @@ export function createScopeSerializer(): Serializer { } if (isESTreeNode(data)) { - if (data.type === 'Identifier') { - return { - type: 'ref', - name: data.type, - range: { - start: { ...data.loc.start }, - end: { ...data.loc.end }, - }, - value: `<"${data.name}">`, - }; - } - return { type: 'ref', name: data.type, - value: '', + range: { + start: { ...data.loc.start }, + end: { ...data.loc.end }, + }, + value: data.type === 'Identifier' ? `<"${data.name}">` : '', }; } From 078867317707bf8d0549cc0c6dfdffb6a5dfc2af Mon Sep 17 00:00:00 2001 From: Armano Date: Mon, 20 Dec 2021 05:49:00 +0100 Subject: [PATCH 07/13] docs(website): minor adjustments to serializer --- packages/website-eslint/types/index.d.ts | 12 ++-- .../website/src/components/Playground.tsx | 3 +- .../ast/serializer/serializerScope.ts | 63 +++++++++++++------ .../website/src/components/editor/types.ts | 3 +- 4 files changed, 52 insertions(+), 29 deletions(-) diff --git a/packages/website-eslint/types/index.d.ts b/packages/website-eslint/types/index.d.ts index 582e5b0860fa..4e923c2fef1c 100644 --- a/packages/website-eslint/types/index.d.ts +++ b/packages/website-eslint/types/index.d.ts @@ -1,18 +1,17 @@ -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; + getAst(): TSESTree.Program; + getTsAst(): SourceFile; getScope(): Record; lint( @@ -26,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'; diff --git a/packages/website/src/components/Playground.tsx b/packages/website/src/components/Playground.tsx index fd6d66c480c4..b766bd9a9746 100644 --- a/packages/website/src/components/Playground.tsx +++ b/packages/website/src/components/Playground.tsx @@ -18,6 +18,7 @@ import ASTViewerTS from './ASTViewerTS'; import type { RuleDetails, SelectedRange } from './types'; import type { TSESTree } from '@typescript-eslint/website-eslint'; +import type { SourceFile } from 'typescript'; import ASTViewerScope from '@site/src/components/ASTViewerScope'; function rangeReducer( @@ -49,7 +50,7 @@ function Playground(): JSX.Element { }); const { isDarkTheme } = useThemeContext(); const [esAst, setEsAst] = useState(); - const [tsAst, setTsAST] = useState | string | null>(); + const [tsAst, setTsAST] = useState(); const [scope, setScope] = useState | string | null>(); const [ruleNames, setRuleNames] = useState([]); const [isLoading, setIsLoading] = useState(true); diff --git a/packages/website/src/components/ast/serializer/serializerScope.ts b/packages/website/src/components/ast/serializer/serializerScope.ts index 06cfaa972790..42b212c69299 100644 --- a/packages/website/src/components/ast/serializer/serializerScope.ts +++ b/packages/website/src/components/ast/serializer/serializerScope.ts @@ -8,7 +8,7 @@ function isESTreeNode( return Boolean(value) && isRecord(value) && 'type' in value && 'loc' in value; } -export function getClassName(value: Record): string { +function getClassName(value: Record): string { // eslint-disable-next-line @typescript-eslint/ban-types return (Object.getPrototypeOf(value) as Object).constructor.name.replace( /\$[0-9]+$/, @@ -16,21 +16,20 @@ export function getClassName(value: Record): string { ); } -export function getNodeName(data: Record): string | undefined { +function getNodeName( + className: string, + data: Record, +): string | undefined { const id = data.$id != null ? `$${String(data.$id)}` : ''; - let constructorName = getClassName(data); - - if (constructorName === 'ImplicitLibVariable' && data.name === 'const') { - constructorName = 'ImplicitGlobalConstTypeVariable'; + if (className === 'ImplicitLibVariable' && data.name === 'const') { + className = 'ImplicitGlobalConstTypeVariable'; } - return `${constructorName}${id}`; + return `${className}${id}`; } -export function getRange( - value: Record, -): SelectedRange | undefined { +function getRange(value: Record): SelectedRange | undefined { if (isESTreeNode(value.block)) { return { start: value.block.loc.start, @@ -60,9 +59,35 @@ export function getRange( return undefined; } -export function getProps(nodeName: string | undefined): string[] | undefined { +type NodeType = + | 'Scope' + | 'Definition' + | 'Variable' + | 'ScopeManager' + | 'Reference'; + +function getNodeType(nodeName: string | undefined): NodeType | undefined { if (nodeName) { - if (nodeName.endsWith('Scope')) { + if (nodeName === 'ScopeManager') { + return 'ScopeManager'; + } else if (nodeName.endsWith('Scope')) { + return 'Scope'; + } else if (nodeName.endsWith('Definition')) { + return 'Definition'; + } else if (nodeName === 'Variable' || nodeName === 'ImplicitLibVariable') { + return 'Variable'; + } else if (nodeName === 'Reference') { + return 'Reference'; + } + } + return undefined; +} + +function getProps(nodeType: NodeType | undefined): string[] | undefined { + switch (nodeType) { + case 'ScopeManager': + return ['variables', 'scopes', 'references']; + case 'Scope': return [ 'block', 'isStrict', @@ -74,9 +99,9 @@ export function getProps(nodeName: string | undefined): string[] | undefined { 'variableScope', 'functionExpressionScope', ]; - } else if (nodeName.endsWith('Definition')) { + case 'Definition': return ['name', 'type', 'node']; - } else if (nodeName === 'Reference') { + case 'Reference': return [ 'identifier', 'init', @@ -87,7 +112,7 @@ export function getProps(nodeName: string | undefined): string[] | undefined { 'resolved', 'writeExpr', ]; - } else if (nodeName === 'Variable' || nodeName === 'ImplicitLibVariable') { + case 'Variable': return [ 'defs', 'name', @@ -97,9 +122,6 @@ export function getProps(nodeName: string | undefined): string[] | undefined { 'eslintUsed', 'identifiers', ]; - } else if (nodeName === 'ScopeManager') { - return ['variables', 'scopes', 'references']; - } } return undefined; } @@ -115,7 +137,8 @@ export function createScopeSerializer(): Serializer { const className = getClassName(data); if (className !== 'Object') { - const nodeName = getNodeName(data); + const nodeName = getNodeName(className, data); + const nodeType = getNodeType(className); const value = data.name != null ? `<"${String(data.name)}">` : ''; const uniqName = `${nodeName}${value}`; @@ -132,7 +155,7 @@ export function createScopeSerializer(): Serializer { let values: [string, unknown][]; - const props = getProps(className); + const props = getProps(nodeType); if (props) { values = props.map(key => [key, data[key]]); } else { diff --git a/packages/website/src/components/editor/types.ts b/packages/website/src/components/editor/types.ts index e0e099c0fe86..edcbcf842d38 100644 --- a/packages/website/src/components/editor/types.ts +++ b/packages/website/src/components/editor/types.ts @@ -1,12 +1,13 @@ import type Monaco from 'monaco-editor'; import type { ConfigModel, SelectedRange } from '../types'; import type { TSESTree } from '@typescript-eslint/website-eslint'; +import type { SourceFile } from 'typescript'; export interface CommonEditorProps extends ConfigModel { readonly darkTheme: boolean; readonly decoration: SelectedRange | null; readonly onChange: (value: string) => void; - readonly onTsASTChange: (value: string | Record) => void; + readonly onTsASTChange: (value: string | SourceFile) => void; readonly onEsASTChange: (value: string | TSESTree.Program) => void; readonly onScopeChange: (value: string | Record) => void; readonly onSelect: (position: Monaco.Position | null) => void; From a3390dc05425c0269446ec1a9e1a07aa43781dbe Mon Sep 17 00:00:00 2001 From: Armano Date: Wed, 22 Dec 2021 11:58:08 +0100 Subject: [PATCH 08/13] docs(website): add guard against infinite loops --- .../components/ast/serializer/serializer.ts | 15 ++++--- .../ast/serializer/serializerESTree.ts | 2 +- .../ast/serializer/serializerScope.ts | 39 +++++++++++-------- .../components/ast/serializer/serializerTS.ts | 2 +- .../src/components/ast/serializer/types.ts | 9 ----- packages/website/src/components/ast/types.ts | 6 +++ 6 files changed, 41 insertions(+), 32 deletions(-) delete mode 100644 packages/website/src/components/ast/serializer/types.ts diff --git a/packages/website/src/components/ast/serializer/serializer.ts b/packages/website/src/components/ast/serializer/serializer.ts index 48e840a79da4..153c84f5fc6d 100644 --- a/packages/website/src/components/ast/serializer/serializer.ts +++ b/packages/website/src/components/ast/serializer/serializer.ts @@ -1,4 +1,4 @@ -import type { ASTViewerModel, Serializer } from './types'; +import type { ASTViewerModel, Serializer } from '../types'; import { isRecord, objType } from '../utils'; function getSimpleModel(data: unknown): ASTViewerModel { @@ -39,7 +39,10 @@ function getSimpleModel(data: unknown): ASTViewerModel { }; } -export function serialize(data: unknown, mapper?: Serializer): ASTViewerModel { +export function serialize( + data: unknown, + serializer?: Serializer, +): ASTViewerModel { function processValue(data: [string, unknown][]): ASTViewerModel[] { return data .filter(item => !item[0].startsWith('_') && item[1] !== undefined) @@ -48,11 +51,13 @@ export function serialize(data: unknown, mapper?: Serializer): ASTViewerModel { function _serialize(data: unknown, key?: string): ASTViewerModel { if (isRecord(data)) { - const mapped = mapper ? mapper(data, key, processValue) : undefined; - if (mapped) { + const serialized = serializer + ? serializer(data, key, processValue) + : undefined; + if (serialized) { return { key, - ...mapped, + ...serialized, }; } return { diff --git a/packages/website/src/components/ast/serializer/serializerESTree.ts b/packages/website/src/components/ast/serializer/serializerESTree.ts index 914721afe3e9..7f467757c8a3 100644 --- a/packages/website/src/components/ast/serializer/serializerESTree.ts +++ b/packages/website/src/components/ast/serializer/serializerESTree.ts @@ -1,4 +1,4 @@ -import type { ASTViewerModel, Serializer } from './types'; +import type { ASTViewerModel, Serializer } from '../types'; import { isRecord } from '../utils'; import type { TSESTree } from '@typescript-eslint/website-eslint'; diff --git a/packages/website/src/components/ast/serializer/serializerScope.ts b/packages/website/src/components/ast/serializer/serializerScope.ts index 42b212c69299..260be9889d38 100644 --- a/packages/website/src/components/ast/serializer/serializerScope.ts +++ b/packages/website/src/components/ast/serializer/serializerScope.ts @@ -1,4 +1,4 @@ -import type { ASTViewerModel, Serializer, SelectedRange } from './types'; +import type { ASTViewerModel, Serializer, SelectedRange } from '../types'; import type { TSESTree } from '@typescript-eslint/website-eslint'; import { isRecord } from '../utils'; @@ -86,7 +86,7 @@ function getNodeType(nodeName: string | undefined): NodeType | undefined { function getProps(nodeType: NodeType | undefined): string[] | undefined { switch (nodeType) { case 'ScopeManager': - return ['variables', 'scopes', 'references']; + return ['scopes', 'variables', 'references']; case 'Scope': return [ 'block', @@ -127,7 +127,7 @@ function getProps(nodeType: NodeType | undefined): string[] | undefined { } export function createScopeSerializer(): Serializer { - const SEEN_THINGS = new Set(); + const SEEN_THINGS = new Map(); return function serializer( data, @@ -144,14 +144,25 @@ export function createScopeSerializer(): Serializer { const uniqName = `${nodeName}${value}`; if (SEEN_THINGS.has(uniqName)) { - return { - range: getRange(data), - type: 'ref', - name: nodeName, - value: value, - }; + const found = SEEN_THINGS.get(uniqName); + if (found && Array.isArray(found.value) && found.value.length === 0) { + return { + range: found.range, + type: 'ref', + name: found.name, + value: '', + }; + } + return found; } - SEEN_THINGS.add(uniqName); + + const result: ASTViewerModel = { + range: getRange(data), + type: 'object', + name: nodeName, + value: [], + }; + SEEN_THINGS.set(uniqName, result); let values: [string, unknown][]; @@ -162,12 +173,8 @@ export function createScopeSerializer(): Serializer { values = Object.entries(data); } - return { - range: getRange(data), - type: 'object', - name: nodeName, - value: processValue(values), - }; + result.value = processValue(values); + return result; } if (isESTreeNode(data)) { diff --git a/packages/website/src/components/ast/serializer/serializerTS.ts b/packages/website/src/components/ast/serializer/serializerTS.ts index cbca4df1c41d..e1a86f054a75 100644 --- a/packages/website/src/components/ast/serializer/serializerTS.ts +++ b/packages/website/src/components/ast/serializer/serializerTS.ts @@ -1,4 +1,4 @@ -import type { ASTViewerModel, Serializer, SelectedPosition } from './types'; +import type { ASTViewerModel, Serializer, SelectedPosition } from '../types'; import type { SourceFile, Node } from 'typescript'; import { isRecord } from '../utils'; diff --git a/packages/website/src/components/ast/serializer/types.ts b/packages/website/src/components/ast/serializer/types.ts deleted file mode 100644 index eb917d64a404..000000000000 --- a/packages/website/src/components/ast/serializer/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { ASTViewerModel, SelectedRange, SelectedPosition } from '../types'; - -export type Serializer = ( - data: Record, - key: string | undefined, - processValue: (data: [string, unknown][]) => ASTViewerModel[], -) => ASTViewerModel | undefined; - -export { ASTViewerModel, SelectedRange, SelectedPosition }; diff --git a/packages/website/src/components/ast/types.ts b/packages/website/src/components/ast/types.ts index 8e8f3cf66efa..328900ca9304 100644 --- a/packages/website/src/components/ast/types.ts +++ b/packages/website/src/components/ast/types.ts @@ -53,4 +53,10 @@ export interface ASTViewerProps extends ASTViewerBaseProps { readonly value: ASTViewerModel | string; } +export type Serializer = ( + data: Record, + key: string | undefined, + processValue: (data: [string, unknown][]) => ASTViewerModel[], +) => ASTViewerModel | undefined; + export type { SelectedPosition, SelectedRange }; From 325a0d63210a287cb07facbb29d42f9f62708075 Mon Sep 17 00:00:00 2001 From: Armano Date: Wed, 22 Dec 2021 13:05:17 +0100 Subject: [PATCH 09/13] docs(website): enable printing circular tree --- .../src/components/ASTViewerESTree.tsx | 4 +- .../website/src/components/ASTViewerScope.tsx | 4 +- .../website/src/components/ASTViewerTS.tsx | 20 +++----- .../website/src/components/ast/ASTViewer.tsx | 2 +- .../website/src/components/ast/Elements.tsx | 51 +++++++++---------- .../website/src/components/ast/HiddenItem.tsx | 6 +-- .../website/src/components/ast/ItemGroup.tsx | 12 ++--- .../src/components/ast/PropertyValue.tsx | 20 ++++---- .../website/src/components/ast/SimpleItem.tsx | 31 ++++++----- .../components/ast/serializer/serializer.ts | 37 +++++++------- .../ast/serializer/serializerScope.ts | 11 +--- packages/website/src/components/ast/types.ts | 24 ++++++--- packages/website/src/components/ast/utils.ts | 10 ++-- 13 files changed, 114 insertions(+), 118 deletions(-) diff --git a/packages/website/src/components/ASTViewerESTree.tsx b/packages/website/src/components/ASTViewerESTree.tsx index 0af598dcb321..25286af0a3ce 100644 --- a/packages/website/src/components/ASTViewerESTree.tsx +++ b/packages/website/src/components/ASTViewerESTree.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import ASTViewer from './ast/ASTViewer'; -import type { ASTViewerBaseProps, ASTViewerModel } from './ast/types'; +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'; @@ -15,7 +15,7 @@ export default function ASTViewerESTree({ position, onSelectNode, }: ASTESTreeViewerProps): JSX.Element { - const [model, setModel] = useState(''); + const [model, setModel] = useState(''); useEffect(() => { if (typeof value === 'string') { diff --git a/packages/website/src/components/ASTViewerScope.tsx b/packages/website/src/components/ASTViewerScope.tsx index e2a76c5f3c0d..b14514855381 100644 --- a/packages/website/src/components/ASTViewerScope.tsx +++ b/packages/website/src/components/ASTViewerScope.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import ASTViewer from './ast/ASTViewer'; -import type { ASTViewerBaseProps, ASTViewerModel } from './ast/types'; +import type { ASTViewerBaseProps, ASTViewerModelMap } from './ast/types'; import { serialize } from './ast/serializer/serializer'; import { createScopeSerializer } from './ast/serializer/serializerScope'; @@ -14,7 +14,7 @@ export default function ASTViewerScope({ value, onSelectNode, }: ASTScopeViewerProps): JSX.Element { - const [model, setModel] = useState(''); + const [model, setModel] = useState(''); useEffect(() => { if (typeof value === 'string') { diff --git a/packages/website/src/components/ASTViewerTS.tsx b/packages/website/src/components/ASTViewerTS.tsx index ac66f5d10557..6cd080262960 100644 --- a/packages/website/src/components/ASTViewerTS.tsx +++ b/packages/website/src/components/ASTViewerTS.tsx @@ -1,11 +1,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import ASTViewer from './ast/ASTViewer'; -import type { - ASTViewerBaseProps, - ASTViewerModel, - ASTViewerModelSimple, -} from './ast/types'; +import type { ASTViewerBaseProps, ASTViewerModelMap } from './ast/types'; import type { SourceFile } from 'typescript'; import { serialize } from './ast/serializer/serializer'; import { createTsSerializer } from './ast/serializer/serializerTS'; @@ -44,7 +40,7 @@ export default function ASTViewerTS({ position, onSelectNode, }: ASTTsViewerProps): JSX.Element { - const [model, setModel] = useState(''); + const [model, setModel] = useState(''); const [syntaxKind] = useState(() => extractEnum(window.ts.SyntaxKind)); const [nodeFlags] = useState(() => extractEnum(window.ts.NodeFlags)); const [tokenFlags] = useState(() => extractEnum(window.ts.TokenFlags)); @@ -61,29 +57,29 @@ export default function ASTViewerTS({ // TODO: move this to serializer const getTooltip = useCallback( - (data: ASTViewerModelSimple): string | undefined => { - if (data.type === 'number') { + (data: ASTViewerModelMap): string | undefined => { + if (data.model.type === 'number') { switch (data.key) { case 'flags': return getFlagNamesFromEnum( nodeFlags, - Number(data.value), + Number(data.model.value), 'NodeFlags', ).join('\n'); case 'numericLiteralFlags': return getFlagNamesFromEnum( tokenFlags, - Number(data.value), + Number(data.model.value), 'TokenFlags', ).join('\n'); case 'modifierFlagsCache': return getFlagNamesFromEnum( modifierFlags, - Number(data.value), + Number(data.model.value), 'ModifierFlags', ).join('\n'); case 'kind': - return `SyntaxKind.${syntaxKind[Number(data.value)]}`; + return `SyntaxKind.${syntaxKind[Number(data.model.value)]}`; } } return undefined; diff --git a/packages/website/src/components/ast/ASTViewer.tsx b/packages/website/src/components/ast/ASTViewer.tsx index f4892897947a..8b54f3420d58 100644 --- a/packages/website/src/components/ast/ASTViewer.tsx +++ b/packages/website/src/components/ast/ASTViewer.tsx @@ -30,7 +30,7 @@ function ASTViewer({
): JSX.Element { +}: GenericParams): JSX.Element { const [isExpanded, setIsExpanded] = useState(() => level === 'ast'); const [isSelected, setIsSelected] = useState(false); const onHover = useCallback( (state: boolean) => { if (onSelectNode) { - const range = value.range; + const range = data.model.range; if (range) { onSelectNode(state ? range : null); } } }, - [value], + [data], ); useEffect(() => { const selected = selection - ? value.type === 'array' - ? isArrayInRange(selection, value) - : isInRange(selection, value) + ? data.model.type === 'array' + ? isArrayInRange(selection, data.model) + : isInRange(selection, data.model) : false; setIsSelected( - level !== 'ast' && selected && !hasChildInRange(selection, value), + level !== 'ast' && selected && !hasChildInRange(selection, data.model), ); if (selected && !isExpanded) { setIsExpanded(selected); } - }, [selection, value]); + }, [selection, data]); return ( setIsExpanded(!isExpanded)} > - {value.type === 'array' ? '[' : '{'} + {data.model.type === 'array' ? '[' : '{'} {isExpanded ? (
- {value.value.map((item, index) => ( + {data.model.value.map((item, index) => ( ))} @@ -81,11 +79,11 @@ export function ComplexItem({ ) : ( )} - {value.type === 'array' ? ']' : '}'} + {data.model.type === 'array' ? ']' : '}'} ); } @@ -94,25 +92,24 @@ export function ElementItem({ level, getTooltip, selection, - value, + data, onSelectNode, -}: GenericParams): JSX.Element { - if (value.type === 'array' || value.type === 'object') { +}: GenericParams): JSX.Element { + if (data.model.type === 'array' || data.model.type === 'object') { return ( ); } else { return ( ); diff --git a/packages/website/src/components/ast/HiddenItem.tsx b/packages/website/src/components/ast/HiddenItem.tsx index fb6ca1fc8c72..284bc2c2cad1 100644 --- a/packages/website/src/components/ast/HiddenItem.tsx +++ b/packages/website/src/components/ast/HiddenItem.tsx @@ -1,10 +1,10 @@ import React, { useEffect, useState } from 'react'; import styles from './ASTViewer.module.css'; import PropertyValue from './PropertyValue'; -import type { ASTViewerModel } from './types'; +import type { ASTViewerModelMap } from './types'; export interface HiddenItemProps { - readonly value: ASTViewerModel[]; + readonly value: ASTViewerModelMap[]; readonly level: string; readonly isArray?: boolean; } @@ -20,7 +20,7 @@ export default function HiddenItem({ useEffect(() => { if (isArray) { const filtered = value.filter(item => !isNaN(Number(item.key))); - setIsComplex(filtered.some(item => item.type !== 'number')); + setIsComplex(filtered.some(item => item.model.type !== 'number')); setLength(filtered.length); } }, [value, isArray]); diff --git a/packages/website/src/components/ast/ItemGroup.tsx b/packages/website/src/components/ast/ItemGroup.tsx index cb2d1fd46b81..bf6c5e3ca77d 100644 --- a/packages/website/src/components/ast/ItemGroup.tsx +++ b/packages/website/src/components/ast/ItemGroup.tsx @@ -5,11 +5,10 @@ import clsx from 'clsx'; import styles from './ASTViewer.module.css'; import PropertyName from './PropertyName'; -import type { ASTViewerModel } from './types'; +import type { ASTViewerModelMap } from './types'; export interface ItemGroupProps { - readonly propName?: string; - readonly value: ASTViewerModel; + readonly data: ASTViewerModelMap; readonly isSelected?: boolean; readonly isExpanded?: boolean; readonly canExpand?: boolean; @@ -19,8 +18,7 @@ export interface ItemGroupProps { } export default function ItemGroup({ - propName, - value, + data, isSelected, isExpanded, canExpand, @@ -46,8 +44,8 @@ export default function ItemGroup({ )} > diff --git a/packages/website/src/components/ast/PropertyValue.tsx b/packages/website/src/components/ast/PropertyValue.tsx index 6d5d385552dc..6539042d9af0 100644 --- a/packages/website/src/components/ast/PropertyValue.tsx +++ b/packages/website/src/components/ast/PropertyValue.tsx @@ -1,32 +1,32 @@ import React from 'react'; import styles from './ASTViewer.module.css'; -import type { ASTViewerModel } from './types'; +import type { ASTViewerModelMap } from './types'; export interface PropertyValueProps { - readonly value: ASTViewerModel; + readonly value: ASTViewerModelMap; } function PropertyValue({ value }: PropertyValueProps): JSX.Element { - switch (value.type) { + switch (value.model.type) { case 'string': - return {value.value}; + return {value.model.value}; case 'bigint': - return {value.value}; + return {value.model.value}; case 'number': - return {value.value}; + return {value.model.value}; case 'regexp': - return {value.value}; + return {value.model.value}; case 'undefined': - return {value.value}; + return {value.model.value}; case 'boolean': - return {value.value}; + return {value.model.value}; case 'array': case 'object': return {value.key}; case 'class': case 'ref': default: - return {value.value}; + return {value.model.value}; } } diff --git a/packages/website/src/components/ast/SimpleItem.tsx b/packages/website/src/components/ast/SimpleItem.tsx index b9a892f347a5..cb20dc0ccbb8 100644 --- a/packages/website/src/components/ast/SimpleItem.tsx +++ b/packages/website/src/components/ast/SimpleItem.tsx @@ -3,47 +3,46 @@ import ItemGroup from './ItemGroup'; import Tooltip from '@site/src/components/inputs/Tooltip'; import PropertyValue from './PropertyValue'; -import type { ASTViewerModelSimple, GetTooltipFn } from './types'; -import { OnSelectNodeFn } from './types'; +import type { + ASTViewerModelMapSimple, + GetTooltipFn, + OnSelectNodeFn, +} from './types'; export interface SimpleItemProps { readonly getTooltip?: GetTooltipFn; - readonly value: ASTViewerModelSimple; + readonly data: ASTViewerModelMapSimple; readonly onSelectNode?: OnSelectNodeFn; } export function SimpleItem({ getTooltip, - value, + data, onSelectNode, }: SimpleItemProps): JSX.Element { const [tooltip, setTooltip] = useState(); useEffect(() => { - setTooltip(getTooltip?.(value)); - }, [getTooltip, value]); + setTooltip(getTooltip?.(data)); + }, [getTooltip, data]); const onHover = useCallback( (state: boolean) => { - if (onSelectNode && value.range) { - onSelectNode(state ? value.range : null); + if (onSelectNode && data.model.range) { + onSelectNode(state ? data.model.range : null); } }, - [value], + [data], ); return ( - + {tooltip ? ( - + ) : ( - + )} ); diff --git a/packages/website/src/components/ast/serializer/serializer.ts b/packages/website/src/components/ast/serializer/serializer.ts index 153c84f5fc6d..9496ac3a86ef 100644 --- a/packages/website/src/components/ast/serializer/serializer.ts +++ b/packages/website/src/components/ast/serializer/serializer.ts @@ -1,7 +1,11 @@ -import type { ASTViewerModel, Serializer } from '../types'; +import type { + ASTViewerModelSimple, + ASTViewerModelMap, + Serializer, +} from '../types'; import { isRecord, objType } from '../utils'; -function getSimpleModel(data: unknown): ASTViewerModel { +function getSimpleModel(data: unknown): ASTViewerModelSimple { if (typeof data === 'string') { return { value: JSON.stringify(data), @@ -42,41 +46,40 @@ function getSimpleModel(data: unknown): ASTViewerModel { export function serialize( data: unknown, serializer?: Serializer, -): ASTViewerModel { - function processValue(data: [string, unknown][]): ASTViewerModel[] { +): ASTViewerModelMap { + function processValue(data: [string, unknown][]): ASTViewerModelMap[] { return data .filter(item => !item[0].startsWith('_') && item[1] !== undefined) .map(item => _serialize(item[1], item[0])); } - function _serialize(data: unknown, key?: string): ASTViewerModel { + function _serialize(data: unknown, key?: string): ASTViewerModelMap { if (isRecord(data)) { const serialized = serializer ? serializer(data, key, processValue) : undefined; if (serialized) { - return { - key, - ...serialized, - }; + // @ts-expect-error: typescript is funky + return { key, model: serialized }; } return { key, - value: processValue(Object.entries(data)), - type: 'object', + model: { + value: processValue(Object.entries(data)), + type: 'object', + }, }; } else if (Array.isArray(data)) { return { key, - value: processValue(Object.entries(data)), - type: 'array', + model: { + value: processValue(Object.entries(data)), + type: 'array', + }, }; } - return { - key, - ...getSimpleModel(data), - }; + return { key, model: getSimpleModel(data) }; } return _serialize(data); diff --git a/packages/website/src/components/ast/serializer/serializerScope.ts b/packages/website/src/components/ast/serializer/serializerScope.ts index 260be9889d38..495f768e504d 100644 --- a/packages/website/src/components/ast/serializer/serializerScope.ts +++ b/packages/website/src/components/ast/serializer/serializerScope.ts @@ -94,10 +94,11 @@ function getProps(nodeType: NodeType | undefined): string[] | undefined { 'references', 'set', 'type', - 'upper', 'variables', 'variableScope', 'functionExpressionScope', + 'childScopes', + 'upper', ]; case 'Definition': return ['name', 'type', 'node']; @@ -145,14 +146,6 @@ export function createScopeSerializer(): Serializer { if (SEEN_THINGS.has(uniqName)) { const found = SEEN_THINGS.get(uniqName); - if (found && Array.isArray(found.value) && found.value.length === 0) { - return { - range: found.range, - type: 'ref', - name: found.name, - value: '', - }; - } return found; } diff --git a/packages/website/src/components/ast/types.ts b/packages/website/src/components/ast/types.ts index 328900ca9304..f8c6a530c8c1 100644 --- a/packages/website/src/components/ast/types.ts +++ b/packages/website/src/components/ast/types.ts @@ -1,7 +1,7 @@ import type { SelectedPosition, SelectedRange } from '../types'; import Monaco from 'monaco-editor'; -export type GetTooltipFn = (data: ASTViewerModelSimple) => string | undefined; +export type GetTooltipFn = (data: ASTViewerModelMap) => string | undefined; export type OnSelectNodeFn = (node: SelectedRange | null) => void; export type ASTViewerModelTypeSimple = @@ -17,7 +17,6 @@ export type ASTViewerModelTypeSimple = export type ASTViewerModelTypeComplex = 'object' | 'array'; export interface ASTViewerModelBase { - key?: string; name?: string; range?: SelectedRange; } @@ -29,14 +28,25 @@ export interface ASTViewerModelSimple extends ASTViewerModelBase { export interface ASTViewerModelComplex extends ASTViewerModelBase { type: ASTViewerModelTypeComplex; - value: ASTViewerModel[]; + value: ASTViewerModelMap[]; +} + +export interface ASTViewerModelMapSimple { + key: string | undefined; + model: ASTViewerModelSimple; +} +export interface ASTViewerModelMapComplex { + key: string | undefined; + model: ASTViewerModelComplex; } export type ASTViewerModel = ASTViewerModelSimple | ASTViewerModelComplex; +export type ASTViewerModelMap = + | ASTViewerModelMapSimple + | ASTViewerModelMapComplex; export interface GenericParams { - readonly propName?: string; - readonly value: V; + readonly data: V; readonly level: string; readonly selection?: SelectedPosition | null; readonly onSelectNode?: OnSelectNodeFn; @@ -50,13 +60,13 @@ export interface ASTViewerBaseProps { export interface ASTViewerProps extends ASTViewerBaseProps { readonly getTooltip?: GetTooltipFn; - readonly value: ASTViewerModel | string; + readonly value: ASTViewerModelMap | string; } export type Serializer = ( data: Record, key: string | undefined, - processValue: (data: [string, unknown][]) => ASTViewerModel[], + processValue: (data: [string, unknown][]) => ASTViewerModelMap[], ) => ASTViewerModel | undefined; export type { SelectedPosition, SelectedRange }; diff --git a/packages/website/src/components/ast/utils.ts b/packages/website/src/components/ast/utils.ts index 0bc048d37fb9..bfe21860afd4 100644 --- a/packages/website/src/components/ast/utils.ts +++ b/packages/website/src/components/ast/utils.ts @@ -43,7 +43,7 @@ export function isArrayInRange( value: ASTViewerModelComplex, ): boolean { return Boolean( - position && value.value.some(item => isInRange(position, item)), + position && value.value.some(item => isInRange(position, item.model)), ); } @@ -54,10 +54,10 @@ export function hasChildInRange( return Boolean( position && value.value.some(item => - item.type === 'object' - ? isInRange(position, item) - : item.type === 'array' - ? isArrayInRange(position, item) + item.model.type === 'object' + ? isInRange(position, item.model) + : item.model.type === 'array' + ? isArrayInRange(position, item.model) : false, ), ); From e216f633ce287d9887c583f5b4f4de56f826480f Mon Sep 17 00:00:00 2001 From: Armano Date: Wed, 22 Dec 2021 13:22:02 +0100 Subject: [PATCH 10/13] docs(website): simplify types --- packages/website/src/components/ast/types.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/website/src/components/ast/types.ts b/packages/website/src/components/ast/types.ts index f8c6a530c8c1..1d5976424d2d 100644 --- a/packages/website/src/components/ast/types.ts +++ b/packages/website/src/components/ast/types.ts @@ -31,19 +31,15 @@ export interface ASTViewerModelComplex extends ASTViewerModelBase { value: ASTViewerModelMap[]; } -export interface ASTViewerModelMapSimple { - key: string | undefined; - model: ASTViewerModelSimple; -} -export interface ASTViewerModelMapComplex { - key: string | undefined; - model: ASTViewerModelComplex; +export type ASTViewerModel = ASTViewerModelSimple | ASTViewerModelComplex; + +export interface ASTViewerModelMap { + key?: string; + model: T; } -export type ASTViewerModel = ASTViewerModelSimple | ASTViewerModelComplex; -export type ASTViewerModelMap = - | ASTViewerModelMapSimple - | ASTViewerModelMapComplex; +export type ASTViewerModelMapSimple = ASTViewerModelMap; +export type ASTViewerModelMapComplex = ASTViewerModelMap; export interface GenericParams { readonly data: V; From 3963a4a5b4856c0dfc4aab31aa7f2f9bdb7034ad Mon Sep 17 00:00:00 2001 From: Armano Date: Wed, 22 Dec 2021 15:20:32 +0100 Subject: [PATCH 11/13] docs(website): correct issue with printing functions --- .../website/src/components/ast/serializer/serializer.ts | 5 ++++- .../src/components/ast/serializer/serializerScope.ts | 8 +++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/website/src/components/ast/serializer/serializer.ts b/packages/website/src/components/ast/serializer/serializer.ts index 9496ac3a86ef..3cbea9174a10 100644 --- a/packages/website/src/components/ast/serializer/serializer.ts +++ b/packages/website/src/components/ast/serializer/serializer.ts @@ -59,7 +59,6 @@ export function serialize( ? serializer(data, key, processValue) : undefined; if (serialized) { - // @ts-expect-error: typescript is funky return { key, model: serialized }; } return { @@ -79,6 +78,10 @@ export function serialize( }; } + if (typeof data === 'function' && key) { + return { key: `${key}()`, model: getSimpleModel(data()) }; + } + return { key, model: getSimpleModel(data) }; } diff --git a/packages/website/src/components/ast/serializer/serializerScope.ts b/packages/website/src/components/ast/serializer/serializerScope.ts index 495f768e504d..e63086d77554 100644 --- a/packages/website/src/components/ast/serializer/serializerScope.ts +++ b/packages/website/src/components/ast/serializer/serializerScope.ts @@ -145,8 +145,7 @@ export function createScopeSerializer(): Serializer { const uniqName = `${nodeName}${value}`; if (SEEN_THINGS.has(uniqName)) { - const found = SEEN_THINGS.get(uniqName); - return found; + return SEEN_THINGS.get(uniqName); } const result: ASTViewerModel = { @@ -161,7 +160,10 @@ export function createScopeSerializer(): Serializer { const props = getProps(nodeType); if (props) { - values = props.map(key => [key, data[key]]); + values = props.map(key => { + const res = data[key]; + return [key, typeof res === 'function' ? res.bind(data) : res]; + }); } else { values = Object.entries(data); } From 8fca54865062de822385667f4b6e9ad4a0edc132 Mon Sep 17 00:00:00 2001 From: Armano Date: Wed, 22 Dec 2021 16:09:36 +0100 Subject: [PATCH 12/13] docs(website): add missing public properties --- .../ast/serializer/serializerScope.ts | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/website/src/components/ast/serializer/serializerScope.ts b/packages/website/src/components/ast/serializer/serializerScope.ts index e63086d77554..176f49f92c5c 100644 --- a/packages/website/src/components/ast/serializer/serializerScope.ts +++ b/packages/website/src/components/ast/serializer/serializerScope.ts @@ -86,12 +86,13 @@ function getNodeType(nodeName: string | undefined): NodeType | undefined { function getProps(nodeType: NodeType | undefined): string[] | undefined { switch (nodeType) { case 'ScopeManager': - return ['scopes', 'variables', 'references']; + return ['scopes', 'globalScope', 'variables']; case 'Scope': return [ 'block', 'isStrict', 'references', + 'through', 'set', 'type', 'variables', @@ -101,27 +102,40 @@ function getProps(nodeType: NodeType | undefined): string[] | undefined { 'upper', ]; case 'Definition': - return ['name', 'type', 'node']; + return [ + 'name', + 'type', + 'node', + 'isTypeDefinition', + 'isVariableDefinition', + 'rest', + 'parent', + ]; case 'Reference': return [ - 'identifier', 'init', - 'isRead', + 'identifier', + 'from', 'isTypeReference', 'isValueReference', + 'maybeImplicitGlobal', + 'isRead', 'isWrite', 'resolved', 'writeExpr', ]; case 'Variable': return [ - 'defs', 'name', + 'identifiers', 'references', + 'defs', + 'eslintUsed', + 'tainted', + 'scope', 'isValueVariable', 'isTypeVariable', - 'eslintUsed', - 'identifiers', + 'writeable', ]; } return undefined; From 6cc81df5d0a9a70e9654c6277b3dda269925a617 Mon Sep 17 00:00:00 2001 From: Armano Date: Wed, 22 Dec 2021 21:40:22 +0100 Subject: [PATCH 13/13] fix(website): rename scopeSerializer to astSerializer Co-authored-by: Brad Zacher --- packages/website/src/components/ASTViewerESTree.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/website/src/components/ASTViewerESTree.tsx b/packages/website/src/components/ASTViewerESTree.tsx index 25286af0a3ce..7d46efdfb110 100644 --- a/packages/website/src/components/ASTViewerESTree.tsx +++ b/packages/website/src/components/ASTViewerESTree.tsx @@ -21,8 +21,8 @@ export default function ASTViewerESTree({ if (typeof value === 'string') { setModel(value); } else { - const scopeSerializer = createESTreeSerializer(); - setModel(serialize(value, scopeSerializer)); + const astSerializer = createESTreeSerializer(); + setModel(serialize(value, astSerializer)); } }, [value]);