mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-19 23:20:31 +08:00
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>
This commit is contained in:
@@ -14,6 +14,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
### Fixes
|
||||
|
||||
- In PHP, CFML, Pascal, COBOL and VB.NET, a call written in a different case than its definition now resolves, like `formatprice()` for `FormatPrice` or CFML's `ToMatchWithCase()` for `toMatchWithCase`. These languages ignore case in names, but such calls used to be dropped before resolution.
|
||||
- A PHP function call written after `=>` in an array, like `'count' => count($items)`, `'by' => user()->id` or `'label' => trans('…')`, is now read as a plain function call. It used to be taken for a method call and linked to whichever class had a method of that name. On BookStack, that meant 166 wrong links.
|
||||
- Vue components written with the Options API (`export default { methods: {…}, computed: {…}, watch: {…}, mounted() {…} }`), as every Vue 2 app is, now have their methods, computed properties, watchers and lifecycle hooks in the graph. Each one has its own callers and calls, and a template's `@click="handleLogin"` links to the method it runs. On vue-element-admin that's over 800 new links, where a component's calls used to belong to the whole file.
|
||||
- In Java, Kotlin, Swift, C#, C and C++, Go, Scala, Ruby, Dart and the other case-sensitive languages, a name no longer links to a symbol whose name differs only in case. On jsoup, JUnit's `@Test` had linked every test to a `test` method; `new CookieManager()` had linked to a `cookieManager()` getter, and a `Method` type to a `method()`. PHP, Pascal, CFML, COBOL and VB.NET, whose names really do ignore case, are unchanged.
|
||||
- React Native events whose name is held in a constant now link native code to the JavaScript that listens for them, like NetInfo's `addListener(PrivateTypes.DEVICE_CONNECTIVITY_EVENT, …)` or a Java `.emit(PROGRESS_EVENT, …)`. The constant is read the way the language scopes it, from the same file, an import or its owning class, so a parameter that happens to share a constant's name is never mistaken for it.
|
||||
|
||||
@@ -66,4 +66,45 @@ public class ParserTest {
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+20
-1
@@ -21,7 +21,7 @@ import {
|
||||
isImportableKind,
|
||||
CPP_DEFINE_SIGNATURE,
|
||||
} from './types';
|
||||
import { matchJsStoreBindingCall, isUnresolvedJsMemberCall, isVisibleAcrossFiles, matchReference, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope } from './name-matcher';
|
||||
import { matchJsStoreBindingCall, isUnresolvedJsMemberCall, isVisibleAcrossFiles, matchReference, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope, CASE_INSENSITIVE_LANGUAGES } from './name-matcher';
|
||||
import { isVisibleCppMacro, clearCppMacroVisibility } from './cpp-macro-visibility';
|
||||
import { isCppConstructorRef, matchCppConstructor } from './cpp-constructor';
|
||||
import { gateSwiftTypeTarget, clearSwiftTypeVisibility, swiftExtendedConformances } from './swift-type-visibility';
|
||||
@@ -445,6 +445,7 @@ export class ReferenceResolver {
|
||||
this.fileExistsMemo.clear();
|
||||
this.manifestScopes.clear();
|
||||
this.knownNames = null;
|
||||
this.knownLowerNames = null;
|
||||
this.knownFiles = null;
|
||||
this.cachesWarmed = false;
|
||||
// The import-resolver's and name-matcher's per-context memos assume the
|
||||
@@ -884,6 +885,21 @@ export class ReferenceResolver {
|
||||
};
|
||||
}
|
||||
|
||||
/** Lowercased `knownNames`, built the first time a case-insensitive language asks. */
|
||||
private knownLowerNames: Set<string> | null = null;
|
||||
|
||||
/** `hasAnyPossibleMatch` for a language whose names ignore case: the name, or any `.`/`::`/`->` part of it. */
|
||||
private hasAnyPossibleMatchIgnoringCase(name: string): boolean {
|
||||
if (!this.knownNames) return true;
|
||||
if (!this.knownLowerNames || this.knownLowerNames.size === 0) {
|
||||
this.knownLowerNames = new Set();
|
||||
for (const known of this.knownNames) this.knownLowerNames.add(known.toLowerCase());
|
||||
}
|
||||
const lower = name.toLowerCase();
|
||||
if (this.knownLowerNames.has(lower)) return true;
|
||||
return lower.split(/::|->|\./).some((part) => part.length > 0 && this.knownLowerNames!.has(part));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a reference name has any possible match in the codebase.
|
||||
* Uses the pre-built knownNames set to skip expensive resolution
|
||||
@@ -1070,6 +1086,9 @@ export class ReferenceResolver {
|
||||
const preFilterPass =
|
||||
isNixPathImportRef(ref) ||
|
||||
this.hasAnyPossibleMatch(existenceName) ||
|
||||
// PHP, Pascal, CFML, COBOL and VB.NET names ignore case: `formatprice()`
|
||||
// calls `FormatPrice`, which the exact-name set never lists.
|
||||
(CASE_INSENSITIVE_LANGUAGES.has(ref.language) && this.hasAnyPossibleMatchIgnoringCase(existenceName)) ||
|
||||
this.matchesAnyImport(ref) ||
|
||||
this.frameworks.some((f) => f.claimsReference?.(ref.referenceName));
|
||||
if (this.profileStages) this.stageAdd('preFilter', ref, preFilterPass, tPre);
|
||||
|
||||
@@ -944,7 +944,7 @@ const JS_FAMILY = new Set<string>(['typescript', 'tsx', 'javascript', 'jsx', 'vu
|
||||
const JS_TS = new Set<string>(['typescript', 'tsx', 'javascript', 'jsx']);
|
||||
|
||||
/** Languages whose identifiers resolve regardless of case. */
|
||||
const CASE_INSENSITIVE_LANGUAGES = new Set<string>(['php', 'pascal', 'cfml', 'cfscript', 'cfquery', 'cobol', 'vbnet']);
|
||||
export const CASE_INSENSITIVE_LANGUAGES = new Set<string>(['php', 'pascal', 'cfml', 'cfscript', 'cfquery', 'cobol', 'vbnet']);
|
||||
|
||||
/**
|
||||
* Whether a JS/TS `calls` ref is a RECEIVER-LESS call — `serialize(x)`, not
|
||||
@@ -999,8 +999,9 @@ function isBarePhpCall(ref: UnresolvedRef, context: ResolutionContext): boolean
|
||||
if (!CALL_OPENER.test(line)) return false;
|
||||
let end = at;
|
||||
while (end > 0 && WHITESPACE.test(line[end - 1]!)) end--;
|
||||
// `$obj->name(` / `Foo::name(` / `$obj?->name(`, should a column ever land on the name.
|
||||
return !(end > 0 && (line[end - 1] === '>' || line[end - 1] === ':'));
|
||||
// `$obj->name(` / `Foo::name(` / `$obj?->name(`, should a column ever land on
|
||||
// the name — but not `'size' => filesize($zip)` or `$x ? a : name()`.
|
||||
return !(end > 1 && ((line[end - 1] === '>' && line[end - 2] === '-') || (line[end - 1] === ':' && line[end - 2] === ':')));
|
||||
}
|
||||
|
||||
function isReceiverLessCall(ref: UnresolvedRef, context: ResolutionContext): boolean {
|
||||
@@ -4611,6 +4612,8 @@ export function matchFuzzy(
|
||||
// resolve a name without regard to case, which is what this fallback's
|
||||
// lowercase index is for.
|
||||
const callableCandidates = candidates.filter((n) => callableKinds.has(n.kind) && !(typeRef && !canNameInTypePosition(n)) &&
|
||||
// `new …MockData()` makes an instance of a type; a method is never what it names.
|
||||
!(ref.referenceKind === 'instantiates' && n.kind === 'method') &&
|
||||
!(rustBare && !isRustNameInScope(n, ref, context)) &&
|
||||
!(!CASE_INSENSITIVE_LANGUAGES.has(ref.language) && n.name !== ref.referenceName) &&
|
||||
!(pythonShape && !fitsPythonCallShape(n, pythonShape, ref, context)) &&
|
||||
|
||||
Reference in New Issue
Block a user