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

Issue:
Rule: no-unused-placeholders
Reports when a `context.report` call contains a `data` property that does not have a corresponding placeholder in the report message.

Example of invalid code:

```js
context.report({
  node,
  message: "Do not use {{foo}}.",
  data: {
    foo: 1,
    bar: 2
  }
});
```

Pull request description:
New: rule no-unused-placeholders.
fixes #7 

Patch:
diff --git a/README.md b/README.md
index be597f6..f3ba4b8 100644
--- a/README.md
+++ b/README.md
@@ -53,6 +53,7 @@ Name | ✔️ | 🛠 | Description
 [no-deprecated-report-api](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-deprecated-report-api.md) | ✔️ | 🛠 | Prohibits the deprecated `context.report(node, message)` API
 [no-identical-tests](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-identical-tests.md) | ✔️  | 🛠 | Disallows identical tests
 [no-missing-placeholders](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-missing-placeholders.md) | ✔️ | | Disallows missing placeholders in rule report messages
+[no-unused-placeholders](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-unused-placeholders.md) |  | | Disallows unused placeholders in rule report messages
 [no-useless-token-range](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-useless-token-range.md) | ✔️ | 🛠 | Disallows unnecessary calls to sourceCode.getFirstToken and sourceCode.getLastToken
 [prefer-output-null](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-output-null.md) | | | Disallows invalid RuleTester test cases with the output the same as the code.
 [prefer-placeholders](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-placeholders.md) | | | Disallows template literals as report messages
diff --git a/docs/rules/no-unused-placeholders.md b/docs/rules/no-unused-placeholders.md
new file mode 100644
index 0000000..4c9f04d
--- /dev/null
+++ b/docs/rules/no-unused-placeholders.md
@@ -0,0 +1,58 @@
+# Disallow unused placeholders in rule report messages (no-unused-placeholders)
+
+This rule aims to disallow unused placeholders in rule report messages.
+
+## Rule Details
+
+Reports when a context.report call contains a data property that does not have a corresponding placeholder in the report message.
+
+Examples of **incorrect** code for this rule:
+
+```js
+/*eslint eslint-plugin/no-missing-placeholders: error*/
+
+module.exports = {
+  create(context) {
+
+    context.report({
+      node,
+      message: 'something is wrong.',
+      data: { something: 'foo' }
+    });
+
+    context.report(node, 'something is wrong.', { something: 'foo' });
+  }
+};
+```
+
+Examples of **correct** code for this rule:
+
+```js
+/*eslint eslint-plugin/no-missing-placeholders: error*/
+
+module.exports = {
+  create(context) {
+    context.report({
+      node,
+      message: 'something is wrong.'
+    });
+
+    context.report({
+      node,
+      message: '{{something}} is wrong.',
+      data: { something: 'foo' }
+    });
+
+    context.report(node, '{{something}} is wrong.', { something: 'foo' });
+  }
+};
+```
+
+## When Not To Use It
+
+If you want to allow unused placeholders, you should turn off this rule.
+
+## Further Reading
+
+* [context.report() API](http://eslint.org/docs/developer-guide/working-with-rules#contextreport)
+* [no-missing-placeholders](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-missing-placeholders.md)
diff --git a/lib/rules/no-unused-placeholders.js b/lib/rules/no-unused-placeholders.js
new file mode 100644
index 0000000..023cf26
--- /dev/null
+++ b/lib/rules/no-unused-placeholders.js
@@ -0,0 +1,75 @@
+/**
+ * @fileoverview Disallow unused placeholders in rule report messages
+ * @author 薛定谔的猫<hh_2013@foxmail.com>
+ */
+
+'use strict';
+
+const utils = require('../utils');
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'Disallow unused placeholders in rule report messages',
+      category: 'Rules',
+      recommended: false,
+    },
+    fixable: null,
+    schema: [],
+  },
+
+  create (context) {
+    let contextIdentifiers;
+
+    // ----------------------------------------------------------------------
+    // Public
+    // ----------------------------------------------------------------------
+
+    return {
+      Program (ast) {
+        contextIdentifiers = utils.getContextIdentifiers(context, ast);
+      },
+      CallExpression (node) {
+        if (
+          node.callee.type === 'MemberExpression' &&
+            contextIdentifiers.has(node.callee.object) &&
+            node.callee.property.type === 'Identifier' && node.callee.property.name === 'report'
+        ) {
+          const reportInfo = utils.getReportInfo(node.arguments);
+
+          if (
+            reportInfo &&
+                reportInfo.message &&
+                reportInfo.message.type === 'Literal' &&
+                typeof reportInfo.message.value === 'string' &&
+              reportInfo.data && reportInfo.data.type === 'ObjectExpression'
+          ) {
+            const message = reportInfo.message.value;
+            // https://github.com/eslint/eslint/blob/2874d75ed8decf363006db25aac2d5f8991bd969/lib/linter.js#L986
+            const PLACEHOLDER_MATCHER = /\{\{\s*([^{}]+?)\s*\}\}/g;
+            const placeholdersInMessage = new Set();
+
+            message.replace(PLACEHOLDER_MATCHER, (fullMatch, term) => {
+              placeholdersInMessage.add(term);
+            });
+
+            reportInfo.data.properties.forEach(prop => {
+              const key = utils.getKeyName(prop);
+              if (!placeholdersInMessage.has(key)) {
+                context.report({
+                  node: reportInfo.message,
+                  message: 'The placeholder {{{{unusedKey}}}} is unused.',
+                  data: { unusedKey: key },
+                });
+              }
+            });
+          }
+        }
+      },
+    };
+  },
+};


Test patch:
diff --git a/tests/lib/rules/no-unused-placeholders.js b/tests/lib/rules/no-unused-placeholders.js
new file mode 100644
index 0000000..febe8c4
--- /dev/null
+++ b/tests/lib/rules/no-unused-placeholders.js
@@ -0,0 +1,142 @@
+/**
+ * @fileoverview Disallow unused placeholders in rule report messages
+ * @author 薛定谔的猫<hh_2013@foxmail.com>
+ */
+
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/no-unused-placeholders');
+const RuleTester = require('eslint').RuleTester;
+
+/**
+* Create an error for the given key
+* @param {string} unusedKey The placeholder that is unused
+* @returns {object} An expected error
+*/
+function error (unusedKey) {
+  return { type: 'Literal', message: `The placeholder {{${unusedKey}}} is unused.` };
+}
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
+ruleTester.run('no-unused-placeholders', rule, {
+
+  valid: [
+    `
+          module.exports = {
+            create(context) {
+              context.report({
+                node,
+                message: 'foo{{ bar }}',
+                data: { bar: 'baz' }
+              });
+            }
+          };
+        `,
+    `
+        module.exports = {
+        create(context) {
+          context.report({
+            node,
+            message: 'foo bar'
+          });
+        }
+      };
+    `,
+    `
+      module.exports = {
+        create(context) {
+          context.report({
+            node,
+            message: 'foo {{bar}}',
+            data: { bar: 'baz' }
+          });
+        }
+      };
+    `,
+    `
+      module.exports = {
+        create(context) {
+          context.report({
+            node,
+            message: 'foo {{bar}}',
+            data: { 'bar': 'baz' }
+          });
+        }
+      };
+    `,
+    `
+      module.exports = {
+        create(context) {
+          context.report({
+            node,
+            message: 'foo {{bar}}',
+            data: { bar }
+          });
+        }
+      };
+    `,
+    `
+      module.exports = context => {
+        context.report(node, 'foo {{bar}}', { bar: 'baz' });
+      };
+    `,
+    `
+      module.exports = context => {
+        context.report(node, { line: 1, column: 3 }, 'foo {{bar}}', { bar: 'baz' });
+      };
+    `,
+  ],
+
+  invalid: [
+    {
+      code: `
+        module.exports = {
+          create(context) {
+            context.report({
+              node,
+              message: 'foo',
+              data: { bar }
+            });
+          }
+        };
+      `,
+      errors: [error('bar')],
+    },
+    {
+      code: `
+        module.exports = {
+          create(context) {
+            context.report({
+              node,
+              message: 'foo',
+              data: { bar: '' }
+            });
+          }
+        };
+      `,
+      errors: [error('bar')],
+    },
+    {
+      code: `
+        module.exports = {
+          create(context) {
+            context.report({
+              node,
+              message: 'foo {{bar}}',
+              data: { baz: '' }
+            });
+          }
+        };
+      `,
+      errors: [error('baz')],
+    },
+  ],
+});