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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,9 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- When an agent asks CodeGraph about a separate git repository nested inside an indexed project, one the project's index leaves out (for example because the parent's `.gitignore` excludes it), it now gets the usual "isn't indexed" guidance instead of answers from the parent project's code. Nested repositories the parent does index, such as submodules, work as before. Thanks @wstczyw for the report. (#2110)
- Editing a file that defines the same name more than once, like two classes that each have an `execute` method or a method's overloads, no longer moves every caller from other files onto one of them during `codegraph sync`. A sync interrupted partway through a file also no longer loses those callers until a full re-index. Thanks @ijbranch for the report. (#2276)
- Delphi and Free Pascal include files (`.inc`) are now indexed as Pascal. Before, they were read as PHP and came up empty. A `.inc` file with a `<?php` tag is still PHP, and a `codegraph.json` mapping for `.inc` still wins. Thanks @ijbranch for the report. (#2279)
- In Python, a call written on the instance, like `self.get_breadcrumbs(...)`, now links to the method the class declares or inherits instead of a same-named imported or module-level function, unless the class body imports or assigns that name.
- Indexing a C or C++ project whose headers are included under a build flag codegraph cannot know no longer slows down exponentially with the depth of the include tree during "Resolving refs". Each such header is now read once per include state, and a hard limit stops the check on any include tree that is still too large. Thanks @danusha2345, and @coolhitmanleon for the report. (#2127)
- In tag-based CFML, calls written in tags are now linked: in `<cfset>`, `<cfif>`/`<cfelseif>`, `<cfreturn>`, `<cfloop condition>` and `#…#` expressions. Before, only `<cfscript>` and `<cfquery>` code was read, so callers and impact found almost nothing in tag-based components. Functions wrapped in tags like `<cfprocessingdirective>` or `<cfsilent>` are now indexed too, and a `<cfinterface>` is indexed as an interface that `implements` links to. Re-index CFML projects after upgrading. Thanks @HarryMuc for the report. (#2091)

## [1.6.1] - 2026-09-29

Expand Down
4 changes: 2 additions & 2 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 [`a5184ed5`](https://github.com/colbymchenry/codegraph/commit/a5184ed5d4cb4bb0a3341cdc1afdec0016152dc7) (after v1.6.1, merged 2026-10-02) 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 [`7c01910e`](https://github.com/colbymchenry/codegraph/commit/7c01910e545165cae56353044014ca03d479e646) (after v1.6.1, merged 2026-10-02) 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 @@ -1033,7 +1033,7 @@ is written):
| Lua | `.lua` | Full support (functions, methods with receivers, local variables, `require` imports, call edges) |
| R | `.R` `.r` | Full support (functions in every assignment form, S4/R5/R6 classes with methods, `library`/`require` imports, `source()` file references, call edges) |
| Luau | `.luau` | Full support (everything in Lua, plus `type`/`export type` aliases, typed signatures, and Roblox instance-path `require`) |
| CFML | `.cfc`, `.cfm`, `.cfs` | Full support (tag-based `<cfcomponent>`/`<cffunction>` and bare-script `component { ... }` styles, `extends`/`implements`, embedded `<cfscript>` delegation, call edges) |
| CFML | `.cfc`, `.cfm`, `.cfs` | Full support (tag-based `<cfcomponent>`/`<cfinterface>`/`<cffunction>` and bare-script `component { ... }` styles, `extends`/`implements`, embedded `<cfscript>` delegation, call edges, including calls written in tags such as `<cfset>`, `<cfif>`, `<cfloop condition>` and `#…#` expressions) |
| COBOL | `.cbl`, `.cob`, `.cpy` | Full support (programs, sections/paragraphs with PERFORM/GO TO call edges, CALL 'literal' cross-program calls, COPY copybook imports — including standalone `.cpy` files — DATA DIVISION records/fields/88-levels, EXEC CICS LINK/XCTL and EXEC SQL INCLUDE targets; fixed and free format) |
| Visual Basic .NET | `.vb` | Full support (classes, Modules, interfaces, structures, enums, properties, events, `Declare` P/Invoke, `Handles`/`WithEvents`, `Inherits`/`Implements` edges, call edges through VB's call/index paren ambiguity, `As New` instantiation, interpolated strings, LINQ, Unicode identifiers) |
| Erlang | `.erl`, `.hrl`, `.escript`, `.app.src`, `.app` | Full support (functions with multi-clause/multi-arity grouping, `-spec` signatures, records with fields, `-type`/`-opaque` aliases, `-define` macros, `-include`/`-include_lib`/`-import` edges, local and `mod:fn` remote call edges, `fun name/arity` references, `spawn`/`apply`/`proc_lib`/`timer`/`rpc` MFA-argument call edges, `gen_server:call/cast(?MODULE)` → own `handle_call`/`handle_cast` links, `-behaviour` links, `-export`-based visibility) |
Expand Down
85 changes: 85 additions & 0 deletions __tests__/cfml-tag-calls.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* Tag-based CFML end to end (#2091). Most calls in a tag-based component sit
* in `<cfset>`/`<cfif>`/`<cfreturn>` and `#hash#` expressions, not in
* `<cfscript>`; on a 6,000-file tag-based codebase only 6.4% of methods had a
* caller in the graph. Functions wrapped in `<cfprocessingdirective>` (the
* Application.cfc shape) and `<cfinterface>` components were missing as well,
* so their callers and implementers had nothing to land on.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';

let root = '';
let cg: CodeGraph;

beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-cfml-tag-calls-'));
const files: Record<string, string> = {
'model/Svc.cfc': `<cfcomponent>
\t<cffunction name="a">
\t\t<cfset x = b(1)>
\t\t<cfif c(2)></cfif>
\t\t<cfreturn d()>
\t</cffunction>
\t<cffunction name="b"></cffunction>
\t<cffunction name="c"></cffunction>
\t<cffunction name="d"></cffunction>
</cfcomponent>
`,
'Application.cfc': `<cfcomponent>
\t<cfprocessingdirective suppresswhitespace="true">
\t\t<cffunction name="onRequestStart">
\t\t\t<cfset loadConfig()>
\t\t</cffunction>
\t\t<cffunction name="loadConfig"></cffunction>
\t</cfprocessingdirective>
</cfcomponent>
`,
'model/ISearchable.cfc': `<cfinterface>
\t<cffunction name="search" access="public"></cffunction>
</cfinterface>
`,
'model/Catalog.cfc': `<cfcomponent implements="ISearchable">
\t<cffunction name="search" access="public"></cffunction>
</cfcomponent>
`,
};
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
cg = await CodeGraph.init(root, { index: true });
});

afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});

const edgesFrom = (file: string, kind: string): string[] => {
const ids = cg.getNodesInFile(file).map((n) => n.id);
return cg
.getOutgoingEdgesFrom(ids)
.filter((e) => e.kind === kind)
.map((e) => `${cg.getNode(e.source)!.qualifiedName} -> ${cg.getNode(e.target)!.qualifiedName}`)
.sort();
};

describe('tag-based CFML (#2091)', () => {
it('links calls written in <cfset>, <cfif> and <cfreturn> to the methods they call', () => {
expect(edgesFrom('model/Svc.cfc', 'calls')).toEqual(['Svc::a -> Svc::b', 'Svc::a -> Svc::c', 'Svc::a -> Svc::d']);
});

it('indexes a method wrapped in <cfprocessingdirective> and the calls it makes', () => {
expect(edgesFrom('Application.cfc', 'calls')).toEqual(['Application::onRequestStart -> Application::loadConfig']);
});

it('resolves <cfcomponent implements="…"> to a <cfinterface> component', () => {
const iface = cg.getNodesInFile('model/ISearchable.cfc').find((n) => n.kind === 'interface');
expect(iface?.name).toBe('ISearchable');
expect(edgesFrom('model/Catalog.cfc', 'implements')).toEqual(['model/Catalog.cfc::Catalog -> model/ISearchable.cfc::ISearchable']);
});
});
150 changes: 150 additions & 0 deletions __tests__/cpp-macro-include-walk.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/**
* #2127 — the C/C++ macro-visibility walk must stay bounded on deep include
* graphs.
*
* A header whose guard the walk cannot decide, such as any header reached
* under an unknown `#if`, would be re-scanned on every inclusion path: a
* layered include graph where each header includes the two of the next layer
* costs 2^depth header scans (upstream ran out of heap on osgEarth, whose
* `#define X_H 1` guards its reader does not take for guards; the kernel's
* does). The answers must not change: a macro definitely visible at the call
* site is still a macro, and an unknown one still is not.
*/
import { describe, it, expect, afterEach } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';

type Guard = 'value' | 'bare' | 'unknown-flag';

/** `depth` layers of two headers; each includes both headers of the next layer. */
function layeredHeaders(depth: number, guard: Guard): Record<string, string> {
const files: Record<string, string> = {};
for (let d = 0; d < depth; d++) {
for (const side of ['a', 'b']) {
const g = `H_${d}_${side}`;
const open =
guard === 'value' ? `#ifndef ${g}\n#define ${g} 1\n` :
guard === 'bare' ? `#ifndef ${g}\n#define ${g}\n` :
`#ifdef USE_${d}\n`;
const next = d + 1 < depth ? `#include "h${d + 1}_a.h"\n#include "h${d + 1}_b.h"\n` : '';
files[`h${d}_${side}.h`] = `${open}${next}#define M_${d}_${side}(x) (x)\nint f_${d}_${side}(int);\n#endif\n`;
}
}
return files;
}

const roots: string[] = [];
afterEach(() => {
for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
});

/** Index `files` and return, per caller, the files its `calls` edges land in. */
async function calleeFiles(files: Record<string, string>, callers: string[]): Promise<Record<string, string[]>> {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-walk-'));
roots.push(root);
for (const [rel, content] of Object.entries(files)) fs.writeFileSync(path.join(root, rel), content);
const cg = await CodeGraph.init(root, { index: true });
try {
const fn = (name: string) => cg.getNodesByKind('function').find((n) => n.name === name && n.filePath !== 'decoy.cpp')!;
return Object.fromEntries(callers.map((name) => [
name,
cg.getCallees(fn(name).id).filter((r) => r.edge.kind === 'calls').map((r) => `${r.node.name}@${r.node.filePath}`).sort(),
]));
} finally {
cg.close();
}
}

describe('#2127 — the macro-visibility include walk is bounded', () => {
it.each(['value', 'bare', 'unknown-flag'] as const)(
'a 40-layer include graph (2^40 inclusion paths) indexes (%s guards)',
async (guard) => {
const got = await calleeFiles({
...layeredHeaders(40, guard),
'trace.h': '#define TRACE(v) ((void)(v))\n',
'unit.cpp': '#include "h0_a.h"\n#include "h0_b.h"\nvoid early() { TRACE(0); }\n#include "trace.h"\nvoid unit() { TRACE(1); M_39_a(2); }\n',
'decoy.cpp': 'void TRACE(int v) {}\nint M_39_a(int v) { return v; }\n',
}, ['early', 'unit']);
// A call above the include that defines TRACE is still a call; a header
// macro under an undecidable `#ifdef` is not definitely visible. The
// kernel reads `#define X_H 1` as a guard, so a value guard decides.
expect(got).toEqual({
early: ['TRACE@decoy.cpp'],
unit: guard === 'unknown-flag' ? ['M_39_a@decoy.cpp'] : [],
});
},
60_000,
);

it('a walk that cannot be shortened stops at its budget and suppresses nothing past it', async () => {
// No guards and a known flag toggled on every inclusion: each visit
// changes the state, so no re-entry repeats an earlier one and the real
// preprocessor would expand all 2^22 paths too. The walk gives up instead:
// before the exploding include the macro is still known, after it nothing is.
const files: Record<string, string> = {
'toggle.h': '#ifdef T\n#undef T\n#else\n#define T\n#endif\n',
'unit.cpp': '#define T\n#define TRACE(v) ((void)(v))\nvoid before() { TRACE(1); }\n#include "h0.h"\nvoid after() { TRACE(2); }\n',
'decoy.cpp': 'void TRACE(int v) {}\n',
};
for (let d = 0; d < 22; d++) {
files[`h${d}.h`] = d + 1 < 22 ? `#include "toggle.h"\n#include "h${d + 1}.h"\n#include "h${d + 1}.h"\n` : '#include "toggle.h"\n';
}
expect(await calleeFiles(files, ['before', 'after'])).toEqual({
before: [],
after: ['TRACE@decoy.cpp'],
});
}, 60_000);
});

describe('#2127 — a walk cut off at its budget reads no type aliases', () => {
it('an Objective-C superclass alias redefined past the budget is not the earlier one', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-walk-'));
roots.push(root);
const files: Record<string, string> = {
'toggle.h': '#ifdef T\n#undef T\n#else\n#define T\n#endif\n',
'Images.h': '@interface DesktopImage : NSObject\n- (id)initWithBytes:(id)bytes;\n@end\n@interface MobileImage : NSObject\n@end\n',
'Images.m': '#import "Images.h"\n@implementation DesktopImage\n- (id)initWithBytes:(id)bytes { return self; }\n@end\n',
// The walk runs out inside h0.h, so it never reads the redefinition.
'Aliased.h': '#import "Images.h"\n#define T\n#define Image DesktopImage\n#include "h0.h"\n#undef Image\n#define Image MobileImage\n@interface Aliased : Image\n@end\n',
'Aliased.m': '#import "Aliased.h"\n@implementation Aliased\n- (id)initWithBytes:(id)bytes { return [super initWithBytes:bytes]; }\n@end\n',
};
for (let d = 0; d < 22; d++) {
files[`h${d}.h`] = d + 1 < 22 ? `#include "toggle.h"\n#include "h${d + 1}.h"\n#include "h${d + 1}.h"\n` : '#include "toggle.h"\n';
}
for (const [rel, content] of Object.entries(files)) fs.writeFileSync(path.join(root, rel), content);
const cg = await CodeGraph.init(root, { index: true });
try {
const ids = cg.getNodesInFile('Aliased.m').map((n) => n.id);
const sends = cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === 'calls').map((e) => cg.getNode(e.target)!.qualifiedName);
expect(sends).toEqual([]);
} finally {
cg.close();
}
}, 60_000);
});

describe('#2127 — indexing a deep include graph keeps #1838 macro suppression', () => {
it('the macro reached through value-guarded headers still does not bind to the decoy function', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-walk-'));
roots.push(root);
const files = {
...layeredHeaders(24, 'value'),
'trace.h': '#define TRACE(v) ((void)(v))\n',
'unit.cpp': '#include "h0_a.h"\n#include "h0_b.h"\n#include "trace.h"\nvoid unit() { TRACE(1); }\n',
'decoy.cpp': 'void TRACE(int v) {}\nvoid caller() { TRACE(2); }\n',
};
for (const [rel, content] of Object.entries(files)) fs.writeFileSync(path.join(root, rel), content);
const cg = await CodeGraph.init(root, { index: true });
try {
const fn = (name: string) => cg.getNodesByKind('function').find((n) => n.name === name)!;
const callees = (name: string) =>
cg.getCallees(fn(name).id).filter((r) => r.edge.kind === 'calls').map((r) => r.node.filePath);
expect(callees('unit')).toEqual([]);
expect(callees('caller')).toEqual(['decoy.cpp']);
} finally {
cg.close();
}
}, 60_000);
});
Loading