Skip to content

fix(affected): a --filter glob matches whole names, from any folder boundary - #3029

Merged
colbymchenry merged 1 commit into
mainfrom
claude/fix-affected-filter-globs
Oct 29, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/fix-affected-filter-globs

Conversation

@colbymchenry

@colbymchenry colbymchenry commented Oct 29, 2026 •

Copy link
Copy Markdown
Owner

Summary

codegraph affected --filter <glob> matched its glob anywhere inside a path, so tests/**/*.py missed the tests directly inside tests/, including a changed test file itself, a changed tests/x.py.bak counted as a test under tests/*.py, and e2e/*.spec.ts matched an xe2e/ folder. The glob now has to match the whole name. A pattern with a folder in it still matches that folder at any depth, as the help text's own e2e/*.spec.ts always did; a leading / anchors it at the project root.

Root cause

The affected action in src/bin/codegraph.ts turned the glob into a regex by hand, with no anchors:

const regex = options.filter
  .replace(/[+[\]{}()^$|\\]/g, '\\$&')
  .replace(/\./g, '\\.')
  .replace(/\*\*/g, '.+')
  .replace(/\*/g, '[^/]*');
customFilter = new RegExp(regex);
  • No ^/$: tests/[^/]*\.py matches inside tests/x.py.bak and other/tests/x.py, and e2e/... inside xe2e/....
  • ** became .+, which needs at least one character, so tests/.+/[^/]*\.py needs a folder below tests/.
  • /tests/*.py became /tests/..., which only a nested tests/ can match.

Fix

A small affectedFilterMatcher builds a picomatch matcher (an existing dependency, loaded lazily, only by this command) with dot: true and windows: false (stored paths always use /), tested against the project-relative path:

Pattern Matches
no slash: *_test.go, test_*.py the file name, at any depth (as before)
a slash: e2e/*.spec.ts, tests/**/*.py the path from any folder boundary, an implicit leading **/ (depth-free, as before, but only at a folder boundary)
leading / or ./: /tests/*.py from the project root only (new)
trailing /: tests/ everything under that folder (as before)

The whole name must match, and ** matches zero folders. A pattern picomatch rejects (an empty one) is reported as an invalid --filter before the index is opened. The --filter help text now says how each form matches.

A choice for Colby: slash patterns stay depth-free, so tests/*.py still matches other/tests/test_x.py, the way e2e/*.spec.ts in the help text has always matched apps/web/e2e/login.spec.ts. Our triage (T8) suggested root-anchoring instead, which is picomatch's default and what #1273 does, with **/e2e/*.spec.ts documented for the depth-free case. I kept today's reading so that no existing filter silently starts matching fewer tests, and added the leading / for anchoring. Switching to root-anchoring later is a one-line change in affectedFilterMatcher plus the help text.

Validation

  • New __tests__/cli-affected-filter-globs.test.ts (6 tests, through the built CLI, beside cli-affected-paths.test.ts): 4 fail with main's src/bin/codegraph.ts and all pass with the fix. The other 2 pin what stays the same on purpose: file-name patterns at any depth, checks/*.py (a folder outside the test conventions), dotfiles, and a trailing-slash folder.
  • cli-affected-paths and cli-affected-test-conventions pass unchanged.
  • The triage fixture (math_utils.py imported by tests at several depths), main's build vs this branch's:
Query main fix
affected math_utils.py --filter 'tests/**/*.py' tests/unit/deep/…, tests/unit/… + tests/test_math_utils.py, other/tests/test_math_utils.py
affected tests/test_math_utils.py.bak --filter 'tests/*.py' the .bak file nothing
affected tests/test_math_utils.py --filter 'tests/**/*.py' nothing tests/test_math_utils.py
--filter '/tests/*.py' other/tests/test_math_utils.py only tests/test_math_utils.py only
test_*.py, checks/*.py, tests/*.py on tests/.hidden.py unchanged unchanged
  • Full suite on the pushed commit (a062778, main 8e854d1, kernel built from that main, CODEGRAPH_KERNEL_EXPECT=1, --maxWorkers=2 for memory headroom): 791 files (785 passed, 6 skipped), 9,424 tests: 9,339 passed, 0 failed, 85 skipped.

Left out

codegraph files --pattern and the codegraph_files tool's pattern use their own unanchored glob converter (globToRegex) and have the same substring behavior; they are a different surface and are not changed here.

Reimplements #1273 by @Jseca666, who found the substring matching and the missing root-level tests; this is an independent implementation (depth-free slash patterns, root anchoring with /, a trailing-slash folder, lazy picomatch) with new tests, credited in the CHANGELOG.

🤖 Generated with Claude Code

…oundary

`codegraph affected --filter` turned its glob into an unanchored regex by
hand (`**` to `.+`, `*` to `[^/]*`, no `^` or `$`), so it matched anywhere
inside a path: `tests/**/*.py` missed `tests/x.py` (`.+` needs a folder),
a changed `tests/x.py.bak` counted as a test under `tests/*.py`, the
changed test itself was missed under `tests/**/*.py`, `e2e/*.spec.ts`
took an `xe2e/` folder, and `/tests/*.py` matched only `other/tests/...`.

The filter is now a picomatch glob (an existing dependency, loaded only by
this command) with `dot: true`, matched against the whole project-relative
path. A pattern with no slash matches the file name at any depth. One with
a slash matches from any folder boundary, an implicit leading `**/`, which
keeps the help text's own `e2e/*.spec.ts` matching `apps/web/e2e/...` as
the filter always did; a leading `/` or `./` anchors it at the project
root, and a trailing `/` takes everything under that folder. A pattern
picomatch rejects is reported as an invalid --filter instead of failing
later.

Validation: new CLI-level cli-affected-filter-globs tests (6): 4 fail on
main's source and pass with the fix; the other 2 guard behavior kept on
purpose (file-name patterns at any depth, folders outside the test
conventions, dotfiles, a trailing-slash folder). The existing affected
CLI suites pass. The triage fixture shows the same before and after.

Reimplements PR #1273 by @Jseca666.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry force-pushed the claude/fix-affected-filter-globs branch from 7dfaaaa to a062778 Compare October 29, 2026 21:42
@colbymchenry
colbymchenry merged commit 60b0d7a into main Oct 29, 2026
@colbymchenry
colbymchenry deleted the claude/fix-affected-filter-globs branch October 29, 2026 22:26
inth3shadows added a commit to inth3shadows/codegraph that referenced this pull request Oct 29, 2026
bompus added a commit to bompus/codegraph that referenced this pull request Oct 30, 2026
…er globs (#589)

* fix(spring): a route links the method it annotates, including a package-private one (colbymchenry#3009)

The Spring extractor found a mapping's handler with a pattern that wanted
`public`, `private` or `protected` (or Kotlin's `fun`) before the name, in
the next 600 characters. A package-private handler, like Spring Boot 3's
`@GetMapping List<Article> list()`, matched nothing, so its route linked the
next method declared with a modifier, or nothing at all, and a method-level
`@RequestMapping(value = "/feed", method = GET)` on such a method made no
route. A generic Kotlin `fun <T> find()` handler fell through the same way.
Separately, a route bound by name alone took the wrong overload when a tall
annotation stack (`@Operation`, `@ApiResponses`) above the mapping left the
next overload's declaration nearer the annotation.

The extractor now reads the declaration the annotation is written on: past
it and every annotation stacked with it (arguments read as balanced groups,
strings and text blocks included), the modifiers (none at all for a
package-private method), type parameters and the return type (void, a
primitive, a qualified or nested generic type, an array) to `name(`; for
Kotlin, past its modifiers to `fun`, type parameters and `name(`. A field,
a constructor or a class under the annotation gives no handler. The mapping
annotation's own arguments are read the same balanced way. At resolution,
the route's handler reference binds to the method of that name whose span
(which starts at its first annotation) holds the annotation's line, the
innermost such method; any other case resolves by name as before.

Validation, kernel engine, main 01e0ac3 vs this commit:
- realworld-java21 93e018e: Spring routes linked 4/19 -> 19/19; the 15 new
  edges each go to the method the mapping is written on.
- spring-petclinic 500158f (17 routes), spring-petclinic-kotlin 817dba3
  (18), eladmin 55fbf70 (133), mall dcaa93b (246), dolphinscheduler
  21968bc (239): same targets, only the edge metadata changes (exact-match
  0.7/0.9 -> framework 0.95).
- Controls jsoup 0c508f1 (Java, no Spring), requests 3c7baaa (Python),
  cobra adbc881 (Go): dump-graph byte-identical.
- Index time, median of 3 interleaved: mall 12.3 s vs 12.6 s, eladmin
  3.02 s vs 3.03 s (noise on a shared machine).
- New tests fail on main's java.ts (20 failing) and pass with the fix, in
  both engines; dropping the positional binding fails only the overload
  test.

Reimplements PR colbymchenry#851 by @Liurchen, who found the package-private gap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 3d19da2)

* fix(affected): a --filter glob matches whole names, from any folder boundary (colbymchenry#3029)

`codegraph affected --filter` turned its glob into an unanchored regex by
hand (`**` to `.+`, `*` to `[^/]*`, no `^` or `$`), so it matched anywhere
inside a path: `tests/**/*.py` missed `tests/x.py` (`.+` needs a folder),
a changed `tests/x.py.bak` counted as a test under `tests/*.py`, the
changed test itself was missed under `tests/**/*.py`, `e2e/*.spec.ts`
took an `xe2e/` folder, and `/tests/*.py` matched only `other/tests/...`.

The filter is now a picomatch glob (an existing dependency, loaded only by
this command) with `dot: true`, matched against the whole project-relative
path. A pattern with no slash matches the file name at any depth. One with
a slash matches from any folder boundary, an implicit leading `**/`, which
keeps the help text's own `e2e/*.spec.ts` matching `apps/web/e2e/...` as
the filter always did; a leading `/` or `./` anchors it at the project
root, and a trailing `/` takes everything under that folder. A pattern
picomatch rejects is reported as an invalid --filter instead of failing
later.

Validation: new CLI-level cli-affected-filter-globs tests (6): 4 fail on
main's source and pass with the fix; the other 2 guard behavior kept on
purpose (file-name patterns at any depth, folders outside the test
conventions, dotfiles, a trailing-slash folder). The existing affected
CLI suites pass. The triage fixture shows the same before and after.

Reimplements PR colbymchenry#1273 by @Jseca666.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 60b0d7a)

* docs(readme): track upstream through 60b0d7a

* fix(spring): read type-use annotations inside return types (colbymchenry#3009 review)

`annotatedMethodName` stopped at a type-use annotation in a qualified or
array return type, so `public String @nullable [] names()` and
`public java.util.@nullable List<String> names()` lost their route's
handler link. Read annotations at each return-type position and add both
declarations, plus a method-level `@RequestMapping`, to the Spring test.

Also correct two copied [Unreleased] bullets that claimed the CLI read
commands open the index read-only and report a rebuild: the fork's CLI
still opens read-write, so the claims did not describe this fork.
Document the affected filter's whole-name, folder-boundary, root-anchor
and trailing-slash matching in the README and the site guide.

* fix(spring): span consecutive type-use annotations after a qualifier

`java.util.@nullable @readonly List<String>` left the second annotation
unread, so the method name lookup hit `@` and the route lost its handler.
Consume every consecutive annotation after the dot, cover both a
`@GetMapping` and a method-level `@RequestMapping` handler, and add the
missing "no slash matches the file name" rule to the README's affected
filter description.

---------

Co-authored-by: Colby Mchenry <me@colbymchenry.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant