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
169 lines
4.4 KiB
Go
169 lines
4.4 KiB
Go
package languages
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
"github.com/zzet/gortex/internal/parser"
|
|
sitter "github.com/zzet/gortex/internal/parser/tsitter"
|
|
)
|
|
|
|
// detectEcto walks a defmodule body for Ecto schema declarations and
|
|
// emits an EdgeModelsTable from the module to a synthetic KindTable
|
|
// node. Two shapes are recognised, mirroring Ecto's two scaffolding
|
|
// macros:
|
|
//
|
|
// - schema "name" do … (Ecto.Schema, the canonical case)
|
|
// - embedded_schema do … (Ecto.Schema embedded — no table; we
|
|
// skip these because there's no DB
|
|
// binding to surface)
|
|
//
|
|
// `use Ecto.Schema` and friends aren't required for detection — the
|
|
// presence of a `schema "..." do` macro is the load-bearing signal.
|
|
func detectEcto(body *sitter.Node, src []byte, modID, modName, filePath string, result *parser.ExtractionResult) {
|
|
if body == nil {
|
|
return
|
|
}
|
|
tableName, line, ok := elixirEctoSchemaCall(body, src)
|
|
if !ok {
|
|
return
|
|
}
|
|
if tableName == "" {
|
|
return
|
|
}
|
|
tableID := ormTableNodeID(tableName)
|
|
if !ormTableNodeAlreadyEmitted(result, tableID) {
|
|
result.Nodes = append(result.Nodes, &graph.Node{
|
|
ID: tableID,
|
|
Kind: graph.KindTable,
|
|
Name: tableName,
|
|
FilePath: filePath,
|
|
Language: "elixir",
|
|
Meta: map[string]any{
|
|
"dialect": "orm",
|
|
"schema": "",
|
|
"source": "elixir-orm",
|
|
},
|
|
})
|
|
}
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: modID,
|
|
To: tableID,
|
|
Kind: graph.EdgeModelsTable,
|
|
FilePath: filePath,
|
|
Line: line,
|
|
Origin: graph.OriginASTResolved,
|
|
Meta: map[string]any{
|
|
"orm": "ecto",
|
|
"binding": "schema-macro",
|
|
"table_name": tableName,
|
|
"derivation": "override",
|
|
"source_attr": "schema/2",
|
|
},
|
|
})
|
|
}
|
|
|
|
// elixirEctoSchemaCall scans a do_block for `schema "<name>" do`
|
|
// macro calls and returns (tableName, lineNo, found). The Elixir
|
|
// tree-sitter grammar exposes a `schema "users" do … end` invocation
|
|
// as a `call` node whose target identifier is `schema` and whose
|
|
// first argument is a string literal — same shape every macro takes.
|
|
func elixirEctoSchemaCall(body *sitter.Node, src []byte) (string, int, bool) {
|
|
var found string
|
|
var foundLine int
|
|
walkAST(body, func(n *sitter.Node) bool {
|
|
if n == nil || found != "" {
|
|
return false
|
|
}
|
|
if n.Type() != "call" {
|
|
return true
|
|
}
|
|
target := n.ChildByFieldName("target")
|
|
if target == nil {
|
|
return true
|
|
}
|
|
name := strings.TrimSpace(target.Content(src))
|
|
if name != "schema" {
|
|
return true
|
|
}
|
|
// First arg of `schema/2` macro must be a string literal —
|
|
// Ecto rejects anything else. Defensive: bail when shape
|
|
// differs (e.g. dynamic schema name from a variable).
|
|
args := elixirCallArgs(n)
|
|
if args == nil {
|
|
return true
|
|
}
|
|
first := elixirFirstArg(args)
|
|
if first == nil {
|
|
return true
|
|
}
|
|
if first.Type() != "string" {
|
|
return true
|
|
}
|
|
lit := elixirStringLiteral(first, src)
|
|
if lit == "" {
|
|
return true
|
|
}
|
|
found = lit
|
|
foundLine = int(n.StartPoint().Row) + 1
|
|
return false
|
|
})
|
|
return found, foundLine, found != ""
|
|
}
|
|
|
|
// elixirCallArgs returns the `arguments` child of a call node, or nil
|
|
// when absent. Mirrors the tree-sitter-elixir convention.
|
|
func elixirCallArgs(call *sitter.Node) *sitter.Node {
|
|
if call == nil {
|
|
return nil
|
|
}
|
|
if a := call.ChildByFieldName("arguments"); a != nil {
|
|
return a
|
|
}
|
|
for i, _nc := 0, int(call.NamedChildCount()); i < _nc; i++ {
|
|
c := call.NamedChild(i)
|
|
if c == nil {
|
|
continue
|
|
}
|
|
if c.Type() == "arguments" {
|
|
return c
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// elixirFirstArg returns the first non-do-block argument from an
|
|
// `arguments` node. Skips over `keywords`/`do_block` siblings the
|
|
// grammar may attach.
|
|
func elixirFirstArg(args *sitter.Node) *sitter.Node {
|
|
if args == nil {
|
|
return nil
|
|
}
|
|
for i, _nc := 0, int(args.NamedChildCount()); i < _nc; i++ {
|
|
c := args.NamedChild(i)
|
|
if c == nil {
|
|
continue
|
|
}
|
|
switch c.Type() {
|
|
case "do_block", "keywords":
|
|
continue
|
|
}
|
|
return c
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// elixirStringLiteral unwraps a tree-sitter-elixir `string` node to
|
|
// the literal characters. Strips quotes when no `quoted_content`
|
|
// child is present.
|
|
func elixirStringLiteral(node *sitter.Node, src []byte) string {
|
|
for i, _nc := 0, int(node.NamedChildCount()); i < _nc; i++ {
|
|
c := node.NamedChild(i)
|
|
if c != nil && c.Type() == "quoted_content" {
|
|
return c.Content(src)
|
|
}
|
|
}
|
|
raw := node.Content(src)
|
|
return strings.Trim(raw, "\"'")
|
|
}
|