Skip to content

Commit

Permalink
Merge pull request #270 from bmish/multiple-rule-lists
Browse files Browse the repository at this point in the history
Allow specifying multiple rules lists with `--path-rule-list`
  • Loading branch information
bmish committed Nov 23, 2022
2 parents b4821d7 + c5a7dfa commit c82db05
Show file tree
Hide file tree
Showing 11 changed files with 190 additions and 47 deletions.
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -126,7 +126,7 @@ There's also an optional path argument if you need to point the CLI to an ESLint
| `--ignore-deprecated-rules` | Whether to ignore deprecated rules from being checked, displayed, or updated (default: `false`). |
| `--init-rule-docs` | Whether to create rule doc files if they don't yet exist (default: `false`). |
| `--path-rule-doc` | Path to markdown file for each rule doc. Use `{name}` placeholder for the rule name (default: `docs/rules/{name}.md`). |
| `--path-rule-list` | Path to markdown file with a rules section where the rules table list should live (default: `README.md`). |
| `--path-rule-list` | Path to markdown file where the rules table list should live. Default: `README.md`. Option can be repeated. |
| `--rule-doc-notices` | Ordered, comma-separated list of notices to display in rule doc. 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). Default: `deprecated,configs,fixableAndHasSuggestions,requiresTypeChecking`. |
| `--rule-doc-section-exclude` | Disallowed section in each rule doc. Exit with failure if present. Option can be repeated. |
| `--rule-doc-section-include` | Required section in each rule doc. Exit with failure if missing. Option can be repeated. |
Expand Down
14 changes: 11 additions & 3 deletions lib/cli.ts
Expand Up @@ -46,7 +46,7 @@ async function loadConfigFileOptions(): Promise<GenerateOptions> {
ignoreDeprecatedRules: { type: 'boolean' },
initRuleDocs: { type: 'boolean' },
pathRuleDoc: { type: 'string' },
pathRuleList: { type: 'string' },
pathRuleList: { anyOf: [{ type: 'string' }, schemaStringArray] },
ruleDocNotices: { type: 'string' },
ruleDocSectionExclude: schemaStringArray,
ruleDocSectionInclude: schemaStringArray,
Expand Down Expand Up @@ -138,9 +138,11 @@ export async function run(
)
.option(
'--path-rule-list <path>',
`(optional) Path to markdown file with a rules section where the rules table list should live. (default: ${
`(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(
'--rule-doc-notices <notices>',
Expand Down Expand Up @@ -203,6 +205,12 @@ export async function run(
// For this to work, we can't have any default values from the CLI options that will override the config file options (except empty arrays, as arrays will be merged).
// Default values should be handled in the callback function.
const configFileOptions = await loadConfigFileOptions();

// Perform any normalization needed ahead of merging.
if (typeof configFileOptions.pathRuleList === 'string') {
configFileOptions.pathRuleList = [configFileOptions.pathRuleList];
}

const generateOptions = merge(configFileOptions, options); // Recursive merge.

// Invoke callback.
Expand Down
2 changes: 1 addition & 1 deletion lib/comment-markers.ts
@@ -1,4 +1,4 @@
// Markers so that the README rules list can be automatically updated.
// Markers so that the rules table list can be automatically updated.
export const BEGIN_RULE_LIST_MARKER =
'<!-- begin auto-generated rules list -->';
export const END_RULE_LIST_MARKER = '<!-- end auto-generated rules list -->';
Expand Down
76 changes: 41 additions & 35 deletions lib/generator.ts
Expand Up @@ -248,43 +248,49 @@ export async function generate(path: string, options?: GenerateOptions) {
);
}

// Find the README.
const pathToReadme = getPathWithExactFileNameCasing(join(path, pathRuleList));
if (!pathToReadme || !existsSync(pathToReadme)) {
throw new Error(`Could not find ${pathRuleList} in ESLint plugin.`);
}
for (const pathRuleListItem of Array.isArray(pathRuleList)
? pathRuleList
: [pathRuleList]) {
// Find the exact filename.
const pathToFile = getPathWithExactFileNameCasing(
join(path, pathRuleListItem)
);
if (!pathToFile || !existsSync(pathToFile)) {
throw new Error(`Could not find ${pathRuleList} in ESLint plugin.`);
}

// Update the rules list in the README.
const readmeContents = readFileSync(pathToReadme, 'utf8');
const readmeContentsNew = updateRulesList(
details,
readmeContents,
plugin,
configsToRules,
pluginPrefix,
pathRuleDoc,
pathToReadme,
path,
configEmojis,
ignoreConfig,
ruleListColumns,
urlConfigs,
urlRuleDoc,
splitBy
);
// Update the rules list in this file.
const fileContents = readFileSync(pathToFile, 'utf8');
const fileContentsNew = updateRulesList(
details,
fileContents,
plugin,
configsToRules,
pluginPrefix,
pathRuleDoc,
pathToFile,
path,
configEmojis,
ignoreConfig,
ruleListColumns,
urlConfigs,
urlRuleDoc,
splitBy
);

if (check) {
if (readmeContentsNew !== readmeContents) {
console.error(
`Please run eslint-doc-generator. ${relative(
getPluginRoot(path),
pathToReadme
)} is out-of-date.`
);
console.error(diff(readmeContentsNew, readmeContents, { expand: false }));
process.exitCode = 1;
if (check) {
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 {
writeFileSync(pathToFile, fileContentsNew, 'utf8');
}
} else {
writeFileSync(pathToReadme, readmeContentsNew, 'utf8');
}
}
4 changes: 2 additions & 2 deletions lib/rule-list.ts
Expand Up @@ -300,7 +300,7 @@ export function updateRulesList(
configsToRules: ConfigsToRules,
pluginPrefix: string,
pathRuleDoc: string,
pathToReadme: string,
pathRuleList: string,
pathToPlugin: string,
configEmojis: ConfigEmojis,
ignoreConfig: string[],
Expand Down Expand Up @@ -336,7 +336,7 @@ export function updateRulesList(
throw new Error(
`${relative(
getPluginRoot(pathToPlugin),
pathToReadme
pathRuleList
)} is missing rules list markers: ${BEGIN_RULE_LIST_MARKER}${END_RULE_LIST_MARKER}`
);
}
Expand Down
2 changes: 1 addition & 1 deletion lib/types.ts
Expand Up @@ -110,7 +110,7 @@ export type GenerateOptions = {
ignoreDeprecatedRules?: boolean;
initRuleDocs?: boolean;
pathRuleDoc?: string;
pathRuleList?: string;
pathRuleList?: string | string[];
ruleDocNotices?: string;
ruleDocSectionExclude?: string[];
ruleDocSectionInclude?: string[];
Expand Down
35 changes: 32 additions & 3 deletions test/lib/__snapshots__/cli-test.ts.snap
Expand Up @@ -18,7 +18,10 @@ exports[`cli all CLI options and all config files options merges correctly, with
"ignoreDeprecatedRules": true,
"initRuleDocs": false,
"pathRuleDoc": "www.example.com/rule-doc-from-cli",
"pathRuleList": "www.example.com/rule-list-from-cli",
"pathRuleList": [
"www.example.com/rule-list-from-config-file",
"www.example.com/rule-list-from-cli",
],
"ruleDocNotices": "type",
"ruleDocSectionExclude": [
"excludedSectionFromConfigFile1",
Expand Down Expand Up @@ -57,7 +60,9 @@ exports[`cli all CLI options, no config file options is called correctly 1`] = `
"ignoreDeprecatedRules": true,
"initRuleDocs": false,
"pathRuleDoc": "www.example.com/rule-doc-from-cli",
"pathRuleList": "www.example.com/rule-list-from-cli",
"pathRuleList": [
"www.example.com/rule-list-from-cli",
],
"ruleDocNotices": "type",
"ruleDocSectionExclude": [
"excludedSectionFromCli1",
Expand Down Expand Up @@ -92,7 +97,9 @@ exports[`cli all config files options, no CLI options is called correctly 1`] =
"ignoreDeprecatedRules": true,
"initRuleDocs": true,
"pathRuleDoc": "www.example.com/rule-doc-from-config-file",
"pathRuleList": "www.example.com/rule-list-from-config-file",
"pathRuleList": [
"www.example.com/rule-list-from-config-file",
],
"ruleDocNotices": "type",
"ruleDocSectionExclude": [
"excludedSectionFromConfigFile1",
Expand All @@ -119,6 +126,7 @@ exports[`cli boolean option - false (explicit) is called correctly 1`] = `
"configEmoji": [],
"ignoreConfig": [],
"ignoreDeprecatedRules": false,
"pathRuleList": [],
"ruleDocSectionExclude": [],
"ruleDocSectionInclude": [],
},
Expand All @@ -132,6 +140,7 @@ exports[`cli boolean option - true (explicit) is called correctly 1`] = `
"configEmoji": [],
"ignoreConfig": [],
"ignoreDeprecatedRules": true,
"pathRuleList": [],
"ruleDocSectionExclude": [],
"ruleDocSectionInclude": [],
},
Expand All @@ -145,6 +154,7 @@ exports[`cli boolean option - true (implicit) is called correctly 1`] = `
"configEmoji": [],
"ignoreConfig": [],
"ignoreDeprecatedRules": true,
"pathRuleList": [],
"ruleDocSectionExclude": [],
"ruleDocSectionInclude": [],
},
Expand All @@ -157,6 +167,25 @@ exports[`cli no options is called correctly 1`] = `
{
"configEmoji": [],
"ignoreConfig": [],
"pathRuleList": [],
"ruleDocSectionExclude": [],
"ruleDocSectionInclude": [],
},
]
`;

exports[`cli pathRuleList as array in config file and CLI merges correctly 1`] = `
[
".",
{
"configEmoji": [],
"ignoreConfig": [],
"pathRuleList": [
"listFromConfigFile1.md",
"listFromConfigFile2.md",
"listFromCli1.md",
"listFromCli2.md",
],
"ruleDocSectionExclude": [],
"ruleDocSectionInclude": [],
},
Expand Down
39 changes: 39 additions & 0 deletions test/lib/cli-test.ts
Expand Up @@ -194,6 +194,45 @@ describe('cli', function () {
});
});

describe('pathRuleList as array in config file and CLI', function () {
beforeEach(function () {
mockFs({
'package.json': JSON.stringify({
name: 'eslint-plugin-test',
main: 'index.js',
type: 'module',
version: '1.0.0',
}),

'.eslint-doc-generatorrc.json': JSON.stringify({
pathRuleList: ['listFromConfigFile1.md', 'listFromConfigFile2.md'],
}),
});
});

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

it('merges correctly', async function () {
const stub = sinon.stub().resolves();
await run(
[
'node', // Path to node.
'eslint-doc-generator.js', // Path to this binary.

'--path-rule-list',
'listFromCli1.md',
'--path-rule-list',
'listFromCli2.md',
],
stub
);
expect(stub.callCount).toBe(1);
expect(stub.firstCall.args).toMatchSnapshot();
});
});

describe('boolean option - false (explicit)', function () {
it('is called correctly', async function () {
const stub = sinon.stub().resolves();
Expand Down
20 changes: 20 additions & 0 deletions test/lib/generate/__snapshots__/file-paths-test.ts.snap
Expand Up @@ -33,3 +33,23 @@ exports[`generate (file paths) missing rule doc when initRuleDocs is true create
<!-- end auto-generated rule header -->
"
`;

exports[`generate (file paths) multiple rules lists generates the documentation 1`] = `
"<!-- begin auto-generated rules list -->
| Name |
| :----------------------------- |
| [no-foo](docs/rules/no-foo.md) |
<!-- end auto-generated rules list -->"
`;

exports[`generate (file paths) multiple rules lists generates the documentation 2`] = `
"<!-- begin auto-generated rules list -->
| Name |
| :----------------------------- |
| [no-foo](docs/rules/no-foo.md) |
<!-- end auto-generated rules list -->"
`;
41 changes: 41 additions & 0 deletions test/lib/generate/file-paths-test.ts
Expand Up @@ -213,4 +213,45 @@ describe('generate (file paths)', function () {
expect(readFileSync('rules/no-foo/no-foo.md', 'utf8')).toMatchSnapshot();
});
});

describe('multiple rules lists', 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: { }, create(context) {} },
},
};`,

'rules/list1.md':
'<!-- begin auto-generated rules list --><!-- end auto-generated rules list -->',
'rules/list2.md':
'<!-- begin auto-generated rules list --><!-- end auto-generated rules list -->',
'docs/rules/no-foo.md': '',

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

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

it('generates the documentation', async function () {
await generate('.', {
pathRuleList: ['rules/list1.md', 'rules/list2.md'],
});
expect(readFileSync('rules/list1.md', 'utf8')).toMatchSnapshot();
expect(readFileSync('rules/list2.md', 'utf8')).toMatchSnapshot();
});
});
});
2 changes: 1 addition & 1 deletion test/lib/generate/option-check-test.ts
Expand Up @@ -55,7 +55,7 @@ describe('generate (--check)', function () {
]);
expect(consoleErrorStub.secondCall.args).toMatchSnapshot(); // Diff
expect(consoleErrorStub.thirdCall.args).toStrictEqual([
'Please run eslint-doc-generator. README.md is out-of-date.',
'Please run eslint-doc-generator. The rules table in README.md is out-of-date.',
]);
expect(consoleErrorStub.getCall(3).args).toMatchSnapshot(); // Diff
consoleErrorStub.restore();
Expand Down

0 comments on commit c82db05

Please sign in to comment.