Repository: not-an-aardvark/eslint-plugin-eslint-plugin (js)

Issue:
Rule proposal: require-meta-docs-url
Since https://github.com/eslint/eslint/pull/9788, `meta.docs.url` is the official place to show the link to rule documentation. It would be good if there is a rule to enforce to write the link. Additionally, it's awesome if the rule is fixable.

```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`) ... the URL pattern. Default is undefined. If this option is specified, the rule fixes `meta.docs.url` automatically. It replaces the `{{name}}` placeholder to rule name (`path.basename(context.getFilename(), ".js")`).
  

Pull request description:
New: require-meta-docs-url (fixes #55)
(Fixes #55)

This is a reference implementation of #55.

1. This rule validates `meta.docs.url` property. 
2. This rule makes `meta.docs.url` property automatically if people set `pattern` option. This would reduce the cost of plugin's contributors.

Patch:
diff --git a/.eslintrc.yml b/.eslintrc.yml
index b753f7b..ce7ec1d 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -8,6 +8,7 @@ extends:
 root: true
 rules:
   require-jsdoc: error
+  self/require-meta-docs-url: off
   self/report-message-format:
     - error
     - '^[^a-z].*\.$'
diff --git a/README.md b/README.md
index 17993af..b02bde3 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/docs/rules/require-meta-docs-url.md b/docs/rules/require-meta-docs-url.md
new file mode 100644
index 0000000..e0875c2
--- /dev/null
+++ b/docs/rules/require-meta-docs-url.md
@@ -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 execute `eslint --fix` command.
+
+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.
diff --git a/lib/rules/require-meta-docs-url.js b/lib/rules/require-meta-docs-url.js
new file mode 100644
index 0000000..dfde416
--- /dev/null
+++ b/lib/rules/require-meta-docs-url.js
@@ -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;
+          },
+        });
+      },
+    };
+  },
+};


Test patch:
diff --git a/tests/lib/rules/require-meta-docs-url.js b/tests/lib/rules/require-meta-docs-url.js
new file mode 100644
index 0000000..1b4e42a
--- /dev/null
+++ b/tests/lib/rules/require-meta-docs-url.js
@@ -0,0 +1,683 @@
+/**
+ * @author Toru Nagashima
+ * @copyright 2016 Toru Nagashima. All rights reserved.
+ * See LICENSE file in root directory for full license.
+ */
+
+'use strict';
+
+// -----------------------------------------------------------------------------
+// Requirements
+// -----------------------------------------------------------------------------
+
+const RuleTester = require('eslint').RuleTester;
+const rule = require('../../../lib/rules/require-meta-docs-url');
+
+// -----------------------------------------------------------------------------
+// Tests
+// -----------------------------------------------------------------------------
+
+const tester = new RuleTester({
+  parserOptions: {
+    ecmaVersion: 2015,
+  },
+});
+
+tester.run('require-meta-docs-url', rule, {
+  valid: [
+    'foo()',
+    `
+      module.exports.meta = {docs: {url: ""}}
+      module.exports.create = function() {}
+    `,
+    `
+      module.exports = {
+        meta: {docs: {url: ""}},
+        create() {}
+      }
+    `,
+    `
+      module.exports = {
+        ["meta"]: {["docs"]: {["url"]: ""}},
+        create() {}
+      }
+    `,
+    {
+      code: `
+        // If filename is not provided, don't check the value.
+        module.exports = {
+          meta: {docs: {url: ""}},
+          create() {}
+        }
+      `,
+      options: [{
+        pattern: 'path/to/{{name}}.md',
+      }],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: {docs: {url: "path/to/test-rule.md"}},
+          create() {}
+        }
+      `,
+      options: [{
+        pattern: 'path/to/{{name}}.md',
+      }],
+      filename: 'test-rule',
+    },
+  ],
+
+  invalid: [
+    {
+      code: `
+        module.exports = function() {}
+      `,
+      output: null,
+      errors: ['Rules should export a `meta.docs.url` property.'],
+    },
+    {
+      code: `
+        module.exports = {
+          meta,
+          create() {}
+        }
+      `,
+      output: null,
+      errors: ['Rules should export a `meta.docs.url` property.'],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: 100,
+          create() {}
+        }
+      `,
+      output: null,
+      errors: ['Rules should export a `meta.docs.url` property.'],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: {},
+          create() {}
+        }
+      `,
+      output: null,
+      errors: ['Rules should export a `meta.docs.url` property.'],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: {
+            fixable: null
+          },
+          create() {}
+        }
+      `,
+      output: null,
+      errors: ['Rules should export a `meta.docs.url` property.'],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: {
+            fixable: null,
+          },
+          create() {}
+        }
+      `,
+      output: null,
+      errors: ['Rules should export a `meta.docs.url` property.'],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: {
+            docs
+          },
+          create() {}
+        }
+      `,
+      output: null,
+      errors: ['Rules should export a `meta.docs.url` property.'],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: {
+            docs: {}
+          },
+          create() {}
+        }
+      `,
+      output: null,
+      errors: ['Rules should export a `meta.docs.url` property.'],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: {
+            docs: {
+              description: ""
+            }
+          },
+          create() {}
+        }
+      `,
+      output: null,
+      errors: ['Rules should export a `meta.docs.url` property.'],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: {
+            docs: {
+              description: "",
+            }
+          },
+          create() {}
+        }
+      `,
+      output: null,
+      errors: ['Rules should export a `meta.docs.url` property.'],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: {
+            docs: {
+              url: 100,
+            }
+          },
+          create() {}
+        }
+      `,
+      output: null,
+      errors: ['`meta.docs.url` property must be a string.'],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: {
+            docs: {
+              ...url
+            }
+          },
+          create() {}
+        }
+      `,
+      output: null,
+      parserOptions: {
+        ecmaVersion: 2015,
+        ecmaFeatures: {
+          experimentalObjectRestSpread: true,
+        },
+      },
+      errors: ['Rules should export a `meta.docs.url` property.'],
+    },
+
+    // -------------------------------------------------------------------------
+    // pattern option without filename
+    // -------------------------------------------------------------------------
+    {
+      code: `
+        module.exports = function() {}
+      `,
+      output: null,
+      options: [{
+        pattern: 'plugin-name/{{ name }}.md',
+      }],
+      errors: ['Rules should export a `meta.docs.url` property.'],
+    },
+    {
+      code: `
+        module.exports = {
+          meta,
+          create() {}
+        }
+      `,
+      output: null,
+      options: [{
+        pattern: 'plugin-name/{{ name }}.md',
+      }],
+      errors: ['Rules should export a `meta.docs.url` property.'],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: 100,
+          create() {}
+        }
+      `,
+      output: null,
+      options: [{
+        pattern: 'plugin-name/{{ name }}.md',
+      }],
+      errors: ['Rules should export a `meta.docs.url` property.'],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: {},
+          create() {}
+        }
+      `,
+      output: null,
+      options: [{
+        pattern: 'plugin-name/{{ name }}.md',
+      }],
+      errors: ['Rules should export a `meta.docs.url` property.'],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: {
+            fixable: null
+          },
+          create() {}
+        }
+      `,
+      output: null,
+      options: [{
+        pattern: 'plugin-name/{{ name }}.md',
+      }],
+      errors: ['Rules should export a `meta.docs.url` property.'],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: {
+            fixable: null,
+          },
+          create() {}
+        }
+      `,
+      output: null,
+      options: [{
+        pattern: 'plugin-name/{{ name }}.md',
+      }],
+      errors: ['Rules should export a `meta.docs.url` property.'],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: {
+            docs
+          },
+          create() {}
+        }
+      `,
+      output: null,
+      options: [{
+        pattern: 'plugin-name/{{ name }}.md',
+      }],
+      errors: ['Rules should export a `meta.docs.url` property.'],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: {
+            docs: {}
+          },
+          create() {}
+        }
+      `,
+      output: null,
+      options: [{
+        pattern: 'plugin-name/{{ name }}.md',
+      }],
+      errors: ['Rules should export a `meta.docs.url` property.'],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: {
+            docs: {
+              description: ""
+            }
+          },
+          create() {}
+        }
+      `,
+      output: null,
+      options: [{
+        pattern: 'plugin-name/{{ name }}.md',
+      }],
+      errors: ['Rules should export a `meta.docs.url` property.'],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: {
+            docs: {
+              description: "",
+            }
+          },
+          create() {}
+        }
+      `,
+      output: null,
+      options: [{
+        pattern: 'plugin-name/{{ name }}.md',
+      }],
+      errors: ['Rules should export a `meta.docs.url` property.'],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: {
+            docs: {
+              url: 100,
+            }
+          },
+          create() {}
+        }
+      `,
+      output: null,
+      options: [{
+        pattern: 'plugin-name/{{ name }}.md',
+      }],
+      errors: ['`meta.docs.url` property must be a string.'],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: {
+            docs: {
+              ...url
+            }
+          },
+          create() {}
+        }
+      `,
+      output: null,
+      options: [{
+        pattern: 'plugin-name/{{ name }}.md',
+      }],
+      parserOptions: {
+        ecmaVersion: 2015,
+        ecmaFeatures: {
+          experimentalObjectRestSpread: true,
+        },
+      },
+      errors: ['Rules should export a `meta.docs.url` property.'],
+    },
+
+    // -------------------------------------------------------------------------
+    // pattern option with filename
+    // -------------------------------------------------------------------------
+    {
+      code: `
+        module.exports = function() {}
+      `,
+      output: null,
+      options: [{
+        pattern: 'plugin-name/{{ name }}.md',
+      }],
+      errors: ['Rules should export a `meta.docs.url` property.'],
+      filename: 'test.js',
+    },
+    {
+      code: `
+        module.exports = {
+          meta,
+          create() {}
+        }
+      `,
+      output: null,
+      options: [{
+        pattern: 'plugin-name/{{ name }}.md',
+      }],
+      errors: ['Rules should export a `meta.docs.url` property.'],
+      filename: 'test.js',
+    },
+    {
+      code: `
+        module.exports = {
+          meta: 100,
+          create() {}
+        }
+      `,
+      output: null,
+      options: [{
+        pattern: 'plugin-name/{{ name }}.md',
+      }],
+      errors: ['Rules should export a `meta.docs.url` property.'],
+      filename: 'test.js',
+    },
+    {
+      code: `
+        module.exports = {
+          meta: {},
+          create() {}
+        }
+      `,
+      output: `
+        module.exports = {
+          meta: {
+docs: {
+url: "plugin-name/test.md"
+}
+},
+          create() {}
+        }
+      `,
+      options: [{
+        pattern: 'plugin-name/{{ name }}.md',
+      }],
+      errors: ['Rules should export a `meta.docs.url` property.'],
+      filename: 'test.js',
+    },
+    {
+      code: `
+        module.exports = {
+          meta: {
+            fixable: null
+          },
+          create() {}
+        }
+      `,
+      output: `
+        module.exports = {
+          meta: {
+            fixable: null,
+docs: {
+url: "plugin-name/test.md"
+}
+          },
+          create() {}
+        }
+      `,
+      options: [{
+        pattern: 'plugin-name/{{ name }}.md',
+      }],
+      errors: ['Rules should export a `meta.docs.url` property.'],
+      filename: 'test.js',
+    },
+    {
+      code: `
+        module.exports = {
+          meta: {
+            fixable: null,
+          },
+          create() {}
+        }
+      `,
+      output: `
+        module.exports = {
+          meta: {
+            fixable: null,
+docs: {
+url: "plugin-name/test.md"
+},
+          },
+          create() {}
+        }
+      `,
+      options: [{
+        pattern: 'plugin-name/{{ name }}.md',
+      }],
+      errors: ['Rules should export a `meta.docs.url` property.'],
+      filename: 'test.js',
+    },
+    {
+      code: `
+        module.exports = {
+          meta: {
+            docs
+          },
+          create() {}
+        }
+      `,
+      output: null,
+      options: [{
+        pattern: 'plugin-name/{{ name }}.md',
+      }],
+      errors: ['Rules should export a `meta.docs.url` property.'],
+      filename: 'test.js',
+    },
+    {
+      code: `
+        module.exports = {
+          meta: {
+            docs: {}
+          },
+          create() {}
+        }
+      `,
+      output: `
+        module.exports = {
+          meta: {
+            docs: {
+url: "plugin-name/test.md"
+}
+          },
+          create() {}
+        }
+      `,
+      options: [{
+        pattern: 'plugin-name/{{ name }}.md',
+      }],
+      errors: ['Rules should export a `meta.docs.url` property.'],
+      filename: 'test.js',
+    },
+    {
+      code: `
+        module.exports = {
+          meta: {
+            docs: {
+              description: ""
+            }
+          },
+          create() {}
+        }
+      `,
+      output: `
+        module.exports = {
+          meta: {
+            docs: {
+              description: "",
+url: "plugin-name/test.md"
+            }
+          },
+          create() {}
+        }
+      `,
+      options: [{
+        pattern: 'plugin-name/{{ name }}.md',
+      }],
+      errors: ['Rules should export a `meta.docs.url` property.'],
+      filename: 'test.js',
+    },
+    {
+      code: `
+        module.exports = {
+          meta: {
+            docs: {
+              description: "",
+            }
+          },
+          create() {}
+        }
+      `,
+      output: `
+        module.exports = {
+          meta: {
+            docs: {
+              description: "",
+url: "plugin-name/test.md",
+            }
+          },
+          create() {}
+        }
+      `,
+      options: [{
+        pattern: 'plugin-name/{{ name }}.md',
+      }],
+      errors: ['Rules should export a `meta.docs.url` property.'],
+      filename: 'test.js',
+    },
+    {
+      code: `
+        module.exports = {
+          meta: {
+            docs: {
+              url: 100,
+            }
+          },
+          create() {}
+        }
+      `,
+      output: `
+        module.exports = {
+          meta: {
+            docs: {
+              url: "plugin-name/test.md",
+            }
+          },
+          create() {}
+        }
+      `,
+      options: [{
+        pattern: 'plugin-name/{{ name }}.md',
+      }],
+      errors: ['`meta.docs.url` property must be `plugin-name/test.md`.'],
+      filename: 'test.js',
+    },
+    {
+      code: `
+        module.exports = {
+          meta: {
+            docs: {
+              ...url
+            }
+          },
+          create() {}
+        }
+      `,
+      output: `
+        module.exports = {
+          meta: {
+            docs: {
+              ...url,
+url: "plugin-name/test.md"
+            }
+          },
+          create() {}
+        }
+      `,
+      options: [{
+        pattern: 'plugin-name/{{ name }}.md',
+      }],
+      parserOptions: {
+        ecmaVersion: 2015,
+        ecmaFeatures: {
+          experimentalObjectRestSpread: true,
+        },
+      },
+      errors: ['Rules should export a `meta.docs.url` property.'],
+      filename: 'test.js',
+    },
+  ],
+});