Skip to content

docs(website): add simple ts ast viewer #4243

New issue

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

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

Already on GitHub? Sign in to your account

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/website-eslint/src/linter/linter.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ const PARSER_NAME = '@typescript-eslint/parser';
export function loadLinter() {
const linter = new Linter();
let storedAST;
let storedTsAST;

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

getTsAst() {
return storedTsAST;
},

lint(code, parserOptions, rules) {
return linter.verify(code, {
parser: PARSER_NAME,
Expand Down
4 changes: 3 additions & 1 deletion packages/website-eslint/src/linter/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ function parseAndGenerateServices(code, options) {

return {
ast: estree,
tsAst: ast,
services: {
hasFullTypeInformation: true,
program,
Expand All @@ -24,7 +25,7 @@ function parseAndGenerateServices(code, options) {
}

export function parseForESLint(code, parserOptions) {
const { ast, services } = parseAndGenerateServices(code, {
const { ast, tsAst, services } = parseAndGenerateServices(code, {
...parserOptions,
jsx: parserOptions.ecmaFeatures?.jsx ?? false,
useJSXTextNode: true,
Expand All @@ -40,6 +41,7 @@ export function parseForESLint(code, parserOptions) {

return {
ast,
tsAst,
services,
scopeManager,
visitorKeys,
Expand Down
1 change: 1 addition & 0 deletions packages/website-eslint/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface WebLinter {
ruleNames: { name: string; description?: string }[];

getAst(): ESLintAST;
getTsAst(): Record<string, unknown>;

lint(
code: string,
Expand Down
52 changes: 52 additions & 0 deletions packages/website/src/components/ASTViewerESTree.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React, { useCallback } 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';

function isESTreeNode(
value: unknown,
): value is Record<string, unknown> & TSESTree.BaseNode {
return isRecord(value) && 'type' in value && 'loc' in value;
}

export const propsToFilter = ['parent', 'comments', 'tokens'];

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

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

const getNodeName = useCallback(
(value: unknown): string | undefined =>
isESTreeNode(value) ? String(value.type) : undefined,
[],
);

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

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

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

function extractEnum(
obj: Record<number, string | number>,
): Record<number, string> {
const result: Record<number, string> = {};
const keys = Object.entries(obj);
for (const [name, value] of keys) {
if (typeof value === 'number') {
if (!(value in result)) {
result[value] = name;
}
}
}
return result;
}

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

function getFlagNamesFromEnum(
allFlags: Record<number, string>,
flags: number,
prefix: string,
): string[] {
return Object.entries(allFlags)
.filter(([f, _]) => (Number(f) & flags) !== 0)
.map(([_, name]) => `${prefix}.${name}`);
}

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

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

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

export default function ASTViewerTS(props: ASTTsViewerProps): JSX.Element {
const [syntaxKind, setSyntaxKind] = useState<Record<number, string>>({});
const [nodeFlags, setNodeFlags] = useState<Record<number, string>>({});
const [tokenFlags, setTokenFlags] = useState<Record<number, string>>({});
const [modifierFlags, setModifierFlags] = useState<Record<number, string>>(
{},
);

useEffect(() => {
setSyntaxKind(extractEnum(window.ts.SyntaxKind));
setNodeFlags(extractEnum(window.ts.NodeFlags));
setTokenFlags(extractEnum(window.ts.TokenFlags));
setModifierFlags(extractEnum(window.ts.ModifierFlags));
}, [props.version]);

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]}`;
}
return undefined;
},
[nodeFlags, tokenFlags, syntaxKind],
);

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

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

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

return (
<ASTViewer
filterProps={filterProps}
getRange={getRange}
getTooltip={getTooltip}
getNodeName={getNodeName}
{...props}
/>
);
}
35 changes: 19 additions & 16 deletions packages/website/src/components/OptionsSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,15 @@ import Tooltip from './inputs/Tooltip';
import EditIcon from './icons/EditIcon';
import CopyIcon from './icons/CopyIcon';

import useDebouncedToggle from './hooks/useDebouncedToggle';

import { createMarkdown } from './lib/markdown';

import type { RuleDetails } from './types';

import styles from './OptionsSelector.module.css';

import type {
CompilerFlags,
ConfigModel,
SourceType,
RulesRecord,
} from './types';
import type { CompilerFlags, ConfigModel, RulesRecord } from './types';

export interface OptionsSelectorParams {
readonly ruleOptions: RuleDetails[];
Expand All @@ -31,6 +28,12 @@ export interface OptionsSelectorParams {
readonly isLoading: boolean;
}

const ASTOptions = [
{ value: false, label: 'Disabled' },
{ value: 'es', label: 'ESTree' },
{ value: 'ts', label: 'TypeScript' },
] as const;

function OptionsSelector({
ruleOptions,
state,
Expand All @@ -40,8 +43,8 @@ function OptionsSelector({
}: OptionsSelectorParams): JSX.Element {
const [eslintModal, setEslintModal] = useState<boolean>(false);
const [typeScriptModal, setTypeScriptModal] = useState<boolean>(false);
const [copyLink, setCopyLink] = useState<boolean>(false);
const [copyMarkdown, setCopyMarkdown] = useState<boolean>(false);
const [copyLink, setCopyLink] = useDebouncedToggle<boolean>(false);
const [copyMarkdown, setCopyMarkdown] = useDebouncedToggle<boolean>(false);

const updateTS = useCallback(
(version: string) => {
Expand Down Expand Up @@ -145,20 +148,20 @@ function OptionsSelector({
/>
</label>
<label className={styles.optionLabel}>
Show AST
<Checkbox
name="ast"
checked={state.showAST}
AST Viewer
<Dropdown
name="showAST"
value={state.showAST}
onChange={(e): void => setState({ showAST: e })}
className={styles.optionCheckbox}
options={ASTOptions}
/>
</label>
<label className={styles.optionLabel}>
Source type
<Dropdown
name="sourceType"
value={state.sourceType}
onChange={(e): void => setState({ sourceType: e as SourceType })}
onChange={(e): void => setState({ sourceType: e })}
options={['script', 'module']}
/>
</label>
Expand All @@ -180,7 +183,7 @@ function OptionsSelector({
<Expander label="Actions">
<button className={styles.optionLabel} onClick={copyLinkToClipboard}>
Copy Link
<Tooltip open={copyLink} text="Copied" close={setCopyLink}>
<Tooltip open={copyLink} text="Copied">
<CopyIcon className={styles.clickableIcon} />
</Tooltip>
</button>
Expand All @@ -189,7 +192,7 @@ function OptionsSelector({
onClick={copyMarkdownToClipboard}
>
Copy Markdown
<Tooltip open={copyMarkdown} text="Copied" close={setCopyMarkdown}>
<Tooltip open={copyMarkdown} text="Copied">
<CopyIcon className={styles.clickableIcon} />
</Tooltip>
</button>
Expand Down
2 changes: 1 addition & 1 deletion packages/website/src/components/Playground.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
height: 100%;
width: 50%;
border: 1px solid var(--ifm-color-emphasis-100);
padding: 0;
overflow: auto;
background: var(--ifm-background-surface-color);
word-wrap: initial;
white-space: nowrap;
background: var(--code-editor-bg);
Expand Down
Loading