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

### Fixes

- Incremental updates now detect frameworks introduced by newly added files, so their routes appear without a full re-index.

### Fixes

- In VB.NET, every member of a `Structure` is now indexed, including its fields, properties, methods, constructors and nested enums. Before, only the first member was, so the rest could not be found and their callers looked empty.
- In VB.NET and C#, what a property's `Get` and `Set` code calls, creates and reads now belongs to that property, as do C#'s `get => …` accessors and `=> …` property bodies. Before, it was dropped, so a method used only from a property looked unused.
- In VB.NET, a field or property initializer like `= Compute()` or `As New List(Of Order)` now links what it calls and creates, and so do a `Custom Event`'s `AddHandler`, `RemoveHandler` and `RaiseEvent` blocks. Re-index VB.NET and C# projects after upgrading.
Expand Down
28 changes: 28 additions & 0 deletions __tests__/sync-framework-detection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { afterEach, describe, expect, it } from 'vitest';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import CodeGraph from '../src/index';
const projects: Array<{ root: string; graph: CodeGraph }> = [];
afterEach(async () => {
for (const { root, graph } of projects.splice(0)) {
await graph.close();
fs.rmSync(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
}
});
describe('framework detection across incremental updates', () => {
it.each([false, true])('extracts newly introduced Express routes (scoped=%s)', async (scoped) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-framework-sync-'));
fs.mkdirSync(path.join(root, 'src'));
fs.writeFileSync(path.join(root, 'src/app.ts'), 'export function boot() { return 1; }\n');
const graph = CodeGraph.initSync(root);
projects.push({ root, graph });
await graph.indexAll();
expect(graph.getDetectedFrameworks()).not.toContain('express');
fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ dependencies: { express: '^4.18.0' } }));
fs.writeFileSync(path.join(root, 'src/routes.js'), "const app = express();\napp.get('/hello', handler);\n");
await graph.sync(scoped ? { paths: ['src/routes.js'] } : {});
expect(graph.getDetectedFrameworks()).toContain('express');
expect(graph.getNodesInFile('src/routes.js').some(node => node.kind === 'route' && node.name === 'GET /hello')).toBe(true);
});
});
14 changes: 12 additions & 2 deletions src/extraction/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1850,6 +1850,7 @@ export class ExtractionOrchestrator {
* hasn't run yet so single-file re-index paths can detect on the spot.
*/
private detectedFrameworkNames: string[] | null = null;
private detectedFrameworkProbes = new Set<string>();
/**
* Scope matcher for SCOPED syncs, memoized on the mtimes of the two root
* files it is derived from (`codegraph.json`, `.gitignore`). See
Expand Down Expand Up @@ -1907,7 +1908,7 @@ export class ExtractionOrchestrator {
* the DB hasn't been populated yet, but detect() only uses readFile,
* fileExists, and getAllFiles, so that's fine.
*/
private buildDetectionContext(files: string[]): ResolutionContext {
private buildDetectionContext(files: string[], probes?: Set<string>): ResolutionContext {
const rootDir = this.rootDir;
return {
getNodesInFile: () => [],
Expand All @@ -1919,6 +1920,7 @@ export class ExtractionOrchestrator {
getAllFiles: () => files,
getProjectRoot: () => rootDir,
fileExists: (relativePath: string) => {
probes?.add(relativePath);
const full = validatePathWithinRoot(rootDir, relativePath);
if (!full) return false;
try {
Expand All @@ -1928,6 +1930,7 @@ export class ExtractionOrchestrator {
}
},
readFile: (relativePath: string) => {
probes?.add(relativePath);
const full = validatePathWithinRoot(rootDir, relativePath);
if (!full) return null;
try {
Expand Down Expand Up @@ -1967,8 +1970,10 @@ export class ExtractionOrchestrator {
private ensureDetectedFrameworks(files?: string[]): string[] {
if (this.detectedFrameworkNames !== null) return this.detectedFrameworkNames;
const fileList = files ?? scanDirectory(this.rootDir);
const context = this.buildDetectionContext(fileList);
const probes = new Set<string>();
const context = this.buildDetectionContext(fileList, probes);
this.detectedFrameworkNames = detectFrameworks(context).map((r) => r.name);
this.detectedFrameworkProbes = probes;
const declared = declaredDependencies(context);
this.gatedFrameworks = new Map();
for (const name of this.detectedFrameworkNames) {
Expand Down Expand Up @@ -3497,6 +3502,11 @@ export class ExtractionOrchestrator {
await loadGrammarsForLanguages(preloadLanguagesForFiles(filesToIndex, overrides, this.rootDir));
}

if (filesAdded > 0 || filesRemoved > 0 || filesToIndex.some(file => this.detectedFrameworkProbes.has(file))) {
this.detectedFrameworkNames = null;
this.ensureDetectedFrameworks(scopedPaths?.length ? undefined : currentFiles);
}

// Index changed files
const total = filesToIndex.length;
for (let i = 0; i < filesToIndex.length; i++) {
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -970,6 +970,9 @@ export class CodeGraph {
// removal case — and runPostExtract starts by dropping the resolver's
// name caches, which a long-lived daemon warmed against the
// pre-removal graph.)
if (result.filesAdded > 0 || result.filesRemoved > 0) {
this.resolver.initialize();
}
if (result.filesAdded > 0 || result.filesModified > 0 || result.filesRemoved > 0) {
this.resolver.runPostExtract();
}
Expand Down