package languages import ( "fmt" "strings" "github.com/zzet/gortex/internal/excludes" "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" ) // DartExtractor extracts Dart source files. type DartExtractor struct { lang *sitter.Language } func NewDartExtractor() *DartExtractor { return &DartExtractor{lang: dart.GetLanguage()} } func (e *DartExtractor) Language() string { return "dart" } func (e *DartExtractor) Extensions() []string { return []string{".dart"} } func (e *DartExtractor) Extract(filePath string, src []byte) (*parser.ExtractionResult, error) { 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: "dart", } result.Nodes = append(result.Nodes, fileNode) if dartIsGenerated(filePath) { // Generated Dart (build_runner .g.dart, freezed, protobuf .pb.dart, …) // is machine-emitted boilerplate that mirrors hand-written declarations. // Keep the file node so incremental tracking sees the file, but skip // symbol extraction so duplicate generated symbols never pollute // search / find_usages. The marker lets callers tell it apart. fileNode.Meta = map[string]any{"generated": true} return result, nil } seen := make(map[string]bool) // Classes, enums, mixins, extensions — walk the tree to distinguish types. e.extractTypes(root, src, filePath, fileNode, result, seen) // Methods inside class/mixin/enum/extension bodies. e.extractMethods(root, src, filePath, fileNode, result, seen) // Top-level functions (function_signature + function_body at program level). e.extractTopLevelFunctions(root, src, filePath, fileNode, result, seen) // Top-level variables. e.extractTopLevelVariables(root, src, filePath, fileNode, result, seen) // Imports — also returns the alias map (`import 'pkg:foo/bar.dart' // as f;` → "f" → "package:foo/bar.dart") so the call walker can // attribute alias-prefixed calls (`f.method()`) to the right URI. imports := e.extractImports(root, src, filePath, fileNode, result) // Call sites. e.extractCalls(root, src, filePath, result, imports) e.mineDartFactoryChains(root, src, filePath, result) // Cross-file type-usage edges for declaration-position types (fields, // parameters, return types, typed locals) — runs after the symbol // extractors so the enclosing-owner ranges are populated. e.extractTypeUses(root, src, filePath, fileNode, result) // Expression-site reference edges — instantiation, inheritance, casts / // type-tests, and static access. Runs after the symbol extractors so the // enclosing-owner ranges and local-type set are populated. e.emitDartReferenceForms(root, src, filePath, fileNode, result) captureValueRefCandidates(result, root, filePath, src) captureFnValueCandidates(result, root, filePath, src) return result, nil } // extractTypes walks the root for class_definition, enum_declaration, mixin_declaration, extension_declaration. func (e *DartExtractor) extractTypes( root *sitter.Node, src []byte, filePath string, fileNode *graph.Node, result *parser.ExtractionResult, seen map[string]bool, ) { walkNodes(root, func(node *sitter.Node) { var name string var kind graph.NodeKind switch node.Type() { case "class_definition": nameNode := node.ChildByFieldName("name") if nameNode == nil { return } name = nameNode.Content(src) kind = graph.KindType // Check for abstract interface class → KindInterface. if e.hasChildType(node, "abstract") && e.hasChildType(node, "interface") { kind = graph.KindInterface } case "enum_declaration": nameNode := node.ChildByFieldName("name") if nameNode == nil { return } name = nameNode.Content(src) kind = graph.KindType case "mixin_declaration": // mixin_declaration has identifier as a child, not a named field. name = e.findChildIdentifier(node, src) if name == "" { return } kind = graph.KindType case "extension_declaration": nameNode := node.ChildByFieldName("name") if nameNode == nil { // Anonymous extension — skip. return } name = nameNode.Content(src) kind = graph.KindType default: return } id := filePath + "::" + name if seen[id] { return } seen[id] = true startLine := int(node.StartPoint().Row) + 1 endLine := int(node.EndPoint().Row) + 1 meta := map[string]any{"visibility": VisibilityByUnderscore(name)} if doc := ExtractDocAbove(src, int(node.StartPoint().Row), DocLangSlashSlash); doc != "" { meta["doc"] = doc } if node.Type() == "class_definition" { e.dartMarkFlutterWidget(meta, node, src) } result.Nodes = append(result.Nodes, &graph.Node{ ID: id, Kind: kind, Name: name, FilePath: filePath, StartLine: startLine, EndLine: endLine, Language: "dart", Meta: meta, }) result.Edges = append(result.Edges, &graph.Edge{ From: fileNode.ID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: startLine, }) if node.Type() == "class_definition" { e.emitDartMixinEdges(node, src, id, filePath, result) } }) } // dartSuperclassName returns the name of a class's `extends` base — the // direct type_identifier child of the `superclass` node (the `with` / // `implements` types are nested under mixins / interfaces). Returns "" // when the class has no extends base. func (e *DartExtractor) dartSuperclassName(classNode *sitter.Node, src []byte) string { for i, _nc := 0, int(classNode.ChildCount()); i < _nc; i++ { sup := classNode.Child(i) if sup == nil || sup.Type() != "superclass" { continue } for j, _nc := 0, int(sup.ChildCount()); j < _nc; j++ { c := sup.Child(j) if c != nil && c.Type() == "type_identifier" { return c.Content(src) } } } return "" } // dartMarkFlutterWidget stamps the Flutter component marker onto a class // meta map when the class extends StatelessWidget / StatefulWidget. func (e *DartExtractor) dartMarkFlutterWidget(meta map[string]any, classNode *sitter.Node, src []byte) { switch e.dartSuperclassName(classNode, src) { case "StatelessWidget": meta["ui_component"] = "flutter" meta["component_kind"] = "stateless" meta["type_flavor"] = "class" case "StatefulWidget": meta["ui_component"] = "flutter" meta["component_kind"] = "stateful" meta["type_flavor"] = "class" } } // emitDartMixinEdges emits an EdgeExtends edge (Meta via="mixin") from a class // to each type named in its `with` clause, so a Dart mixin application is a // first-class subtype relationship in the graph and the mixed-in members are // reachable through the class — codegraph does not model mixins at all. The // grammar nests them as class_definition → superclass → mixins → type_identifier. func (e *DartExtractor) emitDartMixinEdges(classNode *sitter.Node, src []byte, classID, filePath string, result *parser.ExtractionResult) { for i, _nc := 0, int(classNode.ChildCount()); i < _nc; i++ { sup := classNode.Child(i) if sup.Type() != "superclass" { continue } for j, _nc := 0, int(sup.ChildCount()); j < _nc; j++ { mixins := sup.Child(j) if mixins.Type() != "mixins" { continue } for k, _nc := 0, int(mixins.ChildCount()); k < _nc; k++ { m := mixins.Child(k) if m.Type() != "type_identifier" { continue } name := m.Content(src) result.Edges = append(result.Edges, &graph.Edge{ From: classID, To: "unresolved::" + name, Kind: graph.EdgeExtends, FilePath: filePath, Line: int(m.StartPoint().Row) + 1, Meta: map[string]any{"via": "mixin"}, }) } } } } // extractMethods finds method_signature nodes inside class_body, extension_body, enum_body. func (e *DartExtractor) extractMethods( root *sitter.Node, src []byte, filePath string, fileNode *graph.Node, result *parser.ExtractionResult, seen map[string]bool, ) { // Collect type body ranges for ownership detection. typeBodyRanges := e.collectTypeBodyRanges(root, src) walkNodes(root, func(node *sitter.Node) { if node.Type() != "method_signature" { return } // Must be inside a body (class_body, extension_body, enum_body). parent := node.Parent() if parent == nil { return } parentType := parent.Type() if parentType != "class_body" && parentType != "extension_body" && parentType != "enum_body" { return } name := e.extractMethodName(node, src) if name == "" { return } // Find enclosing type name. typeName := "" startLine := int(node.StartPoint().Row) if tn, ok := e.findEnclosingType(typeBodyRanges, startLine); ok { typeName = tn } // An unnamed constructor (`Foo()` inside class Foo) is matched as a // method whose name equals the enclosing type. Emitting a Foo.Foo // method creates a phantom that hijacks resolution of `Foo(...)` — // which constructs the class, not calls a method. Skip it; extractCalls // emits an EdgeInstantiates to the class node instead. if typeName != "" && name == typeName { return } methodID := filePath + "::" + typeName + "." + name if seen[methodID] { methodID = filePath + "::" + typeName + "." + name + "_L" + fmt.Sprint(startLine+1) } if seen[methodID] { return } seen[methodID] = true seen[filePath+"::_method_L"+fmt.Sprint(startLine+1)] = true // Dart's tree-sitter grammar splits a method into a leading // method_signature node (declaration line) and a following // function_body sibling that carries the `{ ... }` block. // Using method_signature.EndPoint alone makes every method // one line long and breaks downstream tooling that wants to // read the body (source viewer, shape extractor, // brace-balancing fallbacks, coverage). Stretch EndLine to // cover the adjacent function_body when present. endLine := int(node.EndPoint().Row) if body := nextDartFunctionBody(node); body != nil { endLine = int(body.EndPoint().Row) } methodMeta := map[string]any{ "receiver": typeName, "visibility": VisibilityByUnderscore(name), } // Declared return type — seeds the chained-factory receiver walker // (helpers_chaintype) so `Widget.create().build()` resolves. if rt := dartMethodReturnType(node, src); rt != "" { methodMeta["return_type"] = rt } if doc := ExtractDocAbove(src, startLine, DocLangSlashSlash); doc != "" { methodMeta["doc"] = doc } result.Nodes = append(result.Nodes, &graph.Node{ ID: methodID, Kind: graph.KindMethod, Name: name, FilePath: filePath, StartLine: startLine + 1, EndLine: endLine + 1, Language: "dart", Meta: methodMeta, }) result.Edges = append(result.Edges, &graph.Edge{ From: fileNode.ID, To: methodID, Kind: graph.EdgeDefines, FilePath: filePath, Line: startLine + 1, }) if typeName != "" { typeID := filePath + "::" + typeName result.Edges = append(result.Edges, &graph.Edge{ From: methodID, To: typeID, Kind: graph.EdgeMemberOf, FilePath: filePath, Line: startLine + 1, }) } }) } // dartIsGenerated reports whether a Dart file path is a code-generator output // (build_runner, freezed, protobuf, json_serializable, auto_route, injectable). // Such files mirror hand-written declarations and only add duplicate, // machine-managed symbols, so they are indexed as a bare (generated-marked) // file node without their symbols. Delegates to the shared excludes set so the // Dart suffixes stay in one source of truth. func dartIsGenerated(filePath string) bool { return excludes.IsGenerated(filePath) } // dartMethodReturnType returns a Dart method's declared return type — the type // that precedes the name in its function_signature (`Widget build()` → Widget). // Returns "" when the method has no leading return type (e.g. a void-inferred // `build()` or a getter/setter). func dartMethodReturnType(methodSig *sitter.Node, src []byte) string { for i, _nc := 0, int(methodSig.NamedChildCount()); i < _nc; i++ { fs := methodSig.NamedChild(i) if fs.Type() != "function_signature" { continue } for j, _nc := 0, int(fs.NamedChildCount()); j < _nc; j++ { c := fs.NamedChild(j) switch c.Type() { case "type_identifier", "type", "nullable_type": return strings.TrimSpace(c.Content(src)) case "identifier", "function_type": return "" // reached the method name before any return type } } } return "" } // nextDartFunctionBody returns the function_body sibling that follows // a method_signature / function_signature node, walking through // immediate siblings (the Dart grammar occasionally interposes // whitespace / comment nodes between them). Returns nil when the // method is abstract or declared with `=>` without a brace block. func nextDartFunctionBody(sig *sitter.Node) *sitter.Node { for s := sig.NextSibling(); s != nil; s = s.NextSibling() { if s.Type() == "function_body" { return s } // Stop if we've walked past the declaration grouping — the // next signature wipes any chance of finding "this" one's // body. if s.Type() == "method_signature" || s.Type() == "function_signature" { return nil } } return nil } // extractTopLevelFunctions finds function_signature nodes that are direct children of program // (followed by function_body). func (e *DartExtractor) extractTopLevelFunctions( root *sitter.Node, src []byte, filePath string, fileNode *graph.Node, result *parser.ExtractionResult, seen map[string]bool, ) { for i, _nc := 0, int(root.ChildCount()); i < _nc; i++ { child := root.Child(i) if child.Type() != "function_signature" { continue } nameNode := child.ChildByFieldName("name") if nameNode == nil { continue } name := nameNode.Content(src) startLine := int(child.StartPoint().Row) + 1 // Check if next sibling is function_body to get end line. endLine := int(child.EndPoint().Row) + 1 if i+1 < int(root.ChildCount()) { next := root.Child(i + 1) if next.Type() == "function_body" { endLine = int(next.EndPoint().Row) + 1 } } id := filePath + "::" + name if seen[id] { id = filePath + "::" + name + "_L" + fmt.Sprint(startLine) } if seen[id] { continue } seen[id] = true fnMeta := map[string]any{"visibility": VisibilityByUnderscore(name)} if doc := ExtractDocAbove(src, startLine-1, DocLangSlashSlash); doc != "" { fnMeta["doc"] = doc } result.Nodes = append(result.Nodes, &graph.Node{ ID: id, Kind: graph.KindFunction, Name: name, FilePath: filePath, StartLine: startLine, EndLine: endLine, Language: "dart", Meta: fnMeta, }) result.Edges = append(result.Edges, &graph.Edge{ From: fileNode.ID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: startLine, }) } } // extractTopLevelVariables finds top-level initialized_variable_definition and // static_final_declaration_list nodes at program level. func (e *DartExtractor) extractTopLevelVariables( root *sitter.Node, src []byte, filePath string, fileNode *graph.Node, result *parser.ExtractionResult, seen map[string]bool, ) { for i, _nc := 0, int(root.ChildCount()); i < _nc; i++ { child := root.Child(i) switch child.Type() { case "initialized_variable_definition": nameNode := child.ChildByFieldName("name") if nameNode == nil { continue } name := nameNode.Content(src) startLine := int(child.StartPoint().Row) + 1 id := filePath + "::" + name if seen[id] { continue } seen[id] = true result.Nodes = append(result.Nodes, &graph.Node{ ID: id, Kind: graph.KindVariable, Name: name, FilePath: filePath, StartLine: startLine, EndLine: int(child.EndPoint().Row) + 1, Language: "dart", }) result.Edges = append(result.Edges, &graph.Edge{ From: fileNode.ID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: startLine, }) case "static_final_declaration_list": // Walk children for static_final_declaration nodes. for j, _nc := 0, int(child.ChildCount()); j < _nc; j++ { decl := child.Child(j) if decl.Type() != "static_final_declaration" { continue } name := e.findChildIdentifier(decl, src) if name == "" { continue } startLine := int(decl.StartPoint().Row) + 1 id := filePath + "::" + name if seen[id] { continue } seen[id] = true // A static_final_declaration is a `const` / `final` / `static // final` binding — immutable by grammar. A distinctive-named one // is a Dart value constant; kind it as KindConstant so // value-reference impact analysis reaches its readers (mirrors // the Scala/Ruby constant handling). declKind := graph.KindVariable if isDistinctiveConstName(name) { declKind = graph.KindConstant } result.Nodes = append(result.Nodes, &graph.Node{ ID: id, Kind: declKind, Name: name, FilePath: filePath, StartLine: startLine, EndLine: int(decl.EndPoint().Row) + 1, Language: "dart", }) result.Edges = append(result.Edges, &graph.Edge{ From: fileNode.ID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: startLine, }) } } } } // extractImports walks `import_or_export` nodes, emits the import // edge, and returns the per-file alias map captured from `as // ` clauses. The map is consumed by `extractCalls` so calls // like `f.method(args)` (where `f` was bound by `import '…' as f;`) // attribute to the originating URI rather than landing as the // unresolved-method fallback. func (e *DartExtractor) extractImports( root *sitter.Node, src []byte, filePath string, fileNode *graph.Node, result *parser.ExtractionResult, ) map[string]string { imports := map[string]string{} walkNodes(root, func(node *sitter.Node) { if node.Type() != "import_or_export" { return } text := node.Content(src) uri := extractDartImportURI(text) if uri == "" { return } startLine := int(node.StartPoint().Row) + 1 result.Edges = append(result.Edges, &graph.Edge{ From: fileNode.ID, To: "unresolved::import::" + uri, Kind: graph.EdgeImports, FilePath: filePath, Line: startLine, }) if alias := extractDartImportAlias(text); alias != "" { // Re-binding the same alias to two different URIs in // one file would be a Dart compile error; keep // last-write-wins so a freshly-edited file with a // half-typed import doesn't poison earlier state. imports[alias] = uri } }) return imports } // extractCalls finds function/method call sites and emits EdgeCalls // edges to the resolver-stub target. // // Dart's tree-sitter grammar exposes both a flat `bareCall()` and a // chained `fl.runApp()` as siblings under the surrounding scope: // // bareCall(): // identifier "bareCall" // selector // argument_part(...) // // fl.runApp(): // identifier "fl" // selector // unconditional_assignable_selector // "." // identifier "runApp" // selector // argument_part(...) // // We anchor on the leading identifier (the one that is NOT itself a // child of a selector / unconditional_assignable_selector) and scan // forward through `selector` siblings, collecting `.method` // segments until a selector with `argument_part` confirms the // call. The trailing-most identifier is the call name; the leading // identifier is the receiver. When the receiver matches an import // alias the edge attributes to `unresolved::extern::::` // so the resolver-side module-attribution pass can land it on a // KindModule; otherwise we fall back to the legacy name-only stub. func (e *DartExtractor) extractCalls( root *sitter.Node, src []byte, filePath string, result *parser.ExtractionResult, imports map[string]string, ) { funcRanges := buildFuncRanges(result) // Local type names — a bare call to one is a construction, not a function // call. Populated from the types extractTypes already emitted (it runs // before extractCalls). localTypes := map[string]bool{} for _, n := range result.Nodes { if n != nil && (n.Kind == graph.KindType || n.Kind == graph.KindInterface) { localTypes[n.Name] = true } } walkNodes(root, func(node *sitter.Node) { if node.Type() != "identifier" { return } // Skip identifiers that live inside a selector chain; the // chain is reconstructed once from the leading identifier // below. Without this guard we would emit duplicate // edges (one per identifier in the chain). if p := node.Parent(); p != nil { switch p.Type() { case "unconditional_assignable_selector", "conditional_assignable_selector", "selector": return } } methodChain := []string{node.Content(src)} isCall := false for sib := node.NextSibling(); sib != nil && !isCall; sib = sib.NextSibling() { if sib.Type() != "selector" { break } for j, _nc := 0, int(sib.ChildCount()); j < _nc; j++ { child := sib.Child(j) switch child.Type() { case "argument_part", "arguments": isCall = true case "unconditional_assignable_selector", "conditional_assignable_selector": if id := firstIdentifierChild(child); id != nil { methodChain = append(methodChain, id.Content(src)) } } } } if !isCall { return } line := int(node.StartPoint().Row) + 1 callerID := findEnclosingFunc(funcRanges, line) if callerID == "" { return } callName := methodChain[len(methodChain)-1] leadingReceiver := "" if len(methodChain) > 1 { leadingReceiver = methodChain[0] } // Instantiation: a bare call to a known local type name (`Widget()`, // also `new`/`const` Widget()) constructs the class. Emit a typed // EdgeInstantiates to the class node rather than a flat unresolved call // — impact/trace then see the construction and the resolver binds it to // the real type. (codegraph emits only a flat call edge here.) if len(methodChain) == 1 && localTypes[callName] { result.Edges = append(result.Edges, &graph.Edge{ From: callerID, To: filePath + "::" + callName, Kind: graph.EdgeInstantiates, FilePath: filePath, Line: line, Origin: graph.OriginASTResolved, }) return } target := "unresolved::*." + callName if leadingReceiver != "" { if uri, ok := imports[leadingReceiver]; ok { target = "unresolved::extern::" + uri + "::" + callName } } result.Edges = append(result.Edges, &graph.Edge{ From: callerID, To: target, Kind: graph.EdgeCalls, FilePath: filePath, Line: line, }) }) } // firstIdentifierChild returns the first direct child of node whose // type is "identifier", or nil. Used to pull the method name out of // a Dart selector wrapper. func firstIdentifierChild(node *sitter.Node) *sitter.Node { if node == nil { return nil } for i, _nc := 0, int(node.ChildCount()); i < _nc; i++ { c := node.Child(i) if c != nil && c.Type() == "identifier" { return c } } return nil } // --- helpers --- func (e *DartExtractor) hasChildType(node *sitter.Node, typeName string) bool { for i, _nc := 0, int(node.ChildCount()); i < _nc; i++ { if node.Child(i).Type() == typeName { return true } } return false } func (e *DartExtractor) findChildIdentifier(node *sitter.Node, src []byte) string { for i, _nc := 0, int(node.ChildCount()); i < _nc; i++ { child := node.Child(i) if child.Type() == "identifier" { return child.Content(src) } } return "" } // extractMethodName extracts the name from a method_signature node. // method_signature wraps function_signature, getter_signature, setter_signature, // constructor_signature, operator_signature, factory_constructor_signature. func (e *DartExtractor) extractMethodName(node *sitter.Node, src []byte) string { for i, _nc := 0, int(node.ChildCount()); i < _nc; i++ { child := node.Child(i) switch child.Type() { case "function_signature": nameNode := child.ChildByFieldName("name") if nameNode != nil { return nameNode.Content(src) } case "getter_signature": nameNode := child.ChildByFieldName("name") if nameNode != nil { return nameNode.Content(src) } case "setter_signature": nameNode := child.ChildByFieldName("name") if nameNode != nil { return "set " + nameNode.Content(src) } case "constructor_signature": nameNode := child.ChildByFieldName("name") if nameNode != nil { return nameNode.Content(src) } case "factory_constructor_signature": return e.findChildIdentifier(child, src) case "operator_signature": // operator + binary_operator child for j, _nc := 0, int(child.ChildCount()); j < _nc; j++ { c := child.Child(j) if c.Type() == "binary_operator" || c.Type() == "tilde_operator" { return "operator " + strings.TrimSpace(c.Content(src)) } } } } return "" } type dartTypeRange struct { typeName string startLine int // 0-based endLine int // 0-based } func (e *DartExtractor) collectTypeBodyRanges(root *sitter.Node, src []byte) []dartTypeRange { var ranges []dartTypeRange walkNodes(root, func(node *sitter.Node) { var name string switch node.Type() { case "class_definition": n := node.ChildByFieldName("name") if n != nil { name = n.Content(src) } case "enum_declaration": n := node.ChildByFieldName("name") if n != nil { name = n.Content(src) } case "mixin_declaration": name = e.findChildIdentifier(node, src) case "extension_declaration": n := node.ChildByFieldName("name") if n != nil { name = n.Content(src) } default: return } if name == "" { return } ranges = append(ranges, dartTypeRange{ typeName: name, startLine: int(node.StartPoint().Row), endLine: int(node.EndPoint().Row), }) }) return ranges } func (e *DartExtractor) findEnclosingType(ranges []dartTypeRange, line int) (string, bool) { best := "" bestSize := int(^uint(0) >> 1) for _, r := range ranges { if line >= r.startLine && line <= r.endLine { size := r.endLine - r.startLine if size < bestSize { bestSize = size best = r.typeName } } } if best == "" { return "", false } return best, true } // extractDartImportAlias pulls the prefix bound by ` as ` // from an `import_or_export` statement's text. Returns empty when // the import has no alias clause. The Dart grammar guarantees // `as ` follows the URI; we scan the suffix after the // closing quote so we don't confuse `as` inside the URI itself // (e.g. `package:as_a_string/...`). func extractDartImportAlias(text string) string { closing := -1 for _, q := range []byte{'\'', '"'} { start := strings.IndexByte(text, q) if start < 0 { continue } end := strings.IndexByte(text[start+1:], q) if end < 0 { continue } closing = start + 1 + end break } if closing < 0 || closing+1 >= len(text) { return "" } rest := text[closing+1:] idx := strings.Index(rest, " as ") if idx < 0 { return "" } tail := strings.TrimLeft(rest[idx+len(" as "):], " \t") end := 0 for end < len(tail) { c := tail[end] if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '$' { end++ continue } break } return tail[:end] } // extractDartImportURI extracts the URI string from an import/export statement text. // e.g. "import 'package:flutter/material.dart';" → "package:flutter/material.dart" func extractDartImportURI(text string) string { // Find content between quotes. for _, q := range []byte{'\'', '"'} { start := strings.IndexByte(text, q) if start < 0 { continue } end := strings.IndexByte(text[start+1:], q) if end < 0 { continue } return text[start+1 : start+1+end] } return "" } // mineDartFactoryChains emits a member call per chained `.method(...)` whose // receiver is itself a call -- a factory chain like builder().withX().build(). // extractCalls anchors on the leading identifier and stops at the first call, // so these inner segments are otherwise dropped. Gated on a call-bearing // receiver (so plain recv.method() is left to extractCalls, not double-counted) // and carries the receiver text through the shared chained-receiver walker. func (e *DartExtractor) mineDartFactoryChains(root *sitter.Node, src []byte, filePath string, result *parser.ExtractionResult) { funcRanges := buildFuncRanges(result) if len(funcRanges) == 0 { return } walkNodes(root, func(node *sitter.Node) { if node.Type() != "identifier" { return } if p := node.Parent(); p != nil { switch p.Type() { case "unconditional_assignable_selector", "conditional_assignable_selector", "selector": return } } var pending *sitter.Node for sib := node.NextSibling(); sib != nil; sib = sib.NextSibling() { if sib.Type() != "selector" { break } var assignable *sitter.Node hasArgs := false for j, _nc := 0, int(sib.ChildCount()); j < _nc; j++ { switch sib.Child(j).Type() { case "argument_part", "arguments": hasArgs = true case "unconditional_assignable_selector", "conditional_assignable_selector": assignable = sib.Child(j) } } if assignable != nil { pending = assignable continue } if hasArgs && pending != nil { if id := firstIdentifierChild(pending); id != nil { receiver := strings.TrimSpace(string(src[node.StartByte():pending.StartByte()])) if strings.Contains(receiver, "(") { line := int(pending.StartPoint().Row) + 1 if callerID := findEnclosingFunc(funcRanges, line); callerID != "" { edge := &graph.Edge{ From: callerID, To: "unresolved::*." + id.Content(src), Kind: graph.EdgeCalls, FilePath: filePath, Line: line, } stampFactoryChainReceiver(edge, receiver, resolveChainType(receiver, nil, result)) result.Edges = append(result.Edges, edge) } } } pending = nil } } }) }