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

207 lines
8.0 KiB
Go

package languages
import (
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser"
sitter "github.com/zzet/gortex/internal/parser/tsitter"
)
// jsImportBindingCap bounds how many per-binding import / re-export edges a
// single statement may emit. Barrel files (`export { ... } from`) routinely
// re-export hundreds of names; without a cap one statement could add hundreds
// of edges and dwarf the rest of the file's graph. Over the cap we keep only
// the module-level edge so the dependency is still recorded.
const jsImportBindingCap = 64
// jsSpecifierNames reads an import_specifier / export_specifier and returns its
// upstream (original) name and its renamed name. The grammar lays both out
// positionally: the first identifier is the name in the source module, the
// second (present only for `x as y`) is the local alias (imports) or the
// exported name (re-exports). Returns ("", "") when no identifier is present.
func jsSpecifierNames(spec *sitter.Node, src []byte) (orig, alias string) {
var ids []*sitter.Node
for i, _nc := 0, int(spec.NamedChildCount()); i < _nc; i++ {
if c := spec.NamedChild(i); c != nil && c.Type() == "identifier" {
ids = append(ids, c)
}
}
if len(ids) > 0 {
orig = ids[0].Content(src)
}
if len(ids) > 1 {
alias = ids[1].Content(src)
}
return orig, alias
}
// jsCollectChildren returns every direct child of node whose type matches t.
func jsCollectChildren(node *sitter.Node, t string) []*sitter.Node {
var out []*sitter.Node
for i, _nc := 0, int(node.ChildCount()); i < _nc; i++ {
if c := node.Child(i); c != nil && c.Type() == t {
out = append(out, c)
}
}
return out
}
// jsNamedImportsNode locates the named_imports node of an import_statement.
// The grammar nests it under import_clause (`import { a } from …`), but tolerate
// it appearing directly under the statement across grammar revisions.
func jsNamedImportsNode(importNode *sitter.Node) *sitter.Node {
if n := findChildByType(importNode, "named_imports"); n != nil {
return n
}
if clause := findChildByType(importNode, "import_clause"); clause != nil {
return findChildByType(clause, "named_imports")
}
return nil
}
// emitJSPerBindingImports emits one EdgeImports per named binding of an import
// statement — `import { foo, bar as baz } from "mod"` yields an edge to
// unresolved::import::mod::foo and one to unresolved::import::mod::bar with
// Alias "baz". The module-level import edge is emitted separately by the
// caller and left untouched; these per-binding edges let find_usages and
// dependency analysis answer "who imports `foo` from mod" instead of only
// "who imports mod". Over jsImportBindingCap bindings the per-binding edges are
// skipped (the module edge still records the dependency).
func emitJSPerBindingImports(importNode *sitter.Node, importPath, fileID, filePath string, src []byte, result *parser.ExtractionResult) {
named := jsNamedImportsNode(importNode)
if named == nil {
return
}
specs := jsCollectChildren(named, "import_specifier")
if len(specs) == 0 || len(specs) > jsImportBindingCap {
return
}
for _, sp := range specs {
orig, alias := jsSpecifierNames(sp, src)
if orig == "" {
continue
}
edge := &graph.Edge{
From: fileID, To: "unresolved::import::" + importPath + "::" + orig,
Kind: graph.EdgeImports, FilePath: filePath, Line: int(sp.StartPoint().Row) + 1,
}
if alias != "" && alias != orig {
edge.Alias = alias
}
result.Edges = append(result.Edges, edge)
}
}
// emitJSReExport emits EdgeReExports edges for an `export ... from "mod"`
// statement — the re-export forms a barrel file uses to forward another
// module's surface. Three shapes:
//
// - named `export { a, b as c } from "mod"` — one edge per specifier to
// unresolved::import::mod::<original>, Alias set to the exported name when
// renamed (`b as c` → Alias "c"). Over jsImportBindingCap specifiers it
// collapses to a single module-level re-export edge.
// - namespace `export * as ns from "mod"` — one module-level edge, Alias "ns".
// - wildcard `export * from "mod"` — one module-level edge, no Alias.
//
// For each named (value) specifier within the cap it also mints a queryable
// node at the barrel site under the exported (post-alias) name, so the public
// façade a consumer imports (`import { persist } from 'zustand/middleware'`)
// resolves as a real symbol whose id follows the standard `<file>::<name>`
// scheme. The node carries a `reexport` marker and a node→canonical
// EdgeReExports edge (resolved through the same import machinery as the
// file-level edge) so find_usages can delegate to the canonical declaration's
// usages. Type-only specifiers are left as reference edges by
// emitTSTypeOnlyExportRefs and never mint a value node here.
func emitJSReExport(exportNode *sitter.Node, importPath, fileID, filePath, lang string, src []byte, result *parser.ExtractionResult) {
line := int(exportNode.StartPoint().Row) + 1
if clause := findChildByType(exportNode, "export_clause"); clause != nil {
specs := jsCollectChildren(clause, "export_specifier")
if len(specs) > jsImportBindingCap {
result.Edges = append(result.Edges, &graph.Edge{
From: fileID, To: "unresolved::import::" + importPath,
Kind: graph.EdgeReExports, FilePath: filePath, Line: line,
})
return
}
stmtTypeOnly := tsExportStatementTypeOnly(exportNode, src)
mintedNode := map[string]bool{}
for _, sp := range specs {
orig, alias := jsSpecifierNames(sp, src)
if orig == "" {
continue
}
specLine := int(sp.StartPoint().Row) + 1
edge := &graph.Edge{
From: fileID, To: "unresolved::import::" + importPath + "::" + orig,
Kind: graph.EdgeReExports, FilePath: filePath, Line: specLine,
}
if alias != "" && alias != orig {
edge.Alias = alias
}
result.Edges = append(result.Edges, edge)
// A type-only re-export (`export type { X } from` or
// `export { type X } from`) names a type, not a value — it is
// forwarded as a reference edge by emitTSTypeOnlyExportRefs, so
// no value node is minted for it.
if stmtTypeOnly || tsSpecifierTypeOnly(sp, src) {
continue
}
// The public binding name is the alias when renamed, else the
// original — that is the name a consumer imports.
publicName := orig
if alias != "" {
publicName = alias
}
nodeID := fileID + "::" + publicName
if mintedNode[nodeID] {
continue
}
mintedNode[nodeID] = true
meta := map[string]any{
"reexport": true,
"reexport_source": importPath,
}
if publicName != orig {
meta["reexport_original"] = orig
}
result.Nodes = append(result.Nodes, &graph.Node{
ID: nodeID, Kind: graph.KindVariable, Name: publicName,
FilePath: filePath, StartLine: specLine, EndLine: specLine,
Language: lang, Meta: meta,
})
// File → binding: EdgeDefines attaches the binding to the barrel
// file (get_file_summary and friends walk defines/contains).
result.Edges = append(result.Edges, &graph.Edge{
From: fileID, To: nodeID, Kind: graph.EdgeDefines,
FilePath: filePath, Line: specLine,
})
// Binding → canonical: a second EdgeReExports edge, this one
// sourced from the node, gives the barrel binding a direct link
// to the declaration it forwards once the resolver rewrites the
// unresolved target. find_usages walks it to delegate.
forward := &graph.Edge{
From: nodeID, To: "unresolved::import::" + importPath + "::" + orig,
Kind: graph.EdgeReExports, FilePath: filePath, Line: specLine,
}
if publicName != orig {
forward.Alias = publicName
}
result.Edges = append(result.Edges, forward)
}
return
}
edge := &graph.Edge{
From: fileID, To: "unresolved::import::" + importPath,
Kind: graph.EdgeReExports, FilePath: filePath, Line: line,
}
// `export * as ns from "mod"` — record the namespace alias.
if ns := findChildByType(exportNode, "namespace_export"); ns != nil {
if id := findChildByType(ns, "identifier"); id != nil {
edge.Alias = id.Content(src)
}
}
result.Edges = append(result.Edges, edge)
}