diff --git a/packages/eslint-plugin/tests/docs.test.ts b/packages/eslint-plugin/tests/docs.test.ts index c3f08de4bbda..705b0dfa6823 100644 --- a/packages/eslint-plugin/tests/docs.test.ts +++ b/packages/eslint-plugin/tests/docs.test.ts @@ -137,6 +137,40 @@ describe('Validating rule docs', () => { const rulesWithComplexOptions = new Set(['array-type', 'member-ordering']); + // TODO: whittle this list down to as few as possible + const rulesWithComplexOptionHeadings = new Set([ + 'ban-ts-comment', + 'ban-types', + 'consistent-type-exports', + 'consistent-type-imports', + 'explicit-function-return-type', + 'explicit-member-accessibility', + 'explicit-module-boundary-types', + 'no-base-to-string', + 'no-confusing-void-expression', + 'no-duplicate-type-constituents', + 'no-empty-interface', + 'no-explicit-any', + 'no-floating-promises', + 'no-inferrable-types', + 'no-invalid-void-type', + 'no-meaningless-void-operator', + 'no-misused-promises', + 'no-type-alias', + 'no-unnecessary-condition', + 'no-unnecessary-type-assertion', + 'parameter-properties', + 'prefer-nullish-coalescing', + 'prefer-optional-chain', + 'prefer-string-starts-ends-with', + 'promise-function-async', + 'restrict-template-expressions', + 'strict-boolean-expressions', + 'switch-exhaustiveness-check', + 'switch-exhaustiveness-check', + 'unbound-method', + ]); + it('All rules must have a corresponding rule doc', () => { const files = fs .readdirSync(docsRoot) @@ -238,22 +272,55 @@ describe('Validating rule docs', () => { !rule.meta.docs?.extendsBaseRule && rule.meta.type !== 'layout' ) { - test('each rule option should be mentioned in a heading', () => { - const headingTextAfterOptions = headings - .slice(headings.findIndex(header => header.text === 'Options')) - .map(header => header.text) - .join('\n'); + describe('rule options', () => { + const headingsAfterOptions = headings.slice( + headings.findIndex(header => header.text === 'Options'), + ); for (const schemaItem of schema) { if (schemaItem.type === 'object') { for (const property of Object.keys( schemaItem.properties as object, )) { - if (!headingTextAfterOptions.includes(`\`${property}\``)) { - throw new Error( - `At least one header should include \`${property}\`.`, + test(property, () => { + const correspondingHeadingIndex = + headingsAfterOptions.findIndex(heading => + heading.text.includes(`\`${property}\``), + ); + + if (correspondingHeadingIndex === -1) { + throw new Error( + `At least one header should include \`${property}\`.`, + ); + } + + if (rulesWithComplexOptionHeadings.has(ruleName)) { + return; + } + + const relevantChildren = tokens.slice( + tokens.indexOf( + headingsAfterOptions[correspondingHeadingIndex], + ), + tokens.indexOf( + headingsAfterOptions[correspondingHeadingIndex + 1], + ), ); - } + + for (const rawTab of [ + ``, + ``, + ]) { + if ( + !relevantChildren.some( + child => + child.type === 'html' && child.raw.includes(rawTab), + ) + ) { + throw new Error(`Missing option example tab: ${rawTab}`); + } + } + }); } } }