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

### Fixes

- `codegraph status` now reports added, changed and deleted source files explicitly included despite Git ignoring them, including edits that preserve file size and modification time, instead of incorrectly reporting no pending changes.
- In TypeScript, an awaited call with type arguments like `await fetchApi<User>("/me")` or `await api.get<T>(url)` now links to the function it calls. Before, the call was dropped, so typed fetch wrappers looked like they had no callers. (#2586)
- In JavaScript and TypeScript, a call or `new` in a parameter's default value, like `function renderWithNote(note = buildNote())`, a destructured `{ size = defaultSize() }` or `constructor(private svc = new Service())`, now counts as the function's own call. Before, it was dropped, so `buildNote` was missing that caller and impact on it came back incomplete. Re-index JavaScript and TypeScript projects after upgrading. Thanks @inth3shadows for the report and the pull request. (#2588)
- In React, a component rendered at module scope, in a top-level constant or inside a test's `it(…)` callback, like `createRoot(el).render(<App />)` or `render(<Card />)`, now counts as rendered there, so an app's entry reaches its root component and a component's tests show among its callers. (#2587)
Expand Down
52 changes: 52 additions & 0 deletions __tests__/included-ignored-source-status.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { execFileSync } from 'node:child_process';
import { CodeGraph } from '../src';

describe('Explicitly included gitignored source status', () => {
let dir: string;
let cg: CodeGraph | undefined;
const write = (rel: string, source: string) => {
const file = path.join(dir, rel);
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, source);
};
beforeEach(() => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-included-status-'));
});
afterEach(() => {
cg?.destroy();
cg = undefined;
fs.rmSync(dir, { recursive: true, force: true });
});

it('reports included additions, edits with preserved metadata, and deletions', async () => {
write('local/Device.java', 'package p; public class Device { public void run() {} }');
write('local/excluded/Noise.java', 'public class Noise {}');
write('.gitignore', 'local/\n');
write('codegraph.json', JSON.stringify({ include: ['local/'], exclude: ['local/excluded/'] }));
execFileSync('git', ['init', '-q'], { cwd: dir });
execFileSync('git', ['add', '-A'], { cwd: dir });
execFileSync('git', ['-c', 'user.name=t', '-c', 'user.email=a@b.c', 'commit', '-qm', 'base'], { cwd: dir });
cg = await CodeGraph.init(dir, { index: true });
expect(cg.getNodesByName('Device')).toHaveLength(1);
expect(cg.getNodesByName('Noise')).toHaveLength(0);
expect(cg.getChangedFiles()).toEqual({ added: [], modified: [], removed: [] });

const file = path.join(dir, 'local/Device.java');
const originalStat = fs.statSync(file);
// Git never reports this ignored file, and metadata cannot prove it unchanged.
write('local/Device.java', 'package p; public class Device { public void end() {} }');
fs.utimesSync(file, originalStat.atime, originalStat.mtime);
expect(fs.statSync(file).size).toBe(originalStat.size);
expect(cg.getChangedFiles().modified).toEqual(['local/Device.java']);

write('local/New.java', 'package p; public class New {}');
write('local/excluded/NewNoise.java', 'public class NewNoise {}');
expect(cg.getChangedFiles().added).toEqual(['local/New.java']);
fs.rmSync(file);
expect(cg.getChangedFiles().removed).toEqual(['local/Device.java']);
});
});
7 changes: 7 additions & 0 deletions src/extraction/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4137,6 +4137,13 @@ export class ExtractionOrchestrator {
// have been recreated locally; a previously indexed dirty path may have
// vanished from git status after restore. Classify current disk vs DB once.
const candidates = new Set([...gitChanges.deleted, ...gitChanges.modified, ...gitChanges.added, ...dirtyPaths!]);
const include = loadIncludeMatcher(this.rootDir);
if (include) {
for (const file of collectIncludedFilesForRoot(this.rootDir)) candidates.add(file);
for (const file of this.queries.getAllFiles()) {
if (include.ignores(file.path)) candidates.add(file.path);
}
}
const scope = this.scopedSyncMatcher();
const overrides = loadExtensionOverrides(this.rootDir);
for (const filePath of candidates) {
Expand Down