Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

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"])
}
}