Skip to content

New: require-meta-docs-url (fixes #55) #56

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ extends:
root: true
rules:
require-jsdoc: error
self/require-meta-docs-url: off
self/report-message-format:
- error
- '^[^a-z].*\.$'
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Name | ✔️ | 🛠 | Description
[prefer-placeholders](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-placeholders.md) | | | disallow template literals as report messages
[prefer-replace-text](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-replace-text.md) | | | prefer using replaceText instead of replaceTextRange.
[report-message-format](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/report-message-format.md) | | | enforce a consistent format for rule report messages
[require-meta-docs-url](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-docs-url.md) | | 🛠 | require rules to implement a meta.docs.url property
[require-meta-fixable](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-fixable.md) | ✔️ | | require rules to implement a meta.fixable property
[test-case-property-ordering](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/test-case-property-ordering.md) | | 🛠 | Requires the properties of a test case to be placed in a consistent order
[test-case-shorthand-strings](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/test-case-shorthand-strings.md) | | 🛠 | Enforce consistent usage of shorthand strings for test cases with no options
Expand Down
150 changes: 150 additions & 0 deletions docs/rules/require-meta-docs-url.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# require rules to implement a meta.docs.url property (require-meta-docs-url)

`meta.docs.url` property is the official location to store a URL to their documentation in the rule metadata.
Some integration tools will show the URL to users to understand rules.

## Rule Details

This rule aims to require ESLint rules to have a `meta.docs.url` property.

This rule has an option.

```json
{
"eslint-plugin/require-meta-docs-url": ["error", {
"pattern": "https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/{{name}}.md"
}]
}
```

- `pattern` (`string`) ... A pattern to enforce rule's document URL. It replaces `{{name}}` placeholder by each rule name. The rule name is the basename of each rule file. Default is undefined.

If you set the `pattern` option, this rule adds `meta.docs.url` property automatically when you executed `eslint --fix` command.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: I think this should say "when you execute the eslint --fix command".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, I fixed it!


The following patterns are considered warnings:

```js

/* eslint eslint-plugin/require-meta-docs-url: "error" */

module.exports = {
meta: {},
create(context) {
}
};

```

```js

/* eslint eslint-plugin/require-meta-docs-url: "error" */

module.exports = {
meta: {
docs: {
url: undefined
}
},
create(context) {
}
};

```

```js

/* eslint eslint-plugin/require-meta-docs-url: ["error", {"pattern": "path/to/{{name}}.md"}] */

module.exports = {
meta: {
docs: {
url: "wrong URL"
}
},
create(context) {
}
};

```

The following patterns are not warnings:

```js

/* eslint eslint-plugin/require-meta-docs-url: "error" */

module.exports = {
meta: {
docs: {
url: "a URL"
}
},
create(context) {
}
};

```

```js

/* eslint eslint-plugin/require-meta-docs-url: ["error", {"pattern": "path/to/{{name}}.md"}] */

module.exports = {
meta: {
docs: {
url: "path/to/rule-name.md"
}
},
create(context) {
}
};

```

## Version specific URL

If you want to enforce version-specific URLs, it's feasible easily with `.eslintrc.js` and `npm version <type>` script.
For example:

**.eslintrc.js**:

```js
"use strict"

const version = require("./package.json").version

module.exports = {
plugins: ["eslint-plugin"],
// ... leaving out ...
rules: {
"eslint-plugin/require-meta-docs-url": ["error", {
pattern: `path/to/v${version}/docs/rules/{{name}}.md`,
}],
}
}
```

**package.json**:

```json
{
"version": "1.0.0",
"scripts": {
"pretest": "eslint .",
"test": "... leaving out ...",
"preversion": "npm test",
"version": "eslint . --fix && git add ."
},
// ... leaving out ...
}
```

Then `npm version <type>` command will update every rule to the new version's URL.

> npm runs `preversion` script on the current version, runs `version` script on the new version, and commits and makes a tag.
>
> Further reading: https://docs.npmjs.com/cli/version

## When Not To Use It

If you do not plan to provide rule's documentation in website, you can turn off this rule.
139 changes: 139 additions & 0 deletions lib/rules/require-meta-docs-url.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/**
* @author Toru Nagashima <https://github.com/mysticatea>
*/

'use strict';

// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------

const path = require('path');
const util = require('../utils');

// -----------------------------------------------------------------------------
// Rule Definition
// -----------------------------------------------------------------------------

module.exports = {
meta: {
docs: {
description: 'require rules to implement a meta.docs.url property',
category: 'Rules',
recommended: false,
},
fixable: 'code',
schema: [{
type: 'object',
properties: {
pattern: { type: 'string' },
},
additionalProperties: false,
}],
},

/**
* Creates AST event handlers for require-meta-docs-url.
* @param {RuleContext} context - The rule context.
* @returns {Object} AST event handlers.
*/
create (context) {
const options = context.options[0] || {};
const sourceCode = context.getSourceCode();
const filename = context.getFilename();
const ruleName = filename === '<input>' ? undefined : path.basename(filename, '.js');
const expectedUrl = !options.pattern || !ruleName
? undefined
: options.pattern.replace(/{{\s*name\s*}}/g, ruleName);

/**
* Check whether a given node is the expected URL.
* @param {Node} node The node of property value to check.
* @returns {boolean} `true` if the node is the expected URL.
*/
function isExpectedUrl (node) {
return Boolean(
node &&
node.type === 'Literal' &&
typeof node.value === 'string' &&
(
expectedUrl === undefined ||
node.value === expectedUrl
)
);
}

/**
* Insert a given property into a given object literal.
* @param {SourceCodeFixer} fixer The fixer.
* @param {Node} node The ObjectExpression node to insert a property.
* @param {string} propertyText The property code to insert.
* @returns {void}
*/
function insertProperty (fixer, node, propertyText) {
if (node.properties.length === 0) {
return fixer.replaceText(node, `{\n${propertyText}\n}`);
}
return fixer.insertTextAfter(
sourceCode.getLastToken(node.properties[node.properties.length - 1]),
`,\n${propertyText}`
);
}

return {
Program (node) {
const info = util.getRuleInfo(node);
if (info === null) {
return;
}

const metaNode = info.meta;
const docsPropNode =
metaNode &&
metaNode.properties &&
metaNode.properties.find(p => p.type === 'Property' && util.getKeyName(p) === 'docs');
const urlPropNode =
docsPropNode &&
docsPropNode.value.properties &&
docsPropNode.value.properties.find(p => p.type === 'Property' && util.getKeyName(p) === 'url');

if (isExpectedUrl(urlPropNode && urlPropNode.value)) {
return;
}

context.report({
loc:
(urlPropNode && urlPropNode.value.loc) ||
(docsPropNode && docsPropNode.value.loc) ||
(metaNode && metaNode.loc) ||
node.loc.start,

message:
!urlPropNode ? 'Rules should export a `meta.docs.url` property.' :
!expectedUrl ? '`meta.docs.url` property must be a string.' :
/* otherwise */ '`meta.docs.url` property must be `{{expectedUrl}}`.',

data: {
expectedUrl,
},

fix (fixer) {
if (expectedUrl) {
const urlString = JSON.stringify(expectedUrl);
if (urlPropNode) {
return fixer.replaceText(urlPropNode.value, urlString);
}
if (docsPropNode && docsPropNode.value.type === 'ObjectExpression') {
return insertProperty(fixer, docsPropNode.value, `url: ${urlString}`);
}
if (!docsPropNode && metaNode && metaNode.type === 'ObjectExpression') {
return insertProperty(fixer, metaNode, `docs: {\nurl: ${urlString}\n}`);
}
}
return null;
},
});
},
};
},
};
Loading