Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,10 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- For Hermes Agent, `codegraph install` and `upgrade` no longer add `mcp-codegraph` to `platform_toolsets.cli`. Hermes up to 0.22.2 reported that name as an unknown toolset on every start, and Hermes 0.22.4 reads it as the only MCP server to load, hiding your others from CLI sessions; an entry an earlier install added is taken out again. A refresh also no longer resets your own `mcp_servers.codegraph` settings, such as an absolute command path, timeouts or `enabled: false`, and a config indented or written differently from Hermes's default no longer gets a duplicate or unreadable entry. Thanks @Huy3ko for the report and @conifercone for confirming it. (#2485)
- `codegraph upgrade` now upgrades a global npm install when run from your home folder, which holds npm's global folder on Windows and with tools like nvm: before, it ran `npm install` without `-g` there, which left the global copy on the old version and put a second copy in your home folder. A copy installed in a project is now upgraded in that project wherever you run the command, with a warning if npm left it on the old version, and a copy installed with pnpm, yarn or bun gets the command that upgrades it instead of npm. Thanks @timlincool for the report and @maxmilian for the first fix. (#2477)
- `codegraph status` no longer says the index is up to date while indexed files are missing their symbols: it now names files the parser couldn't read and files stored without their symbols (which `codegraph sync` repairs), `status --json` counts both, and `codegraph files --json` lists each file's recorded errors. Thanks @lipchey for the report. (#2336)
- A qualified name given to `codegraph callers`, `callees` or `impact`, or to `codegraph_node`, like `Format.Price`, now names only the symbol whose scope is spelled that way, so its answer no longer merges in `LegacyFormat.Price` or any other class whose name only ends with `Format`, nor a class that just sits in a file named `Format.cs`. In C#, `BasketService.AddItemToBasket` no longer also selects the `IBasketService` interface's method; ask for `IBasketService.AddItemToBasket` for that one. A file or folder still names the module in Rust, Python and Go, and a PHP namespace can be written with its own backslashes, like `Billing\Format::price`. No re-index needed. Thanks @Gobakabana for the report and @yang0228. (#2908)
- `codegraph affected --filter` now matches its glob against whole names: `tests/**/*.py` also finds the tests directly inside `tests/`, including a changed test file itself, `tests/*.py` no longer counts a changed `tests/x.py.bak` as a test, and `e2e/*.spec.ts` no longer matches a folder that only ends in `e2e`, like `xe2e/`. A pattern with a folder in it still matches that folder at any depth, so `e2e/*.spec.ts` keeps finding `apps/web/e2e/login.spec.ts`; start it with `/` to match from the project root only. Thanks @Jseca666.
- `codegraph init` and `codegraph index` no longer report a file you deleted as one that "could not be read" while git still lists it, which it does until the deletion is staged: such a file is now skipped, and so is a symlink that points at nothing. A file that is there but can't be read is still reported. Thanks @xianzhi-xzw for the report and the first fix. (#2904)
- A full index of a project that holds a video named `.ts`, like an MPEG-TS clip among test fixtures, no longer warns that the index is PARTIAL and missing a file, and `codegraph status` no longer reports the index as partial: the video is skipped on purpose, and it now counts as skipped. Thanks @Hetox for the original report. (#1910)
- In CFML, a `<cfquery>` or `<cfscript>` block the parser can't read no longer marks its whole file as contributing nothing to the graph: `codegraph init` no longer warns about the file, and `codegraph status` no longer lists it among files whose symbols are missing, since its component, functions and calls are all indexed. The file's recorded errors now name the block and its line instead. Thanks @HarryMuc for the report. (#2654)
- In Erlang, a comment inside an argument or parameter list, like the one in `add_to_includes( % give access to .hrl in app's src/` before its two arguments, no longer counts as an argument. The call now links to the function of its real arity, and a function whose parameter list holds a comment keeps its own arity. Before, the comment raised the arity by one, so the call linked to nothing. Re-index Erlang projects after upgrading. Thanks @Dshuishui. (#1619)
- Indexing large Python projects is much faster again and needs less memory: since 1.6.2, resolving Python references re-read source files over and over, so a project the size of CPython took several times as long to index. The graph it builds is unchanged. Thanks @bompus for the report. (#2332)
Expand Down Expand Up @@ -512,6 +516,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- Axum routes now link to their handler when rustfmt wraps the `.route(` call so the handler sits on a later line, and when the handler's name also appears elsewhere on the line, as in `delete(handlers::user::delete)` or `.route("/login", post(handlers::user::login))`; Actix's `web::resource(…)` and `.route(…)` registrations get the same fix. A call inside a closure handler is no longer mistaken for a route, and an Actix resource no longer takes the next route's handler as its own. Thanks @mg-mg-mg for the report and @danusha2345. (#2326)

- In Play projects, a route or a call like `MediaType.parse(...)` now links to a method of the class or object it names, and a call to an overloaded one to the overload its arguments fit. Before, it went to the first method of that name anywhere in the class's file, often a nested or sibling class's. When a Scala and a Java type share a name, as Play's own `RequestAuthority` does, each language's calls now reach their own. Thanks @danusha2345.
- In Spring projects, a route now links to the method its mapping annotation is written on when that method has no access modifier, like `@GetMapping List<Article> list()` in a package-private controller, and a method-level `@RequestMapping(value = "/feed", method = GET)` on such a method now makes a route. Before, such a handler was skipped, so its route linked the next method declared `public`, `private` or `protected`, or nothing, and the handler looked unused. An overloaded handler under a tall stack of annotations, like `@Operation` and `@ApiResponses` above `@GetMapping`, now links the overload it annotates rather than its neighbour. Re-index Spring projects after upgrading. Thanks @Liurchen.
- In JavaScript and TypeScript, the functions written inside a named object literal are now symbols of their own even when the object isn't exported: a plain `const api = { load() {…} }`, an object declared inside an IIFE or a function, and a namespace hung on the page or on another object, like `window.App = { init() {…} }`, `App.utils = {…}` or `dw_page = {…}`. Calls such as `api.load()`, `window.App.init()`, `App.utils.pad()`, a sibling's `this.render()`, and `App.init()` from another script on the page now reach them, the calls made inside a member belong to that member instead of the object, and a bare `init()` is no longer taken for `App.init`, so script-tag apps no longer lose most of their code from `callers` and impact. A member's qualified name now carries its object, exported or not (`api::load`, `window.App::init`), so asking for `App.init` finds it. Re-index JavaScript and TypeScript projects after upgrading. Thanks @tkhoaaa for the report and @danusha2345. (#2300)

- Indexing large Python projects is much faster again and needs less memory: since 1.6.2, resolving Python references re-read source files over and over, so a project the size of CPython took several times as long to index. The graph it builds is unchanged. Thanks @bompus for the report. (#2332)
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Follow [@getcodegraph](https://x.com/getcodegraph) on X for updates.

## About this fork

This is **bompus/codegraph**, a fork of [colbymchenry/codegraph](https://github.com/colbymchenry/codegraph). Its default branch, `fork/consolidated`, contains upstream `main` through [`69af19ac`](https://github.com/colbymchenry/codegraph/commit/69af19ace270f2e940444d514de6dde971df12df) (after v1.6.2) plus the fork's own work, and it takes upstream changes as they land. Changes that suit upstream are also offered there as pull requests.
This is **bompus/codegraph**, a fork of [colbymchenry/codegraph](https://github.com/colbymchenry/codegraph). Its default branch, `fork/consolidated`, contains upstream `main` through [`60b0d7a9`](https://github.com/colbymchenry/codegraph/commit/60b0d7a92dc001de1be37943ef3cd1cc955d693d) (after v1.6.2) plus the fork's own work, and it takes upstream changes as they land. Changes that suit upstream are also offered there as pull requests.

The fork publishes no releases. The install scripts, npm package, badges and `codegraph upgrade` further down this page install **upstream's** releases. To run the fork, build it from source (below).

Expand Down Expand Up @@ -80,7 +80,7 @@ Session search uses `.codegraph/sessions-v2.db`. The first session search import

### What the fork adds

Compared with upstream `main` at `69af19ac` (after v1.6.2). Each item here and in the dispatch and framework lists below was checked against upstream's tree at that commit.
Compared with upstream `main` at `60b0d7a9` (after v1.6.2). Each item here and in the dispatch and framework lists below was checked against upstream's tree at that commit.

| Feature | Upstream | Fork | What it does |
|---|:-:|:-:|---|
Expand Down Expand Up @@ -812,14 +812,14 @@ Traces import dependencies transitively to find which test files are affected by
```bash
codegraph affected src/utils.ts src/api.ts # Pass files as arguments
git diff --name-only | codegraph affected --stdin # Pipe from git diff
codegraph affected src/auth.ts --filter "e2e/*" # Custom test file pattern
codegraph affected src/auth.ts --filter "e2e/*" # Whole-name glob; matches at any folder depth
```

| Option | Description | Default |
|--------|-------------|---------|
| `--stdin` | Read file list from stdin | `false` |
| `-d, --depth <n>` | Max dependency traversal depth | `5` |
| `-f, --filter <glob>` | Custom glob to identify test files | auto-detect |
| `-f, --filter <glob>` | Whole-name glob for test files: a pattern with no slash matches the file name at any depth; one with `/` matches from any folder boundary; a leading `/` or `./` anchors it at the project root; a trailing `/` takes everything under that folder | auto-detect |
| `-j, --json` | Output as JSON | `false` |
| `-q, --quiet` | Output file paths only | `false` |

Expand Down
122 changes: 122 additions & 0 deletions __tests__/cli-affected-filter-globs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// `codegraph affected --filter <glob>` matches whole names (#1273).
//
// The filter used to be hand-converted to an unanchored regex (`**` → `.+`,
// `*` → `[^/]*`), so it matched anywhere inside a path: `tests/**/*.py`
// missed `tests/x.py` (`.+` needs a folder), `tests/*.py` took a changed
// `tests/x.py.bak` for a test, and `e2e/*.spec.ts` took an `xe2e/` folder.
// A pattern with a slash still matches from any folder boundary, as the help
// text's own `e2e/*.spec.ts` always did for `apps/web/e2e/…`; a leading `/`
// anchors it at the project root. Exercised end-to-end against the built binary.
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { execFileSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';

const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js');

describe('codegraph affected --filter globs (#1273)', () => {
let dir: string;

function affected(changed: string, filter: string): string[] {
const out = execFileSync(process.execPath, [BIN, 'affected', changed, '--filter', filter, '--quiet', '-p', dir], {
encoding: 'utf-8',
env: { ...process.env, CODEGRAPH_NO_DAEMON: '1', CODEGRAPH_WASM_RELAUNCHED: '1' },
stdio: ['ignore', 'pipe', 'pipe'],
});
return out.split('\n').map((s) => s.trim()).filter(Boolean);
}

beforeAll(async () => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-affected-globs-'));
const w = (rel: string, body: string) => {
fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true });
fs.writeFileSync(path.join(dir, rel), body);
};
const pyTest = 'from math_utils import add_one\n\ndef test_add_one():\n assert add_one(1) == 2\n';
w('math_utils.py', 'def add_one(value):\n return value + 1\n');
w('tests/test_math_utils.py', pyTest);
w('tests/unit/test_math_utils.py', pyTest);
w('tests/unit/deep/test_math_utils.py', pyTest);
w('other/tests/test_math_utils.py', pyTest);
w('checks/math_check.py', pyTest);
w('tests/test_math_utils.py.bak', '# a backup, not a test\n');
w('tests/.hidden.py', '# a dotfile the filter names\n');
w('src/login.ts', 'export function login(user: string): boolean { return user.length > 0; }\n');
const spec = (rel: string) => `import { login } from '${rel}';\ntest('logs in', () => login('a'));\n`;
w('e2e/login.spec.ts', spec('../src/login'));
w('apps/web/e2e/login.spec.ts', spec('../../../src/login'));
w('xe2e/login.spec.ts', spec('../src/login'));
const cg = CodeGraph.initSync(dir);
try {
await cg.indexAll();
} finally {
cg.close();
}
}, 60_000);

afterAll(() => {
fs.rmSync(dir, { recursive: true, force: true });
});

it('lets ** match zero folders, so a test directly in tests/ counts', () => {
// main: only the two files below tests/unit/.
expect(affected('math_utils.py', 'tests/**/*.py')).toEqual([
'other/tests/test_math_utils.py',
'tests/test_math_utils.py',
'tests/unit/deep/test_math_utils.py',
'tests/unit/test_math_utils.py',
]);
// main: [] — the changed test itself didn't match its own filter.
expect(affected('tests/test_math_utils.py', 'tests/**/*.py')).toEqual(['tests/test_math_utils.py']);
});

it('matches the whole file name, so a .bak file is not a test', () => {
// main: listed the backup as an affected test.
expect(affected('tests/test_math_utils.py.bak', 'tests/*.py')).toEqual([]);
});

it('matches a slash pattern at folder boundaries only, at any depth', () => {
// main: also listed xe2e/login.spec.ts.
expect(affected('src/login.ts', 'e2e/*.spec.ts')).toEqual(['apps/web/e2e/login.spec.ts', 'e2e/login.spec.ts']);
// Depth-free by design: tests/*.py keeps matching a nested tests/ folder.
expect(affected('math_utils.py', 'tests/*.py')).toEqual(['other/tests/test_math_utils.py', 'tests/test_math_utils.py']);
});

it('anchors a pattern that starts with / or ./ at the project root', () => {
expect(affected('math_utils.py', '/tests/*.py')).toEqual(['tests/test_math_utils.py']);
expect(affected('math_utils.py', './tests/*.py')).toEqual(['tests/test_math_utils.py']);
expect(affected('math_utils.py', '/tests/**/*.py')).toEqual([
'tests/test_math_utils.py',
'tests/unit/deep/test_math_utils.py',
'tests/unit/test_math_utils.py',
]);
expect(affected('src/login.ts', '/e2e/*.spec.ts')).toEqual(['e2e/login.spec.ts']);
});

it('takes everything under a folder named with a trailing slash', () => {
expect(affected('math_utils.py', 'tests/')).toEqual([
'other/tests/test_math_utils.py',
'tests/test_math_utils.py',
'tests/unit/deep/test_math_utils.py',
'tests/unit/test_math_utils.py',
]);
});

it('keeps file-name patterns at any depth, folders outside the test conventions, and dotfiles', () => {
expect(affected('math_utils.py', 'test_*.py')).toEqual([
'other/tests/test_math_utils.py',
'tests/test_math_utils.py',
'tests/unit/deep/test_math_utils.py',
'tests/unit/test_math_utils.py',
]);
expect(affected('src/login.ts', '*.spec.ts')).toEqual([
'apps/web/e2e/login.spec.ts',
'e2e/login.spec.ts',
'xe2e/login.spec.ts',
]);
expect(affected('math_utils.py', 'checks/*.py')).toEqual(['checks/math_check.py']);
expect(affected('tests/.hidden.py', 'tests/*.py')).toEqual(['tests/.hidden.py']);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"timestamp": "2026-10-30T05:02:30.605Z",
"corpus": "petclinic",
"repo": "https://github.com/spring-projects/spring-petclinic.git",
"commit": "818c413",
"codegraphSha": "98213654",
"summary": {
"absentCases": 0,
"absentHeld": 0,
"presentCases": 1,
"presentHeld": 1,
"unjudgeable": 0
},
"resolvedBy": {
"(none)": 986,
"instance-method": 270,
"exact-match": 152,
"import": 44,
"framework": 27,
"function-ref": 2,
"file-path": 1
},
"edges": 1482,
"results": [
{
"caseId": "petclinic-bounded-type-parameter",
"expect": "present",
"pass": true,
"found": [
{
"from": "method:2160e6191fa145dc29200531c30d1a5d",
"to": "method:efb8586338e2f81ea9d7cb6f694a2f0d",
"resolvedBy": "instance-method"
},
{
"from": "method:2160e6191fa145dc29200531c30d1a5d",
"to": "method:efb8586338e2f81ea9d7cb6f694a2f0d",
"resolvedBy": "instance-method"
}
],
"missingEndpoints": []
}
]
}
Loading