Skip to content

Commit

Permalink
feat: enable using multiple properties in --rule-list-split
Browse files Browse the repository at this point in the history
  • Loading branch information
bmish committed Dec 19, 2022
1 parent 1bb2280 commit ec54e07
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 66 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ There's also a `postprocess` option that's only available via a [config file](#c
| `--rule-doc-section-options` | Whether to require an "Options" or "Config" rule doc section and mention of any named options for rules with options. Default: `true`. |
| `--rule-doc-title-format` | The format to use for rule doc titles. Defaults to `desc-parens-prefix-name`. See choices in below [table](#--rule-doc-title-format). |
| `--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`. |
| `--url-configs` | Link to documentation about the ESLint configurations exported by the plugin. |
| `--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. |

Expand Down
10 changes: 8 additions & 2 deletions lib/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ async function loadConfigFileOptions(): Promise<GenerateOptions> {
ruleDocSectionOptions: { type: 'boolean' },
ruleDocTitleFormat: { type: 'string' },
ruleListColumns: schemaStringArray,
ruleListSplit: { type: 'string' },
ruleListSplit: { anyOf: [{ type: 'string' }, schemaStringArray] },
urlConfigs: { type: 'string' },
urlRuleDoc: { type: 'string' },
};
Expand Down Expand Up @@ -140,6 +140,10 @@ async function loadConfigFileOptions(): Promise<GenerateOptions> {
if (typeof config.pathRuleList === 'string') {
config.pathRuleList = [config.pathRuleList]; // eslint-disable-line @typescript-eslint/no-unsafe-member-access
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (typeof config.ruleListSplit === 'string') {
config.ruleListSplit = [config.ruleListSplit]; // eslint-disable-line @typescript-eslint/no-unsafe-member-access
}

return explorerResults.config as GenerateOptions;
}
Expand Down Expand Up @@ -260,7 +264,9 @@ export async function run(
)
.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`.'
'(optional) Rule property(s) to split the rules list by. A separate list and header will be created for each value. Example: `meta.type`.',
collectCSV,
[]
)
.option(
'--url-configs <url>',
Expand Down
6 changes: 4 additions & 2 deletions lib/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,10 @@ export async function generate(path: string, options?: GenerateOptions) {
options?.ruleDocTitleFormat ??
OPTION_DEFAULTS[OPTION_TYPE.RULE_DOC_TITLE_FORMAT];
const ruleListColumns = parseRuleListColumnsOption(options?.ruleListColumns);
const ruleListSplit =
options?.ruleListSplit ?? OPTION_DEFAULTS[OPTION_TYPE.RULE_LIST_SPLIT];
const ruleListSplit = stringOrArrayToArrayWithFallback(
options?.ruleListSplit,
OPTION_DEFAULTS[OPTION_TYPE.RULE_LIST_SPLIT]
);
const urlConfigs =
options?.urlConfigs ?? OPTION_DEFAULTS[OPTION_TYPE.URL_CONFIGS];
const urlRuleDoc =
Expand Down
2 changes: 1 addition & 1 deletion lib/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export const OPTION_DEFAULTS = {
)
.filter(([_col, enabled]) => enabled)
.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 Record<OPTION_TYPE, unknown>; // Satisfies is used to ensure all options are included, but without losing type information.
117 changes: 61 additions & 56 deletions lib/rule-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,10 +220,7 @@ function generateRulesListMarkdown(
}

type RulesAndHeaders = { header?: string; rules: RuleNamesAndRules }[];
type RulesAndHeadersReadOnly = readonly {
header?: string;
rules: RuleNamesAndRules;
}[];
type RulesAndHeadersReadOnly = Readonly<RulesAndHeaders>;

function generateRuleListMarkdownForRulesAndHeaders(
rulesAndHeaders: RulesAndHeadersReadOnly,
Expand Down Expand Up @@ -269,66 +266,74 @@ function generateRuleListMarkdownForRulesAndHeaders(
function getRulesAndHeadersForSplit(
ruleNamesAndRules: RuleNamesAndRules,
plugin: Plugin,
ruleListSplit: string
ruleListSplit: readonly string[]
): RulesAndHeadersReadOnly {
const rulesAndHeaders: RulesAndHeaders = [];

const values = new Set(
ruleNamesAndRules.map(([name]) =>
getPropertyFromRule(plugin, name, ruleListSplit)
)
);
const valuesAll = [...values.values()];
// Initially, all rules are unused.
let unusedRules: [name: string, rule: RuleModule][] = [...ruleNamesAndRules]; // TODO: use type but readonly

if (values.size === 1 && isConsideredFalse(valuesAll[0])) {
throw new Error(
`No rules found with --rule-list-split property "${ruleListSplit}".`
for (const ruleListSplitItem of ruleListSplit) {
const rulesAndHeadersForThisSplit: RulesAndHeaders = [];

// Check what possible values this split property can have.
const values = new Set(
unusedRules.map(([name]) =>
getPropertyFromRule(plugin, name, ruleListSplitItem)
)
);
}
const valuesAll = [...values.values()];

// 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 = ruleNamesAndRules.filter(([name]) =>
isConsideredFalse(getPropertyFromRule(plugin, name, ruleListSplit))
if (values.size === 1 && isConsideredFalse(valuesAll[0])) {
// TODO: this check needs to be performed on all rules
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 = 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 = 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),
});

rulesAndHeadersForThisSplit.push({
header: String(isBooleanableTrue(value) ? ruleListSplitTitle : value),
rules: rulesForThisValue,
});

// Remove these rules from the unused rules.
unusedRules = unusedRules.filter(
(rule) => !rulesForThisValue.includes(rule)
);
}

rulesAndHeaders.push({
rules: rulesForThisValue,
});
rulesAndHeaders.unshift(...rulesAndHeadersForThisSplit);
}

// 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 = ruleNamesAndRules.filter(([name]) => {
const property = getPropertyFromRule(plugin, 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),
});

rulesAndHeaders.push({
header: String(isBooleanableTrue(value) ? ruleListSplitTitle : value),
rules: rulesForThisValue,
});
if (unusedRules.length > 0) {
rulesAndHeaders.unshift({ rules: unusedRules });
}

return rulesAndHeaders;
Expand All @@ -346,7 +351,7 @@ export function updateRulesList(
configEmojis: ConfigEmojis,
ignoreConfig: readonly string[],
ruleListColumns: readonly COLUMN_TYPE[],
ruleListSplit?: string,
ruleListSplit: readonly string[],
urlConfigs?: string,
urlRuleDoc?: string
): string {
Expand Down Expand Up @@ -414,7 +419,7 @@ export function updateRulesList(

// Determine the pairs of rules and headers based on any split property.
const rulesAndHeaders: RulesAndHeaders = [];
if (ruleListSplit) {
if (ruleListSplit.length > 0) {
rulesAndHeaders.push(
...getRulesAndHeadersForSplit(ruleNamesAndRules, plugin, ruleListSplit)
);
Expand Down
2 changes: 1 addition & 1 deletion lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export type GenerateOptions = {
* A separate list and header will be created for each value.
* Example: `meta.type`.
*/
readonly ruleListSplit?: string;
readonly ruleListSplit?: string | readonly string[];
/** Link to documentation about the ESLint configurations exported by the plugin. */
readonly urlConfigs?: string;
/**
Expand Down
18 changes: 15 additions & 3 deletions test/lib/__snapshots__/cli-test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ exports[`cli all CLI options and all config files options merges correctly, with
"hasSuggestions",
"type",
],
"ruleListSplit": "meta.docs.foo-from-cli",
"ruleListSplit": [
"meta.docs.foo-from-config-file",
"meta.docs.foo-from-cli",
],
"urlConfigs": "https://example.com/configs-url-from-cli",
"urlRuleDoc": "https://example.com/rule-doc-url-from-cli",
},
Expand Down Expand Up @@ -95,7 +98,9 @@ exports[`cli all CLI options, no config file options is called correctly 1`] = `
"ruleListColumns": [
"type",
],
"ruleListSplit": "meta.docs.foo-from-cli",
"ruleListSplit": [
"meta.docs.foo-from-cli",
],
"urlConfigs": "https://example.com/configs-url-from-cli",
"urlRuleDoc": "https://example.com/rule-doc-url-from-cli",
},
Expand Down Expand Up @@ -140,7 +145,9 @@ exports[`cli all config files options, no CLI options is called correctly 1`] =
"fixable",
"hasSuggestions",
],
"ruleListSplit": "meta.docs.foo-from-config-file",
"ruleListSplit": [
"meta.docs.foo-from-config-file",
],
"urlConfigs": "https://example.com/configs-url-from-config-file",
"urlRuleDoc": "https://example.com/rule-doc-url-from-config-file",
},
Expand All @@ -159,6 +166,7 @@ exports[`cli boolean option - false (explicit) is called correctly 1`] = `
"ruleDocSectionExclude": [],
"ruleDocSectionInclude": [],
"ruleListColumns": [],
"ruleListSplit": [],
},
]
`;
Expand All @@ -175,6 +183,7 @@ exports[`cli boolean option - true (explicit) is called correctly 1`] = `
"ruleDocSectionExclude": [],
"ruleDocSectionInclude": [],
"ruleListColumns": [],
"ruleListSplit": [],
},
]
`;
Expand All @@ -191,6 +200,7 @@ exports[`cli boolean option - true (implicit) is called correctly 1`] = `
"ruleDocSectionExclude": [],
"ruleDocSectionInclude": [],
"ruleListColumns": [],
"ruleListSplit": [],
},
]
`;
Expand All @@ -206,6 +216,7 @@ exports[`cli no options is called correctly 1`] = `
"ruleDocSectionExclude": [],
"ruleDocSectionInclude": [],
"ruleListColumns": [],
"ruleListSplit": [],
},
]
`;
Expand All @@ -226,6 +237,7 @@ exports[`cli pathRuleList as array in config file and CLI merges correctly 1`] =
"ruleDocSectionExclude": [],
"ruleDocSectionInclude": [],
"ruleListColumns": [],
"ruleListSplit": [],
},
]
`;
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,37 @@ exports[`generate (--rule-list-split) ignores case when sorting headers splits t
"
`;

exports[`generate (--rule-list-split) multiple properties splits the list by multiple properties 1`] = `
"## Rules
<!-- begin auto-generated rules list -->
💼 Configurations enabled in.\\
✅ Set in the \`recommended\` configuration.\\
❌ Deprecated.
### Hello
| Name | 💼 | ❌ |
| :----------------------------- | :- | :- |
| [no-foo](docs/rules/no-foo.md) | ✅ | |
### World
| Name | 💼 | ❌ |
| :----------------------------- | :- | :- |
| [no-biz](docs/rules/no-biz.md) | | |
### Deprecated
| Name | 💼 | ❌ |
| :----------------------------- | :- | :- |
| [no-bar](docs/rules/no-bar.md) | | ❌ |
| [no-baz](docs/rules/no-baz.md) | | ❌ |
<!-- end auto-generated rules list -->
"
`;

exports[`generate (--rule-list-split) with boolean (CONSTANT_CASE) splits the list with the right header 1`] = `
"## Rules
<!-- begin auto-generated rules list -->
Expand Down
47 changes: 47 additions & 0 deletions test/lib/generate/option-rule-list-split-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -573,4 +573,51 @@ describe('generate (--rule-list-split)', function () {
expect(readFileSync('README.md', 'utf8')).toMatchSnapshot();
});
});

describe('multiple properties', function () {
beforeEach(function () {
mockFs({
'package.json': JSON.stringify({
name: 'eslint-plugin-test',
exports: 'index.js',
type: 'module',
}),

'index.js': `
export default {
rules: {
'no-foo': { meta: { deprecated: false, docs: { category: 'Hello' } }, create(context) {} },
'no-bar': { meta: { deprecated: true, docs: { category: 'Should Not Show Since Deprecated' } }, create(context) {} },
'no-baz': { meta: { deprecated: true, docs: { category: 'Should Not Show Since Deprecated' } }, create(context) {} },
'no-biz': { meta: { deprecated: false, docs: { category: 'World' } }, create(context) {} },
},
configs: {
recommended: { rules: { 'test/no-foo': 'error' } },
}
};`,

'README.md': '## Rules\n',

'docs/rules/no-foo.md': '',
'docs/rules/no-bar.md': '',
'docs/rules/no-baz.md': '',
'docs/rules/no-biz.md': '',

// Needed for some of the test infrastructure to work.
node_modules: mockFs.load(PATH_NODE_MODULES),
});
});

afterEach(function () {
mockFs.restore();
jest.resetModules();
});

it('splits the list by multiple properties', async function () {
await generate('.', {
ruleListSplit: ['meta.deprecated', 'meta.docs.category'],
});
expect(readFileSync('README.md', 'utf8')).toMatchSnapshot();
});
});
});

0 comments on commit ec54e07

Please sign in to comment.