mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-14 16:09:26 +00:00
fix(extract): drop Scala generic type parameters from references
A `class Box[T]` / `def map[R](...)` binds `T`/`R` as local names, not corpus symbols, but `_scala_collect_type_refs` appended the bare identifiers used in a signature as type references and ran them through the sourceless-stub path. Every generic class/method in the corpus then collapsed its `T` onto one bare `t` node — a god node that pollutes god-node rankings, since single-letter type parameters are not in the analyzer's builtin-noise filter. C#, Java, TypeScript, Kotlin, and Go already exclude theirs. Mirrors that pattern: a new `_scala_type_parameters_in_scope` helper walks up to every enclosing declaration's `type_parameters` list (the use site is `type_arguments`, a distinct node) and collects parameter names from direct identifiers and variance-annotated params, skipping `context_bound`/ `upper_bound`/`lower_bound` nodes (those name real types). `_scala_collect_type_refs` auto-computes and threads that skip set. Context bounds (#2046), genuine user types, and the generic container head still reference.
This commit is contained in:
1 parent
a9860f70dc
commit
585e909535
2 files changed
+98
-7
No files matched your search
@@ -1407,15 +1407,55 @@ def _scala_given_synthetic_name(node, source: bytes) -> str | None:
|
||||
return "given"
|
||||
|
||||
|
||||
def _scala_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple[str, str]]) -> None:
|
||||
def _scala_type_parameters_in_scope(node, source: bytes) -> frozenset[str]:
|
||||
"""Return Scala type-parameter names visible from ``node``.
|
||||
|
||||
A `class Box[T]` / `def map[R](...)` binds `T`/`R` as local names, not
|
||||
corpus symbols. Walking up to every enclosing declaration's `type_parameters`
|
||||
list (the use site is `type_arguments`, a distinct node, so an instantiation
|
||||
argument is never collected) collects them for the scope skip set. A
|
||||
variance-annotated parameter (`[+A]`/`[-B]`) wraps its name one level
|
||||
deeper. A bound (`[A: Ordering]`, `[T <: Base]`) names a real type, not a
|
||||
parameter, so its `context_bound`/`upper_bound`/`lower_bound` node is skipped.
|
||||
"""
|
||||
names: set[str] = set()
|
||||
scope = node
|
||||
while scope is not None:
|
||||
for child in scope.children:
|
||||
if child.type != "type_parameters":
|
||||
continue
|
||||
for param in child.children:
|
||||
if param.type in ("identifier", "type_identifier"):
|
||||
name = _read_text(param, source)
|
||||
if name:
|
||||
names.add(name)
|
||||
elif param.type in ("covariant_type_parameter", "contravariant_type_parameter"):
|
||||
for sub in param.children:
|
||||
if sub.type in ("identifier", "type_identifier"):
|
||||
name = _read_text(sub, source)
|
||||
if name:
|
||||
names.add(name)
|
||||
break
|
||||
scope = scope.parent
|
||||
return frozenset(names)
|
||||
|
||||
|
||||
def _scala_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple[str, str]],
|
||||
skip: frozenset[str] | None = None) -> None:
|
||||
"""Walk a Scala type expression; append (name, role) tuples.
|
||||
Handles type_identifier, generic_type (List[T]), and common type wrappers."""
|
||||
Handles type_identifier, generic_type (List[T]), and common type wrappers.
|
||||
|
||||
Generic type parameters in scope (``skip``) are local bindings, not
|
||||
references; emitting one mints a sourceless stub (the bare `t` god node a
|
||||
`[T]` class or method would otherwise share corpus-wide)."""
|
||||
if node is None:
|
||||
return
|
||||
if skip is None:
|
||||
skip = _scala_type_parameters_in_scope(node, source)
|
||||
t = node.type
|
||||
if t == "type_identifier":
|
||||
text = _read_text(node, source)
|
||||
if text:
|
||||
if text and text not in skip:
|
||||
out.append((text, "generic_arg" if generic else "type"))
|
||||
return
|
||||
if t == "generic_type":
|
||||
@@ -1427,19 +1467,19 @@ def _scala_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple
|
||||
break
|
||||
if base is not None and base.type == "type_identifier":
|
||||
text = _read_text(base, source)
|
||||
if text:
|
||||
if text and text not in skip:
|
||||
out.append((text, "generic_arg" if generic else "type"))
|
||||
for c in node.children:
|
||||
if c.type == "type_arguments":
|
||||
for arg in c.children:
|
||||
if arg.is_named:
|
||||
_scala_collect_type_refs(arg, source, True, out)
|
||||
_scala_collect_type_refs(arg, source, True, out, skip)
|
||||
return
|
||||
if t in ("compound_type", "infix_type", "function_type", "tuple_type",
|
||||
"annotated_type", "projected_type"):
|
||||
for c in node.children:
|
||||
if c.is_named:
|
||||
_scala_collect_type_refs(c, source, generic, out)
|
||||
_scala_collect_type_refs(c, source, generic, out, skip)
|
||||
# A `match type` (`type Elem[X] = X match { case String => Char }`) names a
|
||||
# real type in every case clause -- both the type it discriminates on and
|
||||
# the type it yields. Without these two the walker returned nothing for a
|
||||
@@ -1448,7 +1488,7 @@ def _scala_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple
|
||||
if t in ("match_type", "type_case_clause"):
|
||||
for c in node.children:
|
||||
if c.is_named:
|
||||
_scala_collect_type_refs(c, source, generic, out)
|
||||
_scala_collect_type_refs(c, source, generic, out, skip)
|
||||
|
||||
|
||||
def _scala_collect_context_bounds(
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Scala generic type parameters must not mint sourceless god nodes.
|
||||
|
||||
A `class Box[T]` / `def map[R](...)` binds `T`/`R` as local names, not corpus
|
||||
symbols. The Scala type-ref pass appended the bare `T`/`R` identifiers used in
|
||||
a signature as type references and ran them through the sourceless-stub path,
|
||||
so every generic class/method in the corpus collapsed its `T` onto one bare `t`
|
||||
node — a god node (single-letter type params are not in the analyzer's
|
||||
builtin-noise filter). C#, Java, TypeScript, Kotlin, and Go already exclude
|
||||
their type parameters; Scala now matches.
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
from graphify.extract import extract
|
||||
|
||||
|
||||
def _sourceless_labels(r):
|
||||
return sorted({n.get("label") for n in r["nodes"] if not n.get("source_file")})
|
||||
|
||||
|
||||
def test_generic_class_and_method_type_params_mint_no_stub(tmp_path):
|
||||
(tmp_path / "S.scala").write_text(
|
||||
"class Box[T](v: T) {\n"
|
||||
" def get(): T = v\n"
|
||||
" def map[R](f: T => R): R = ???\n"
|
||||
"}\n"
|
||||
)
|
||||
r = extract([tmp_path / "S.scala"], cache_root=tmp_path, parallel=False)
|
||||
assert not any(e.get("target") in ("t", "r") for e in r["edges"])
|
||||
assert "T" not in _sourceless_labels(r)
|
||||
assert "R" not in _sourceless_labels(r)
|
||||
|
||||
|
||||
def test_bounds_param_dropped_but_context_bound_and_real_types_kept(tmp_path):
|
||||
# `A` is a parameter (dropped); the `Ordering` context bound is a real
|
||||
# dependency (kept, #2046), and a genuine user type in the signature still
|
||||
# references. (An upper bound `<: User` is deliberately not emitted, #2046.)
|
||||
(tmp_path / "S.scala").write_text(
|
||||
"class User(name: String)\n"
|
||||
"class Processor[A: Ordering] {\n"
|
||||
" def run(u: User): User = u\n"
|
||||
"}\n"
|
||||
)
|
||||
r = extract([tmp_path / "S.scala"], cache_root=tmp_path, parallel=False)
|
||||
refs = {
|
||||
rn["label"]
|
||||
for e in r["edges"] if e.get("relation") == "references"
|
||||
for rn in r["nodes"] if rn["id"] == e["target"]
|
||||
}
|
||||
assert "Ordering" in refs
|
||||
assert "User" in refs
|
||||
assert "A" not in refs
|
||||
Reference in new issue
Block a user