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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### New Features

- CodeGraph now indexes **CFML** (`.cfc`, `.cfm`, `.cfs`) — both the classic tag-based style (`<cfcomponent>`/`<cffunction>`) and modern bare-script `component { ... }` syntax, including `extends`/`implements`, embedded `<cfscript>` blocks (at any nesting depth, including inside `<cfif>`/`<cfloop>`/`<cftry>`), call edges, and calls embedded in `#hash#` expressions inside `<cfquery>` SQL bodies. Files saved with a UTF-8 byte-order mark and tags with unquoted attribute values — both common in long-lived CFML codebases — are handled too. Thanks @ghedwards. (#1118)
- The Claude Code context hook now recognizes prompts that describe code in plain words — in any language — by checking the prompt's words against the symbol names actually in your project's index. Asking about "the state machine des commandes" finds `OrderStateMachine` with no keyword involved. Confidence decides how much gets injected: structural questions and prompts naming a real symbol still get full context up front; a plain-words match gets a short pointer to the matching symbols so the agent queries them itself; everything else stays silent, exactly as before.
- Anonymous usage telemetry now counts how often the context hook injected context, offered a hint, or stayed silent — fixed counter names only; the prompt's content is never stored or sent. This makes the hook's accuracy measurable instead of guessed. The counters record what actually happened, not what was attempted: a lookup that errors or comes back empty counts as a distinct silent outcome, never as delivered context (#1143, thanks @inth3shadows).
- Metal shader files (`.metal`) are now indexed. Metal Shading Language is close enough to C++ that vertex/fragment/kernel functions, structs, type aliases, and the calls between them all land in the graph — so shader pipelines in Apple-platform projects show up in impact analysis and flow traces instead of being silently skipped. Metal's `[[buffer(0)]]`-style attribute annotations are handled so they can't corrupt what gets extracted. Thanks @FluxKo for the report. (#1121)
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ The reliable, universal payoff is **surgical context and speed**: CodeGraph coll
| **Full-Text Search** | Find code by name instantly across your entire codebase, powered by FTS5 |
| **Impact Analysis** | Trace callers, callees, and the full impact radius of any symbol before making changes |
| **Always Fresh** | File watcher uses native OS events (FSEvents/inotify/ReadDirectoryChangesW) with debounced auto-sync — the graph stays current as you code, zero config |
| **20+ Languages** | TypeScript, JavaScript, Python, Go, Rust, Java, C#, PHP, Ruby, C, C++, Objective-C, Metal, Swift, Kotlin, Scala, Dart, Lua, Luau, R, Svelte, Vue, Astro, Liquid, Pascal/Delphi |
| **20+ Languages** | TypeScript, JavaScript, Python, Go, Rust, Java, C#, PHP, Ruby, C, C++, Objective-C, Metal, Swift, Kotlin, Scala, Dart, Lua, Luau, R, CFML, Svelte, Vue, Astro, Liquid, Pascal/Delphi |
| **Framework-aware Routes** | Recognizes web-framework routing files and links URL patterns to their handlers across 17 frameworks |
| **Mixed iOS / React Native / Expo** | Closes cross-language flows that static parsing misses: Swift ↔ ObjC bridging, React Native legacy bridge + TurboModules + Fabric view components, native → JS event emitters, Expo Modules |
| **100% Local** | No data leaves your machine. No API keys. No external services. SQLite database only |
Expand Down Expand Up @@ -715,6 +715,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) |

## Measured cross-file coverage

Expand Down
308 changes: 308 additions & 0 deletions __tests__/extraction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8018,3 +8018,311 @@ GeomPoint <- ggproto("GeomPoint", Geom,
});
});
});

// =============================================================================
// CFML (ColdFusion Markup Language — .cfc/.cfm tag-based and bare-script, .cfs)
// =============================================================================

describe('CFML Extraction', () => {
describe('Language detection', () => {
it('should detect .cfc/.cfm as cfml and .cfs as cfscript', () => {
expect(detectLanguage('Service.cfc')).toBe('cfml');
expect(detectLanguage('index.cfm')).toBe('cfml');
expect(detectLanguage('Helper.cfs')).toBe('cfscript');
});

it('should report cfml and cfscript as supported', () => {
expect(isLanguageSupported('cfml')).toBe(true);
expect(isLanguageSupported('cfscript')).toBe(true);
expect(getSupportedLanguages()).toContain('cfml');
expect(getSupportedLanguages()).toContain('cfscript');
});
});

describe('Bare-script .cfc (component { ... })', () => {
const code = `
component extends="BaseService" implements="IService" {

property name="name" type="string";

function init(required string name) {
variables.name = arguments.name;
return this;
}

public string function getName() {
return variables.name;
}

private void function logSomething(required string msg) {
writeLog(text=msg);
}
}
`;

it('should name the component from the file name (the grammar has no name field)', () => {
const result = extractFromSource('SampleService.cfc', code);
const cls = result.nodes.find((n) => n.kind === 'class');
expect(cls).toBeDefined();
expect(cls?.name).toBe('SampleService');
expect(cls?.language).toBe('cfml');
});

it('should extract methods with visibility and contains edges to the class', () => {
const result = extractFromSource('SampleService.cfc', code);
const cls = result.nodes.find((n) => n.kind === 'class');
const methods = result.nodes.filter((n) => n.kind === 'method');
expect(methods.map((m) => m.name)).toEqual(
expect.arrayContaining(['init', 'getName', 'logSomething'])
);
const logSomething = methods.find((m) => m.name === 'logSomething');
expect(logSomething?.visibility).toBe('private');
const containsLog = result.edges.find(
(e) => e.source === cls?.id && e.target === logSomething?.id && e.kind === 'contains'
);
expect(containsLog).toBeDefined();
});

it('should extract extends/implements as unresolved references from the class', () => {
const result = extractFromSource('SampleService.cfc', code);
const cls = result.nodes.find((n) => n.kind === 'class');
const extendsRef = result.unresolvedReferences.find((r) => r.referenceKind === 'extends');
expect(extendsRef?.referenceName).toBe('BaseService');
expect(extendsRef?.fromNodeId).toBe(cls?.id);
const implRef = result.unresolvedReferences.find((r) => r.referenceKind === 'implements');
expect(implRef?.referenceName).toBe('IService');
expect(implRef?.fromNodeId).toBe(cls?.id);
});
});

describe('Standalone .cfs (pure CFScript)', () => {
it('should also name an anonymous component from the file name', () => {
const code = `
component {
function ping() {
return "pong";
}
}
`;
const result = extractFromSource('Sample.cfs', code);
const cls = result.nodes.find((n) => n.kind === 'class');
expect(cls).toBeDefined();
expect(cls?.name).toBe('Sample');
expect(cls?.language).toBe('cfscript');
});

it('should extract top-level imports with no enclosing component', () => {
const code = `
import com.foo.Bar;
import foo.cfm;
`;
const result = extractFromSource('Includes.cfs', code);
const imports = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name);
expect(imports).toContain('com.foo.Bar');
expect(imports).toContain('foo.cfm');
});
});

describe('Tag-based .cfc (<cfcomponent>/<cffunction>)', () => {
const code = `<cfcomponent extends="Base" implements="IFoo,IBar" output="false">
\t<cffunction name="getName" access="public" returntype="string">
\t\t<cfreturn this.name>
\t</cffunction>
\t<cffunction name="doWork" access="private" returntype="void">
\t\t<cfscript>
\t\t\tvar x = helper();
\t\t\tanotherCall(x);
\t\t</cfscript>
\t</cffunction>
</cfcomponent>
`;

it('should name the component from the file name when the tag has no name attribute', () => {
const result = extractFromSource('TagStyle.cfc', code);
const cls = result.nodes.find((n) => n.kind === 'class');
expect(cls?.name).toBe('TagStyle');
expect(cls?.language).toBe('cfml');
});

it('should prefer an explicit name attribute on the cfcomponent tag', () => {
const named = `<cfcomponent name="ExplicitName">\n<cffunction name="a"><cfreturn 1></cffunction>\n</cfcomponent>`;
const result = extractFromSource('File.cfc', named);
expect(result.nodes.find((n) => n.kind === 'class')?.name).toBe('ExplicitName');
});

it('should extract cffunction tags as methods with access-derived visibility', () => {
const result = extractFromSource('TagStyle.cfc', code);
const methods = result.nodes.filter((n) => n.kind === 'method');
expect(methods.map((m) => m.name)).toEqual(expect.arrayContaining(['getName', 'doWork']));
const getName = methods.find((m) => m.name === 'getName');
expect(getName?.visibility).toBe('public');
expect(getName?.returnType).toBe('string');
const doWork = methods.find((m) => m.name === 'doWork');
expect(doWork?.visibility).toBe('private');
});

it('should not double-extract symbols from the component body (implicit-end-tag walk)', () => {
const result = extractFromSource('TagStyle.cfc', code);
const methods = result.nodes.filter((n) => n.kind === 'method' && n.name === 'getName');
expect(methods).toHaveLength(1);
const doWorkMethods = result.nodes.filter((n) => n.kind === 'method' && n.name === 'doWork');
expect(doWorkMethods).toHaveLength(1);
});

it('should delegate <cfscript> tag bodies to the cfscript grammar and attribute calls to the enclosing method', () => {
const result = extractFromSource('TagStyle.cfc', code);
const doWork = result.nodes.find((n) => n.kind === 'method' && n.name === 'doWork');
const helperCall = result.unresolvedReferences.find(
(r) => r.referenceKind === 'calls' && r.referenceName === 'helper'
);
expect(helperCall?.fromNodeId).toBe(doWork?.id);
});

it('should produce exactly one correctly-ranged file node, not a leaked snippet-scoped one', () => {
const result = extractFromSource('TagStyle.cfc', code);
const fileNodes = result.nodes.filter((n) => n.kind === 'file');
expect(fileNodes).toHaveLength(1);
expect(fileNodes[0].startLine).toBe(1);
const cls = result.nodes.find((n) => n.kind === 'class');
const containsClass = result.edges.find(
(e) => e.source === fileNodes[0].id && e.target === cls?.id && e.kind === 'contains'
);
expect(containsClass).toBeDefined();
});
});

describe('Top-level cffunction with no enclosing cfcomponent (.cfm template)', () => {
it('should extract as a top-level function contained by the file', () => {
const code = `<cffunction name="helper" access="public" returntype="string">
\t<cfreturn "hi">
</cffunction>
`;
const result = extractFromSource('helper.cfm', code);
const fn = result.nodes.find((n) => n.kind === 'function' && n.name === 'helper');
expect(fn).toBeDefined();
const fileNode = result.nodes.find((n) => n.kind === 'file');
const containsFn = result.edges.find(
(e) => e.source === fileNode?.id && e.target === fn?.id && e.kind === 'contains'
);
expect(containsFn).toBeDefined();
});
});

describe('<cfscript> nested inside control-flow tags (<cfif>/<cfloop>/<cftry>)', () => {
it('should delegate a <cfscript> body nested inside <cfif> within a <cffunction>', () => {
const code = `<cfcomponent>
<cffunction name="doStuff">
<cfif true>
<cfscript>
helper();
</cfscript>
</cfif>
</cffunction>
</cfcomponent>
`;
const result = extractFromSource('Nested.cfc', code);
const doStuff = result.nodes.find((n) => n.kind === 'method' && n.name === 'doStuff');
expect(doStuff).toBeDefined();
const helperCall = result.unresolvedReferences.find(
(r) => r.referenceKind === 'calls' && r.referenceName === 'helper'
);
expect(helperCall?.fromNodeId).toBe(doStuff?.id);
});

it('should delegate a <cfscript> body nested inside <cfif> at top-level component scope', () => {
const code = `<cfcomponent>
<cfif true>
<cfscript>
topLevelHelper();
</cfscript>
</cfif>
</cfcomponent>
`;
const result = extractFromSource('Nested2.cfc', code);
const cls = result.nodes.find((n) => n.kind === 'class');
expect(cls).toBeDefined();
const helperCall = result.unresolvedReferences.find(
(r) => r.referenceKind === 'calls' && r.referenceName === 'topLevelHelper'
);
expect(helperCall?.fromNodeId).toBe(cls?.id);
});
});

describe('<cfquery> SQL bodies (cfquery grammar)', () => {
it('should extract a call expression embedded in a #hash# inside the SQL body', () => {
const code = `<cfcomponent>
<cffunction name="getUsers">
<cfquery name="qUsers" datasource="#variables.dsn#">
SELECT id, name FROM users WHERE owner = #getCurrentUser().getId()#
</cfquery>
<cfreturn qUsers>
</cffunction>
</cfcomponent>
`;
const result = extractFromSource('Query.cfc', code);
const getUsers = result.nodes.find((n) => n.kind === 'method' && n.name === 'getUsers');
expect(getUsers).toBeDefined();
const getCurrentUserCall = result.unresolvedReferences.find(
(r) => r.referenceKind === 'calls' && r.referenceName === 'getCurrentUser'
);
expect(getCurrentUserCall?.fromNodeId).toBe(getUsers?.id);
const getIdCall = result.unresolvedReferences.find(
(r) => r.referenceKind === 'calls' && r.referenceName === 'getId'
);
expect(getIdCall?.fromNodeId).toBe(getUsers?.id);
});
});

describe('UTF-8 BOM handling (common in CFML saved by Windows editors)', () => {
it('should route a BOM-prefixed tag-based .cfc to the tag grammar, not the script grammar', () => {
const code = `\uFEFF<cfcomponent output="false">\n<cffunction name="configure" access="public">\n<cfreturn 1>\n</cffunction>\n</cfcomponent>\n`;
const result = extractFromSource('ModuleConfig.cfc', code);
const cls = result.nodes.find((n) => n.kind === 'class');
expect(cls?.name).toBe('ModuleConfig');
const configure = result.nodes.find((n) => n.kind === 'method' && n.name === 'configure');
expect(configure).toBeDefined();
});

it('should still treat a BOM-prefixed bare-script .cfc as script', () => {
const code = `\uFEFFcomponent {\n function ping() { return "pong"; }\n}\n`;
const result = extractFromSource('Ping.cfc', code);
expect(result.nodes.find((n) => n.kind === 'class')?.name).toBe('Ping');
expect(result.nodes.find((n) => n.kind === 'method')?.name).toBe('ping');
});
});

describe('Unquoted tag attribute values (legal in older CFML)', () => {
it('should extract functions and inheritance from unquoted attributes', () => {
const code = `<cfcomponent extends=Base>\n<cffunction name=doThing access=private>\n<cfreturn 1>\n</cffunction>\n</cfcomponent>\n`;
const result = extractFromSource('Unquoted.cfc', code);
const doThing = result.nodes.find((n) => n.kind === 'method' && n.name === 'doThing');
expect(doThing).toBeDefined();
expect(doThing?.visibility).toBe('private');
const extendsRef = result.unresolvedReferences.find((r) => r.referenceKind === 'extends');
expect(extendsRef?.referenceName).toBe('Base');
});
});

describe('Functions in a component-level <cfscript> block', () => {
it('should classify them as methods of the component (ColdBox ModuleConfig shape)', () => {
const code = `<cfcomponent output="false">\n<cfscript>\nfunction configure() {\n return settings();\n}\nfunction onLoad() {\n return 1;\n}\n</cfscript>\n</cfcomponent>\n`;
const result = extractFromSource('ModuleConfig.cfc', code);
const cls = result.nodes.find((n) => n.kind === 'class');
const methods = result.nodes.filter((n) => n.kind === 'method').map((n) => n.name);
expect(methods).toEqual(expect.arrayContaining(['configure', 'onLoad']));
expect(result.nodes.filter((n) => n.kind === 'function')).toHaveLength(0);
const configure = result.nodes.find((n) => n.kind === 'method' && n.name === 'configure');
const containsEdge = result.edges.find(
(e) => e.source === cls?.id && e.target === configure?.id && e.kind === 'contains'
);
expect(containsEdge).toBeDefined();
});

it('should keep kind function for a <cfscript> inside a cffunction body', () => {
const code = `<cfcomponent>\n<cffunction name="outer">\n<cfscript>\nfunction innerHelper() { return 1; }\n</cfscript>\n</cffunction>\n</cfcomponent>\n`;
const result = extractFromSource('Outer.cfc', code);
expect(result.nodes.find((n) => n.name === 'outer')?.kind).toBe('method');
expect(result.nodes.find((n) => n.name === 'innerHelper')?.kind).toBe('function');
});
});
});
Loading