Files
codegraph/__tests__/fuzzy-case-sensitivity.test.ts
Colby Mchenry ca3bb444f7 fix(resolution): case-insensitive names pass the prefilter; PHP => fn() is a bare call (#2141)
- The name prefilter checks a case-SENSITIVE set of known names, so in PHP,
  Pascal, CFML, COBOL and VB.NET — whose identifiers ignore case — a call
  written in another case (`formatprice()` for `FormatPrice`, TestBox's
  `ToMatchWithCase()`) was dropped before any strategy ran. Those languages
  now also check a lowercase set (built lazily, only when asked).
- `isBarePhpCall` took any `>` before a name for `->`: every call after `=>`
  in a PHP array (`'total' => count($items)`, `'by' => user()->id`,
  `'label' => trans('…')`) was a member call, and found a same-named method
  — 166 wrong edges on BookStack (`trans` → LocaleDefinition::trans,
  `new Response` → ImageStorageDisk::response, `url()`, `config()`, `count()`).
  It now requires `->` / `?->` / `::`.
- A fuzzy `instantiates` never names a method.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 08:31:10 +00:00

111 lines
3.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* The last-resort fuzzy match compares names without regard to case. That
* is right for PHP, Pascal, CFML, COBOL and VB.NET, whose identifiers
* resolve that way, and wrong everywhere else: jsoup's 1,844 `@Test`
* annotations decorated a `CharPredicate.test` method, `new CookieManager()`
* instantiated a `cookieManager()` getter, gson's `Method` type referenced a
* test's `method()`.
*/
import { describe, it, expect, afterAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
const roots: string[] = [];
afterAll(() => {
for (const r of roots.splice(0)) fs.rmSync(r, { recursive: true, force: true });
});
async function project(files: Record<string, string>): Promise<CodeGraph> {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fuzzy-case-'));
roots.push(root);
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
return CodeGraph.init(root, { index: true });
}
const targetsFrom = (cg: CodeGraph, file: string): string[] =>
cg
.getOutgoingEdgesFrom(cg.getNodesInFile(file).map((n) => n.id))
.filter((e) => e.kind !== 'contains')
.map((e) => cg.getNode(e.target)!)
.map((n) => `${n.kind}:${n.name}`)
.sort();
describe('fuzzy matching and case', () => {
it('Java: an annotation or a type is not a method whose name differs only in case', async () => {
const cg = await project({
'src/main/java/app/CharPredicate.java': `package app;
public interface CharPredicate {
boolean test(char c);
}
`,
'src/main/java/app/Request.java': `package app;
public class Request {
public Object cookieManager() { return null; }
}
`,
'src/test/java/app/ParserTest.java': `package app;
import org.junit.jupiter.api.Test;
import java.net.CookieManager;
public class ParserTest {
@Test void parses() {
Object manager = new CookieManager();
}
}
`,
});
try {
const targets = targetsFrom(cg, 'src/test/java/app/ParserTest.java');
expect(targets).not.toContain('method:test');
expect(targets).not.toContain('method:cookieManager');
} finally {
cg.close();
}
});
it('PHP still resolves a function whatever case the call is written in', async () => {
const cg = await project({
'src/helpers.php': `<?php
function FormatPrice($amount) { return $amount; }
`,
'src/view.php': `<?php
function render() { return formatprice(10); }
`,
});
try {
expect(targetsFrom(cg, 'src/view.php')).toContain('function:FormatPrice');
} finally {
cg.close();
}
});
it('PHP: a call written after `=>` in an array is a function call, not some class’s method', async () => {
const cg = await project({
'app/CommentTree.php': `<?php
class CommentTree {
public function count(): int { return 0; }
}
`,
'app/helpers.php': `<?php
function user() { return null; }
`,
'app/Controller.php': `<?php
function summary($items) {
return ['total' => count($items), 'by' => user()];
}
`,
});
try {
const targets = targetsFrom(cg, 'app/Controller.php');
expect(targets).not.toContain('method:count');
expect(targets).toContain('function:user');
} finally {
cg.close();
}
});
});