UNPKG

eslint-doc-generator

Version:

Automatic documentation generator for ESLint plugins and rules.

143 lines (142 loc) 8.36 kB
import { existsSync } from 'node:fs'; import { dirname, join, relative, resolve } from 'node:path'; import { getAllNamedOptions, hasOptions } from './rule-options.js'; import { getPluginRoot, getPathWithExactFileNameCasing, } from './package-json.js'; import { updateRulesList } from './rule-list.js'; import { updateConfigsList } from './config-list.js'; import { generateRuleHeaderLines } from './rule-doc-notices.js'; import { BEGIN_RULE_OPTIONS_LIST_MARKER, END_RULE_HEADER_MARKER, END_RULE_OPTIONS_LIST_MARKER, } from './comment-markers.js'; import { replaceOrCreateHeader, expectContentOrFail, expectSectionHeaderOrFail, } from './markdown.js'; import { diff } from 'jest-diff'; import { replaceRulePlaceholder } from './rule-link.js'; import { updateRuleOptionsList } from './rule-options-list.js'; import { mkdir, readFile, writeFile } from 'node:fs/promises'; import { getContext } from './context.js'; // eslint-disable-next-line complexity export async function generate(path, userOptions) { const context = await getContext(path, userOptions); const { endOfLine, options, plugin } = context; // Destructure options that are only used in this function. Other options are passed around using // the "context" object. const { check, ignoreDeprecatedRules, initRuleDocs, pathRuleDoc, pathRuleList, postprocess, ruleDocSectionExclude, ruleDocSectionInclude, ruleDocSectionOptions, } = options; if (!plugin.rules) { throw new Error('Could not find exported `rules` object in ESLint plugin.'); } // Gather the normalized list of rules. const ruleNamesAndRules = Object.entries(plugin.rules) .map(([name, ruleModule]) => { // Convert deprecated function-style rules to object-style rules so that we don't have to handle function-style rules everywhere throughout the codebase. // @ts-expect-error -- this type unfortunately requires us to choose a `meta.type` even though the deprecated function-style rule won't have one. const ruleModuleAsObject = typeof ruleModule === 'function' ? { // Deprecated function-style rule don't support most of the properties that object-style rules support, so we'll just use the bare minimum. meta: { // @ts-expect-error -- type is missing for this property schema: ruleModule.schema, // eslint-disable-line @typescript-eslint/no-unsafe-assignment -- type is missing for this property // @ts-expect-error -- type is missing for this property deprecated: ruleModule.deprecated, // eslint-disable-line @typescript-eslint/no-unsafe-assignment -- type is missing for this property }, create: ruleModule, } : ruleModule; const tuple = [name, ruleModuleAsObject]; return tuple; }) .filter( // Filter out deprecated rules from being checked, displayed, or updated if the option is set. ([, rule]) => !ignoreDeprecatedRules || !rule.meta?.deprecated) .toSorted(([a], [b]) => a.toLowerCase().localeCompare(b.toLowerCase())); // Update rule doc for each rule. let initializedRuleDoc = false; for (const [name, rule] of ruleNamesAndRules) { const schema = rule.meta?.schema; const description = rule.meta?.docs?.description; const pathCurrentPage = replaceRulePlaceholder(pathRuleDoc, name); const pathToDoc = join(path, pathCurrentPage); const ruleHasOptions = hasOptions(schema); if (!existsSync(pathToDoc)) { if (!initRuleDocs) { throw new Error(`Could not find rule doc (run with --init-rule-docs to create): ${relative(getPluginRoot(path), pathToDoc)}`); } // Determine content for fresh rule doc, including any mandatory sections. // The rule doc header will be added later. let newRuleDocContents = [ ruleDocSectionInclude.length > 0 ? ruleDocSectionInclude .map((title) => `## ${title}`) .join(`${endOfLine}${endOfLine}`) : undefined, /* istanbul ignore next -- both branches tested but coverage has instrumentation issue with ternary in array */ ruleHasOptions ? `## Options${endOfLine}${endOfLine}${BEGIN_RULE_OPTIONS_LIST_MARKER}${endOfLine}${END_RULE_OPTIONS_LIST_MARKER}` : undefined, ] .filter((section) => section !== undefined) .join(`${endOfLine}${endOfLine}`); /* istanbul ignore next -- V8 branch coverage doesn't detect this branch is tested */ if (newRuleDocContents !== '') { newRuleDocContents = `${endOfLine}${newRuleDocContents}${endOfLine}`; } await mkdir(dirname(pathToDoc), { recursive: true }); await writeFile(pathToDoc, newRuleDocContents); initializedRuleDoc = true; } // Regenerate the header (title/notices) of each rule doc. const newHeaderLines = generateRuleHeaderLines(context, description, name); const contentsOldBuffer = await readFile(pathToDoc); const contentsOld = contentsOldBuffer.toString(); const contentsNew = await postprocess(updateRuleOptionsList(context, replaceOrCreateHeader(context, contentsOld, newHeaderLines, END_RULE_HEADER_MARKER), rule), resolve(pathToDoc)); if (check) { /* istanbul ignore next -- V8 branch coverage doesn't detect this branch is tested */ if (contentsNew !== contentsOld) { console.error(`Please run eslint-doc-generator. A rule doc is out-of-date: ${relative(getPluginRoot(path), pathToDoc)}`); console.error(diff(contentsNew, contentsOld, { expand: false })); process.exitCode = 1; } } else { await writeFile(pathToDoc, contentsNew); } // Check for potential issues with the rule doc. // Check for required sections. for (const section of ruleDocSectionInclude) { expectSectionHeaderOrFail(context, `\`${name}\` rule doc`, contentsNew, [section], true); } // Check for disallowed sections. for (const section of ruleDocSectionExclude) { expectSectionHeaderOrFail(context, `\`${name}\` rule doc`, contentsNew, [section], false); } if (ruleDocSectionOptions) { // Options section. expectSectionHeaderOrFail(context, `\`${name}\` rule doc`, contentsNew, ['Options', 'Config'], ruleHasOptions); for (const { name: namedOption } of getAllNamedOptions(schema)) { expectContentOrFail(`\`${name}\` rule doc`, 'rule option', contentsNew, namedOption, true); // Each rule option is mentioned. } } } if (initRuleDocs && !initializedRuleDoc) { throw new Error('--init-rule-docs was enabled, but no rule doc file needed to be created.'); } for (const pathRuleListItem of pathRuleList) { // Find the exact filename. const pathToFile = await getPathWithExactFileNameCasing(join(path, pathRuleListItem)); if (!pathToFile || !existsSync(pathToFile)) { throw new Error(`Could not find ${String(pathRuleList)} in ESLint plugin.`); } // Update the rules list in this file. const fileContents = await readFile(pathToFile, 'utf8'); const rulesList = updateRulesList(context, ruleNamesAndRules, fileContents, pathToFile); const fileContentsNew = await postprocess(updateConfigsList(context, rulesList), resolve(pathToFile)); if (check) { /* istanbul ignore next -- V8 branch coverage doesn't detect this branch is tested */ if (fileContentsNew !== fileContents) { console.error(`Please run eslint-doc-generator. The rules table in ${relative(getPluginRoot(path), pathToFile)} is out-of-date.`); console.error(diff(fileContentsNew, fileContents, { expand: false })); process.exitCode = 1; } } else { await writeFile(pathToFile, fileContentsNew, 'utf8'); } } }