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 @@ -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)
Expand Down
93 changes: 93 additions & 0 deletions __tests__/cfml-embedded-parse-warning.test.ts
Original file line number Diff line number Diff line change
@@ -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([]);
});
});
});
33 changes: 27 additions & 6 deletions src/extraction/cfml-extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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. */
Expand Down