mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-19 06:22:38 +08:00
fix(kotlin): a Gradle script's bare call is never a project class's member (#2232)
In a .kts file, a bare call reaches a method/field/property only when its owner is no class the project declares (build logic's extensions on Gradle's types); the old rule let every member through. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Bare calls in Gradle Kotlin scripts (`build.gradle.kts`), like `plugins { }`, `dependencies { }` or `api(…)`, are no longer linked to a method or property of one of the project's own classes. Build logic's extension functions on Gradle's types still resolve. Before, nowinandroid's build scripts pointed every `plugins { }` and `dependencies { }` block at its build logic's `Graph` class, and every `api(…)` at a lint registry's `api` property.
|
||||
- In Rust, `Self::Error` in a signature now resolves to the enclosing impl's or trait's own associated type. A path through a generic parameter, like `V::Value`, no longer resolves to an unrelated struct of that name. Before, serde's `Self::Error` signatures pointed at its one `struct Error`, and axum's `Self::Error` at `axum-core`'s error type.
|
||||
- In Scala, a package object's member now resolves only where it is in scope: in its own package, the packages under it, or a file that imports through that package. Before, cats' laws resolved `Eq`, `Order` and `Monoid` to the `algebra` package object's aliases rather than the `cats` package object's, and sttp's akka tests reached a zio package object's `whenRequestMatches`.
|
||||
- A type that extends another type of the same name now links to that other type instead of to itself. When the source spells out the qualifier, that qualifier picks between same-named candidates. Before, cats' `trait BigDecimalInstances extends cats.kernel.instances.BigDecimalInstances` and every `trait AllOps … with Functor.AllOps[…]` pointed back at the declaring trait, and Newtonsoft.Json's `JsonConverter<T> : JsonConverter` extended itself.
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* A Gradle script's bare calls — `plugins { }`, `dependencies { }`,
|
||||
* `api(projects.core)` — run on the build tool's own types, never on a class
|
||||
* the project declares: nowinandroid's 110 such calls went to its build
|
||||
* logic's `Graph.plugins()` / `Graph.dependencies()` and a lint registry's
|
||||
* `api` property. Build logic's extension functions on Gradle's types stay.
|
||||
*/
|
||||
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-gradle-kts-'));
|
||||
const files: Record<string, string> = {
|
||||
'build-logic/src/main/kotlin/com/example/Graph.kt': `package com.example
|
||||
|
||||
class Graph {
|
||||
fun plugins(): List<String> = emptyList()
|
||||
fun dependencies(): List<String> = emptyList()
|
||||
}
|
||||
`,
|
||||
'build-support/src/main/kotlin/SourceSets.kt': `import org.gradle.api.NamedDomainObjectContainer
|
||||
|
||||
fun NamedDomainObjectContainer<String>.createSourceSet(name: String) {
|
||||
}
|
||||
`,
|
||||
'app/build.gradle.kts': `plugins {
|
||||
id("com.android.application")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.core)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
createSourceSet("nonJvm")
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
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 });
|
||||
});
|
||||
|
||||
describe('Gradle Kotlin script calls', () => {
|
||||
it('never reach a member of a project class, but reach build-logic extensions', () => {
|
||||
const ids = cg.getNodesInFile('app/build.gradle.kts').map((n) => n.id);
|
||||
const targets = cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === 'calls').map((e) => cg.getNode(e.target)!.name);
|
||||
expect(targets).not.toContain('plugins');
|
||||
expect(targets).not.toContain('dependencies');
|
||||
expect(targets).toContain('createSourceSet');
|
||||
});
|
||||
});
|
||||
@@ -2724,13 +2724,22 @@ function kotlinHeadNames(head: string): string[] | null {
|
||||
* private `module`.
|
||||
*/
|
||||
function isKotlinMemberReachable(n: Node, ref: UnresolvedRef, context: ResolutionContext): boolean {
|
||||
// A Gradle script's bare calls — `plugins { }`, `dependencies { }`,
|
||||
// `api(projects.core)` — run on the build tool's own types: never a member
|
||||
// of a class the project declares (nowinandroid's `Graph.plugins()`, a lint
|
||||
// registry's `api` property). Build logic's extensions on Gradle's types
|
||||
// (`NamedDomainObjectContainer.createSourceSet(…)`) stay.
|
||||
if (ref.filePath.endsWith('.kts')) {
|
||||
if (n.kind !== 'method' && n.kind !== 'field' && n.kind !== 'property') return true;
|
||||
const cut = n.qualifiedName.lastIndexOf('::');
|
||||
const owner = cut > 0 ? n.qualifiedName.slice(0, cut).split(/::|\./).pop()! : '';
|
||||
return owner !== '' && !context.getNodesByName(owner).some((c) => isMethodOwnerKind(c) || c.kind === 'module');
|
||||
}
|
||||
// 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(/::|\./);
|
||||
|
||||
Reference in New Issue
Block a user