Skip to content
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

feat!: Rule Tester checks for missing placeholder data in the message #18073

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
feat: ignore introduced message placeholders
  • Loading branch information
DMartens committed Feb 9, 2024
commit a39edf77ea0ec4022be61f8a8f44203fc7360a07
31 changes: 29 additions & 2 deletions lib/rule-tester/rule-tester.js
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,29 @@ function getMessagePlaceholders(message) {
return Array.from(message.matchAll(matcher), ([, name]) => name.trim());
}

/**
* Returns the placeholders in the reported messages but
* only includes the placeholders available in the raw message if data is not specified or
* excludes introduced placeholders specified in the data values.
* @param {string} message The reported message
* @param {string} raw The raw message specified in the rule meta.messages
* @param {undefined|Record<unknown, unknown>} data The passed
* @returns {string[]} Missing placeholder names
*/
function getMissingMessagePlaceholders(message, raw, data) {
const existing = getMessagePlaceholders(message);

if (data === void 0) {
const available = getMessagePlaceholders(raw);

return existing.filter(name => available.includes(name));
}

const introduced = new Set(Object.values(data).flatMap(value => (typeof value === "string" ? getMessagePlaceholders(value) : [])));

return existing.filter(name => !introduced.has(name));
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
const existing = getMessagePlaceholders(message);
if (data === void 0) {
const available = getMessagePlaceholders(raw);
return existing.filter(name => available.includes(name));
}
const introduced = new Set(Object.values(data).flatMap(value => (typeof value === "string" ? getMessagePlaceholders(value) : [])));
return existing.filter(name => !introduced.has(name));
let existing = getMessagePlaceholders(message);
// no potential placeholders were found in the reported message, so there's nothing further to check
if (existing.length === 0) {
return existing;
}
const original = getMessagePlaceholders(raw);
// filter out those that don't appear in the original template message as they're certainly not placeholders
existing = existing.filter(name => original.includes(name));
if (data) {
const provided = Object.keys(data);
// since the hydrated message texts are compared, we can ignore those for which the data is provided in the test
existing = existing.filter(name => !provided.includes(name));
}
return existing;

I think this check would be more robust if we always exclude those that don't appear in the meta message and those for which the data is provided in the test.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I refactored this method in another way and also kept the case for adding placeholders via data properties (see no-restricted-syntax case).

}

const metaSchemaDescription = `
\t- If the rule has options, set \`meta.schema\` to an array or non-empty object to enable options validation.
\t- If the rule doesn't have options, omit \`meta.schema\` to enforce that no options can be passed to the rule.
Expand Down Expand Up @@ -1010,7 +1033,7 @@ class RuleTester {
`messageId '${message.messageId}' does not match expected messageId '${error.messageId}'.`
);

const missingPlaceholders = getMessagePlaceholders(message.message);
const missingPlaceholders = getMissingMessagePlaceholders(message.message, rule.meta.messages[message.messageId], error.data);

assert.ok(
missingPlaceholders.length === 0,
Expand Down Expand Up @@ -1117,7 +1140,11 @@ class RuleTester {
`${suggestionPrefix} messageId should be '${expectedSuggestion.messageId}' but got '${actualSuggestion.messageId}' instead.`
);

const missingPlaceholders = getMessagePlaceholders(actualSuggestion.desc);
const missingPlaceholders = getMissingMessagePlaceholders(
actualSuggestion.desc,
rule.meta.messages[expectedSuggestion.messageId],
expectedSuggestion.data
);

assert.ok(
missingPlaceholders.length === 0,
Expand Down
22 changes: 22 additions & 0 deletions tests/fixtures/testers/rule-tester/messageId.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,25 @@ module.exports.withPlaceholdersInData = {
};
}
};

module.exports.withSamePlaceholdersInData = {
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: '{{ name }}' },
});
}
}
};
}
};
29 changes: 26 additions & 3 deletions tests/lib/rule-tester/rule-tester.js
DMartens marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -1934,13 +1934,36 @@ describe("RuleTester", () => {
}, "The reported message has the missing placeholders: 'type', 'name'. Please provide them via the 'data' property in the context.report() call.");
});

it("should throw if the data in the message contains placeholders", () => {
it("should not throw if the data in the message contains placeholders not present in the raw message", () => {
ruleTester.run("foo", require("../../fixtures/testers/rule-tester/messageId").withPlaceholdersInData, {
valid: [],
invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo" }] }]
});
});

it("should throw if the data in the message contains the same placeholder and data is not specified", () => {
assert.throws(() => {
ruleTester.run("foo", require("../../fixtures/testers/rule-tester/messageId").withPlaceholdersInData, {
ruleTester.run("foo", require("../../fixtures/testers/rule-tester/messageId").withSamePlaceholdersInData, {
valid: [],
invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo" }] }]
});
}, "The reported message has a missing placeholder 'placeholder'. Please provide them via the 'data' property in the context.report() call.");
}, "The reported message has a missing placeholder 'name'. Please provide them via the 'data' property in the context.report() call.");
});

it("should not throw if the data in the message contains the same placeholder and data is specified", () => {
ruleTester.run("foo", require("../../fixtures/testers/rule-tester/messageId").withSamePlaceholdersInData, {
valid: [],
invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo", data: { name: "{{ name }}" } }] }]
});
});

it("should throw an error for only specifying ignored non-string data property values", () => {
assert.throws(() => {
ruleTester.run("foo", require("../../fixtures/testers/rule-tester/messageId").withSamePlaceholdersInData, {
valid: [],
invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo", data: { name: 0 } }] }]
});
}, "The reported message has a missing placeholder 'name'. Please provide them via the 'data' property in the context.report() call.");
});

// messageId/message misconfiguration cases
Expand Down