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

#### Symbols, tests and the viewer

- **Fuzzy matching no longer lands on a closure it cannot reach.** A function nested inside another function is only callable from inside its container, and exact-name matching already filtered such candidates; the fuzzy fallback did not, so a builtin method call (`res.text()`, `items.push()`) whose only same-named project symbol was some file's closure resolved onto that closure at confidence 0.5. The fallback now applies the same reachability filter. On a 584-file repo that removed the 11 fuzzy edges onto nested functions and nothing else. Re-index after upgrading.
- **Files under an `e2e/` directory count as tests.** Their calls no longer appear as production callers in Steps, dead-code and test badges.

- **Production code under a `samples` or `examples` package path is no longer treated as test code.** A Kotlin or Java project whose package path runs through `com/google/samples/…` (Now in Android, for one) had nearly every file counted as a fixture, so the Map opened on `build-logic`, the entry points hid the app, and dead-code and test badges were wrong. Only the project layout above a `src/` folder decides now; the package path below it never does.
Expand Down
73 changes: 73 additions & 0 deletions __tests__/fuzzy-lexical-reach.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* A function nested inside another function is only callable from inside its
* container. matchByExactName already filters candidates that way; matchFuzzy
* must too, or a call to a builtin method (`res.text()`) whose only same-named
* project symbol is some file's closure resolves onto that closure.
*/

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { CodeGraph } from '../src';

describe('fuzzy matching respects lexical reachability of nested functions', () => {
let tempDir: string;
let cg: CodeGraph | null = null;

beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-fuzzy-reach-'));
});

afterEach(() => {
cg?.destroy();
cg = null;
try {
fs.rmSync(tempDir, { recursive: true, force: true });
} catch {
// Windows can still hold the SQLite handle for a moment; the OS temp dir is swept anyway.
}
});

it('does not resolve a builtin method call onto another file\'s closure of the same name', async () => {
fs.writeFileSync(
path.join(tempDir, 'seed.ts'),
[
'export function readSeedState(raw: string): string {',
' function text(): string {',
' return raw.trim();',
' }',
' return text();',
'}',
'',
].join('\n')
);
fs.writeFileSync(
path.join(tempDir, 'fetch.ts'),
[
'export async function readOkText(settled: { value: Response }): Promise<string> {',
' // A chained receiver reaches the resolver as the bare method name.',
' return settled.value.text();',
'}',
'',
].join('\n')
);
cg = await CodeGraph.init(tempDir, { index: true });
cg.resolveReferences();

const closure = cg
.getNodesByKind('function')
.find((n) => n.name === 'text' && n.filePath === 'seed.ts');
const caller = cg.getNodesByKind('function').find((n) => n.name === 'readOkText');
expect(closure).toBeDefined();
expect(caller).toBeDefined();

const fromCaller = cg.getOutgoingEdges(caller!.id).filter((e) => e.kind === 'calls');
expect(fromCaller.map((e) => e.target)).not.toContain(closure!.id);

// The in-container call still resolves.
const container = cg.getNodesByKind('function').find((n) => n.name === 'readSeedState');
const inside = cg.getOutgoingEdges(container!.id).filter((e) => e.kind === 'calls');
expect(inside.map((e) => e.target)).toContain(closure!.id);
});
});
5 changes: 4 additions & 1 deletion src/resolution/name-matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2412,7 +2412,10 @@ export function matchFuzzy(

// Filter to callable kinds only (function, method, class)
const callableKinds = new Set(['function', 'method', 'class']);
const callableCandidates = applyLanguageGate(candidates.filter((n) => callableKinds.has(n.kind)), ref);
const callableCandidates = applyLanguageGate(
candidates.filter((n) => callableKinds.has(n.kind) && isLexicallyReachable(n, ref, context)),
ref
);

// Prefer same-language matches
const sameLanguageCandidates = callableCandidates.filter(n => n.language === ref.language);
Expand Down