From 27da2563484560295a30a9bdfe18dcc9bbf99f54 Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Thu, 1 Oct 2026 02:04:33 +0000 Subject: [PATCH] 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 --- CHANGELOG.md | 1 + __tests__/gradle-kts-dsl-calls.test.ts | 67 ++++++++++++++++++++++++++ src/resolution/name-matcher.ts | 13 ++++- 3 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 __tests__/gradle-kts-dsl-calls.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ec97e15c4..676d09ed9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 : JsonConverter` extended itself. diff --git a/__tests__/gradle-kts-dsl-calls.test.ts b/__tests__/gradle-kts-dsl-calls.test.ts new file mode 100644 index 000000000..62d34d5ee --- /dev/null +++ b/__tests__/gradle-kts-dsl-calls.test.ts @@ -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 = { + 'build-logic/src/main/kotlin/com/example/Graph.kt': `package com.example + +class Graph { + fun plugins(): List = emptyList() + fun dependencies(): List = emptyList() +} +`, + 'build-support/src/main/kotlin/SourceSets.kt': `import org.gradle.api.NamedDomainObjectContainer + +fun NamedDomainObjectContainer.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'); + }); +}); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index b148f2e7f..2934d2a48 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -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(/::|\./);