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
318 lines
11 KiB
Go
318 lines
11 KiB
Go
package forest
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
"github.com/zzet/gortex/internal/parser"
|
|
sitter "github.com/zzet/gortex/internal/parser/tsitter"
|
|
)
|
|
|
|
// extractByWalker is the fallback for grammars that do not ship a
|
|
// tags.scm. It walks every named node in the parse tree and matches
|
|
// kind names against a small set of suffix/prefix heuristics that
|
|
// catch the conventional tree-sitter naming pattern
|
|
// `<thing>_definition` / `<thing>_declaration` / `<thing>_specifier`.
|
|
//
|
|
// This is naive on purpose. For the long tail (~440 grammars without
|
|
// tags.scm) it produces good-enough signature-only extraction without
|
|
// hand-tuning queries per language. Languages where the heuristic
|
|
// underfits get a tags.scm contribution upstream or a bespoke
|
|
// extractor in internal/parser/languages.
|
|
func (e *Extractor) extractByWalker(
|
|
root *sitter.Node, src []byte, filePath string, fileNode *graph.Node, result *parser.ExtractionResult,
|
|
) {
|
|
seen := make(map[string]bool)
|
|
|
|
var walk func(n *sitter.Node)
|
|
walk = func(n *sitter.Node) {
|
|
if n == nil {
|
|
return
|
|
}
|
|
if kind := classifyKind(e.language, n.Type()); kind != "" {
|
|
if name := nodeName(n, src); name != "" {
|
|
e.emitWalkerNode(filePath, fileNode, kind, name, n, seen, result)
|
|
}
|
|
}
|
|
for i := 0; i < int(n.NamedChildCount()); i++ {
|
|
walk(n.NamedChild(i))
|
|
}
|
|
}
|
|
walk(root)
|
|
}
|
|
|
|
// emitWalkerNode is the walker's adaptation of emitDefinition — it
|
|
// builds the same shape but takes raw sitter.Node positions rather
|
|
// than CapturedNode.
|
|
func (e *Extractor) emitWalkerNode(
|
|
filePath string, fileNode *graph.Node, kind graph.NodeKind, name string,
|
|
n *sitter.Node, seen map[string]bool, result *parser.ExtractionResult,
|
|
) {
|
|
id := filePath + "::" + name
|
|
if seen[id] {
|
|
return
|
|
}
|
|
seen[id] = true
|
|
|
|
startLine := int(n.StartPoint().Row) + 1
|
|
endLine := int(n.EndPoint().Row) + 1
|
|
|
|
result.Nodes = append(result.Nodes, &graph.Node{
|
|
ID: id, Kind: kind, Name: name,
|
|
FilePath: filePath, StartLine: startLine, EndLine: endLine,
|
|
Language: e.language,
|
|
})
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: fileNode.ID, To: id, Kind: graph.EdgeDefines,
|
|
FilePath: filePath, Line: startLine,
|
|
})
|
|
}
|
|
|
|
// classifyKind maps a tree-sitter node kind name to a graph.NodeKind.
|
|
// The dispatch is two-tier:
|
|
//
|
|
// 1. Per-language overrides — `languageKindMap[language][nodeKind]`
|
|
// handles grammars whose rule names don't match the conventional
|
|
// `*_definition` / `*_declaration` suffixes (Erlang's
|
|
// `fun_decl` / `function_clause`, Haskell's `function` /
|
|
// `signature`, Crystal's `class_def` / `method_def`, etc.).
|
|
// Researched once per grammar via the dump_kinds_test helper.
|
|
// 2. Generic suffix matching — covers the long tail of grammars that
|
|
// follow the standard `*_definition` / `*_declaration` /
|
|
// `*_specifier` convention.
|
|
//
|
|
// Order matters within suffix matching: longer / more specific
|
|
// patterns checked first ("function_declaration" beats "_declaration").
|
|
func classifyKind(language, t string) graph.NodeKind {
|
|
if t == "" {
|
|
return ""
|
|
}
|
|
if perLang, ok := languageKindMap[language]; ok {
|
|
if k, ok := perLang[t]; ok {
|
|
return k
|
|
}
|
|
}
|
|
|
|
// Methods first — `method_*` is more specific than `function_*`,
|
|
// and a method declaration shouldn't fall through to function.
|
|
switch {
|
|
case hasAnySuffix(t, "method_definition", "method_declaration", "method_signature", "method_spec"):
|
|
return graph.KindMethod
|
|
case hasAnySuffix(t, "function_definition", "function_declaration", "function_signature", "function_spec", "function_item"):
|
|
return graph.KindFunction
|
|
case hasAnySuffix(t, "class_definition", "class_declaration", "class_specifier"):
|
|
return graph.KindType
|
|
case hasAnySuffix(t, "interface_definition", "interface_declaration"):
|
|
return graph.KindInterface
|
|
case hasAnySuffix(t, "trait_definition", "trait_declaration"):
|
|
return graph.KindInterface
|
|
case hasAnySuffix(t, "struct_definition", "struct_declaration", "struct_specifier", "struct_item"):
|
|
return graph.KindType
|
|
case hasAnySuffix(t, "enum_definition", "enum_declaration", "enum_specifier", "enum_item"):
|
|
return graph.KindType
|
|
case hasAnySuffix(t, "union_definition", "union_declaration", "union_specifier"):
|
|
return graph.KindType
|
|
case hasAnySuffix(t, "type_definition", "type_declaration", "type_alias_declaration", "type_alias", "type_item"):
|
|
return graph.KindType
|
|
case hasAnySuffix(t, "module_definition", "module_declaration", "namespace_definition", "namespace_declaration"):
|
|
return graph.KindPackage
|
|
case hasAnySuffix(t, "constant_declaration", "const_declaration", "const_item"):
|
|
return graph.KindConstant
|
|
case hasAnySuffix(t, "variable_declaration", "var_declaration"):
|
|
return graph.KindVariable
|
|
case hasAnySuffix(t, "field_declaration", "field_definition"):
|
|
return graph.KindField
|
|
case hasAnySuffix(t, "macro_definition", "macro_declaration"):
|
|
return graph.KindFunction
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// languageKindMap holds per-language node-kind → graph.NodeKind
|
|
// overrides. Add a row when a grammar's rule names diverge from the
|
|
// conventional `*_definition` / `*_declaration` patterns and the
|
|
// generic walker emits zero definitions on real source. Run the
|
|
// dump_kinds_test helper for that language to find the right names.
|
|
var languageKindMap = map[string]map[string]graph.NodeKind{
|
|
"erlang": {
|
|
"fun_decl": graph.KindFunction,
|
|
// `-module(name)` is a `module_attribute` and the name lives
|
|
// inside an `atom` child the generic nodeName helper doesn't
|
|
// recognise — leave it to the regex idiom layer in
|
|
// erlang.go.
|
|
},
|
|
"haskell": {
|
|
"function": graph.KindFunction,
|
|
"signature": graph.KindFunction,
|
|
"data_type": graph.KindType,
|
|
"newtype": graph.KindType,
|
|
"type_synonym": graph.KindType,
|
|
// Upstream tree-sitter-haskell ships the rule name as
|
|
// `type_synomym` — typo and all. Match both spellings so
|
|
// we don't depend on the grammar fixing it.
|
|
"type_synomym": graph.KindType,
|
|
"class": graph.KindInterface,
|
|
"instance": graph.KindType,
|
|
},
|
|
"crystal": {
|
|
"class_def": graph.KindType,
|
|
"module_def": graph.KindType,
|
|
"struct_def": graph.KindType,
|
|
"method_def": graph.KindMethod,
|
|
},
|
|
"nim": {
|
|
"proc_declaration": graph.KindFunction,
|
|
"func_declaration": graph.KindFunction,
|
|
"type_declaration": graph.KindType,
|
|
// object_declaration / enum_declaration nest inside
|
|
// type_declaration; emit on the outer wrapper to avoid
|
|
// duplicate nodes.
|
|
},
|
|
"ada": {
|
|
"function_specification": graph.KindFunction,
|
|
"procedure_specification": graph.KindFunction,
|
|
},
|
|
"fortran": {
|
|
"function": graph.KindFunction,
|
|
"function_statement": graph.KindFunction,
|
|
"module": graph.KindPackage,
|
|
"module_statement": graph.KindPackage,
|
|
},
|
|
"perl": {
|
|
"function": graph.KindFunction,
|
|
"subroutine_declaration_statement": graph.KindFunction,
|
|
},
|
|
"powershell": {
|
|
"function_statement": graph.KindFunction,
|
|
"class_statement": graph.KindType,
|
|
},
|
|
"odin": {
|
|
"procedure_declaration": graph.KindFunction,
|
|
"struct_declaration": graph.KindType,
|
|
"package_declaration": graph.KindPackage,
|
|
},
|
|
"cmake": {
|
|
"function_def": graph.KindFunction,
|
|
"macro_def": graph.KindFunction,
|
|
},
|
|
"apex": {
|
|
"class_declaration": graph.KindType,
|
|
"method_declaration": graph.KindMethod,
|
|
"trigger_declaration": graph.KindFunction,
|
|
},
|
|
"solidity": {
|
|
"contract_declaration": graph.KindType,
|
|
"interface_declaration": graph.KindInterface,
|
|
"modifier_definition": graph.KindFunction,
|
|
"event_definition": graph.KindFunction,
|
|
"enum_declaration": graph.KindType,
|
|
"struct_declaration": graph.KindType,
|
|
// function_definition already covered by the generic
|
|
// `*_definition` suffix in classifyKind.
|
|
},
|
|
"tact": {
|
|
"trait": graph.KindInterface,
|
|
"contract": graph.KindType,
|
|
"init_function": graph.KindFunction,
|
|
"receive_function": graph.KindFunction,
|
|
"storage_function": graph.KindFunction,
|
|
},
|
|
"fsharp": {
|
|
"function_or_value_defn": graph.KindFunction,
|
|
"named_module": graph.KindPackage,
|
|
"record_type_defn": graph.KindType,
|
|
// type_definition handled by generic suffix.
|
|
},
|
|
"gdscript": {
|
|
"class_name_statement": graph.KindType,
|
|
},
|
|
"jinja": {
|
|
"macro_statement": graph.KindFunction,
|
|
},
|
|
"twig": {
|
|
"macro_statement": graph.KindFunction,
|
|
},
|
|
"rescript": {
|
|
"let_declaration": graph.KindFunction,
|
|
"module_declaration": graph.KindPackage,
|
|
"type_declaration": graph.KindType,
|
|
},
|
|
"objc": {
|
|
"class_interface": graph.KindType,
|
|
"class_implementation": graph.KindType,
|
|
"method_declaration": graph.KindMethod,
|
|
"method_definition": graph.KindMethod,
|
|
"implementation_definition": graph.KindFunction,
|
|
},
|
|
"al": {
|
|
"codeunit_declaration": graph.KindType,
|
|
"table_declaration": graph.KindType,
|
|
"page_declaration": graph.KindType,
|
|
"procedure": graph.KindMethod,
|
|
// AL's `procedure` node holds the name in an `identifier`
|
|
// child, but the test fixtures use `attributed_procedure`
|
|
// wrappers; both routes converge on the same identifier.
|
|
},
|
|
}
|
|
|
|
func hasAnySuffix(s string, suffixes ...string) bool {
|
|
for _, suf := range suffixes {
|
|
if s == suf || strings.HasSuffix(s, suf) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// nodeName tries the conventional `name:` field first, then falls
|
|
// back to the first identifier-like child within a depth-3 search.
|
|
// Returns "" if neither is present (anonymous functions / unnamed
|
|
// structs).
|
|
//
|
|
// Three levels of recursion catches the common "wrapper holds the
|
|
// name in a typed sub-node" pattern: Erlang `fun_decl ▶
|
|
// function_clause ▶ atom`, Nim `proc_declaration ▶
|
|
// symbol_declaration ▶ exported_symbol ▶ identifier`. Going
|
|
// deeper would risk returning a parameter name when the
|
|
// function-name capture is missing entirely.
|
|
//
|
|
// "Identifier-like" covers the conventional names plus a few
|
|
// language-specific tokens that grammars use for the same role:
|
|
// `constant` (Ruby/Crystal class names), `atom` (Erlang),
|
|
// `variable` (Haskell binding names), `lower_case_identifier`
|
|
// and `upper_case_identifier` (Elm).
|
|
func nodeName(n *sitter.Node, src []byte) string {
|
|
if name := n.ChildByFieldName("name"); name != nil {
|
|
return strings.TrimSpace(name.Content(src))
|
|
}
|
|
return findFirstNameIn(n, src, 3)
|
|
}
|
|
|
|
func findFirstNameIn(n *sitter.Node, src []byte, depth int) string {
|
|
if n == nil || depth < 0 {
|
|
return ""
|
|
}
|
|
for i := 0; i < int(n.NamedChildCount()); i++ {
|
|
c := n.NamedChild(i)
|
|
if c == nil {
|
|
continue
|
|
}
|
|
if isIdentifierKind(c.Type()) {
|
|
return strings.TrimSpace(c.Content(src))
|
|
}
|
|
if name := findFirstNameIn(c, src, depth-1); name != "" {
|
|
return name
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func isIdentifierKind(t string) bool {
|
|
return strings.Contains(t, "identifier") ||
|
|
t == "name" ||
|
|
t == "type_identifier" ||
|
|
t == "constant" ||
|
|
t == "variable" ||
|
|
t == "atom"
|
|
}
|