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

148 lines
4.7 KiB
Go

package languages
import (
"regexp"
"sort"
"strings"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser"
)
// Expo Modules cross-language support. An Expo native module is declared
// in Swift or Kotlin with a DSL inside `definition() -> ModuleDefinition`:
// `Name("Foo")` sets the JS module name and `Function("bar") { ... }` /
// `AsyncFunction("baz") { ... }` declare the JS-callable methods, while
// `Property("p")`, `Events("e")`, and `Constants { ... }` declare the other
// member kinds. On the JS side they are consumed via
// `requireNativeModule('Foo').bar(...)` — which the JS/TS extractor already
// emits as an rn-native placeholder, so the Expo bridge synthesizer can land
// it on these synthetic nodes.
var (
expoNameRe = regexp.MustCompile(`\bName\s*\(\s*"([^"]+)"`)
expoMemberRe = regexp.MustCompile(`\b(Function|AsyncFunction|Property|Events)\s*(?:<[^(]*>)?\s*\(\s*"([^"]+)"`)
expoConstantsRe = regexp.MustCompile(`\bConstants\s*[({]`)
expoModuleClassRe = regexp.MustCompile(`\bclass\s+(\w+)\s*(?:\([^)]*\))?\s*:\s*Module\b`)
)
// expoExport is one Expo DSL member declaration (function, property, events,
// or constants) attributed to a module.
type expoExport struct {
module string
method string
async bool
off int
kind string
}
// extractExpoModules scans Swift/Kotlin source for the Expo module DSL and
// returns each member declaration attributed to the most recent preceding
// Name("..."). A definition() body with no Name() falls back to its enclosing
// `class XxxModule : Module` (the "Module" suffix stripped) rather than being
// dropped. Returns nil when the file is not an Expo module (no
// ModuleDefinition marker).
func extractExpoModules(src []byte) []expoExport {
s := string(src)
if !strings.Contains(s, "ModuleDefinition") {
return nil
}
type marker struct {
off int
kind string
name string
async bool
}
var markers []marker
for _, m := range expoNameRe.FindAllStringSubmatchIndex(s, -1) {
markers = append(markers, marker{off: m[0], kind: "name", name: s[m[2]:m[3]]})
}
for _, m := range expoMemberRe.FindAllStringSubmatchIndex(s, -1) {
kw := s[m[2]:m[3]]
kind := "function"
switch kw {
case "Property":
kind = "property"
case "Events":
kind = "events"
}
markers = append(markers, marker{off: m[0], kind: kind, name: s[m[4]:m[5]], async: kw == "AsyncFunction"})
}
for _, m := range expoConstantsRe.FindAllStringIndex(s, -1) {
markers = append(markers, marker{off: m[0], kind: "constants", name: "Constants"})
}
sort.Slice(markers, func(i, j int) bool { return markers[i].off < markers[j].off })
// Module classes back the no-Name() fallback.
type classDecl struct {
off int
name string
}
var classes []classDecl
for _, m := range expoModuleClassRe.FindAllStringSubmatchIndex(s, -1) {
classes = append(classes, classDecl{off: m[0], name: s[m[2]:m[3]]})
}
enclosingModule := func(off int) string {
name, best := "", -1
for _, cd := range classes {
if cd.off <= off && cd.off > best {
best, name = cd.off, cd.name
}
}
if name == "" {
return ""
}
if trimmed := strings.TrimSuffix(name, "Module"); trimmed != "" {
return trimmed
}
return name
}
curModule := ""
var out []expoExport
for _, mk := range markers {
if mk.kind == "name" {
curModule = mk.name
continue
}
module := curModule
if module == "" {
module = enclosingModule(mk.off)
}
if module == "" {
continue
}
out = append(out, expoExport{module: module, method: mk.name, async: mk.async, off: mk.off, kind: mk.kind})
}
return out
}
// emitExpoModuleNodes materialises a synthetic node per Expo member
// declaration, carrying expo_module + expo_method + expo_kind so the Expo
// bridge synthesizer can pair a JS requireNativeModule('<module>').<member>
// access to it. A Property becomes a field node; every other member kind a
// method node.
func emitExpoModuleNodes(src []byte, filePath, language, fileID string, result *parser.ExtractionResult, seen map[string]bool) {
for _, ex := range extractExpoModules(src) {
id := filePath + "::expo:" + ex.module + ":" + ex.method
if seen[id] {
continue
}
seen[id] = true
line := lineAt(src, ex.off)
nodeKind := graph.KindMethod
if ex.kind == "property" {
nodeKind = graph.KindField
}
result.Nodes = append(result.Nodes, &graph.Node{
ID: id, Kind: nodeKind, Name: ex.method,
FilePath: filePath, StartLine: line, EndLine: line,
Language: language,
Meta: map[string]any{"expo_module": ex.module, "expo_method": ex.method, "expo_async": ex.async, "expo_kind": ex.kind},
})
result.Edges = append(result.Edges, &graph.Edge{
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: line,
})
}
}