Files
codegraph/__tests__/scala-bare-call-scope.test.ts
Colby Mchenry 7aa124c0aa fix(scala): a bare call reached by name alone must be in reach (#2160)
Read at the site:
- a later link of a chain (the extractor keeps one receiver level; the line
  shows the dot) reaches a member whose owner the receiver chain is named
  after (call / type arguments dropped: `proc("x").call()` → proc,
  `checker.value.onWrite` → checker), or a same-file member; never a package
  object's function unless it is a Scala 3 `extension` method;
- a name the enclosing def binds (parameter, val/var/def, lambda / for param)
  is only a def inside that function;
- otherwise a member of the types around it, an anonymous subclass's bases
  (`new OptionParser[C]("x") { head(…) }`), their extends/with supertypes
  (source-read heads), same file, or an imported object (`import Foo._`,
  `import Foo.{bar}`); a value import (`import builder._`) leaves the file
  unjudged.

A/B (edges removed / added): cats -1693/+678, sttp -887/+271, munit
-265/+175, scopt -245/+78, os-lib -115/+43.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 11:46:04 +00:00

91 lines
2.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* A bare Scala name reached by name alone must be in reach:
* - a parameter or local binds it: `f(true)` with `f: A => B` is not a case
* class's field `f` (cats: 438 times);
* - a later link of a chain (`fa.iterator.map(f)`) reaches a member of what
* the receiver is named after (`basicRequest.send` → Request), never a
* package object's function;
* - otherwise a member of the types around it — an anonymous subclass's base
* included (`new OptionParser[C]("x") { head("x") }`) — of their supertypes,
* or of an imported object.
*/
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-scope-'));
const files: Record<string, string> = {
'src/main/scala/app/FreeT.scala': `package app
final case class FlatMapped[A](f: A => A)
`,
'src/main/scala/app/LazyOps.scala': `package app
class LazyOps[A](xs: List[A]) {
def map[B](g: A => B): List[B] = xs.map(g)
}
`,
'src/main/scala/app/OptionParser.scala': `package app
abstract class OptionParser[C](name: String) {
def head(xs: String*): Unit = ()
}
`,
'src/main/scala/app/Base.scala': `package app
trait Base {
def helper(): Int = 1
}
`,
'src/main/scala/app/Request.scala': `package app
class Request {
def send(backend: String): String = backend
}
`,
'src/main/scala/app/Use.scala': `package app
object Use extends Base {
def tabulate[A](f: Boolean => A): (A, A) = (f(true), f(false))
def twice(fa: List[Int]): List[Int] = fa.iterator.map(_ + 1).toList
def run(basicRequest: Request): String = basicRequest.send("b")
def local(): Int = helper()
val parser = new OptionParser[Int]("scopt") {
head("scopt", "3.x")
}
}
`,
};
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('bare Scala calls', () => {
it('stay with locals, the receiver’s type, the class hierarchy and anonymous bases', () => {
const ids = cg.getNodesInFile('src/main/scala/app/Use.scala').map((n) => n.id);
const targets = cg
.getOutgoingEdgesFrom(ids)
.filter((e) => e.kind === 'calls')
.map((e) => cg.getNode(e.target)!.qualifiedName);
expect(targets.some((t) => t.endsWith('FlatMapped::f'))).toBe(false);
expect(targets.some((t) => t.endsWith('LazyOps::map'))).toBe(false);
expect(targets.some((t) => t.endsWith('Request::send'))).toBe(true);
expect(targets.some((t) => t.endsWith('Base::helper'))).toBe(true);
expect(targets.some((t) => t.endsWith('OptionParser::head'))).toBe(true);
});
});