Skip to content

Commit 60b7fb4

Browse files
docs: add guide for building ESLint plugins (typescript-eslint#9684)
* docs: add guide for building ESLint plugins * Corrected languageOptions placement * Corrected languageOptions placement a bit more * nit on custom rules: four * Notes from Josh * yarn format --write * One last string parser reference * remove requiresTypeChecking
1 parent e05c2e5 commit 60b7fb4

File tree

4 files changed

+134
-50
lines changed

4 files changed

+134
-50
lines changed

docs/developers/Custom_Rules.mdx

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ You should be familiar with [ESLint's developer guide](https://eslint.org/docs/d
1010
:::
1111

1212
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.
13-
The main three changes to custom rules writing are:
13+
The main four changes to custom rules writing are:
1414

1515
- [Utils Package](#utils-package): we recommend using `@typescript-eslint/utils` to create custom rules
1616
- [AST Extensions](#ast-extensions): targeting TypeScript-specific syntax in your rule selectors
1717
- [Typed Rules](#typed-rules): using the TypeScript type checker to inform rule logic
18+
- [Testing](#testing): using `@typescript-eslint/rule-tester`'s `RuleTester` instead of ESLint core's
1819

1920
## Utils Package
2021

@@ -305,7 +306,6 @@ This rule bans for-of looping over an enum by using the TypeScript type checker
305306

306307
```ts
307308
import { ESLintUtils } from '@typescript-eslint/utils';
308-
import * as tsutils from 'ts-api-utils';
309309
import * as ts from 'typescript';
310310

311311
export const rule = createRule({
@@ -316,10 +316,10 @@ export const rule = createRule({
316316
const services = ESLintUtils.getParserServices(context);
317317

318318
// 2. Find the TS type for the ES node
319-
const type = services.getTypeAtLocation(node);
319+
const type = services.getTypeAtLocation(node.right);
320320

321-
// 3. Check the TS type using the TypeScript APIs
322-
if (tsutils.isTypeFlagSet(type, ts.TypeFlags.EnumLike)) {
321+
// 3. Check the TS type's backing symbol for being an enum
322+
if (type.symbol.flags & ts.SymbolFlags.Enum) {
323323
context.report({
324324
messageId: 'loopOverEnum',
325325
node: node.right,
@@ -348,11 +348,15 @@ Rules can retrieve their full backing TypeScript type checker with `services.pro
348348
This can be necessary for TypeScript APIs not wrapped by the parser services.
349349
:::
350350

351-
:::caution
352-
We recommend against changing rule logic based solely on whether `services.program` exists.
351+
### Conditional Type Information
352+
353+
We recommend _against_ changing rule logic based solely on whether `services.program` exists.
353354
In our experience, users are generally surprised when rules behave differently with or without type information.
354355
Additionally, if they misconfigure their ESLint config, they may not realize why the rule started behaving differently.
355356
Consider either gating type checking behind an explicit option for the rule or creating two versions of the rule instead.
357+
358+
:::tip
359+
Documentation generators such as [`eslint-doc-generator`](https://github.com/bmish/eslint-doc-generator) can automatically indicate in a rule's docs whether it needs type information.
356360
:::
357361

358362
## Testing
@@ -364,15 +368,13 @@ Below is a quick-start guide. For more in-depth docs and examples [see the `@typ
364368

365369
### Testing Untyped Rules
366370

367-
For rules that don't need type information, passing just the `parser` will do:
371+
For rules that don't need type information, no constructor parameters are necessary:
368372

369373
```ts
370374
import { RuleTester } from '@typescript-eslint/rule-tester';
371375
import rule from './my-rule';
372376

373-
const ruleTester = new RuleTester({
374-
parser: '@typescript-eslint/parser',
375-
});
377+
const ruleTester = new RuleTester();
376378

377379
ruleTester.run('my-rule', rule, {
378380
valid: [
@@ -387,17 +389,21 @@ ruleTester.run('my-rule', rule, {
387389
### Testing Typed Rules
388390

389391
For rules that do need type information, `parserOptions` must be passed in as well.
390-
Tests must have at least an absolute `tsconfigRootDir` path provided as well as a relative `project` path from that directory:
392+
We recommend using `parserOptions.projectService` with options to allow a default project for each test file.
391393

392394
```ts
393395
import { RuleTester } from '@typescript-eslint/rule-tester';
394396
import rule from './my-typed-rule';
395397

396398
const ruleTester = new RuleTester({
397-
parser: '@typescript-eslint/parser',
398-
parserOptions: {
399-
project: './tsconfig.json',
400-
tsconfigRootDir: __dirname,
399+
languageOptions: {
400+
parserOptions: {
401+
projectService: {
402+
allowDefaultProjectForFiles: ['*.ts*'],
403+
defaultProject: 'tsconfig.json',
404+
},
405+
tsconfigRootDir: __dirname,
406+
},
401407
},
402408
});
403409

@@ -411,12 +417,4 @@ ruleTester.run('my-typed-rule', rule, {
411417
});
412418
```
413419

414-
:::note
415-
For now, `RuleTester` requires the following physical files be present on disk for typed rules:
416-
417-
- `tsconfig.json`: tsconfig used as the test "project"
418-
- One of the following two files:
419-
- `file.ts`: blank test file used for normal TS tests
420-
- `react.tsx`: blank test file used for tests with `parserOptions: { ecmaFeatures: { jsx: true } }`
421-
422-
:::
420+
See [_Rule Tester_ > _Type-Aware Testing_](../packages/Rule_Tester.mdx#type-aware-testing) for more details.

docs/developers/ESLint_Plugins.mdx

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
---
2+
id: eslint-plugins
3+
sidebar_label: Building ESLint Plugins
4+
title: ESLint Plugins
5+
---
6+
7+
:::important
8+
This page describes how to write your own custom ESLint plugins using typescript-eslint.
9+
You should be familiar with [ESLint's plugins guide](https://eslint.org/docs/latest/extend/plugins) and [typescript-eslint Custom Rules](./Custom_Rules.mdx) before writing custom plugins.
10+
:::
11+
12+
Custom plugins that support TypeScript code and typed linting look very similar to any other ESLint plugin.
13+
Follow the same general steps as [ESLint's plugins guide > _Creating a plugin_](https://eslint.org/docs/latest/extend/plugins#creating-a-plugin) to set up your plugin.
14+
The required differences are noted on this page.
15+
16+
:::tip
17+
See [**`eslint-plugin-example-typed-linting`**](https://github.com/typescript-eslint/examples/tree/main/packages/eslint-plugin-example-typed-linting) for an example plugin that supports typed linting.
18+
:::
19+
20+
## Package Dependencies
21+
22+
Your plugin should have the following `package.json` entries.
23+
24+
For all `@typescript-eslint` and `typescript-eslint` packages, keep them at the same semver versions.
25+
As an example, you might set each of them to `^8.1.2` or `^7.12.0 || ^8.0.0`.
26+
27+
### `dependencies`
28+
29+
[`@typescript-eslint/utils`](../packages/Utils.mdx) is required for the [`RuleCreator` factory to create rules](#rulecreator-usage).
30+
31+
### `devDependencies`
32+
33+
[`@typescript-eslint/rule-tester`](../packages/Rule_Tester.mdx) is strongly recommended to be able to [test rules with our `RuleTester`](./Custom_Rules.mdx).
34+
35+
### `peerDependencies`
36+
37+
Include the following to enforce the version range allowed without making users' package managers install them:
38+
39+
- `@typescript-eslint/parser` and any other parsers users are expected to be using
40+
- `eslint`
41+
- `typescript`
42+
43+
Those are all packages consumers are expected to be using already.
44+
45+
## `RuleCreator` Usage
46+
47+
We recommend including at least the following three properties in your plugin's [`RuleCreator` extra rule docs types](./Custom_Rules.mdx#extra-rule-docs-types):
48+
49+
- `description: string`: a succinct description of what the rule does
50+
- `recommended?: boolean`: whether the rule exists in your plugin's shared _"`recommended`"_ config
51+
- `requiresTypeChecking?: boolean`: whether the rule will use type information, for documentation generators such as [`eslint-doc-generator`](https://github.com/bmish/eslint-doc-generator)
52+
53+
For example, from [`eslint-plugin-example-typed-linting`'s `utils.ts`](https://github.com/typescript-eslint/examples/blob/main/packages/eslint-plugin-example-typed-linting/src/utils.ts):
54+
55+
```ts
56+
import { ESLintUtils } from '@typescript-eslint/utils';
57+
58+
export interface ExamplePluginDocs {
59+
description: string;
60+
recommended?: boolean;
61+
requiresTypeChecking?: boolean;
62+
}
63+
64+
export const createRule = ESLintUtils.RuleCreator<ExamplePluginDocs>(
65+
name =>
66+
`https://github.com/your/eslint-plugin-example/tree/main/docs/${name}.md`,
67+
);
68+
```
69+
70+
## Type Checking and Configs
71+
72+
Most ESLint plugins export a _"`recommended`"_ [ESLint shared config](https://eslint.org/docs/latest/extend/shareable-configs).
73+
Many ESLint users assume enabling a plugin's `recommended` config is enough to enable all its relevant rules.
74+
75+
However, at the same time, not all users want to or are able to enabled typed linting.
76+
If your plugin's rules heavily use type information, it might be difficult to enable those in a `recommended` config.
77+
78+
You have roughly two options:
79+
80+
- Have your plugin's `recommended` config require enabling type information
81+
- Have a separate config with a name like `recommendedTypeChecked`
82+
83+
Either way, explicitly mention the strategy taken in your docs.
84+
85+
:::info
86+
Per [_Custom Rules_ > _Conditional Type Information_](./Custom_Rules.mdx#conditional-type-information), we recommend not changing rule logic based on whether type information is available.
87+
:::
88+
89+
:::tip
90+
See [**`eslint-plugin-example-typed-linting`**](https://github.com/typescript-eslint/examples/tree/main/packages/eslint-plugin-example-typed-linting) for an example plugin that supports typed linting.
91+
:::

docs/packages/Rule_Tester.mdx

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,11 @@ ruleTester.run('my-rule', rule, {
3636
// you can enable JSX parsing by passing parserOptions.ecmaFeatures.jsx = true
3737
{
3838
code: 'const z = <div />;',
39-
parserOptions: {
40-
ecmaFeatures: {
41-
jsx: true,
39+
languageOptions: {
40+
parserOptions: {
41+
ecmaFeatures: {
42+
jsx: true,
43+
},
4244
},
4345
},
4446
},
@@ -120,26 +122,11 @@ ruleTester.run('my-rule', rule, {
120122

121123
### Type-Aware Testing
122124

123-
Type-aware rules can be tested in almost exactly the same way, except you need to create some files on disk.
124-
We require files on disk due to a limitation with TypeScript in that it requires physical files on disk to initialize the project.
125-
We suggest creating a `fixture` folder nearby that contains three files:
126-
127-
1. `file.ts` - this should be an empty file.
128-
2. `react.tsx` - this should be an empty file.
129-
3. `tsconfig.json` - this should be the config to use for your test, for example:
130-
```json
131-
{
132-
"compilerOptions": {
133-
"strict": true
134-
},
135-
"include": ["file.ts", "react.tsx"]
136-
}
137-
```
138-
139-
:::caution
140-
It's important to note that both `file.ts` and `react.tsx` must both be empty files!
141-
The rule tester will automatically use the string content from your tests - the empty files are just there for initialization.
142-
:::
125+
Type-aware rules can be tested in almost exactly the same way as regular code, using `parserOptions.projectService`.
126+
Most rule tests can use settings like:
127+
128+
- `allowDefaultProjectForFiles: ["*.ts*"]`: to include files in your tests
129+
- `defaultProject: "tsconfig.json"`: to use the same TSConfig as other files
143130

144131
You can then test your rule by providing the type-aware config:
145132

@@ -148,8 +135,11 @@ const ruleTester = new RuleTester({
148135
// Added lines start
149136
languageOptions: {
150137
parserOptions: {
138+
projectServices: {
139+
allowDefaultProject: ['*.ts*'],
140+
defaultProject: 'tsconfig.json',
141+
},
151142
tsconfigRootDir: './path/to/your/folder/fixture',
152-
project: './tsconfig.json',
153143
},
154144
},
155145
// Added lines end
@@ -158,6 +148,11 @@ const ruleTester = new RuleTester({
158148

159149
With that config the parser will automatically run in type-aware mode and you can write tests just like before.
160150

151+
When not specified with a `filename` option, `RuleTester` uses the following test file names:
152+
153+
- `file.ts`: by default
154+
- `react.tsx`: if `parserOptions.ecmaFeatures.jsx` is enabled
155+
161156
### Test Dependency Constraints
162157

163158
Sometimes it's desirable to test your rule against multiple versions of a dependency to ensure backwards and forwards compatibility.

packages/website/sidebars/sidebar.base.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ module.exports = {
8888
},
8989
{
9090
collapsible: false,
91-
items: ['developers/custom-rules'],
91+
items: ['developers/custom-rules', 'developers/eslint-plugins'],
9292
label: 'Developers',
9393
link: {
9494
id: 'developers',

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy