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
143 lines
4.3 KiB
Go
143 lines
4.3 KiB
Go
package resolver
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// Rails receiver-constant name-resolution. A Ruby `Const.method` call loses
|
|
// its receiver in the base call graph (it becomes `unresolved::*.method`), so
|
|
// the Ruby extractor stamps the receiver constant as Meta["recv_const"]. This
|
|
// pass binds those residual calls to the directory-located Rails definition
|
|
// the constant names:
|
|
//
|
|
// - `*Service` → /app/services/, `*Job` → /app/jobs/, `*Worker` → /app/workers/
|
|
// - `*Helper` → /app/helpers/ (a Ruby module)
|
|
// - a bare PascalCase constant that is a detected ActiveRecord model →
|
|
// /app/models/
|
|
//
|
|
// The class is resolved by directory convention (ResolveByConvention) and the
|
|
// call binds to the named method on it, or to the class itself when the method
|
|
// is inherited (ActiveRecord finders like `find`/`where` are not declared in
|
|
// the model). It is a fallback — an already-resolved call is never touched —
|
|
// and an ambiguous constant across two convention dirs is left unresolved.
|
|
|
|
var (
|
|
railsServiceDirs = []string{"/app/services/"}
|
|
railsJobDirs = []string{"/app/jobs/"}
|
|
railsWorkerDirs = []string{"/app/workers/"}
|
|
railsHelperDirs = []string{"/app/helpers/"}
|
|
railsModelDirs = []string{"/app/models/"}
|
|
)
|
|
|
|
// ResolveRailsRefs binds residual unresolved Ruby `Const.method` calls to
|
|
// their Rails definitions by directory convention. Returns the count bound.
|
|
func ResolveRailsRefs(g graph.Store) int {
|
|
if g == nil {
|
|
return 0
|
|
}
|
|
models := railsModelClassSet(g)
|
|
methodsByClass := railsClassMethodIndex(g)
|
|
resolved := 0
|
|
var reindex []graph.EdgeReindex
|
|
for e := range g.EdgesByKind(graph.EdgeCalls) {
|
|
if e == nil || e.Meta == nil || !graph.IsUnresolvedTarget(e.To) {
|
|
continue
|
|
}
|
|
recv, _ := e.Meta["recv_const"].(string)
|
|
if recv == "" {
|
|
continue
|
|
}
|
|
method := strings.TrimPrefix(graph.UnresolvedName(e.To), "*.")
|
|
fromFile := ""
|
|
if n := g.GetNode(e.From); n != nil {
|
|
fromFile = n.FilePath
|
|
}
|
|
if !strings.HasSuffix(fromFile, ".rb") {
|
|
continue
|
|
}
|
|
isModel, dirs := railsRefDirs(recv)
|
|
classID, _ := ResolveByConvention(g, recv, "", dirs, fromFile)
|
|
if classID == "" {
|
|
continue
|
|
}
|
|
if isModel && !models[classID] {
|
|
continue // bare PascalCase that isn't an ActiveRecord model
|
|
}
|
|
target := classID
|
|
if m := methodsByClass[classID][method]; m != "" {
|
|
target = m
|
|
}
|
|
oldTo := e.To
|
|
e.To = target
|
|
e.Origin = graph.OriginASTInferred
|
|
e.Confidence = 0.8
|
|
e.ConfidenceLabel = graph.ConfidenceLabelFor(graph.EdgeCalls, 0.8)
|
|
StampSynthesized(e, SynthRailsResolve)
|
|
reindex = append(reindex, graph.EdgeReindex{Edge: e, OldTo: oldTo})
|
|
resolved++
|
|
}
|
|
if len(reindex) > 0 {
|
|
g.ReindexEdges(reindex)
|
|
}
|
|
return resolved
|
|
}
|
|
|
|
// railsRefDirs classifies a receiver constant into its convention dirs;
|
|
// isModel marks the bare-PascalCase (ActiveRecord model) case, which is
|
|
// additionally gated on the resolved class being a detected model.
|
|
func railsRefDirs(recv string) (isModel bool, dirs []string) {
|
|
switch {
|
|
case strings.HasSuffix(recv, "Service"):
|
|
return false, railsServiceDirs
|
|
case strings.HasSuffix(recv, "Job"):
|
|
return false, railsJobDirs
|
|
case strings.HasSuffix(recv, "Worker"):
|
|
return false, railsWorkerDirs
|
|
case strings.HasSuffix(recv, "Helper"):
|
|
return false, railsHelperDirs
|
|
}
|
|
return true, railsModelDirs
|
|
}
|
|
|
|
// railsModelClassSet returns the set of class node IDs detected as
|
|
// ActiveRecord models — those with an outgoing EdgeModelsTable.
|
|
func railsModelClassSet(g graph.Store) map[string]bool {
|
|
out := map[string]bool{}
|
|
for e := range g.EdgesByKind(graph.EdgeModelsTable) {
|
|
if e != nil && e.From != "" {
|
|
out[e.From] = true
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// railsClassMethodIndex maps class/module node ID → method name → method node
|
|
// ID via the EdgeMemberOf edges.
|
|
func railsClassMethodIndex(g graph.Store) map[string]map[string]string {
|
|
classOf := map[string]string{}
|
|
for e := range g.EdgesByKind(graph.EdgeMemberOf) {
|
|
if e != nil && e.From != "" && e.To != "" {
|
|
classOf[e.From] = e.To
|
|
}
|
|
}
|
|
out := map[string]map[string]string{}
|
|
for _, n := range nodesByKindsOrAll(g, graph.KindMethod, graph.KindFunction) {
|
|
if n == nil {
|
|
continue
|
|
}
|
|
cls := classOf[n.ID]
|
|
if cls == "" {
|
|
continue
|
|
}
|
|
if out[cls] == nil {
|
|
out[cls] = map[string]string{}
|
|
}
|
|
if _, ok := out[cls][n.Name]; !ok {
|
|
out[cls][n.Name] = n.ID
|
|
}
|
|
}
|
|
return out
|
|
}
|