mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-02 01:37:32 +08:00
fix(scala): a chain link is never an object named like the member (#2199)
A later link of a Scala chain (`arbitrary[Int].map { ... }`) accepted any
type or object candidate: alleycats' `object map`, cats' `instances.map`,
`syntax.flatMap` and `show` objects took 418 such calls. A type or object is
now a chain link only when the receiver names what holds it - its outer type,
or for a top-level one its file's package (Scala qualified names leave the
package out): `sttp.client4.Response(...)`, `fs2.concurrent.Topic(1)`.
cats -418, sttp -5 (akka's `FormData.BodyPart`, a Java `_.stream()`), fs2
unchanged.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
71202c38df
commit
7493fdef21
@@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
### Fixes
|
||||
|
||||
- A Scala chained call like `xs.map { … }` no longer lands on an object that happens to be named `map`, as with cats' `alleycats.std.map` and its `syntax`/`instances` objects. A type or object in a chain is only matched when the receiver names what holds it, as in `sttp.client4.Response(…)`.
|
||||
- A Python call to a name the function or module binds itself now means that local value, not a same-named function in another file. In Django REST Framework's tests, `view = SomeView.as_view()` followed by `view(request)` had pointed at one test file's `def view`. The same goes for a parameter or a `for`/`with … as` target. A pytest fixture a test takes as a parameter is still the fixture.
|
||||
- A method call on a variable is no longer guessed to be a test double's method just because the double has the only method of that name. That covered classes named `Mocked…`, `Fake…`, `Stub…` or `Dummy…`: django-allauth's `resp.json()` calls had all gone to its `MockedResponse`. A test that names the double still calls it.
|
||||
- Kotlin imports now decide what a name means, as Java imports already do. `import java.net.Proxy` or `import org.mockito.Mockito.mock` no longer links to a same-named project symbol, and a member the file declares itself still comes first. An `import x.Foo` picks the right one of several same-named classes: on okhttp, `MockResponse.Builder()` and `Request.Builder()` now reach their classes. A renaming import like `import …internal.unsafeFlow as flow` means the function it names.
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* A Scala chain link `x.map { … }` is a member of what `x` is — never an
|
||||
* object that happens to be named `map` (alleycats' `object map`, cats'
|
||||
* `instances.map` / `syntax.flatMap` objects took 418 such calls). A type or
|
||||
* object is a chain link only when the receiver names what holds it:
|
||||
* `sttp.client4.Response(…)`, `fs2.concurrent.Topic(1)`.
|
||||
*/
|
||||
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-chain-'));
|
||||
const files: Record<string, string> = {
|
||||
'src/main/scala/alleycats/std/map.scala': `package alleycats.std
|
||||
|
||||
object map {
|
||||
val instances: Int = 1
|
||||
}
|
||||
`,
|
||||
'src/main/scala/app/client/Response.scala': `package app.client
|
||||
|
||||
case class Response(code: Int)
|
||||
`,
|
||||
'src/main/scala/app/Use.scala': `package app
|
||||
|
||||
class Use {
|
||||
def gen[A](a: A): List[A] = List(a)
|
||||
def run(): List[Int] = gen[Int](1).map { x => x + 1 }
|
||||
def make(): Any = app.client.Response(200)
|
||||
}
|
||||
`,
|
||||
};
|
||||
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 });
|
||||
});
|
||||
|
||||
const targetsFrom = (file: string) => {
|
||||
const ids = cg.getNodesInFile(file).map((n) => n.id);
|
||||
return cg.getOutgoingEdgesFrom(ids)
|
||||
.filter((e) => e.kind === 'calls' || e.kind === 'instantiates')
|
||||
.map((e) => cg.getNode(e.target)!.filePath)
|
||||
.sort();
|
||||
};
|
||||
|
||||
describe('Scala chain links', () => {
|
||||
it('never land on an object named like the member', () => {
|
||||
expect(targetsFrom('src/main/scala/app/Use.scala')).not.toContain('src/main/scala/alleycats/std/map.scala');
|
||||
});
|
||||
|
||||
it('reach a type the receiver’s package path names', () => {
|
||||
expect(targetsFrom('src/main/scala/app/Use.scala')).toContain('src/main/scala/app/client/Response.scala');
|
||||
});
|
||||
});
|
||||
@@ -2646,6 +2646,15 @@ function isScalaMemberInScope(n: Node, ref: UnresolvedRef, context: ResolutionCo
|
||||
// A later link of a chain: a member of what the receiver is named after,
|
||||
// never a package object's function — unless it is an `extension` method.
|
||||
if (n.filePath === ref.filePath) return true;
|
||||
// A type or object as a chain link (`pkg.Obj(…)`) is named by what holds it:
|
||||
// cats' `arbitrary[Int].map { … }` is no call of alleycats' `object map`.
|
||||
if (SCALA_TYPE_KINDS.has(n.kind)) {
|
||||
// Scala qualified names leave the package out: a top-level type's holder is its file's package.
|
||||
const outer = n.qualifiedName.split('::').slice(-2, -1)[0];
|
||||
const holder = outer !== undefined ? outer.split('.').pop()!
|
||||
: [...(context.readFile(n.filePath) ?? '').matchAll(/^\s*package\s+([\w.]+)\s*$/gm)].pop()?.[1]?.split('.').pop() ?? '';
|
||||
return holder !== '' && scalaReceiverName(before).split('.').pop() === holder;
|
||||
}
|
||||
if (!SCALA_MEMBER_KINDS.has(n.kind)) return n.kind !== 'function' || isScalaExtensionMethod(n, context);
|
||||
const receiver = scalaReceiverName(before);
|
||||
return receiver !== '' && sharesReceiverWord(receiver, n);
|
||||
|
||||
Reference in New Issue
Block a user