fallow_extract/
graphql.rs1use std::path::Path;
7use std::sync::LazyLock;
8
9use oxc_span::Span;
10
11use crate::{ImportInfo, ImportedName, ModuleInfo};
12use fallow_types::discover::FileId;
13
14static GRAPHQL_IMPORT_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
15 regex::Regex::new(r#"(?m)^[ \t]*#\s*import\s+["']([^"'\r\n]+)["']"#).expect("valid regex")
16});
17
18pub(crate) fn is_graphql_file(path: &Path) -> bool {
19 path.extension()
20 .and_then(|e| e.to_str())
21 .is_some_and(|ext| ext == "graphql" || ext == "gql")
22}
23
24fn is_relative_graphql_import(source: &str) -> bool {
25 source.starts_with("./") || source.starts_with("../")
26}
27
28#[expect(
29 clippy::cast_possible_truncation,
30 reason = "source spans are bounded by source file size, which is practically below u32::MAX"
31)]
32fn span_from_usize(start: usize, end: usize) -> Span {
33 Span::new(start as u32, end as u32)
34}
35
36#[must_use]
37pub(crate) fn extract_graphql_imports(source: &str) -> Vec<ImportInfo> {
38 let mut imports = Vec::new();
39
40 for cap in GRAPHQL_IMPORT_RE.captures_iter(source) {
41 let Some(source_match) = cap.get(1) else {
42 continue;
43 };
44 let import_source = source_match.as_str().trim();
45 if import_source.is_empty() || !is_relative_graphql_import(import_source) {
46 continue;
47 }
48
49 imports.push(ImportInfo {
50 source: import_source.to_string(),
51 imported_name: ImportedName::SideEffect,
52 local_name: String::new(),
53 is_type_only: false,
54 from_style: false,
55 span: cap
56 .get(0)
57 .map_or_else(Span::default, |m| span_from_usize(m.start(), m.end())),
58 source_span: span_from_usize(source_match.start(), source_match.end()),
59 });
60 }
61
62 imports.sort_unstable_by(|a, b| {
63 a.source
64 .cmp(&b.source)
65 .then(a.source_span.start.cmp(&b.source_span.start))
66 });
67 imports.dedup_by(|a, b| a.source == b.source);
68 imports
69}
70
71pub(crate) fn parse_graphql_to_module(
72 file_id: FileId,
73 source: &str,
74 content_hash: u64,
75) -> ModuleInfo {
76 ModuleInfo {
77 file_id,
78 exports: Vec::new(),
79 imports: extract_graphql_imports(source),
80 re_exports: Vec::new(),
81 dynamic_imports: Vec::new(),
82 dynamic_import_patterns: Vec::new(),
83 require_calls: Vec::new(),
84 member_accesses: Vec::new(),
85 whole_object_uses: Vec::new(),
86 has_cjs_exports: false,
87 content_hash,
88 suppressions: crate::suppress::parse_suppressions_from_source(source),
89 unused_import_bindings: Vec::new(),
90 type_referenced_import_bindings: Vec::new(),
91 value_referenced_import_bindings: Vec::new(),
92 line_offsets: fallow_types::extract::compute_line_offsets(source),
93 complexity: Vec::new(),
94 flag_uses: Vec::new(),
95 class_heritage: Vec::new(),
96 local_type_declarations: Vec::new(),
97 public_signature_type_references: Vec::new(),
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn graphql_file_extensions_are_supported() {
107 assert!(is_graphql_file(Path::new("schema.graphql")));
108 assert!(is_graphql_file(Path::new("fragment.gql")));
109 assert!(!is_graphql_file(Path::new("query.ts")));
110 }
111
112 #[test]
113 fn extracts_relative_hash_imports() {
114 let imports = extract_graphql_imports(
115 r#"
116 #import "./content.graphql"
117 # import '../shared/leaf.gql'
118 #import "package/schema.graphql"
119 fragment Story on Story { id }
120 "#,
121 );
122
123 let sources: Vec<&str> = imports
124 .iter()
125 .map(|import| import.source.as_str())
126 .collect();
127 assert_eq!(sources, vec!["../shared/leaf.gql", "./content.graphql"]);
128 assert!(
129 imports
130 .iter()
131 .all(|import| matches!(import.imported_name, ImportedName::SideEffect))
132 );
133 }
134
135 #[test]
136 fn parse_graphql_to_module_sets_imports_and_offsets() {
137 let info = parse_graphql_to_module(
138 FileId(7),
139 "#import \"./content.graphql\"\nfragment Story on Story { id }\n",
140 42,
141 );
142
143 assert_eq!(info.file_id, FileId(7));
144 assert_eq!(info.content_hash, 42);
145 assert_eq!(info.imports.len(), 1);
146 assert_eq!(info.imports[0].source, "./content.graphql");
147 assert_eq!(info.line_offsets, vec![0, 28, 59]);
148 }
149}