chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,310 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// emitPHPReferenceForms emits the PHP reference edges that the symbol /
|
||||
// type-use walk (EdgeTypedAs on typed properties, parameters, and return
|
||||
// types) does not cover — the *expression-site* and *inheritance-clause*
|
||||
// uses of a class / interface name:
|
||||
//
|
||||
// - INSTANTIATION `new Foo(...)` (object_creation_expression) whose
|
||||
// type is a Capitalized name / qualified_name → EdgeInstantiates
|
||||
// - INHERITANCE `class X extends Base` (base_clause) /
|
||||
// `class X implements I, J` (class_interface_clause)
|
||||
// → EdgeReferences, Meta["ref_context"]="inherit"
|
||||
// - TYPE-TEST `$x instanceof Foo` (binary_expression with the
|
||||
// `instanceof` operator) → EdgeReferences, Meta["ref_context"]="cast"
|
||||
// - STATIC / CONST `Foo::CONST` / `Foo::class`
|
||||
// (class_constant_access_expression), `Foo::method()`
|
||||
// (scoped_call_expression) whose scope is a Capitalized name
|
||||
// → EdgeReferences, Meta["ref_context"]="static_access"
|
||||
// - ATTRIBUTE `#[Foo]` / `#[Foo(...)]` (attribute, PHP 8) names the
|
||||
// attribute type Foo → EdgeReferences, Meta["ref_context"]="static_access"
|
||||
//
|
||||
// The inheritance forms are emitted here as EdgeReferences (ref_context
|
||||
// "inherit") rather than EdgeExtends / EdgeImplements: #143's
|
||||
// extractPhpTraitUse already owns trait composition (EdgeExtends via:trait),
|
||||
// and scope_parent on the class node carries the single base class for the
|
||||
// scope resolver. These ref edges give find_usages a "used as a base /
|
||||
// conformed interface" surface without changing the inheritance modelling.
|
||||
//
|
||||
// Each expression-site edge attributes to the enclosing function / method
|
||||
// (file node when nothing encloses the line); each inheritance edge
|
||||
// attributes to the declaring class / interface node, so find_usages lands
|
||||
// the reference without a language server. Targets are left at the canonical
|
||||
// bare-name placeholder `unresolved::Foo` for the resolver to bind.
|
||||
//
|
||||
// Origin is load-bearing. Every edge rides graph.OriginASTResolved: the
|
||||
// cross-package guard (internal/resolver/cross_pkg_guard.go) reverts
|
||||
// EdgeReferences / EdgeCalls edges to their unresolved placeholder *only* at
|
||||
// the two weakest tiers (text_matched / ast_inferred), so an ast_resolved
|
||||
// edge is out of its scope and survives. EdgeInstantiates is not a call-like
|
||||
// edge the guard polices at all, but it carries the same origin for
|
||||
// consistency.
|
||||
//
|
||||
// Scope discipline keeps the graph clean: only Capitalized leaf type names
|
||||
// survive (lowercase locals / variables and PHP builtins are skipped via the
|
||||
// Capitalization gate + phpBuiltinType), the leading namespace separator and
|
||||
// any namespace qualification are reduced to the bare class name by
|
||||
// canonicalizePHPTypeRef (`\App\Foo` → `Foo`), and the relative scopes
|
||||
// `self` / `static` / `parent` are excluded — a `self::x` scoped call uses a
|
||||
// relative_scope node (not a name), and the explicit name guard backstops it.
|
||||
func emitPHPReferenceForms(root *sitter.Node, src []byte, filePath, fileID string, result *parser.ExtractionResult) {
|
||||
if root == nil {
|
||||
return
|
||||
}
|
||||
funcRanges := buildFuncRanges(result)
|
||||
typeRanges := buildPHPTypeRanges(result)
|
||||
|
||||
// Dedup across the whole file on (owner, type, line, ref_context) so a
|
||||
// type referenced twice in the same role on the same line emits one edge.
|
||||
seen := map[string]bool{}
|
||||
|
||||
ownerFor := func(line int) string {
|
||||
if owner := findEnclosingFunc(funcRanges, line); owner != "" {
|
||||
return owner
|
||||
}
|
||||
// Fall back to the enclosing class (a class-level attribute /
|
||||
// inheritance clause is not inside any method) then the file node.
|
||||
if owner := findEnclosingFunc(typeRanges, line); owner != "" {
|
||||
return owner
|
||||
}
|
||||
return fileID
|
||||
}
|
||||
|
||||
// emit appends one reference-form edge for a single Capitalized,
|
||||
// non-builtin, non-relative type name. owner "" means "derive from the
|
||||
// enclosing function / class / file at this line".
|
||||
emit := func(rawType string, line int, owner string, kind graph.EdgeKind, refContext string) {
|
||||
canon := canonicalizePHPTypeRef(rawType)
|
||||
if canon == "" || phpBuiltinType(canon) || !isPHPTypeNameCapitalized(canon) || isPHPRelativeScope(canon) {
|
||||
return
|
||||
}
|
||||
if owner == "" {
|
||||
owner = ownerFor(line)
|
||||
}
|
||||
if owner == "" {
|
||||
return
|
||||
}
|
||||
key := owner + "\x00" + canon + "\x00" + strconv.Itoa(line) + "\x00" + refContext
|
||||
if seen[key] {
|
||||
return
|
||||
}
|
||||
seen[key] = true
|
||||
edge := &graph.Edge{
|
||||
From: owner,
|
||||
To: "unresolved::" + canon,
|
||||
Kind: kind,
|
||||
FilePath: filePath,
|
||||
Line: line,
|
||||
Origin: graph.OriginASTResolved,
|
||||
}
|
||||
if refContext != "" {
|
||||
edge.Meta = map[string]any{"ref_context": refContext}
|
||||
}
|
||||
result.Edges = append(result.Edges, edge)
|
||||
}
|
||||
|
||||
walkNodes(root, func(n *sitter.Node) {
|
||||
line := int(n.StartPoint().Row) + 1
|
||||
switch n.Type() {
|
||||
|
||||
case "object_creation_expression":
|
||||
// `new Foo(...)` / `new \App\Http\Client()` — the constructed type
|
||||
// is the leading type-shaped named child. `new $klass()` (a
|
||||
// variable) and `new class {}` (anonymous) have no name child.
|
||||
if name := phpCreationTypeName(n, src); name != "" {
|
||||
emit(name, line, "", graph.EdgeInstantiates, "")
|
||||
}
|
||||
|
||||
case "base_clause":
|
||||
// `class X extends Base` — emit one inherit reference per base
|
||||
// name (PHP single inheritance for classes, but interfaces may
|
||||
// extend several). Attributed to the enclosing class node.
|
||||
owner := ownerFor(line)
|
||||
for _, name := range phpClauseTypeNames(n, src) {
|
||||
emit(name, int(n.StartPoint().Row)+1, owner, graph.EdgeReferences, graph.RefContextInherit)
|
||||
}
|
||||
|
||||
case "class_interface_clause":
|
||||
// `class X implements I, J` / `interface X extends A, B` — one
|
||||
// inherit reference per named interface.
|
||||
owner := ownerFor(line)
|
||||
for _, name := range phpClauseTypeNames(n, src) {
|
||||
emit(name, int(n.StartPoint().Row)+1, owner, graph.EdgeReferences, graph.RefContextInherit)
|
||||
}
|
||||
|
||||
case "binary_expression":
|
||||
// `$x instanceof Foo` — only the instanceof operator names a type.
|
||||
if op := n.ChildByFieldName("operator"); op != nil && op.Type() == "instanceof" {
|
||||
if name := phpInstanceofTypeName(n, src); name != "" {
|
||||
emit(name, line, "", graph.EdgeReferences, graph.RefContextCast)
|
||||
}
|
||||
}
|
||||
|
||||
case "class_constant_access_expression":
|
||||
// `Foo::CONST` / `Foo::class` — the scope (first named child) is
|
||||
// the referenced type when it is a bare Capitalized name.
|
||||
if name := phpScopeTypeName(n, src); name != "" {
|
||||
emit(name, line, "", graph.EdgeReferences, graph.RefContextStaticAccess)
|
||||
}
|
||||
|
||||
case "scoped_call_expression":
|
||||
// `Foo::method()` — same scope rule. `self::` / `parent::` /
|
||||
// `static::` use a relative_scope node, so phpScopeTypeName
|
||||
// returns "" for them.
|
||||
if name := phpScopeTypeName(n, src); name != "" {
|
||||
emit(name, line, "", graph.EdgeReferences, graph.RefContextStaticAccess)
|
||||
}
|
||||
|
||||
case "attribute":
|
||||
// `#[Foo]` / `#[Foo(...)]` names the attribute type Foo.
|
||||
if name := phpAttributeRefName(n, src); name != "" {
|
||||
emit(name, line, "", graph.EdgeReferences, graph.RefContextStaticAccess)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// buildPHPTypeRanges returns the line ranges of every class / interface /
|
||||
// type node in the extraction result, so an inheritance clause or
|
||||
// class-level attribute (not inside any method) is attributed to its
|
||||
// declaring type rather than the file.
|
||||
func buildPHPTypeRanges(result *parser.ExtractionResult) []funcRange {
|
||||
var ranges []funcRange
|
||||
for _, n := range result.Nodes {
|
||||
if n.Kind == graph.KindType || n.Kind == graph.KindInterface {
|
||||
ranges = append(ranges, funcRange{id: n.ID, startLine: n.StartLine, endLine: n.EndLine})
|
||||
}
|
||||
}
|
||||
return ranges
|
||||
}
|
||||
|
||||
// isPHPTypeNameCapitalized reports whether a canonicalized type name begins
|
||||
// with an uppercase ASCII letter — the Capitalization gate that keeps
|
||||
// lowercase locals / variables / keywords out of the reference surface.
|
||||
// canonicalizePHPTypeRef has already stripped the nullable marker and
|
||||
// namespace qualification, so the first rune of the bare name discriminates.
|
||||
func isPHPTypeNameCapitalized(name string) bool {
|
||||
if name == "" {
|
||||
return false
|
||||
}
|
||||
c := name[0]
|
||||
return c >= 'A' && c <= 'Z'
|
||||
}
|
||||
|
||||
// isPHPRelativeScope reports whether a name is one of PHP's relative class
|
||||
// scopes — these never name a concrete type to reference. They are normally
|
||||
// caught earlier (relative_scope is a distinct grammar node, not a name),
|
||||
// but the guard backstops the Capitalized-name path defensively.
|
||||
func isPHPRelativeScope(name string) bool {
|
||||
switch strings.ToLower(name) {
|
||||
case "self", "static", "parent":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// phpCreationTypeName returns the constructed type's spelling from an
|
||||
// object_creation_expression (`new Foo()` → "Foo", `new \App\Client()` →
|
||||
// "\App\Client"). The type is the leading type-shaped named child; `new
|
||||
// $klass()` (dynamic) and `new class {}` (anonymous) have none and yield "".
|
||||
func phpCreationTypeName(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 "name", "qualified_name":
|
||||
return strings.TrimSpace(c.Content(src))
|
||||
case "arguments":
|
||||
// Reached the call arguments without a type name (dynamic /
|
||||
// anonymous creation) — nothing to reference.
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// phpClauseTypeNames returns every class / interface name listed in a
|
||||
// base_clause (`extends Base`) or class_interface_clause (`implements I, J`).
|
||||
func phpClauseTypeNames(clause *sitter.Node, src []byte) []string {
|
||||
var out []string
|
||||
for i, _nc := 0, int(clause.NamedChildCount()); i < _nc; i++ {
|
||||
c := clause.NamedChild(i)
|
||||
if c == nil {
|
||||
continue
|
||||
}
|
||||
switch c.Type() {
|
||||
case "name", "qualified_name":
|
||||
if t := strings.TrimSpace(c.Content(src)); t != "" {
|
||||
out = append(out, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// phpInstanceofTypeName returns the right-operand type of an instanceof
|
||||
// binary_expression (`$x instanceof Foo` → "Foo"). The grammar tags the type
|
||||
// as the `right` field. A dynamic test (`$x instanceof $klass`) yields a
|
||||
// variable_name on the right and is skipped by the type-shape check.
|
||||
func phpInstanceofTypeName(n *sitter.Node, src []byte) string {
|
||||
right := n.ChildByFieldName("right")
|
||||
if right == nil {
|
||||
return ""
|
||||
}
|
||||
switch right.Type() {
|
||||
case "name", "qualified_name":
|
||||
return strings.TrimSpace(right.Content(src))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// phpScopeTypeName returns the scope type name of a
|
||||
// class_constant_access_expression (`Foo::CONST`, `Foo::class`) or a
|
||||
// scoped_call_expression (`Foo::method()`) — the first named child when it is
|
||||
// a bare class name. `self::` / `static::` / `parent::` use a relative_scope
|
||||
// node and `$obj::method()` a variable_name, both of which return "".
|
||||
func phpScopeTypeName(n *sitter.Node, src []byte) string {
|
||||
scope := n.ChildByFieldName("scope")
|
||||
if scope == nil && n.NamedChildCount() > 0 {
|
||||
scope = n.NamedChild(0)
|
||||
}
|
||||
if scope == nil {
|
||||
return ""
|
||||
}
|
||||
switch scope.Type() {
|
||||
case "name", "qualified_name":
|
||||
return strings.TrimSpace(scope.Content(src))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// phpAttributeRefName returns the bare attribute type name from an `attribute`
|
||||
// node (`#[Route("/x")]` → "Route"). The name is the `name` field, or the
|
||||
// first name / qualified_name child.
|
||||
func phpAttributeRefName(n *sitter.Node, src []byte) string {
|
||||
if nm := n.ChildByFieldName("name"); nm != nil {
|
||||
return strings.TrimSpace(nm.Content(src))
|
||||
}
|
||||
for i, _nc := 0, int(n.NamedChildCount()); i < _nc; i++ {
|
||||
c := n.NamedChild(i)
|
||||
if c == nil {
|
||||
continue
|
||||
}
|
||||
switch c.Type() {
|
||||
case "name", "qualified_name":
|
||||
return strings.TrimSpace(c.Content(src))
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user