Skip to content

Commit

Permalink
feat(utils): allow specifying additional rule meta.docs in RuleCreator (
Browse files Browse the repository at this point in the history
#9025)

* feat(utils): [RuleCreator] require specifying additional rule meta.docs

* test fix

* Explicit start and end types

* Fix lint failures

* lil more linting
  • Loading branch information
JoshuaKGoldberg committed May 9, 2024
1 parent 07044c6 commit b1c92d4
Show file tree
Hide file tree
Showing 25 changed files with 227 additions and 126 deletions.
26 changes: 26 additions & 0 deletions docs/developers/Custom_Rules.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,32 @@ export const rule = createRule<Options, MessageIds>({
});
```

#### Extra Rule Docs Types

By default, rule `meta.docs` is allowed to contain only `description` and `url` as described in [ESLint's Custom Rules > Rule Structure docs](https://eslint.org/docs/latest/extend/custom-rules#rule-structure).
Additional docs properties may be added as a type argument to `ESLintUtils.RuleCreator`:

```ts
interface MyPluginDocs {
recommended: boolean;
}

const createRule = ESLintUtils.RuleCreator<MyPluginDocs>(
name => `https://example.com/rule/${name}`,
);

createRule({
// ...
meta: {
docs: {
description: '...',
recommended: true,
},
// ...
},
});
```

### Undocumented Rules

Although it is generally not recommended to create custom rules without documentation, if you are sure you want to do this you can use the `ESLintUtils.RuleCreator.withoutDocs` function to directly create a rule.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export default createRule({
docs: {
description:
"Enforce that rules don't use TS API properties with known bad type definitions",
recommended: 'recommended',
requiresTypeChecking: true,
},
fixable: 'code',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export default createRule({
meta: {
type: 'problem',
docs: {
recommended: 'recommended',
description: 'Disallow relative paths to internal packages',
},
messages: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export default createRule({
docs: {
description:
"Enforce that packages rules don't do `import ts from 'typescript';`",
recommended: 'recommended',
},
fixable: 'code',
schema: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export default createRule({
type: 'problem',
docs: {
description: `Enforce that eslint-plugin rules don't require anything from ${TSESTREE_NAME} or ${TYPES_NAME}`,
recommended: 'recommended',
},
fixable: 'code',
schema: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ export default createRule<Options, MessageIds>({
type: 'problem',
docs: {
description: `Enforce that eslint-plugin test snippets are correctly formatted`,
recommended: 'recommended',
requiresTypeChecking: true,
},
fixable: 'code',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export default createRule({
meta: {
type: 'problem',
docs: {
recommended: 'recommended',
description:
'Enforce consistent usage of `AST_NODE_TYPES`, `AST_TOKEN_TYPES` and `DefinitionType` enums',
},
Expand Down
6 changes: 5 additions & 1 deletion packages/eslint-plugin-internal/src/util/createRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { ESLintUtils } from '@typescript-eslint/utils';
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { version }: { version: string } = require('../../package.json');

const createRule = ESLintUtils.RuleCreator(
export interface ESLintPluginInternalDocs {
requiresTypeChecking?: true;
}

const createRule = ESLintUtils.RuleCreator<ESLintPluginInternalDocs>(
name =>
`https://github.com/typescript-eslint/typescript-eslint/blob/v${version}/packages/eslint-plugin-internal/src/rules/${name}.ts`,
);
Expand Down
8 changes: 6 additions & 2 deletions packages/eslint-plugin/rules.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,15 @@ This is likely not portable. A type annotation is necessary. ts(2742)
```
*/

import type { RuleModule } from '@typescript-eslint/utils/ts-eslint';
import type { RuleModuleWithMetaDocs } from '@typescript-eslint/utils/ts-eslint';

import type { ESLintPluginDocs, ESLintPluginRuleModule } from './src/util';

export { ESLintPluginDocs, ESLintPluginRuleModule };

export type TypeScriptESLintRules = Record<
string,
RuleModule<string, unknown[]>
RuleModuleWithMetaDocs<string, unknown[], ESLintPluginDocs>
>;
declare const rules: TypeScriptESLintRules;
// eslint-disable-next-line import/no-default-export
Expand Down
34 changes: 33 additions & 1 deletion packages/eslint-plugin/src/util/createRule.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,37 @@
import { ESLintUtils } from '@typescript-eslint/utils';
import type {
RuleModuleWithMetaDocs,
RuleRecommendation,
RuleRecommendationAcrossConfigs,
} from '@typescript-eslint/utils/ts-eslint';

export const createRule = ESLintUtils.RuleCreator(
export interface ESLintPluginDocs {
/**
* Does the rule extend (or is it based off of) an ESLint code rule?
* Alternately accepts the name of the base rule, in case the rule has been renamed.
* This is only used for documentation purposes.
*/
extendsBaseRule?: boolean | string;

/**
* If a string config name, which starting config this rule is enabled in.
* If an object, which settings it has enabled in each of those configs.
*/
recommended?: RuleRecommendation | RuleRecommendationAcrossConfigs<unknown[]>;

/**
* Does the rule require us to create a full TypeScript Program in order for it
* to type-check code. This is only used for documentation purposes.
*/
requiresTypeChecking?: boolean;
}

export const createRule = ESLintUtils.RuleCreator<ESLintPluginDocs>(
name => `https://typescript-eslint.io/rules/${name}`,
);

export type ESLintPluginRuleModule = RuleModuleWithMetaDocs<
string,
readonly unknown[],
ESLintPluginDocs
>;
4 changes: 2 additions & 2 deletions packages/eslint-plugin/src/util/getFunctionHeadLoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,8 @@ export function getFunctionHeadLoc(
sourceCode: TSESLint.SourceCode,
): TSESTree.SourceLocation {
const parent = node.parent;
let start = null;
let end = null;
let start: TSESTree.Position | null = null;
let end: TSESTree.Position | null = null;

if (
parent.type === AST_NODE_TYPES.MethodDefinition ||
Expand Down
12 changes: 5 additions & 7 deletions packages/eslint-plugin/tools/generate-breaking-changes.mts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import type { TypeScriptESLintRules } from '@typescript-eslint/eslint-plugin/use-at-your-own-risk/rules';
import type { RuleModule } from '@typescript-eslint/utils/ts-eslint';
import type {
ESLintPluginRuleModule,
TypeScriptESLintRules,
} from '@typescript-eslint/eslint-plugin/use-at-your-own-risk/rules';
import { fetch } from 'cross-fetch';
// markdown-table is ESM, hence this file needs to be `.mts`
import { markdownTable } from 'markdown-table';

type RuleModuleWithDocs = RuleModule<string, unknown[]> & {
meta: { docs: object };
};

async function main(): Promise<void> {
const rulesImport = await import('../src/rules/index.js');
/*
Expand All @@ -18,7 +16,7 @@ async function main(): Promise<void> {
*/
const rules = rulesImport.default as unknown as Record<
string,
RuleModuleWithDocs
ESLintPluginRuleModule
>;

// Annotate which rules are new since the last version
Expand Down
26 changes: 26 additions & 0 deletions packages/eslint-plugin/typings/eslint-rules.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ declare module 'eslint/lib/rules/arrow-parens' {
requireForBlockBody?: boolean;
}?,
],
unknown,
{
ArrowFunctionExpression(node: TSESTree.ArrowFunctionExpression): void;
}
Expand All @@ -67,6 +68,7 @@ declare module 'eslint/lib/rules/consistent-return' {
treatUndefinedAsUnspecified?: boolean;
}?,
],
unknown,
{
ReturnStatement(node: TSESTree.ReturnStatement): void;
'FunctionDeclaration:exit'(node: TSESTree.FunctionDeclaration): void;
Expand All @@ -92,6 +94,7 @@ declare module 'eslint/lib/rules/camelcase' {
genericType?: 'always' | 'never';
},
],
unknown,
{
Identifier(node: TSESTree.Identifier): void;
}
Expand All @@ -107,6 +110,7 @@ declare module 'eslint/lib/rules/max-params' {
| { max: number; countVoidThis?: boolean }
| { maximum: number; countVoidThis?: boolean }
)[],
unknown,
{
FunctionDeclaration(node: TSESTree.FunctionDeclaration): void;
FunctionExpression(node: TSESTree.FunctionExpression): void;
Expand All @@ -122,6 +126,7 @@ declare module 'eslint/lib/rules/no-dupe-class-members' {
const rule: TSESLint.RuleModule<
'unexpected',
[],
unknown,
{
Program(): void;
ClassBody(): void;
Expand All @@ -140,6 +145,7 @@ declare module 'eslint/lib/rules/no-dupe-args' {
const rule: TSESLint.RuleModule<
'unexpected',
[],
unknown,
{
FunctionDeclaration(node: TSESTree.FunctionDeclaration): void;
FunctionExpression(node: TSESTree.FunctionExpression): void;
Expand All @@ -158,6 +164,7 @@ declare module 'eslint/lib/rules/no-empty-function' {
allow?: string[];
},
],
unknown,
{
FunctionDeclaration(node: TSESTree.FunctionDeclaration): void;
FunctionExpression(node: TSESTree.FunctionExpression): void;
Expand All @@ -176,6 +183,7 @@ declare module 'eslint/lib/rules/no-implicit-globals' {
| 'globalVariableLeak'
| 'redeclarationOfReadonlyGlobal',
[],
unknown,
{
Program(node: TSESTree.Program): void;
}
Expand All @@ -189,6 +197,7 @@ declare module 'eslint/lib/rules/no-loop-func' {
const rule: TSESLint.RuleModule<
'unsafeRefs',
[],
unknown,
{
ArrowFunctionExpression(node: TSESTree.ArrowFunctionExpression): void;
FunctionExpression(node: TSESTree.FunctionExpression): void;
Expand All @@ -215,6 +224,7 @@ declare module 'eslint/lib/rules/no-magic-numbers' {
ignoreTypeIndexes?: boolean;
},
],
unknown,
{
Literal(node: TSESTree.Literal): void;
}
Expand All @@ -232,6 +242,7 @@ declare module 'eslint/lib/rules/no-redeclare' {
builtinGlobals?: boolean;
}?,
],
unknown,
{
ArrowFunctionExpression(node: TSESTree.ArrowFunctionExpression): void;
}
Expand All @@ -251,6 +262,7 @@ declare module 'eslint/lib/rules/no-restricted-globals' {
message?: string;
}
)[],
unknown,
{
ArrowFunctionExpression(node: TSESTree.ArrowFunctionExpression): void;
}
Expand All @@ -271,6 +283,7 @@ declare module 'eslint/lib/rules/no-shadow' {
ignoreOnInitialization?: boolean;
},
],
unknown,
{
ArrowFunctionExpression(node: TSESTree.ArrowFunctionExpression): void;
}
Expand All @@ -288,6 +301,7 @@ declare module 'eslint/lib/rules/no-undef' {
typeof?: boolean;
},
],
unknown,
{
ArrowFunctionExpression(node: TSESTree.ArrowFunctionExpression): void;
}
Expand All @@ -314,6 +328,7 @@ declare module 'eslint/lib/rules/no-unused-vars' {
destructuredArrayIgnorePattern?: string;
},
],
unknown,
{
ArrowFunctionExpression(node: TSESTree.ArrowFunctionExpression): void;
}
Expand All @@ -333,6 +348,7 @@ declare module 'eslint/lib/rules/no-unused-expressions' {
allowTaggedTemplates?: boolean;
},
],
unknown,
{
ExpressionStatement(node: TSESTree.ExpressionStatement): void;
}
Expand All @@ -353,6 +369,7 @@ declare module 'eslint/lib/rules/no-use-before-define' {
variables?: boolean;
}
)[],
unknown,
{
ArrowFunctionExpression(node: TSESTree.ArrowFunctionExpression): void;
}
Expand All @@ -375,6 +392,7 @@ declare module 'eslint/lib/rules/strict' {
| 'unnecessaryInClasses'
| 'wrap',
['function' | 'global' | 'never' | 'safe'],
unknown,
{
ArrowFunctionExpression(node: TSESTree.ArrowFunctionExpression): void;
}
Expand All @@ -388,6 +406,7 @@ declare module 'eslint/lib/rules/no-useless-constructor' {
const rule: TSESLint.RuleModule<
'noUselessConstructor',
[],
unknown,
{
MethodDefinition(node: TSESTree.MethodDefinition): void;
}
Expand All @@ -406,6 +425,7 @@ declare module 'eslint/lib/rules/init-declarations' {
ignoreForLoopInit?: boolean;
}?,
],
unknown,
{
'VariableDeclaration:exit'(node: TSESTree.VariableDeclaration): void;
}
Expand All @@ -423,6 +443,7 @@ declare module 'eslint/lib/rules/no-invalid-this' {
capIsConstructor?: boolean;
}?,
],
unknown,
{
// Common
ThisExpression(node: TSESTree.ThisExpression): void;
Expand All @@ -444,6 +465,7 @@ declare module 'eslint/lib/rules/dot-notation' {
allowIndexSignaturePropertyAccess?: boolean;
},
],
unknown,
{
MemberExpression(node: TSESTree.MemberExpression): void;
}
Expand All @@ -457,6 +479,7 @@ declare module 'eslint/lib/rules/no-loss-of-precision' {
const rule: TSESLint.RuleModule<
'noLossOfPrecision',
[],
unknown,
{
Literal(node: TSESTree.Literal): void;
}
Expand All @@ -475,6 +498,7 @@ declare module 'eslint/lib/rules/prefer-const' {
ignoreReadBeforeAssign?: boolean;
},
],
unknown,
{
'Program:exit'(node: TSESTree.Program): void;
VariableDeclaration(node: TSESTree.VariableDeclaration): void;
Expand Down Expand Up @@ -502,6 +526,7 @@ declare module 'eslint/lib/rules/prefer-destructuring' {
const rule: TSESLint.RuleModule<
'preferDestructuring',
[Option0, Option1?],
unknown,
{
VariableDeclarator(node: TSESTree.VariableDeclarator): void;
AssignmentExpression(node: TSESTree.AssignmentExpression): void;
Expand Down Expand Up @@ -556,6 +581,7 @@ declare module 'eslint/lib/rules/no-restricted-imports' {
| 'patterns'
| 'patternWithCustomMessage',
rule.ArrayOfStringOrObject | [ObjectOfPathsAndPatterns],
unknown,
rule.RuleListener
>;
export = rule;
Expand Down

0 comments on commit b1c92d4

Please sign in to comment.