Files
codegraph/__tests__/scala-block-locals.test.ts
Colby Mchenry 8410db42a1 fix(scala): block-local vals, package imports and package objects (#2193)
- A `val` the graph files under a class but that is written inside a block
  of its body (cats' `test("…") { val f = … }`) is that block's alone. It
  took `f(…)` calls from lambda parameters in other suites and from cats'
  core library (`FreeT::FlatMapped::f` from option.scala, Invariant.scala…).
  A `val`/`const` declared inside a function's lines is likewise local
  (outside C/C++, whose nesting is not trusted; a Lua global stays global).
- In a class body with no enclosing method, a binder (a lambda parameter, a
  `val`) counts as local only inside a block still open at the site.
- Only an import rooted at a value (`import builder._`) lets any type's
  members in; a lowercase package path (`import cats.syntax.all._`) did too.
- An imported object's supertypes are in scope, and `package object x
  extends Api` (no node in the graph) is read from source - imported, or the
  file's own package: sttp's `multipart(...)`, `asJson(...)`, `asEither(...)`.

cats -478 +63, sttp -153 +81 (sampled losses: other tests' locals, scalatest
`test`/`eventually`, libc `free`, `sttp.model.Header`). fs2, http4s, okio,
koin, Exposed, lodash, excalidraw, flask, kong, jekyll, halo unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:15:47 +00:00

119 lines
3.3 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 Scala `val` written inside a block of a class body — cats' `test("…") {
* val f = … }` — is that block's, not a member of the class: a lambda
* parameter `f` in another suite (`forAll { (e: Int, f: Int => Int) => f(e) }`)
* and a sibling test's `f(2)` are not calls to it. A lowercase package import
* (`import cats.syntax.all._`) no longer lets every class's members in; an
* imported package object's supertypes do (`package object api extends Api`),
* as does the package object of the file's own package.
*/
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-block-'));
const files: Record<string, string> = {
'src/test/scala/app/ASuite.scala': `package app
class ASuite {
def test(name: String)(body: => Unit): Unit = ()
test("a") {
val f = (i: Int) => i
f(1)
}
test("c") {
f(2)
}
}
`,
'src/test/scala/app/BSuite.scala': `package app
import app.syntax.all._
class BSuite {
def test(name: String)(body: => Unit): Unit = ()
def forAll(p: (Int, Int => Int) => Boolean): Unit = ()
test("b") {
forAll { (e: Int, f: Int => Int) =>
f(e) == 1
}
}
}
`,
'src/main/scala/app/syntax/all.scala': `package app.syntax
object all
`,
'src/main/scala/app/Api.scala': `package app
trait Api {
def multipart(s: String): String = s
}
`,
'src/main/scala/app/api/package.scala': `package app
package object api extends Api
`,
'src/main/scala/app/client/Client.scala': `package app.client
import app.api._
class Client {
def send(): String = multipart("x")
}
`,
'src/main/scala/app/api/Inner.scala': `package app.api
class Inner {
def send(): String = multipart("y")
}
`,
};
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 });
});
/** `qualified name:line` of every call edge out of a file. */
function callsFrom(file: string): string[] {
const ids = cg.getNodesInFile(file).map((n) => n.id);
return cg
.getOutgoingEdgesFrom(ids)
.filter((e) => e.kind === 'calls')
.map((e) => `${cg.getNode(e.target)!.qualifiedName}:${e.line}`)
.sort();
}
describe('Scala block-local vals', () => {
it('are called from their own block only', () => {
expect(callsFrom('src/test/scala/app/ASuite.scala').filter((c) => c.startsWith('ASuite::f'))).toEqual(['ASuite::f:8']);
});
it('are not another suite’s lambda parameter, whatever the file imports', () => {
expect(callsFrom('src/test/scala/app/BSuite.scala').filter((c) => c.includes('::f:'))).toEqual([]);
});
});
describe('Scala package objects', () => {
it('bring their supertypes’ members in when imported, and into their own package', () => {
expect(callsFrom('src/main/scala/app/client/Client.scala')).toEqual(['Api::multipart:6']);
expect(callsFrom('src/main/scala/app/api/Inner.scala')).toEqual(['Api::multipart:4']);
});
});