8000 docs: fleshed out dedicated Architecture section (#4124) · dotJack/typescript-eslint@ebaf947 · GitHub
[go: up one dir, main page]

Skip to content

Commit ebaf947

Browse files
Josh Goldbergarmano2bradzacher
authored
docs: fleshed out dedicated Architecture section (typescript-eslint#4124)
* docs: fleshed out dedicated Architecture section * Update docs/README.md * Update docs/README.md * Update docs/development/CUSTOM_RULES.md Co-authored-by: Armano <armano2@users.noreply.github.com> * chore: format, and expand root Development sidebar section * chore: review feedback * Update .cspell.json Co-authored-by: Armano <armano2@users.noreply.github.com> Co-authored-by: Brad Zacher <brad.zacher@gmail.com>
1 parent 62b8098 commit ebaf947

File tree

15 files changed

+441
-153
lines changed

15 files changed

+441
-153
lines changed

.cspell.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@
103103
"typedef",
104104
"typedefs",
105105
"unfixable",
106+
"unoptimized",
106107
"unprefixed",
107108
"Zacher"
108109
],

README.md

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,16 @@
2020
👆
2121
</p>
2222

23-
---
24-
2523
## Packages included in this project
2624

27-
- [`@typescript-eslint/eslint-plugin`](./packages/eslint-plugin/) - An ESLint-plugin with many lint rules for TypeScript features, as well as type-aware lint rules.
28-
29-
- [`@typescript-eslint/parser`](./packages/parser/) - A ESLint-parser which enables ESLint to be able to understand TypeScript syntax. It's powered by our `typescript-estree` package.
30-
31-
- [`@typescript-eslint/eslint-plugin-tslint`](./packages/eslint-plugin-tslint) - An ESLint-plugin that allows you to bridge ESLint and TSLint to help you migrate from TSLint to ESLint.
32-
33-
- [`@typescript-eslint/experimental-utils`](./packages/experimental-utils) - Public utilities for working ESLint and for writing ESLint-plugins in TypeScript.
34-
35-
- [`@typescript-eslint/typescript-estree`](./packages/typescript-estree/) - A parser which takes TypeScript source code and produces an [ESTree](https://github.com/estree/estree)-compatible AST.
25+
See https://typescript-eslint.io/docs/development/architecture/packages for more details.
3626

37-
- [`@typescript-eslint/scope-manager`](./packages/scope-manager) - An [`eslint-scope`](https://github.com/eslint/eslint-scope)-compatible scope analyser that can understand TypeScript language features such as types, enums and namespaces.
27+
- [`@typescript-eslint/eslint-plugin`](./packages/eslint-plugin)
28+
- [`@typescript-eslint/parser`](./packages/parser)
29+
- [`@typescript-eslint/eslint-plugin-tslint`](./packages/eslint-plugin-tslint)
30+
- [`@typescript-eslint/experimental-utils`](./packages/experimental-utils)
31+
- [`@typescript-eslint/typescript-estree`](./packages/typescript-estree)
32+
- [`@typescript-eslint/scope-manager`](./packages/scope-manager)
3833

3934
## Versioning
4035

docs/getting-started/README.md renamed to docs/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ The docs are broken down into the following categories:
1010

1111
- [I want to lint my TypeScript codebase.](./linting/README.md)
1212

13-
- [(TODO) I want to write an ESLint plugin in TypeScript.](./plugin-development/README.md)
13+
- [I want to develop an ESLint plugin in TypeScript.](./development/CUSTOM_RULES.md)

docs/development/CUSTOM_RULES.md

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
---
2+
id: custom-rules
3+
sidebar_label: Custom Rules
4+
title: Custom Rules
5+
---
6+
7+
:::important
8+
You should be familiar with [ESLint's developer guide](https://eslint.org/docs/developer-guide) and [Development > Architecture](./architecture/asts) before writing custom rules.
9+
:::
10+
11+
As long as you are using `@typescript-eslint/parser` as the `parser` in your ESLint configuration, custom ESLint rules generally work the same way for JavaScript and TypeScript code.
12+
The main two changes to custom rules writing are:
13+
14+
- [AST Extensions](#ast-extensions): targeting TypeScript-specific syntax in your rule selectors
15+
- [Typed Rules](#typed-rules): using the TypeScript type checker to inform rule logic
16+
17+
## AST Extensions
18+
19+
`@typescript-eslint/estree` creates AST nodes for TypeScript syntax with names that begin with `TS`, such as `TSInterfaceDeclaration` and `TSTypeAnnotation`.
20+
These nodes are treated just like any other AST node.
21+
You can query for them in your rule selectors.
22+
23+
This rule written in JavaScript bans interfaces that start with a lower-case letter:
24+
25+
```js
26+
export const rule = {
27+
create(context) {
28+
return {
29+
TSInterfaceDeclaration(node) {
30+
if (/[a-z]/.test(node.id.name[0])) {
31+
context.report({
32+
messageId: 'uppercase',
33+
node: node.id,
34+
});
35+
}
36+
},
37+
};
38+
},
39+
meta: {
40+
docs: {
41+
category: 'Best Practices',
42+
description: 'Interface names should start with an upper-case letter.',
43+
},
44+
messages: {
45+
uppercase: 'Start this name with an upper-case letter.',
46+
},
47+
type: 'suggestion',
48+
schema: [],
49+
},
50+
};
51+
```
52+
53+
### Writing Rules in TypeScript
54+
55+
The `@typescript-eslint/experimental-utils` package acts as a replacement package for `eslint` that exports all the same objects and types, but with typescript-eslint support.
56+
57+
:::caution
58+
`@types/eslint` types are based on `@types/estree` and do not recognize typescript-eslint nodes and properties.
59+
You should generally not need to import from `eslint` when writing custom typescript-eslint rules in TypeScript.
60+
:::
61+
62+
#### Rule Types
63+
64+
`@typescript-eslint/experimental-utils` exports a `RuleModule` interface that allows specifying generics for:
65+
66+
- `MessageIds`: a union of string literal message IDs that may be reported
67+
- `Options`: what options users may configure for the rule
68+
69+
```ts
70+
import { TSESLint } from '@typescript-eslint/experimental-utils';
71+
72+
export const rule: TSESLint.RuleModule<'uppercase', []> = {
73+
create(context /* : Readonly<RuleContext<TMessageIds, TOptions>> */) {
74+
// ...
75+
},
76+
};
77+
```
78+
79+
For groups of rules that share a common documentation URL, a `RuleCreator` function is exported.
80+
It takes in a function that transforms a rule name into its documentation URL, then returns a function that takes in a rule module object.
81+
The returned function is able to infer message IDs from `meta.messages`.
82+
83+
```ts
84+
import { ESLintUtils } from '@typescript-eslint/experimental-utils';
85+
86+
const createRule = ESLintUtils.RuleCreator(
87+
name => `https://example.com/rule/${name}`,
88+
);
89+
90+
// Type: const rule: RuleModule<"uppercase", ...>
91+
export const rule = createRule({
92+
create(context) {
93+
// ...
94+
},
95+
meta: {
96+
messages: {
97+
uppercase: 'Start this name with an upper-case letter.',
98+
},
99+
// ...
100+
},
101+
});
102+
```
103+
104+
#### Node Types
105+
106+
TypeScript types for nodes exist in a `TSESTree` namespace exported by `@typescript-eslint/experimental-utils`.
107+
The above rule body could be better written in TypeScript with a type annotation on the `node`:
108+
109+
```ts
110+
import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';
111+
112+
// ...
113+
114+
export const rule = createRule({
115+
create(context) {
116+
return {
117+
TSInterfaceDeclaration(node: TSESTree.TSInterfaceDeclaration) {
118+
// ...
119+
},
120+
};
121+
},
122+
// ...
123+
});
124+
```
125+
126+
An `AST_NODE_TYPES` enum is exported as well to hold the values for AST node `type` properties.
127+
`TSESTree.Node` is available as union type that uses its `type` member as a discriminant.
128+
129+
For example, checking `node.type` can narrow down the type of the `node`:
130+
131+
```ts
132+
import {
133+
AST_NODE_TYPES,
134+
TSESTree,
135+
} from '@typescript-eslint/experimental-utils';
136+
137+
export function describeNode(node: TSESTree.Node): string {
138+
switch (node.type) {
139+
case AST_NODE_TYPES.ArrayExpression:
140+
return `Array containing ${node.elements.map(describeNode).join(', ')}`;
141+
142+
case AST_NODE_TYPES.Literal:
143+
return `Literal value ${node.raw}`;
144+
145+
default:
146+
return '🤷';
147+
}
148+
}
149+
```
150+
151+
## Type Checking
152+
153+
:::tip
154+
Read TypeScript's [Compiler APIs > Using the Type Checker](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API#using-the-type-checker) section for how to use a program's type checker.
155+
:::
156+
157+
The biggest addition typescript-eslint brings to ESLint rules is the ability to use TypeScript's type checker APIs.
158+
159+
`@typescript-eslint/experimental-utils` exports an `ESLintUtils` namespace containing a `getParserServices` function that takes in an ESLint context and returns a `parserServices` object.
160+
161+
That `parserServices` object contains:
162+
163+
- `program`: A full TypeScript `ts.Program` object
164+
- `esTreeNodeToTSNodeMap`: Map of `@typescript-eslint/estree` `TSESTree.Node` nodes to their TypeScript `ts.Node` equivalents
165+
- `tsNodeToESTreeNodeMap`: Map of TypeScript `ts.Node` nodes to their `@typescript-eslint/estree` `TSESTree.Node` equivalents
166+
167+
By mapping from ESTree nodes to TypeScript nodes and retrieving the TypeScript program from the parser services, rules are able to ask TypeScript for full type information on those nodes.
168+
169+
This rule bans for-of looping over an enum by using the type-checker via typescript-eslint and TypeScript APIs:
170+
171+
```ts
172+
import { ESLintUtils } from '@typescript-eslint/experimental-utils';
173+
import * as ts from 'typescript';
174+
import * as tsutils from 'tsutils';
175+
176+
export const rule: eslint.Rule.RuleModule = {
177+
create(context) {
178+
return {
179+
ForOfStatement(node) {
180+
// 1. Grab the TypeScript program from parser services
181+
const parserServices = ESLintUtils.getParserServices(context);
182+
const checker = parserServices.program.getTypeChecker();
183+
184+
// 2. Find the backing TS node for the ES node, then that TS type
185+
const originalNode = parserServices.esTreeNodeToTSNodeMap.get(
186+
node.right,
187+
);
188+
const nodeType = checker.getTypeAtLocation(node);
189+
190+
// 3. Check the TS node type using the TypeScript APIs
191+
if (tsutils.isTypeFlagSet(nodeType, ts.TypeFlags.EnumLike)) {
192+
context.report({
193+
messageId: 'loopOverEnum',
194+
node: node.right,
195+
});
196+
}
197+
},
198+
};
199+
},
200+
meta: {
201+
docs: {
202+
category: 'Best Practices',
203+
description: 'Avoid looping over enums.',
204+
},
205+
messages: {
206+
loopOverEnum: 'Do not loop over enums.',
207+
},
208+
type: 'suggestion',
209+
schema: [],
210+
},
211+
};
212+
```
213+
214+
## Testing
215+
216+
`@typescript-eslint/experimental-utils` exports a `RuleTester` with a similar API to the built-in [ESLint `RuleTester`](https://eslint.org/docs/developer-guide/nodejs-api#ruletester).
217+
It should be provided with the same `parser` and `parserOptions` you would use in your ESLint configuration.
218+
219+
### Testing Untyped Rules
220+
221+
For rules that don't need type information, passing just the `parser` will do:
222+
223+
```ts
224+
import { ESLintUtils } from '@typescript-eslint/experimental-utils';
225+
import rule from './my-rule';
226+
227+
const ruleTester = new ESLintUtils.RuleTester({
228+
parser: '@typescript-eslint/parser',
229+
});
230+
231+
ruleTester.run('my-rule', rule {
232+
valid: [/* ... */],
233+
invalid: [/* ... */],
234+
});
235+
```
236+
237+
### Testing Typed Rules
238+
239+
For rules that do need type information, `parserOptions` must be passed in as well.
240+
Tests must have at least an absolute `tsconfigRootDir` path provided as well as a relative `project` path from that directory:
241+
242+
```ts
243+
import { ESLintUtils } from '@typescript-eslint/experimental-utils';
244+
import rule from './my-typed-rule';
245+
246+
const ruleTester = new ESLintUtils.RuleTester({
247+
parser: '@typescript-eslint/parser',
248+
parserOptions: {
249+
project: './tsconfig.json',
250+
tsconfigRootDir: __dirname,
251+
}
252+
});
253+
254+
ruleTester.run('my-typed-rule', rule {
255+
valid: [/* ... */],
256+
invalid: [/* ... */],
257+
});
258+
```
259+
260+
:::note
261+
For now, `ESLintUtils.RuleTester` requires the following physical files be present on disk for typed rules:
262+
263+
- `tsconfig.json`: tsconfig used as the test "project"
264+
- One of the following two files:
265+
- `file.ts`: blank test file used for normal TS tests
266+
- `file.tsx`: blank test file used for tests with `parserOptions: { ecmaFeatures: { jsx: true } }`
267+
268+
:::

docs/development/architecture/ASTS.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
id: asts
3+
title: ASTs
4+
sidebar_label: ASTs
5+
---
6+
7+
## Abstract Syntax Trees (AST)s
8+
9+
Parsers such as those in ESLint and TypeScript read in the text of source code and parse it into a standard format they can reason about known as an **Abstract Syntax Tree** (AST).
10+
ASTs are called such because although they might contain information on the location of constructs within source code, they are an abstract representation that cares more about the semantic structure.
11+
12+
For example, given this line of code:
13+
14+
```js
15+
1 + 2;
16+
```
17+
18+
ESLint would natively understand it as an object like:
19+
20+
```json
21+
{
22+
"type": "ExpressionStatement",
23+
"expression": {
24+
"type": "BinaryExpression",
25+
"left": {
26+
"type": "Literal",
27+
"value": 1,
28+
"raw": "1"
29+
},
30+
"operator": "+",
31+
"right": {
32+
"type": "Literal",
33+
"value": 2,
34+
"raw": "2"
35+
}
36+
}
37+
}
38+
```
39+
40+
ESLint uses an AST format known as **[`estree`]**.
41+
42+
ESTree is more broadly used than just for ESLint -- it is the de facto community standard.
43+
ESLint's built-in parser that outputs an `estree`-shaped AST is also a separate package, called **[`espree`]**.
44+
45+
:::note
46+
You can play more with various ASTs such as ESTree on [astexplorer.net] and read more details on their [Wikipedia article](https://en.wikipedia.org/wiki/Abstract_syntax_tree).
47+
:::
48+
49+
[astexplorer.net]: https://astexplorer.net
50+
[`espree`]: https://github.com/eslint/espree
51+
[`estree`]: https://github.com/estree/estree

0 commit comments

Comments
 (0)
0