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

415 lines
13 KiB
Go

package languages
import (
"strings"
"github.com/zzet/gortex/internal/parser"
sitter "github.com/zzet/gortex/internal/parser/tsitter"
)
// emitSwiftAnnotationEdges scans a declaration node for `modifiers` /
// `attribute` children and emits one EdgeAnnotated per attribute onto
// the synthetic `annotation::swift::<name>` node. Covers:
//
// @objc → annotation::swift::objc
// @objc(legacyName) → annotation::swift::objc (args="legacyName")
// @available(iOS 13.0, *) → annotation::swift::available
// @MainActor → annotation::swift::MainActor
// @Published → annotation::swift::Published
// @State / @Binding / @Environment — SwiftUI property wrappers
// @objcMembers, @inlinable, @inline, @dynamicCallable, @propertyWrapper, …
//
// Property wrappers and actor attributes are dispatch-relevant — they
// change how the property is accessed (KVO, observation framework,
// main-thread isolation) — so making them queryable via `find_usages`
// on the synthetic annotation node lets agents answer "every @Published
// property in this module" with one hop.
//
// The Swift tree-sitter grammar nests attributes under a `modifiers`
// child of the declaration, so the scan walks that level. If the
// declaration has no modifiers child the function is a silent no-op.
func emitSwiftAnnotationEdges(
defNode *sitter.Node, fromID, filePath string, src []byte,
result *parser.ExtractionResult, seen map[string]bool,
) {
if defNode == nil || fromID == "" {
return
}
mods := defNode.ChildByFieldName("modifiers")
if mods == nil {
// Some declarations expose modifiers as a positional named
// child rather than via a named field; scan top-level
// children for a `modifiers` node as a fallback.
for i, _nc := 0, int(defNode.NamedChildCount()); i < _nc; i++ {
c := defNode.NamedChild(i)
if c != nil && c.Type() == "modifiers" {
mods = c
break
}
}
}
if mods == nil {
return
}
for i, _nc := 0, int(mods.NamedChildCount()); i < _nc; i++ {
attr := mods.NamedChild(i)
if attr == nil || attr.Type() != "attribute" {
continue
}
name, args := swiftAttributeNameAndArgs(attr, src)
if name == "" {
continue
}
line := int(attr.StartPoint().Row) + 1
EmitAnnotationEdge(fromID, "swift", name, args, filePath, line, result, seen)
}
}
// swiftAttributeNameAndArgs reads an `attribute` AST node and returns
// (name, args). The name comes from the first `user_type` /
// `type_identifier` child (Swift's grammar wraps attribute names in a
// type position). Arguments come from any remaining named children
// joined by ", " — the verbatim form is preserved so route paths and
// availability shims stay queryable.
//
// For qualified attribute names (`@SomeModule.SomeAttr`) the trailing
// segment is returned so the synthetic annotation node groups every
// equivalent use regardless of import alias.
func swiftAttributeNameAndArgs(attr *sitter.Node, src []byte) (string, string) {
if attr == nil {
return "", ""
}
var name string
var argParts []string
for i, _nc := 0, int(attr.NamedChildCount()); i < _nc; i++ {
c := attr.NamedChild(i)
if c == nil {
continue
}
switch c.Type() {
case "user_type":
if name == "" {
name = swiftUserTypeName(c, src)
}
case "type_identifier", "simple_identifier", "identifier":
if name == "" {
name = strings.TrimSpace(c.Content(src))
} else {
argParts = append(argParts, strings.TrimSpace(c.Content(src)))
}
default:
argParts = append(argParts, strings.TrimSpace(c.Content(src)))
}
}
args := strings.Join(argParts, ", ")
return name, args
}
// swiftModifiers returns the `modifiers` child of a declaration node,
// trying the named field first and falling back to a positional scan —
// the two shapes Swift's tree-sitter grammar uses across versions.
func swiftModifiers(defNode *sitter.Node) *sitter.Node {
if defNode == nil {
return nil
}
if mods := defNode.ChildByFieldName("modifiers"); mods != nil {
return mods
}
for i, _nc := 0, int(defNode.NamedChildCount()); i < _nc; i++ {
if c := defNode.NamedChild(i); c != nil && c.Type() == "modifiers" {
return c
}
}
return nil
}
// swiftObjCAttr reports whether a declaration carries @objc and, if so,
// the explicit selector from an @objc(customSelector:) override (empty
// when @objc is bare). The explicit form is read verbatim from the
// attribute's parenthesised text so a full keyword selector
// (`@objc(moveFrom:to:)`) survives intact.
func swiftObjCAttr(defNode *sitter.Node, src []byte) (isObjC bool, explicit string) {
mods := swiftModifiers(defNode)
if mods == nil {
return false, ""
}
for i, _nc := 0, int(mods.NamedChildCount()); i < _nc; i++ {
attr := mods.NamedChild(i)
if attr == nil || attr.Type() != "attribute" {
continue
}
name, _ := swiftAttributeNameAndArgs(attr, src)
if name != "objc" {
continue
}
isObjC = true
text := attr.Content(src)
if open := strings.IndexByte(text, '('); open >= 0 {
if closeIdx := strings.LastIndexByte(text, ')'); closeIdx > open {
explicit = strings.TrimSpace(text[open+1 : closeIdx])
}
}
return isObjC, explicit
}
return false, ""
}
// swiftObjCSelector computes the Objective-C selector a Swift method is
// exposed under when it carries @objc, or "" when it is not @objc. An
// explicit @objc("sel:") override wins; otherwise the selector is derived
// from the method's base name and argument labels per Swift's bridging
// rules: the first argument label folds into the base name capitalised
// (`move(from:to:)` → `moveFrom:to:`), `init` inserts "With"
// (`init(frame:)` → `initWithFrame:`), an omitted label (`_`) contributes
// a bare colon (`insertSubview(_:at:)` → `insertSubview:at:`), and a
// no-argument method keeps its bare name (`viewDidLoad`).
func swiftObjCSelector(defNode *sitter.Node, baseName string, src []byte) string {
isObjC, explicit := swiftObjCAttr(defNode, src)
if !isObjC {
return ""
}
if explicit != "" {
return explicit
}
return buildSwiftObjCSelector(baseName, swiftArgLabels(swiftParamClause(defNode, src)))
}
// swiftObjCPropertySelectors computes the Objective-C accessor selectors an
// @objc property is exposed under: the getter is the property name (or an
// explicit @objc(custom) override), and a mutable `var` also exposes a
// `set<Name>:` setter. Returns ("", "") when the property is not @objc, and an
// empty setter for an immutable `let`.
func swiftObjCPropertySelectors(defNode *sitter.Node, name string, mutable bool, src []byte) (getter, setter string) {
isObjC, explicit := swiftObjCAttr(defNode, src)
if !isObjC || name == "" {
return "", ""
}
getter = name
if explicit != "" {
getter = explicit
}
if mutable {
setter = "set" + capitalizeFirst(getter) + ":"
}
return getter, setter
}
// swiftParamClause returns the text inside a declaration's parameter
// parentheses, skipping any leading attribute parens (`@objc(x)`) by
// starting the scan after the `func` keyword.
func swiftParamClause(defNode *sitter.Node, src []byte) string {
if defNode == nil {
return ""
}
text := defNode.Content(src)
from := 0
if kw := strings.Index(text, "func"); kw >= 0 {
from = kw + len("func")
}
open := strings.IndexByte(text[from:], '(')
if open < 0 {
return ""
}
open += from
depth := 0
for i := open; i < len(text); i++ {
switch text[i] {
case '(':
depth++
case ')':
depth--
if depth == 0 {
return text[open+1 : i]
}
}
}
return ""
}
// swiftArgLabels splits a parameter clause at top-level commas and
// returns each parameter's external argument label, with "_" (omitted
// label) mapped to the empty string.
func swiftArgLabels(paramClause string) []string {
paramClause = strings.TrimSpace(paramClause)
if paramClause == "" {
return nil
}
var labels []string
for _, p := range splitSwiftTopLevelCommas(paramClause) {
p = strings.TrimSpace(p)
if p == "" {
continue
}
label := firstSwiftArgLabel(p)
if label == "_" {
label = ""
}
labels = append(labels, label)
}
return labels
}
// firstSwiftArgLabel returns the first identifier token of a parameter
// declaration — its external argument label — stripping a trailing colon
// from the single-name form (`x: Int` → "x").
func firstSwiftArgLabel(param string) string {
end := len(param)
for i, r := range param {
if r == ' ' || r == '\t' || r == ':' {
end = i
break
}
}
return param[:end]
}
// buildSwiftObjCSelector assembles the ObjC selector from a Swift base
// name and its ordered argument labels.
func buildSwiftObjCSelector(base string, labels []string) string {
if len(labels) == 0 {
return base
}
var sb strings.Builder
sb.WriteString(base)
for i, label := range labels {
switch {
case i == 0 && base == "init" && label != "":
sb.WriteString("With")
sb.WriteString(capitalizeFirst(label))
case i == 0 && label != "":
sb.WriteString(capitalizeFirst(label))
case i > 0:
sb.WriteString(label)
}
sb.WriteByte(':')
}
return sb.String()
}
// splitSwiftTopLevelCommas splits s at commas not nested inside (), <>,
// [], or {} — so a generic / closure / default-value argument stays one
// parameter.
func splitSwiftTopLevelCommas(s string) []string {
var parts []string
depth := 0
start := 0
for i := 0; i < len(s); i++ {
switch s[i] {
case '(', '<', '[', '{':
depth++
case ')', '>', ']', '}':
if depth > 0 {
depth--
}
case ',':
if depth == 0 {
parts = append(parts, s[start:i])
start = i + 1
}
}
}
parts = append(parts, s[start:])
return parts
}
// capitalizeFirst upper-cases the first ASCII letter of s.
func capitalizeFirst(s string) string {
if s == "" {
return s
}
if s[0] >= 'a' && s[0] <= 'z' {
return string(s[0]-'a'+'A') + s[1:]
}
return s
}
// swiftUserTypeName pulls the trailing `type_identifier` out of a
// `user_type` chain (`Foo.Bar.Baz` → "Baz") so qualified annotation
// references collapse onto the same synthetic node.
func swiftUserTypeName(node *sitter.Node, src []byte) string {
if node == nil {
return ""
}
// Walk forward and remember the last type_identifier; Swift's
// user_type nests left-to-right so the last identifier is the
// leaf.
var last string
for i, _nc := 0, int(node.NamedChildCount()); i < _nc; i++ {
c := node.NamedChild(i)
if c == nil {
continue
}
if c.Type() == "type_identifier" {
last = strings.TrimSpace(c.Content(src))
}
if c.Type() == "user_type" {
if inner := swiftUserTypeName(c, src); inner != "" {
last = inner
}
}
}
if last == "" {
// Fallback: take the verbatim content and slice on the last
// `.` separator so qualified names still surface a leaf.
text := strings.TrimSpace(node.Content(src))
if idx := strings.LastIndex(text, "."); idx >= 0 {
text = text[idx+1:]
}
last = text
}
return last
}
// swiftHasAttr reports whether a declaration carries the named attribute
// (e.g. "objc", "nonobjc", "objcMembers") among its modifiers.
func swiftHasAttr(defNode *sitter.Node, attrName string, src []byte) bool {
mods := swiftModifiers(defNode)
if mods == nil {
return false
}
for i, _nc := 0, int(mods.NamedChildCount()); i < _nc; i++ {
attr := mods.NamedChild(i)
if attr == nil || attr.Type() != "attribute" {
continue
}
if name, _ := swiftAttributeNameAndArgs(attr, src); name == attrName {
return true
}
}
return false
}
// swiftObjCSelectorExposed computes the Objective-C selector a Swift method
// is exposed under, accounting for an @objcMembers class that exposes every
// member implicitly. The member's own @objc wins; otherwise, when the
// enclosing class is @objcMembers and the member is not opted out with
// @nonobjc, the selector is derived as if the member were @objc. Returns ""
// when the member is not exposed to Objective-C.
func swiftObjCSelectorExposed(defNode *sitter.Node, baseName string, classObjCMembers bool, src []byte) string {
if sel := swiftObjCSelector(defNode, baseName, src); sel != "" {
return sel
}
if classObjCMembers && !swiftHasAttr(defNode, "nonobjc", src) {
return buildSwiftObjCSelector(baseName, swiftArgLabels(swiftParamClause(defNode, src)))
}
return ""
}
// swiftObjCPropertySelectorsExposed mirrors swiftObjCPropertySelectors but
// also exposes a property implicitly when its enclosing class is
// @objcMembers and the property is not opted out with @nonobjc.
func swiftObjCPropertySelectorsExposed(defNode *sitter.Node, name string, mutable, classObjCMembers bool, src []byte) (getter, setter string) {
if g, s := swiftObjCPropertySelectors(defNode, name, mutable, src); g != "" {
return g, s
}
if name != "" && classObjCMembers && !swiftHasAttr(defNode, "nonobjc", src) {
getter = name
if mutable {
setter = "set" + capitalizeFirst(getter) + ":"
}
return getter, setter
}
return "", ""
}