Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

347 lines
13 KiB
Go

package languages
import (
"strconv"
"strings"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser"
sitter "github.com/zzet/gortex/internal/parser/tsitter"
)
// emitScalaReferenceForms walks a parsed Scala compilation unit and emits the
// structural reference edges that #143's type-annotation pass did not cover:
//
// - INSTANTIATION — `new Foo(...)` (instance_expression) and a Capitalized
// apply `Foo(...)` (call_expression on a Capitalized identifier) emit
// graph.EdgeInstantiates to the constructed type.
// - INHERITANCE — every supertype / mixin in a class/object/trait's
// `extends_clause` (`class X extends Base with T1 with T2`) emits
// EdgeReferences with Meta["ref_context"]=RefContextInherit, attributed to
// the defining type.
// - CASTS / TYPE-TESTS — `x.isInstanceOf[Foo]`, `x.asInstanceOf[Foo]`, and a
// `case _: Foo` typed match pattern emit EdgeReferences with
// ref_context=RefContextCast.
// - STATIC / OBJECT access — member access whose head is a bare Capitalized
// identifier (`Foo.CONST`, `Foo.apply`, `Foo.method`) emits EdgeReferences
// with ref_context=RefContextStaticAccess to the access root.
//
// All edges are stamped Origin=graph.OriginASTResolved so the cross-package
// name-match guard (which only reverts the two weakest tiers) leaves the bare
// `unresolved::X` placeholders intact for resolveTypeOrFunc / resolveTypeRef to
// bind. Type names are canonicalized with the shared Scala normalizer (square
// bracket generics stripped, container wrappers unwrapped, dotted prefix
// dropped) and primitives are filtered, exactly as the annotation pass does.
//
// Scope rules that keep the pass from double-emitting #143's val/var/param/
// return annotation edges or flooding the graph with noise:
// - only Capitalized leaf type names survive (a `Foo`, never a `foo`);
// - static access fires only when the access head is a bare Capitalized
// identifier — `this.x`, `super.x`, and lowercase-val selects are excluded;
// - per-(owner, type, line, ref_context) dedup drops repeats.
func emitScalaReferenceForms(root *sitter.Node, filePath string, src []byte, result *parser.ExtractionResult) {
if root == nil {
return
}
funcRanges := buildFuncRanges(result)
emitted := make(map[string]bool)
// emit appends one reference-form edge, deduplicated per
// (owner, type, line, ref_context). kind is EdgeInstantiates for
// construction and EdgeReferences for the structural ref_context forms.
emit := func(ownerID, typeName, filePath string, line int, kind graph.EdgeKind, refCtx string) {
if ownerID == "" || typeName == "" {
return
}
canon := canonicalizeScalaTypeRef(typeName)
if canon == "" || isScalaPrimitive(canon) || !isScalaCapitalized(canon) {
return
}
key := ownerID + "\x00" + canon + "\x00" + strconv.Itoa(line) + "\x00" + refCtx
if emitted[key] {
return
}
emitted[key] = true
e := &graph.Edge{
From: ownerID,
To: "unresolved::" + canon,
Kind: kind,
FilePath: filePath,
Line: line,
Origin: graph.OriginASTResolved,
}
if refCtx != "" {
e.Meta = map[string]any{"ref_context": refCtx}
}
result.Edges = append(result.Edges, e)
}
// ownerAt returns the enclosing function/method node id for an
// expression-position reference, or the file node when none.
ownerAt := func(line int) string {
if id := findEnclosingFunc(funcRanges, line); id != "" {
return id
}
return filePath
}
walkNodes(root, func(n *sitter.Node) {
switch n.Type() {
case "class_definition", "object_definition", "trait_definition", "enum_definition":
emitScalaInheritEdges(n, filePath, src, emit)
case "instance_expression":
// `new Foo(...)` — the constructed type is the type_identifier /
// generic_type child directly under the `new`.
line := int(n.StartPoint().Row) + 1
owner := ownerAt(line)
if t := scalaInstanceType(n, src); t != "" {
emit(owner, t, filePath, line, graph.EdgeInstantiates, "")
}
case "call_expression":
// A Capitalized apply `Foo(...)` is companion-apply construction.
// (`foo()` and method-selector calls are left to extractCall.)
callee := n.NamedChild(0)
if callee == nil || callee.Type() != "identifier" {
return
}
name := strings.TrimSpace(callee.Content(src))
if !isScalaCapitalized(name) {
return
}
line := int(n.StartPoint().Row) + 1
emit(ownerAt(line), name, filePath, line, graph.EdgeInstantiates, "")
case "generic_function":
// `x.isInstanceOf[Foo]` / `x.asInstanceOf[Foo]` — the cast target
// is the lone type_arguments entry; only fire for the two builtin
// type-test methods.
if !scalaIsInstanceCheck(n, src) {
return
}
line := int(n.StartPoint().Row) + 1
owner := ownerAt(line)
for _, t := range scalaTypeArgNames(n, src) {
emit(owner, t, filePath, line, graph.EdgeReferences, graph.RefContextCast)
}
case "typed_pattern":
// `case _: Foo` / `case d: Foo` — the matched-against type is the
// type_identifier / generic_type child of the pattern.
line := int(n.StartPoint().Row) + 1
owner := ownerAt(line)
if t := scalaPatternType(n, src); t != "" {
emit(owner, t, filePath, line, graph.EdgeReferences, graph.RefContextCast)
}
case "field_expression":
// `Foo.CONST` / `Foo.apply` — static / companion-object access when
// the head is a bare Capitalized identifier. Excludes `this.x`,
// `super.x`, lowercase-val selects, and the receiver of an
// isInstanceOf/asInstanceOf check (its head is lowercase anyway).
head := n.NamedChild(0)
if head == nil || head.Type() != "identifier" {
return
}
name := strings.TrimSpace(head.Content(src))
if !isScalaCapitalized(name) {
return
}
line := int(n.StartPoint().Row) + 1
emit(ownerAt(line), name, filePath, line, graph.EdgeReferences, graph.RefContextStaticAccess)
case "generic_type":
// `List[Foo]`, `Map[String, Bar]`, `Future[Seq[Repo]]`, … — every
// type argument is a use of the named element type. The type-
// annotation pass only keeps the container head (`Map`) or the single
// unwrapped element (`Option[User]` -> `User`), so an argument in a
// non-unwrap position (`Map[String, Bar]` -> `Bar`) or a deeper-nested
// argument (`Map[String, List[Account]]` -> `Account`) would otherwise
// never surface to find_usages. This case emits a generic_arg
// reference for each direct argument's named head; nested
// `generic_type` arguments are revisited by the walker, so their own
// arguments are covered too.
//
// Fires for a `generic_type` in any position — val/var annotation,
// parameter, return type, a supertype in `extends_clause`, an
// isInstanceOf/asInstanceOf or pattern target, or nested inside
// another generic — because walkNodes visits them all.
line := int(n.StartPoint().Row) + 1
owner := ownerAt(line)
for _, arg := range scalaGenericArgHeads(n, src) {
emit(owner, arg, filePath, line, graph.EdgeReferences, graph.RefContextGenericArg)
}
}
})
}
// emitScalaInheritEdges emits an inherit-context EdgeReferences from a defining
// type to every supertype / mixin listed in its extends_clause. The owner is
// the type's own node id (`filePath::Name`), matching the id the extractors
// mint for the class/object/trait/enum.
func emitScalaInheritEdges(def *sitter.Node, filePath string, src []byte, emit func(ownerID, typeName, filePath string, line int, kind graph.EdgeKind, refCtx string)) {
name := scalaFindChildIdentifier(def, src)
if name == "" {
return
}
ownerID := filePath + "::" + name
for i, _nc := 0, int(def.NamedChildCount()); i < _nc; i++ {
c := def.NamedChild(i)
if c == nil || c.Type() != "extends_clause" {
continue
}
for j, _nc := 0, int(c.NamedChildCount()); j < _nc; j++ {
t := c.NamedChild(j)
if t == nil {
continue
}
switch t.Type() {
case "type_identifier", "generic_type", "stable_type_identifier", "projected_type", "compound_type":
line := int(t.StartPoint().Row) + 1
emit(ownerID, strings.TrimSpace(t.Content(src)), filePath, line, graph.EdgeReferences, graph.RefContextInherit)
}
}
}
}
// scalaInstanceType returns the constructed type text of a `new Foo(...)`
// instance_expression — the type_identifier / generic_type child following the
// `new` keyword, generics intact — or "".
func scalaInstanceType(n *sitter.Node, src []byte) string {
for i, _nc := 0, int(n.NamedChildCount()); i < _nc; i++ {
c := n.NamedChild(i)
if c == nil {
continue
}
switch c.Type() {
case "type_identifier", "generic_type", "stable_type_identifier", "projected_type", "compound_type":
return strings.TrimSpace(c.Content(src))
}
}
return ""
}
// scalaIsInstanceCheck reports whether a generic_function node is an
// `x.isInstanceOf[...]` / `x.asInstanceOf[...]` invocation, by reading the
// method-name identifier of its field_expression callee.
func scalaIsInstanceCheck(n *sitter.Node, src []byte) bool {
for i, _nc := 0, int(n.NamedChildCount()); i < _nc; i++ {
fe := n.NamedChild(i)
if fe == nil || fe.Type() != "field_expression" {
continue
}
// The method name is the last identifier child of the selector.
for j := int(fe.NamedChildCount()) - 1; j >= 0; j-- {
id := fe.NamedChild(j)
if id == nil || id.Type() != "identifier" {
continue
}
switch strings.TrimSpace(id.Content(src)) {
case "isInstanceOf", "asInstanceOf":
return true
}
return false
}
}
return false
}
// scalaTypeArgNames returns the named type entries of a node's `type_arguments`
// child (`[Foo, Bar]` -> ["Foo", "Bar"]), generics intact, or nil.
func scalaTypeArgNames(n *sitter.Node, src []byte) []string {
var out []string
for i, _nc := 0, int(n.NamedChildCount()); i < _nc; i++ {
ta := n.NamedChild(i)
if ta == nil || ta.Type() != "type_arguments" {
continue
}
for j, _nc := 0, int(ta.NamedChildCount()); j < _nc; j++ {
t := ta.NamedChild(j)
if t == nil {
continue
}
switch t.Type() {
case "type_identifier", "generic_type", "stable_type_identifier", "projected_type", "compound_type":
out = append(out, strings.TrimSpace(t.Content(src)))
}
}
}
return out
}
// scalaGenericArgHeads returns the head type name of every direct argument in a
// `generic_type` node's `type_arguments` child. A leaf `type_identifier`
// argument (`Foo`) yields its bare name; a nested `generic_type` argument
// (`Seq[Repo]`) yields its container head (`Seq`) — the walker revisits the
// nested node so its own arguments (`Repo`) are captured on that visit. Other
// type forms (tuple / function / wildcard arguments) are skipped. The emit
// closure canonicalizes (dotted prefix / single-arg container unwrap) and
// filters primitives, so this helper only locates the argument heads.
func scalaGenericArgHeads(n *sitter.Node, src []byte) []string {
var out []string
for i, _nc := 0, int(n.NamedChildCount()); i < _nc; i++ {
ta := n.NamedChild(i)
if ta == nil || ta.Type() != "type_arguments" {
continue
}
for j, _nc := 0, int(ta.NamedChildCount()); j < _nc; j++ {
arg := ta.NamedChild(j)
if arg == nil {
continue
}
switch arg.Type() {
case "type_identifier", "stable_type_identifier", "projected_type":
if name := strings.TrimSpace(arg.Content(src)); name != "" {
out = append(out, name)
}
case "generic_type":
// Emit the nested container head (`Seq` of `Seq[Repo]`); the
// walker visits this nested node separately for its own args.
if head := scalaGenericHead(arg, src); head != "" {
out = append(out, head)
}
}
}
}
return out
}
// scalaGenericHead returns the head type_identifier text of a `generic_type`
// node (`Seq[Repo]` -> "Seq"), or "".
func scalaGenericHead(n *sitter.Node, src []byte) string {
for i, _nc := 0, int(n.NamedChildCount()); i < _nc; i++ {
c := n.NamedChild(i)
if c == nil {
continue
}
if c.Type() == "type_identifier" {
return strings.TrimSpace(c.Content(src))
}
}
return ""
}
// scalaPatternType returns the type a typed_pattern matches against
// (`case d: Foo` -> "Foo"), generics intact, or "".
func scalaPatternType(n *sitter.Node, src []byte) string {
for i, _nc := 0, int(n.NamedChildCount()); i < _nc; i++ {
c := n.NamedChild(i)
if c == nil {
continue
}
switch c.Type() {
case "type_identifier", "generic_type", "stable_type_identifier", "projected_type", "compound_type":
return strings.TrimSpace(c.Content(src))
}
}
return ""
}
// isScalaCapitalized reports whether name's first rune is an uppercase ASCII
// letter — the lexical gate that separates a type / object reference (`Foo`)
// from a value / method (`foo`, `this`, lowercase vals). Empty / non-letter
// leading names are rejected.
func isScalaCapitalized(name string) bool {
name = strings.TrimSpace(name)
if name == "" {
return false
}
c := name[0]
return c >= 'A' && c <= 'Z'
}