mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-27 04:49:43 +08:00
fix(kotlin): record infix calls as calls (#2174)
`Users.id eq id1`, `single { … } bind I::class`: an infix_expression was
never extracted, so a project's infix DSL had no callers (Exposed's
`eq` > 1,000). Extract it as a call of the middle simple_identifier —
`lhs.fn` when the left operand is a plain name, else bare — in the TS
walker and the kernel (kernel-parity 0 diffs on okio/okhttp/Exposed).
A standard infix name on an expression receiver is gated like a chain
link (`alias(x) apply false` in a build script); a project's bitwise
extension on a number type (okio's `Byte.and`) is never chosen, typed or
guessed, since the stdlib's Int/Long members take `x and 0xff`.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0b74e94ee7
commit
c6491ff62d
@@ -1423,6 +1423,9 @@ export class TreeSitterExtractor {
|
||||
else if (this.extractor.callTypes.includes(nodeType)) {
|
||||
this.extractCall(node);
|
||||
}
|
||||
else if (this.language === 'kotlin' && nodeType === 'infix_expression') {
|
||||
this.extractKotlinInfixCall(node);
|
||||
}
|
||||
// `new Foo(...)` / `Foo::new(...)` / object_creation_expression —
|
||||
// produce an `instantiates` reference. Children still walked so
|
||||
// nested calls inside the constructor args (`new Foo(bar())`) get
|
||||
@@ -4064,6 +4067,31 @@ export class TreeSitterExtractor {
|
||||
return this.erlangAtomMacros.get(macroName) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A Kotlin infix call — `Users.id eq id1`, `a to b`, `x shouldBe y` — is a
|
||||
* call of the infix function in the middle: `receiver.fn` when the left
|
||||
* operand is a plain name (as `receiver.fn(arg)` would be), else the bare
|
||||
* name. Nothing records it otherwise, so a project's infix DSL had no
|
||||
* callers. Mirrored in the kernel's extract_infix_call (kotlin.rs).
|
||||
*/
|
||||
private extractKotlinInfixCall(node: SyntaxNode): void {
|
||||
if (this.nodeStack.length === 0 || node.namedChildCount !== 3) return;
|
||||
const lhs = node.namedChild(0);
|
||||
const fn = node.namedChild(1);
|
||||
if (!lhs || !fn || fn.type !== 'simple_identifier' || LITERAL_RECEIVER_TYPES.has(lhs.type)) return;
|
||||
const callerId = this.nodeStack[this.nodeStack.length - 1];
|
||||
if (!callerId) return;
|
||||
const name = getNodeText(fn, this.source);
|
||||
const receiver = lhs.type === 'simple_identifier' ? getNodeText(lhs, this.source) : '';
|
||||
this.unresolvedReferences.push({
|
||||
fromNodeId: callerId,
|
||||
referenceName: receiver && receiver !== 'this' && receiver !== 'super' ? `${receiver}.${name}` : name,
|
||||
referenceKind: 'calls',
|
||||
line: node.startPosition.row + 1,
|
||||
column: node.startPosition.column,
|
||||
});
|
||||
}
|
||||
|
||||
private extractCall(node: SyntaxNode): void {
|
||||
if (this.nodeStack.length === 0) return;
|
||||
|
||||
@@ -5904,6 +5932,8 @@ export class TreeSitterExtractor {
|
||||
this.extractAnonymousClass(node, anonBody);
|
||||
return;
|
||||
}
|
||||
} else if (this.language === 'kotlin' && nodeType === 'infix_expression') {
|
||||
this.extractKotlinInfixCall(node);
|
||||
} else if (this.extractor!.extractBareCall) {
|
||||
const calleeName = this.extractor!.extractBareCall(node, this.source);
|
||||
if (calleeName && this.nodeStack.length > 0) {
|
||||
|
||||
@@ -1674,12 +1674,32 @@ function kotlinChainReceiver(ref: UnresolvedRef, context: ResolutionContext): st
|
||||
const m = new RegExp(`(?<![\\w$])${name}\\s*[({]`).exec(line);
|
||||
start = m ? m.index : -1;
|
||||
}
|
||||
if (start < 0) return null;
|
||||
if (start < 0) {
|
||||
// An infix call on an expression — `alias(libs.x) apply false` — whose
|
||||
// receiver the extractor could not name.
|
||||
const infix = new RegExp(`(?<=[\\w)\\]"'}]\\s+)${name}\\s+(?=[^\\s=])`).exec(line);
|
||||
return infix ? rustGoReceiverName(line.slice(0, infix.index).trimEnd()) : null;
|
||||
}
|
||||
const before = line.slice(0, start);
|
||||
if (!/\.\s*$/.test(before)) return null;
|
||||
return rustGoReceiverName(before.replace(/\?\s*\.\s*$/, '').replace(/!!\s*$/, ''));
|
||||
}
|
||||
|
||||
const KOTLIN_BITWISE_INFIX: ReadonlySet<string> = new Set(['and', 'or', 'xor', 'shl', 'shr', 'ushr', 'inv']);
|
||||
const KOTLIN_NUMBER_TYPES: ReadonlySet<string> = new Set(['Byte', 'Short', 'Int', 'Long', 'UByte', 'UShort', 'UInt', 'ULong', 'Char']);
|
||||
|
||||
/**
|
||||
* A project's bitwise extension on a number type — okio's `infix fun
|
||||
* Byte.and(mask: Int)` — is indistinguishable, without the operand's type,
|
||||
* from the standard library's own `Int.and` / `Long.shr` members every other
|
||||
* `x and 0xff` / `h shr 8` calls; neither is a safe edge.
|
||||
*/
|
||||
function isKotlinNumberBitwise(n: Node, ref: UnresolvedRef): boolean {
|
||||
if (ref.language !== 'kotlin' || !KOTLIN_BITWISE_INFIX.has(n.name)) return false;
|
||||
const cut = n.qualifiedName.lastIndexOf('::');
|
||||
return cut > 0 && KOTLIN_NUMBER_TYPES.has(n.qualifiedName.slice(0, cut).split(/::|\./).pop()!);
|
||||
}
|
||||
|
||||
/** 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];
|
||||
@@ -3386,6 +3406,7 @@ export function matchByExactName(
|
||||
!(javaBare && n.kind === 'method' && !isJavaMethodInScope(n, ref, context)) &&
|
||||
!(kotlinCall && !isKotlinTopLevelVisible(n, ref, context)) &&
|
||||
!(kotlinBare && !isKotlinMemberReachable(n, ref, context)) &&
|
||||
!isKotlinNumberBitwise(n, ref) &&
|
||||
!(dartBare && isDartMember(n) && !isDartMethodInScope(n, ref, context)) &&
|
||||
!(phpSelf && (n.kind !== 'method' || !isPhpMethodInScope(n, ref, phpSelf, context))) &&
|
||||
!(pythonShape && !fitsPythonCallShape(n, pythonShape, ref, context)) &&
|
||||
@@ -5478,6 +5499,11 @@ export function matchMethodCall(
|
||||
'instance-method',
|
||||
importedFqn,
|
||||
));
|
||||
if (typedMatch && ref.language === 'kotlin') {
|
||||
// `medium and 0xff` on an Int: the standard library's member, not a project `Int.and(Long)`.
|
||||
const target = context.getNodeById?.(typedMatch.targetNodeId);
|
||||
if (target && isKotlinNumberBitwise(target, ref)) return null;
|
||||
}
|
||||
if (typedMatch) {
|
||||
if (awaited) {
|
||||
const target = context.getNodeById?.(typedMatch.targetNodeId);
|
||||
@@ -5766,6 +5792,7 @@ export function matchMethodCall(
|
||||
narrowed = kept.length !== targetMethods.length;
|
||||
targetMethods = kept;
|
||||
}
|
||||
targetMethods = targetMethods.filter((m) => !isKotlinNumberBitwise(m, ref));
|
||||
// Production code never calls into a test suite: a guess from
|
||||
// rest_framework/renderers.py's `view.reverse_action(…)` is not a test's
|
||||
// `DummyView`. The test's methods were never in the running.
|
||||
@@ -6943,6 +6970,7 @@ export function matchFuzzy(
|
||||
!(dartBare && isDartMember(n) && !isDartMethodInScope(n, ref, context)) &&
|
||||
!(kotlinCall && !isKotlinTopLevelVisible(n, ref, context)) &&
|
||||
!(kotlinBare && !isKotlinMemberReachable(n, ref, context)) &&
|
||||
!isKotlinNumberBitwise(n, ref) &&
|
||||
!(rubyBare && n.kind === 'method' && !isRubyMethodInScope(n, ref, context)) &&
|
||||
!(cfmlBare && n.kind === 'method' && !isCfmlMethodInScope(n, ref, context)) &&
|
||||
!(vbReceiver !== null && !isVbMemberReachable(n, vbReceiver)) &&
|
||||
|
||||
Reference in New Issue
Block a user