Skip to content

Fix selector-no-qualifying-type false positives for :has()#9106

Closed
ragini-pandey wants to merge 9 commits into
stylelint:mainfrom
ragini-pandey:fix/9103-has-false-positives
Closed

Fix selector-no-qualifying-type false positives for :has()#9106
ragini-pandey wants to merge 9 commits into
stylelint:mainfrom
ragini-pandey:fix/9103-has-false-positives

Conversation

@ragini-pandey

@ragini-pandey ragini-pandey commented Feb 24, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes #9103

What's the problem?

The selector-no-qualifying-type rule incorrectly flagged type selectors inside :has() as qualifying type selectors. For example:

/* incorrectly flagged */
.form-field:has(input:disabled) { color: gray; }
.foo:has(a) {}
.foo:has(a:hover) {}

Why is it a false positive?

:has() is not a grouping pseudo-class (unlike :is() or :where()). Its argument selects descendant (or relational) elements, not the current element. So input inside .form-field:has(input:disabled) is not qualifying .form-field It expresses a condition on child nodes.

What's the fix?

In SELECTOR_CONTAINING_PSEUDO_CLASSES, :has was included (via logicalCombinationsPseudoClasses), which caused groupByCompoundSelectors to merge the inner selectors of :has() with the outer compound selector. This made inner type selectors appear to be at the same compound level as the outer class/id/attribute — triggering a false positive.

The fix excludes has from SELECTOR_CONTAINING_PSEUDO_CLASSES so its inner selectors are not merged into the outer compound. Type selectors outside :has() (e.g., a.foo:has(.bar)) are still correctly flagged.

Tests

Added four accept test cases covering the patterns from the issue report.

Type selectors inside :has() do not qualify the current element —
they apply conditions on descendant nodes. Exclude :has() from the
SELECTOR_CONTAINING_PSEUDO_CLASSES set so inner type selectors are
not merged into the outer compound selector for qualifying-type checks.

Fixes stylelint#9103
@changeset-bot

changeset-bot Bot commented Feb 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f0249f8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
stylelint Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Feb 24, 2026

Copy link
Copy Markdown
Contributor

This PR is packaged and the instant preview is available (f0249f8). View the demo website.

Install it locally:

npm i -D https://pkg.pr.new/stylelint@d3547e9

@jeddy3 jeddy3 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@ragini-pandey Thanks for making a start on this.

I've requested a change to the tests and posed a question we'll want to discuss, giving people time to chime in.

Comment thread lib/rules/selector-no-qualifying-type/__tests__/index.mjs Outdated
Comment thread lib/rules/selector-no-qualifying-type/__tests__/index.mjs Outdated
@ragini-pandey ragini-pandey requested a review from jeddy3 February 24, 2026 16:50
Comment thread lib/rules/selector-no-qualifying-type/index.mjs Outdated
@ragini-pandey ragini-pandey requested a review from Mouvedia March 1, 2026 04:27

@jeddy3 jeddy3 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM, thank you!

@jeddy3 jeddy3 mentioned this pull request Mar 18, 2026
4 tasks
Comment on lines +120 to +134
if (isHasPseudoClass(node)) {
const hasPseudo = /** @type {selectorParser.Pseudo} */ (node);

// Add :has() to the outer compound without merging inner selectors
currentCompoundSelectors.forEach((compoundSelector) => {
compoundSelector.push(hasPseudo);
});

// Evaluate inner selectors independently
hasPseudo.each((childSelector) => {
compoundSelectors.push(...groupByCompoundSelectors(childSelector));
});

return;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is incorrect even if it is very hard to observe.

I think it also exposes an existing issue in the code I wrote for groupByCompoundSelectors.

@jeddy3 Can we hold of on merging this one?
I will try to make time to investigate.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I will try to make time to investigate.

That would be fantastic. I don't believe it's a popular rule or use case, so in no way pressing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think I have a good working fix for this issue but splitting out this code into a helper and adding some unit tests will help to verify.

@Mouvedia Mouvedia added the pr: blocked is blocked by another issue or pr label Mar 18, 2026
@jeddy3 jeddy3 removed the pr: blocked is blocked by another issue or pr label Mar 19, 2026
@jeddy3

jeddy3 commented Mar 19, 2026

Copy link
Copy Markdown
Member

(I've removed the blocked label as we reserve that for external factors or dependencies. A maintainer trying to find time is just normal in OSS.)

@romainmenke

Copy link
Copy Markdown
Member

Thank you for working on this @ragini-pandey.
You changes have been merged through this PR: #9182

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Fix selector-no-qualifying-type false positives for :has()

4 participants