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
289 lines
7.6 KiB
Go
289 lines
7.6 KiB
Go
package languages
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
func TestRbExtractor_Class(t *testing.T) {
|
|
src := []byte(`class UserService
|
|
def initialize(db)
|
|
@db = db
|
|
end
|
|
|
|
def find_user(id)
|
|
@db.query(id)
|
|
end
|
|
end
|
|
`)
|
|
e := NewRubyExtractor()
|
|
result, err := e.Extract("service.rb", src)
|
|
require.NoError(t, err)
|
|
|
|
types := nodesOfKind(result.Nodes, graph.KindType)
|
|
require.Len(t, types, 1)
|
|
assert.Equal(t, "UserService", types[0].Name)
|
|
|
|
methods := nodesOfKind(result.Nodes, graph.KindMethod)
|
|
assert.Len(t, methods, 2)
|
|
|
|
memberEdges := edgesOfKind(result.Edges, graph.EdgeMemberOf)
|
|
assert.Len(t, memberEdges, 2)
|
|
}
|
|
|
|
func TestRbExtractor_TopLevelMethod(t *testing.T) {
|
|
src := []byte(`def greet(name)
|
|
puts "Hello #{name}"
|
|
end
|
|
`)
|
|
e := NewRubyExtractor()
|
|
result, err := e.Extract("app.rb", src)
|
|
require.NoError(t, err)
|
|
|
|
funcs := nodesOfKind(result.Nodes, graph.KindFunction)
|
|
require.Len(t, funcs, 1)
|
|
assert.Equal(t, "greet", funcs[0].Name)
|
|
}
|
|
|
|
func TestRbExtractor_Require(t *testing.T) {
|
|
src := []byte(`require "json"
|
|
require_relative "helper"
|
|
`)
|
|
e := NewRubyExtractor()
|
|
result, err := e.Extract("app.rb", src)
|
|
require.NoError(t, err)
|
|
|
|
imports := edgesOfKind(result.Edges, graph.EdgeImports)
|
|
assert.GreaterOrEqual(t, len(imports), 1)
|
|
}
|
|
|
|
func TestRbExtractor_Module(t *testing.T) {
|
|
src := []byte(`module Authentication
|
|
def self.verify(token)
|
|
true
|
|
end
|
|
end
|
|
`)
|
|
e := NewRubyExtractor()
|
|
result, err := e.Extract("auth.rb", src)
|
|
require.NoError(t, err)
|
|
|
|
pkgs := nodesOfKind(result.Nodes, graph.KindPackage)
|
|
require.Len(t, pkgs, 1)
|
|
assert.Equal(t, "Authentication", pkgs[0].Name)
|
|
}
|
|
|
|
func TestRbExtractor_Constants(t *testing.T) {
|
|
src := []byte(`MAX_RETRIES = 3
|
|
DEFAULT_HOST = "localhost"
|
|
`)
|
|
e := NewRubyExtractor()
|
|
result, err := e.Extract("config.rb", src)
|
|
require.NoError(t, err)
|
|
|
|
consts := nodesOfKind(result.Nodes, graph.KindConstant)
|
|
require.Len(t, consts, 2)
|
|
|
|
names := []string{consts[0].Name, consts[1].Name}
|
|
assert.Contains(t, names, "MAX_RETRIES")
|
|
assert.Contains(t, names, "DEFAULT_HOST")
|
|
}
|
|
|
|
func TestRbExtractor_CallSites(t *testing.T) {
|
|
src := []byte(`class Greeter
|
|
def greet(name)
|
|
puts("Hello")
|
|
logger.info("greeting")
|
|
end
|
|
end
|
|
`)
|
|
e := NewRubyExtractor()
|
|
result, err := e.Extract("greeter.rb", src)
|
|
require.NoError(t, err)
|
|
|
|
calls := edgesOfKind(result.Edges, graph.EdgeCalls)
|
|
assert.GreaterOrEqual(t, len(calls), 1)
|
|
}
|
|
|
|
func TestRbExtractor_NoTopLevelMethodInClass(t *testing.T) {
|
|
// Methods inside a class should NOT appear as top-level functions.
|
|
src := []byte(`class Foo
|
|
def bar
|
|
end
|
|
end
|
|
|
|
def baz
|
|
end
|
|
`)
|
|
e := NewRubyExtractor()
|
|
result, err := e.Extract("test.rb", src)
|
|
require.NoError(t, err)
|
|
|
|
funcs := nodesOfKind(result.Nodes, graph.KindFunction)
|
|
require.Len(t, funcs, 1)
|
|
assert.Equal(t, "baz", funcs[0].Name)
|
|
|
|
methods := nodesOfKind(result.Nodes, graph.KindMethod)
|
|
require.Len(t, methods, 1)
|
|
assert.Equal(t, "bar", methods[0].Name)
|
|
}
|
|
|
|
func TestRubyExtractor_RailsBeforeAction(t *testing.T) {
|
|
// before_action :name binds a callback method to the class's
|
|
// action methods. With only: filters, only the listed actions
|
|
// fire the callback. This test covers the default (all actions
|
|
// bound) plus an only: list.
|
|
src := []byte(`
|
|
class UsersController
|
|
before_action :authenticate
|
|
before_action :load_user, only: [:show, :update]
|
|
|
|
def index; end
|
|
def show; @user end
|
|
def update; @user end
|
|
|
|
private
|
|
|
|
def authenticate; end
|
|
def load_user; end
|
|
end
|
|
`)
|
|
e := NewRubyExtractor()
|
|
result, err := e.Extract("users_controller.rb", src)
|
|
require.NoError(t, err)
|
|
|
|
var authEdges, loadEdges int
|
|
actionsForAuth := map[string]bool{}
|
|
actionsForLoad := map[string]bool{}
|
|
for _, ed := range edgesOfKind(result.Edges, graph.EdgeCalls) {
|
|
if ed.Meta == nil {
|
|
continue
|
|
}
|
|
cb, _ := ed.Meta["rails_callback"].(string)
|
|
if cb == "authenticate" {
|
|
authEdges++
|
|
actionsForAuth[ed.From] = true
|
|
}
|
|
if cb == "load_user" {
|
|
loadEdges++
|
|
actionsForLoad[ed.From] = true
|
|
}
|
|
}
|
|
// authenticate guards every action (no only:/except:).
|
|
assert.Equal(t, 3, authEdges, "authenticate should bind to every action")
|
|
// load_user has only: [:show, :update].
|
|
assert.Equal(t, 2, loadEdges, "load_user should bind only to :show, :update")
|
|
assert.Contains(t, actionsForLoad, "users_controller.rb::UsersController.show")
|
|
assert.Contains(t, actionsForLoad, "users_controller.rb::UsersController.update")
|
|
assert.NotContains(t, actionsForLoad, "users_controller.rb::UsersController.index")
|
|
}
|
|
|
|
func TestRubyExtractor_SingletonMethod(t *testing.T) {
|
|
// `def self.x` (singleton_method) must be extracted alongside
|
|
// regular instance methods — previously the extractor missed them.
|
|
src := []byte(`
|
|
class User
|
|
def instance; 1 end
|
|
def self.class_method; 2 end
|
|
end
|
|
`)
|
|
e := NewRubyExtractor()
|
|
result, err := e.Extract("user.rb", src)
|
|
require.NoError(t, err)
|
|
names := make(map[string]bool)
|
|
for _, n := range result.Nodes {
|
|
if n.Kind == graph.KindMethod {
|
|
names[n.Name] = true
|
|
}
|
|
}
|
|
assert.True(t, names["instance"])
|
|
assert.True(t, names["class_method"])
|
|
}
|
|
|
|
func TestRubyExtractor_DocAndVisibility(t *testing.T) {
|
|
src := []byte(`# Greeter handles welcomes.
|
|
class Greeter
|
|
# Says hi to the user.
|
|
def hello
|
|
end
|
|
|
|
def silent
|
|
end
|
|
end
|
|
`)
|
|
e := NewRubyExtractor()
|
|
result, err := e.Extract("greeter.rb", src)
|
|
require.NoError(t, err)
|
|
|
|
byID := map[string]*graph.Node{}
|
|
for _, n := range result.Nodes {
|
|
byID[n.ID] = n
|
|
}
|
|
|
|
greeter := byID["greeter.rb::Greeter"]
|
|
require.NotNil(t, greeter)
|
|
if greeter.Meta["visibility"] != "public" {
|
|
t.Fatalf("Greeter.vis = %q", greeter.Meta["visibility"])
|
|
}
|
|
if greeter.Meta["doc"] != "Greeter handles welcomes." {
|
|
t.Fatalf("Greeter.doc = %q", greeter.Meta["doc"])
|
|
}
|
|
|
|
hello := byID["greeter.rb::Greeter.hello"]
|
|
require.NotNil(t, hello)
|
|
if hello.Meta["doc"] != "Says hi to the user." {
|
|
t.Fatalf("hello.doc = %q", hello.Meta["doc"])
|
|
}
|
|
if hello.Meta["visibility"] != "public" {
|
|
t.Fatalf("hello.vis = %q", hello.Meta["visibility"])
|
|
}
|
|
}
|
|
|
|
// TestRubyExtractor_MixinsBareCallsVisibility is the C5 test: include/extend/
|
|
// prepend produce module-composition edges, a bare receiver-less call becomes a
|
|
// call edge, and private/protected section markers (and the targeted form) set
|
|
// method visibility.
|
|
func TestRubyExtractor_MixinsBareCallsVisibility(t *testing.T) {
|
|
src := []byte("class Foo\n" +
|
|
" include Comparable\n" +
|
|
" prepend Loggable\n" +
|
|
" def pub; helper; end\n" +
|
|
" private\n" +
|
|
" def sec; end\n" +
|
|
" public\n" +
|
|
" def pub2; end\n" +
|
|
" private :pub2\n" +
|
|
"end\n")
|
|
res, err := NewRubyExtractor().Extract("foo.rb", src)
|
|
require.NoError(t, err)
|
|
|
|
vis := map[string]string{}
|
|
for _, n := range res.Nodes {
|
|
if n.Kind == graph.KindMethod {
|
|
v, _ := n.Meta["visibility"].(string)
|
|
vis[n.Name] = v
|
|
}
|
|
}
|
|
assert.Equal(t, "public", vis["pub"], "pub before any marker is public")
|
|
assert.Equal(t, "private", vis["sec"], "sec after `private` is private")
|
|
assert.Equal(t, "private", vis["pub2"], "pub2 flipped private by the targeted `private :pub2`")
|
|
|
|
mixins := map[string]string{}
|
|
var sawBareCall bool
|
|
for _, e := range res.Edges {
|
|
if e.Kind == graph.EdgeExtends && e.From == "foo.rb::Foo" && e.Meta != nil {
|
|
via, _ := e.Meta["via"].(string)
|
|
mixins[e.To] = via
|
|
}
|
|
if e.Kind == graph.EdgeCalls && e.From == "foo.rb::Foo.pub" && e.To == "unresolved::helper" {
|
|
sawBareCall = true
|
|
}
|
|
}
|
|
assert.Equal(t, "include", mixins["unresolved::Comparable"], "include composes Comparable")
|
|
assert.Equal(t, "prepend", mixins["unresolved::Loggable"], "prepend composes Loggable")
|
|
assert.True(t, sawBareCall, "a bare `helper` call should produce a call edge")
|
|
}
|