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 d2267d1
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 23 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
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'."});
}
}
};
}
};
110 changes: 110 additions & 0 deletions tests/lib/testers/rule-tester.js
Expand Up @@ -830,6 +830,116 @@ describe("RuleTester", () => {
}, "Test Scenarios for rule foo is invalid:\nCould not find any valid test scenarios");
});

// Nominal message/messageId use cases
it("should assert match if message provided in both test and result.", () => {
assert.throws(() => {
ruleTester.run("foo", require("../../fixtures/testers/rule-tester/messageId").withoutMetaWithoutData, {
valid: [],
invalid: [{ code: "foo", errors: [{ message: "something" }] }]
});
}, /Avoid using variables named/);

assert.doesNotThrow(() => {
ruleTester.run("foo", require("../../fixtures/testers/rule-tester/messageId").withoutMetaWithoutData, {
valid: [],
invalid: [{ code: "foo", errors: [{ message: "Avoid using variables named 'foo'." }] }]
});
});
});

it("should assert match between messageId if provided in both test and result and test data is undefined.", () => {
assert.throws(() => {
ruleTester.run("foo", require("../../fixtures/testers/rule-tester/messageId").withMetaWithData, {
valid: [],
invalid: [{ code: "foo", errors: [{ messageId: "unused" }] }]
});
}, "messageId mismatch saw 'avoidFoo' and expected 'unused'.");

assert.doesNotThrow(() => {
ruleTester.run("foo", require("../../fixtures/testers/rule-tester/messageId").withMetaWithData, {
valid: [],
invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo" }] }]
});
});
});

it("should assert match between messageId and data if provided in both test and result.", () => {
const testConfigsData = [
{ name: "foo", bad: "data" },
{ name: "not foo" },
{ notName: "no foo" },
{ }
];

testConfigsData.forEach(data => {
assert.throws(() => {
ruleTester.run("foo", require("../../fixtures/testers/rule-tester/messageId").withMetaWithData, {
valid: [],
invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo", data }] }]
});
}, /Data properties do not match. Saw `\{[^}]*}` and expected `\{[^}]*}`/);
});

assert.throws(() => {
ruleTester.run("foo", require("../../fixtures/testers/rule-tester/messageId").withMetaWithData, {
valid: [],
invalid: [{ code: "foo", errors: [{ messageId: "unused", data: { name: "foo" } }] }]
});
}, "messageId mismatch saw 'avoidFoo' and expected 'unused'.");

assert.doesNotThrow(() => {
ruleTester.run("foo", require("../../fixtures/testers/rule-tester/messageId").withMetaWithData, {
valid: [],
invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo", data: { name: "foo" } }] }]
});
});
});

// messageId/message misconfiguration cases

it("should throw if user tests for both message and messageId", () => {
assert.throws(() => {
ruleTester.run("foo", require("../../fixtures/testers/rule-tester/messageId").withMetaWithData, {
valid: [],
invalid: [{ code: "foo", errors: [{ message: "something", messageId: "avoidFoo" }] }]
});
}, "Error should not specify both 'message' and a 'messageId'.");
});
it("should throw if user tests for messageId but the rule doesn't use the messageId meta syntax.", () => {
assert.throws(() => {
ruleTester.run("foo", require("../../fixtures/testers/rule-tester/messageId").withoutMetaWithoutData, {
valid: [],
invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo" }] }]
});
}, "Error can not use 'messageId' if rule under test doesn't define 'meta.messages'");
});
it("should throw if user tests for message as well as data.", () => {
assert.throws(() => {
ruleTester.run("foo", require("../../fixtures/testers/rule-tester/messageId").withoutMetaWithoutData, {
valid: [],
invalid: [{ code: "foo", errors: [{ message: "A failure message", data: { some: "data" } }] }]
});
}, "Error should not specify both 'data' and 'message'.");
});

it("should not throw if user tests for messageId but does not include data as long as the test's messageId matches the result's messageId.", () => {
assert.doesNotThrow(() => {
ruleTester.run("foo", require("../../fixtures/testers/rule-tester/messageId").withMetaWithData, {
valid: [],
invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo" }] }]
});
}, "Message");
});

it("should not throw if user tests for a messageId and data and the rule passes messageId and data if and only if messageIds match and data in the test is present in the result's data.", () => {
assert.doesNotThrow(() => {
ruleTester.run("foo", require("../../fixtures/testers/rule-tester/messageId").withMetaWithData, {
valid: [],
invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo", data: { name: "foo" } }] }]
});
}, "Message");
});

describe("naming test cases", () => {

/**
Expand Down

0 comments on commit d2267d1

Please sign in to comment.