mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-16 20:53:57 +08:00
8410db42a1
- 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>
119 lines
3.3 KiB
TypeScript
119 lines
3.3 KiB
TypeScript
/**
|
||
* 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']);
|
||
});
|
||
});
|