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

Conversation

DMartens
Copy link
Contributor

@DMartens DMartens commented Feb 2, 2024

Prerequisites checklist

What is the purpose of this pull request? (put an "X" next to an item)

[ ] Documentation update
[ ] Bug fix (template)
[ ] New rule (template)
[ ] Changes an existing rule (template)
[ ] Add autofix to a rule
[ ] Add a CLI option
[ ] Add something to the core
[x] Other, please explain: Rule Tester

What changes did you make? (Give an overview)

Fixes #18016.
Added a check for errors and suggestions which provide a messageId but not specify a data property.
This check makes sure that the replaced message has no placeholders as the required data properties were provided.

Is there anything you'd like reviewers to focus on?

  • I copied the regular expression from interpolate.js; maybe the regular expression should be exported?
  • Currently the check is only performed if there is no data property in the error. Maybe this check should run before as currently the data check does two things: detect missing placeholder data and checks whether the values match
  • Maybe add a mention for suggestions that the data is not shared with the top level data ?

@DMartens DMartens requested a review from a team as a code owner February 2, 2024 15:34
@eslint-github-bot
Copy link

Hi @DMartens!, thanks for the Pull Request

The pull request title isn't properly formatted. We ask that you update the pull request title to match this format, as we use it to generate changelogs and automate releases.

  • The commit message tag wasn't recognized. Did you mean "docs", "fix", or "feat"?
  • There should be a space following the initial tag and colon, for example 'feat: Message'.
  • The first letter of the tag should be in lowercase

To Fix: You can fix this problem by clicking 'Edit' next to the pull request title at the top of this page.

Read more about contributing to ESLint here

Copy link

netlify bot commented Feb 2, 2024

Deploy Preview for docs-eslint ready!

Name Link
🔨 Latest commit 371fa25
🔍 Latest deploy log https://app.netlify.com/sites/docs-eslint/deploys/65c67f91a197d70008c70230
😎 Deploy Preview https://deploy-preview-18073--docs-eslint.netlify.app/use/migrate-to-9.0.0
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@DMartens DMartens changed the title RuleTester: Always check for missing placeholder data in the message feat: Rule Tester checks for missing placeholder data in the message Feb 2, 2024
@eslint-github-bot eslint-github-bot bot added the feature This change adds a new feature to ESLint label Feb 2, 2024
tests/lib/rule-tester/rule-tester.js Outdated Show resolved Hide resolved
tests/lib/rule-tester/rule-tester.js Outdated Show resolved Hide resolved
lib/rule-tester/rule-tester.js Outdated Show resolved Hide resolved
@@ -304,6 +304,17 @@ function throwForbiddenMethodError(methodName, prototype) {
};
}

const messagePlaceholderRegex = /\{\{([^{}]+?)\}\}/gu;
Copy link
Sponsor Member

@bmish bmish Feb 2, 2024

Choose a reason for hiding this comment

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

I do think this regexp should be exported by a shared util file so it can be used in multiple places.

lib/rule-tester/rule-tester.js Outdated Show resolved Hide resolved
lib/rule-tester/rule-tester.js Outdated Show resolved Hide resolved
Copy link
Contributor

@JoshuaKGoldberg JoshuaKGoldberg left a comment

Choose a reason for hiding this comment

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

Seems reasonable and straightforward to me! 🙌

+1 to everything bmish said. Left a couple small comments too.

tests/lib/rule-tester/rule-tester.js Outdated Show resolved Hide resolved
tests/lib/rule-tester/rule-tester.js Show resolved Hide resolved
@DMartens
Copy link
Contributor Author

DMartens commented Feb 5, 2024

Implemented all the suggestions except for the potential exporting of the regular expression.
This could be done in different ways:

  • export directly from interpolate.js
  • create a new utility file in lib/shared/ which both interpolate.js and the rule tester uses
    Further instead of exporting the regular expression, we could export a function returning an iterator for the regular expression matches.

Also now that the missing placeholders are also checked when data is specified, there is a case in no-restricted-syntax which uses a placeholder in the provided placeholder data.
How should this be handled (e.g. should this be supported (e.g. via a recursive interpolate)?
Otherwise we could check the passed data properties from the error and do not include the detected missing placeholders.

@mdjermanovic
Copy link
Member

  • export directly from interpolate.js

I think this would be fine. interpolate.js could export { interpolate, getPlaceholderMatcher }:

  • interpolate is the same function as the current export from interpolate.js.
  • getPlaceholderMatcher returns a new instance of the regex (return /\{\{([^{}]+?)\}\}/gu). Since the regex is global and thus mutable, seems safer to always create a new instance.

@mdjermanovic
Copy link
Member

e.g. should this be supported (e.g. via a recursive interpolate)?

I think interpolate shouldn't be multipass/recursive, and the test that fails indeed expects the data text to appear in its raw form in the final message.

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.");
Copy link
Sponsor Member

@bmish bmish Feb 5, 2024

Choose a reason for hiding this comment

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

This error message seems misleading since, as @mdjermanovic said, we don't want to suggest that recursive/multipass interpolation is allowed. If there was some way to escape placeholders in data, that could solve it.

Copy link
Member

Choose a reason for hiding this comment

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

When only messageId is specified in the test case, I think we could just check if the meta message really has those placeholders we found as missing. While theoretically the same name can appear as a placeholder and in data, that seems like an extreme edge case.

@mdjermanovic
Copy link
Member

Also now that the missing placeholders are also checked when data is specified, there is a case in no-restricted-syntax which uses a placeholder in the provided placeholder data.
How should this be handled (e.g. should this be supported (e.g. via a recursive interpolate)?
Otherwise we could check the passed data properties from the error and do not include the detected missing placeholders.

If there is test data, we could just check if all placeholders from the meta message have corresponding properties in the test data object. With this check, and the already existing check whether the calculated test message text matches the actual message text, I think there's even no need to look for missing placeholders in the actual message text.

If there isn't test data, I think we can't be 100% sure that the placeholder wasn't replaced (theoretically, it could have been replaced with the exact same text).

Copy link
Member

@nzakas nzakas left a comment

Choose a reason for hiding this comment

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

Also, please update the migrate-to-9.0.0.md file to mention this change.

Comment on lines 329 to 339
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).


assert.ok(
missingPlaceholders.length === 0,
`The reported message has ${missingPlaceholders.length > 1 ? `the missing placeholders: ${missingPlaceholders.map(name => `'${name}'`).join(", ")}` : `a missing placeholder '${missingPlaceholders[0]}'`}. Please provide them via the 'data' property in the context.report() call.`
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
`The reported message has ${missingPlaceholders.length > 1 ? `the missing placeholders: ${missingPlaceholders.map(name => `'${name}'`).join(", ")}` : `a missing placeholder '${missingPlaceholders[0]}'`}. Please provide them via the 'data' property in the context.report() call.`
`The reported message has ${missingPlaceholders.length > 1 ? `unsubstituted placeholders: ${missingPlaceholders.map(name => `'${name}'`).join(", ")}` : `an unsubstituted placeholder '${missingPlaceholders[0]}'`}. Please provide them via the 'data' property in the context.report() call.`

I think that "missing placeholders" sounds like the data was provided but the corresponding placeholders were not found in the message template, while this is the opposite case - placeholders are in the template but the data for them was not provided. Maybe "missing" -> "unsubstituted"?

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 used the term missing as this is the cause of the issue. That the placeholder cannot be substituted is the result of that. But I also don't really like missing and I cannot come up with a better name for it, so I changed it in the messages and variable names.

Copy link
Member

Choose a reason for hiding this comment

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

I think this is a grammar issue: the placeholder isn't missing, it's the value to use for the placeholder that's missing. Another option would be "missing placeholder value".

lib/rule-tester/rule-tester.js Outdated Show resolved Hide resolved
@mdjermanovic
Copy link
Member

Also, there are merge conflicts in docs/src/use/migrate-to-9.0.0.md.

@mdjermanovic
Copy link
Member

@DMartens can you fix merge conflicts? We'd like to include this in today's v9.0.0-beta.0 release.

@DMartens DMartens force-pushed the rule-tester-check-missing-placeholder-data branch from 7378079 to 09420cf Compare February 9, 2024 11:42
Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>
@DMartens DMartens force-pushed the rule-tester-check-missing-placeholder-data branch from 09420cf to 22ee891 Compare February 9, 2024 11:44
docs/src/use/migrate-to-9.0.0.md Outdated Show resolved Hide resolved
lib/rule-tester/rule-tester.js Outdated Show resolved Hide resolved
lib/rule-tester/rule-tester.js Outdated Show resolved Hide resolved
@mdjermanovic mdjermanovic added core Relates to ESLint's core APIs and features accepted There is consensus among the team that this change meets the criteria for inclusion labels Feb 9, 2024
Copy link
Member

@nzakas nzakas left a comment

Choose a reason for hiding this comment

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

LGTM. Just one suggestion for clarity but otherwise good to go.

lib/linter/interpolate.js Outdated Show resolved Hide resolved
tests/lib/rule-tester/rule-tester.js Outdated Show resolved Hide resolved
docs/src/use/migrate-to-9.0.0.md Outdated Show resolved Hide resolved
lib/rule-tester/rule-tester.js Outdated Show resolved Hide resolved
lib/rule-tester/rule-tester.js Outdated Show resolved Hide resolved
Copy link
Contributor

@snitin315 snitin315 left a comment

Choose a reason for hiding this comment

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

LGTM, thank you for your contribution.

Copy link
Member

@mdjermanovic mdjermanovic left a comment

Choose a reason for hiding this comment

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

LGTM, thanks!

@mdjermanovic
Copy link
Member

I've verified that all review comments have been addressed, so merging for the release.

@mdjermanovic mdjermanovic changed the title feat: Rule Tester checks for missing placeholder data in the message feat!: Rule Tester checks for missing placeholder data in the message Feb 9, 2024
@eslint-github-bot eslint-github-bot bot added the breaking This change is backwards-incompatible label Feb 9, 2024
@mdjermanovic mdjermanovic merged commit 9163646 into eslint:main Feb 9, 2024
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
accepted There is consensus among the team that this change meets the criteria for inclusion breaking This change is backwards-incompatible contributor pool core Relates to ESLint's core APIs and features feature This change adds a new feature to ESLint
Projects
Status: Complete
Development

Successfully merging this pull request may close these issues.

Change Request: Always check data for message in rule tester
6 participants