diff --git a/packages/eslint-plugin/src/configs/strict-type-checked-only.ts b/packages/eslint-plugin/src/configs/strict-type-checked-only.ts index 4188f5f73465..6ab9b2cc0e53 100644 --- a/packages/eslint-plugin/src/configs/strict-type-checked-only.ts +++ b/packages/eslint-plugin/src/configs/strict-type-checked-only.ts @@ -15,7 +15,7 @@ export = { '@typescript-eslint/no-base-to-string': 'error', '@typescript-eslint/no-confusing-void-expression': 'error', '@typescript-eslint/no-duplicate-type-constituents': 'error', - '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: false }], '@typescript-eslint/no-for-in-array': 'error', 'no-implied-eval': 'off', '@typescript-eslint/no-implied-eval': 'error', @@ -43,8 +43,27 @@ export = { '@typescript-eslint/prefer-return-this-type': 'error', 'require-await': 'off', '@typescript-eslint/require-await': 'error', - '@typescript-eslint/restrict-plus-operands': 'error', - '@typescript-eslint/restrict-template-expressions': 'error', + '@typescript-eslint/restrict-plus-operands': [ + 'error', + { + allowAny: false, + allowBoolean: false, + allowNullish: false, + allowNumberAndString: false, + allowRegExp: false, + }, + ], + '@typescript-eslint/restrict-template-expressions': [ + 'error', + { + allowAny: false, + allowBoolean: false, + allowNullish: false, + allowNumber: false, + allowRegExp: false, + allowNever: false, + }, + ], '@typescript-eslint/unbound-method': 'error', }, } satisfies ClassicConfig.Config; diff --git a/packages/eslint-plugin/src/configs/strict-type-checked.ts b/packages/eslint-plugin/src/configs/strict-type-checked.ts index 836a0ec76e7c..de748c677576 100644 --- a/packages/eslint-plugin/src/configs/strict-type-checked.ts +++ b/packages/eslint-plugin/src/configs/strict-type-checked.ts @@ -11,7 +11,10 @@ export = { extends: ['./configs/base', './configs/eslint-recommended'], rules: { '@typescript-eslint/await-thenable': 'error', - '@typescript-eslint/ban-ts-comment': 'error', + '@typescript-eslint/ban-ts-comment': [ + 'error', + { minimumDescriptionLength: 10 }, + ], '@typescript-eslint/ban-types': 'error', 'no-array-constructor': 'off', '@typescript-eslint/no-array-constructor': 'error', @@ -24,7 +27,7 @@ export = { '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-extra-non-null-assertion': 'error', '@typescript-eslint/no-extraneous-class': 'error', - '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: false }], '@typescript-eslint/no-for-in-array': 'error', 'no-implied-eval': 'off', '@typescript-eslint/no-implied-eval': 'error', @@ -71,8 +74,27 @@ export = { '@typescript-eslint/prefer-ts-expect-error': 'error', 'require-await': 'off', '@typescript-eslint/require-await': 'error', - '@typescript-eslint/restrict-plus-operands': 'error', - '@typescript-eslint/restrict-template-expressions': 'error', + '@typescript-eslint/restrict-plus-operands': [ + 'error', + { + allowAny: false, + allowBoolean: false, + allowNullish: false, + allowNumberAndString: false, + allowRegExp: false, + }, + ], + '@typescript-eslint/restrict-template-expressions': [ + 'error', + { + allowAny: false, + allowBoolean: false, + allowNullish: false, + allowNumber: false, + allowRegExp: false, + allowNever: false, + }, + ], '@typescript-eslint/triple-slash-reference': 'error', '@typescript-eslint/unbound-method': 'error', '@typescript-eslint/unified-signatures': 'error', diff --git a/packages/eslint-plugin/src/configs/strict.ts b/packages/eslint-plugin/src/configs/strict.ts index e49b8cbcadc1..dbf57cc2c3f6 100644 --- a/packages/eslint-plugin/src/configs/strict.ts +++ b/packages/eslint-plugin/src/configs/strict.ts @@ -10,7 +10,10 @@ import type { ClassicConfig } from '@typescript-eslint/utils/ts-eslint'; export = { extends: ['./configs/base', './configs/eslint-recommended'], rules: { - '@typescript-eslint/ban-ts-comment': 'error', + '@typescript-eslint/ban-ts-comment': [ + 'error', + { minimumDescriptionLength: 10 }, + ], '@typescript-eslint/ban-types': 'error', 'no-array-constructor': 'off', '@typescript-eslint/no-array-constructor': 'error', diff --git a/packages/eslint-plugin/src/rules/ban-ts-comment.ts b/packages/eslint-plugin/src/rules/ban-ts-comment.ts index 5a5aeef35273..cd90e4cc6580 100644 --- a/packages/eslint-plugin/src/rules/ban-ts-comment.ts +++ b/packages/eslint-plugin/src/rules/ban-ts-comment.ts @@ -32,7 +32,10 @@ export default createRule<[Options], MessageIds>({ docs: { description: 'Disallow `@ts-` comments or require descriptions after directives', - recommended: 'recommended', + recommended: { + recommended: true, + strict: [{ minimumDescriptionLength: 10 }], + }, }, messages: { tsDirectiveComment: diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts index b3ac65296992..9e439118747f 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -50,7 +50,10 @@ export default createRule({ docs: { description: 'Require Promise-like statements to be handled appropriately', - recommended: 'recommended', + recommended: { + recommended: true, + strict: [{ ignoreVoid: false }], + }, requiresTypeChecking: true, }, hasSuggestions: true, diff --git a/packages/eslint-plugin/src/rules/restrict-plus-operands.ts b/packages/eslint-plugin/src/rules/restrict-plus-operands.ts index 9e17fa486b52..1953c15c70cb 100644 --- a/packages/eslint-plugin/src/rules/restrict-plus-operands.ts +++ b/packages/eslint-plugin/src/rules/restrict-plus-operands.ts @@ -31,7 +31,18 @@ export default createRule({ docs: { description: 'Require both operands of addition to be the same type and be `bigint`, `number`, or `string`', - recommended: 'recommended', + recommended: { + recommended: true, + strict: [ + { + allowAny: false, + allowBoolean: false, + allowNullish: false, + allowNumberAndString: false, + allowRegExp: false, + }, + ], + }, requiresTypeChecking: true, }, messages: { diff --git a/packages/eslint-plugin/src/rules/restrict-template-expressions.ts b/packages/eslint-plugin/src/rules/restrict-template-expressions.ts index 960617ab044c..cc719fe7fb7b 100644 --- a/packages/eslint-plugin/src/rules/restrict-template-expressions.ts +++ b/packages/eslint-plugin/src/rules/restrict-template-expressions.ts @@ -62,7 +62,19 @@ export default createRule({ docs: { description: 'Enforce template literal expressions to be of `string` type', - recommended: 'recommended', + recommended: { + recommended: true, + strict: [ + { + allowAny: false, + allowBoolean: false, + allowNullish: false, + allowNumber: false, + allowRegExp: false, + allowNever: false, + }, + ], + }, requiresTypeChecking: true, }, messages: { diff --git a/packages/eslint-plugin/tests/configs.test.ts b/packages/eslint-plugin/tests/configs.test.ts index 23f8d5ffd35c..db9130e18011 100644 --- a/packages/eslint-plugin/tests/configs.test.ts +++ b/packages/eslint-plugin/tests/configs.test.ts @@ -23,7 +23,9 @@ function entriesToObject(value: [string, T][]): Record { }, {}); } -function filterRules(values: Record): [string, string][] { +function filterRules( + values: Record, +): [string, string | unknown[]][] { return Object.entries(values).filter(([name]) => name.startsWith(RULE_NAME_PREFIX), ); @@ -39,7 +41,7 @@ function filterAndMapRuleConfigs({ excludeDeprecated, typeChecked, recommendations, -}: FilterAndMapRuleConfigsSettings = {}): [string, string][] { +}: FilterAndMapRuleConfigsSettings = {}): [string, unknown][] { let result = Object.entries(rules); if (excludeDeprecated) { @@ -55,16 +57,41 @@ function filterAndMapRuleConfigs({ } if (recommendations) { - result = result.filter(([, rule]) => - recommendations.includes(rule.meta.docs?.recommended), - ); + result = result.filter(([, rule]) => { + switch (typeof rule.meta.docs?.recommended) { + case 'undefined': + return false; + case 'object': + return Object.keys(rule.meta.docs.recommended).some(recommended => + recommendations.includes(recommended as RuleRecommendation), + ); + case 'string': + return recommendations.includes(rule.meta.docs.recommended); + } + }); } - return result.map(([name]) => [`${RULE_NAME_PREFIX}${name}`, 'error']); + const highestRecommendation = recommendations?.filter(Boolean).at(-1); + + return result.map(([name, rule]) => { + const customRecommendation = + highestRecommendation && + typeof rule.meta.docs?.recommended === 'object' && + rule.meta.docs.recommended[ + highestRecommendation as 'recommended' | 'strict' + ]; + + return [ + `${RULE_NAME_PREFIX}${name}`, + customRecommendation && typeof customRecommendation !== 'boolean' + ? ['error', customRecommendation[0]] + : 'error', + ]; + }); } function itHasBaseRulesOverriden( - unfilteredConfigRules: Record, + unfilteredConfigRules: Record, ): void { it('has the base rules overriden by the appropriate extension rules', () => { const ruleNames = new Set(Object.keys(unfilteredConfigRules)); @@ -166,7 +193,7 @@ describe('recommended-type-checked-only.ts', () => { }); describe('strict.ts', () => { - const unfilteredConfigRules: Record = + const unfilteredConfigRules: Record = plugin.configs.strict.rules; it('contains all strict rules, excluding type checked ones', () => { @@ -185,7 +212,7 @@ describe('strict.ts', () => { }); describe('strict-type-checked.ts', () => { - const unfilteredConfigRules: Record = + const unfilteredConfigRules: Record = plugin.configs['strict-type-checked'].rules; it('contains all strict rules', () => { @@ -202,7 +229,7 @@ describe('strict-type-checked.ts', () => { }); describe('strict-type-checked-only.ts', () => { - const unfilteredConfigRules: Record = + const unfilteredConfigRules: Record = plugin.configs['strict-type-checked-only'].rules; it('contains only type-checked strict rules', () => { @@ -221,7 +248,7 @@ describe('strict-type-checked-only.ts', () => { }); describe('stylistic.ts', () => { - const unfilteredConfigRules: Record = + const unfilteredConfigRules: Record = plugin.configs.stylistic.rules; it('contains all stylistic rules, excluding deprecated or type checked ones', () => { diff --git a/packages/repo-tools/src/generate-configs.mts b/packages/repo-tools/src/generate-configs.mts index 870b60c6de07..05ba57b30706 100644 --- a/packages/repo-tools/src/generate-configs.mts +++ b/packages/repo-tools/src/generate-configs.mts @@ -61,7 +61,10 @@ async function main(): Promise { config: PRETTIER_CONFIG_PATH, }); - type LinterConfigRules = Record; + type LinterConfigRules = Record< + string, + ClassicConfig.RuleLevel | [ClassicConfig.RuleLevel, ...unknown[]] + >; interface LinterConfig extends ClassicConfig.Config { extends?: string[] | string; @@ -93,8 +96,13 @@ async function main(): Promise { (a, b) => a[0].localeCompare(b[0]), ); - interface RuleFilter { + type GetRuleOptions = ( + rule: RuleModule, + ) => true | readonly unknown[] | undefined; + + interface ConfigRuleSettings { deprecated?: 'exclude'; + getOptions?: GetRuleOptions | undefined; typeChecked?: 'exclude' | 'include-only'; baseRuleForExtensionRule?: 'exclude'; forcedRuleLevel?: Linter.RuleLevel; @@ -106,7 +114,7 @@ async function main(): Promise { function reducer( config: LinterConfigRules, [key, value]: RuleEntry, - settings: RuleFilter = {}, + settings: ConfigRuleSettings = {}, ): LinterConfigRules { if (settings.deprecated && value.meta.deprecated) { return config; @@ -149,7 +157,14 @@ async function main(): Promise { '=', chalk.red('error'), ); - config[ruleName] = settings.forcedRuleLevel ?? 'error'; + + const ruleLevel = settings.forcedRuleLevel ?? 'error'; + const ruleOptions = settings.getOptions?.(value); + + config[ruleName] = + ruleOptions && ruleOptions !== true + ? [ruleLevel, ...ruleOptions] + : ruleLevel; return config; } @@ -247,20 +262,20 @@ async function main(): Promise { interface ExtendedConfigSettings { name: string; - filters?: RuleFilter; ruleEntries: readonly RuleEntry[]; + settings?: ConfigRuleSettings; } async function writeExtendedConfig({ - filters, name, ruleEntries, + settings, }: ExtendedConfigSettings): Promise { await writeConfig( () => ({ extends: [...CLASSIC_EXTENDS], rules: ruleEntries.reduce( - (config, entry) => reducer(config, entry, filters), + (config, entry) => reducer(config, entry, settings), {}, ), }), @@ -272,20 +287,34 @@ async function main(): Promise { ...recommendations: (RuleRecommendation | undefined)[] ): RuleEntry[] { return allRuleEntries.filter(([, rule]) => - recommendations.includes(rule.meta.docs?.recommended), + typeof rule.meta.docs?.recommended === 'object' + ? Object.keys(rule.meta.docs.recommended).some(level => + recommendations.includes(level as RuleRecommendation), + ) + : recommendations.includes(rule.meta.docs?.recommended), ); } + function createGetOptionsForLevel( + level: 'recommended' | 'strict', + ): GetRuleOptions { + return rule => + typeof rule.meta.docs?.recommended === 'object' + ? rule.meta.docs.recommended[level] + : undefined; + } + await writeExtendedConfig({ name: 'all', - filters: { + settings: { deprecated: 'exclude', }, ruleEntries: allRuleEntries, }); await writeExtendedConfig({ - filters: { + settings: { + getOptions: createGetOptionsForLevel('recommended'), typeChecked: 'exclude', }, name: 'recommended', @@ -295,10 +324,14 @@ async function main(): Promise { await writeExtendedConfig({ name: 'recommended-type-checked', ruleEntries: filterRuleEntriesTo('recommended'), + settings: { + getOptions: createGetOptionsForLevel('recommended'), + }, }); await writeExtendedConfig({ - filters: { + settings: { + getOptions: createGetOptionsForLevel('recommended'), typeChecked: 'include-only', }, name: 'recommended-type-checked-only', @@ -306,7 +339,8 @@ async function main(): Promise { }); await writeExtendedConfig({ - filters: { + settings: { + getOptions: createGetOptionsForLevel('strict'), typeChecked: 'exclude', }, name: 'strict', @@ -314,12 +348,16 @@ async function main(): Promise { }); await writeExtendedConfig({ + settings: { + getOptions: createGetOptionsForLevel('strict'), + }, name: 'strict-type-checked', ruleEntries: filterRuleEntriesTo('recommended', 'strict'), }); await writeExtendedConfig({ - filters: { + settings: { + getOptions: createGetOptionsForLevel('strict'), typeChecked: 'include-only', }, name: 'strict-type-checked-only', @@ -327,7 +365,7 @@ async function main(): Promise { }); await writeExtendedConfig({ - filters: { + settings: { typeChecked: 'exclude', }, name: 'stylistic', @@ -340,7 +378,7 @@ async function main(): Promise { }); await writeExtendedConfig({ - filters: { + settings: { typeChecked: 'include-only', }, name: 'stylistic-type-checked-only', diff --git a/packages/repo-tools/src/postinstall.mts b/packages/repo-tools/src/postinstall.mts index 5facbf0defc9..b89cb17cc867 100644 --- a/packages/repo-tools/src/postinstall.mts +++ b/packages/repo-tools/src/postinstall.mts @@ -20,7 +20,8 @@ if (process.env.SKIP_POSTINSTALL) { process.exit(0); } -void (async function (): Promise { +// eslint-disable-next-line @typescript-eslint/no-floating-promises +(async function (): Promise { // make sure we're running from the workspace root const { default: { workspaceRoot }, diff --git a/packages/rule-tester/src/utils/config-validator.ts b/packages/rule-tester/src/utils/config-validator.ts index ac90b8c21eea..ff616a0e57de 100644 --- a/packages/rule-tester/src/utils/config-validator.ts +++ b/packages/rule-tester/src/utils/config-validator.ts @@ -78,7 +78,8 @@ function validateRuleSchema( const validateRule = ruleValidators.get(rule); if (validateRule) { - void validateRule(localOptions); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + validateRule(localOptions); if (validateRule.errors) { throw new Error( validateRule.errors diff --git a/packages/typescript-eslint/src/configs/strict-type-checked-only.ts b/packages/typescript-eslint/src/configs/strict-type-checked-only.ts index e1f60b92e692..034ac9bf6c6f 100644 --- a/packages/typescript-eslint/src/configs/strict-type-checked-only.ts +++ b/packages/typescript-eslint/src/configs/strict-type-checked-only.ts @@ -23,7 +23,10 @@ export default ( '@typescript-eslint/no-base-to-string': 'error', '@typescript-eslint/no-confusing-void-expression': 'error', '@typescript-eslint/no-duplicate-type-constituents': 'error', - '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/no-floating-promises': [ + 'error', + { ignoreVoid: false }, + ], '@typescript-eslint/no-for-in-array': 'error', 'no-implied-eval': 'off', '@typescript-eslint/no-implied-eval': 'error', @@ -51,8 +54,27 @@ export default ( '@typescript-eslint/prefer-return-this-type': 'error', 'require-await': 'off', '@typescript-eslint/require-await': 'error', - '@typescript-eslint/restrict-plus-operands': 'error', - '@typescript-eslint/restrict-template-expressions': 'error', + '@typescript-eslint/restrict-plus-operands': [ + 'error', + { + allowAny: false, + allowBoolean: false, + allowNullish: false, + allowNumberAndString: false, + allowRegExp: false, + }, + ], + '@typescript-eslint/restrict-template-expressions': [ + 'error', + { + allowAny: false, + allowBoolean: false, + allowNullish: false, + allowNumber: false, + allowRegExp: false, + allowNever: false, + }, + ], '@typescript-eslint/unbound-method': 'error', }, }, diff --git a/packages/typescript-eslint/src/configs/strict-type-checked.ts b/packages/typescript-eslint/src/configs/strict-type-checked.ts index 91abadd4b563..14d30d005440 100644 --- a/packages/typescript-eslint/src/configs/strict-type-checked.ts +++ b/packages/typescript-eslint/src/configs/strict-type-checked.ts @@ -19,7 +19,10 @@ export default ( { rules: { '@typescript-eslint/await-thenable': 'error', - '@typescript-eslint/ban-ts-comment': 'error', + '@typescript-eslint/ban-ts-comment': [ + 'error', + { minimumDescriptionLength: 10 }, + ], '@typescript-eslint/ban-types': 'error', 'no-array-constructor': 'off', '@typescript-eslint/no-array-constructor': 'error', @@ -32,7 +35,10 @@ export default ( '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-extra-non-null-assertion': 'error', '@typescript-eslint/no-extraneous-class': 'error', - '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/no-floating-promises': [ + 'error', + { ignoreVoid: false }, + ], '@typescript-eslint/no-for-in-array': 'error', 'no-implied-eval': 'off', '@typescript-eslint/no-implied-eval': 'error', @@ -79,8 +85,27 @@ export default ( '@typescript-eslint/prefer-ts-expect-error': 'error', 'require-await': 'off', '@typescript-eslint/require-await': 'error', - '@typescript-eslint/restrict-plus-operands': 'error', - '@typescript-eslint/restrict-template-expressions': 'error', + '@typescript-eslint/restrict-plus-operands': [ + 'error', + { + allowAny: false, + allowBoolean: false, + allowNullish: false, + allowNumberAndString: false, + allowRegExp: false, + }, + ], + '@typescript-eslint/restrict-template-expressions': [ + 'error', + { + allowAny: false, + allowBoolean: false, + allowNullish: false, + allowNumber: false, + allowRegExp: false, + allowNever: false, + }, + ], '@typescript-eslint/triple-slash-reference': 'error', '@typescript-eslint/unbound-method': 'error', '@typescript-eslint/unified-signatures': 'error', diff --git a/packages/typescript-eslint/src/configs/strict.ts b/packages/typescript-eslint/src/configs/strict.ts index c1eb5e29cf3d..dabfa2f78a92 100644 --- a/packages/typescript-eslint/src/configs/strict.ts +++ b/packages/typescript-eslint/src/configs/strict.ts @@ -18,7 +18,10 @@ export default ( eslintRecommendedConfig(plugin, parser), { rules: { - '@typescript-eslint/ban-ts-comment': 'error', + '@typescript-eslint/ban-ts-comment': [ + 'error', + { minimumDescriptionLength: 10 }, + ], '@typescript-eslint/ban-types': 'error', 'no-array-constructor': 'off', '@typescript-eslint/no-array-constructor': 'error', diff --git a/packages/typescript-eslint/tests/configs.test.ts b/packages/typescript-eslint/tests/configs.test.ts index 28c2f5a337e1..6d8add503b4b 100644 --- a/packages/typescript-eslint/tests/configs.test.ts +++ b/packages/typescript-eslint/tests/configs.test.ts @@ -45,7 +45,7 @@ function filterAndMapRuleConfigs({ excludeDeprecated, typeChecked, recommendations, -}: FilterAndMapRuleConfigsSettings = {}): [string, string][] { +}: FilterAndMapRuleConfigsSettings = {}): [string, unknown][] { let result = Object.entries(rules); if (excludeDeprecated) { @@ -61,12 +61,37 @@ function filterAndMapRuleConfigs({ } if (recommendations) { - result = result.filter(([, rule]) => - recommendations.includes(rule.meta.docs?.recommended), - ); + result = result.filter(([, rule]) => { + switch (typeof rule.meta.docs?.recommended) { + case 'undefined': + return false; + case 'object': + return Object.keys(rule.meta.docs.recommended).some(recommended => + recommendations.includes(recommended as RuleRecommendation), + ); + case 'string': + return recommendations.includes(rule.meta.docs.recommended); + } + }); } - return result.map(([name]) => [`${RULE_NAME_PREFIX}${name}`, 'error']); + const highestRecommendation = recommendations?.filter(Boolean).at(-1); + + return result.map(([name, rule]) => { + const customRecommendation = + highestRecommendation && + typeof rule.meta.docs?.recommended === 'object' && + rule.meta.docs.recommended[ + highestRecommendation as 'recommended' | 'strict' + ]; + + return [ + `${RULE_NAME_PREFIX}${name}`, + customRecommendation && typeof customRecommendation !== 'boolean' + ? ['error', customRecommendation[0]] + : 'error', + ]; + }); } function itHasBaseRulesOverriden( diff --git a/packages/utils/src/eslint-utils/RuleCreator.ts b/packages/utils/src/eslint-utils/RuleCreator.ts index 40ebe7f49120..784585415f05 100644 --- a/packages/utils/src/eslint-utils/RuleCreator.ts +++ b/packages/utils/src/eslint-utils/RuleCreator.ts @@ -10,12 +10,15 @@ import { applyDefault } from './applyDefault'; export type { RuleListener, RuleModule }; // we automatically add the url -export type NamedCreateRuleMetaDocs = Omit; -export type NamedCreateRuleMeta = Omit< - RuleMetaData, - 'docs' -> & { - docs: NamedCreateRuleMetaDocs; +export type NamedCreateRuleMetaDocs = Omit< + RuleMetaDataDocs, + 'url' +>; +export type NamedCreateRuleMeta< + MessageIds extends string, + Options extends readonly unknown[], +> = Omit, 'docs'> & { + docs: NamedCreateRuleMetaDocs; }; export interface RuleCreateAndOptions< @@ -33,14 +36,14 @@ export interface RuleWithMeta< Options extends readonly unknown[], MessageIds extends string, > extends RuleCreateAndOptions { - meta: RuleMetaData; + meta: RuleMetaData; } export interface RuleWithMetaAndName< Options extends readonly unknown[], MessageIds extends string, > extends RuleCreateAndOptions { - meta: NamedCreateRuleMeta; + meta: NamedCreateRuleMeta; name: string; } diff --git a/packages/utils/src/ts-eslint/Rule.ts b/packages/utils/src/ts-eslint/Rule.ts index 3c654cedc64e..c0a3678a00cc 100644 --- a/packages/utils/src/ts-eslint/Rule.ts +++ b/packages/utils/src/ts-eslint/Rule.ts @@ -8,7 +8,14 @@ import type { SourceCode } from './SourceCode'; export type RuleRecommendation = 'recommended' | 'strict' | 'stylistic'; -export interface RuleMetaDataDocs { +export interface RuleRecommendationAcrossConfigs< + Options extends readonly unknown[], +> { + recommended: true; + strict: Partial; +} + +export interface RuleMetaDataDocs { /** * Concise description of the rule */ @@ -18,7 +25,7 @@ export interface RuleMetaDataDocs { * Used by the build tools to generate the recommended and strict configs. * Exclude to not include it as a recommendation. */ - recommended?: RuleRecommendation; + recommended?: RuleRecommendation | RuleRecommendationAcrossConfigs; /** * The URL of the rule's docs */ @@ -36,7 +43,10 @@ export interface RuleMetaDataDocs { extendsBaseRule?: boolean | string; } -export interface RuleMetaData { +export interface RuleMetaData< + MessageIds extends string, + Options extends readonly unknown[], +> { /** * True if the rule is deprecated, false otherwise */ @@ -44,7 +54,7 @@ export interface RuleMetaData { /** * Documentation for the rule, unnecessary for custom rules/plugins */ - docs?: RuleMetaDataDocs; + docs?: RuleMetaDataDocs; /** * The fixer category. Omit if there is no fixer */ @@ -630,7 +640,7 @@ export interface RuleModule< /** * Metadata about the rule */ - meta: RuleMetaData; + meta: RuleMetaData; /** * Function which returns an object with methods that ESLint calls to “visit” diff --git a/packages/website/plugins/generated-rule-docs/utils.ts b/packages/website/plugins/generated-rule-docs/utils.ts index e26ce248dbff..d8a2d61aad1d 100644 --- a/packages/website/plugins/generated-rule-docs/utils.ts +++ b/packages/website/plugins/generated-rule-docs/utils.ts @@ -55,8 +55,8 @@ export function getUrlForRuleTest(ruleName: string): string { throw new Error(`Could not find test file for ${ruleName}.`); } -export type RuleMetaDataWithDocs = RuleMetaData & { - docs: RuleMetaDataDocs; +export type RuleMetaDataWithDocs = RuleMetaData & { + docs: RuleMetaDataDocs; }; export type RuleModuleWithMetaDocs = RuleModule & { diff --git a/packages/website/src/components/RulesTable/index.tsx b/packages/website/src/components/RulesTable/index.tsx index 9a1235079303..dec57b97a50f 100644 --- a/packages/website/src/components/RulesTable/index.tsx +++ b/packages/website/src/components/RulesTable/index.tsx @@ -41,6 +41,8 @@ function RuleRow({ } const { fixable, hasSuggestions, type, deprecated } = rule; const { recommended, requiresTypeChecking, extendsBaseRule } = rule.docs; + const actualRecommended = + typeof recommended === 'object' ? 'recommended' : recommended; const formatting = type === 'layout'; return ( @@ -51,9 +53,9 @@ function RuleRow({
{interpolateCode(rule.docs.description)} - + {(() => { - switch (recommended) { + switch (actualRecommended) { case 'recommended': return RECOMMENDED_CONFIG_EMOJI; case 'strict': diff --git a/packages/website/src/hooks/useClipboard.ts b/packages/website/src/hooks/useClipboard.ts index c97ecf383141..d8191784f8e2 100644 --- a/packages/website/src/hooks/useClipboard.ts +++ b/packages/website/src/hooks/useClipboard.ts @@ -8,7 +8,8 @@ export function useClipboard(code: () => string): useClipboardResult { const [copied, setCopied] = useDebouncedToggle(false); const copy = useCallback(() => { - void navigator.clipboard.writeText(code()).then(() => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + navigator.clipboard.writeText(code()).then(() => { setCopied(true); }); }, [setCopied, code]); diff --git a/packages/website/src/theme/MDXComponents/RuleAttributes.tsx b/packages/website/src/theme/MDXComponents/RuleAttributes.tsx index 53da1d3fdc42..7950da8b397b 100644 --- a/packages/website/src/theme/MDXComponents/RuleAttributes.tsx +++ b/packages/website/src/theme/MDXComponents/RuleAttributes.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import Link from '@docusaurus/Link'; import type { RuleMetaDataDocs } from '@site/../utils/dist/ts-eslint/Rule'; import { useRulesMeta } from '@site/src/hooks/useRulesMeta'; @@ -20,14 +21,25 @@ const recommendations = { stylistic: [STYLISTIC_CONFIG_EMOJI, 'stylistic'], }; -type RecommendedRuleMetaDataDocs = RuleMetaDataDocs & { recommended: string }; +type MakeRequired = Omit & { + [K in Key]-?: NonNullable; +}; + +type RecommendedRuleMetaDataDocs = + MakeRequired, 'recommended'>; const isRecommendedDocs = ( - docs: RuleMetaDataDocs, -): docs is RecommendedRuleMetaDataDocs => !!docs.recommended; + docs: RuleMetaDataDocs, +): docs is RecommendedRuleMetaDataDocs => !!docs.recommended; -const getRecommendation = (docs: RecommendedRuleMetaDataDocs): string[] => { - const recommendation = recommendations[docs.recommended]; +const getRecommendation = ( + docs: RecommendedRuleMetaDataDocs, +): string[] => { + const recommended = docs.recommended; + const recommendation = + recommendations[ + typeof recommended === 'object' ? 'recommended' : recommended + ]; return docs.requiresTypeChecking ? [recommendation[0], `${recommendation[1]}-type-checked`]