eslint-doc-generator
Advanced tools
Comparing version 1.1.0 to 1.2.0
@@ -86,3 +86,10 @@ import { Command, Argument, Option } from 'commander'; | ||
ruleListColumns: schemaStringArray, | ||
ruleListSplit: { type: 'string' }, | ||
ruleListSplit: | ||
/* istanbul ignore next -- TODO: haven't tested JavaScript config files yet https://github.com/bmish/eslint-doc-generator/issues/366 */ | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access | ||
typeof explorerResults.config.ruleListSplit === 'function' | ||
? { | ||
/* Functions are allowed but JSON Schema can't validate them so no-op in this case. */ | ||
} | ||
: { anyOf: [{ type: 'string' }, schemaStringArray] }, | ||
urlConfigs: { type: 'string' }, | ||
@@ -107,13 +114,16 @@ urlRuleDoc: { type: 'string' }, | ||
// Additional validation that couldn't be handled by ajv. | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access | ||
/* eslint-disable @typescript-eslint/no-unsafe-member-access -- disabled for same reason above */ | ||
if (config.postprocess && typeof config.postprocess !== 'function') { | ||
throw new Error('postprocess must be a function'); | ||
throw new Error('postprocess must be a function.'); | ||
} | ||
// Perform any normalization. | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access | ||
if (typeof config.pathRuleList === 'string') { | ||
config.pathRuleList = [config.pathRuleList]; // eslint-disable-line @typescript-eslint/no-unsafe-member-access | ||
config.pathRuleList = [config.pathRuleList]; | ||
} | ||
if (typeof config.ruleListSplit === 'string') { | ||
config.ruleListSplit = [config.ruleListSplit]; | ||
} | ||
return explorerResults.config; | ||
} | ||
/* eslint-enable @typescript-eslint/no-unsafe-member-access */ | ||
return {}; | ||
@@ -139,3 +149,3 @@ } | ||
.option('--path-rule-doc <path>', `(optional) Path to markdown file for each rule doc. Use \`{name}\` placeholder for the rule name. (default: ${OPTION_DEFAULTS[OPTION_TYPE.PATH_RULE_DOC]})`) | ||
.option('--path-rule-list <path>', `(optional) Path to markdown file where the rules table list should live. Option can be repeated. Defaults to ${OPTION_DEFAULTS[OPTION_TYPE.PATH_RULE_LIST]} if not provided.`, collect, []) | ||
.option('--path-rule-list <path>', `(optional) Path to markdown file where the rules table list should live. Option can be repeated. Defaults to ${String(OPTION_DEFAULTS[OPTION_TYPE.PATH_RULE_LIST])} if not provided.`, collect, []) | ||
.option('--rule-doc-notices <notices>', `(optional) Ordered, comma-separated list of notices to display in rule doc. Non-applicable notices will be hidden. (choices: "${Object.values(NOTICE_TYPE).join('", "')}") (default: ${String(OPTION_DEFAULTS[OPTION_TYPE.RULE_DOC_NOTICES])})`, collectCSV, []) | ||
@@ -147,3 +157,3 @@ .option('--rule-doc-section-exclude <section>', '(optional) Disallowed section in each rule doc (option can be repeated).', collect, []) | ||
.option('--rule-list-columns <columns>', `(optional) Ordered, comma-separated list of columns to display in rule list. Empty columns will be hidden. (choices: "${Object.values(COLUMN_TYPE).join('", "')})" (default: ${String(OPTION_DEFAULTS[OPTION_TYPE.RULE_LIST_COLUMNS])})`, collectCSV, []) | ||
.option('--rule-list-split <property>', '(optional) Rule property to split the rules list by. A separate list and header will be created for each value. Example: `meta.type`.') | ||
.option('--rule-list-split <property>', '(optional) Rule property(s) to split the rules list by. A separate list and header will be created for each value. Example: `meta.type`. To specify a function, use a JavaScript-based config file.', collectCSV, []) | ||
.option('--url-configs <url>', '(optional) Link to documentation about the ESLint configurations exported by the plugin.') | ||
@@ -158,2 +168,9 @@ .option('--url-rule-doc <url>', '(optional) Link to documentation for each rule. Useful when it differs from the rule doc path on disk (e.g. custom documentation site in use). Use `{name}` placeholder for the rule name.') | ||
const generateOptions = merge(configFileOptions, options); // Recursive merge. | ||
// Options with both a CLI/config-file variant will lose the function value during the merge, so restore it here. | ||
// TODO: figure out a better way to handle this. | ||
/* istanbul ignore next -- TODO: haven't tested JavaScript config files yet https://github.com/bmish/eslint-doc-generator/issues/366 */ | ||
if (typeof configFileOptions.ruleListSplit === 'function') { | ||
// @ts-expect-error -- The array is supposed to be read-only at this point. | ||
generateOptions.ruleListSplit = configFileOptions.ruleListSplit; | ||
} | ||
// Invoke callback. | ||
@@ -160,0 +177,0 @@ await cb(path, generateOptions); |
@@ -51,2 +51,14 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; | ||
} | ||
function stringOrArrayToArrayWithFallback(stringOrArray, fallback) { | ||
const asArray = stringOrArray instanceof Array // eslint-disable-line unicorn/no-instanceof-array -- using Array.isArray() loses type information about the array. | ||
? stringOrArray | ||
: stringOrArray | ||
? [stringOrArray] | ||
: []; | ||
const csvStringItem = asArray.find((item) => item.includes(',')); | ||
if (csvStringItem) { | ||
throw new Error(`Provide property as array, not a CSV string: ${csvStringItem}`); | ||
} | ||
return asArray && asArray.length > 0 ? asArray : fallback; | ||
} | ||
// eslint-disable-next-line complexity | ||
@@ -68,3 +80,3 @@ export async function generate(path, options) { | ||
const pathRuleDoc = options?.pathRuleDoc ?? OPTION_DEFAULTS[OPTION_TYPE.PATH_RULE_DOC]; | ||
const pathRuleList = stringOrArrayWithFallback(options?.pathRuleList, OPTION_DEFAULTS[OPTION_TYPE.PATH_RULE_LIST]); | ||
const pathRuleList = stringOrArrayToArrayWithFallback(options?.pathRuleList, OPTION_DEFAULTS[OPTION_TYPE.PATH_RULE_LIST]); | ||
const postprocess = options?.postprocess ?? OPTION_DEFAULTS[OPTION_TYPE.POSTPROCESS]; | ||
@@ -79,41 +91,36 @@ const ruleDocNotices = parseRuleDocNoticesOption(options?.ruleDocNotices); | ||
const ruleListColumns = parseRuleListColumnsOption(options?.ruleListColumns); | ||
const ruleListSplit = options?.ruleListSplit ?? OPTION_DEFAULTS[OPTION_TYPE.RULE_LIST_SPLIT]; | ||
const ruleListSplit = typeof options?.ruleListSplit === 'function' | ||
? options.ruleListSplit | ||
: stringOrArrayToArrayWithFallback(options?.ruleListSplit, OPTION_DEFAULTS[OPTION_TYPE.RULE_LIST_SPLIT]); | ||
const urlConfigs = options?.urlConfigs ?? OPTION_DEFAULTS[OPTION_TYPE.URL_CONFIGS]; | ||
const urlRuleDoc = options?.urlRuleDoc ?? OPTION_DEFAULTS[OPTION_TYPE.URL_RULE_DOC]; | ||
// Gather details about rules. | ||
const ruleDetails = Object.entries(plugin.rules) | ||
.map(([name, rule]) => { | ||
return typeof rule === 'object' | ||
? // Object-style rule. | ||
{ | ||
name, | ||
description: rule.meta?.docs?.description, | ||
fixable: rule.meta?.fixable | ||
? ['code', 'whitespace'].includes(rule.meta.fixable) | ||
: false, | ||
hasSuggestions: rule.meta?.hasSuggestions ?? false, | ||
requiresTypeChecking: rule.meta?.docs?.requiresTypeChecking ?? false, | ||
deprecated: rule.meta?.deprecated ?? false, | ||
schema: rule.meta?.schema, | ||
type: rule.meta?.type, | ||
} | ||
: // Deprecated function-style rule (does not support most of these features). | ||
{ | ||
name, | ||
description: undefined, | ||
fixable: false, | ||
hasSuggestions: false, | ||
requiresTypeChecking: false, | ||
deprecated: false, | ||
schema: [], | ||
type: undefined, | ||
}; | ||
// Gather 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, | ||
// @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. | ||
(ruleDetails) => !ignoreDeprecatedRules || !ruleDetails.deprecated) | ||
.sort(({ name: a }, { name: b }) => a.toLowerCase().localeCompare(b.toLowerCase())); | ||
([, rule]) => !ignoreDeprecatedRules || !rule.meta.deprecated) | ||
.sort(([a], [b]) => a.toLowerCase().localeCompare(b.toLowerCase())); | ||
// Update rule doc for each rule. | ||
let initializedRuleDoc = false; | ||
for (const { name, description, schema } of ruleDetails) { | ||
for (const [name, rule] of ruleNamesAndRules) { | ||
const schema = rule.meta?.schema; | ||
const description = rule.meta?.docs?.description; | ||
const pathToDoc = replaceRulePlaceholder(join(path, pathRuleDoc), name); | ||
@@ -162,6 +169,3 @@ if (!existsSync(pathToDoc)) { | ||
} | ||
// eslint-disable-next-line unicorn/no-instanceof-array -- using Array.isArray() loses type information about the array. | ||
for (const pathRuleListItem of pathRuleList instanceof Array | ||
? pathRuleList | ||
: [pathRuleList]) { | ||
for (const pathRuleListItem of pathRuleList) { | ||
// Find the exact filename. | ||
@@ -174,3 +178,3 @@ const pathToFile = getPathWithExactFileNameCasing(join(path, pathRuleListItem)); | ||
const fileContents = readFileSync(pathToFile, 'utf8'); | ||
const fileContentsNew = await postprocess(updateRulesList(ruleDetails, fileContents, plugin, configsToRules, pluginPrefix, pathRuleDoc, pathToFile, path, configEmojis, ignoreConfig, ruleListColumns, ruleListSplit, urlConfigs, urlRuleDoc), resolve(pathToFile)); | ||
const fileContentsNew = await postprocess(updateRulesList(ruleNamesAndRules, fileContents, plugin, configsToRules, pluginPrefix, pathRuleDoc, pathToFile, path, configEmojis, ignoreConfig, ruleListColumns, ruleListSplit, urlConfigs, urlRuleDoc), resolve(pathToFile)); | ||
if (check) { | ||
@@ -177,0 +181,0 @@ if (fileContentsNew !== fileContents) { |
@@ -6,3 +6,3 @@ import { COLUMN_TYPE, NOTICE_TYPE } from './types.js'; | ||
*/ | ||
export declare function parseConfigEmojiOptions(plugin: Plugin, configEmoji?: readonly (readonly string[])[]): ConfigEmojis; | ||
export declare function parseConfigEmojiOptions(plugin: Plugin, configEmoji?: readonly ([configName: string, emoji: string] | [configName: string])[]): ConfigEmojis; | ||
/** | ||
@@ -9,0 +9,0 @@ * Parse the option, check for errors, and set defaults. |
@@ -15,3 +15,3 @@ import { COLUMN_TYPE, NOTICE_TYPE } from './types.js'; | ||
pathRuleDoc: string; | ||
pathRuleList: string; | ||
pathRuleList: string[]; | ||
postprocess: (content: string) => string; | ||
@@ -24,5 +24,5 @@ ruleDocNotices: string[]; | ||
ruleListColumns: string[]; | ||
ruleListSplit: undefined; | ||
ruleListSplit: never[]; | ||
urlConfigs: undefined; | ||
urlRuleDoc: undefined; | ||
}; |
@@ -40,3 +40,3 @@ import { join } from 'node:path'; | ||
[OPTION_TYPE.PATH_RULE_DOC]: join('docs', 'rules', '{name}.md'), | ||
[OPTION_TYPE.PATH_RULE_LIST]: 'README.md', | ||
[OPTION_TYPE.PATH_RULE_LIST]: ['README.md'], | ||
[OPTION_TYPE.POSTPROCESS]: (content) => content, | ||
@@ -53,5 +53,5 @@ [OPTION_TYPE.RULE_DOC_NOTICES]: Object.entries(NOTICE_TYPE_DEFAULT_PRESENCE_AND_ORDERING) | ||
.map(([col]) => col), | ||
[OPTION_TYPE.RULE_LIST_SPLIT]: undefined, | ||
[OPTION_TYPE.RULE_LIST_SPLIT]: [], | ||
[OPTION_TYPE.URL_CONFIGS]: undefined, | ||
[OPTION_TYPE.URL_RULE_DOC]: undefined, | ||
}; // Satisfies is used to ensure all options are included, but without losing type information. |
import { COLUMN_TYPE } from './types.js'; | ||
import type { RuleDetails, ConfigsToRules, Plugin } from './types.js'; | ||
import type { ConfigsToRules, Plugin, RuleNamesAndRules } from './types.js'; | ||
/** | ||
@@ -8,3 +8,3 @@ * An object containing the column header for each column (as a string or function to generate the string). | ||
[key in COLUMN_TYPE]: string | ((data: { | ||
ruleDetails: readonly RuleDetails[]; | ||
ruleNamesAndRules: RuleNamesAndRules; | ||
}) => string); | ||
@@ -16,2 +16,2 @@ }; | ||
*/ | ||
export declare function getColumns(plugin: Plugin, ruleDetails: readonly RuleDetails[], configsToRules: ConfigsToRules, ruleListColumns: readonly COLUMN_TYPE[], pluginPrefix: string, ignoreConfig: readonly string[]): Record<COLUMN_TYPE, boolean>; | ||
export declare function getColumns(plugin: Plugin, ruleNamesAndRules: RuleNamesAndRules, configsToRules: ConfigsToRules, ruleListColumns: readonly COLUMN_TYPE[], pluginPrefix: string, ignoreConfig: readonly string[]): Record<COLUMN_TYPE, boolean>; |
@@ -10,6 +10,6 @@ import { EMOJI_DEPRECATED, EMOJI_FIXABLE, EMOJI_HAS_SUGGESTIONS, EMOJI_REQUIRES_TYPE_CHECKING, EMOJI_TYPE, EMOJI_CONFIG_FROM_SEVERITY, EMOJI_OPTIONS, } from './emojis.js'; | ||
export const COLUMN_HEADER = { | ||
[COLUMN_TYPE.NAME]: ({ ruleDetails }) => { | ||
const ruleNames = ruleDetails.map((ruleDetail) => ruleDetail.name); | ||
[COLUMN_TYPE.NAME]: ({ ruleNamesAndRules }) => { | ||
const ruleNames = ruleNamesAndRules.map(([name]) => name); | ||
const longestRuleNameLength = Math.max(...ruleNames.map(({ length }) => length)); | ||
const ruleDescriptions = ruleDetails.map((ruleDetail) => ruleDetail.description); | ||
const ruleDescriptions = ruleNamesAndRules.map(([, rule]) => rule.meta?.docs?.description); | ||
const longestRuleDescriptionLength = Math.max(...ruleDescriptions.map((description) => description ? description.length : 0)); | ||
@@ -43,3 +43,3 @@ const title = 'Name'; | ||
*/ | ||
export function getColumns(plugin, ruleDetails, configsToRules, ruleListColumns, pluginPrefix, ignoreConfig) { | ||
export function getColumns(plugin, ruleNamesAndRules, configsToRules, ruleListColumns, pluginPrefix, ignoreConfig) { | ||
const columns = { | ||
@@ -50,12 +50,12 @@ // Alphabetical order. | ||
[COLUMN_TYPE.CONFIGS_WARN]: getConfigsThatSetARule(plugin, configsToRules, pluginPrefix, ignoreConfig, SEVERITY_TYPE.warn).length > 0, | ||
[COLUMN_TYPE.DEPRECATED]: ruleDetails.some((ruleDetail) => ruleDetail.deprecated), | ||
[COLUMN_TYPE.DESCRIPTION]: ruleDetails.some((ruleDetail) => ruleDetail.description), | ||
[COLUMN_TYPE.FIXABLE]: ruleDetails.some((ruleDetail) => ruleDetail.fixable), | ||
[COLUMN_TYPE.FIXABLE_AND_HAS_SUGGESTIONS]: ruleDetails.some((ruleDetail) => ruleDetail.fixable || ruleDetail.hasSuggestions), | ||
[COLUMN_TYPE.HAS_SUGGESTIONS]: ruleDetails.some((ruleDetail) => ruleDetail.hasSuggestions), | ||
[COLUMN_TYPE.DEPRECATED]: ruleNamesAndRules.some(([, rule]) => rule.meta?.deprecated), | ||
[COLUMN_TYPE.DESCRIPTION]: ruleNamesAndRules.some(([, rule]) => rule.meta?.docs?.description), | ||
[COLUMN_TYPE.FIXABLE]: ruleNamesAndRules.some(([, rule]) => rule.meta?.fixable), | ||
[COLUMN_TYPE.FIXABLE_AND_HAS_SUGGESTIONS]: ruleNamesAndRules.some(([, rule]) => rule.meta?.fixable || rule.meta?.hasSuggestions), | ||
[COLUMN_TYPE.HAS_SUGGESTIONS]: ruleNamesAndRules.some(([, rule]) => rule.meta?.hasSuggestions), | ||
[COLUMN_TYPE.NAME]: true, | ||
[COLUMN_TYPE.OPTIONS]: ruleDetails.some((ruleDetail) => hasOptions(ruleDetail.schema)), | ||
[COLUMN_TYPE.REQUIRES_TYPE_CHECKING]: ruleDetails.some((ruleDetail) => ruleDetail.requiresTypeChecking), | ||
[COLUMN_TYPE.OPTIONS]: ruleNamesAndRules.some(([, rule]) => hasOptions(rule.meta?.schema)), | ||
[COLUMN_TYPE.REQUIRES_TYPE_CHECKING]: ruleNamesAndRules.some(([, rule]) => rule.meta?.docs?.requiresTypeChecking), | ||
// Show type column only if we found at least one rule with a standard type. | ||
[COLUMN_TYPE.TYPE]: ruleDetails.some((ruleDetail) => ruleDetail.type && RULE_TYPES.includes(ruleDetail.type)), | ||
[COLUMN_TYPE.TYPE]: ruleNamesAndRules.some(([, rule]) => rule.meta?.type && RULE_TYPES.includes(rule.meta?.type)), | ||
}; | ||
@@ -62,0 +62,0 @@ // Recreate object using the ordering and presence of columns specified in ruleListColumns. |
@@ -60,3 +60,3 @@ import { EMOJI_DEPRECATED, EMOJI_FIXABLE, EMOJI_HAS_SUGGESTIONS, EMOJI_OPTIONS, EMOJI_REQUIRES_TYPE_CHECKING, EMOJI_TYPE, EMOJI_CONFIG_FROM_SEVERITY, } from './emojis.js'; | ||
for (const ruleType of RULE_TYPES) { | ||
const hasThisRuleType = Object.values(rules).some((rule) => typeof rule === 'object' && rule.meta.type === ruleType); | ||
const hasThisRuleType = Object.values(rules).some((rule) => typeof rule === 'object' && rule.meta?.type === ruleType); | ||
if (hasThisRuleType) { | ||
@@ -63,0 +63,0 @@ if (!hasAnyRuleType) { |
@@ -1,3 +0,3 @@ | ||
import { COLUMN_TYPE } from './types.js'; | ||
import type { Plugin, RuleDetails, ConfigsToRules, ConfigEmojis } from './types.js'; | ||
export declare function updateRulesList(ruleDetails: readonly RuleDetails[], markdown: string, plugin: Plugin, configsToRules: ConfigsToRules, pluginPrefix: string, pathRuleDoc: string, pathRuleList: string, pathPlugin: string, configEmojis: ConfigEmojis, ignoreConfig: readonly string[], ruleListColumns: readonly COLUMN_TYPE[], ruleListSplit?: string, urlConfigs?: string, urlRuleDoc?: string): string; | ||
import { COLUMN_TYPE, RuleListSplitFunction } from './types.js'; | ||
import type { Plugin, ConfigsToRules, ConfigEmojis, RuleNamesAndRules } from './types.js'; | ||
export declare function updateRulesList(ruleNamesAndRules: RuleNamesAndRules, markdown: string, plugin: Plugin, configsToRules: ConfigsToRules, pluginPrefix: string, pathRuleDoc: string, pathRuleList: string, pathPlugin: string, configEmojis: ConfigEmojis, ignoreConfig: readonly string[], ruleListColumns: readonly COLUMN_TYPE[], ruleListSplit: readonly string[] | RuleListSplitFunction, urlConfigs?: string, urlRuleDoc?: string): string; |
@@ -9,3 +9,3 @@ import { BEGIN_RULE_LIST_MARKER, END_RULE_LIST_MARKER, } from './comment-markers.js'; | ||
import { relative } from 'node:path'; | ||
import { COLUMN_TYPE, SEVERITY_TYPE } from './types.js'; | ||
import { COLUMN_TYPE, SEVERITY_TYPE, } from './types.js'; | ||
import { markdownTable } from 'markdown-table'; | ||
@@ -19,2 +19,3 @@ import { EMOJIS_TYPE } from './rule-type.js'; | ||
import { boolean, isBooleanable } from 'boolean'; | ||
import Ajv from 'ajv'; | ||
function isBooleanableTrue(value) { | ||
@@ -40,28 +41,28 @@ return isBooleanable(value) && boolean(value); | ||
} | ||
function getConfigurationColumnValueForRule(rule, configsToRules, pluginPrefix, configEmojis, ignoreConfig, severityType) { | ||
function getConfigurationColumnValueForRule(ruleName, configsToRules, pluginPrefix, configEmojis, ignoreConfig, severityType) { | ||
const configsToRulesWithoutIgnored = Object.fromEntries(Object.entries(configsToRules).filter(([configName]) => !ignoreConfig?.includes(configName))); | ||
// Collect the emojis for the configs that set the rule to this severity level. | ||
return getEmojisForConfigsSettingRuleToSeverity(rule.name, configsToRulesWithoutIgnored, pluginPrefix, configEmojis, severityType).join(' '); | ||
return getEmojisForConfigsSettingRuleToSeverity(ruleName, configsToRulesWithoutIgnored, pluginPrefix, configEmojis, severityType).join(' '); | ||
} | ||
function buildRuleRow(columnsEnabled, rule, configsToRules, pluginPrefix, pathPlugin, pathRuleDoc, pathRuleList, configEmojis, ignoreConfig, urlRuleDoc) { | ||
function buildRuleRow(ruleName, rule, columnsEnabled, configsToRules, pluginPrefix, pathPlugin, pathRuleDoc, pathRuleList, configEmojis, ignoreConfig, urlRuleDoc) { | ||
const columns = { | ||
// Alphabetical order. | ||
[COLUMN_TYPE.CONFIGS_ERROR]: getConfigurationColumnValueForRule(rule, configsToRules, pluginPrefix, configEmojis, ignoreConfig, SEVERITY_TYPE.error), | ||
[COLUMN_TYPE.CONFIGS_OFF]: getConfigurationColumnValueForRule(rule, configsToRules, pluginPrefix, configEmojis, ignoreConfig, SEVERITY_TYPE.off), | ||
[COLUMN_TYPE.CONFIGS_WARN]: getConfigurationColumnValueForRule(rule, configsToRules, pluginPrefix, configEmojis, ignoreConfig, SEVERITY_TYPE.warn), | ||
[COLUMN_TYPE.DEPRECATED]: rule.deprecated ? EMOJI_DEPRECATED : '', | ||
[COLUMN_TYPE.DESCRIPTION]: rule.description || '', | ||
[COLUMN_TYPE.FIXABLE]: rule.fixable ? EMOJI_FIXABLE : '', | ||
[COLUMN_TYPE.FIXABLE_AND_HAS_SUGGESTIONS]: `${rule.fixable ? EMOJI_FIXABLE : ''}${rule.hasSuggestions ? EMOJI_HAS_SUGGESTIONS : ''}`, | ||
[COLUMN_TYPE.HAS_SUGGESTIONS]: rule.hasSuggestions | ||
[COLUMN_TYPE.CONFIGS_ERROR]: getConfigurationColumnValueForRule(ruleName, configsToRules, pluginPrefix, configEmojis, ignoreConfig, SEVERITY_TYPE.error), | ||
[COLUMN_TYPE.CONFIGS_OFF]: getConfigurationColumnValueForRule(ruleName, configsToRules, pluginPrefix, configEmojis, ignoreConfig, SEVERITY_TYPE.off), | ||
[COLUMN_TYPE.CONFIGS_WARN]: getConfigurationColumnValueForRule(ruleName, configsToRules, pluginPrefix, configEmojis, ignoreConfig, SEVERITY_TYPE.warn), | ||
[COLUMN_TYPE.DEPRECATED]: rule.meta?.deprecated ? EMOJI_DEPRECATED : '', | ||
[COLUMN_TYPE.DESCRIPTION]: rule.meta?.docs?.description || '', | ||
[COLUMN_TYPE.FIXABLE]: rule.meta?.fixable ? EMOJI_FIXABLE : '', | ||
[COLUMN_TYPE.FIXABLE_AND_HAS_SUGGESTIONS]: `${rule.meta?.fixable ? EMOJI_FIXABLE : ''}${rule.meta?.hasSuggestions ? EMOJI_HAS_SUGGESTIONS : ''}`, | ||
[COLUMN_TYPE.HAS_SUGGESTIONS]: rule.meta?.hasSuggestions | ||
? EMOJI_HAS_SUGGESTIONS | ||
: '', | ||
[COLUMN_TYPE.NAME]() { | ||
return getLinkToRule(rule.name, pluginPrefix, pathPlugin, pathRuleDoc, pathRuleList, false, false, urlRuleDoc); | ||
return getLinkToRule(ruleName, pluginPrefix, pathPlugin, pathRuleDoc, pathRuleList, false, false, urlRuleDoc); | ||
}, | ||
[COLUMN_TYPE.OPTIONS]: hasOptions(rule.schema) ? EMOJI_OPTIONS : '', | ||
[COLUMN_TYPE.REQUIRES_TYPE_CHECKING]: rule.requiresTypeChecking | ||
[COLUMN_TYPE.OPTIONS]: hasOptions(rule.meta?.schema) ? EMOJI_OPTIONS : '', | ||
[COLUMN_TYPE.REQUIRES_TYPE_CHECKING]: rule.meta?.docs?.requiresTypeChecking | ||
? EMOJI_REQUIRES_TYPE_CHECKING | ||
: '', | ||
[COLUMN_TYPE.TYPE]: rule.type ? EMOJIS_TYPE[rule.type] : '', | ||
[COLUMN_TYPE.TYPE]: rule.meta?.type ? EMOJIS_TYPE[rule.meta?.type] : '', | ||
}; | ||
@@ -80,3 +81,3 @@ // List columns using the ordering and presence of columns specified in columnsEnabled. | ||
} | ||
function generateRulesListMarkdown(columns, ruleDetails, configsToRules, pluginPrefix, pathPlugin, pathRuleDoc, pathRuleList, configEmojis, ignoreConfig, urlRuleDoc) { | ||
function generateRulesListMarkdown(ruleNamesAndRules, columns, configsToRules, pluginPrefix, pathPlugin, pathRuleDoc, pathRuleList, configEmojis, ignoreConfig, urlRuleDoc) { | ||
const listHeaderRow = Object.entries(columns).flatMap(([columnType, enabled]) => { | ||
@@ -89,3 +90,3 @@ if (!enabled) { | ||
typeof headerStrOrFn === 'function' | ||
? headerStrOrFn({ ruleDetails }) | ||
? headerStrOrFn({ ruleNamesAndRules }) | ||
: headerStrOrFn, | ||
@@ -96,46 +97,77 @@ ]; | ||
listHeaderRow, | ||
...ruleDetails.map((rule) => buildRuleRow(columns, rule, configsToRules, pluginPrefix, pathPlugin, pathRuleDoc, pathRuleList, configEmojis, ignoreConfig, urlRuleDoc)), | ||
...ruleNamesAndRules.map(([name, rule]) => buildRuleRow(name, rule, columns, configsToRules, pluginPrefix, pathPlugin, pathRuleDoc, pathRuleList, configEmojis, ignoreConfig, urlRuleDoc)), | ||
], { align: 'l' } // Left-align headers. | ||
); | ||
} | ||
function generateRuleListMarkdownForRulesAndHeaders(rulesAndHeaders, headerLevel, columns, configsToRules, pluginPrefix, pathPlugin, pathRuleDoc, pathRuleList, configEmojis, ignoreConfig, urlRuleDoc) { | ||
const parts = []; | ||
for (const { title, rules } of rulesAndHeaders) { | ||
if (title) { | ||
parts.push(`${'#'.repeat(headerLevel)} ${title}`); | ||
} | ||
parts.push(generateRulesListMarkdown(rules, columns, configsToRules, pluginPrefix, pathPlugin, pathRuleDoc, pathRuleList, configEmojis, ignoreConfig, urlRuleDoc)); | ||
} | ||
return parts.join('\n\n'); | ||
} | ||
/** | ||
* Generate multiple rule lists given the `ruleListSplit` property. | ||
* Get the pairs of rules and headers for a given split property. | ||
*/ | ||
function generateRulesListMarkdownWithRuleListSplit(columns, ruleDetails, plugin, configsToRules, pluginPrefix, pathPlugin, pathRuleDoc, pathRuleList, configEmojis, ignoreConfig, ruleListSplit, headerLevel, urlRuleDoc) { | ||
const values = new Set(ruleDetails.map((ruleDetail) => getPropertyFromRule(plugin, ruleDetail.name, ruleListSplit))); | ||
const valuesAll = [...values.values()]; | ||
if (values.size === 1 && isConsideredFalse(valuesAll[0])) { | ||
throw new Error(`No rules found with --rule-list-split property "${ruleListSplit}".`); | ||
function getRulesAndHeadersForSplit(ruleNamesAndRules, plugin, ruleListSplit) { | ||
const rulesAndHeaders = []; | ||
// Initially, all rules are unused. | ||
let unusedRules = ruleNamesAndRules; | ||
// Loop through each split property. | ||
for (const ruleListSplitItem of ruleListSplit) { | ||
// Store the rules and headers for this split property. | ||
const rulesAndHeadersForThisSplit = []; | ||
// Check what possible values this split property can have. | ||
const valuesForThisPropertyFromUnusedRules = [ | ||
...new Set(unusedRules.map(([name]) => getPropertyFromRule(plugin, name, ruleListSplitItem))).values(), | ||
]; | ||
const valuesForThisPropertyFromAllRules = [ | ||
...new Set(ruleNamesAndRules.map(([name]) => getPropertyFromRule(plugin, name, ruleListSplitItem))).values(), | ||
]; | ||
// Throw an exception if there are no possible rules with this split property. | ||
if (valuesForThisPropertyFromAllRules.length === 1 && | ||
isConsideredFalse(valuesForThisPropertyFromAllRules[0])) { | ||
throw new Error(`No rules found with --rule-list-split property "${ruleListSplitItem}".`); | ||
} | ||
// For each possible non-disabled value, show a header and list of corresponding rules. | ||
const valuesNotFalseAndNotTrue = valuesForThisPropertyFromUnusedRules.filter((val) => !isConsideredFalse(val) && !isBooleanableTrue(val)); | ||
const valuesTrue = valuesForThisPropertyFromUnusedRules.filter((val) => isBooleanableTrue(val)); | ||
const valuesNew = [ | ||
...valuesNotFalseAndNotTrue, | ||
...(valuesTrue.length > 0 ? [true] : []), // If there are multiple true values, combine them all into one. | ||
]; | ||
for (const value of valuesNew.sort((a, b) => String(a).toLowerCase().localeCompare(String(b).toLowerCase()))) { | ||
// Rules with the property set to this value. | ||
const rulesForThisValue = unusedRules.filter(([name]) => { | ||
const property = getPropertyFromRule(plugin, name, ruleListSplitItem); | ||
return (property === value || (value === true && isBooleanableTrue(property))); | ||
}); | ||
// Turn ruleListSplit into a title. | ||
// E.g. meta.docs.requiresTypeChecking to "Requires Type Checking". | ||
const ruleListSplitParts = ruleListSplitItem.split('.'); | ||
const ruleListSplitFinalPart = ruleListSplitParts[ruleListSplitParts.length - 1]; | ||
const ruleListSplitTitle = noCase(ruleListSplitFinalPart, { | ||
transform: (str) => capitalizeOnlyFirstLetter(str), | ||
}); | ||
// Add a list for the rules with property set to this value. | ||
rulesAndHeadersForThisSplit.push({ | ||
title: String(isBooleanableTrue(value) ? ruleListSplitTitle : value), | ||
rules: rulesForThisValue, | ||
}); | ||
// Remove these rules from the unused rules. | ||
unusedRules = unusedRules.filter((rule) => !rulesForThisValue.includes(rule)); | ||
} | ||
// Add the rules and headers for this split property to the beginning of the list of all rules and headers. | ||
rulesAndHeaders.unshift(...rulesAndHeadersForThisSplit); | ||
} | ||
const parts = []; | ||
// Show any rules that don't have a value for this rule-list-split property first, or for which the boolean property is off. | ||
if (valuesAll.some((val) => isConsideredFalse(val))) { | ||
const rulesForThisValue = ruleDetails.filter((ruleDetail) => isConsideredFalse(getPropertyFromRule(plugin, ruleDetail.name, ruleListSplit))); | ||
parts.push(generateRulesListMarkdown(columns, rulesForThisValue, configsToRules, pluginPrefix, pathPlugin, pathRuleDoc, pathRuleList, configEmojis, ignoreConfig, urlRuleDoc)); | ||
// All remaining unused rules go at the beginning. | ||
if (unusedRules.length > 0) { | ||
rulesAndHeaders.unshift({ rules: unusedRules }); | ||
} | ||
// For each possible non-disabled value, show a header and list of corresponding rules. | ||
const valuesNotFalseAndNotTrue = valuesAll.filter((val) => !isConsideredFalse(val) && !isBooleanableTrue(val)); | ||
const valuesTrue = valuesAll.filter((val) => isBooleanableTrue(val)); | ||
const valuesNew = [ | ||
...valuesNotFalseAndNotTrue, | ||
...(valuesTrue.length > 0 ? [true] : []), // If there are multiple true values, combine them all into one. | ||
]; | ||
for (const value of valuesNew.sort((a, b) => String(a).toLowerCase().localeCompare(String(b).toLowerCase()))) { | ||
const rulesForThisValue = ruleDetails.filter((ruleDetail) => { | ||
const property = getPropertyFromRule(plugin, ruleDetail.name, ruleListSplit); | ||
return (property === value || (value === true && isBooleanableTrue(property))); | ||
}); | ||
// Turn ruleListSplit into a title. | ||
// E.g. meta.docs.requiresTypeChecking to "Requires Type Checking". | ||
const ruleListSplitParts = ruleListSplit.split('.'); | ||
const ruleListSplitFinalPart = ruleListSplitParts[ruleListSplitParts.length - 1]; | ||
const ruleListSplitTitle = noCase(ruleListSplitFinalPart, { | ||
transform: (str) => capitalizeOnlyFirstLetter(str), | ||
}); | ||
parts.push(`${'#'.repeat(headerLevel)} ${isBooleanableTrue(value) ? ruleListSplitTitle : value // eslint-disable-line @typescript-eslint/restrict-template-expressions -- TODO: better handling to ensure value is a string. | ||
}`, generateRulesListMarkdown(columns, rulesForThisValue, configsToRules, pluginPrefix, pathPlugin, pathRuleDoc, pathRuleList, configEmojis, ignoreConfig, urlRuleDoc)); | ||
} | ||
return parts.join('\n\n'); | ||
return rulesAndHeaders; | ||
} | ||
export function updateRulesList(ruleDetails, markdown, plugin, configsToRules, pluginPrefix, pathRuleDoc, pathRuleList, pathPlugin, configEmojis, ignoreConfig, ruleListColumns, ruleListSplit, urlConfigs, urlRuleDoc) { | ||
export function updateRulesList(ruleNamesAndRules, markdown, plugin, configsToRules, pluginPrefix, pathRuleDoc, pathRuleList, pathPlugin, configEmojis, ignoreConfig, ruleListColumns, ruleListSplit, urlConfigs, urlRuleDoc) { | ||
let listStartIndex = markdown.indexOf(BEGIN_RULE_LIST_MARKER); | ||
@@ -171,11 +203,62 @@ let listEndIndex = markdown.indexOf(END_RULE_LIST_MARKER); | ||
// Determine columns to include in the rules list. | ||
const columns = getColumns(plugin, ruleDetails, configsToRules, ruleListColumns, pluginPrefix, ignoreConfig); | ||
const columns = getColumns(plugin, ruleNamesAndRules, configsToRules, ruleListColumns, pluginPrefix, ignoreConfig); | ||
// New legend. | ||
const legend = generateLegend(columns, plugin, configsToRules, configEmojis, pluginPrefix, ignoreConfig, urlConfigs); | ||
// Determine the pairs of rules and headers based on any split property. | ||
const rulesAndHeaders = []; | ||
if (typeof ruleListSplit === 'function') { | ||
const userDefinedLists = ruleListSplit(ruleNamesAndRules); | ||
// Schema for the user-defined lists. | ||
const schema = { | ||
// Array of rule lists. | ||
type: 'array', | ||
items: { | ||
type: 'object', | ||
properties: { | ||
title: { type: 'string' }, | ||
rules: { | ||
type: 'array', | ||
items: { | ||
type: 'array', | ||
items: [ | ||
{ type: 'string' }, | ||
{ type: 'object' }, // The rule object (won't bother trying to validate deeper than this). | ||
], | ||
minItems: 2, | ||
maxItems: 2, | ||
}, | ||
minItems: 1, | ||
uniqueItems: true, | ||
}, | ||
}, | ||
required: ['rules'], | ||
additionalProperties: false, | ||
}, | ||
minItems: 1, | ||
uniqueItems: true, | ||
}; | ||
// Validate the user-defined lists. | ||
const ajv = new Ajv(); | ||
const validate = ajv.compile(schema); | ||
const valid = validate(userDefinedLists); | ||
if (!valid) { | ||
throw new Error(validate.errors | ||
? ajv.errorsText(validate.errors, { | ||
dataVar: 'ruleListSplit return value', | ||
}) | ||
: /* istanbul ignore next -- this shouldn't happen */ | ||
'Invalid ruleListSplit return value'); | ||
} | ||
rulesAndHeaders.push(...userDefinedLists); | ||
} | ||
else if (ruleListSplit.length > 0) { | ||
rulesAndHeaders.push(...getRulesAndHeadersForSplit(ruleNamesAndRules, plugin, ruleListSplit)); | ||
} | ||
else { | ||
rulesAndHeaders.push({ rules: ruleNamesAndRules }); | ||
} | ||
// New rule list. | ||
const list = ruleListSplit | ||
? generateRulesListMarkdownWithRuleListSplit(columns, ruleDetails, plugin, configsToRules, pluginPrefix, pathPlugin, pathRuleDoc, pathRuleList, configEmojis, ignoreConfig, ruleListSplit, ruleListSplitHeaderLevel, urlRuleDoc) | ||
: generateRulesListMarkdown(columns, ruleDetails, configsToRules, pluginPrefix, pathPlugin, pathRuleDoc, pathRuleList, configEmojis, ignoreConfig, urlRuleDoc); | ||
const list = generateRuleListMarkdownForRulesAndHeaders(rulesAndHeaders, ruleListSplitHeaderLevel, columns, configsToRules, pluginPrefix, pathPlugin, pathRuleDoc, pathRuleList, configEmojis, ignoreConfig, urlRuleDoc); | ||
const newContent = `${legend ? `${legend}\n\n` : ''}${list}`; | ||
return `${preList}${BEGIN_RULE_LIST_MARKER}\n\n${newContent}\n\n${END_RULE_LIST_MARKER}${postList}`; | ||
} |
import type { RuleDocTitleFormat } from './rule-doc-title-format.js'; | ||
import type { TSESLint, JSONSchema } from '@typescript-eslint/utils'; | ||
import type { RULE_TYPE } from './rule-type.js'; | ||
import type { TSESLint } from '@typescript-eslint/utils'; | ||
export type RuleModule = TSESLint.RuleModule<string, readonly unknown[]>; | ||
@@ -21,15 +20,12 @@ export type Rules = TSESLint.Linter.RulesRecord; | ||
export type ConfigsToRules = Record<string, Rules>; | ||
export interface RuleDetails { | ||
name: string; | ||
description?: string; | ||
fixable: boolean; | ||
hasSuggestions: boolean; | ||
requiresTypeChecking: boolean; | ||
deprecated: boolean; | ||
schema: JSONSchema.JSONSchema4; | ||
type?: `${RULE_TYPE}`; | ||
} | ||
/** | ||
* Some configs may have an emoji defined. | ||
* List of rules in the form of tuples (rule name and the actual rule). | ||
*/ | ||
export type RuleNamesAndRules = readonly (readonly [ | ||
name: string, | ||
rule: RuleModule | ||
])[]; | ||
/** | ||
* The emoji for each config that has one after option parsing and defaults have been applied. | ||
*/ | ||
export type ConfigEmojis = readonly { | ||
@@ -92,2 +88,12 @@ config: string; | ||
} | ||
/** | ||
* Function for splitting the rule list into multiple sections. | ||
* Can be provided via a JavaScript-based config file using the `ruleListSplit` option. | ||
* @param rules - all rules from the plugin | ||
* @returns an array of sections, each with a title (optional) and list of rules | ||
*/ | ||
export type RuleListSplitFunction = (rules: RuleNamesAndRules) => readonly { | ||
title?: string; | ||
rules: RuleNamesAndRules; | ||
}[]; | ||
/** The type for the config file (e.g. `.eslint-doc-generatorrc.js`) and internal `generate()` function. */ | ||
@@ -103,3 +109,3 @@ export type GenerateOptions = { | ||
*/ | ||
readonly configEmoji?: readonly (readonly string[])[]; | ||
readonly configEmoji?: readonly ([configName: string, emoji: string] | [configName: string])[]; | ||
/** Configs to ignore from being displayed. Often used for an `all` config. */ | ||
@@ -118,3 +124,3 @@ readonly ignoreConfig?: readonly string[]; | ||
* Useful for applying custom transformations such as formatting with tools like prettier. | ||
* Only available via a JavaScript config file. | ||
* Only available via a JavaScript-based config file. | ||
*/ | ||
@@ -125,3 +131,3 @@ readonly postprocess?: (content: string, pathToFile: string) => string | Promise<string>; | ||
* Non-applicable notices will be hidden. | ||
* Choices: `configs`, `deprecated`, `fixable` (off by default), `fixableAndHasSuggestions`, `hasSuggestions` (off by default), `options` (off by default), `requiresTypeChecking`, `type` (off by default). | ||
* Choices: `configs`, `deprecated`, `description` (off by default), `fixable` (off by default), `fixableAndHasSuggestions`, `hasSuggestions` (off by default), `options` (off by default), `requiresTypeChecking`, `type` (off by default). | ||
* Default: `['deprecated', 'configs', 'fixableAndHasSuggestions', 'requiresTypeChecking']`. | ||
@@ -146,7 +152,7 @@ */ | ||
/** | ||
* Rule property to split the rules list by. | ||
* Rule property(s) or function to split the rules list by. | ||
* A separate list and header will be created for each value. | ||
* Example: `meta.type`. | ||
*/ | ||
readonly ruleListSplit?: string; | ||
readonly ruleListSplit?: string | readonly string[] | RuleListSplitFunction; | ||
/** Link to documentation about the ESLint configurations exported by the plugin. */ | ||
@@ -153,0 +159,0 @@ readonly urlConfigs?: string; |
{ | ||
"name": "eslint-doc-generator", | ||
"version": "1.1.0", | ||
"version": "1.2.0", | ||
"description": "Automatic documentation generator for ESLint plugins and rules.", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
@@ -145,3 +145,3 @@ # eslint-doc-generator<!-- omit from toc --> | ||
| `--rule-list-columns` | Ordered, comma-separated list of columns to display in rule list. Empty columns will be hidden. See choices in below [table](#column-and-notice-types). Default: `name,description,configsError,configsWarn,configsOff,fixable,hasSuggestions,requiresTypeChecking,deprecated`. | | ||
| `--rule-list-split` | Rule property to split the rules list by. A separate list and header will be created for each value. Example: `meta.type`. | | ||
| `--rule-list-split` | Rule property(s) to split the rules list by. A separate list and header will be created for each value. Example: `meta.type`. A function can also be provided for this option via a [config file](#configuration-file). | | ||
| `--url-configs` | Link to documentation about the ESLint configurations exported by the plugin. | | ||
@@ -191,4 +191,7 @@ | `--url-rule-doc` | Link to documentation for each rule. Useful when it differs from the rule doc path on disk (e.g. custom documentation site in use). Use `{name}` placeholder for the rule name. | | ||
Using a JavaScript-based config file also allows you to provide a `postprocess` function to be called with the generated content and file path for each processed file. This is useful for applying custom transformations such as formatting with tools like prettier (see [prettier example](#prettier)). | ||
Some options are exclusive to a JavaScript-based config file: | ||
- `postprocess` - A function-only option useful for applying custom transformations such as formatting with tools like prettier. See [prettier example](#prettier). | ||
- [`ruleListSplit`](#configuration-options) with a function - This is useful for customizing the grouping of rules into lists. | ||
Example `.eslint-doc-generatorrc.js`: | ||
@@ -205,2 +208,28 @@ | ||
Example `.eslint-doc-generatorrc.js` with `ruleListSplit` function: | ||
```js | ||
/** @type {import('eslint-doc-generator').GenerateOptions} */ | ||
const config = { | ||
ruleListSplit(rules) { | ||
return [ | ||
{ | ||
// No header for this list. | ||
rules: rules.filter(([name, rule]) => !rule.meta.someProp), | ||
}, | ||
{ | ||
title: 'Foo', | ||
rules: rules.filter(([name, rule]) => rule.meta.someProp === 'foo'), | ||
}, | ||
{ | ||
title: 'Bar', | ||
rules: rules.filter(([name, rule]) => rule.meta.someProp === 'bar'), | ||
}, | ||
]; | ||
}, | ||
}; | ||
module.exports = config; | ||
``` | ||
### Badges | ||
@@ -207,0 +236,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
128405
2211
310