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
147 lines
4.7 KiB
Go
147 lines
4.7 KiB
Go
package resolver
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// resolveLuaRequires binds Lua / Luau `require(...)` import edges that the
|
|
// extractor left on `unresolved::import::<module>` placeholders onto the
|
|
// indexed module file they reference.
|
|
//
|
|
// The Lua extractor carries the full dotted module path for a classic string
|
|
// require (`require("a.b.c")` → target `a.b.c`) and the leaf module name plus
|
|
// a `roblox_path` for an instance-path require (`require(script.Parent.Foo)` →
|
|
// target `Foo`, with `roblox_path` set). Classic requires map the dotted path
|
|
// to candidate files (`a/b/c.lua`, the `.luau` form, then the `a/b/c/init.*`
|
|
// directory module), the direct repo-relative path winning, then a unique
|
|
// path-suffix match (the package-root net). Roblox instance-path requires bind
|
|
// to the unique `<leaf>.lua` / `<leaf>.luau` file in the repo, refusing on
|
|
// ambiguity. Edges whose target is not indexed stay external.
|
|
//
|
|
// Runs serially in ResolveAll's relative-import settle window, after
|
|
// resolveRelativeImports (which never touches Lua).
|
|
func (r *Resolver) resolveLuaRequires() {
|
|
if !r.graphHasLanguage("lua") && !r.graphHasLanguage("luau") {
|
|
return
|
|
}
|
|
fileLang := r.collectFileLanguages()
|
|
|
|
fileIDs := make(map[string]struct{}, 1024)
|
|
// filesByBase indexes every KindFile by basename for the suffix nets.
|
|
filesByBase := make(map[string][]string, 1024)
|
|
for n := range r.graph.NodesByKind(graph.KindFile) {
|
|
if n == nil || n.ID == "" {
|
|
continue
|
|
}
|
|
fileIDs[n.ID] = struct{}{}
|
|
base := n.ID
|
|
if i := strings.LastIndex(n.ID, "/"); i >= 0 {
|
|
base = n.ID[i+1:]
|
|
}
|
|
filesByBase[base] = append(filesByBase[base], n.ID)
|
|
}
|
|
|
|
var reindexBatch []graph.EdgeReindex
|
|
for e := range r.graph.EdgesByKind(graph.EdgeImports) {
|
|
if e == nil || !strings.HasPrefix(e.To, "unresolved::import::") {
|
|
continue
|
|
}
|
|
// Scoped warm pass: an unchanged repo's requires were already bound by a
|
|
// prior full pass, so only reconsider the changed repos' imports.
|
|
if !r.edgeFromInScope(e.From) {
|
|
continue
|
|
}
|
|
if lang := fileLang[e.From]; lang != "lua" && lang != "luau" {
|
|
continue
|
|
}
|
|
name := strings.TrimPrefix(e.To, "unresolved::import::")
|
|
if name == "" {
|
|
continue
|
|
}
|
|
var resolved string
|
|
if _, isRoblox := e.Meta["roblox_path"]; isRoblox {
|
|
resolved = resolveLuaRobloxRequire(filesByBase, name)
|
|
} else {
|
|
resolved = resolveLuaModuleRequire(fileIDs, filesByBase, name)
|
|
}
|
|
if resolved == "" {
|
|
continue
|
|
}
|
|
oldTo := e.To
|
|
e.To = resolved
|
|
e.Origin = graph.OriginASTResolved
|
|
reindexBatch = append(reindexBatch, graph.EdgeReindex{Edge: e, OldTo: oldTo})
|
|
}
|
|
if len(reindexBatch) > 0 {
|
|
r.graph.ReindexEdges(reindexBatch)
|
|
}
|
|
}
|
|
|
|
// luaModuleCandidates converts a dotted/slashed Lua module path to its ordered
|
|
// candidate file paths: the `.lua` / `.luau` file, then the `init` directory
|
|
// module.
|
|
func luaModuleCandidates(modPath string) []string {
|
|
p := strings.Trim(strings.ReplaceAll(modPath, ".", "/"), "/")
|
|
if p == "" {
|
|
return nil
|
|
}
|
|
return []string{p + ".lua", p + ".luau", p + "/init.lua", p + "/init.luau"}
|
|
}
|
|
|
|
// resolveLuaModuleRequire resolves a classic string require's dotted module
|
|
// path: the direct repo-relative candidate file wins; otherwise a unique
|
|
// path-suffix match (handling package-root-prefixed layouts) wins, refusing on
|
|
// ambiguity.
|
|
func resolveLuaModuleRequire(fileIDs map[string]struct{}, filesByBase map[string][]string, modPath string) string {
|
|
cands := luaModuleCandidates(modPath)
|
|
for _, c := range cands {
|
|
if _, ok := fileIDs[c]; ok {
|
|
return c
|
|
}
|
|
}
|
|
for _, c := range cands {
|
|
if m := luaUniqueSuffixMatch(filesByBase, c); m != "" {
|
|
return m
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// luaUniqueSuffixMatch returns the single indexed file equal to, or ending with
|
|
// `/`+path, or "" when there is none or more than one (ambiguous).
|
|
func luaUniqueSuffixMatch(filesByBase map[string][]string, path string) string {
|
|
base := path
|
|
if i := strings.LastIndex(path, "/"); i >= 0 {
|
|
base = path[i+1:]
|
|
}
|
|
suffix := "/" + path
|
|
match := ""
|
|
for _, cand := range filesByBase[base] {
|
|
if cand == path || strings.HasSuffix(cand, suffix) {
|
|
if match != "" && match != cand {
|
|
return "" // ambiguous across roots
|
|
}
|
|
match = cand
|
|
}
|
|
}
|
|
return match
|
|
}
|
|
|
|
// resolveLuaRobloxRequire binds a Roblox instance-path require by its leaf
|
|
// module name to the unique indexed `<leaf>.lua` / `<leaf>.luau` file, refusing
|
|
// on ambiguity.
|
|
func resolveLuaRobloxRequire(filesByBase map[string][]string, leaf string) string {
|
|
match := ""
|
|
for _, ext := range []string{".lua", ".luau"} {
|
|
for _, cand := range filesByBase[leaf+ext] {
|
|
if match != "" && match != cand {
|
|
return "" // ambiguous module name in the repo
|
|
}
|
|
match = cand
|
|
}
|
|
}
|
|
return match
|
|
}
|