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