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
@@ -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([]);
});
});
});