Skip to content
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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(utils): allow specifying additional rule meta.docs in RuleCreator #9025

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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
JoshuaKGoldberg marked this conversation as resolved.
Show resolved Hide resolved
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