Skip to content

Commit 6416350

Browse files
fix: handle rust lifetimes in qualified path parsing
1 parent 5b4edf0 commit 6416350

2 files changed

Lines changed: 52 additions & 115 deletions

File tree

depgraph/languages/rust/parser_rust.go

Lines changed: 36 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,9 @@ func parseRustImportsFast(sourceCode []byte) ([]RustImport, bool) {
157157
continue
158158
}
159159
if c == '\'' {
160+
if isRustLifetimeStart(sourceCode, i) && !looksLikeRustCharLiteralStart(sourceCode, i) {
161+
continue
162+
}
160163
inChar = true
161164
continue
162165
}
@@ -441,6 +444,9 @@ func sanitizeRustSourceForPathMatching(sourceCode []byte) []byte {
441444
continue
442445
}
443446
if c == '\'' {
447+
if isRustLifetimeStart(sourceCode, i) && !looksLikeRustCharLiteralStart(sourceCode, i) {
448+
continue
449+
}
444450
cleaned[i] = ' '
445451
inChar = true
446452
continue
@@ -496,138 +502,53 @@ func isRustIdentChar(c byte) bool {
496502
return c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
497503
}
498504

499-
func dedupeRustImports(imports []RustImport) []RustImport {
500-
if len(imports) == 0 {
501-
return nil
502-
}
503-
seen := make(map[RustImport]bool, len(imports))
504-
result := make([]RustImport, 0, len(imports))
505-
for _, imp := range imports {
506-
if imp.Path == "" {
507-
continue
508-
}
509-
if seen[imp] {
510-
continue
511-
}
512-
seen[imp] = true
513-
result = append(result, imp)
514-
}
515-
return result
516-
}
517-
518-
func parseTopLevelRustImportStatement(stmt string) (RustImport, bool) {
519-
s := strings.TrimSpace(stmt)
520-
if s == "" {
521-
return RustImport{}, false
522-
}
523-
s = stripLeadingRustAttributes(s)
524-
if s == "" {
525-
return RustImport{}, false
505+
func isRustLifetimeStart(source []byte, idx int) bool {
506+
if idx+1 >= len(source) || !isRustIdentChar(source[idx+1]) {
507+
return false
526508
}
527509

528-
s = stripRustVisibilityPrefix(s)
529-
switch {
530-
case strings.HasPrefix(s, "use "):
531-
path := normalizeUsePath(strings.TrimSpace(strings.TrimPrefix(s, "use ")))
532-
if path == "" {
533-
return RustImport{}, false
534-
}
535-
return RustImport{Path: path, Kind: RustImportUse}, true
536-
case strings.HasPrefix(s, "extern crate "):
537-
name := leadingRustIdent(strings.TrimSpace(strings.TrimPrefix(s, "extern crate ")))
538-
if name == "" {
539-
return RustImport{}, false
540-
}
541-
return RustImport{Path: name, Kind: RustImportExternCrate}, true
542-
case strings.HasPrefix(s, "mod "):
543-
name := leadingRustIdent(strings.TrimSpace(strings.TrimPrefix(s, "mod ")))
544-
if name == "" {
545-
return RustImport{}, false
546-
}
547-
return RustImport{Path: name, Kind: RustImportModDecl}, true
510+
prev := previousNonWhitespaceByte(source, idx)
511+
switch prev {
512+
case '&', '<', '>', ',', ':', '+', '(':
513+
return true
548514
default:
549-
return RustImport{}, false
550-
}
551-
}
552-
553-
func stripLeadingRustAttributes(s string) string {
554-
trimmed := strings.TrimSpace(s)
555-
for strings.HasPrefix(trimmed, "#[") || strings.HasPrefix(trimmed, "#![") {
556-
open := strings.Index(trimmed, "[")
557-
if open < 0 {
558-
return trimmed
559-
}
560-
level := 0
561-
end := -1
562-
for i := open; i < len(trimmed); i++ {
563-
switch trimmed[i] {
564-
case '[':
565-
level++
566-
case ']':
567-
level--
568-
if level == 0 {
569-
end = i
570-
break
571-
}
572-
}
573-
}
574-
if end < 0 {
575-
return trimmed
576-
}
577-
trimmed = strings.TrimSpace(trimmed[end+1:])
515+
return false
578516
}
579-
return trimmed
580517
}
581518

582-
func stripRustVisibilityPrefix(s string) string {
583-
trimmed := strings.TrimSpace(s)
584-
if strings.HasPrefix(trimmed, "pub ") {
585-
return strings.TrimSpace(strings.TrimPrefix(trimmed, "pub "))
586-
}
587-
if strings.HasPrefix(trimmed, "pub(") {
588-
if idx := strings.Index(trimmed, ")"); idx >= 0 {
589-
return strings.TrimSpace(trimmed[idx+1:])
519+
func previousNonWhitespaceByte(source []byte, idx int) byte {
520+
for i := idx - 1; i >= 0; i-- {
521+
if !isRustWhitespace(source[i]) {
522+
return source[i]
590523
}
591524
}
592-
return trimmed
525+
return 0
593526
}
594527

595-
func normalizeUsePath(expr string) string {
596-
path := strings.TrimSpace(expr)
597-
if path == "" {
598-
return ""
599-
}
600-
if idx := strings.Index(path, " as "); idx >= 0 {
601-
path = strings.TrimSpace(path[:idx])
602-
}
603-
if idx := strings.Index(path, "{"); idx >= 0 {
604-
path = strings.TrimSpace(path[:idx])
605-
}
606-
for strings.HasSuffix(path, "::") {
607-
path = strings.TrimSuffix(path, "::")
608-
path = strings.TrimSpace(path)
528+
func looksLikeRustCharLiteralStart(source []byte, idx int) bool {
529+
if idx+2 < len(source) && source[idx+2] == '\'' {
530+
return true
609531
}
610-
path = strings.TrimPrefix(path, "::")
611-
return strings.TrimSpace(path)
532+
return idx+3 < len(source) && source[idx+1] == '\\' && source[idx+3] == '\''
612533
}
613534

614-
func leadingRustIdent(s string) string {
615-
if s == "" {
616-
return ""
535+
func dedupeRustImports(imports []RustImport) []RustImport {
536+
if len(imports) == 0 {
537+
return nil
617538
}
618-
i := 0
619-
for i < len(s) {
620-
c := s[i]
621-
if c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (i > 0 && c >= '0' && c <= '9') {
622-
i++
539+
seen := make(map[RustImport]bool, len(imports))
540+
result := make([]RustImport, 0, len(imports))
541+
for _, imp := range imports {
542+
if imp.Path == "" {
623543
continue
624544
}
625-
break
626-
}
627-
if i == 0 {
628-
return ""
545+
if seen[imp] {
546+
continue
547+
}
548+
seen[imp] = true
549+
result = append(result, imp)
629550
}
630-
return s[:i]
551+
return result
631552
}
632553

633554
func extractImports(rootNode *sitter.Node, sourceCode []byte) []RustImport {

depgraph/languages/rust/parser_rust_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,19 @@ fn run() {
106106
assert.Contains(t, imports, importKey("crate::core::do_work", RustImportUse))
107107
assert.Contains(t, imports, importKey("crate::alpha::beta", RustImportUse))
108108
}
109+
110+
func TestParseRustImports_CollectsQualifiedPathsWhenLifetimesPresent(t *testing.T) {
111+
source := `
112+
const fn marker() -> &'static str { "ok" }
113+
114+
fn run() {
115+
s7e_parser::analyze();
116+
s7e_flow::build_flow_graph();
117+
}
118+
`
119+
imports, err := ParseRustImports([]byte(source))
120+
require.NoError(t, err)
121+
122+
assert.Contains(t, imports, importKey("s7e_parser::analyze", RustImportUse))
123+
assert.Contains(t, imports, importKey("s7e_flow::build_flow_graph", RustImportUse))
124+
}

0 commit comments

Comments
 (0)