Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions scanner/astgrep.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,13 @@ func (s *AstGrepScanner) scanDirectory(parent context.Context, root string) ([]F
if pathVar, ok := m.MetaVariables.Single["PATH"]; ok {
path = pathVar.Text
}
if len(expandRustUsePaths(path)) > 0 || strings.ContainsAny(path, "{}") {
// Brace trees that fail to expand (comments in the group,
// empty groups, external groups) stay rust-use: the
// resolver emits nothing instead of rust-path splitting
// the raw braces into a false crate-root edge.
kind = "rust-use"
}
case "rust-askama-template-imports":
kind = "rust-askama-template"
if targetVar, ok := m.MetaVariables.Single["TARGET"]; ok && !rustAttributeAssigns(m.Text, "config") {
Expand Down
5 changes: 4 additions & 1 deletion scanner/cancellation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,16 @@ exec sleep 10
}
time.Sleep(5 * time.Millisecond)
}
// Anchor the termination budget to the deadline: it runs concurrently
// with the deadline, so a bare budget leaves only the spawn latency
// of slack after the deadline fires.
const terminateBudget = 5 * time.Second
select {
case err := <-done:
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("caller-deadline ScanDirectory error = %v, want context.DeadlineExceeded", err)
}
case <-time.After(terminateBudget):
case <-time.After(scanDeadline + terminateBudget):
t.Fatal("ScanDirectory did not terminate deadline-exceeded subprocess")
}
if elapsed := time.Since(started); elapsed > scanDeadline+terminateBudget {
Expand Down
6 changes: 6 additions & 0 deletions scanner/rustgraph.go
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,12 @@ func resolveRustReferences(root string, analysis FileAnalysis, idx *fileIndex, w
if target := resolveRustModule(ref.Path, analysis.Path, idx, workspace); target != "" && target != analysis.Path {
resolved = append(resolved, target)
}
case "rust-use":
for _, path := range expandRustUsePaths(ref.Path) {
if target := resolveRustPath(path, analysis.Path, idx, workspace); target != "" && target != analysis.Path {
resolved = append(resolved, target)
}
}
case "rust-path":
if target := resolveRustPath(ref.Path, analysis.Path, idx, workspace); target != "" && target != analysis.Path {
resolved = append(resolved, target)
Expand Down
217 changes: 217 additions & 0 deletions scanner/rustuse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package scanner

import (
"strings"
"unicode"
)

const maxRustUseTreeDepth = 64

func expandRustUsePaths(path string) []string {
path = strings.TrimSpace(path)
root := path
if end := strings.IndexAny(root, ":{ \r\n"); end >= 0 {
root = root[:end]
}
if root != "crate" && root != "self" && root != "super" {
return nil
}

paths, ok := expandRustUseTree(path, "", 0)
if !ok {
return nil
}
return dedupe(paths)
}

func expandRustUseTree(tree, prefix string, depth int) ([]string, bool) {
if depth > maxRustUseTreeDepth {
return nil, false
}
tree = strings.TrimSpace(tree)
if tree == "" {
return nil, false
}

var ok bool
tree, ok = trimRustUseAlias(tree)
if !ok {
return nil, false
}

open := strings.IndexByte(tree, '{')
if open < 0 {
return expandRustUseLeaf(tree, prefix)
}

close, ok := matchingRustUseBrace(tree, open)
if !ok || strings.TrimSpace(tree[close+1:]) != "" {
return nil, false
}
head := strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(tree[:open]), "::"))
if strings.ContainsAny(head, "{},") {
return nil, false
}
if head != "" {
prefix = joinRustUsePath(prefix, head)
}
if prefix == "" {
return nil, false
}

items, ok := splitRustUseItems(tree[open+1 : close])
if !ok {
return nil, false
}
var paths []string
for _, item := range items {
expanded, ok := expandRustUseTree(item, prefix, depth+1)
if !ok {
return nil, false
}
paths = append(paths, expanded...)
}
return paths, true
}

func expandRustUseLeaf(leaf, prefix string) ([]string, bool) {
if strings.ContainsAny(leaf, "{},") {
return nil, false
}
if strings.HasSuffix(leaf, "::*") {
leaf = strings.TrimSuffix(leaf, "::*")
} else if leaf == "self" || leaf == "*" {
leaf = ""
}
path := joinRustUsePath(prefix, leaf)
if !validRustUsePath(path) {
return nil, false
}
return []string{path}, true
}

func trimRustUseAlias(tree string) (string, bool) {
depth := 0
for i := 0; i < len(tree); i++ {
switch tree[i] {
case '{':
depth++
case '}':
depth--
if depth < 0 {
return "", false
}
default:
if depth == 0 && i+2 <= len(tree) && tree[i:i+2] == "as" &&
(i == 0 || isRustUseSpace(tree[i-1])) &&
(i+2 == len(tree) || isRustUseSpace(tree[i+2])) {
path := strings.TrimSpace(tree[:i])
alias := strings.TrimSpace(tree[i+2:])
if path == "" || !validRustUseIdentifier(alias) {
return "", false
}
return path, true
}
}
}
return tree, depth == 0
}

func matchingRustUseBrace(tree string, open int) (int, bool) {
depth := 0
for i := open; i < len(tree); i++ {
switch tree[i] {
case '{':
depth++
case '}':
depth--
if depth == 0 {
return i, true
}
if depth < 0 {
return 0, false
}
}
}
return 0, false
}

func splitRustUseItems(body string) ([]string, bool) {
depth := 0
start := 0
var items []string
for i := 0; i < len(body); i++ {
switch body[i] {
case '{':
depth++
case '}':
depth--
if depth < 0 {
return nil, false
}
case ',':
if depth == 0 {
item := strings.TrimSpace(body[start:i])
if item == "" {
return nil, false
}
items = append(items, item)
start = i + 1
}
}
}
if depth != 0 {
return nil, false
}
if item := strings.TrimSpace(body[start:]); item != "" {
items = append(items, item)
} else if len(items) == 0 {
return nil, false
}
return items, true
}

func joinRustUsePath(prefix, path string) string {
path = strings.TrimSpace(strings.TrimPrefix(path, "::"))
if prefix == "" {
return path
}
if path == "" {
return prefix
}
return prefix + "::" + path
}

func validRustUsePath(path string) bool {
parts := strings.Split(path, "::")
if len(parts) == 0 {
return false
}
for _, part := range parts {
if !validRustUseIdentifier(part) {
return false
}
}
return true
}

func validRustUseIdentifier(value string) bool {
value = strings.TrimPrefix(value, "r#")
if value == "" {
return false
}
for i, r := range value {
if i == 0 {
if r != '_' && !unicode.IsLetter(r) {
return false
}
} else if r != '_' && !unicode.IsLetter(r) && !unicode.IsDigit(r) {
return false
}
}
return true
}

func isRustUseSpace(value byte) bool {
return value == ' ' || value == '\t' || value == '\r' || value == '\n'
}
Loading
Loading