fix(kotlin): a bare call reaches only a member in reach (#2172)

A receiver-less Kotlin call exact-matched any class's same-named method:
koin's module { } in one test went to another test class's private
module 268 times, error("…") to a Logger's error, get() inside a
definition lambda to Koin's get rather than the Scope it runs on.

A bare call now reaches a method of a class, anonymous object or extension
receiver around it (read from nodes and from the source's braces, so a
class the parser lost still counts) and what those inherit (with Android
framework ancestry), of a type the project's function types take as a
lambda receiver (plus outside types it writes extensions on), or of an
object the file imports. with/apply/run blocks and .kts scripts are left
unjudged; Kotlin's require/check/assert preconditions are the stdlib's.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-09-30 15:20:22 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent f9530d7d5f
commit 2e2b98c94e
4 changed files with 384 additions and 3 deletions
+1
View File
@@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### Fixes
- In Kotlin, a call with no receiver now reaches a method of another class only when that class is in reach. That means a class, an anonymous `object : Base()` or an extension function's receiver around the call, what those inherit, or the receiver of a DSL lambda (koin's `single { get() }` runs on a `Scope`, Exposed's `withTables { … }` on a transaction). An object member the file imports is in reach too. Kotlin's own `error(…)`, `require(x >= 0) { … }` and `check(…)` no longer land on a project class's method of that name. On koin, `module { … }` in one test used to land on another test class's private `module` over 260 times, and inside a lambda `get()` landed on the wrong class's `get`. Re-index after upgrading.
- In C#, Java and Kotlin, a call through a field, property or parameter now lands on the method of the type it is declared with. That includes a type inherited from a base class with its type argument filled in, a generic or `for`-each declaration, a C# `using` alias, and a type parameter's bound. A call no longer falls back to a guess at some project class's same-named method, and a type from outside the project gives no link. On Newtonsoft.Json, `_innerWriter.WriteValue(…)` inside a writer wrapper used to land on the wrapper's own `WriteValue`, and a `TextWriter`'s `Write(…)` on a test writer's. On commons-lang, a `DateFormat` field's `parse(…)` used to land on the project's `DateParser` 284 times.
- C# members of an interface are now public and types declared without a modifier are now internal, as the language defines. A call on an interface-typed field now reaches the interface's method, where it used to find nothing. A guess from a receiver's name now weighs only the owner type's own name, not its namespace. Standard .NET names like `ToString`, `Add` and `Write` need a receiver named after the owner. In every language, a guess from production code no longer lands on a mock or helper defined in a test suite. Re-index after upgrading.
- In Swift, a call written with no receiver, or through `self.` or `super.`, now reaches only a member of the type around it or of what that type inherits or conforms to, and the nearest override wins. A standard-library name later in a chain (`axis.entries.removeAll()`, `names.contains(x)`) no longer lands on a project type's method, unless the receiver or the call's argument label points to it. On Charts, `min(a, b)` and `max(a, b)` used to land on a range type's `min` field and a `Sequence` extension, and `super.init(…)` on an Objective-C demo's `init` over 170 times. A global in a playground or a test target is no longer linked from the main code. Re-index after upgrading.
+130
View File
@@ -0,0 +1,130 @@
/**
* A bare Kotlin call reaches a member of a type around it — a class, an
* anonymous `object : Base()`, an extension function's receiver — or of what
* those inherit; a member of a type the project's function types take as a
* lambda receiver (a DSL: koin's `single { get() }` runs on a Scope); or an
* object member the file imports. It never reaches another class's member by
* name: koin's `error("…")` — Kotlin's — went to a Logger's `error`, and
* `module { … }` in one test to another test class's private `module`.
* `require(n >= 0) { … }` is Kotlin's precondition, not a member `require`.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-kotlin-member-'));
const files: Record<string, string> = {
'src/main/kotlin/app/dsl/Module.kt': `package app.dsl
class Scope {
fun get(): Any = 1
}
class Koin {
fun get(): Any = 2
}
class Module {
fun single(definition: Scope.() -> Any) { }
}
fun module(declaration: Module.() -> Unit): Module = Module().apply(declaration)
`,
'src/main/kotlin/app/log/Logger.kt': `package app.log
class Logger {
fun error(message: String) { }
}
`,
'src/main/kotlin/app/db/Table.kt': `package app.db
abstract class Table {
fun integer(name: String): Int = 0
}
`,
'src/main/kotlin/app/io/Source.kt': `package app.io
interface Source {
fun require(byteCount: Long)
}
class RealSource : Source {
override fun require(byteCount: Long) { }
fun read(byteCount: Long) {
require(byteCount >= 0L) { "byteCount < 0: $byteCount" }
require(byteCount)
}
}
`,
'src/test/kotlin/app/TestUtil.kt': `package app
object TestUtil {
fun deepCopy(value: Int): Int = value
}
`,
'src/test/kotlin/app/AppTest.kt': `package app
import app.TestUtil.deepCopy
import app.db.Table
import app.dsl.module
class AppTest {
fun wires() {
val m = module {
single { get() }
}
val table = object : Table() {
val id = integer("id")
}
val copy = deepCopy(1)
if (copy < 0) error("negative")
}
}
`,
};
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);
}
cg = await CodeGraph.init(root, { index: true });
});
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
/** `Owner::member` of every call edge out of a file. */
function callsFrom(file: string): string[] {
const ids = cg.getNodesInFile(file).map((n) => n.id);
return cg
.getOutgoingEdgesFrom(ids)
.filter((e) => e.kind === 'calls')
.map((e) => cg.getNode(e.target)!.qualifiedName)
.sort();
}
describe('bare Kotlin calls', () => {
it('reach a DSL lambda’s receiver, an anonymous object’s base and an imported object member — nothing else', () => {
const calls = callsFrom('src/test/kotlin/app/AppTest.kt');
expect(calls).toContain('app.dsl::Module::single');
expect(calls).toContain('app.dsl::Scope::get');
expect(calls).toContain('app.db::Table::integer');
expect(calls).toContain('app::TestUtil::deepCopy');
expect(calls).not.toContain('app.dsl::Koin::get');
expect(calls).not.toContain('app.log::Logger::error');
});
it('leave Kotlin’s precondition `require(cond) { … }` to the standard library', () => {
const read = cg.getNodesInFile('src/main/kotlin/app/io/Source.kt').find((n) => n.name === 'read')!;
const requires = cg.getOutgoingEdgesFrom([read.id]).filter((e) => e.kind === 'calls').map((e) => e.line);
expect(requires).toEqual([12]);
});
});
@@ -31,21 +31,26 @@ beforeAll(async () => {
class JdbcTransaction
fun withTables(statement: JdbcTransaction.() -> Unit) {}
fun <T> JdbcTransaction.assertEquals(exp: T, act: T) {}
`,
'r2dbc/src/main/kotlin/app/r2dbc/tests/Assert.kt': `package app.r2dbc.tests
class R2dbcTransaction
fun withTables(statement: R2dbcTransaction.() -> Unit) {}
fun <T> R2dbcTransaction.assertEquals(exp: T, act: T) {}
`,
'jdbc/src/test/kotlin/app/jdbc/JdbcTest.kt': `package app.jdbc
import app.jdbc.tests.assertEquals
import app.jdbc.tests.withTables
class JdbcTest {
fun works() {
assertEquals(1, 1)
withTables { assertEquals(1, 1) }
}
}
`,
@@ -55,7 +60,7 @@ import app.r2dbc.tests.*
class R2dbcTest {
fun works() {
assertEquals(1, 1)
withTables { assertEquals(1, 1) }
}
}
`,
@@ -72,7 +77,7 @@ class PlainTest {
'jdbc/src/main/kotlin/app/jdbc/tests/Local.kt': `package app.jdbc.tests
fun check() {
assertEquals(1, 1)
withTables { assertEquals(1, 1) }
}
`,
'jdbc/src/main/kotlin/app/jdbc/tests/DatabaseTestsBase.kt': `package app.jdbc.tests
+245
View File
@@ -1680,6 +1680,244 @@ function kotlinChainReceiver(ref: UnresolvedRef, context: ResolutionContext): st
return rustGoReceiverName(before.replace(/\?\s*\.\s*$/, '').replace(/!!\s*$/, ''));
}
/** Whether a bare Kotlin name is written with no receiver at its call — not a later link of a chain (`….name(`). */
function isReceiverLessKotlinCall(ref: UnresolvedRef, context: ResolutionContext): boolean {
const line = context.getFileLines?.(ref.filePath)?.[ref.line - 1] ?? context.readFile(ref.filePath)?.split('\n')[ref.line - 1];
if (line === undefined) return false;
const name = ref.referenceName;
let start = line.startsWith(name, ref.column) && !/[\w$]/.test(line[ref.column - 1] ?? '') ? ref.column : -1;
if (start < 0) {
const m = new RegExp(`(?<![\\w$])${name.replace(/\$/g, '\\$')}\\s*[({<]`).exec(line);
start = m ? m.index : -1;
}
return start >= 0 && !/[.:]\s*$/.test(line.slice(0, start));
}
/**
* Whether a bare `require(…)` / `check(…)` / `assert(…)` is Kotlin's
* precondition: its condition is a comparison or boolean expression, or a
* lazy message follows it.
*/
function isKotlinPreconditionCall(ref: UnresolvedRef, context: ResolutionContext): boolean {
if (!/^(?:require|check|assert)$/.test(ref.referenceName)) return false;
const line = context.getFileLines?.(ref.filePath)?.[ref.line - 1] ?? context.readFile(ref.filePath)?.split('\n')[ref.line - 1];
const at = line ? new RegExp(`(?<![\\w.])${ref.referenceName}\\s*\\(`).exec(line) : null;
if (!line || !at) return false;
let depth = 0;
let args = '';
let rest = '';
for (let i = at.index + at[0].length; i < line.length; i++) {
const ch = line[i]!;
if (ch === '(') depth++;
else if (ch === ')' && depth-- === 0) {
rest = line.slice(i + 1);
break;
}
args += ch;
}
return /[<>=!]=|&&|\|\||(?:^|[\s(])!|\s[<>]\s|\bis\b|\bin\b|\btrue\b|\bfalse\b/.test(args) || /^\s*\{/.test(rest);
}
const KOTLIN_RECEIVER_TYPES = new WeakMap<ResolutionContext, Set<string>>();
/**
* Every type a Kotlin function type in the project takes as its receiver —
* `Module` in `typealias ModuleDeclaration = Module.() -> Unit`, `Scope` in
* `Scope.(ParametersHolder) -> T`, `JdbcTransaction` in `statement:
* JdbcTransaction.(TestDB) -> Unit`. A lambda of such a type runs with that
* receiver, so a bare call inside one reaches its members.
*/
function kotlinReceiverTypes(context: ResolutionContext): Set<string> {
const hit = KOTLIN_RECEIVER_TYPES.get(context);
if (hit) return hit;
const types = new Set<string>();
const outside = new Set<string>();
for (const file of context.getAllFiles()) {
if (!/\.kts?$/.test(file)) continue;
const source = stripCommentsForRegex(context.readFile(file) ?? '', 'java');
for (const m of source.matchAll(/\b([A-Z]\w*)(?:<[^<>()]*(?:<[^<>()]*>[^<>()]*)*>)?\s*\.\s*\(/g)) types.add(m[1]!);
// An extension on a type from outside the project (`fun
// MacrobenchmarkScope.waitForContent()`, `fun StringBuilder.padInt(…)`)
// is written to be called inside that library's lambdas.
for (const m of source.matchAll(/\bfun\s+(?:<[^>]*>\s*)?([A-Z]\w*(?:\.[A-Z]\w*)*)(?:<[^<>()]*(?:<[^<>()]*>[^<>()]*)*>)?\??\.[A-Za-z_`][\w`]*\s*\(/g)) {
for (const part of m[1]!.split('.')) outside.add(part);
}
}
for (const name of outside) {
if (!context.getNodesByName(name).some((n) => MEMBER_CLASS_KINDS.has(n.kind) && (n.language === 'kotlin' || n.language === 'java'))) types.add(name);
}
// A receiver's members include those it inherits: Exposed's `mergeFrom`
// body runs on a MergeTableStatement, whose `whenMatchedDelete` is
// MergeStatement's.
const queue = [...types];
while (queue.length > 0 && types.size < 5000) {
const name = queue.shift()!;
for (const decl of context.getNodesByName(name)) {
if (decl.language !== 'kotlin' || !MEMBER_CLASS_KINDS.has(decl.kind)) continue;
for (const sup of classHeadSupertypes(decl, context)) {
if (!types.has(sup)) {
types.add(sup);
queue.push(sup);
}
}
}
}
KOTLIN_RECEIVER_TYPES.set(context, types);
return types;
}
/**
* What the Android framework and AndroidX classes a Kotlin class commonly
* extends inherit, so an extension on `ComponentCallbacks` or
* `ComponentActivity` is in reach of an `AppCompatActivity` subclass.
*/
const KOTLIN_PLATFORM_SUPERS: Readonly<Record<string, readonly string[]>> = {
AppCompatActivity: ['FragmentActivity'], FragmentActivity: ['ComponentActivity'],
ComponentActivity: ['Activity', 'LifecycleOwner', 'ViewModelStoreOwner', 'SavedStateRegistryOwner'],
Activity: ['ContextThemeWrapper', 'ComponentCallbacks2'], ContextThemeWrapper: ['ContextWrapper'],
ContextWrapper: ['Context'], Application: ['ContextWrapper', 'ComponentCallbacks2'],
Service: ['ContextWrapper', 'ComponentCallbacks2'], ComponentCallbacks2: ['ComponentCallbacks'],
Fragment: ['ComponentCallbacks', 'LifecycleOwner', 'ViewModelStoreOwner', 'SavedStateRegistryOwner'],
DialogFragment: ['Fragment'], AppCompatDialogFragment: ['DialogFragment'],
BottomSheetDialogFragment: ['AppCompatDialogFragment'], AndroidViewModel: ['ViewModel'],
};
const KOTLIN_HIERARCHIES = new WeakMap<ResolutionContext, WeakMap<UnresolvedRef, Set<string>>>();
/**
* The Kotlin types a bare call is written inside — the classes and objects
* around it and the receiver of the extension function it is in — and what
* they inherit.
*/
function kotlinHierarchyAt(ref: UnresolvedRef, context: ResolutionContext): Set<string> {
let memo = KOTLIN_HIERARCHIES.get(context);
if (!memo) {
memo = new WeakMap();
KOTLIN_HIERARCHIES.set(context, memo);
}
const hit = memo.get(ref);
if (hit) return hit;
const queue: string[] = [];
for (const n of context.getNodesInFile(ref.filePath)) {
if (n.startLine > ref.line || n.endLine < ref.line) continue;
if (MEMBER_CLASS_KINDS.has(n.kind)) queue.push(n.name);
// `fun Foo.bar() { baz() }`: Foo is the implicit receiver.
else if ((n.kind === 'method' || n.kind === 'function') && n.qualifiedName.includes('::')) {
queue.push(n.qualifiedName.slice(0, n.qualifiedName.lastIndexOf('::')).split(/::|\./).pop()!);
}
}
// A Gradle build script runs on the Project (a settings script on Settings).
if (ref.filePath.endsWith('.gradle.kts')) queue.push(/(?:^|\/)settings\.gradle\.kts$/.test(ref.filePath) ? 'Settings' : 'Project');
// The same read from the source's braces, which also sees an anonymous
// `object : Table("t") { … }` and survives a class the parser lost.
for (const frame of kotlinBraceFrames(ref.filePath, context)) {
if (frame.start <= ref.line && frame.end >= ref.line) queue.push(...frame.names);
}
const names = new Set<string>();
while (queue.length > 0 && names.size < 60) {
const name = queue.shift()!;
if (names.has(name)) continue;
names.add(name);
queue.push(...(KOTLIN_PLATFORM_SUPERS[name] ?? []));
for (const decl of context.getNodesByName(name)) {
if (decl.language === 'kotlin' && MEMBER_CLASS_KINDS.has(decl.kind)) queue.push(...classHeadSupertypes(decl, context));
}
}
memo.set(ref, names);
return names;
}
const KOTLIN_FRAMES = new WeakMap<ResolutionContext, Map<string, Array<{ start: number; end: number; names: string[] }>>>();
/**
* The type bodies of a Kotlin file by line range, read from its braces: each
* `class` / `object` / `interface` body with its name and supertypes, an
* anonymous `object : Base(…)` with its base, and an extension function's
* body with its receiver type.
*/
function kotlinBraceFrames(file: string, context: ResolutionContext): Array<{ start: number; end: number; names: string[] }> {
let memo = KOTLIN_FRAMES.get(context);
if (!memo) {
memo = new Map();
KOTLIN_FRAMES.set(context, memo);
}
const hit = memo.get(file);
if (hit) return hit;
const frames: Array<{ start: number; end: number; names: string[] }> = [];
const source = blankStringContents(stripCommentsForRegex(context.readFile(file) ?? '', 'java'));
const stack: Array<{ start: number; names: string[] | null }> = [];
let line = 1;
let pending = '';
for (const ch of source) {
if (ch === '\n') line++;
if (ch === '{') {
// `with(x) {`, `x.apply {`, `x.run {`: a receiver of whatever type x is.
const scoped = /(?:\bwith\s*\([^{}]*\)|\.\s*(?:apply|run)(?:\s*<[^<>]*>)?)\s*$/.test(pending);
stack.push({ start: line, names: scoped ? ['*'] : kotlinHeadNames(pending) });
pending = '';
} else if (ch === '}') {
const open = stack.pop();
if (open?.names && open.names.length > 0) frames.push({ start: open.start, end: line, names: open.names });
pending = '';
} else if (ch === ';') pending = '';
else if (pending.length < 600) pending += ch;
else pending = pending.slice(300) + ch;
}
memo.set(file, frames);
return frames;
}
/** The type names a Kotlin block head introduces: a type declaration's name and supertypes, or an extension function's receiver. */
function kotlinHeadNames(head: string): string[] | null {
let depth = 0;
let flat = '';
for (const ch of head) {
if (ch === '<' || ch === '(') depth++;
else if (ch === '>' || ch === ')') depth = Math.max(0, depth - 1);
else if (depth === 0) flat += ch;
}
const decl = /\b(?:class|interface|object)\b(?:\s+([A-Za-z_]\w*))?([^=]*)$/.exec(flat);
if (decl) {
const supers = /:\s*([\s\S]*)$/.exec(decl[2] ?? '')?.[1] ?? '';
const names = [...supers.replace(/\bwhere\b[\s\S]*$/, '').matchAll(/([A-Z]\w*)\s*(?=,|$|\bby\b)/g)].map((m) => m[1]!);
return decl[1] ? [decl[1], ...names] : names;
}
const receiver = /\bfun\s+(?:<[^>]*>\s*)?([A-Z]\w*)(?:<[^>]*>)?\??\.[A-Za-z_]\w*\s*\(/.exec(head)?.[1];
return receiver ? [receiver] : null;
}
/**
* Whether a bare Kotlin call can reach method `n`: a member of a type around
* the call or of what it inherits, or of a type the project's function types
* take as a lambda receiver (a DSL). koin's `error("…")` — Kotlin's — went to
* a Logger's `error`, and `module { … }` in one test to another test class's
* private `module`.
*/
function isKotlinMemberReachable(n: Node, ref: UnresolvedRef, context: ResolutionContext): boolean {
// A Java class's method, too: Kotlin calls it bare only from a subclass or through a static import.
if (n.kind !== 'method' || (n.language !== 'kotlin' && n.language !== 'java')) return true;
// `require(n >= 0) { … }`, `check(!closed)`: Kotlin's preconditions, not a
// member `require(byteCount: Long)` of the type around the call.
if (isKotlinPreconditionCall(ref, context)) return false;
// A Gradle script's DSL blocks run on the build tool's own types.
if (ref.filePath.endsWith('.kts')) return true;
const cut = n.qualifiedName.lastIndexOf('::');
if (cut <= 0) return true;
const path = n.qualifiedName.slice(0, cut).split(/::|\./);
let owner = path.pop()!;
const companion = owner === 'Companion' && path.length > 0;
if (companion) owner = path.pop()!;
// A (Java) constructor is the type's, called by its name.
if (n.name === owner) return true;
const hierarchy = kotlinHierarchyAt(ref, context);
if (hierarchy.has('*') || kotlinReceiverTypes(context).has(owner) || hierarchy.has(owner)) return true;
// `import okio.TestUtil.deepCopy` / `import okio.TestUtil.*` names an object's members.
const pkg = kotlinFileScope(n.filePath, context).pkg;
const objectPath = (pkg ? `${pkg}.${owner}` : owner) + (companion ? '.Companion' : '');
const here = kotlinFileScope(ref.filePath, context);
return here.imports.has(`${objectPath}.${n.name}`) || here.stars.has(objectPath);
}
/** Whether a standard-named Kotlin chain link can mean `n`: only through a receiver named after its owner. */
function isKotlinStdChainTarget(n: Node, receiver: string): boolean {
if (n.kind !== 'method' && n.kind !== 'function') return true;
@@ -3118,6 +3356,7 @@ export function matchByExactName(
const javaBare = ref.language === 'java' && ref.referenceKind === 'calls' && /^[A-Za-z_$][\w$]*$/.test(ref.referenceName);
const dartBare = ref.language === 'dart' && ref.referenceKind === 'calls' && /^[A-Za-z_$][\w$]*$/.test(ref.referenceName) && isReceiverLessDartCall(ref, context);
const kotlinCall = ref.language === 'kotlin' && ref.referenceKind === 'calls' && /^[A-Za-z_$][\w$]*$/.test(ref.referenceName);
const kotlinBare = kotlinCall && isReceiverLessKotlinCall(ref, context);
const rubyBare = ref.language === 'ruby' && ref.referenceKind === 'calls' && /^[A-Za-z_]\w*[?!]?$/.test(ref.referenceName);
const cfmlBare = (ref.language === 'cfml' || ref.language === 'cfscript') && ref.referenceKind === 'calls' && /^[A-Za-z_]\w*$/.test(ref.referenceName);
const vbReceiver = ref.language === 'vbnet' && (ref.referenceKind === 'calls' || ref.referenceKind === 'instantiates') && /^\w+$/.test(ref.referenceName)
@@ -3146,6 +3385,7 @@ export function matchByExactName(
!(cfmlBare && n.kind === 'method' && !isCfmlMethodInScope(n, ref, context)) &&
!(javaBare && n.kind === 'method' && !isJavaMethodInScope(n, ref, context)) &&
!(kotlinCall && !isKotlinTopLevelVisible(n, ref, context)) &&
!(kotlinBare && !isKotlinMemberReachable(n, ref, context)) &&
!(dartBare && isDartMember(n) && !isDartMethodInScope(n, ref, context)) &&
!(phpSelf && (n.kind !== 'method' || !isPhpMethodInScope(n, ref, phpSelf, context))) &&
!(pythonShape && !fitsPythonCallShape(n, pythonShape, ref, context)) &&
@@ -4267,6 +4507,9 @@ export function clearNameMatcherMemos(context: ResolutionContext): void {
DART_SUPERS.delete(context);
DART_HIERARCHIES.delete(context);
SWIFT_DECLS.delete(context);
KOTLIN_RECEIVER_TYPES.delete(context);
KOTLIN_HIERARCHIES.delete(context);
KOTLIN_FRAMES.delete(context);
MEMBER_TYPE_MEMO.delete(context);
CSHARP_ALIASES.delete(context);
SWIFT_HIERARCHIES.delete(context);
@@ -6668,6 +6911,7 @@ export function matchFuzzy(
const javaBare = ref.language === 'java' && ref.referenceKind === 'calls' && /^[A-Za-z_$][\w$]*$/.test(ref.referenceName);
const dartBare = ref.language === 'dart' && ref.referenceKind === 'calls' && /^[A-Za-z_$][\w$]*$/.test(ref.referenceName) && isReceiverLessDartCall(ref, context);
const kotlinCall = ref.language === 'kotlin' && ref.referenceKind === 'calls' && /^[A-Za-z_$][\w$]*$/.test(ref.referenceName);
const kotlinBare = kotlinCall && isReceiverLessKotlinCall(ref, context);
const rubyBare = ref.language === 'ruby' && ref.referenceKind === 'calls' && /^[A-Za-z_]\w*[?!]?$/.test(ref.referenceName);
const cfmlBare = (ref.language === 'cfml' || ref.language === 'cfscript') && ref.referenceKind === 'calls' && /^[A-Za-z_]\w*$/.test(ref.referenceName);
const vbReceiver = ref.language === 'vbnet' && (ref.referenceKind === 'calls' || ref.referenceKind === 'instantiates') && /^\w+$/.test(ref.referenceName)
@@ -6698,6 +6942,7 @@ export function matchFuzzy(
!(javaBare && n.kind === 'method' && !isJavaMethodInScope(n, ref, context)) &&
!(dartBare && isDartMember(n) && !isDartMethodInScope(n, ref, context)) &&
!(kotlinCall && !isKotlinTopLevelVisible(n, ref, context)) &&
!(kotlinBare && !isKotlinMemberReachable(n, ref, context)) &&
!(rubyBare && n.kind === 'method' && !isRubyMethodInScope(n, ref, context)) &&
!(cfmlBare && n.kind === 'method' && !isCfmlMethodInScope(n, ref, context)) &&
!(vbReceiver !== null && !isVbMemberReachable(n, vbReceiver)) &&