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
461 lines
12 KiB
Go
461 lines
12 KiB
Go
package languages
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/zzet/gortex/internal/graph"
|
|
"github.com/zzet/gortex/internal/parser"
|
|
sitter "github.com/zzet/gortex/internal/parser/tsitter"
|
|
"github.com/zzet/gortex/internal/parser/tsitter/dart"
|
|
)
|
|
|
|
// TestDartAST_Debug dumps the AST to verify node types used in queries.
|
|
func TestDartAST_Debug(t *testing.T) {
|
|
src := []byte(`import 'package:flutter/material.dart';
|
|
|
|
abstract class Animal {
|
|
String get name;
|
|
void speak();
|
|
}
|
|
|
|
class Dog extends Animal {
|
|
@override
|
|
String get name => 'Dog';
|
|
|
|
@override
|
|
void speak() {
|
|
print('Woof!');
|
|
}
|
|
|
|
void fetch(String item) {
|
|
print('Fetching $item');
|
|
}
|
|
}
|
|
|
|
enum Color { red, green, blue }
|
|
|
|
mixin Swimming {
|
|
void swim() {
|
|
print('Swimming!');
|
|
}
|
|
}
|
|
|
|
extension StringExt on String {
|
|
String capitalize() {
|
|
return '${this[0].toUpperCase()}${substring(1)}';
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
final dog = Dog();
|
|
dog.speak();
|
|
dog.fetch('ball');
|
|
}
|
|
|
|
const version = '1.0.0';
|
|
`)
|
|
lang := dart.GetLanguage()
|
|
tree, err := parser.ParseFile(src, lang)
|
|
require.NoError(t, err)
|
|
defer tree.Close()
|
|
|
|
root := tree.RootNode()
|
|
var walk func(n *sitter.Node, depth int)
|
|
walk = func(n *sitter.Node, depth int) {
|
|
indent := ""
|
|
for i := 0; i < depth; i++ {
|
|
indent += " "
|
|
}
|
|
if n.IsNamed() {
|
|
t.Logf("%s%s [%d:%d - %d:%d] %q", indent, n.Type(),
|
|
n.StartPoint().Row, n.StartPoint().Column,
|
|
n.EndPoint().Row, n.EndPoint().Column,
|
|
truncate(n.Content(src), 60))
|
|
}
|
|
for i := 0; i < int(n.ChildCount()); i++ {
|
|
walk(n.Child(i), depth+1)
|
|
}
|
|
}
|
|
walk(root, 0)
|
|
}
|
|
|
|
func TestDartExtractor_ClassWithMethods(t *testing.T) {
|
|
src := []byte(`class UserService {
|
|
Future<User> getUser(String id) async {
|
|
return await findById(id);
|
|
}
|
|
|
|
void deleteUser(String id) {
|
|
remove(id);
|
|
}
|
|
}
|
|
`)
|
|
e := NewDartExtractor()
|
|
result, err := e.Extract("user_service.dart", 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)
|
|
require.Len(t, methods, 2)
|
|
names := []string{methods[0].Name, methods[1].Name}
|
|
assert.Contains(t, names, "getUser")
|
|
assert.Contains(t, names, "deleteUser")
|
|
|
|
memberEdges := edgesOfKind(result.Edges, graph.EdgeMemberOf)
|
|
require.Len(t, memberEdges, 2)
|
|
for _, edge := range memberEdges {
|
|
assert.Equal(t, "user_service.dart::UserService", edge.To)
|
|
}
|
|
|
|
// Methods must span signature + body, not the declaration line
|
|
// alone. A one-line span (end == start) breaks source viewers
|
|
// and the shape extractor.
|
|
for _, m := range methods {
|
|
if m.EndLine <= m.StartLine {
|
|
t.Errorf("method %s has end_line (%d) <= start_line (%d) — body span missing",
|
|
m.Name, m.EndLine, m.StartLine)
|
|
}
|
|
}
|
|
// Exact expected spans given the fixture above. Changing the
|
|
// fixture is fine; changing these numbers without is a regression.
|
|
byName := map[string]*graph.Node{}
|
|
for _, m := range methods {
|
|
byName[m.Name] = m
|
|
}
|
|
if m := byName["getUser"]; m != nil {
|
|
assert.Equal(t, 2, m.StartLine, "getUser start")
|
|
assert.Equal(t, 4, m.EndLine, "getUser end (through closing brace)")
|
|
}
|
|
if m := byName["deleteUser"]; m != nil {
|
|
assert.Equal(t, 6, m.StartLine, "deleteUser start")
|
|
assert.Equal(t, 8, m.EndLine, "deleteUser end (through closing brace)")
|
|
}
|
|
}
|
|
|
|
func TestDartExtractor_AbstractClass(t *testing.T) {
|
|
src := []byte(`abstract class Repository {
|
|
Future<User> findById(String id);
|
|
Future<void> save(User user);
|
|
}
|
|
`)
|
|
e := NewDartExtractor()
|
|
result, err := e.Extract("repository.dart", src)
|
|
require.NoError(t, err)
|
|
|
|
types := nodesOfKind(result.Nodes, graph.KindType)
|
|
require.Len(t, types, 1)
|
|
assert.Equal(t, "Repository", types[0].Name)
|
|
}
|
|
|
|
func TestDartExtractor_TopLevelFunction(t *testing.T) {
|
|
src := []byte(`void greet(String name) {
|
|
print('Hello, $name');
|
|
}
|
|
|
|
int add(int a, int b) => a + b;
|
|
`)
|
|
e := NewDartExtractor()
|
|
result, err := e.Extract("utils.dart", src)
|
|
require.NoError(t, err)
|
|
|
|
funcs := nodesOfKind(result.Nodes, graph.KindFunction)
|
|
require.Len(t, funcs, 2)
|
|
names := []string{funcs[0].Name, funcs[1].Name}
|
|
assert.Contains(t, names, "greet")
|
|
assert.Contains(t, names, "add")
|
|
|
|
methods := nodesOfKind(result.Nodes, graph.KindMethod)
|
|
assert.Empty(t, methods)
|
|
}
|
|
|
|
func TestDartExtractor_Enum(t *testing.T) {
|
|
src := []byte(`enum Status {
|
|
active,
|
|
inactive,
|
|
pending;
|
|
|
|
String get label => name.toUpperCase();
|
|
}
|
|
`)
|
|
e := NewDartExtractor()
|
|
result, err := e.Extract("status.dart", src)
|
|
require.NoError(t, err)
|
|
|
|
types := nodesOfKind(result.Nodes, graph.KindType)
|
|
require.Len(t, types, 1)
|
|
assert.Equal(t, "Status", types[0].Name)
|
|
}
|
|
|
|
func TestDartExtractor_Mixin(t *testing.T) {
|
|
src := []byte(`mixin Logging {
|
|
void log(String message) {
|
|
print('[LOG] $message');
|
|
}
|
|
}
|
|
`)
|
|
e := NewDartExtractor()
|
|
result, err := e.Extract("logging.dart", src)
|
|
require.NoError(t, err)
|
|
|
|
types := nodesOfKind(result.Nodes, graph.KindType)
|
|
require.Len(t, types, 1)
|
|
assert.Equal(t, "Logging", types[0].Name)
|
|
|
|
methods := nodesOfKind(result.Nodes, graph.KindMethod)
|
|
require.Len(t, methods, 1)
|
|
assert.Equal(t, "log", methods[0].Name)
|
|
}
|
|
|
|
func TestDartExtractor_Extension(t *testing.T) {
|
|
src := []byte(`extension NumberParsing on String {
|
|
int toInt() {
|
|
return int.parse(this);
|
|
}
|
|
|
|
double toDouble() {
|
|
return double.parse(this);
|
|
}
|
|
}
|
|
`)
|
|
e := NewDartExtractor()
|
|
result, err := e.Extract("extensions.dart", src)
|
|
require.NoError(t, err)
|
|
|
|
types := nodesOfKind(result.Nodes, graph.KindType)
|
|
require.Len(t, types, 1)
|
|
assert.Equal(t, "NumberParsing", types[0].Name)
|
|
|
|
methods := nodesOfKind(result.Nodes, graph.KindMethod)
|
|
require.Len(t, methods, 2)
|
|
names := []string{methods[0].Name, methods[1].Name}
|
|
assert.Contains(t, names, "toInt")
|
|
assert.Contains(t, names, "toDouble")
|
|
}
|
|
|
|
func TestDartExtractor_Imports(t *testing.T) {
|
|
src := []byte(`import 'dart:async';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:http/http.dart' as http;
|
|
export 'src/widget.dart';
|
|
|
|
void main() {}
|
|
`)
|
|
e := NewDartExtractor()
|
|
result, err := e.Extract("main.dart", src)
|
|
require.NoError(t, err)
|
|
|
|
imports := edgesOfKind(result.Edges, graph.EdgeImports)
|
|
require.Len(t, imports, 4)
|
|
}
|
|
|
|
func TestDartExtractor_CallSites(t *testing.T) {
|
|
src := []byte(`void main() {
|
|
print('hello');
|
|
greet('world');
|
|
}
|
|
|
|
void greet(String name) {
|
|
print(name);
|
|
}
|
|
`)
|
|
e := NewDartExtractor()
|
|
result, err := e.Extract("main.dart", src)
|
|
require.NoError(t, err)
|
|
|
|
calls := edgesOfKind(result.Edges, graph.EdgeCalls)
|
|
assert.GreaterOrEqual(t, len(calls), 2, "expected at least 2 call edges")
|
|
|
|
targets := make(map[string]bool)
|
|
for _, c := range calls {
|
|
targets[c.To] = true
|
|
}
|
|
assert.True(t, targets["unresolved::*.print"], "missing print call")
|
|
assert.True(t, targets["unresolved::*.greet"], "missing greet call")
|
|
}
|
|
|
|
func TestDartExtractor_FlutterWidget(t *testing.T) {
|
|
src := []byte(`import 'package:flutter/material.dart';
|
|
|
|
class MyApp extends StatelessWidget {
|
|
const MyApp({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: Text('Hello Flutter'),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
runApp(const MyApp());
|
|
}
|
|
`)
|
|
e := NewDartExtractor()
|
|
result, err := e.Extract("main.dart", src)
|
|
require.NoError(t, err)
|
|
|
|
types := nodesOfKind(result.Nodes, graph.KindType)
|
|
require.Len(t, types, 1)
|
|
assert.Equal(t, "MyApp", types[0].Name)
|
|
|
|
methods := nodesOfKind(result.Nodes, graph.KindMethod)
|
|
assert.GreaterOrEqual(t, len(methods), 1)
|
|
|
|
funcs := nodesOfKind(result.Nodes, graph.KindFunction)
|
|
require.Len(t, funcs, 1)
|
|
assert.Equal(t, "main", funcs[0].Name)
|
|
}
|
|
|
|
func TestDartExtractor_DocAndVisibility(t *testing.T) {
|
|
src := []byte(`/// The greeter widget.
|
|
class Greeter {
|
|
/// Builds the widget.
|
|
Widget build() => Container();
|
|
|
|
void _privateHelper() {}
|
|
}
|
|
|
|
/// Top-level helper.
|
|
void hello() {}
|
|
|
|
void _internal() {}
|
|
`)
|
|
e := NewDartExtractor()
|
|
result, err := e.Extract("greeter.dart", src)
|
|
require.NoError(t, err)
|
|
|
|
byID := map[string]*graph.Node{}
|
|
for _, n := range result.Nodes {
|
|
byID[n.ID] = n
|
|
}
|
|
|
|
greeter := byID["greeter.dart::Greeter"]
|
|
require.NotNil(t, greeter)
|
|
if greeter.Meta["visibility"] != "public" {
|
|
t.Fatalf("Greeter.vis = %q", greeter.Meta["visibility"])
|
|
}
|
|
if greeter.Meta["doc"] != "The greeter widget." {
|
|
t.Fatalf("Greeter.doc = %q", greeter.Meta["doc"])
|
|
}
|
|
|
|
build := byID["greeter.dart::Greeter.build"]
|
|
require.NotNil(t, build)
|
|
if build.Meta["doc"] != "Builds the widget." {
|
|
t.Fatalf("build.doc = %q", build.Meta["doc"])
|
|
}
|
|
if build.Meta["visibility"] != "public" {
|
|
t.Fatalf("build.vis = %q", build.Meta["visibility"])
|
|
}
|
|
|
|
priv := byID["greeter.dart::Greeter._privateHelper"]
|
|
require.NotNil(t, priv)
|
|
if priv.Meta["visibility"] != "private" {
|
|
t.Fatalf("_privateHelper.vis = %q", priv.Meta["visibility"])
|
|
}
|
|
|
|
hello := byID["greeter.dart::hello"]
|
|
require.NotNil(t, hello)
|
|
if hello.Meta["doc"] != "Top-level helper." {
|
|
t.Fatalf("hello.doc = %q", hello.Meta["doc"])
|
|
}
|
|
|
|
internalFn := byID["greeter.dart::_internal"]
|
|
require.NotNil(t, internalFn)
|
|
if internalFn.Meta["visibility"] != "private" {
|
|
t.Fatalf("_internal.vis = %q", internalFn.Meta["visibility"])
|
|
}
|
|
}
|
|
|
|
// TestUnnamedCtorNotEmittedAndInstantiationEdge is the B2 named test: an
|
|
// unnamed Dart constructor must not be emitted as a phantom Foo.Foo method, and
|
|
// `Foo()` must produce a typed EdgeInstantiates to the class. A `with` clause
|
|
// produces a mixin EdgeExtends edge.
|
|
func TestUnnamedCtorNotEmittedAndInstantiationEdge(t *testing.T) {
|
|
src := []byte(`mixin Logger {}
|
|
|
|
class Widget with Logger {
|
|
Widget();
|
|
void build() {}
|
|
}
|
|
|
|
class App {
|
|
void run() {
|
|
var w = Widget();
|
|
}
|
|
}
|
|
`)
|
|
res, err := NewDartExtractor().Extract("w.dart", []byte(src))
|
|
require.NoError(t, err)
|
|
|
|
// No phantom unnamed-constructor method node.
|
|
for _, n := range res.Nodes {
|
|
if n.Kind == graph.KindMethod && n.ID == "w.dart::Widget.Widget" {
|
|
t.Fatalf("unnamed constructor was emitted as a phantom method node %q", n.ID)
|
|
}
|
|
}
|
|
|
|
// `var w = Widget()` is a construction, not a call.
|
|
var sawInstantiate, sawMixin bool
|
|
for _, e := range res.Edges {
|
|
if e.Kind == graph.EdgeInstantiates && e.From == "w.dart::App.run" && e.To == "w.dart::Widget" {
|
|
sawInstantiate = true
|
|
}
|
|
if e.Kind == graph.EdgeExtends && e.From == "w.dart::Widget" && e.To == "unresolved::Logger" {
|
|
if v, _ := e.Meta["via"].(string); v == "mixin" {
|
|
sawMixin = true
|
|
}
|
|
}
|
|
}
|
|
assert.True(t, sawInstantiate, "expected an EdgeInstantiates from App.run to Widget; edges=%v", res.Edges)
|
|
assert.True(t, sawMixin, "expected a mixin EdgeExtends from Widget to Logger; edges=%v", res.Edges)
|
|
|
|
// The real method still resolves.
|
|
var sawBuild bool
|
|
for _, n := range res.Nodes {
|
|
if n.Kind == graph.KindMethod && n.Name == "build" {
|
|
sawBuild = true
|
|
}
|
|
}
|
|
assert.True(t, sawBuild, "the real method build should still be emitted")
|
|
}
|
|
|
|
func TestDartExtractor_FactoryChainReceiver(t *testing.T) {
|
|
src := []byte("Widget builder() { return Widget(); }\n" +
|
|
"void run() {\n" +
|
|
" builder().withX().build();\n" +
|
|
"}\n")
|
|
res, err := NewDartExtractor().Extract("w.dart", src)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var build *graph.Edge
|
|
seen := map[string]int{}
|
|
for _, e := range res.Edges {
|
|
if e.Kind == graph.EdgeCalls {
|
|
seen[e.To]++
|
|
if e.To == "unresolved::*.build" {
|
|
build = e
|
|
}
|
|
}
|
|
}
|
|
if build == nil {
|
|
t.Fatal("build() call edge not found")
|
|
}
|
|
if got, _ := build.Meta["receiver_expr"].(string); got != "builder().withX()" {
|
|
t.Errorf("receiver_expr = %q, want builder().withX()", got)
|
|
}
|
|
if seen["unresolved::*.build"] != 1 {
|
|
t.Errorf("build() emitted %d times, want exactly 1 (no double-count)", seen["unresolved::*.build"])
|
|
}
|
|
}
|