1#![warn(missing_docs)]
8#![cfg_attr(not(test), deny(clippy::disallowed_methods))]
9#![cfg_attr(
10 test,
11 allow(
12 clippy::unwrap_used,
13 clippy::expect_used,
14 reason = "tests use unwrap and expect to keep fixture setup concise"
15 )
16)]
17
18pub mod alchemy;
19mod asset_url;
20pub mod astro;
21mod binding_references;
22pub mod cache;
23pub(crate) mod complexity;
24mod complexity_names;
25mod component_contracts;
26pub(crate) mod cost;
27pub mod css;
28pub mod css_classes;
29pub mod css_in_js;
30mod css_mask;
31mod css_masked_bytes;
32pub mod css_metrics;
33mod css_module_classes;
34pub mod effect_facts;
35pub mod federation_runtime;
36pub mod flags;
37mod function_body;
38pub(crate) mod function_hash;
39pub mod glimmer;
40pub(crate) mod graphql;
41pub(crate) mod html;
42pub(crate) mod iconify;
43pub mod inventory;
44pub mod js_text;
45mod jsdoc_attach;
46mod jsdoc_deprecated;
47mod literal_membership;
48pub mod mdx;
49mod module_info;
50mod module_parse;
51mod nesting;
52pub mod og_image;
53mod parse;
54mod parse_degradation;
55pub mod sfc;
56pub mod sfc_css;
57mod sfc_props;
58mod sfc_template;
59pub mod similar_code;
60mod source_map;
61mod stack;
62pub mod suppress;
63pub mod tailwind;
65pub(crate) mod template_complexity;
66mod template_expression_scan;
67mod template_usage;
68pub mod visitor;
70
71use std::path::Path;
72use std::sync::atomic::{AtomicBool, Ordering};
73
74use rayon::prelude::*;
75
76use cache::CacheStore;
77use fallow_types::discover::{DiscoveredFile, FileId};
78
79pub use fallow_types::extract::{
80 AngularComponentFieldArrayTypeFact, AngularTemplateMemberAccessFact, AngularThisSpreadFact,
81 ClassHeritageInfo, ClassThisMemberAccessFact, ClassThisWholeObjectUseFact,
82 ComputedEnumKeyUseFact, DefaultImportWholeObjectUseFact, DynamicCustomElementRenderFact,
83 DynamicImportInfo, DynamicImportPattern, ExportInfo, ExportName,
84 ExportedObjectInstancePropertyFact, FactoryCallMemberAccessFact, FactoryFnMemberAccessFact,
85 FactoryFnWholeObjectFact, FactoryPropertySource, FactoryReturnExport,
86 FactoryReturnObjectPropertyAccessFact, FactoryReturnObjectPropertySourceFact,
87 FactoryReturnObjectShapeExport, FlagPatterns, FluentChainMemberAccessFact,
88 FluentChainNewMemberAccessFact, ImportInfo, ImportedName, InstanceExportBindingFact,
89 LocalTypeDeclaration, MemberAccess, MemberInfo, MemberKind, ModuleInfo, ModuleLoadMechanism,
90 ParseResult, PlaywrightConditionalFixtureDefinitionFact, PlaywrightFixtureAliasFact,
91 PlaywrightFixtureDefinitionFact, PlaywrightFixtureTypeFact, PlaywrightFixtureUseFact,
92 PlaywrightTestBindingFact, PublicSignatureTypeReference, QualifiedClassMemberAccessFact,
93 ReExportInfo, RequireCallInfo, RequiredTypeMemberFact, SemanticFact, SourceParseDegradation,
94 SourceReadFailure, StringEnumMemberValueFact, TypeAliasSurfaceTargetFact, TypeMemberTypeEntry,
95 TypedPropertyMemberAccessFact, VisibilityTag, VitestModuleMockAction,
96 VitestModuleMockOperationFact, compute_line_offsets,
97};
98
99pub use astro::{
100 extract_astro_frontmatter, extract_astro_style_regions, extract_astro_template_regions,
101};
102pub use css::{
103 StylesheetTokens, ThemeScan, ThemeTokenDef, extract_apply_tokens, extract_apply_tokens_located,
104 extract_css_module_exports, extract_css_var_reads_located, scan_stylesheet_tokens,
105 scan_theme_blocks,
106};
107pub use css_classes::{
108 MarkupClassScan, MarkupClassToken, is_edit_distance_one, is_typo_edit, scan_markup_class_tokens,
109};
110pub use css_in_js::{
111 ConsumerQuery, CssInJsObjectSheets, CssInJsToken, CssInJsTokenDef, CssInJsTokenOrigin,
112 TokenConsumerHit, css_in_js_consumer_scan, css_in_js_object_sheets, css_in_js_theme_token_defs,
113 css_in_js_token_defs, css_in_js_virtual_stylesheet,
114};
115pub use css_metrics::{compute_css_analytics, parse_css_color_rgb};
116pub use glimmer::{is_glimmer_file, strip_glimmer_templates};
117pub use js_text::JsText;
118pub use mdx::{extract_mdx_statements, extract_mdx_statements_mapped};
119pub use nesting::{
120 MAX_NESTING_DEPTH, exceeds_nesting_limit, parse_bounded, parse_bounded_or_default,
121 parse_style_bounded,
122};
123pub use sfc::{
124 SourceRegion, extract_sfc_scripts, extract_sfc_styles, extract_sfc_template_regions,
125 is_sfc_file, sfc_merged_script_source,
126};
127pub use sfc_css::{
128 scoped_unused_classes, sfc_preprocessor_virtual_stylesheet, sfc_virtual_stylesheet,
129};
130pub use similar_code::extract_similar_code_functions;
131pub use source_map::{ExtractionResult, ScriptSection, embedded_script_sections};
132pub use stack::register_thread_stack;
133pub use tailwind::{TailwindArbitraryUse, scan_tailwind_arbitrary_values};
134
135#[expect(
136 clippy::expect_used,
137 reason = "static regex patterns are hard-coded analyzer invariants covered by extraction tests"
138)]
139fn static_regex(pattern: &str) -> regex::Regex {
140 regex::Regex::new(pattern).expect("static regex pattern should compile")
141}
142
143pub use module_parse::{parse_source_to_module, parse_source_to_module_with_flags};
144
145const BOM_CHAR: char = '\u{FEFF}';
152const PARALLEL_PARSE_FILE_THRESHOLD: usize = 32;
155
156#[must_use]
164fn strip_bom(source: &str) -> &str {
165 source.strip_prefix(BOM_CHAR).unwrap_or(source)
166}
167
168pub fn parse_all_files(
182 files: &[DiscoveredFile],
183 cache: Option<&CacheStore>,
184 need_complexity: bool,
185) -> ParseResult {
186 parse_all_files_cancellable(
187 files,
188 cache,
189 need_complexity,
190 None,
191 &FlagPatterns::default(),
192 )
193}
194
195pub fn parse_all_files_cancellable(
207 files: &[DiscoveredFile],
208 cache: Option<&CacheStore>,
209 need_complexity: bool,
210 cancellation: Option<&AtomicBool>,
211 flag_patterns: &FlagPatterns,
212) -> ParseResult {
213 let parse_one = |file: &DiscoveredFile| {
214 if cancellation.is_some_and(|cancelled| cancelled.load(Ordering::SeqCst)) {
215 return ParseFileResult::default();
216 }
217 parse_single_file_cached(file, cache, need_complexity, flag_patterns)
218 };
219 let results: Vec<ParseFileResult> = if files.len() <= PARALLEL_PARSE_FILE_THRESHOLD {
220 files.iter().map(parse_one).collect()
221 } else {
222 files.par_iter().map(parse_one).collect()
223 };
224
225 let mut modules = Vec::with_capacity(results.len());
226 let mut read_failures = Vec::new();
227 let mut parse_degradations = Vec::new();
228 let mut hits = 0usize;
229 let mut misses = 0usize;
230 let mut parse_cpu_nanos = 0u64;
231 let mut files_read = 0u64;
232 let mut source_bytes_read = 0u64;
233 let mut css_masked_bytes = 0u64;
234
235 for (file, result) in files.iter().zip(results) {
238 hits += result.cache_hits;
239 misses += result.cache_misses;
240 parse_cpu_nanos = parse_cpu_nanos.saturating_add(result.parse_cpu_nanos);
241 if let Some(bytes) = result.source_bytes_read {
242 files_read += 1;
243 source_bytes_read += bytes;
244 }
245 css_masked_bytes += result.css_masked_bytes;
246 if let Some(module) = result.module {
247 if module.parse_error_count > 0 {
248 parse_degradations.push(SourceParseDegradation {
249 file_id: module.file_id,
250 path: file.path.clone(),
251 error_count: module.parse_error_count,
252 panicked: module.parse_panicked,
253 nesting_limit: module.parse_nesting_too_deep.then_some(MAX_NESTING_DEPTH),
254 });
255 }
256 modules.push(module);
257 }
258 if let Some(failure) = result.read_failure {
259 read_failures.push(failure);
260 }
261 }
262
263 if hits > 0 || misses > 0 {
264 tracing::info!(
265 cache_hits = hits,
266 cache_misses = misses,
267 "incremental cache stats"
268 );
269 }
270
271 ParseResult {
272 modules,
273 read_failures,
274 parse_degradations,
275 cache_hits: hits,
276 cache_misses: misses,
277 parse_cpu_ms: parse_cpu_nanos as f64 / 1_000_000.0,
278 files_read,
279 source_bytes_read,
280 css_masked_bytes,
281 }
282}
283
284#[derive(Default)]
285struct ParseFileResult {
286 module: Option<ModuleInfo>,
287 read_failure: Option<SourceReadFailure>,
288 cache_hits: usize,
289 cache_misses: usize,
290 parse_cpu_nanos: u64,
291 source_bytes_read: Option<u64>,
294 css_masked_bytes: u64,
296}
297
298impl ParseFileResult {
299 fn cache_hit(module: ModuleInfo) -> Self {
300 Self {
301 module: Some(module),
302 read_failure: None,
303 cache_hits: 1,
304 cache_misses: 0,
305 parse_cpu_nanos: 0,
306 source_bytes_read: None,
307 css_masked_bytes: 0,
308 }
309 }
310
311 fn cache_miss(module: ModuleInfo, parse_cpu_nanos: u64) -> Self {
312 Self {
313 module: Some(module),
314 read_failure: None,
315 cache_hits: 0,
316 cache_misses: 1,
317 parse_cpu_nanos,
318 source_bytes_read: None,
319 css_masked_bytes: 0,
320 }
321 }
322
323 const fn with_source_bytes_read(mut self, bytes: usize) -> Self {
324 self.source_bytes_read = Some(bytes as u64);
325 self
326 }
327
328 fn read_failure(file: &DiscoveredFile, error: &std::io::Error) -> Self {
329 Self {
330 module: None,
331 read_failure: Some(SourceReadFailure {
332 file_id: file.id,
333 path: file.path.clone(),
334 error: error.to_string(),
335 }),
336 cache_hits: 0,
337 cache_misses: 0,
338 parse_cpu_nanos: 0,
339 source_bytes_read: None,
340 css_masked_bytes: 0,
341 }
342 }
343}
344
345fn parse_single_file_cached(
371 file: &DiscoveredFile,
372 cache: Option<&CacheStore>,
373 need_complexity: bool,
374 flag_patterns: &FlagPatterns,
375) -> ParseFileResult {
376 let cached_by_path = cache.and_then(|store| store.get_by_path_only(&file.path));
377
378 if let Some(cached) = cached_by_path
379 && cached.file_size == file.size_bytes
380 {
381 let source_file = match std::fs::File::open(&file.path) {
382 Ok(source_file) => source_file,
383 Err(error) => return ParseFileResult::read_failure(file, &error),
384 };
385 if let Ok(metadata) = source_file.metadata()
386 && metadata.len() == cached.file_size
387 {
388 let fingerprint =
389 fallow_types::source_fingerprint::SourceFingerprint::from_metadata(&metadata);
390 if cached.source_fingerprint() == fingerprint
391 && fingerprint.is_trustworthy_without_content()
392 && (!need_complexity || cached.complexity_extracted)
393 {
394 return ParseFileResult::cache_hit(cache::cached_to_module_opts(
395 cached,
396 file.id,
397 need_complexity,
398 ));
399 }
400 }
401 }
402
403 let raw = match std::fs::read_to_string(&file.path) {
404 Ok(raw) => raw,
405 Err(error) => return ParseFileResult::read_failure(file, &error),
406 };
407 let source = strip_bom(&raw);
408 let content_hash = xxhash_rust::xxh3::xxh3_64(source.as_bytes());
409
410 if let Some(cached) = cached_by_path
411 && cached.content_hash == content_hash
412 && (!need_complexity || cached.complexity_extracted)
413 {
414 return ParseFileResult::cache_hit(cache::cached_to_module_opts(
415 cached,
416 file.id,
417 need_complexity,
418 ))
419 .with_source_bytes_read(raw.len());
420 }
421
422 let parse_start = std::time::Instant::now();
423 css_masked_bytes::take();
425 let module = parse_source_to_module_with_flags(
426 file.id,
427 &file.path,
428 source,
429 content_hash,
430 need_complexity,
431 flag_patterns,
432 );
433 let parse_cpu_nanos = u64::try_from(parse_start.elapsed().as_nanos()).unwrap_or(u64::MAX);
434 let mut result =
435 ParseFileResult::cache_miss(module, parse_cpu_nanos).with_source_bytes_read(raw.len());
436 result.css_masked_bytes = css_masked_bytes::take();
437 result
438}
439
440#[must_use]
442pub fn parse_single_file(file: &DiscoveredFile) -> Option<ModuleInfo> {
443 let raw = std::fs::read_to_string(&file.path).ok()?;
444 let source = strip_bom(&raw);
445 let content_hash = xxhash_rust::xxh3::xxh3_64(source.as_bytes());
446 Some(parse_source_to_module(
447 file.id,
448 &file.path,
449 source,
450 content_hash,
451 false,
452 ))
453}
454
455#[must_use]
457pub fn parse_from_content(file_id: FileId, path: &Path, content: &str) -> ModuleInfo {
458 let content = strip_bom(content);
459 let content_hash = xxhash_rust::xxh3::xxh3_64(content.as_bytes());
460 parse_source_to_module(file_id, path, content, content_hash, true)
461}
462
463#[cfg(all(test, not(miri)))]
464mod tests;