Skip to main content

_diffctx/edges/semantic/
graphql.rs

1use std::path::{Path, PathBuf};
2
3use once_cell::sync::Lazy;
4use regex::Regex;
5use rustc_hash::{FxHashMap, FxHashSet};
6
7use crate::config::weights::EDGE_WEIGHTS;
8use crate::types::Fragment;
9
10use super::super::EdgeDict;
11use super::super::base::{self, EdgeBuilder, add_edge, add_edges_from_ids, discover_files_by_refs};
12
13fn is_graphql_file(path: &Path) -> bool {
14    let ext = base::file_ext(path);
15    ext == ".graphql" || ext == ".gql"
16}
17
18static TYPE_DEF_RE: Lazy<Regex> = Lazy::new(|| {
19    Regex::new(r"(?m)^\s*(?:type|input|interface|enum|union|scalar)\s+(\w+)").unwrap()
20});
21static EXTEND_RE: Lazy<Regex> = Lazy::new(|| {
22    Regex::new(r"(?m)^\s*extend\s+(?:type|input|interface|enum|union)\s+(\w+)").unwrap()
23});
24static FIELD_TYPE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r":\s*\[?([A-Z]\w+)").unwrap());
25static IMPLEMENTS_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"implements\s+([\w\s&]+)").unwrap());
26static UNION_MEMBERS_RE: Lazy<Regex> =
27    Lazy::new(|| Regex::new(r"union\s+\w+\s*=\s*([\w\s|]+)").unwrap());
28
29static GQL_BUILTINS: Lazy<FxHashSet<&str>> = Lazy::new(|| {
30    ["String", "Int", "Float", "Boolean", "ID"]
31        .iter()
32        .copied()
33        .collect()
34});
35
36fn extract_defs(content: &str) -> FxHashSet<String> {
37    TYPE_DEF_RE
38        .captures_iter(content)
39        .map(|c| c[1].to_string())
40        .collect()
41}
42
43fn extract_type_refs(content: &str) -> FxHashSet<String> {
44    let mut refs: FxHashSet<String> = FIELD_TYPE_RE
45        .captures_iter(content)
46        .map(|c| c[1].to_string())
47        .filter(|n| !GQL_BUILTINS.contains(n.as_str()))
48        .collect();
49    for cap in IMPLEMENTS_RE.captures_iter(content) {
50        for part in cap[1].split('&') {
51            let name = part.trim();
52            if !name.is_empty() {
53                refs.insert(name.to_string());
54            }
55        }
56    }
57    for cap in UNION_MEMBERS_RE.captures_iter(content) {
58        for part in cap[1].split('|') {
59            let name = part.trim();
60            if !name.is_empty() {
61                refs.insert(name.to_string());
62            }
63        }
64    }
65    refs
66}
67
68fn extract_extends(content: &str) -> FxHashSet<String> {
69    EXTEND_RE
70        .captures_iter(content)
71        .map(|c| c[1].to_string())
72        .collect()
73}
74
75pub struct GraphqlEdgeBuilder;
76
77impl EdgeBuilder for GraphqlEdgeBuilder {
78    fn build(&self, fragments: &[Fragment], _repo_root: Option<&Path>) -> EdgeDict {
79        let frags: Vec<&Fragment> = fragments
80            .iter()
81            .filter(|f| is_graphql_file(Path::new(f.path())))
82            .collect();
83        if frags.is_empty() {
84            return FxHashMap::default();
85        }
86
87        let type_w = EDGE_WEIGHTS["graphql_type_ref"].forward;
88        let extend_w = EDGE_WEIGHTS["graphql_extend"].forward;
89        let reverse_factor = EDGE_WEIGHTS["graphql_type_ref"].reverse_factor;
90
91        let mut name_to_defs: FxHashMap<String, Vec<_>> = FxHashMap::default();
92        for f in &frags {
93            for name in extract_defs(&f.content) {
94                name_to_defs
95                    .entry(name.to_lowercase())
96                    .or_default()
97                    .push(f.id.clone());
98            }
99        }
100
101        let mut edges: EdgeDict = FxHashMap::default();
102
103        for f in &frags {
104            let self_defs = extract_defs(&f.content);
105            for ext_name in extract_extends(&f.content) {
106                if let Some(targets) = name_to_defs.get(&ext_name.to_lowercase()) {
107                    add_edges_from_ids(&mut edges, &f.id, targets, extend_w, reverse_factor);
108                }
109            }
110            for tref in extract_type_refs(&f.content) {
111                if self_defs.contains(&tref) {
112                    continue;
113                }
114                if let Some(targets) = name_to_defs.get(&tref.to_lowercase()) {
115                    for t in targets {
116                        if t != &f.id {
117                            add_edge(&mut edges, &f.id, t, type_w, reverse_factor);
118                        }
119                    }
120                }
121            }
122        }
123        edges
124    }
125
126    fn discover_related_files(
127        &self,
128        changed: &[PathBuf],
129        candidates: &[PathBuf],
130        repo_root: Option<&Path>,
131        file_cache: Option<&FxHashMap<PathBuf, String>>,
132    ) -> Vec<PathBuf> {
133        let gql_changed: Vec<&PathBuf> = changed.iter().filter(|f| is_graphql_file(f)).collect();
134        if gql_changed.is_empty() {
135            return vec![];
136        }
137        let mut refs = FxHashSet::default();
138        for f in &gql_changed {
139            if let Some(content) = base::read_file_cached(f, file_cache) {
140                refs.extend(extract_type_refs(&content));
141                refs.extend(extract_extends(&content));
142            }
143        }
144        discover_files_by_refs(&refs, changed, candidates, repo_root)
145    }
146}