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 mod css;
27pub mod css_classes;
28pub mod css_in_js;
29pub mod css_metrics;
30pub mod effect_facts;
31pub mod federation_runtime;
32pub mod flags;
33mod function_body;
34pub mod glimmer;
35pub(crate) mod graphql;
36pub(crate) mod html;
37pub(crate) mod iconify;
38pub mod inventory;
39mod jsdoc_attach;
40mod jsdoc_deprecated;
41pub mod mdx;
42mod module_info;
43pub mod og_image;
44mod parse;
45pub mod sfc;
46pub mod sfc_css;
47mod sfc_props;
48mod sfc_template;
49pub mod similar_code;
50mod source_map;
51pub mod suppress;
52pub mod tailwind;
54pub(crate) mod template_complexity;
55mod template_expression_scan;
56mod template_usage;
57pub mod visitor;
59
60use std::path::Path;
61use std::sync::atomic::{AtomicBool, Ordering};
62
63use rayon::prelude::*;
64
65use cache::CacheStore;
66use fallow_types::discover::{DiscoveredFile, FileId};
67
68pub use fallow_types::extract::{
69 AngularComponentFieldArrayTypeFact, AngularTemplateMemberAccessFact, AngularThisSpreadFact,
70 ClassHeritageInfo, ClassThisMemberAccessFact, ClassThisWholeObjectUseFact,
71 ComputedEnumKeyUseFact, DefaultImportWholeObjectUseFact, DynamicCustomElementRenderFact,
72 DynamicImportInfo, DynamicImportPattern, ExportInfo, ExportName,
73 ExportedObjectInstancePropertyFact, FactoryCallMemberAccessFact, FactoryFnMemberAccessFact,
74 FactoryFnWholeObjectFact, FactoryPropertySource, FactoryReturnExport,
75 FactoryReturnObjectPropertyAccessFact, FactoryReturnObjectPropertySourceFact,
76 FactoryReturnObjectShapeExport, FlagPatterns, FluentChainMemberAccessFact,
77 FluentChainNewMemberAccessFact, ImportInfo, ImportedName, InstanceExportBindingFact,
78 LocalTypeDeclaration, MemberAccess, MemberInfo, MemberKind, ModuleInfo, ModuleLoadMechanism,
79 ParseResult, PlaywrightConditionalFixtureDefinitionFact, PlaywrightFixtureAliasFact,
80 PlaywrightFixtureDefinitionFact, PlaywrightFixtureTypeFact, PlaywrightFixtureUseFact,
81 PlaywrightTestBindingFact, PublicSignatureTypeReference, QualifiedClassMemberAccessFact,
82 ReExportInfo, RequireCallInfo, RequiredTypeMemberFact, SemanticFact, SourceParseDegradation,
83 SourceReadFailure, StringEnumMemberValueFact, TypeAliasSurfaceTargetFact, TypeMemberTypeEntry,
84 TypedPropertyMemberAccessFact, VisibilityTag, VitestModuleMockAction,
85 VitestModuleMockOperationFact, compute_line_offsets,
86};
87
88pub use astro::{
89 extract_astro_frontmatter, extract_astro_style_regions, extract_astro_template_regions,
90};
91pub use css::{
92 StylesheetTokens, ThemeScan, ThemeTokenDef, extract_apply_tokens, extract_apply_tokens_located,
93 extract_css_module_exports, extract_css_var_reads_located, scan_stylesheet_tokens,
94 scan_theme_blocks,
95};
96pub use css_classes::{
97 MarkupClassScan, MarkupClassToken, is_edit_distance_one, is_typo_edit, scan_markup_class_tokens,
98};
99pub use css_in_js::{
100 ConsumerQuery, CssInJsObjectSheets, CssInJsToken, CssInJsTokenDef, CssInJsTokenOrigin,
101 TokenConsumerHit, css_in_js_consumer_scan, css_in_js_object_sheets, css_in_js_theme_token_defs,
102 css_in_js_token_defs, css_in_js_virtual_stylesheet,
103};
104pub use css_metrics::{compute_css_analytics, parse_css_color_rgb};
105pub use glimmer::{is_glimmer_file, strip_glimmer_templates};
106pub use mdx::{extract_mdx_statements, extract_mdx_statements_mapped};
107pub use sfc::{
108 SourceRegion, extract_sfc_scripts, extract_sfc_styles, extract_sfc_template_regions,
109 is_sfc_file, sfc_merged_script_source,
110};
111pub use sfc_css::{
112 scoped_unused_classes, sfc_preprocessor_virtual_stylesheet, sfc_virtual_stylesheet,
113};
114pub use similar_code::extract_similar_code_functions;
115pub use source_map::{ExtractionResult, ScriptSection, embedded_script_sections};
116pub use tailwind::{TailwindArbitraryUse, scan_tailwind_arbitrary_values};
117
118#[expect(
119 clippy::expect_used,
120 reason = "static regex patterns are hard-coded analyzer invariants covered by extraction tests"
121)]
122fn static_regex(pattern: &str) -> regex::Regex {
123 regex::Regex::new(pattern).expect("static regex pattern should compile")
124}
125
126pub use parse::{parse_source_to_module, parse_source_to_module_with_flags};
127
128const BOM_CHAR: char = '\u{FEFF}';
135const PARALLEL_PARSE_FILE_THRESHOLD: usize = 32;
138
139#[must_use]
147fn strip_bom(source: &str) -> &str {
148 source.strip_prefix(BOM_CHAR).unwrap_or(source)
149}
150
151pub fn parse_all_files(
165 files: &[DiscoveredFile],
166 cache: Option<&CacheStore>,
167 need_complexity: bool,
168) -> ParseResult {
169 parse_all_files_cancellable(
170 files,
171 cache,
172 need_complexity,
173 None,
174 &FlagPatterns::default(),
175 )
176}
177
178pub fn parse_all_files_cancellable(
190 files: &[DiscoveredFile],
191 cache: Option<&CacheStore>,
192 need_complexity: bool,
193 cancellation: Option<&AtomicBool>,
194 flag_patterns: &FlagPatterns,
195) -> ParseResult {
196 let parse_one = |file: &DiscoveredFile| {
197 if cancellation.is_some_and(|cancelled| cancelled.load(Ordering::SeqCst)) {
198 return ParseFileResult::default();
199 }
200 parse_single_file_cached(file, cache, need_complexity, flag_patterns)
201 };
202 let results: Vec<ParseFileResult> = if files.len() <= PARALLEL_PARSE_FILE_THRESHOLD {
203 files.iter().map(parse_one).collect()
204 } else {
205 files.par_iter().map(parse_one).collect()
206 };
207
208 let mut modules = Vec::with_capacity(results.len());
209 let mut read_failures = Vec::new();
210 let mut parse_degradations = Vec::new();
211 let mut hits = 0usize;
212 let mut misses = 0usize;
213 let mut parse_cpu_nanos = 0u64;
214 let mut files_read = 0u64;
215 let mut source_bytes_read = 0u64;
216 let mut css_masked_bytes = 0u64;
217
218 for (file, result) in files.iter().zip(results) {
221 hits += result.cache_hits;
222 misses += result.cache_misses;
223 parse_cpu_nanos = parse_cpu_nanos.saturating_add(result.parse_cpu_nanos);
224 if let Some(bytes) = result.source_bytes_read {
225 files_read += 1;
226 source_bytes_read += bytes;
227 }
228 css_masked_bytes += result.css_masked_bytes;
229 if let Some(module) = result.module {
230 if module.parse_error_count > 0 {
231 parse_degradations.push(SourceParseDegradation {
232 file_id: module.file_id,
233 path: file.path.clone(),
234 error_count: module.parse_error_count,
235 panicked: module.parse_panicked,
236 });
237 }
238 modules.push(module);
239 }
240 if let Some(failure) = result.read_failure {
241 read_failures.push(failure);
242 }
243 }
244
245 if hits > 0 || misses > 0 {
246 tracing::info!(
247 cache_hits = hits,
248 cache_misses = misses,
249 "incremental cache stats"
250 );
251 }
252
253 ParseResult {
254 modules,
255 read_failures,
256 parse_degradations,
257 cache_hits: hits,
258 cache_misses: misses,
259 parse_cpu_ms: parse_cpu_nanos as f64 / 1_000_000.0,
260 files_read,
261 source_bytes_read,
262 css_masked_bytes,
263 }
264}
265
266#[derive(Default)]
267struct ParseFileResult {
268 module: Option<ModuleInfo>,
269 read_failure: Option<SourceReadFailure>,
270 cache_hits: usize,
271 cache_misses: usize,
272 parse_cpu_nanos: u64,
273 source_bytes_read: Option<u64>,
276 css_masked_bytes: u64,
278}
279
280impl ParseFileResult {
281 fn cache_hit(module: ModuleInfo) -> Self {
282 Self {
283 module: Some(module),
284 read_failure: None,
285 cache_hits: 1,
286 cache_misses: 0,
287 parse_cpu_nanos: 0,
288 source_bytes_read: None,
289 css_masked_bytes: 0,
290 }
291 }
292
293 fn cache_miss(module: ModuleInfo, parse_cpu_nanos: u64) -> Self {
294 Self {
295 module: Some(module),
296 read_failure: None,
297 cache_hits: 0,
298 cache_misses: 1,
299 parse_cpu_nanos,
300 source_bytes_read: None,
301 css_masked_bytes: 0,
302 }
303 }
304
305 const fn with_source_bytes_read(mut self, bytes: usize) -> Self {
306 self.source_bytes_read = Some(bytes as u64);
307 self
308 }
309
310 fn read_failure(file: &DiscoveredFile, error: &std::io::Error) -> Self {
311 Self {
312 module: None,
313 read_failure: Some(SourceReadFailure {
314 file_id: file.id,
315 path: file.path.clone(),
316 error: error.to_string(),
317 }),
318 cache_hits: 0,
319 cache_misses: 0,
320 parse_cpu_nanos: 0,
321 source_bytes_read: None,
322 css_masked_bytes: 0,
323 }
324 }
325}
326
327fn parse_single_file_cached(
353 file: &DiscoveredFile,
354 cache: Option<&CacheStore>,
355 need_complexity: bool,
356 flag_patterns: &FlagPatterns,
357) -> ParseFileResult {
358 let cached_by_path = cache.and_then(|store| store.get_by_path_only(&file.path));
359
360 if let Some(cached) = cached_by_path
361 && cached.file_size == file.size_bytes
362 {
363 let source_file = match std::fs::File::open(&file.path) {
364 Ok(source_file) => source_file,
365 Err(error) => return ParseFileResult::read_failure(file, &error),
366 };
367 if let Ok(metadata) = source_file.metadata()
368 && metadata.len() == cached.file_size
369 {
370 let fingerprint =
371 fallow_types::source_fingerprint::SourceFingerprint::from_metadata(&metadata);
372 if cached.source_fingerprint() == fingerprint
373 && fingerprint.is_trustworthy_without_content()
374 && (!need_complexity || cached.complexity_extracted)
375 {
376 return ParseFileResult::cache_hit(cache::cached_to_module_opts(
377 cached,
378 file.id,
379 need_complexity,
380 ));
381 }
382 }
383 }
384
385 let raw = match std::fs::read_to_string(&file.path) {
386 Ok(raw) => raw,
387 Err(error) => return ParseFileResult::read_failure(file, &error),
388 };
389 let source = strip_bom(&raw);
390 let content_hash = xxhash_rust::xxh3::xxh3_64(source.as_bytes());
391
392 if let Some(cached) = cached_by_path
393 && cached.content_hash == content_hash
394 && (!need_complexity || cached.complexity_extracted)
395 {
396 return ParseFileResult::cache_hit(cache::cached_to_module_opts(
397 cached,
398 file.id,
399 need_complexity,
400 ))
401 .with_source_bytes_read(raw.len());
402 }
403
404 let parse_start = std::time::Instant::now();
405 css::take_comment_masked_bytes();
407 let module = parse_source_to_module_with_flags(
408 file.id,
409 &file.path,
410 source,
411 content_hash,
412 need_complexity,
413 flag_patterns,
414 );
415 let parse_cpu_nanos = u64::try_from(parse_start.elapsed().as_nanos()).unwrap_or(u64::MAX);
416 let mut result =
417 ParseFileResult::cache_miss(module, parse_cpu_nanos).with_source_bytes_read(raw.len());
418 result.css_masked_bytes = css::take_comment_masked_bytes();
419 result
420}
421
422#[must_use]
424pub fn parse_single_file(file: &DiscoveredFile) -> Option<ModuleInfo> {
425 let raw = std::fs::read_to_string(&file.path).ok()?;
426 let source = strip_bom(&raw);
427 let content_hash = xxhash_rust::xxh3::xxh3_64(source.as_bytes());
428 Some(parse_source_to_module(
429 file.id,
430 &file.path,
431 source,
432 content_hash,
433 false,
434 ))
435}
436
437#[must_use]
439pub fn parse_from_content(file_id: FileId, path: &Path, content: &str) -> ModuleInfo {
440 let content = strip_bom(content);
441 let content_hash = xxhash_rust::xxh3::xxh3_64(content.as_bytes());
442 parse_source_to_module(file_id, path, content, content_hash, true)
443}
444
445#[cfg(all(test, not(miri)))]
446mod tests;