Skip to content

Commit

Permalink
Breaking: Make rule-tester strictly check messageId and data. (fixes e…
Browse files Browse the repository at this point in the history
  • Loading branch information
betaorbust committed Jan 28, 2018
1 parent 084351b commit bae2e9d
Show file tree
Hide file tree
Showing 10 changed files with 216 additions and 29 deletions.
22 changes: 22 additions & 0 deletions docs/developer-guide/working-with-rules.md
Expand Up @@ -190,6 +190,8 @@ Instead of typing out messages in both the `context.report()` call and your test

This allows you to avoid retyping error messages. It also prevents errors reported in different sections of your rule from having out-of-date messages.

If the `data` property is included, it is checked for strict deep equality with the resulting runtime error's `data` field. However, if the `data` property is omitted, only the `messageId` will be checked.

```js
{% raw %}
// in your rule
Expand Down Expand Up @@ -226,14 +228,34 @@ var rule = require("../../../lib/rules/my-rule");
var RuleTester = require("eslint").RuleTester;

var ruleTester = new RuleTester();

ruleTester.run("my-rule", rule, {
valid: ["bar", "baz"],
invalid: [
{
code: "foo",
errors: [
{
// Here we are asserting that the avoideName message
// was used, but not checking against the data object
// used to format it. This form is also used when the
// message has no formatting options and is just a string.
messageId: "avoidName"
}
]
}
]
});

ruleTester.run("my-rule", rule, {
valid: ["bar", "baz"],
invalid: [
{
code: "foo",
errors: [
{
// Here we're asserting that both the messageId "avoidName"
// was used as well as the exact data used to format it.
messageId: "avoidName",
data: {
name: "foo"
Expand Down
8 changes: 7 additions & 1 deletion lib/report-translator.js
Expand Up @@ -198,6 +198,8 @@ function normalizeFixes(descriptor, sourceCode) {
* @param {(0|1|2)} options.severity Rule severity
* @param {(ASTNode|null)} options.node Node
* @param {string} options.message Error message
* @param {string} [options.messageId] The error message ID.
* @param {Object} [options.data] The error message data.
* @param {{start: SourceLocation, end: (SourceLocation|null)}} options.loc Start and end location
* @param {{text: string, range: (number[]|null)}} options.fix The fix object
* @param {string[]} options.sourceLines Source lines
Expand All @@ -222,6 +224,10 @@ function createProblem(options) {
problem.messageId = options.messageId;
}

if (options.data) {
problem.data = options.data;
}

if (options.loc.end) {
problem.endLine = options.loc.end.line;
problem.endColumn = options.loc.end.column + 1;
Expand Down Expand Up @@ -271,13 +277,13 @@ module.exports = function createReportTranslator(metadata) {
descriptor.message = messages[id];
}


return createProblem({
ruleId: metadata.ruleId,
severity: metadata.severity,
node: descriptor.node,
message: normalizeMessagePlaceholders(descriptor),
messageId: descriptor.messageId,
data: descriptor.data,
loc: normalizeReportLoc(descriptor),
fix: normalizeFixes(descriptor, metadata.sourceCode),
sourceLines: metadata.sourceCode.lines
Expand Down
2 changes: 1 addition & 1 deletion lib/rules/no-undef.js
Expand Up @@ -63,7 +63,7 @@ module.exports = {
context.report({
node: identifier,
message: "'{{name}}' is not defined.",
data: identifier
data: { name: identifier.name }
});
});
}
Expand Down
4 changes: 3 additions & 1 deletion lib/rules/no-unused-vars.js
Expand Up @@ -632,7 +632,9 @@ module.exports = {
message: unusedVar.references.some(ref => ref.isWrite())
? getAssignedMessage()
: getDefinedMessage(unusedVar),
data: unusedVar
data: {
name: unusedVar.name
}
});
}
}
Expand Down
45 changes: 23 additions & 22 deletions lib/testers/rule-tester.js
Expand Up @@ -47,8 +47,7 @@ const lodash = require("lodash"),
ajv = require("../util/ajv"),
Linter = require("../linter"),
Environments = require("../config/environments"),
SourceCodeFixer = require("../util/source-code-fixer"),
interpolate = require("../util/interpolate");
SourceCodeFixer = require("../util/source-code-fixer");

//------------------------------------------------------------------------------
// Private Members
Expand Down Expand Up @@ -487,34 +486,36 @@ class RuleTester {

/*
* Error object.
* This may have a message, node type, line, and/or
* This may have a message, messageId, data, node type, line, and/or
* column.
*/
if (error.message) {
if (hasOwnProperty(error, "message")) {
assert.strictEqual(hasOwnProperty(error, "messageId"), false, "Error should not specify both 'message' and a 'messageId'.");
assert.strictEqual(hasOwnProperty(error, "data"), false, "Error should not specify both 'data' and 'message'.");
assertMessageMatches(message.message, error.message);
}

if (error.messageId) {
const hOP = Object.hasOwnProperty.call.bind(Object.hasOwnProperty);

// verify that `error.message` is `undefined`
assert.strictEqual(error.message, void 0, "Error should not specify both a message and a messageId.");
if (!hOP(rule, "meta") || !hOP(rule.meta, "messages")) {
assert.fail("Rule must specify a messages hash in `meta`");
}
if (!hOP(rule.meta.messages, error.messageId)) {
} else if (hasOwnProperty(error, "messageId")) {
assert.ok(
hasOwnProperty(rule, "meta") && hasOwnProperty(rule.meta, "messages"),
"Error can not use 'messageId' if rule under test doesn't define 'meta.messages'."
);
if (!hasOwnProperty(rule.meta.messages, error.messageId)) {
const friendlyIDList = `[${Object.keys(rule.meta.messages).map(key => `'${key}'`).join(", ")}]`;

assert.fail(`Invalid messageId '${error.messageId}'. Expected one of ${friendlyIDList}.`);
}

let expectedMessage = rule.meta.messages[error.messageId];

if (error.data) {
expectedMessage = interpolate(expectedMessage, error.data);
assert.strictEqual(
error.messageId,
message.messageId,
`messageId mismatch saw '${message.messageId}' and expected '${error.messageId}'.`
);
if (hasOwnProperty(error, "data")) {
assert.ok(hasOwnProperty(message, "data"), "Error referrs to messageId data, but none provided by rule.");
assert.deepStrictEqual(
error.data,
message.data,
`Data properties do not match. Saw \`${util.inspect(message.data, false)}\` and expected \`${util.inspect(error.data, false)}\``
);
}

assertMessageMatches(message.message, expectedMessage);
}

if (error.type) {
Expand Down
36 changes: 36 additions & 0 deletions tests/fixtures/testers/rule-tester/messageId.js
@@ -0,0 +1,36 @@
"use strict";
module.exports.withMetaWithData = {
meta: {
messages: {
avoidFoo: "Avoid using variables named '{{ name }}'.",
unused: "An unused key"
}
},
create(context) {
return {
Identifier(node) {
if (node.name === "foo") {
context.report({
node,
messageId: "avoidFoo",
data: {
name: "foo"
}
});
}
}
};
}
};

module.exports.withoutMetaWithoutData = {
create(context) {
return {
Identifier(node) {
if (node.name === "foo") {
context.report({ node, message: "Avoid using variables named 'foo'."});
}
}
};
}
};
6 changes: 6 additions & 0 deletions tests/lib/cli-engine.js
Expand Up @@ -351,6 +351,7 @@ describe("CLIEngine", () => {
ruleId: "no-undef",
severity: 2,
message: "'foo' is not defined.",
data: { name: "foo" },
line: 1,
column: 11,
endLine: 1,
Expand Down Expand Up @@ -1395,6 +1396,10 @@ describe("CLIEngine", () => {
column: 9,
line: 2,
message: "Expected '===' and instead saw '=='.",
data: {
actualOperator: "==",
expectedOperator: "==="
},
nodeType: "BinaryExpression",
ruleId: "eqeqeq",
severity: 2,
Expand All @@ -1416,6 +1421,7 @@ describe("CLIEngine", () => {
endColumn: 21,
endLine: 1,
message: "'foo' is not defined.",
data: { name: "foo" },
nodeType: "Identifier",
ruleId: "no-undef",
severity: 2,
Expand Down
8 changes: 6 additions & 2 deletions tests/lib/report-translator.js
Expand Up @@ -54,7 +54,7 @@ describe("createReportTranslator", () => {
describe("old-style call with location", () => {
it("should extract the location correctly", () => {
assert.deepStrictEqual(
translateReport(node, location, message, {}),
translateReport(node, location, message),
{
ruleId: "foo-rule",
severity: 2,
Expand All @@ -71,7 +71,7 @@ describe("createReportTranslator", () => {
describe("old-style call without location", () => {
it("should use the start location and end location of the node", () => {
assert.deepStrictEqual(
translateReport(node, message, {}),
translateReport(node, message),
{
ruleId: "foo-rule",
severity: 2,
Expand Down Expand Up @@ -336,6 +336,7 @@ describe("createReportTranslator", () => {
ruleId: "foo-rule",
severity: 2,
message: "my message testing!",
data: ["!", "testing"],
line: 42,
column: 24,
nodeType: "ExpressionStatement",
Expand All @@ -358,6 +359,7 @@ describe("createReportTranslator", () => {
severity: 2,
ruleId: "foo-rule",
message: "hello ExpressionStatement",
data: { dynamic: "ExpressionStatement" },
nodeType: "ExpressionStatement",
line: 1,
column: 4,
Expand All @@ -373,6 +375,7 @@ describe("createReportTranslator", () => {
severity: 2,
ruleId: "foo-rule",
message: "hello ExpressionStatement",
data: { dynamic: "ExpressionStatement" },
nodeType: "ExpressionStatement",
line: 1,
column: 4,
Expand Down Expand Up @@ -558,6 +561,7 @@ describe("createReportTranslator", () => {
severity: 2,
ruleId: "foo-rule",
message: "my message testing!",
data: ["!", "testing"],
nodeType: "ExpressionStatement",
line: 1,
column: 1,
Expand Down
4 changes: 2 additions & 2 deletions tests/lib/rules/no-fallthrough.js
Expand Up @@ -162,8 +162,8 @@ ruleTester.run("no-fallthrough", rule, {
}],
errors: [
{
message: errorsDefault.message,
type: errorsDefault.type,
message: errorsDefault[0].message,
type: errorsDefault[0].type,
line: 4,
column: 1
}
Expand Down

0 comments on commit bae2e9d

Please sign in to comment.