diff --git a/CHANGELOG.md b/CHANGELOG.md index f4dc07df9..02b0bd527 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 +- In Scala, a kind-projector placeholder in a type, like `Align[Either[A, *]]`, is no longer linked to an operator method named `*`. A symbolic name in a type, like cats' `F ~> G`, now resolves only to a type. - C# type names now follow namespaces. A type is in reach from its own namespace or one nested inside it, or through a `using` of its namespace. Also counted: a project-wide `global using`, the project's `` entries, the SDK's implicit usings, or an alias that names it. Before, Newtonsoft's `async Task` tests linked `Task` to a test class of that name in another namespace, and AutoMapper's DTOs linked to another namespace's `BaseEntity`. - Rust type and function names now follow Rust's scoping. A name from another file counts only when a `use` brings it in (or a `*` glob over its module). One imported from outside the project, like `use std::task::{Context, Poll}` or `use futures::Stream`, means that outside item. A path names its module: `io::Result` isn't an unrelated `Result` alias elsewhere, and `std::io::Error` is std's. On tokio, over 1,200 `io::Result` and 600 `Context` references had been landing on private project types. - In Java, C#, Kotlin, Swift, Scala, Dart and VB.NET, a call now reaches the overload whose parameters fit its arguments, not whichever same-named overload was indexed first. For example, `HashCodeBuilder.reflectionHashCode(this)` now reaches the one-argument overload with varargs, not a three-parameter one. Each overload's callers are now its own. diff --git a/__tests__/scala-kind-projector.test.ts b/__tests__/scala-kind-projector.test.ts new file mode 100644 index 000000000..54d195712 --- /dev/null +++ b/__tests__/scala-kind-projector.test.ts @@ -0,0 +1,53 @@ +/** + * A Scala kind-projector placeholder in a type — `Align[Either[A, *]]`, + * `Functor[Map[K, ?]]` — names no method: cats' 567 `*` type arguments went + * to an algebra `Sign`'s `*` operator. + */ +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-scala-kp-')); + const files: Record = { + 'src/main/scala/algebra/Signed.scala': `package algebra + +object Signed { + sealed abstract class Sign(val toInt: Int) { + def *(that: Sign): Sign = this + } +} +`, + 'src/main/scala/cats/Align.scala': `package cats + +trait Align[F[_]] + +object Align { + implicit def catsAlignForEither[A]: Align[Either[A, *]] = null +} +`, + }; + 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('Scala kind-projector placeholders', () => { + it('are not references to a `*` method', () => { + const ids = cg.getNodesInFile('src/main/scala/cats/Align.scala').map((n) => n.id); + const targets = cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === 'references').map((e) => cg.getNode(e.target)!.name); + expect(targets).not.toContain('*'); + }); +}); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index d9b6fb814..5777451ec 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -4308,7 +4308,9 @@ export function matchByExactName( // A Scala type position (`Arbitrary[B]`) never names a method: an // `implicit def A: Order[A]` shares its name with half of cats' type // parameters. Scala's value references only read a file's own vals. - !(ref.language === 'scala' && ref.referenceKind === 'references' && /^[A-Z]/.test(ref.referenceName) && + // Nor does a kind-projector placeholder (`F[*]`, `G[?]`): cats' 567 `*` + // type arguments went to an algebra `Sign`'s `*` method. + !(ref.language === 'scala' && ref.referenceKind === 'references' && /^(?:[A-Z]|[^\w\s]+$)/.test(ref.referenceName) && (n.kind === 'method' || n.kind === 'function')) && // Type/value references retain same-family eligibility: a native namesake // must not hide the actual web type. Calls still gate only the winner. @@ -8278,6 +8280,14 @@ function matchReferenceInner( return null; } + // A symbolic name in a Scala type is a type (`F ~> G`) or a kind-projector + // placeholder (`Either[A, *]`) — never an operator method, by any strategy. + if (ref.language === 'scala' && ref.referenceKind === 'references' && /^[^\w\s]+$/.test(ref.referenceName)) { + const types = context.getNodesByName(ref.referenceName).filter((n) => n.language === 'scala' && (SCALA_TYPE_KINDS.has(n.kind) || n.kind === 'type_alias')); + const chosen = types.length > 1 ? preferCallSiteFile(types, ref.filePath) : types; + return chosen.length === 1 ? { original: ref, targetNodeId: chosen[0]!.id, confidence: 0.8, resolvedBy: 'exact-match' } : null; + } + // A bare Lua call through a `local` alias reaches what the alias names. if ((ref.language === 'lua' || ref.language === 'luau') && ref.referenceKind === 'calls' && /^[A-Za-z_]\w*$/.test(ref.referenceName)) { const aliased = luaAliasTarget(ref, context);