_diffctx/edges/semantic/
graphql.rs1use 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}