|
17 | 17 | equal = require("fast-deep-equal"), |
18 | 18 | Traverser = require("../shared/traverser"), |
19 | 19 | { getRuleOptionsSchema } = require("../config/flat-config-helpers"), |
20 | | - { Linter, SourceCodeFixer, interpolate } = require("../linter"), |
| 20 | + { Linter, SourceCodeFixer } = require("../linter"), |
| 21 | + { interpolate, getPlaceholderMatcher } = require("../linter/interpolate"), |
21 | 22 | stringify = require("json-stable-stringify-without-jsonify"); |
22 | 23 |
|
23 | 24 | const { FlatConfigArray } = require("../config/flat-config-array"); |
@@ -304,6 +305,39 @@ function throwForbiddenMethodError(methodName, prototype) { |
304 | 305 | }; |
305 | 306 | } |
306 | 307 |
|
| 308 | +/** |
| 309 | + * Extracts names of {{ placeholders }} from the reported message. |
| 310 | + * @param {string} message Reported message |
| 311 | + * @returns {string[]} Array of placeholder names |
| 312 | + */ |
| 313 | +function getMessagePlaceholders(message) { |
| 314 | + const matcher = getPlaceholderMatcher(); |
| 315 | + |
| 316 | + return Array.from(message.matchAll(matcher), ([, name]) => name.trim()); |
| 317 | +} |
| 318 | + |
| 319 | +/** |
| 320 | + * Returns the placeholders in the reported messages but |
| 321 | + * only includes the placeholders available in the raw message and not in the provided data. |
| 322 | + * @param {string} message The reported message |
| 323 | + * @param {string} raw The raw message specified in the rule meta.messages |
| 324 | + * @param {undefined|Record<unknown, unknown>} data The passed |
| 325 | + * @returns {string[]} Missing placeholder names |
| 326 | + */ |
| 327 | +function getUnsubstitutedMessagePlaceholders(message, raw, data = {}) { |
| 328 | + const unsubstituted = getMessagePlaceholders(message); |
| 329 | + |
| 330 | + if (unsubstituted.length === 0) { |
| 331 | + return []; |
| 332 | + } |
| 333 | + |
| 334 | + // Remove false positives by only counting placeholders in the raw message, which were not provided in the data matcher or added with a data property |
| 335 | + const known = getMessagePlaceholders(raw); |
| 336 | + const provided = Object.keys(data); |
| 337 | + |
| 338 | + return unsubstituted.filter(name => known.includes(name) && !provided.includes(name)); |
| 339 | +} |
| 340 | + |
307 | 341 | const metaSchemaDescription = ` |
308 | 342 | \t- If the rule has options, set \`meta.schema\` to an array or non-empty object to enable options validation. |
309 | 343 | \t- If the rule doesn't have options, omit \`meta.schema\` to enforce that no options can be passed to the rule. |
@@ -997,6 +1031,18 @@ class RuleTester { |
997 | 1031 | error.messageId, |
998 | 1032 | `messageId '${message.messageId}' does not match expected messageId '${error.messageId}'.` |
999 | 1033 | ); |
| 1034 | + |
| 1035 | + const unsubstitutedPlaceholders = getUnsubstitutedMessagePlaceholders( |
| 1036 | + message.message, |
| 1037 | + rule.meta.messages[message.messageId], |
| 1038 | + error.data |
| 1039 | + ); |
| 1040 | + |
| 1041 | + assert.ok( |
| 1042 | + unsubstitutedPlaceholders.length === 0, |
| 1043 | + `The reported message has ${unsubstitutedPlaceholders.length > 1 ? `unsubstituted placeholders: ${unsubstitutedPlaceholders.map(name => `'${name}'`).join(", ")}` : `an unsubstituted placeholder '${unsubstitutedPlaceholders[0]}'`}. Please provide the missing ${unsubstitutedPlaceholders.length > 1 ? "values" : "value"} via the 'data' property in the context.report() call.` |
| 1044 | + ); |
| 1045 | + |
1000 | 1046 | if (hasOwnProperty(error, "data")) { |
1001 | 1047 |
|
1002 | 1048 | /* |
@@ -1096,6 +1142,18 @@ class RuleTester { |
1096 | 1142 | expectedSuggestion.messageId, |
1097 | 1143 | `${suggestionPrefix} messageId should be '${expectedSuggestion.messageId}' but got '${actualSuggestion.messageId}' instead.` |
1098 | 1144 | ); |
| 1145 | + |
| 1146 | + const unsubstitutedPlaceholders = getUnsubstitutedMessagePlaceholders( |
| 1147 | + actualSuggestion.desc, |
| 1148 | + rule.meta.messages[expectedSuggestion.messageId], |
| 1149 | + expectedSuggestion.data |
| 1150 | + ); |
| 1151 | + |
| 1152 | + assert.ok( |
| 1153 | + unsubstitutedPlaceholders.length === 0, |
| 1154 | + `The message of the suggestion has ${unsubstitutedPlaceholders.length > 1 ? `unsubstituted placeholders: ${unsubstitutedPlaceholders.map(name => `'${name}'`).join(", ")}` : `an unsubstituted placeholder '${unsubstitutedPlaceholders[0]}'`}. Please provide the missing ${unsubstitutedPlaceholders.length > 1 ? "values" : "value"} via the 'data' property for the suggestion in the context.report() call.` |
| 1155 | + ); |
| 1156 | + |
1099 | 1157 | if (hasOwnProperty(expectedSuggestion, "data")) { |
1100 | 1158 | const unformattedMetaMessage = rule.meta.messages[expectedSuggestion.messageId]; |
1101 | 1159 | const rehydratedDesc = interpolate(unformattedMetaMessage, expectedSuggestion.data); |
|
0 commit comments