Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit a61f029

Browse filesBrowse files
committed
feat: support auto-generated rule options lists
1 parent 22a0754 commit a61f029
Copy full SHA for a61f029

File tree

Expand file treeCollapse file tree

9 files changed

+593
-21
lines changed
Filter options
Expand file treeCollapse file tree

9 files changed

+593
-21
lines changed

‎README.md

Copy file name to clipboardExpand all lines: README.md
+29-1Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Generates the following documentation covering a [wide variety](#column-and-noti
99
- `README.md` rules table
1010
- `README.md` configs table
1111
- Rule doc titles and notices
12+
- Rule doc options lists
1213

1314
Also performs [configurable](#configuration-options) section consistency checks on rule docs:
1415

@@ -18,11 +19,16 @@ Also performs [configurable](#configuration-options) section consistency checks
1819

1920
- [Motivation](#motivation)
2021
- [Setup](#setup)
22+
- [Scripts](#scripts)
23+
- [Update `README.md`](#update-readmemd)
24+
- [Update rule docs](#update-rule-docs)
25+
- [Configure linting](#configure-linting)
2126
- [Usage](#usage)
2227
- [Examples](#examples)
2328
- [Rules list table](#rules-list-table)
2429
- [Configs list table](#configs-list-table)
2530
- [Rule doc notices](#rule-doc-notices)
31+
- [Rule doc options lists](#rule-doc-options-lists)
2632
- [Users](#users)
2733
- [Configuration options](#configuration-options)
2834
- [Column and notice types](#column-and-notice-types)
@@ -52,6 +58,8 @@ Install it:
5258
npm i --save-dev eslint-doc-generator
5359
```
5460

61+
### Scripts
62+
5563
Add scripts to `package.json`:
5664

5765
- Both a lint script to ensure everything is up-to-date in CI and an update script for contributors to run locally
@@ -70,30 +78,46 @@ Add scripts to `package.json`:
7078
}
7179
```
7280

81+
### Update `README.md`
82+
7383
Delete any old rules list from your `README.md`. A new one will be automatically added to your `## Rules` section (along with the following marker comments if they don't already exist):
7484

7585
```md
7686
<!-- begin auto-generated rules list -->
7787
<!-- end auto-generated rules list -->
7888
```
7989

80-
Optionally, add these marker comments to your `README.md` where you would like the configs list to go (uses the `description` property exported by each config if available):
90+
Optionally, add these marker comments to your `README.md` in a `## Configs` section or similar location (uses the `description` property exported by each config if available):
8191

8292
```md
8393
<!-- begin auto-generated configs list -->
8494
<!-- end auto-generated configs list -->
8595
```
8696

97+
### Update rule docs
98+
8799
Delete any old recommended/fixable/etc. notices from your rule docs. A new title and notices will be automatically added to the top of each rule doc (along with a marker comment if it doesn't already exist).
88100

89101
```md
90102
<!-- end auto-generated rule header -->
91103
```
92104

105+
Optionally, add these marker comments to your rule docs in an `## Options` section or similar location:
106+
107+
```md
108+
<!-- begin auto-generated rule options list -->
109+
<!-- end auto-generated rule options list -->
110+
```
111+
112+
Note that rule option lists are subject-to-change as we add support for more kinds and properties of schemas. To fully take advantage of them, you'll want to ensure your rules have the `meta.schema` property fleshed out with properties like `description`, `type`, `enum`, `default`, `required`, `deprecated`.
113+
114+
### Configure linting
115+
93116
And be sure to enable the `recommended` rules from [eslint-plugin-eslint-plugin](https://github.com/eslint-community/eslint-plugin-eslint-plugin) as well as:
94117

95118
- [eslint-plugin/require-meta-docs-description](https://github.com/eslint-community/eslint-plugin-eslint-plugin/blob/main/docs/rules/require-meta-docs-description.md) to ensure your rules have consistent descriptions for use in the generated docs
96119
- [eslint-plugin/require-meta-docs-url](https://github.com/eslint-community/eslint-plugin-eslint-plugin/blob/main/docs/rules/require-meta-docs-url.md) to ensure your rule docs are linked to by editors on highlighted violations
120+
- [eslint-plugin/require-meta-schema](https://github.com/eslint-community/eslint-plugin-eslint-plugin/blob/main/docs/rules/require-meta-schema.md) to ensure your rules have schemas for use in determining options
97121

98122
## Usage
99123

@@ -119,6 +143,10 @@ See the generated configs table in our example [`README.md`](./docs/examples/esl
119143

120144
See the generated rule doc title and notices in our example rule docs [`no-foo.md`](./docs/examples/eslint-plugin-test/docs/rules/no-foo.md), [`prefer-bar.md`](./docs/examples/eslint-plugin-test/docs/rules/prefer-bar.md), [`require-baz.md`](./docs/examples/eslint-plugin-test/docs/rules/require-baz.md).
121145

146+
### Rule doc options lists
147+
148+
See the generated rule doc options lists in our example rule doc [`no-foo.md`](./docs/examples/eslint-plugin-test/docs/rules/no-foo.md).
149+
122150
### Users
123151

124152
This tool is used by popular ESLint plugins like:

‎docs/examples/eslint-plugin-test/docs/rules/no-foo.md

Copy file name to clipboardExpand all lines: docs/examples/eslint-plugin-test/docs/rules/no-foo.md
+32-1Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,35 @@ Examples would normally go here.
2222

2323
## Options
2424

25-
Config options would normally go here.
25+
<!-- begin auto-generated rule options list -->
26+
27+
| Name | Description | Type | Choices | Default | Required | Deprecated |
28+
| :---- | :---------------------------- | :------ | :---------------- | :------- | :------- | :--------- |
29+
| `bar` | Choose how to use the rule. | String | `always`, `never` | `always` | Yes | |
30+
| `foo` | Enable some kind of behavior. | Boolean | | `false` | | Yes |
31+
32+
<!-- end auto-generated rule options list -->
33+
34+
For the purpose of this example, below is the `meta.schema` that would generate the above rule options table:
35+
36+
```json
37+
[{
38+
"type": "object",
39+
"properties": {
40+
"foo": {
41+
"type": "boolean",
42+
"description": "Enable some kind of behavior.",
43+
"deprecated": true,
44+
"default": false
45+
},
46+
"bar": {
47+
"description": "Choose how to use the rule.",
48+
"type": "string",
49+
"enum": ["always", "never"],
50+
"default": "always"
51+
}
52+
},
53+
"required": ["bar"],
54+
"additionalProperties": false
55+
}]
56+
```

‎lib/comment-markers.ts

Copy file name to clipboardExpand all lines: lib/comment-markers.ts
+6Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,9 @@ export const BEGIN_CONFIG_LIST_MARKER =
1111
'<!-- begin auto-generated configs list -->';
1212
export const END_CONFIG_LIST_MARKER =
1313
'<!-- end auto-generated configs list -->';
14+
15+
// Markers so that the rule options table list can be automatically updated.
16+
export const BEGIN_RULE_OPTIONS_LIST_MARKER =
17+
'<!-- begin auto-generated rule options list -->';
18+
export const END_RULE_OPTIONS_LIST_MARKER =
19+
'<!-- end auto-generated rule options list -->';

‎lib/generator.ts

Copy file name to clipboardExpand all lines: lib/generator.ts
+6-2Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { diff } from 'jest-diff';
2727
import type { GenerateOptions } from './types.js';
2828
import { OPTION_TYPE, RuleModule } from './types.js';
2929
import { replaceRulePlaceholder } from './rule-link.js';
30+
import { updateRuleOptionsList } from './rule-options-list.js';
3031

3132
function stringOrArrayWithFallback<T extends string | readonly string[]>(
3233
stringOrArray: undefined | T,
@@ -180,7 +181,10 @@ export async function generate(path: string, options?: GenerateOptions) {
180181

181182
const contents = readFileSync(pathToDoc).toString();
182183
const contentsNew = await postprocess(
183-
replaceOrCreateHeader(contents, newHeaderLines, END_RULE_HEADER_MARKER),
184+
updateRuleOptionsList(
185+
replaceOrCreateHeader(contents, newHeaderLines, END_RULE_HEADER_MARKER),
186+
rule
187+
),
184188
resolve(pathToDoc)
185189
);
186190

@@ -229,7 +233,7 @@ export async function generate(path: string, options?: GenerateOptions) {
229233
['Options', 'Config'],
230234
hasOptions(schema)
231235
);
232-
for (const namedOption of getAllNamedOptions(schema)) {
236+
for (const { name: namedOption } of getAllNamedOptions(schema)) {
233237
expectContentOrFail(
234238
`\`${name}\` rule doc`,
235239
'rule option',

‎lib/rule-options-list.ts

Copy file name to clipboard
+150Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import {
2+
BEGIN_RULE_OPTIONS_LIST_MARKER,
3+
END_RULE_OPTIONS_LIST_MARKER,
4+
} from './comment-markers.js';
5+
import { markdownTable } from 'markdown-table';
6+
import type { RuleModule } from './types.js';
7+
import { RuleOption, getAllNamedOptions } from './rule-options.js';
8+
import { capitalizeOnlyFirstLetter } from './string.js';
9+
10+
export enum COLUMN_TYPE {
11+
// Alphabetical order.
12+
DEFAULT = 'default',
13+
DEPRECATED = 'deprecated',
14+
DESCRIPTION = 'description',
15+
ENUM = 'enum',
16+
NAME = 'name',
17+
REQUIRED = 'required',
18+
TYPE = 'type',
19+
}
20+
21+
const HEADERS: {
22+
[key in COLUMN_TYPE]: string;
23+
} = {
24+
// Alphabetical order.
25+
[COLUMN_TYPE.DEFAULT]: 'Default',
26+
[COLUMN_TYPE.DEPRECATED]: 'Deprecated',
27+
[COLUMN_TYPE.DESCRIPTION]: 'Description',
28+
[COLUMN_TYPE.ENUM]: 'Choices',
29+
[COLUMN_TYPE.NAME]: 'Name',
30+
[COLUMN_TYPE.REQUIRED]: 'Required',
31+
[COLUMN_TYPE.TYPE]: 'Type',
32+
};
33+
34+
const COLUMN_TYPE_DEFAULT_PRESENCE_AND_ORDERING: {
35+
[key in COLUMN_TYPE]: boolean;
36+
} = {
37+
// Object keys ordered in display order.
38+
// Object values indicate whether the column is displayed by default.
39+
[COLUMN_TYPE.NAME]: true,
40+
[COLUMN_TYPE.DESCRIPTION]: true,
41+
[COLUMN_TYPE.TYPE]: true,
42+
[COLUMN_TYPE.ENUM]: true,
43+
[COLUMN_TYPE.DEFAULT]: true,
44+
[COLUMN_TYPE.REQUIRED]: true,
45+
[COLUMN_TYPE.DEPRECATED]: true,
46+
};
47+
48+
function ruleOptionToColumnValues(ruleOption: RuleOption): {
49+
[key in COLUMN_TYPE]: string | undefined;
50+
} {
51+
const columns: {
52+
[key in COLUMN_TYPE]: string | undefined;
53+
} = {
54+
// Alphabetical order.
55+
[COLUMN_TYPE.DEFAULT]:
56+
ruleOption.default === undefined
57+
? undefined
58+
: `\`${String(ruleOption.default)}\``,
59+
[COLUMN_TYPE.DEPRECATED]: ruleOption.deprecated ? 'Yes' : undefined,
60+
[COLUMN_TYPE.DESCRIPTION]: ruleOption.description,
61+
[COLUMN_TYPE.ENUM]:
62+
ruleOption.enum && ruleOption.enum.length > 0
63+
? `\`${ruleOption.enum.join('`, `')}\``
64+
: undefined,
65+
[COLUMN_TYPE.NAME]: `\`${ruleOption.name}\``,
66+
[COLUMN_TYPE.REQUIRED]: ruleOption.required ? 'Yes' : undefined,
67+
[COLUMN_TYPE.TYPE]: ruleOption.type
68+
? capitalizeOnlyFirstLetter(ruleOption.type)
69+
: undefined,
70+
};
71+
72+
return columns;
73+
}
74+
75+
function ruleOptionsToColumnsToDisplay(ruleOptions: readonly RuleOption[]): {
76+
[key in COLUMN_TYPE]: boolean;
77+
} {
78+
const columnsToDisplay: {
79+
[key in COLUMN_TYPE]: boolean;
80+
} = {
81+
// Alphabetical order.
82+
[COLUMN_TYPE.DEFAULT]: ruleOptions.some((ruleOption) => ruleOption.default),
83+
[COLUMN_TYPE.DEPRECATED]: ruleOptions.some(
84+
(ruleOption) => ruleOption.deprecated
85+
),
86+
[COLUMN_TYPE.DESCRIPTION]: ruleOptions.some(
87+
(ruleOption) => ruleOption.description
88+
),
89+
[COLUMN_TYPE.ENUM]: ruleOptions.some((ruleOption) => ruleOption.enum),
90+
[COLUMN_TYPE.NAME]: true,
91+
[COLUMN_TYPE.REQUIRED]: ruleOptions.some(
92+
(ruleOption) => ruleOption.required
93+
),
94+
[COLUMN_TYPE.TYPE]: ruleOptions.some((ruleOption) => ruleOption.type),
95+
};
96+
return columnsToDisplay;
97+
}
98+
99+
function generateRuleOptionsListMarkdown(rule: RuleModule): string {
100+
const ruleOptions = getAllNamedOptions(rule.meta.schema);
101+
102+
if (ruleOptions.length === 0) {
103+
return '';
104+
}
105+
106+
const columnsToDisplay = ruleOptionsToColumnsToDisplay(ruleOptions);
107+
const listHeaderRow = Object.keys(COLUMN_TYPE_DEFAULT_PRESENCE_AND_ORDERING)
108+
.filter((type) => columnsToDisplay[type as COLUMN_TYPE])
109+
.map((type) => HEADERS[type as COLUMN_TYPE]);
110+
111+
const rows = [...ruleOptions]
112+
.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()))
113+
.map((ruleOption) => {
114+
const ruleOptionColumnValues = ruleOptionToColumnValues(ruleOption);
115+
116+
// Recreate object using correct ordering and presence of columns.
117+
return Object.keys(COLUMN_TYPE_DEFAULT_PRESENCE_AND_ORDERING)
118+
.filter((type) => columnsToDisplay[type as COLUMN_TYPE])
119+
.map((type) => ruleOptionColumnValues[type as COLUMN_TYPE]);
120+
});
121+
122+
return markdownTable(
123+
[listHeaderRow, ...rows],
124+
{ align: 'l' } // Left-align headers.
125+
);
126+
}
127+
128+
export function updateRuleOptionsList(
129+
markdown: string,
130+
rule: RuleModule
131+
): string {
132+
const listStartIndex = markdown.indexOf(BEGIN_RULE_OPTIONS_LIST_MARKER);
133+
let listEndIndex = markdown.indexOf(END_RULE_OPTIONS_LIST_MARKER);
134+
135+
if (listStartIndex === -1 || listEndIndex === -1) {
136+
// No rule options list found.
137+
return markdown;
138+
}
139+
140+
// Account for length of pre-existing marker.
141+
listEndIndex += END_RULE_OPTIONS_LIST_MARKER.length;
142+
143+
const preList = markdown.slice(0, Math.max(0, listStartIndex));
144+
const postList = markdown.slice(Math.max(0, listEndIndex));
145+
146+
// New rule options list.
147+
const list = generateRuleOptionsListMarkdown(rule);
148+
149+
return `${preList}${BEGIN_RULE_OPTIONS_LIST_MARKER}\n\n${list}\n\n${END_RULE_OPTIONS_LIST_MARKER}${postList}`;
150+
}

‎lib/rule-options.ts

Copy file name to clipboardExpand all lines: lib/rule-options.ts
+34-5Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,28 @@
11
import traverse from 'json-schema-traverse';
22
import type { JSONSchema } from '@typescript-eslint/utils';
33

4+
export type RuleOption = {
5+
name: string;
6+
type?: string;
7+
description?: string;
8+
required?: boolean;
9+
enum?: readonly JSONSchema.JSONSchema4Type[];
10+
default?: JSONSchema.JSONSchema4Type;
11+
deprecated?: boolean;
12+
};
13+
414
/**
515
* Gather a list of named options from a rule schema.
616
* @param jsonSchema - the JSON schema to check
717
* @returns - list of named options we could detect from the schema
818
*/
919
export function getAllNamedOptions(
10-
jsonSchema: JSONSchema.JSONSchema4 | undefined | null
11-
): readonly string[] {
20+
jsonSchema:
21+
| JSONSchema.JSONSchema4
22+
| readonly JSONSchema.JSONSchema4[]
23+
| undefined
24+
| null
25+
): readonly RuleOption[] {
1226
if (!jsonSchema) {
1327
return [];
1428
}
@@ -19,10 +33,23 @@ export function getAllNamedOptions(
1933
);
2034
}
2135

22-
const options: string[] = [];
36+
const options: RuleOption[] = [];
2337
traverse(jsonSchema, (js: JSONSchema.JSONSchema4) => {
2438
if (js.properties) {
25-
options.push(...Object.keys(js.properties));
39+
options.push(
40+
...Object.entries(js.properties).map(([key, value]) => ({
41+
name: key,
42+
type: value.type ? value.type.toString() : undefined,
43+
description: value.description,
44+
default: value.default,
45+
enum: value.enum,
46+
required:
47+
typeof value.required === 'boolean'
48+
? value.required
49+
: Array.isArray(js.required) && js.required.includes(key),
50+
deprecated: value.deprecated, // eslint-disable-line @typescript-eslint/no-unsafe-assignment -- property exists on future JSONSchema version but we can let it be used anyway.
51+
}))
52+
);
2653
}
2754
});
2855
return options;
@@ -33,7 +60,9 @@ export function getAllNamedOptions(
3360
* @param jsonSchema - the JSON schema to check
3461
* @returns - whether the schema has options
3562
*/
36-
export function hasOptions(jsonSchema: JSONSchema.JSONSchema4): boolean {
63+
export function hasOptions(
64+
jsonSchema: JSONSchema.JSONSchema4 | readonly JSONSchema.JSONSchema4[]
65+
): boolean {
3766
return (
3867
(Array.isArray(jsonSchema) && jsonSchema.length > 0) ||
3968
(typeof jsonSchema === 'object' && Object.keys(jsonSchema).length > 0)

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.