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