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
207 lines
8.0 KiB
Go
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)
|
|
}
|