chore: import upstream snapshot with attribution
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

This commit is contained in:
wehub-resource-sync
2026-07-13 12:33:42 +08:00
commit a06f331eb8
3186 changed files with 689843 additions and 0 deletions
+364
View File
@@ -0,0 +1,364 @@
package forest
import (
"fmt"
"sort"
"testing"
"unsafe"
adaforest "github.com/alexaandru/go-sitter-forest/ada"
apexforest "github.com/alexaandru/go-sitter-forest/apex"
bladeforest "github.com/alexaandru/go-sitter-forest/blade"
clojureforest "github.com/alexaandru/go-sitter-forest/clojure"
cmakeforest "github.com/alexaandru/go-sitter-forest/cmake"
cobolforest "github.com/alexaandru/go-sitter-forest/cobol"
crystalforest "github.com/alexaandru/go-sitter-forest/crystal"
dforest "github.com/alexaandru/go-sitter-forest/d"
elispforest "github.com/alexaandru/go-sitter-forest/elisp"
erlangforest "github.com/alexaandru/go-sitter-forest/erlang"
fortranforest "github.com/alexaandru/go-sitter-forest/fortran"
fsharpforest "github.com/alexaandru/go-sitter-forest/fsharp"
gdscriptforest "github.com/alexaandru/go-sitter-forest/gdscript"
groovyforest "github.com/alexaandru/go-sitter-forest/groovy"
hareforest "github.com/alexaandru/go-sitter-forest/hare"
haskellforest "github.com/alexaandru/go-sitter-forest/haskell"
jinjaforest "github.com/alexaandru/go-sitter-forest/jinja"
juliaforest "github.com/alexaandru/go-sitter-forest/julia"
liquidforest "github.com/alexaandru/go-sitter-forest/liquid"
matlabforest "github.com/alexaandru/go-sitter-forest/matlab"
moveforest "github.com/alexaandru/go-sitter-forest/move"
nimforest "github.com/alexaandru/go-sitter-forest/nim"
nixforest "github.com/alexaandru/go-sitter-forest/nix"
alforest "github.com/alexaandru/go-sitter-forest/al"
objcforest "github.com/alexaandru/go-sitter-forest/objc"
odinforest "github.com/alexaandru/go-sitter-forest/odin"
pascalforest "github.com/alexaandru/go-sitter-forest/pascal"
perlforest "github.com/alexaandru/go-sitter-forest/perl"
powershellforest "github.com/alexaandru/go-sitter-forest/powershell"
pugforest "github.com/alexaandru/go-sitter-forest/pug"
racketforest "github.com/alexaandru/go-sitter-forest/racket"
rescriptforest "github.com/alexaandru/go-sitter-forest/rescript"
solidityforest "github.com/alexaandru/go-sitter-forest/solidity"
tactforest "github.com/alexaandru/go-sitter-forest/tact"
tclforest "github.com/alexaandru/go-sitter-forest/tcl"
twigforest "github.com/alexaandru/go-sitter-forest/twig"
valaforest "github.com/alexaandru/go-sitter-forest/vala"
vimforest "github.com/alexaandru/go-sitter-forest/vim"
zigforest "github.com/alexaandru/go-sitter-forest/zig"
"github.com/zzet/gortex/internal/parser"
sitter "github.com/zzet/gortex/internal/parser/tsitter"
)
// TestDumpGrammarKinds is a research helper. It parses a small
// fixture per grammar and prints every named node kind seen, with
// frequencies — feeds the per-language node-kind dispatch table.
// Run with: go test -run TestDumpGrammarKinds -v ./internal/parser/forest/
func TestDumpGrammarKinds(t *testing.T) {
cases := []struct {
name string
get func() unsafe.Pointer
src string
}{
{"erlang", erlangforest.GetLanguage, `-module(m).
-export([add/2]).
add(A, B) -> A + B.
multiply(A, B) -> A * B.
`},
{"haskell", haskellforest.GetLanguage, `module M where
import Data.List
data Color = Red | Green
type Name = String
type Point = (Int, Int)
newtype Wrap a = Wrap a
class Show a where
show :: a -> String
instance Show Color where
show Red = "red"
add :: Int -> Int -> Int
add x y = x + y
`},
{"crystal", crystalforest.GetLanguage, `module Greeter
class Hello
def say
"hi"
end
end
struct Point
end
end
`},
{"nim", nimforest.GetLanguage, `import strutils
import sequtils, tables
type
Point* = object
x*, y*: int
Shape = enum
Circle, Square
proc distance*(a, b: Point): float =
return 0.0
func double(n: int): int =
n * 2
`},
{"ada", adaforest.GetLanguage, `with Ada.Text_IO; use Ada.Text_IO;
package body M is
procedure Hello is begin Put_Line ("hi"); end Hello;
function Add (X, Y : Integer) return Integer is begin return X + Y; end Add;
end M;
`},
{"fortran", fortranforest.GetLanguage, `module greetings
contains
subroutine hello
print *, 'hi'
end subroutine hello
function add(a, b)
integer :: a, b, add
add = a + b
end function add
end module greetings
`},
{"vim", vimforest.GetLanguage, `function! Hello() abort
echo 'hi'
endfunction
function! s:add(a, b)
return a:a + a:b
endfunction
`},
{"tcl", tclforest.GetLanguage, `proc hello {} { puts hi }
proc add {a b} { return [expr {$a + $b}] }
package require Tcl 8.6
`},
{"perl", perlforest.GetLanguage, `package M;
use strict;
sub hello { print "hi\n" }
sub add { my ($a,$b) = @_; return $a+$b; }
1;
`},
{"powershell", powershellforest.GetLanguage, `function Hello { Write-Host "hi" }
function Add($a, $b) { return $a + $b }
class Greeter { [string]$Name }
`},
{"pascal", pascalforest.GetLanguage, `program M;
type Point = record x, y: integer; end;
procedure Hello; begin writeln('hi'); end;
function Add(a, b: integer): integer; begin Add := a + b; end;
begin Hello; end.
`},
{"odin", odinforest.GetLanguage, `package m
import "core:fmt"
Point :: struct { x, y: int }
hello :: proc() { fmt.println("hi") }
add :: proc(a, b: int) -> int { return a + b }
`},
{"hare", hareforest.GetLanguage, `use io;
type point = struct { x: int, y: int };
fn add(a: int, b: int) int = a + b;
export fn hello() void = io::println("hi");
`},
{"zig", zigforest.GetLanguage, `const std = @import("std");
const Point = struct { x: i32, y: i32 };
fn add(a: i32, b: i32) i32 { return a + b; }
pub fn hello() void {}
`},
{"d", dforest.GetLanguage, `module m;
import std.stdio;
struct Point { int x, y; }
class Greeter { void hello() {} }
int add(int a, int b) { return a + b; }
`},
{"vala", valaforest.GetLanguage, `using GLib;
namespace M { class Greeter : Object { public void hello() {} } }
`},
{"groovy", groovyforest.GetLanguage, `package m
class Greeter { String hello() { return 'hi' } }
def add(a, b) { return a + b }
`},
{"clojure", clojureforest.GetLanguage, `(ns m (:require [clojure.string :as s]))
(defn hello [] (println "hi"))
(defn add [a b] (+ a b))
(defrecord Point [x y])
`},
{"cmake", cmakeforest.GetLanguage, `cmake_minimum_required(VERSION 3.10)
function(hello)
message("hi")
endfunction()
macro(double x)
set(${x}_2 ${${x}}*2)
endmacro()
`},
{"cobol", cobolforest.GetLanguage, `IDENTIFICATION DIVISION.
PROGRAM-ID. M.
PROCEDURE DIVISION.
DISPLAY 'hi'.
STOP RUN.
`},
{"julia", juliaforest.GetLanguage, `module M
using LinearAlgebra
struct Point x::Int; y::Int end
function hello() println("hi") end
add(a, b) = a + b
end
`},
{"matlab", matlabforest.GetLanguage, `function out = add(a, b)
out = a + b;
end
classdef Greeter
methods
function hello(obj); disp('hi'); end
end
end
`},
{"apex", apexforest.GetLanguage, `public class Greeter {
public String hello() { return 'hi'; }
public Integer add(Integer a, Integer b) { return a + b; }
}
trigger AccountTrigger on Account (before insert) {}
`},
{"solidity", solidityforest.GetLanguage, `pragma solidity ^0.8.0;
import "./X.sol";
interface IFoo { function bar() external; }
contract Token { function mint(address to, uint256 amt) public {} event Tx(); modifier ok() { _; } struct Holder {} enum State {} }
`},
{"tact", tactforest.GetLanguage, `import "./other";
trait T { fun base() {} }
contract Greeter with T {
init() { }
fun hello(): String { return "hi"; }
receive("ping") {}
}
`},
{"move", moveforest.GetLanguage, `module 0x1::M {
use std::vector;
struct Counter has key { value: u64 }
public fun increment(c: &mut Counter) { c.value = c.value + 1; }
}
`},
{"racket", racketforest.GetLanguage, `#lang racket
(require racket/string)
(define (hello) (displayln "hi"))
(define (add a b) (+ a b))
(struct point (x y))
`},
{"elisp", elispforest.GetLanguage, `(require 'cl-lib)
(defun hello () "hi")
(defun add (a b) (+ a b))
(defvar greeting "hi")
(defmacro when-bind (b &rest body) (list 'let b body))
`},
{"fsharp", fsharpforest.GetLanguage, `module M
open System
type Point = { x: int; y: int }
let hello () = printfn "hi"
let add a b = a + b
`},
{"gdscript", gdscriptforest.GetLanguage, `extends Node
class_name Greeter
signal greeted(name)
func hello():
print("hi")
func add(a: int, b: int) -> int:
return a + b
`},
{"jinja", jinjaforest.GetLanguage, `{% extends 'base.html' %}
{% block content %}
{% macro greet(name) %}Hi, {{ name }}{% endmacro %}
{{ greet('world') }}
{% endblock %}
`},
{"liquid", liquidforest.GetLanguage, `{% include 'header' %}
{% capture greeting %}Hi{% endcapture %}
{% assign name = "world" %}
{{ greeting }}, {{ name }}
`},
{"twig", twigforest.GetLanguage, `{% extends 'base.twig' %}
{% block content %}
{% macro greet(name) %}Hi, {{ name }}{% endmacro %}
{% endblock %}
`},
{"pug", pugforest.GetLanguage, `extends layout
block content
mixin greet(name)
p Hi #{name}
+greet('world')
`},
{"blade", bladeforest.GetLanguage, `@extends('layouts.app')
@section('content')
@include('partials.header')
<p>Hi {{ $name }}</p>
@endsection
`},
{"rescript", rescriptforest.GetLanguage, `module M = {
type point = { x: int, y: int }
let hello = () => Js.log("hi")
let add = (a, b) => a + b
}
`},
{"nix", nixforest.GetLanguage, `{ lib, ... }:
let
greet = name: "Hi, ${name}";
add = a: b: a + b;
in {
inherit greet add;
}
`},
{"objc", objcforest.GetLanguage, `#import <Foundation/Foundation.h>
@interface Greeter : NSObject
- (NSString *)hello;
@end
@implementation Greeter
- (NSString *)hello { return @"hi"; }
@end
`},
{"al", alforest.GetLanguage, `table 50000 Customer
{
fields { field(1; Name; Text[100]) {} }
}
codeunit 50001 CustomerMgt
{
procedure Hello() begin Message('hi'); end;
}
page 50002 CustList { SourceTable = Customer; }
`},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
lang := sitter.NewLanguage(c.get())
tree, err := parser.ParseFile([]byte(c.src), lang)
if err != nil {
t.Fatalf("parse: %v", err)
}
defer tree.Close()
kinds := make(map[string]int)
walkKinds(tree.RootNode(), kinds)
keys := make([]string, 0, len(kinds))
for k := range kinds {
keys = append(keys, k)
}
sort.Strings(keys)
fmt.Printf("=== %s ===\n", c.name)
for _, k := range keys {
fmt.Printf(" %-40s × %d\n", k, kinds[k])
}
})
}
}
func walkKinds(n *sitter.Node, kinds map[string]int) {
if n == nil {
return
}
if n.IsNamed() {
kinds[n.Type()]++
}
for i := 0; i < int(n.NamedChildCount()); i++ {
walkKinds(n.NamedChild(i), kinds)
}
}
+7
View File
@@ -0,0 +1,7 @@
package forest
import "errors"
// errNilLanguage is returned when a grammar's GetLanguage() returns
// nil. Sticky: cached on the Extractor, never retried.
var errNilLanguage = errors.New("forest: grammar returned nil language pointer")
+134
View File
@@ -0,0 +1,134 @@
// Package forest is the long-tail language adapter. It wraps any
// alexaandru/go-sitter-forest grammar (510+ supported) as a Gortex
// Extractor with signature-only depth: function / method / type /
// interface / variable / constant nodes plus EdgeDefines from the
// file. Calls and imports are best-effort via @reference.call captures
// when the grammar ships a tags.scm.
//
// Top-tier languages (Go, TS, Python, Rust, …) keep their bespoke
// extractors in internal/parser/languages — those have hand-tuned
// queries that emit Gortex-specific edges (ORM, contracts, dataflow)
// the generic walker can't produce. forest is for everything else.
package forest
import (
"sync"
"unsafe"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser"
sitter "github.com/zzet/gortex/internal/parser/tsitter"
)
// GetLanguageFn returns the raw C *TSLanguage pointer. Every forest
// language module exposes one as `forestpkg.GetLanguage`.
type GetLanguageFn func() unsafe.Pointer
// GetQueryFn returns the bytes of a named .scm query bundled with the
// grammar. Forest grammars ship a subset of {tags, highlights, locals,
// folds, indents, injections}; we only read tags.scm. The opts
// argument is forest's preference flag (NvimFirst / NativeFirst /
// NvimOnly / NativeOnly). nil is acceptable when the grammar has no
// queries.
type GetQueryFn func(kind string, opts ...byte) []byte
// Extractor is a generic forest-backed signature-only extractor.
// Construct one per language via New() and register it with the
// parser registry exactly like any other Extractor.
type Extractor struct {
language string
extensions []string
getLang GetLanguageFn
getQuery GetQueryFn
once sync.Once
initErr error
lang *sitter.Language
tagsQ *parser.PreparedQuery // nil when grammar ships no tags.scm
}
// New builds a forest-backed Extractor. getQuery may be nil for
// grammars that do not expose any .scm queries — extraction then
// falls back to the generic node-kind walker.
func New(language string, extensions []string, getLang GetLanguageFn, getQuery GetQueryFn) *Extractor {
return &Extractor{
language: language,
extensions: extensions,
getLang: getLang,
getQuery: getQuery,
}
}
func (e *Extractor) Language() string { return e.language }
func (e *Extractor) Extensions() []string { return e.extensions }
// init compiles the language pointer + tags.scm query exactly once.
// Errors are sticky: once an init has failed, every subsequent
// Extract returns the same error rather than retrying CGO setup on
// every file (which would slow indexing to a crawl on a broken
// grammar).
func (e *Extractor) init() error {
e.once.Do(func() {
ptr := e.getLang()
if ptr == nil {
e.initErr = errNilLanguage
return
}
e.lang = sitter.NewLanguage(ptr)
if e.getQuery != nil {
pattern := e.getQuery("tags")
if len(pattern) > 0 {
q, err := parser.NewPreparedQuery(string(pattern), e.lang)
if err == nil {
e.tagsQ = q
}
// Compile failure is non-fatal: drop to walker.
}
}
})
return e.initErr
}
// Extract parses src with the bundled grammar and emits one file
// node, plus one node per detected definition (function / method /
// type / interface / variable / constant / field / module). Edges:
// EdgeDefines from the file to every definition; EdgeCalls
// (unresolved::name) for @reference.call captures when tags.scm is
// present.
func (e *Extractor) Extract(filePath string, src []byte) (*parser.ExtractionResult, error) {
if err := e.init(); err != nil {
return nil, err
}
tree, err := parser.ParseFile(src, e.lang)
if err != nil {
return nil, err
}
defer tree.Close()
root := tree.RootNode()
result := &parser.ExtractionResult{}
fileNode := &graph.Node{
ID: filePath,
Kind: graph.KindFile,
Name: filePath,
FilePath: filePath,
StartLine: 1,
EndLine: int(root.EndPoint().Row) + 1,
Language: e.language,
}
result.Nodes = append(result.Nodes, fileNode)
if e.tagsQ != nil {
e.extractByTags(root, src, filePath, fileNode, result)
return result, nil
}
e.extractByWalker(root, src, filePath, fileNode, result)
return result, nil
}
var _ parser.Extractor = (*Extractor)(nil)
+163
View File
@@ -0,0 +1,163 @@
package forest
import (
"testing"
"unsafe"
"github.com/alexaandru/go-sitter-forest/elm"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zzet/gortex/internal/graph"
)
func TestExtractor_TagsPath(t *testing.T) {
// Elm ships a tags.scm — exercises the tags-driven path.
e := New("elm", []string{".elm"}, elm.GetLanguage, elm.GetQuery)
require.Equal(t, "elm", e.Language())
require.Equal(t, []string{".elm"}, e.Extensions())
src := []byte(`module M exposing (..)
type Color = Red | Green | Blue
double : Int -> Int
double n = n * 2
`)
res, err := e.Extract("M.elm", src)
require.NoError(t, err)
require.NotEmpty(t, res.Nodes)
gotFile := false
gotDouble := false
gotColor := false
for _, n := range res.Nodes {
switch n.Kind {
case graph.KindFile:
gotFile = n.FilePath == "M.elm"
case graph.KindFunction:
if n.Name == "double" {
gotDouble = true
}
case graph.KindType:
if n.Name == "Color" {
gotColor = true
}
}
}
assert.True(t, gotFile, "missing file node")
assert.True(t, gotDouble, "missing `double` function")
assert.True(t, gotColor, "missing `Color` type")
}
func TestExtractor_ParseError(t *testing.T) {
// Garbage input still returns a tree (forest grammars produce a
// best-effort tree with ERROR nodes), so Extract should succeed
// with at least the file node and not panic.
e := New("elm", []string{".elm"}, elm.GetLanguage, elm.GetQuery)
res, err := e.Extract("bad.elm", []byte("@@@@@@@@@"))
require.NoError(t, err)
require.NotEmpty(t, res.Nodes)
assert.Equal(t, graph.KindFile, res.Nodes[0].Kind)
}
func TestExtractor_CallEdgesParentToEnclosingFunc(t *testing.T) {
// Two functions; calls inside `caller`'s body should land on
// `M.elm::caller`, not on the file. Confirms the second-pass
// attribution from buildFuncRanges/findEnclosingFunc.
//
// Note: elm's tags.scm also captures type-annotation references
// (`caller : Int -> Int`) as @reference.function — those land
// outside any function range and parent to the file, which is
// the documented module-level fallback. We only assert the
// in-body case here.
e := New("elm", []string{".elm"}, elm.GetLanguage, elm.GetQuery)
src := []byte(`module M exposing (..)
helper : Int -> Int
helper n = n + 1
caller : Int -> Int
caller x =
helper (helper x)
`)
res, err := e.Extract("M.elm", src)
require.NoError(t, err)
bodyCallToHelper := false
for _, ed := range res.Edges {
if ed.Kind != graph.EdgeCalls {
continue
}
if ed.To == "unresolved::helper" && ed.From == "M.elm::caller" {
bodyCallToHelper = true
break
}
}
assert.True(t, bodyCallToHelper,
"expected a call edge from M.elm::caller to unresolved::helper; got %+v",
callEdgeSummary(res.Edges))
}
// callEdgeSummary builds a compact list of (from, to) pairs for
// every EdgeCalls edge — only used when an assertion fails to give
// the failure message something diagnostic to chew on.
func callEdgeSummary(edges []*graph.Edge) []string {
var out []string
for _, ed := range edges {
if ed.Kind == graph.EdgeCalls {
out = append(out, ed.From+" -> "+ed.To)
}
}
return out
}
func TestExtractor_ModuleLevelCallsParentToFile(t *testing.T) {
// A call at module top-level (no enclosing function) should
// fall back to the file node — better than dropping the edge.
e := New("elm", []string{".elm"}, elm.GetLanguage, elm.GetQuery)
src := []byte(`module M exposing (..)
import X
result =
foo bar
`)
res, err := e.Extract("M.elm", src)
require.NoError(t, err)
// `result = foo bar` is a value declaration, treated as a function
// in elm tags.scm — so its calls would parent to it. We pick a
// file-level expression test that's harder: the assignment itself
// is the def, but if any @reference.call is captured outside any
// function, it should land on the file.
for _, ed := range res.Edges {
if ed.Kind == graph.EdgeCalls {
assert.True(t,
ed.From == "M.elm" || ed.From == "M.elm::result",
"unexpected call parent %q", ed.From)
}
}
}
func TestExtractor_NilGetLanguage(t *testing.T) {
// Sticky-error path: a grammar that returns a nil pointer fails
// the first call and every subsequent call (cached error)
// without panicking.
e := New("broken", []string{".x"}, func() unsafe.Pointer { return nil }, nil)
_, err := e.Extract("a.x", []byte("anything"))
require.Error(t, err)
require.ErrorIs(t, err, errNilLanguage)
// Sticky cache — second call returns same error without
// re-invoking the grammar.
_, err2 := e.Extract("b.x", []byte("again"))
require.ErrorIs(t, err2, errNilLanguage)
}
+59
View File
@@ -0,0 +1,59 @@
package forest
import (
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser"
)
// funcRange is one definition's line span, used to attribute call
// references to their enclosing function.
type funcRange struct {
id string
startLine int
endLine int
}
// buildFuncRanges walks the already-emitted nodes and returns a
// flat slice of every function/method's span. Linear scan is fine —
// even large files emit only hundreds of definitions, and the
// per-call lookup walks this slice in O(N).
func buildFuncRanges(result *parser.ExtractionResult) []funcRange {
if result == nil {
return nil
}
var ranges []funcRange
for _, n := range result.Nodes {
if n == nil {
continue
}
// Forest defs that can host a call: anything code-bearing.
switch n.Kind {
case graph.KindFunction, graph.KindMethod:
ranges = append(ranges, funcRange{
id: n.ID, startLine: n.StartLine, endLine: n.EndLine,
})
}
}
return ranges
}
// findEnclosingFunc returns the most-tightly-enclosing function ID
// for a given 1-based line, or "" if no def covers the line. When
// definitions nest (e.g. a closure inside a function), the
// inner-most range wins because we prefer the smallest covering
// span.
func findEnclosingFunc(ranges []funcRange, line int) string {
bestID := ""
bestSpan := 0
for _, r := range ranges {
if line < r.startLine || line > r.endLine {
continue
}
span := r.endLine - r.startLine
if bestID == "" || span < bestSpan {
bestID = r.id
bestSpan = span
}
}
return bestID
}
+165
View File
@@ -0,0 +1,165 @@
package forest
import (
"strings"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser"
sitter "github.com/zzet/gortex/internal/parser/tsitter"
)
// extractByTags runs the grammar's tags.scm and translates
// nvim-treesitter capture conventions into Gortex graph nodes/edges.
//
// Capture conventions (from
// https://docs.rs/tree-sitter-tags / nvim-treesitter):
//
// @definition.{function,method,class,interface,struct,union,enum,
// module,constant,variable,field,macro,type,parameter}
// @reference.{call,implementation,type}
// @name — the name node bound inside a @definition.* match
//
// Two passes: emit every definition first, then attribute each
// @reference.call to its enclosing function (or fall back to the
// file node when the call is module-level). Calling buildFuncRanges
// after the definitions are committed lets the second pass run a
// simple span-containment lookup without re-parsing.
func (e *Extractor) extractByTags(
root *sitter.Node, src []byte, filePath string, fileNode *graph.Node, result *parser.ExtractionResult,
) {
seen := make(map[string]bool)
type pendingCall struct {
name string
line int
}
var calls []pendingCall
parser.EachMatch(e.tagsQ, root, src, func(qr parser.QueryResult) {
var (
defKind graph.NodeKind
defNode *parser.CapturedNode
isRefCall bool
refLine int
)
nameCap := qr.Captures["name"]
for capName, captured := range qr.Captures {
switch {
case strings.HasPrefix(capName, "definition."):
if defNode == nil {
defKind = mapDefinitionKind(capName)
defNode = captured
}
case capName == "reference.call", capName == "reference.function":
// Two captures cover the same intent across grammars:
// nvim-treesitter convention is `@reference.call`,
// but Elm and a few others use `@reference.function`
// for call-site name references. Treat both as
// calls; the actual identifier comes from @name.
isRefCall = true
refLine = captured.StartLine + 1
}
}
if defNode != nil && defKind != "" && nameCap != nil {
emitDefinition(filePath, fileNode, e.language, defKind, nameCap, defNode, seen, result)
return
}
if isRefCall && nameCap != nil {
calls = append(calls, pendingCall{name: strings.TrimSpace(nameCap.Text), line: refLine})
}
})
if len(calls) == 0 {
return
}
ranges := buildFuncRanges(result)
for _, c := range calls {
callerID := findEnclosingFunc(ranges, c.line)
if callerID == "" {
callerID = fileNode.ID
}
// Skip self-edges (e.g. recursive function calling itself).
if strings.HasSuffix(callerID, "::"+c.name) {
continue
}
result.Edges = append(result.Edges, &graph.Edge{
From: callerID, To: "unresolved::" + c.name,
Kind: graph.EdgeCalls, FilePath: filePath, Line: c.line,
})
}
}
// mapDefinitionKind maps an nvim-treesitter @definition.* capture
// name to a Gortex graph.NodeKind. Unknown buckets fall back to
// KindFunction (the most common signature shape).
func mapDefinitionKind(capName string) graph.NodeKind {
switch strings.TrimPrefix(capName, "definition.") {
case "function", "macro":
return graph.KindFunction
case "method":
return graph.KindMethod
case "class", "struct", "union", "enum", "type":
return graph.KindType
case "interface", "trait":
return graph.KindInterface
case "constant":
return graph.KindConstant
case "variable":
return graph.KindVariable
case "field":
return graph.KindField
case "module", "namespace":
return graph.KindPackage
case "parameter":
// Skip parameters at signature-only depth — the indexer
// would otherwise flood the graph with one node per arg.
return ""
default:
return graph.KindFunction
}
}
// emitDefinition is shared by tags.scm and walker paths. Skips empty
// names, deduplicates by ID, and emits an EdgeDefines from the file.
func emitDefinition(
filePath string,
fileNode *graph.Node,
language string,
kind graph.NodeKind,
nameCap *parser.CapturedNode,
bodyCap *parser.CapturedNode,
seen map[string]bool,
result *parser.ExtractionResult,
) {
name := strings.TrimSpace(nameCap.Text)
if name == "" || kind == "" {
return
}
id := filePath + "::" + name
if seen[id] {
return
}
seen[id] = true
startLine := bodyCap.StartLine + 1
endLine := bodyCap.EndLine + 1
if startLine == 0 {
startLine = nameCap.StartLine + 1
}
if endLine == 0 {
endLine = nameCap.EndLine + 1
}
result.Nodes = append(result.Nodes, &graph.Node{
ID: id, Kind: kind, Name: name,
FilePath: filePath, StartLine: startLine, EndLine: endLine,
Language: language,
})
result.Edges = append(result.Edges, &graph.Edge{
From: fileNode.ID, To: id, Kind: graph.EdgeDefines,
FilePath: filePath, Line: startLine,
})
}
+317
View File
@@ -0,0 +1,317 @@
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"
}