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
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:
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user