mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-15 00:01:39 +08:00
fix(kotlin): a private property, and the members of a private class, stay in their file
A Kotlin property was indexed without its visibility modifier, so the Kotlin file-local rule (#1745), which declines a private fun in another file, read every property as public: a call like token(...) could link to another file's @Volatile private var token, and JavaScript, Go or Python callers to a Kotlin test fixture's private val. A public member of a private class or object was reachable from other files too, though nothing outside its file can name it. Both extractors (wasm and the native kernel) now read a property's modifiers the way they read a function's, and a candidate inside a private Kotlin class or object is declined from another file. That second rule is Kotlin's alone: a Scala private[pkg] class, or one with a private constructor, is its package's, and a C# private nested class serves every file of its partial class. Applied to every language where private is file-local, as the PR had it, it removed 742 edges on http4s (right ones among them, like stream.receiveHeaders() on a private[h2] class H2Stream), 91 on scalachess and 10 on serilog (9 of them right typed edges). Validation: - cross-file-visibility.test.ts: 2 cases, red on main, green here; the kernel parity fixture gains a private val and an internal var. - Kernel parity (kotlin), 0 diffs: okhttp 588/619 files, mihon 891/940, nowinandroid 340/350, SystemUI's Kotlin 188/198 byte-identical, the same files deferred to wasm as with main's kernel; torture 1/1. - A/B against main01e0ac3a(with the Kotlin receiver typing of #1933): okhttp -274, mihon -177, SystemUI -94, every one a call from another file onto a private Kotlin property or a member of a private class or object (okhttp's private inner AndroidQuery.enqueue and RealWebSocketTest's private TestStreams, mihon's private IntentFilter filter that collections' filter { } reached, SystemUI's private MyExecutor.execute); nothing added. nowinandroid, jextract, click and gin identical; against main4eb8c2a5, http4s, scalachess and serilog were identical too. - Cost (mihon, median of 3 interleaved indexes, on main4eb8c2a5): CPU 25.7 s on main vs 25.5 s, wall 8.2 s on both. Original PR #1761 by @danusha2345. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Colby Mchenry
co-authored by
Claude Opus 5.5
parent
01e0ac3aa7
commit
13f983db5a
@@ -2,6 +2,21 @@ import type { Node as SyntaxNode } from 'web-tree-sitter';
|
||||
import { getNodeText } from '../tree-sitter-helpers';
|
||||
import type { LanguageExtractor } from '../tree-sitter-types';
|
||||
|
||||
/** The visibility a Kotlin declaration's `modifiers` child spells out; public by default. */
|
||||
function kotlinVisibility(node: { childCount: number; child(i: number): { type: string; text: string } | null }): 'public' | 'private' | 'protected' | 'internal' {
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const child = node.child(i);
|
||||
if (child?.type === 'modifiers') {
|
||||
const text = child.text;
|
||||
if (text.includes('public')) return 'public';
|
||||
if (text.includes('private')) return 'private';
|
||||
if (text.includes('protected')) return 'protected';
|
||||
if (text.includes('internal')) return 'internal';
|
||||
}
|
||||
}
|
||||
return 'public';
|
||||
}
|
||||
|
||||
/** Kotlin return types that can't be a chained-call receiver (no class to chain on). */
|
||||
const KOTLIN_NON_CLASS_RETURN = new Set(['Unit', 'Nothing']);
|
||||
|
||||
@@ -300,7 +315,10 @@ export const kotlinExtractor: LanguageExtractor = {
|
||||
const sig = typeNode
|
||||
? `${isVal ? 'val' : 'var'} ${name}: ${getNodeText(typeNode, ctx.source)}`
|
||||
: undefined;
|
||||
const created = ctx.createNode(kind, name, node, { signature: sig });
|
||||
// `private val` / `internal var` at class or object scope: the modifier
|
||||
// decides who can name it from another file (#1731's Kotlin rule reads
|
||||
// `visibility`, and a property without one read as public).
|
||||
const created = ctx.createNode(kind, name, node, { signature: sig, visibility: kotlinVisibility(node) });
|
||||
// Walk the initializer ATTRIBUTED to the declared symbol (#693, the Go
|
||||
// fix, ported to Kotlin): the hook consumes this subtree, so without an
|
||||
// explicit walk a lambda / SAM / object initializer
|
||||
@@ -504,20 +522,7 @@ export const kotlinExtractor: LanguageExtractor = {
|
||||
}
|
||||
return sig;
|
||||
},
|
||||
getVisibility: (node) => {
|
||||
// Check for visibility modifiers in Kotlin
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const child = node.child(i);
|
||||
if (child?.type === 'modifiers') {
|
||||
const text = child.text;
|
||||
if (text.includes('public')) return 'public';
|
||||
if (text.includes('private')) return 'private';
|
||||
if (text.includes('protected')) return 'protected';
|
||||
if (text.includes('internal')) return 'internal';
|
||||
}
|
||||
}
|
||||
return 'public'; // Kotlin defaults to public
|
||||
},
|
||||
getVisibility: (node) => kotlinVisibility(node),
|
||||
isStatic: (_node) => {
|
||||
// Kotlin doesn't have static, uses companion objects
|
||||
return false;
|
||||
|
||||
@@ -3384,6 +3384,38 @@ function isSfcPrivate(n: Node, context: ResolutionContext): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Per-context memo: node id → "this member sits inside a `private` class/object". */
|
||||
const PRIVATE_OWNER_MEMO = new WeakMap<ResolutionContext, Map<string, boolean>>();
|
||||
|
||||
/** Kinds that own members and carry a visibility of their own. */
|
||||
const OWNER_KINDS = new Set<string>(['class', 'interface', 'struct', 'enum', 'namespace']);
|
||||
|
||||
/**
|
||||
* Whether `candidate` is a member of a `private` type in its file — a Kotlin
|
||||
* test's `private class Clock { fun now() = value }`: the method is public,
|
||||
* the class is not, so nothing outside the file can name `now`. Found by
|
||||
* line range among the file's own nodes, memoised per node.
|
||||
*/
|
||||
function isInsidePrivateType(candidate: Node, context: ResolutionContext): boolean {
|
||||
let memo = PRIVATE_OWNER_MEMO.get(context);
|
||||
if (!memo) {
|
||||
memo = new Map();
|
||||
PRIVATE_OWNER_MEMO.set(context, memo);
|
||||
}
|
||||
const hit = memo.get(candidate.id);
|
||||
if (hit !== undefined) return hit;
|
||||
const inside = context.getNodesInFile(candidate.filePath).some(
|
||||
(n) =>
|
||||
n.id !== candidate.id &&
|
||||
OWNER_KINDS.has(n.kind) &&
|
||||
n.visibility === 'private' &&
|
||||
n.startLine <= candidate.startLine &&
|
||||
n.endLine >= candidate.endLine
|
||||
);
|
||||
memo.set(candidate.id, inside);
|
||||
return inside;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether Go lets `ref`'s file name the Go declaration `candidate`: an
|
||||
* exported name from anywhere, an unexported one only inside its own package,
|
||||
@@ -3525,7 +3557,12 @@ export function isVisibleAcrossFiles(candidate: Node, ref: UnresolvedRef, contex
|
||||
if (lang === 'dart') {
|
||||
return candidate.visibility !== 'private' || (ref.language === 'dart' && dartPrivateVisible(ref.filePath, candidate.filePath, context));
|
||||
}
|
||||
if (PRIVATE_IS_FILE_LOCAL.has(lang)) return candidate.visibility !== 'private';
|
||||
if (PRIVATE_IS_FILE_LOCAL.has(lang)) {
|
||||
// Only Kotlin's private type keeps its members in its file: a Scala
|
||||
// `private[pkg] class` (or one with a private constructor) is the package's,
|
||||
// and a C# private nested class serves every file of its partial class.
|
||||
return candidate.visibility !== 'private' && !(lang === 'kotlin' && isInsidePrivateType(candidate, context));
|
||||
}
|
||||
// An R test file runs in an environment of its own (testthat): its top-level
|
||||
// `c <- ggplot(…)` is not what the package's 2,843 `c(…)` calls mean. The
|
||||
// `helper-*.R` / `setup-*.R` files are sourced for every test, so theirs are shared.
|
||||
@@ -19425,6 +19462,7 @@ export function clearNameMatcherMemos(context: ResolutionContext): void {
|
||||
JAVA_STATIC_IMPORTS.delete(context);
|
||||
PY_IMPORTS.delete(context);
|
||||
PY_MODULE_LOCAL.delete(context);
|
||||
PRIVATE_OWNER_MEMO.delete(context);
|
||||
SEALED_MODULES.delete(context);
|
||||
LOCAL_BINDING_MEMO.delete(context);
|
||||
LOCAL_BINDING_SITES.delete(context);
|
||||
|
||||
Reference in New Issue
Block a user