fix(cfml): a <cfquery> or <cfscript> block that fails to parse no longer flags the file (#2843)

Both delegates in cfml-extractor.ts read a block with its own grammar
through a fresh TreeSitterExtractor and copied its errors onto the file.
That extractor warns "parse produced no symbols (tree has errors) - the
file is indexed but contributes nothing to the graph" when its tree has
errors and it found no symbols: true of the block, not of the file, and a
`<cfquery>` body never declares a symbol. So `codegraph init` printed the
warning and `codegraph status` counted the file among those whose symbols
are missing, though its component, methods and calls were indexed.
extractTagExpressions already drops the warning for the same reason.

Fix: the delegates record that warning as one naming the block and its
line, with code partial_parse, so `codegraph files --json` shows which
block failed while init and status, which key on parse_error, no longer
report the file. Other errors pass through unchanged.

The issue's own example SQL parses cleanly with the bundled cfquery
grammar, so the reporter's trigger was not reproduced; any query body the
grammar can't read (here `EQV` in a <cfif> condition) and any
statement-only <cfscript> block with a syntax error showed the same
warning.

Validation:
- New cfml-embedded-parse-warning.test.ts: the repro DAO keeps its class,
  both methods and the call inside the failing query, and records one
  partial_parse warning naming <cfquery> at line 6; a failing <cfscript>
  block likewise; getIndexHealth lists neither file. All 3 fail on main's
  extractor and pass. status-index-health.test.ts passes.
- On the repro, main prints the whole-file warning during init and status
  says 1 file could not be parsed so its symbols are missing; with the fix
  neither does.
- A/B vs main f406fe00: FW/1, BlogCFC, ColdBox, ContentBox, Preside and
  CFWheels unchanged; in Wheels' vendor/wheels core (indexed as its own
  project) one file changed, tests/html.cfm, whose <cfscript> block at line
  8 fails: its warning now names that block. Nodes, edges and references
  are identical in every repo. express and flask controls byte-identical.
- Index time, median of 4 interleaved runs, CPU with workers, main vs
  fix: Preside 57.1 / 54.5 s, Wheels core 33.2 / 32.8 s. Within noise.

Fixes #2654

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-23 12:32:36 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent ef05970bb4
commit 6500a3782b
3 changed files with 121 additions and 6 deletions
+1
View File
@@ -63,6 +63,7 @@ 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)
- 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)
- 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)
- In VB.NET, reading or setting a `Shared` field or property through its class or module name, like `AppSession.SessionId`, `AppSession.CurrentUser = "demo"` or `AppSession.Items(0)`, now counts as a use of that member and of the class, and so do reading an `Enum` value like `Mode.Fast` and calling a `Shared` function without parentheses. Before, only calls written with parentheses were linked, so `codegraph callers` on such a field, property or class came back empty and impact missed most of the code that depends on it. A local, parameter or field that only shares a class's name is not mistaken for the class. Thanks @serkanince for the report and @ChrisPrapas. (#2305)
- In Vue and Nuxt components, calls written in the template (`{{ formatDate(post) }}`, `:to="getAccountRoute(account)"`, `@click="save(item)"`) and calls in a destructuring declaration at the top of `<script setup>` or of a TypeScript or JavaScript file (`const { t } = useI18n()`) now link to the function they call, so `callers` and impact for a composable or helper used this way are no longer mostly empty. A `v-for` item or slot prop is never taken for a project function, a handler like `@click="open = false"` no longer links to an unrelated function of the same name, and a component's own function named like a Nuxt helper, such as `clearError`, is now the one its calls reach. Thanks @deniskern for the report, and @drakeo338 and @L0garithmic. (#2340)
@@ -0,0 +1,93 @@
/**
* A `<cfquery>` or `<cfscript>` block that fails to parse (#2654). Each block
* is read with its own grammar, and that extraction warned "parse produced no
* symbols (tree has errors) — the file is indexed but contributes nothing to
* the graph" — about the block, which in a query never declares a symbol. The
* warning landed on the whole file, so `init` printed it and `status` counted
* the file as one whose symbols are missing, though its component, methods and
* calls were all in the graph. The file now records which block failed, under
* a code of its own.
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { CodeGraph } from '../src';
import { extractFromSource } from '../src/extraction';
import { initGrammars, loadAllGrammars } from '../src/extraction/grammars';
// `EQV` in the condition is one of the shapes the bundled query grammar can't read.
const dao = `<cfcomponent output="false">
\t<cffunction name="getPages" access="public" returntype="query">
\t\t<cfargument name="a" type="boolean" required="true">
\t\t<cfargument name="b" type="boolean" required="true">
\t\t<cfset var q = "">
\t\t<cfquery name="q" datasource="#dsn#">
\t\t\tSELECT <cfif arguments.a EQV arguments.b>COUNT(*)<cfelse>p.*</cfif>
\t\t\tFROM pages p
\t\t\tWHERE p.site = <cfqueryparam value="#getSiteId()#" cfsqltype="cf_sql_integer">
\t\t</cfquery>
\t\t<cfreturn q>
\t</cffunction>
\t<cffunction name="getSiteId" access="public" returntype="numeric">
\t\t<cfreturn 1>
\t</cffunction>
</cfcomponent>
`;
const svc = `<cfcomponent output="false">
\t<cffunction name="build" access="public" returntype="any">
\t\t<cfscript>
\t\t\tdoThing(1, ;
\t\t\tother();
\t\t</cfscript>
\t\t<cfreturn this>
\t</cffunction>
</cfcomponent>
`;
beforeAll(async () => {
await initGrammars();
await loadAllGrammars();
});
describe('CFML blocks that fail to parse (#2654)', () => {
it('names the <cfquery> that failed, not the whole file, and keeps what the file declares and calls', () => {
const result = extractFromSource('appsettingsdao.cfc', dao);
expect(result.nodes.filter((n) => n.kind === 'class' || n.kind === 'method').map((n) => n.name).sort())
.toEqual(['appsettingsdao', 'getPages', 'getSiteId']);
expect(result.unresolvedReferences.some((r) => r.referenceName === 'getSiteId' && r.referenceKind === 'calls')).toBe(true);
expect(result.errors).toEqual([
expect.objectContaining({ severity: 'warning', code: 'partial_parse', line: 6, message: expect.stringContaining('<cfquery> at line 6') }),
]);
expect(result.errors.some((e) => /no symbols|contributes nothing/.test(e.message))).toBe(false);
});
it('names a <cfscript> block that failed', () => {
const result = extractFromSource('svc.cfc', svc);
expect(result.nodes.some((n) => n.kind === 'method' && n.name === 'build')).toBe(true);
expect(result.errors).toEqual([
expect.objectContaining({ severity: 'warning', code: 'partial_parse', line: 3, message: expect.stringContaining('<cfscript> at line 3') }),
]);
});
describe('in the index', () => {
let dir = '';
let cg: CodeGraph;
beforeAll(async () => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cfml-block-warning-'));
fs.writeFileSync(path.join(dir, 'appsettingsdao.cfc'), dao);
fs.writeFileSync(path.join(dir, 'svc.cfc'), svc);
cg = await CodeGraph.init(dir, { silent: true });
await cg.indexAll();
});
afterAll(() => {
cg?.close?.();
if (dir) fs.rmSync(dir, { recursive: true, force: true });
});
it('does not count the files as ones whose symbols are missing', () => {
expect(cg.getIndexHealth().parseErrors).toEqual([]);
});
});
});
+27 -6
View File
@@ -699,8 +699,32 @@ export class CfmlExtractor {
}
this.unresolvedReferences.push(ref);
}
for (const error of result.errors) {
if (error.line) error.line += startLine;
this.addBlockErrors(result.errors, scriptTag, 'cfscript', startLine);
}
/**
* Record a delegated block's errors as the file's (#2654). The block's own
* extraction warns "parse produced no symbols … contributes nothing to the
* graph" when its code has a syntax error and declares nothing — which a
* `<cfquery>` body never does, so any SQL its grammar can't read earned it.
* That is about the block, not the file, whose symbols come from the tag
* walk: it is recorded as a block of the file that failed to parse, by its
* tag and line, under its own code, so `init` and `status` don't report the
* file as one that contributes nothing. Other errors pass through.
*/
private addBlockErrors(errors: ExtractionError[], tag: SyntaxNode, tagName: 'cfscript' | 'cfquery', lineOffset: number): void {
for (const error of errors) {
if (error.severity === 'warning' && error.code === 'parse_error') {
const line = tag.startPosition.row + 1;
this.errors.push({
message: `${this.filePath}: <${tagName}> at line ${line} has a syntax error — the rest of the file is indexed, but calls inside that block may be missing from the graph`,
line,
severity: 'warning',
code: 'partial_parse',
});
continue;
}
if (error.line) error.line += lineOffset;
this.errors.push(error);
}
}
@@ -733,10 +757,7 @@ export class CfmlExtractor {
if ((!ref.fromNodeId || ref.fromNodeId === innerFileNodeId) && parentId) ref.fromNodeId = parentId;
this.unresolvedReferences.push(ref);
}
for (const error of result.errors) {
if (error.line) error.line += startLine;
this.errors.push(error);
}
this.addBlockErrors(result.errors, queryTag, 'cfquery', startLine);
}
/** Read a `cf_attribute`'s value by name from a tag node's direct `cf_attribute`/`cf_tag_attributes` children. */