1use crate::model::{SourceLocation, StepDefinition};
4use crate::resource_limits::{read_utf8, MAX_PROJECT_INPUT_BYTES};
5use anyhow::{Context, Result};
6use std::any::{Any, TypeId};
7use std::collections::HashMap;
8use std::panic::{RefUnwindSafe, UnwindSafe};
9use std::path::{Path, PathBuf};
10
11pub(crate) const UNRESOLVED_REGISTRATION_DIAGNOSTIC_PREFIX: &str =
12 "unresolved step-registration calls:";
13
14#[non_exhaustive]
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum SourceLanguage {
18 JavaScript,
20 TypeScript,
22 Tsx,
24}
25
26pub(crate) fn grammar_for_language(language: SourceLanguage) -> tree_sitter::Language {
28 match language {
29 SourceLanguage::JavaScript => tree_sitter_javascript::LANGUAGE.into(),
30 SourceLanguage::TypeScript => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
31 SourceLanguage::Tsx => tree_sitter_typescript::LANGUAGE_TSX.into(),
32 }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct SourceFile {
38 pub path: PathBuf,
40 pub language: SourceLanguage,
42}
43
44#[non_exhaustive]
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum ExtractionDiagnosticLevel {
48 Warning,
50 Error,
52}
53
54#[non_exhaustive]
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct ExtractionDiagnostic {
58 pub level: ExtractionDiagnosticLevel,
60 pub location: SourceLocation,
62 pub message: String,
64}
65
66impl ExtractionDiagnostic {
67 pub fn new(
69 level: ExtractionDiagnosticLevel,
70 location: SourceLocation,
71 message: impl Into<String>,
72 ) -> Self {
73 Self {
74 level,
75 location,
76 message: message.into(),
77 }
78 }
79}
80
81pub(crate) fn is_completeness_diagnostic(diagnostic: &ExtractionDiagnostic) -> bool {
82 diagnostic
83 .message
84 .starts_with(UNRESOLVED_REGISTRATION_DIAGNOSTIC_PREFIX)
85}
86
87#[non_exhaustive]
89#[derive(Debug, Clone, Default, PartialEq)]
90pub struct Extraction {
91 pub definitions: Vec<StepDefinition>,
93 pub diagnostics: Vec<ExtractionDiagnostic>,
95}
96
97impl Extraction {
98 pub fn new(definitions: Vec<StepDefinition>, diagnostics: Vec<ExtractionDiagnostic>) -> Self {
100 Self {
101 definitions,
102 diagnostics,
103 }
104 }
105}
106
107#[derive(Default)]
112pub struct SourceExtractionSession {
113 root: Option<PathBuf>,
114 registrations: Vec<String>,
115 assertion_modules: Vec<String>,
116 states: HashMap<TypeId, Box<dyn Any + Send + Sync + UnwindSafe + RefUnwindSafe>>,
117}
118
119pub(crate) trait AdapterSessionState:
122 Any + Send + Sync + UnwindSafe + RefUnwindSafe
123{
124 fn initialize(
125 root: Option<&Path>,
126 registrations: &[String],
127 assertion_modules: &[String],
128 ) -> Self;
129}
130
131pub(crate) trait StatefulSourceAdapter: SourceAdapter {
133 type State: AdapterSessionState;
134}
135
136impl SourceExtractionSession {
137 pub fn new(root: &Path) -> Self {
139 Self::with_registrations(root, &[])
140 }
141
142 pub fn with_registrations(root: &Path, registrations: &[String]) -> Self {
147 Self::with_options(root, registrations, &[])
148 }
149
150 pub fn with_options(
156 root: &Path,
157 registrations: &[String],
158 assertion_modules: &[String],
159 ) -> Self {
160 let mut session = Self {
161 root: Some(root.to_owned()),
162 registrations: registrations.to_vec(),
163 assertion_modules: assertion_modules.to_vec(),
164 ..Self::default()
165 };
166 for registration in SOURCE_ADAPTER_REGISTRY {
168 if let Some(initialize) = registration.initialize_session {
169 initialize(&mut session);
170 }
171 }
172 session
173 }
174
175 pub(crate) fn initialize<T: AdapterSessionState>(&mut self) {
176 self.states.entry(TypeId::of::<T>()).or_insert_with(|| {
177 Box::new(T::initialize(
178 self.root.as_deref(),
179 &self.registrations,
180 &self.assertion_modules,
181 ))
182 });
183 }
184
185 pub(crate) fn state<T: AdapterSessionState>(&mut self) -> Result<&mut T> {
186 if self.root.is_none() {
188 self.initialize::<T>();
189 }
190 self.states
191 .get_mut(&TypeId::of::<T>())
192 .and_then(|state| {
193 let state: &mut dyn Any = state.as_mut();
194 state.downcast_mut::<T>()
195 })
196 .with_context(|| {
197 format!(
198 "source adapter state {} is uninitialized or incompatible",
199 std::any::type_name::<T>()
200 )
201 })
202 }
203}
204
205pub trait SourceAdapter: Sync {
207 fn name(&self) -> &'static str;
209
210 fn language(&self) -> SourceLanguage;
212
213 fn extract(&self, source: &str, file: &SourceFile) -> Result<Extraction>;
215
216 fn extract_with_session(
218 &self,
219 source: &str,
220 file: &SourceFile,
221 _session: &mut SourceExtractionSession,
222 ) -> Result<Extraction> {
223 self.extract(source, file)
224 }
225
226 fn extract_file(&self, file: &SourceFile) -> Result<Extraction> {
228 let source = read_utf8(&file.path, "definition source", MAX_PROJECT_INPUT_BYTES)?;
229 self.extract(&source, file)
230 }
231
232 fn extract_file_with_session(
234 &self,
235 file: &SourceFile,
236 session: &mut SourceExtractionSession,
237 ) -> Result<Extraction> {
238 let source = read_utf8(&file.path, "definition source", MAX_PROJECT_INPUT_BYTES)?;
239 self.extract_with_session(&source, file, session)
240 }
241}
242
243#[non_exhaustive]
245#[derive(Clone, Copy)]
246pub struct SourceAdapterRegistration {
247 pub suffix: &'static str,
249 pub adapter: &'static dyn SourceAdapter,
251 initialize_session: Option<fn(&mut SourceExtractionSession)>,
252}
253
254impl SourceAdapterRegistration {
255 pub const fn new(suffix: &'static str, adapter: &'static dyn SourceAdapter) -> Self {
257 Self {
258 suffix,
259 adapter,
260 initialize_session: None,
261 }
262 }
263
264 const fn with_session<A: StatefulSourceAdapter>(
265 suffix: &'static str,
266 adapter: &'static A,
267 ) -> Self {
268 Self {
269 suffix,
270 adapter,
271 initialize_session: Some(SourceExtractionSession::initialize::<A::State>),
272 }
273 }
274}
275
276macro_rules! register_source_adapters {
281 ( $( $language:ident => $adapter:path : $($suffix:literal),+ );+ ; ) => {
282 pub static SOURCE_ADAPTER_REGISTRY: &[SourceAdapterRegistration] = &[
284 $(
285 $(
286 SourceAdapterRegistration::with_session($suffix, &$adapter),
287 )+
288 )+
289 ];
290
291 pub fn adapter_for_language(language: SourceLanguage) -> &'static dyn SourceAdapter {
293 match language {
294 $(SourceLanguage::$language => &$adapter,)+
295 }
296 }
297 };
298}
299
300register_source_adapters! {
301 JavaScript => crate::typescript::JAVASCRIPT_ADAPTER : ".mjs", ".cjs", ".jsx", ".js";
302 TypeScript => crate::typescript::TYPESCRIPT_ADAPTER : ".mts", ".cts", ".ts";
303 Tsx => crate::typescript::TSX_ADAPTER : ".tsx";
304}
305
306pub fn adapter_for_path(path: &Path) -> Option<&'static dyn SourceAdapter> {
308 let extension = path.extension()?.to_str()?;
309 if extension == "map" || is_declaration_file(path, extension) {
310 return None;
311 }
312 SOURCE_ADAPTER_REGISTRY
313 .iter()
314 .find(|registration| extension == registration.suffix.trim_start_matches('.'))
315 .map(|registration| registration.adapter)
316}
317
318fn is_declaration_file(path: &Path, extension: &str) -> bool {
319 matches!(extension, "ts" | "mts" | "cts")
320 && path.file_stem().is_some_and(|stem| {
321 stem == ".d"
322 || Path::new(stem)
323 .extension()
324 .is_some_and(|extension| extension == "d")
325 })
326}
327
328pub fn language_for_path(path: &Path) -> Option<SourceLanguage> {
330 adapter_for_path(path).map(SourceAdapter::language)
331}
332
333pub fn registered_suffixes() -> Vec<&'static str> {
335 SOURCE_ADAPTER_REGISTRY
336 .iter()
337 .map(|registration| registration.suffix)
338 .collect()
339}
340
341#[cfg(test)]
342mod tests;