Skip to content
Closed
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 @@ -9,6 +9,9 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### 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.

## [1.1.3] - 2026-06-29

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, 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, 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 @@ -714,6 +714,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
249 changes: 249 additions & 0 deletions __tests__/extraction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7505,3 +7505,252 @@ 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 extract the component name from the cfcomponent tag 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 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);
});
});
});
Loading