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

Wrap around eslint-plugin-jsx-a11y/anchor-has-content to support createInterpolateElement usage #50065

Open
wants to merge 45 commits into
base: trunk
Choose a base branch
from

Conversation

rowasc
Copy link

@rowasc rowasc commented Apr 25, 2023

What?

Fixes #21441.

Changes:

  • packages/eslint-plugin/configs/jsx-a11y.js: disables the rule 'jsx-a11y/anchor-has-content' in favor of wordpress/a11y-interpolated-anchor, which wraps around the former to allow both checking regular JSX and not failing on interpolated elements.

  • a11y-interpolated-anchor.js adds the new wrapper rule. This rule is then enabled in the custom.js rules of the eslint-plugin.

Why?

When using anchors and createInterpolateElement together, we need to eslint-ignore the line to avoid running into the anchor-has-content error even if the anchor is valid.
This pull request introduces a wrapper around the rule so that we do not need to constantly ignore it.

How?

  • The new rule uses jsx-a11y/anchor-has-content under the hood to validate non-interpolated anchors (as normal). I considered just writing the rule from scratch, and I can still do that but as a first attempt, I decided it made sense to add a wrapper and not reinvent the linter rules for normal anchors.
  • The new rule also does some very basic checks for the potential validity of anchor tags used within createInterpolateElement. In particular, it checks that anchors are not empty.
  • The trade-off of having any linting at all for the interpolated anchor is that it could give a false sense of safety, but it would still miss issues (see the tests for some examples).

Testing Instructions

Testing Instructions for Keyboard

  • "Break" an anchor that is created with createInterpolateElement, by making it empty of content (e.g: <a> </a>)
    • It should trigger a @wordpress/a11y-anchor-has-content linter error that says Anchors must have content and the content must be accessible by a screen reader. Check the first parameter to createInterpolateElement
  • Add some empty anchor with JSX like so <a href=""></a></>, without createInterpolateElement
    • It should trigger a @wordpress/a11y-anchor-has-content linter error that says Anchors must have content and the content must be accessible by a screen reader
  • Run the tests for the eslint-plugin rules npm run test:unit -- packages/eslint-plugin/rules/__tests__/jsx-a11y-anchor-has-content.js. The a11y-anchor-has-content.js tests should pass (and all others).

@github-actions github-actions bot added the First-time Contributor Pull request opened by a first-time contributor to Gutenberg repository label Apr 25, 2023
@github-actions
Copy link

👋 Thanks for your first Pull Request and for helping build the future of Gutenberg and WordPress, @rowasc! In case you missed it, we'd love to have you join us in our Slack community, where we hold regularly weekly meetings open to anyone to coordinate with each other.

If you want to learn more about WordPress development in general, check out the Core Handbook full of helpful information.

packages/eslint-plugin/configs/custom.js Outdated Show resolved Hide resolved
};`,
},
{
// ⚠️ This is invalid code that would get flagged by jsx-a11y/anchor-is-valid, but NOT by eslint-plugin-jsx-a11y/anchor-has-content or @wordpress/a11-anchor-has-content.
Copy link
Author

Choose a reason for hiding this comment

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

I added these so that it's obvious to readers what's covered/not covered (also because I was afraid of us forgetting the scope of the linter otherwise), but LMK if they feel irrelevant or if you have suggestions on how to improve them.

Copy link
Member

Choose a reason for hiding this comment

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

It’s also possible to add names for tests according to https://eslint.org/docs/latest/integrate/nodejs-api#ruletester. Maybe that would be a way to provide additional context instead of using inline comments.

Copy link
Author

Choose a reason for hiding this comment

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

Thank you so much, completely missed the name field. I've switched to using name in most cases.

schema: [], // no options
};

const getATagsContent = ( str ) => {
Copy link
Author

Choose a reason for hiding this comment

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

I could improve this with a tokenizer a la createInterpolate element and catch more complex cases (e.g unmatched tags) if we want to. Originally did that, but felt it was adding more complexity than needed and the lint rules ended up being a bit flaky (better to do a focused createInterpolateA11y type linter for that).

Copy link
Member

Choose a reason for hiding this comment

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

It’s better to start with something simpler and improve over time. The bigger the scope, the longer it takes to review and land. Everything is going to be better than disabling the rule everywhere the helper function is used.

@@ -7,6 +7,7 @@ module.exports = {
'@wordpress/no-global-active-element': 'error',
Copy link
Author

Choose a reason for hiding this comment

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

⚠️ Missing: Docs! which I completely forgot until now. Leaving as draft until I get a little time do write some.

Copy link
Author

Choose a reason for hiding this comment

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

@gziolo gziolo added [Tool] ESLint plugin /packages/eslint-plugin [Type] Enhancement A suggestion for improvement. labels Apr 26, 2023
@gziolo
Copy link
Member

gziolo commented Apr 28, 2023

This is solid work, and this PR only needs some final touches to bring it to the finish line. Excellent job tackling this issue. I see that one of the CI jobs errors while running npm run lint:js so there needs to be and edge case in the implementation that unit tests don't catch:

Wrap around eslint-plugin-jsx-a11yanchor-has-content to support createInterpolateElement usage · WordPressgutenberg@cdb2f9e

Run the linter on some code that uses // eslint-disable-next-line jsx-a11y/anchor-has-content ( where the anchor has content )
It should not have an jsx-a11y/anchor-has-content error.
Delete the ignore rule // eslint-disable-next-line jsx-a11y/anchor-has-content in that code
It should not have an jsx-a11y/anchor-has-content error nor a @wordpress/a11y-anchor-has-content

The old rule is disabled now, so it should be safe to remove all those comments from the code. Do I miss something here?

@gziolo
Copy link
Member

gziolo commented Apr 28, 2023

@ciampo and @tyxla, you might be interested in helping to bring this PR to the finish line as it will improve the linting of WordPress components and have a tiny impact on having better a11y support that is about to be audited in #50166.

@tyxla
Copy link
Member

tyxla commented May 2, 2023

Sure thing, thanks for the ping @gziolo!

@rowasc how can we help?

@rowasc
Copy link
Author

rowasc commented May 7, 2023

@gziolo thank you so much for the review, and I am sorry for the delay here, I got back from my meetup and finally got a chance to get back to this today.

The old rule is disabled now, so removing all those comments from the code should. Do I miss something here?

No, not missing anything, sorry! I probably messed up the explanation, I will give it another go. Added the lint-ignore removal here now so that I can make sure they are all ✅ too.

⚠️ I see the unit tests for PHP were failing in the previous commit, but it seems like it's unrelated and probably fine now - will monitor and come back if they fail again.

@tyxla I would appreciate a review if you get a chance, especially after all the changes from today, but no rush at all. Other than that I will just follow your/all reviewers' direction as needed :) moving to "Ready to review" now that I added some docs.

@rowasc rowasc marked this pull request as ready for review May 7, 2023 23:05
Copy link
Member

@tyxla tyxla left a comment

Choose a reason for hiding this comment

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

Lovely work, @rowasc! Really cool idea and nice work to wrap around the existing rule.

I've left a bunch of comments, but it's mostly code style and nothing major or blocking.

I think this already looks in a pretty stable state and the tests cover a great deal of the available scenarios. 🚀

packages/eslint-plugin/configs/jsx-a11y.js Outdated Show resolved Hide resolved
Comment on lines 57 to 59
* @param {*} tags markup used in createInterpolateElement
* @param {*} tagNames names that we recognize as anchors, as configured in the eslint rules
* @return {Array} tagNames an array of the content of the <a> (or other) tags
Copy link
Member

Choose a reason for hiding this comment

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

We could vertically align the different sections for better readability. ESLint will usually complain, but then it is likely just not enabled here.

Copy link
Author

Choose a reason for hiding this comment

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

Thanks, agree. This is actually something I had to do because ESLint complained :( I had them aligned but it kept pestering me about it. Later I realized it was because the alignment rule is different if there is no description in the Doc block

Copy link
Author

Choose a reason for hiding this comment

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

#50065 (comment) Apparently it also gets messed up in other cases - added more context in the link

const regex = new RegExp( `<(${ tagText })>(.*?)</(${ tagText })>`, 'g' );
const tagParts = [ ...tags.matchAll( regex ) ];
return tagParts.map( ( element ) => {
return element[ 2 ].trim();
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps we actually intend to have the leading/trailing space?

Copy link
Author

Choose a reason for hiding this comment

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

yep, but in that case, it would not result in an empty anchor content (for the purposes of the rule, that's all that we care about) - however, the comment made me realize I can improve this abstraction a bit since it's not really clear what the intention is. Thanks!

As an example <a> Hello world </a> would be totally fine, because even if you remove trailing spaces you don't end up with an empty anchor, so the rule won't complain. However <a> </a> is not valid, as the result would be a '' after the trim, and the rule would complain at you.

// An unknown function call in a translate function may produce a valid string
// but verifying it is not straight-forward, so we bail
if ( args.filter( Boolean ).length === 0 ) {
nodeStr = null;
Copy link
Member

Choose a reason for hiding this comment

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

Why is this necessary if we override it immediately on L91?

Copy link
Author

Choose a reason for hiding this comment

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

😬 this was just a mistake while refactoring and I didn't have a test to cover it, thanks for catching it
Added a quick scenario to cover it https://github.com/WordPress/gutenberg/pull/50065/files#diff-4c555e1042f88bdebf5cba43ba582ae78b721c7d64c1d0cc527ea1975ef191edR61

const componentOptions = options.components || [];
const typeCheck = [ 'a' ].concat( componentOptions );
if (
typeCheck.filter( ( type ) => type === node?.name?.name )
Copy link
Member

Choose a reason for hiding this comment

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

Do we also need to filter out nullish values before we check the length?

Copy link
Author

Choose a reason for hiding this comment

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

I didn't think so, but I've added a check to be safe in case something is misconfigured, thanks for catching it

Copy link
Member

@tyxla tyxla left a comment

Choose a reason for hiding this comment

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

Lovely work, @rowasc! Really cool idea and nice work to wrap around the existing rule.

I've left a bunch of comments, but it's mostly code style and nothing major or blocking.

I think this already looks in a pretty stable state and the tests cover a great deal of the available scenarios. 🚀

@gziolo
Copy link
Member

gziolo commented May 8, 2023

No, not missing anything, sorry! I probably messed up the explanation, I will give it another go. Added the lint-ignore removal here now so that I can make sure they are all ✅ too.

Excellent. Removing all comments that disable the original rule with all the unit tests covering possible cases is the best way to validate correctness and all we need to move forward 👍

For coding styles, you might want to try running ESLint with --fix flag as it might help with some comments that @tyxla left 😀

rowasc and others added 24 commits May 30, 2023 10:33
…t in two steps

Before, we had a method that just returned the content of anchors and the actual validation was done by the main function in the linter. The problem there was that there wasn't a clear enough abstraction indicating the intent of the matcher that identified which tags were empty, causing confusion about it https://github.com/WordPress/gutenberg/pull/50065/files#r1187499276. Now, instead of getATagsContent we have validateAnchors which throws an error with the same code we report through eslint if anchors are empty or not valid.

Co-Authored-By: Boro <[email protected]>
…hing useful :)

- Fix issue where we were matching the wrong messageId for nodes with the latest refacotr
Before we were automatically using the e.message thrown by the validator, which we had looked into and should not throw but to be safe, added a clause to rethrow if the unexpected thing happens instead of using the eslint report for normal non lint issues
The previous samples were including more context than strictly necessary, making it harder to understand the point of the correct/incorrect usage scenarios. Hopefully this is minimal and still clear enough to be a good UX
There was so much clutter that it was hard to understand the tests. We now get the same benefits with less code _shrugs_
Instead of re-throwing when there is a problem in the linter code, we use the eslint reporter to tell the user that this is not their fault :) we also give them context and nudge them to report it as a bug. This way they don't spend time chasing down something that is not their problem.

Co-Authored-By: Boro <[email protected]>
@rowasc rowasc force-pushed the eslint/anchor-has-content-interpolated-compat branch from 5fb816e to 8bc1ad9 Compare May 30, 2023 13:36
@ObliviousHarmony ObliviousHarmony removed their request for review June 30, 2023 17:35
@geriux geriux removed their request for review February 26, 2024 09:51
@michalczaplinski michalczaplinski removed their request for review May 29, 2024 09:41
@dcalhoun dcalhoun removed their request for review August 28, 2024 18:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
First-time Contributor Pull request opened by a first-time contributor to Gutenberg repository [Tool] ESLint plugin /packages/eslint-plugin [Type] Enhancement A suggestion for improvement.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

createInterpolateElement() triggers jsx-a11y/anchor-has-content
5 participants