Skip to content

Honor negation patterns under default predicate quantifier#310

Open
Iron-Ham wants to merge 1 commit intodorny:masterfrom
Iron-Ham:Iron-Ham/fix-issue-260-negation-semantics
Open

Honor negation patterns under default predicate quantifier#310
Iron-Ham wants to merge 1 commit intodorny:masterfrom
Iron-Ham:Iron-Ham/fix-issue-260-negation-semantics

Conversation

@Iron-Ham
Copy link
Copy Markdown

@Iron-Ham Iron-Ham commented Apr 27, 2026

Summary

A filter rule like

mobile:
  - 'mobile/**'
  - '!mobile/**/*.md'
  - '!mobile/.config/**'
  - '.github/workflows/test_mobile.yml'

matched nearly every file in a PR — including completely unrelated paths like web/src/foo.tsx — under the default some predicate quantifier.

Each YAML pattern was compiled into its own picomatch matcher and combined via Array.prototype.some. A bare !**/*.md matcher returns true for any file that isn't a markdown file, so once it was OR'd with the positive patterns the whole rule collapsed into "match anything not in the negation set" — the inverse of what authors intended.

This change groups bare string patterns into a single matcher with gitignore-style semantics under the default quantifier:

A file matches a rule when it matches at least one positive pattern AND does not match any negation pattern.

The every quantifier path is left untouched: per-pattern matching under .every() already produces correct subtractive semantics with negations and is the documented escape hatch in the README's "Detect changes in folder only for some file extensions" example.

Behavior changes

Configuration Before After
['mobile/**', '!mobile/**/*.md']  under default quantifier Matches nearly every file in the repo Matches mobile/** excluding mobile/**/*.md
added: ['src/**', '!src/**/*.md'] (status-tagged array) Same #260 bug shape under a status tag Gitignore-style; status tag still applies
Rule consisting only of negation patterns Silently matched (incorrectly) every non-negated file Rejected at parse time with Invalid filter YAML format — users who relied on this should add an explicit ** positive
'!(**/*.tsx|**/*.less)' (single-string extglob) Worked as expected Still works (the leading ! is followed by (, so it is treated as a single picomatch pattern, not split)
every quantifier with '!**/*.md' etc. Worked as documented Unchanged

The third row is the only case where previously-broken-but-permissive behavior becomes a hard error. The previous behavior matched every non-negated file, which is almost never what the author wanted — failing loudly is better than silently flipping rule polarity.

What changed

  • src/filter.ts

    • parseFilterItemYaml now collects bare string patterns from a (possibly nested via YAML anchor) array and builds a single combined matcher via groupedStringMatcher.
    • New compileStatusPattern routes status-tagged string-array patterns through the same gitignore-style grouping so added: ['src/**', '!**/*.md'] behaves correctly.
    • New collectArrayItems recursively flattens nested arrays from YAML anchors and partitions leaves into raw string patterns vs. fully parsed FilterRuleItems.
    • groupedStringMatcher distinguishes leading-! gitignore-style negations from !(extglob) single-pattern strings (the latter are passed straight to picomatch).
    • Rules that resolve to only negation patterns (no positive include) are rejected via throwInvalidFormatError.
  • __tests__/filter.test.ts adds five regression tests:

    • The exact filter from the issue body.
    • Negation patterns inherited via YAML anchors.
    • Mixing string patterns and status-tagged patterns.
    • Status-tagged array form (added: ['src/**', '!**/*.md']).
    • Negations-only rules (bare and status-tagged) throwing at parse time.
  • dist/index.js regenerated to match.

Closes #260

A filter rule that mixed positive and bare negation patterns under the
default ('some') predicate quantifier matched nearly every file in a PR.
Each pattern was compiled into its own picomatch matcher and combined
via Array.prototype.some, so a standalone '!**/*.md' (true for any
non-markdown file) flipped the whole rule into a near-universal match.

Group bare string patterns into a single matcher with gitignore-style
semantics: a file matches when it matches at least one positive pattern
and does not match any negation pattern. The 'every' quantifier path is
unchanged, since per-pattern matching under .every() already produces
correct subtractive semantics with negations. The '!(extglob)' single-
string form is preserved by detecting only '!' not followed by '('.

Apply the same gitignore-style grouping to status-tagged array patterns
so 'added: ["src/**", "!**/*.md"]' behaves correctly. Reject rules made
up entirely of negation patterns (no positive include) so the failure
is loud rather than a silent permanent no-match.

Closes dorny#260
@Iron-Ham Iron-Ham marked this pull request as ready for review April 27, 2026 20:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Filtering out markdown files when there are multiple possible positive matches?

1 participant