1use std::path::{Path, PathBuf};
15
16use fallow_extract::visitor::extract_import_from_callable;
17use oxc_allocator::Allocator;
18#[allow(clippy::wildcard_imports, reason = "many AST types used")]
19use oxc_ast::ast::*;
20use oxc_parser::Parser;
21use oxc_span::SourceType;
22
23#[must_use]
25pub fn extract_imports(source: &str, path: &Path) -> Vec<String> {
26 extract_from_source(source, path, |program| {
27 let mut sources = Vec::new();
28 for stmt in &program.body {
29 if let Statement::ImportDeclaration(decl) = stmt {
30 sources.push(decl.source.value.to_string());
31 }
32 }
33 Some(sources)
34 })
35 .unwrap_or_default()
36}
37
38#[must_use]
46pub fn extract_imports_and_requires(source: &str, path: &Path) -> Vec<String> {
47 extract_from_source(source, path, |program| {
48 let mut sources = Vec::new();
49 for stmt in &program.body {
50 match stmt {
51 Statement::ImportDeclaration(decl) => {
52 sources.push(decl.source.value.to_string());
53 }
54 Statement::ExpressionStatement(expr) => {
55 if let Expression::CallExpression(call) = &expr.expression
56 && is_require_call(call)
57 && let Some(s) = get_require_source(call)
58 {
59 sources.push(s);
60 }
61 }
62 _ => {}
63 }
64 }
65 Some(sources)
66 })
67 .unwrap_or_default()
68}
69
70#[must_use]
72pub fn extract_config_string_array(source: &str, path: &Path, prop_path: &[&str]) -> Vec<String> {
73 extract_from_source(source, path, |program| {
74 let obj = find_config_object(program)?;
75 get_nested_string_array_from_object(obj, prop_path)
76 })
77 .unwrap_or_default()
78}
79
80#[must_use]
82pub fn extract_config_string(source: &str, path: &Path, prop_path: &[&str]) -> Option<String> {
83 extract_from_source(source, path, |program| {
84 let obj = find_config_object(program)?;
85 get_nested_string_from_object(obj, prop_path)
86 })
87}
88
89#[must_use]
96pub fn extract_config_property_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
97 extract_from_source(source, path, |program| {
98 let obj = find_config_object(program)?;
99 let mut values = Vec::new();
100 if let Some(prop) = find_property(obj, key) {
101 collect_all_string_values(&prop.value, &mut values);
102 }
103 Some(values)
104 })
105 .unwrap_or_default()
106}
107
108#[must_use]
115pub fn extract_config_shallow_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
116 extract_from_source(source, path, |program| {
117 let obj = find_config_object(program)?;
118 let prop = find_property(obj, key)?;
119 Some(collect_shallow_string_values(&prop.value))
120 })
121 .unwrap_or_default()
122}
123
124#[must_use]
129pub fn extract_config_shallow_strings_or_object_property(
130 source: &str,
131 path: &Path,
132 key: &str,
133 object_property: &str,
134) -> Vec<String> {
135 extract_from_source(source, path, |program| {
136 let obj = find_config_object(program)?;
137 let prop = find_property(obj, key)?;
138 Some(collect_shallow_string_or_object_property_values(
139 &prop.value,
140 object_property,
141 ))
142 })
143 .unwrap_or_default()
144}
145
146#[must_use]
152pub fn extract_config_nested_shallow_strings(
153 source: &str,
154 path: &Path,
155 outer_path: &[&str],
156 key: &str,
157) -> Vec<String> {
158 extract_from_source(source, path, |program| {
159 let obj = find_config_object(program)?;
160 let nested = get_nested_expression(obj, outer_path)?;
161 if let Expression::ObjectExpression(nested_obj) = nested {
162 let prop = find_property(nested_obj, key)?;
163 Some(collect_shallow_string_values(&prop.value))
164 } else {
165 None
166 }
167 })
168 .unwrap_or_default()
169}
170
171pub fn find_config_object_pub<'a>(program: &'a Program) -> Option<&'a ObjectExpression<'a>> {
173 find_config_object(program)
174}
175
176pub(crate) fn property_expr<'a>(
178 obj: &'a ObjectExpression<'a>,
179 key: &str,
180) -> Option<&'a Expression<'a>> {
181 find_property(obj, key).map(|prop| &prop.value)
182}
183
184pub(crate) fn property_object<'a>(
186 obj: &'a ObjectExpression<'a>,
187 key: &str,
188) -> Option<&'a ObjectExpression<'a>> {
189 property_expr(obj, key).and_then(object_expression)
190}
191
192pub(crate) fn property_string(obj: &ObjectExpression<'_>, key: &str) -> Option<String> {
194 property_expr(obj, key).and_then(expression_to_string)
195}
196
197pub(crate) fn object_expression<'a>(expr: &'a Expression<'a>) -> Option<&'a ObjectExpression<'a>> {
199 match expr {
200 Expression::ObjectExpression(obj) => Some(obj),
201 Expression::ParenthesizedExpression(paren) => object_expression(&paren.expression),
202 Expression::TSSatisfiesExpression(ts_sat) => object_expression(&ts_sat.expression),
203 Expression::TSAsExpression(ts_as) => object_expression(&ts_as.expression),
204 _ => None,
205 }
206}
207
208pub(crate) fn array_expression<'a>(expr: &'a Expression<'a>) -> Option<&'a ArrayExpression<'a>> {
210 match expr {
211 Expression::ArrayExpression(arr) => Some(arr),
212 Expression::ParenthesizedExpression(paren) => array_expression(&paren.expression),
213 Expression::TSSatisfiesExpression(ts_sat) => array_expression(&ts_sat.expression),
214 Expression::TSAsExpression(ts_as) => array_expression(&ts_as.expression),
215 _ => None,
216 }
217}
218
219pub(crate) fn expression_to_path_values(expr: &Expression<'_>) -> Vec<String> {
221 match expr {
222 Expression::ArrayExpression(arr) => arr
223 .elements
224 .iter()
225 .filter_map(|element| element.as_expression().and_then(expression_to_path_string))
226 .collect(),
227 _ => expression_to_path_string(expr).into_iter().collect(),
228 }
229}
230
231pub(crate) fn is_disabled_expression(expr: &Expression<'_>) -> bool {
233 matches!(expr, Expression::BooleanLiteral(boolean) if !boolean.value)
234 || matches!(expr, Expression::NullLiteral(_))
235}
236
237#[must_use]
242pub fn extract_config_object_keys(source: &str, path: &Path, prop_path: &[&str]) -> Vec<String> {
243 extract_from_source(source, path, |program| {
244 let obj = find_config_object(program)?;
245 get_nested_object_keys(obj, prop_path)
246 })
247 .unwrap_or_default()
248}
249
250#[must_use]
259pub fn extract_config_string_or_array(
260 source: &str,
261 path: &Path,
262 prop_path: &[&str],
263) -> Vec<String> {
264 extract_from_source(source, path, |program| {
265 let obj = find_config_object(program)?;
266 get_nested_string_or_array(obj, prop_path)
267 })
268 .unwrap_or_default()
269}
270
271#[must_use]
273pub fn extract_config_path_string(source: &str, path: &Path, prop_path: &[&str]) -> Option<String> {
274 extract_from_source(source, path, |program| {
275 let obj = find_config_object(program)?;
276 let expr = get_nested_expression(obj, prop_path)?;
277 expression_to_path_string(expr)
278 })
279}
280
281#[must_use]
288pub fn extract_config_array_nested_string_or_array(
289 source: &str,
290 path: &Path,
291 array_path: &[&str],
292 inner_path: &[&str],
293) -> Vec<String> {
294 extract_from_source(source, path, |program| {
295 let obj = find_config_object(program)?;
296 let array_expr = get_nested_expression(obj, array_path)?;
297 let Expression::ArrayExpression(arr) = array_expr else {
298 return None;
299 };
300 let mut results = Vec::new();
301 for element in &arr.elements {
302 if let Some(Expression::ObjectExpression(element_obj)) = element.as_expression()
303 && let Some(values) = get_nested_string_or_array(element_obj, inner_path)
304 {
305 results.extend(values);
306 }
307 }
308 if results.is_empty() {
309 None
310 } else {
311 Some(results)
312 }
313 })
314 .unwrap_or_default()
315}
316
317#[must_use]
324pub fn extract_config_object_nested_string_or_array(
325 source: &str,
326 path: &Path,
327 object_path: &[&str],
328 inner_path: &[&str],
329) -> Vec<String> {
330 extract_config_object_nested(source, path, object_path, |value_obj| {
331 get_nested_string_or_array(value_obj, inner_path)
332 })
333}
334
335#[must_use]
340pub fn extract_config_object_nested_strings(
341 source: &str,
342 path: &Path,
343 object_path: &[&str],
344 inner_path: &[&str],
345) -> Vec<String> {
346 extract_config_object_nested(source, path, object_path, |value_obj| {
347 get_nested_string_from_object(value_obj, inner_path).map(|s| vec![s])
348 })
349}
350
351fn extract_config_object_nested(
356 source: &str,
357 path: &Path,
358 object_path: &[&str],
359 extract_fn: impl Fn(&ObjectExpression<'_>) -> Option<Vec<String>>,
360) -> Vec<String> {
361 extract_from_source(source, path, |program| {
362 let obj = find_config_object(program)?;
363 let obj_expr = get_nested_expression(obj, object_path)?;
364 let Expression::ObjectExpression(target_obj) = obj_expr else {
365 return None;
366 };
367 let mut results = Vec::new();
368 for prop in &target_obj.properties {
369 if let ObjectPropertyKind::ObjectProperty(p) = prop
370 && let Expression::ObjectExpression(value_obj) = &p.value
371 && let Some(values) = extract_fn(value_obj)
372 {
373 results.extend(values);
374 }
375 }
376 if results.is_empty() {
377 None
378 } else {
379 Some(results)
380 }
381 })
382 .unwrap_or_default()
383}
384
385#[must_use]
391pub fn extract_config_require_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
392 extract_from_source(source, path, |program| {
393 let obj = find_config_object(program)?;
394 let prop = find_property(obj, key)?;
395 Some(collect_require_sources(&prop.value))
396 })
397 .unwrap_or_default()
398}
399
400#[must_use]
407pub fn extract_config_aliases(
408 source: &str,
409 path: &Path,
410 prop_path: &[&str],
411) -> Vec<(String, String)> {
412 extract_from_source(source, path, |program| {
413 let obj = find_config_object(program)?;
414 let expr = get_nested_expression(obj, prop_path)?;
415 let aliases = expression_to_alias_pairs(expr);
416 (!aliases.is_empty()).then_some(aliases)
417 })
418 .unwrap_or_default()
419}
420
421#[must_use]
429pub fn extract_config_array_nested_aliases(
430 source: &str,
431 path: &Path,
432 array_path: &[&str],
433 alias_path: &[&str],
434) -> Vec<(String, String)> {
435 extract_from_source(source, path, |program| {
436 let obj = find_config_object(program)?;
437 let array_expr = get_nested_expression(obj, array_path)?;
438 let Expression::ArrayExpression(arr) = array_expr else {
439 return None;
440 };
441 let mut results = Vec::new();
442 for element in &arr.elements {
443 if let Some(Expression::ObjectExpression(element_obj)) = element.as_expression()
444 && let Some(alias_expr) = get_nested_expression(element_obj, alias_path)
445 {
446 results.extend(expression_to_alias_pairs(alias_expr));
447 }
448 }
449 (!results.is_empty()).then_some(results)
450 })
451 .unwrap_or_default()
452}
453
454#[must_use]
463pub fn extract_config_aliases_kinded(
464 source: &str,
465 path: &Path,
466 prop_path: &[&str],
467) -> Vec<(String, String, bool)> {
468 extract_from_source(source, path, |program| {
469 let obj = find_config_object(program)?;
470 let expr = get_nested_expression(obj, prop_path)?;
471 let aliases = expression_to_alias_pairs_kinded(expr);
472 (!aliases.is_empty()).then_some(aliases)
473 })
474 .unwrap_or_default()
475}
476
477#[must_use]
480pub fn extract_config_array_nested_aliases_kinded(
481 source: &str,
482 path: &Path,
483 array_path: &[&str],
484 alias_path: &[&str],
485) -> Vec<(String, String, bool)> {
486 extract_from_source(source, path, |program| {
487 let obj = find_config_object(program)?;
488 let array_expr = get_nested_expression(obj, array_path)?;
489 let Expression::ArrayExpression(arr) = array_expr else {
490 return None;
491 };
492 let mut results = Vec::new();
493 for element in &arr.elements {
494 if let Some(Expression::ObjectExpression(element_obj)) = element.as_expression()
495 && let Some(alias_expr) = get_nested_expression(element_obj, alias_path)
496 {
497 results.extend(expression_to_alias_pairs_kinded(alias_expr));
498 }
499 }
500 (!results.is_empty()).then_some(results)
501 })
502 .unwrap_or_default()
503}
504
505#[must_use]
512pub fn extract_default_export_array_aliases_kinded(
513 source: &str,
514 path: &Path,
515 alias_path: &[&str],
516) -> Vec<(String, String, bool)> {
517 extract_from_source(source, path, |program| {
518 let arr = find_default_export_array(program)?;
519 let mut results = Vec::new();
520 for element in &arr.elements {
521 if let Some(Expression::ObjectExpression(element_obj)) = element.as_expression()
522 && let Some(alias_expr) = get_nested_expression(element_obj, alias_path)
523 {
524 results.extend(expression_to_alias_pairs_kinded(alias_expr));
525 }
526 }
527 (!results.is_empty()).then_some(results)
528 })
529 .unwrap_or_default()
530}
531
532#[must_use]
538pub fn config_default_export_unreachable(source: &str, path: &Path) -> bool {
539 extract_from_source(source, path, |program| {
540 let reachable =
541 find_config_object(program).is_some() || find_default_export_array(program).is_some();
542 Some(reachable)
543 })
544 .is_some_and(|reachable| !reachable)
545}
546
547#[must_use]
553pub fn extract_config_array_object_strings(
554 source: &str,
555 path: &Path,
556 array_path: &[&str],
557 key: &str,
558) -> Vec<String> {
559 extract_from_source(source, path, |program| {
560 let obj = find_config_object(program)?;
561 let array_expr = get_nested_expression(obj, array_path)?;
562 let Expression::ArrayExpression(arr) = array_expr else {
563 return None;
564 };
565
566 let mut results = Vec::new();
567 for element in &arr.elements {
568 let Some(expr) = element.as_expression() else {
569 continue;
570 };
571 match expr {
572 Expression::ObjectExpression(item) => {
573 if let Some(prop) = find_property(item, key)
574 && let Some(value) = expression_to_path_string(&prop.value)
575 {
576 results.push(value);
577 }
578 }
579 _ => {
580 if let Some(value) = expression_to_path_string(expr) {
581 results.push(value);
582 }
583 }
584 }
585 }
586
587 (!results.is_empty()).then_some(results)
588 })
589 .unwrap_or_default()
590}
591
592#[must_use]
603pub fn extract_config_array_object_string_pairs(
604 source: &str,
605 path: &Path,
606 array_path: &[&str],
607 primary_key: &str,
608 secondary_key: &str,
609) -> Vec<(String, Option<String>)> {
610 extract_from_source(source, path, |program| {
611 let obj = find_config_object(program)?;
612 let array_expr = get_nested_expression(obj, array_path)?;
613 let Expression::ArrayExpression(arr) = array_expr else {
614 return None;
615 };
616
617 let mut results = Vec::new();
618 for element in &arr.elements {
619 let Some(Expression::ObjectExpression(item)) = element.as_expression() else {
620 continue;
621 };
622 let Some(primary) = find_property(item, primary_key)
623 .and_then(|prop| expression_to_path_string(&prop.value))
624 else {
625 continue;
626 };
627 let secondary = find_property(item, secondary_key)
628 .and_then(|prop| expression_to_path_string(&prop.value));
629 results.push((primary, secondary));
630 }
631
632 (!results.is_empty()).then_some(results)
633 })
634 .unwrap_or_default()
635}
636
637#[must_use]
683pub fn extract_lazy_imports_in_array(source: &str, path: &Path, prop_path: &[&str]) -> Vec<String> {
684 extract_from_source(source, path, |program| {
685 let obj = find_config_object(program)?;
686 let array_expr = get_nested_expression(obj, prop_path)?;
687 let Expression::ArrayExpression(arr) = array_expr else {
688 return None;
689 };
690 let mut specs = Vec::new();
691 for element in &arr.elements {
692 let Some(expr) = element.as_expression() else {
693 continue;
694 };
695 if let Some(spec) = lazy_import_specifier(expr) {
696 specs.push(spec);
697 }
698 }
699 (!specs.is_empty()).then_some(specs)
700 })
701 .unwrap_or_default()
702}
703
704fn lazy_import_specifier(expr: &Expression<'_>) -> Option<String> {
717 let callable = match expr {
718 Expression::ObjectExpression(obj) => &find_property(obj, "file")?.value,
719 _ => expr,
720 };
721 let import_expr = extract_import_from_callable(callable)?;
722 expression_to_string(&import_expr.source)
723}
724
725#[must_use]
732pub fn extract_config_plugin_option_string(
733 source: &str,
734 path: &Path,
735 plugins_path: &[&str],
736 plugin_name: &str,
737 option_key: &str,
738) -> Option<String> {
739 extract_from_source(source, path, |program| {
740 let obj = find_config_object(program)?;
741 let plugins_expr = get_nested_expression(obj, plugins_path)?;
742 let Expression::ArrayExpression(plugins) = plugins_expr else {
743 return None;
744 };
745
746 for entry in &plugins.elements {
747 let Some(Expression::ArrayExpression(tuple)) = entry.as_expression() else {
748 continue;
749 };
750 let Some(plugin_expr) = tuple
751 .elements
752 .first()
753 .and_then(ArrayExpressionElement::as_expression)
754 else {
755 continue;
756 };
757 if expression_to_string(plugin_expr).as_deref() != Some(plugin_name) {
758 continue;
759 }
760
761 let Some(options_expr) = tuple
762 .elements
763 .get(1)
764 .and_then(ArrayExpressionElement::as_expression)
765 else {
766 continue;
767 };
768 let Expression::ObjectExpression(options_obj) = options_expr else {
769 continue;
770 };
771 let option = find_property(options_obj, option_key)?;
772 return expression_to_path_string(&option.value);
773 }
774
775 None
776 })
777}
778
779#[must_use]
781pub fn extract_config_plugin_option_string_from_paths(
782 source: &str,
783 path: &Path,
784 plugin_paths: &[&[&str]],
785 plugin_name: &str,
786 option_key: &str,
787) -> Option<String> {
788 plugin_paths.iter().find_map(|plugins_path| {
789 extract_config_plugin_option_string(source, path, plugins_path, plugin_name, option_key)
790 })
791}
792
793#[must_use]
798pub fn normalize_config_path(raw: &str, config_path: &Path, root: &Path) -> Option<String> {
799 if raw.is_empty() {
800 return None;
801 }
802
803 let candidate = if let Some(stripped) = raw.strip_prefix('/') {
804 lexical_normalize(&root.join(stripped))
805 } else {
806 let path = Path::new(raw);
807 if path.is_absolute() {
808 lexical_normalize(path)
809 } else {
810 let base = config_path.parent().unwrap_or(root);
811 lexical_normalize(&base.join(path))
812 }
813 };
814
815 let relative = candidate.strip_prefix(root).ok()?;
816 let normalized = relative.to_string_lossy().replace('\\', "/");
817 (!normalized.is_empty()).then_some(normalized)
818}
819
820fn extract_from_source<T>(
829 source: &str,
830 path: &Path,
831 extractor: impl FnOnce(&Program) -> Option<T>,
832) -> Option<T> {
833 let source_type = SourceType::from_path(path).unwrap_or_default();
834 let alloc = Allocator::default();
835
836 let is_json = path
839 .extension()
840 .is_some_and(|ext| ext == "json" || ext == "jsonc");
841 if is_json {
842 let wrapped = format!("({source})");
843 let parsed = Parser::new(&alloc, &wrapped, SourceType::mjs()).parse();
844 return extractor(&parsed.program);
845 }
846
847 let parsed = Parser::new(&alloc, source, source_type).parse();
848 extractor(&parsed.program)
849}
850
851fn find_config_object<'a>(program: &'a Program) -> Option<&'a ObjectExpression<'a>> {
863 for stmt in &program.body {
864 match stmt {
865 Statement::ExportDefaultDeclaration(decl) => {
867 let expr: Option<&Expression> = match &decl.declaration {
869 ExportDefaultDeclarationKind::ObjectExpression(obj) => {
870 return Some(obj);
871 }
872 ExportDefaultDeclarationKind::FunctionDeclaration(func) => {
873 return extract_object_from_function(func);
874 }
875 _ => decl.declaration.as_expression(),
876 };
877 if let Some(expr) = expr {
878 if let Some(obj) = extract_object_from_expression(expr) {
880 return Some(obj);
881 }
882 if let Some(name) = unwrap_to_identifier_name(expr) {
885 return find_variable_init_object(program, name);
886 }
887 }
888 }
889 Statement::ExpressionStatement(expr_stmt) => {
891 if let Expression::AssignmentExpression(assign) = &expr_stmt.expression
892 && is_module_exports_target(&assign.left)
893 {
894 return extract_object_from_expression(&assign.right);
895 }
896 }
897 _ => {}
898 }
899 }
900
901 if program.body.len() == 1
904 && let Statement::ExpressionStatement(expr_stmt) = &program.body[0]
905 {
906 match &expr_stmt.expression {
907 Expression::ObjectExpression(obj) => return Some(obj),
908 Expression::ParenthesizedExpression(paren) => {
909 if let Expression::ObjectExpression(obj) = &paren.expression {
910 return Some(obj);
911 }
912 }
913 _ => {}
914 }
915 }
916
917 None
918}
919
920fn extract_object_from_expression<'a>(
922 expr: &'a Expression<'a>,
923) -> Option<&'a ObjectExpression<'a>> {
924 match expr {
925 Expression::ObjectExpression(obj) => Some(obj),
927 Expression::CallExpression(call) => {
929 for arg in &call.arguments {
931 match arg {
932 Argument::ObjectExpression(obj) => return Some(obj),
933 Argument::ArrowFunctionExpression(arrow) => {
935 if arrow.expression
936 && !arrow.body.statements.is_empty()
937 && let Statement::ExpressionStatement(expr_stmt) =
938 &arrow.body.statements[0]
939 {
940 return extract_object_from_expression(&expr_stmt.expression);
941 }
942 }
943 _ => {}
944 }
945 }
946 None
947 }
948 Expression::ParenthesizedExpression(paren) => {
950 extract_object_from_expression(&paren.expression)
951 }
952 Expression::TSSatisfiesExpression(ts_sat) => {
954 extract_object_from_expression(&ts_sat.expression)
955 }
956 Expression::TSAsExpression(ts_as) => extract_object_from_expression(&ts_as.expression),
957 Expression::ArrowFunctionExpression(arrow) => extract_object_from_arrow_function(arrow),
958 Expression::FunctionExpression(func) => extract_object_from_function(func),
959 _ => None,
960 }
961}
962
963fn extract_object_from_arrow_function<'a>(
964 arrow: &'a ArrowFunctionExpression<'a>,
965) -> Option<&'a ObjectExpression<'a>> {
966 if arrow.expression {
967 arrow.body.statements.first().and_then(|stmt| {
968 if let Statement::ExpressionStatement(expr_stmt) = stmt {
969 extract_object_from_expression(&expr_stmt.expression)
970 } else {
971 None
972 }
973 })
974 } else {
975 extract_object_from_function_body(&arrow.body)
976 }
977}
978
979fn extract_object_from_function<'a>(func: &'a Function<'a>) -> Option<&'a ObjectExpression<'a>> {
980 func.body
981 .as_ref()
982 .and_then(|body| extract_object_from_function_body(body))
983}
984
985fn extract_object_from_function_body<'a>(
986 body: &'a FunctionBody<'a>,
987) -> Option<&'a ObjectExpression<'a>> {
988 for stmt in &body.statements {
989 if let Statement::ReturnStatement(ret) = stmt
990 && let Some(argument) = &ret.argument
991 && let Some(obj) = extract_object_from_expression(argument)
992 {
993 return Some(obj);
994 }
995 }
996 None
997}
998
999fn is_module_exports_target(target: &AssignmentTarget) -> bool {
1001 if let AssignmentTarget::StaticMemberExpression(member) = target
1002 && let Expression::Identifier(obj) = &member.object
1003 {
1004 return obj.name == "module" && member.property.name == "exports";
1005 }
1006 false
1007}
1008
1009fn unwrap_to_identifier_name<'a>(expr: &'a Expression<'a>) -> Option<&'a str> {
1013 match expr {
1014 Expression::Identifier(id) => Some(&id.name),
1015 Expression::TSSatisfiesExpression(ts_sat) => unwrap_to_identifier_name(&ts_sat.expression),
1016 Expression::TSAsExpression(ts_as) => unwrap_to_identifier_name(&ts_as.expression),
1017 _ => None,
1018 }
1019}
1020
1021fn find_variable_init_object<'a>(
1026 program: &'a Program,
1027 name: &str,
1028) -> Option<&'a ObjectExpression<'a>> {
1029 for stmt in &program.body {
1030 if let Statement::VariableDeclaration(decl) = stmt {
1031 for declarator in &decl.declarations {
1032 if let BindingPattern::BindingIdentifier(id) = &declarator.id
1033 && id.name == name
1034 && let Some(init) = &declarator.init
1035 {
1036 return extract_object_from_expression(init);
1037 }
1038 }
1039 }
1040 }
1041 None
1042}
1043
1044pub(crate) fn find_property<'a>(
1046 obj: &'a ObjectExpression<'a>,
1047 key: &str,
1048) -> Option<&'a ObjectProperty<'a>> {
1049 for prop in &obj.properties {
1050 if let ObjectPropertyKind::ObjectProperty(p) = prop
1051 && property_key_matches(&p.key, key)
1052 {
1053 return Some(p);
1054 }
1055 }
1056 None
1057}
1058
1059pub(crate) fn property_key_matches(key: &PropertyKey, name: &str) -> bool {
1061 match key {
1062 PropertyKey::StaticIdentifier(id) => id.name == name,
1063 PropertyKey::StringLiteral(s) => s.value == name,
1064 _ => false,
1065 }
1066}
1067
1068fn get_object_string_property(obj: &ObjectExpression, key: &str) -> Option<String> {
1070 find_property(obj, key).and_then(|p| expression_to_string(&p.value))
1071}
1072
1073fn get_object_string_array_property(obj: &ObjectExpression, key: &str) -> Vec<String> {
1075 find_property(obj, key)
1076 .map(|p| expression_to_string_array(&p.value))
1077 .unwrap_or_default()
1078}
1079
1080fn get_nested_string_array_from_object(
1082 obj: &ObjectExpression,
1083 path: &[&str],
1084) -> Option<Vec<String>> {
1085 if path.is_empty() {
1086 return None;
1087 }
1088 if path.len() == 1 {
1089 return Some(get_object_string_array_property(obj, path[0]));
1090 }
1091 let prop = find_property(obj, path[0])?;
1093 if let Expression::ObjectExpression(nested) = &prop.value {
1094 get_nested_string_array_from_object(nested, &path[1..])
1095 } else {
1096 None
1097 }
1098}
1099
1100fn get_nested_string_from_object(obj: &ObjectExpression, path: &[&str]) -> Option<String> {
1102 if path.is_empty() {
1103 return None;
1104 }
1105 if path.len() == 1 {
1106 return get_object_string_property(obj, path[0]);
1107 }
1108 let prop = find_property(obj, path[0])?;
1109 if let Expression::ObjectExpression(nested) = &prop.value {
1110 get_nested_string_from_object(nested, &path[1..])
1111 } else {
1112 None
1113 }
1114}
1115
1116pub(crate) fn expression_to_string(expr: &Expression) -> Option<String> {
1118 match expr {
1119 Expression::StringLiteral(s) => Some(s.value.to_string()),
1120 Expression::TemplateLiteral(t) if t.expressions.is_empty() => {
1121 t.quasis.first().map(|q| q.value.raw.to_string())
1123 }
1124 _ => None,
1125 }
1126}
1127
1128pub(crate) fn expression_to_path_string(expr: &Expression) -> Option<String> {
1130 match expr {
1131 Expression::ParenthesizedExpression(paren) => expression_to_path_string(&paren.expression),
1132 Expression::TSAsExpression(ts_as) => expression_to_path_string(&ts_as.expression),
1133 Expression::TSSatisfiesExpression(ts_sat) => expression_to_path_string(&ts_sat.expression),
1134 Expression::StaticMemberExpression(member) if member.property.name == "pathname" => {
1135 expression_to_path_string(&member.object)
1136 }
1137 Expression::CallExpression(call) => call_expression_to_path_string(call),
1138 Expression::NewExpression(new_expr) => new_expression_to_path_string(new_expr),
1139 _ => expression_to_string(expr),
1140 }
1141}
1142
1143fn call_expression_to_path_string(call: &CallExpression) -> Option<String> {
1144 if matches!(&call.callee, Expression::Identifier(id) if id.name == "fileURLToPath") {
1145 return call
1146 .arguments
1147 .first()
1148 .and_then(Argument::as_expression)
1149 .and_then(expression_to_path_string);
1150 }
1151
1152 let callee_name = match &call.callee {
1153 Expression::Identifier(id) => Some(id.name.as_str()),
1154 Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
1155 _ => None,
1156 }?;
1157
1158 if !matches!(callee_name, "resolve" | "join") {
1159 return None;
1160 }
1161
1162 let mut segments = Vec::new();
1163 for (index, arg) in call.arguments.iter().enumerate() {
1164 let expr = arg.as_expression()?;
1165
1166 if is_dirname_anchor(expr) {
1167 if index == 0 {
1168 continue;
1169 }
1170 return None;
1171 }
1172
1173 segments.push(expression_to_string(expr)?);
1174 }
1175
1176 (!segments.is_empty()).then(|| join_path_segments(&segments))
1177}
1178
1179fn is_dirname_anchor(expr: &Expression) -> bool {
1184 match expr {
1185 Expression::Identifier(id) => id.name == "__dirname",
1186 Expression::StaticMemberExpression(member) => {
1187 member.property.name == "dirname" && is_import_meta_expression(&member.object)
1188 }
1189 _ => false,
1190 }
1191}
1192
1193fn is_import_meta_expression(expr: &Expression) -> bool {
1195 matches!(
1196 expr,
1197 Expression::MetaProperty(meta) if meta.meta.name == "import" && meta.property.name == "meta"
1198 )
1199}
1200
1201fn new_expression_to_path_string(new_expr: &NewExpression) -> Option<String> {
1202 if !matches!(&new_expr.callee, Expression::Identifier(id) if id.name == "URL") {
1203 return None;
1204 }
1205
1206 let source = new_expr
1207 .arguments
1208 .first()
1209 .and_then(Argument::as_expression)
1210 .and_then(expression_to_string)?;
1211
1212 let base = new_expr
1213 .arguments
1214 .get(1)
1215 .and_then(Argument::as_expression)?;
1216 is_import_meta_url_expression(base).then_some(source)
1217}
1218
1219fn is_import_meta_url_expression(expr: &Expression) -> bool {
1220 if let Expression::StaticMemberExpression(member) = expr {
1221 member.property.name == "url" && matches!(member.object, Expression::MetaProperty(_))
1222 } else {
1223 false
1224 }
1225}
1226
1227fn join_path_segments(segments: &[String]) -> String {
1228 let mut joined = PathBuf::new();
1229 for segment in segments {
1230 joined.push(segment);
1231 }
1232 joined.to_string_lossy().replace('\\', "/")
1233}
1234
1235fn expression_to_alias_pairs(expr: &Expression) -> Vec<(String, String)> {
1236 match expr {
1237 Expression::ObjectExpression(obj) => obj
1238 .properties
1239 .iter()
1240 .filter_map(|prop| {
1241 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
1242 return None;
1243 };
1244 let find = property_key_to_string(&prop.key)?;
1245 let replacement = expression_to_path_values(&prop.value).into_iter().next()?;
1246 Some((find, replacement))
1247 })
1248 .collect(),
1249 Expression::ArrayExpression(arr) => arr
1250 .elements
1251 .iter()
1252 .filter_map(|element| {
1253 let Expression::ObjectExpression(obj) = element.as_expression()? else {
1254 return None;
1255 };
1256 let find = find_property(obj, "find")
1257 .and_then(|prop| expression_to_string(&prop.value))?;
1258 let replacement = find_property(obj, "replacement")
1259 .and_then(|prop| expression_to_path_string(&prop.value))?;
1260 Some((find, replacement))
1261 })
1262 .collect(),
1263 _ => Vec::new(),
1264 }
1265}
1266
1267fn expression_to_alias_pairs_kinded(expr: &Expression) -> Vec<(String, String, bool)> {
1271 match expr {
1272 Expression::ObjectExpression(obj) => obj
1273 .properties
1274 .iter()
1275 .filter_map(|prop| {
1276 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
1277 return None;
1278 };
1279 let find = property_key_to_string(&prop.key)?;
1280 let (replacement, is_bare) = alias_replacement_kinded(&prop.value)?;
1281 Some((find, replacement, is_bare))
1282 })
1283 .collect(),
1284 Expression::ArrayExpression(arr) => arr
1285 .elements
1286 .iter()
1287 .filter_map(|element| {
1288 let Expression::ObjectExpression(obj) = element.as_expression()? else {
1289 return None;
1290 };
1291 let find = find_property(obj, "find")
1292 .and_then(|prop| expression_to_string(&prop.value))?;
1293 let (replacement, is_bare) = find_property(obj, "replacement")
1294 .and_then(|prop| alias_replacement_kinded(&prop.value))?;
1295 Some((find, replacement, is_bare))
1296 })
1297 .collect(),
1298 _ => Vec::new(),
1299 }
1300}
1301
1302fn alias_replacement_kinded(expr: &Expression) -> Option<(String, bool)> {
1309 match expr {
1310 Expression::ParenthesizedExpression(paren) => alias_replacement_kinded(&paren.expression),
1311 Expression::TSAsExpression(ts_as) => alias_replacement_kinded(&ts_as.expression),
1312 Expression::TSSatisfiesExpression(ts_sat) => alias_replacement_kinded(&ts_sat.expression),
1313 Expression::StringLiteral(s) => {
1314 let value = s.value.to_string();
1315 let is_bare =
1316 !value.starts_with("./") && !value.starts_with("../") && !value.starts_with('/');
1317 Some((value, is_bare))
1318 }
1319 _ => expression_to_path_string(expr).map(|value| (value, false)),
1322 }
1323}
1324
1325fn find_default_export_array<'a>(program: &'a Program<'a>) -> Option<&'a ArrayExpression<'a>> {
1330 for stmt in &program.body {
1331 if let Statement::ExportDefaultDeclaration(decl) = stmt
1332 && let Some(expr) = decl.declaration.as_expression()
1333 {
1334 return array_from_expression(expr);
1335 }
1336 }
1337 None
1338}
1339
1340fn array_from_expression<'a>(expr: &'a Expression<'a>) -> Option<&'a ArrayExpression<'a>> {
1341 match expr {
1342 Expression::ArrayExpression(arr) => Some(arr),
1343 Expression::ParenthesizedExpression(paren) => array_from_expression(&paren.expression),
1344 Expression::TSAsExpression(ts_as) => array_from_expression(&ts_as.expression),
1345 Expression::TSSatisfiesExpression(ts_sat) => array_from_expression(&ts_sat.expression),
1346 Expression::CallExpression(call) => call
1348 .arguments
1349 .first()
1350 .and_then(Argument::as_expression)
1351 .and_then(array_from_expression),
1352 _ => None,
1353 }
1354}
1355
1356fn lexical_normalize(path: &Path) -> PathBuf {
1357 let mut normalized = PathBuf::new();
1358
1359 for component in path.components() {
1360 match component {
1361 std::path::Component::CurDir => {}
1362 std::path::Component::ParentDir => {
1363 normalized.pop();
1364 }
1365 _ => normalized.push(component.as_os_str()),
1366 }
1367 }
1368
1369 normalized
1370}
1371
1372fn expression_to_string_array(expr: &Expression) -> Vec<String> {
1374 match expr {
1375 Expression::ArrayExpression(arr) => arr
1376 .elements
1377 .iter()
1378 .filter_map(|el| match el {
1379 ArrayExpressionElement::SpreadElement(_) => None,
1380 _ => el.as_expression().and_then(expression_to_string),
1381 })
1382 .collect(),
1383 _ => vec![],
1384 }
1385}
1386
1387fn collect_shallow_string_values(expr: &Expression) -> Vec<String> {
1392 let mut values = Vec::new();
1393 match expr {
1394 Expression::StringLiteral(s) => {
1395 values.push(s.value.to_string());
1396 }
1397 Expression::ArrayExpression(arr) => {
1398 for el in &arr.elements {
1399 if let Some(inner) = el.as_expression() {
1400 match inner {
1401 Expression::StringLiteral(s) => {
1402 values.push(s.value.to_string());
1403 }
1404 Expression::ArrayExpression(sub_arr) => {
1406 if let Some(first) = sub_arr.elements.first()
1407 && let Some(first_expr) = first.as_expression()
1408 && let Some(s) = expression_to_string(first_expr)
1409 {
1410 values.push(s);
1411 }
1412 }
1413 _ => {}
1414 }
1415 }
1416 }
1417 }
1418 Expression::ObjectExpression(obj) => {
1420 for prop in &obj.properties {
1421 if let ObjectPropertyKind::ObjectProperty(p) = prop {
1422 match &p.value {
1423 Expression::StringLiteral(s) => {
1424 values.push(s.value.to_string());
1425 }
1426 Expression::ArrayExpression(sub_arr) => {
1428 if let Some(first) = sub_arr.elements.first()
1429 && let Some(first_expr) = first.as_expression()
1430 && let Some(s) = expression_to_string(first_expr)
1431 {
1432 values.push(s);
1433 }
1434 }
1435 _ => {}
1436 }
1437 }
1438 }
1439 }
1440 _ => {}
1441 }
1442 values
1443}
1444
1445fn collect_shallow_string_or_object_property_values(
1447 expr: &Expression,
1448 object_property: &str,
1449) -> Vec<String> {
1450 match expr {
1451 Expression::ArrayExpression(arr) => arr
1452 .elements
1453 .iter()
1454 .filter_map(|element| {
1455 element
1456 .as_expression()
1457 .and_then(|expr| shallow_string_or_object_property(expr, object_property))
1458 })
1459 .collect(),
1460 _ => shallow_string_or_object_property(expr, object_property)
1461 .into_iter()
1462 .collect(),
1463 }
1464}
1465
1466fn shallow_string_or_object_property(expr: &Expression, object_property: &str) -> Option<String> {
1467 match expr {
1468 Expression::ParenthesizedExpression(paren) => {
1469 shallow_string_or_object_property(&paren.expression, object_property)
1470 }
1471 Expression::TSSatisfiesExpression(ts_sat) => {
1472 shallow_string_or_object_property(&ts_sat.expression, object_property)
1473 }
1474 Expression::TSAsExpression(ts_as) => {
1475 shallow_string_or_object_property(&ts_as.expression, object_property)
1476 }
1477 Expression::ArrayExpression(sub_arr) => sub_arr
1478 .elements
1479 .first()
1480 .and_then(ArrayExpressionElement::as_expression)
1481 .and_then(expression_to_string),
1482 Expression::ObjectExpression(obj) => {
1483 find_property(obj, object_property).and_then(|prop| expression_to_string(&prop.value))
1484 }
1485 _ => expression_to_string(expr),
1486 }
1487}
1488
1489fn collect_all_string_values(expr: &Expression, values: &mut Vec<String>) {
1491 match expr {
1492 Expression::StringLiteral(s) => {
1493 values.push(s.value.to_string());
1494 }
1495 Expression::ArrayExpression(arr) => {
1496 for el in &arr.elements {
1497 if let Some(expr) = el.as_expression() {
1498 collect_all_string_values(expr, values);
1499 }
1500 }
1501 }
1502 Expression::ObjectExpression(obj) => {
1503 for prop in &obj.properties {
1504 if let ObjectPropertyKind::ObjectProperty(p) = prop {
1505 collect_all_string_values(&p.value, values);
1506 }
1507 }
1508 }
1509 _ => {}
1510 }
1511}
1512
1513fn property_key_to_string(key: &PropertyKey) -> Option<String> {
1515 match key {
1516 PropertyKey::StaticIdentifier(id) => Some(id.name.to_string()),
1517 PropertyKey::StringLiteral(s) => Some(s.value.to_string()),
1518 _ => None,
1519 }
1520}
1521
1522fn get_nested_object_keys(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
1524 if path.is_empty() {
1525 return None;
1526 }
1527 let prop = find_property(obj, path[0])?;
1528 if path.len() == 1 {
1529 if let Expression::ObjectExpression(nested) = &prop.value {
1530 let keys = nested
1531 .properties
1532 .iter()
1533 .filter_map(|p| {
1534 if let ObjectPropertyKind::ObjectProperty(p) = p {
1535 property_key_to_string(&p.key)
1536 } else {
1537 None
1538 }
1539 })
1540 .collect();
1541 return Some(keys);
1542 }
1543 return None;
1544 }
1545 if let Expression::ObjectExpression(nested) = &prop.value {
1546 get_nested_object_keys(nested, &path[1..])
1547 } else {
1548 None
1549 }
1550}
1551
1552fn get_nested_expression<'a>(
1554 obj: &'a ObjectExpression<'a>,
1555 path: &[&str],
1556) -> Option<&'a Expression<'a>> {
1557 if path.is_empty() {
1558 return None;
1559 }
1560 let prop = find_property(obj, path[0])?;
1561 if path.len() == 1 {
1562 return Some(&prop.value);
1563 }
1564 if let Expression::ObjectExpression(nested) = &prop.value {
1565 get_nested_expression(nested, &path[1..])
1566 } else {
1567 None
1568 }
1569}
1570
1571fn get_nested_string_or_array(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
1573 if path.is_empty() {
1574 return None;
1575 }
1576 if path.len() == 1 {
1577 let prop = find_property(obj, path[0])?;
1578 return Some(expression_to_string_or_array(&prop.value));
1579 }
1580 let prop = find_property(obj, path[0])?;
1581 if let Expression::ObjectExpression(nested) = &prop.value {
1582 get_nested_string_or_array(nested, &path[1..])
1583 } else {
1584 None
1585 }
1586}
1587
1588fn expression_to_string_or_array(expr: &Expression) -> Vec<String> {
1596 match expr {
1597 Expression::StringLiteral(s) => vec![s.value.to_string()],
1598 Expression::TemplateLiteral(t) if t.expressions.is_empty() => t
1599 .quasis
1600 .first()
1601 .map(|q| vec![q.value.raw.to_string()])
1602 .unwrap_or_default(),
1603 Expression::ArrayExpression(arr) => arr
1604 .elements
1605 .iter()
1606 .filter_map(|el| el.as_expression())
1607 .flat_map(|e| match e {
1608 Expression::ObjectExpression(obj) => find_property(obj, "input")
1609 .map(|p| expression_to_string_or_array(&p.value))
1610 .unwrap_or_default(),
1611 _ => expression_to_path_string(e).into_iter().collect(),
1617 })
1618 .collect(),
1619 Expression::ObjectExpression(obj) => obj
1620 .properties
1621 .iter()
1622 .flat_map(|p| {
1623 if let ObjectPropertyKind::ObjectProperty(p) = p {
1624 match &p.value {
1625 Expression::ArrayExpression(_) => expression_to_string_or_array(&p.value),
1626 Expression::ObjectExpression(value_obj) => {
1627 find_property(value_obj, "import")
1628 .map(|import_prop| {
1629 expression_to_string_or_array(&import_prop.value)
1630 })
1631 .unwrap_or_default()
1632 }
1633 _ => expression_to_path_string(&p.value).into_iter().collect(),
1634 }
1635 } else {
1636 Vec::new()
1637 }
1638 })
1639 .collect(),
1640 _ => expression_to_path_string(expr).into_iter().collect(),
1642 }
1643}
1644
1645fn collect_require_sources(expr: &Expression) -> Vec<String> {
1647 let mut sources = Vec::new();
1648 match expr {
1649 Expression::CallExpression(call) if is_require_call(call) => {
1650 if let Some(s) = get_require_source(call) {
1651 sources.push(s);
1652 }
1653 }
1654 Expression::ArrayExpression(arr) => {
1655 for el in &arr.elements {
1656 if let Some(inner) = el.as_expression() {
1657 match inner {
1658 Expression::CallExpression(call) if is_require_call(call) => {
1659 if let Some(s) = get_require_source(call) {
1660 sources.push(s);
1661 }
1662 }
1663 Expression::ArrayExpression(sub_arr) => {
1665 if let Some(first) = sub_arr.elements.first()
1666 && let Some(Expression::CallExpression(call)) =
1667 first.as_expression()
1668 && is_require_call(call)
1669 && let Some(s) = get_require_source(call)
1670 {
1671 sources.push(s);
1672 }
1673 }
1674 _ => {}
1675 }
1676 }
1677 }
1678 }
1679 _ => {}
1680 }
1681 sources
1682}
1683
1684fn is_require_call(call: &CallExpression) -> bool {
1686 matches!(&call.callee, Expression::Identifier(id) if id.name == "require")
1687}
1688
1689fn get_require_source(call: &CallExpression) -> Option<String> {
1691 call.arguments.first().and_then(|arg| {
1692 if let Argument::StringLiteral(s) = arg {
1693 Some(s.value.to_string())
1694 } else {
1695 None
1696 }
1697 })
1698}
1699
1700#[cfg(test)]
1701mod tests {
1702 use super::*;
1703 use std::path::PathBuf;
1704
1705 fn js_path() -> PathBuf {
1706 PathBuf::from("config.js")
1707 }
1708
1709 fn ts_path() -> PathBuf {
1710 PathBuf::from("config.ts")
1711 }
1712
1713 #[test]
1714 fn extract_lazy_imports_bare_arrows() {
1715 let source = r"
1716 import { defineConfig } from '@adonisjs/core/app'
1717 export default defineConfig({
1718 preloads: [
1719 () => import('#start/routes'),
1720 () => import('#start/kernel'),
1721 ],
1722 })
1723 ";
1724 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["preloads"]);
1725 assert_eq!(specs, vec!["#start/routes", "#start/kernel"]);
1726 }
1727
1728 #[test]
1729 fn extract_lazy_imports_object_form_with_file_key() {
1730 let source = r"
1731 export default defineConfig({
1732 providers: [
1733 () => import('@adonisjs/core/providers/app_provider'),
1734 {
1735 file: () => import('@adonisjs/core/providers/repl_provider'),
1736 environment: ['repl', 'test'],
1737 },
1738 ],
1739 })
1740 ";
1741 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["providers"]);
1742 assert_eq!(
1743 specs,
1744 vec![
1745 "@adonisjs/core/providers/app_provider",
1746 "@adonisjs/core/providers/repl_provider",
1747 ]
1748 );
1749 }
1750
1751 #[test]
1752 fn extract_lazy_imports_block_body_with_return() {
1753 let source = r"
1755 export default defineConfig({
1756 commands: [
1757 () => { return import('@adonisjs/core/commands') },
1758 ],
1759 })
1760 ";
1761 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["commands"]);
1762 assert_eq!(specs, vec!["@adonisjs/core/commands"]);
1763 }
1764
1765 #[test]
1766 fn extract_lazy_imports_skips_unknown_element_shapes() {
1767 let source = r"
1770 export default defineConfig({
1771 commands: [
1772 'string-entry',
1773 42,
1774 { other: 'value' },
1775 () => import('@adonisjs/lucid/commands'),
1776 ],
1777 })
1778 ";
1779 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["commands"]);
1780 assert_eq!(specs, vec!["@adonisjs/lucid/commands"]);
1781 }
1782
1783 #[test]
1784 fn extract_lazy_imports_missing_property_returns_empty() {
1785 let source = r"
1786 export default defineConfig({
1787 preloads: [() => import('#start/routes')],
1788 })
1789 ";
1790 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["providers"]);
1791 assert!(specs.is_empty());
1792 }
1793
1794 #[test]
1795 fn extract_imports_basic() {
1796 let source = r"
1797 import foo from 'foo-pkg';
1798 import { bar } from '@scope/bar';
1799 export default {};
1800 ";
1801 let imports = extract_imports(source, &js_path());
1802 assert_eq!(imports, vec!["foo-pkg", "@scope/bar"]);
1803 }
1804
1805 #[test]
1806 fn extract_default_export_object_property() {
1807 let source = r#"export default { testDir: "./tests" };"#;
1808 let val = extract_config_string(source, &js_path(), &["testDir"]);
1809 assert_eq!(val, Some("./tests".to_string()));
1810 }
1811
1812 #[test]
1813 fn extract_define_config_property() {
1814 let source = r#"
1815 import { defineConfig } from 'vitest/config';
1816 export default defineConfig({
1817 test: {
1818 include: ["**/*.test.ts", "**/*.spec.ts"],
1819 setupFiles: ["./test/setup.ts"]
1820 }
1821 });
1822 "#;
1823 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
1824 assert_eq!(include, vec!["**/*.test.ts", "**/*.spec.ts"]);
1825
1826 let setup = extract_config_string_array(source, &ts_path(), &["test", "setupFiles"]);
1827 assert_eq!(setup, vec!["./test/setup.ts"]);
1828 }
1829
1830 #[test]
1831 fn extract_module_exports_property() {
1832 let source = r#"module.exports = { testEnvironment: "jsdom" };"#;
1833 let val = extract_config_string(source, &js_path(), &["testEnvironment"]);
1834 assert_eq!(val, Some("jsdom".to_string()));
1835 }
1836
1837 #[test]
1838 fn extract_nested_string_array() {
1839 let source = r#"
1840 export default {
1841 resolve: {
1842 alias: {
1843 "@": "./src"
1844 }
1845 },
1846 test: {
1847 include: ["src/**/*.test.ts"]
1848 }
1849 };
1850 "#;
1851 let include = extract_config_string_array(source, &js_path(), &["test", "include"]);
1852 assert_eq!(include, vec!["src/**/*.test.ts"]);
1853 }
1854
1855 #[test]
1856 fn extract_addons_array() {
1857 let source = r#"
1858 export default {
1859 addons: [
1860 "@storybook/addon-a11y",
1861 "@storybook/addon-docs",
1862 "@storybook/addon-links"
1863 ]
1864 };
1865 "#;
1866 let addons = extract_config_property_strings(source, &ts_path(), "addons");
1867 assert_eq!(
1868 addons,
1869 vec![
1870 "@storybook/addon-a11y",
1871 "@storybook/addon-docs",
1872 "@storybook/addon-links"
1873 ]
1874 );
1875 }
1876
1877 #[test]
1878 fn handle_empty_config() {
1879 let source = "";
1880 let result = extract_config_string(source, &js_path(), &["key"]);
1881 assert_eq!(result, None);
1882 }
1883
1884 #[test]
1887 fn object_keys_postcss_plugins() {
1888 let source = r"
1889 module.exports = {
1890 plugins: {
1891 autoprefixer: {},
1892 tailwindcss: {},
1893 'postcss-import': {}
1894 }
1895 };
1896 ";
1897 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1898 assert_eq!(keys, vec!["autoprefixer", "tailwindcss", "postcss-import"]);
1899 }
1900
1901 #[test]
1902 fn object_keys_nested_path() {
1903 let source = r"
1904 export default {
1905 build: {
1906 plugins: {
1907 minify: {},
1908 compress: {}
1909 }
1910 }
1911 };
1912 ";
1913 let keys = extract_config_object_keys(source, &js_path(), &["build", "plugins"]);
1914 assert_eq!(keys, vec!["minify", "compress"]);
1915 }
1916
1917 #[test]
1918 fn object_keys_empty_object() {
1919 let source = r"export default { plugins: {} };";
1920 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1921 assert!(keys.is_empty());
1922 }
1923
1924 #[test]
1925 fn object_keys_non_object_returns_empty() {
1926 let source = r#"export default { plugins: ["a", "b"] };"#;
1927 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1928 assert!(keys.is_empty());
1929 }
1930
1931 #[test]
1934 fn string_or_array_single_string() {
1935 let source = r#"export default { entry: "./src/index.js" };"#;
1936 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1937 assert_eq!(result, vec!["./src/index.js"]);
1938 }
1939
1940 #[test]
1941 fn string_or_array_array() {
1942 let source = r#"export default { entry: ["./src/a.js", "./src/b.js"] };"#;
1943 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1944 assert_eq!(result, vec!["./src/a.js", "./src/b.js"]);
1945 }
1946
1947 #[test]
1948 fn string_or_array_object_values() {
1949 let source =
1950 r#"export default { entry: { main: "./src/main.js", vendor: "./src/vendor.js" } };"#;
1951 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1952 assert_eq!(result, vec!["./src/main.js", "./src/vendor.js"]);
1953 }
1954
1955 #[test]
1956 fn string_or_array_object_array_values() {
1957 let source = r#"export default { entry: { app: ["./src/polyfill.js", "./src/app.js"] } };"#;
1958 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1959 assert_eq!(result, vec!["./src/polyfill.js", "./src/app.js"]);
1960 }
1961
1962 #[test]
1963 fn string_or_array_webpack_entry_descriptors() {
1964 let source = r#"
1965 export default {
1966 entry: {
1967 app: {
1968 import: "./src/app.js",
1969 filename: "pages/app.js",
1970 dependOn: "shared",
1971 },
1972 admin: {
1973 import: ["./src/admin-polyfill.js", "./src/admin.js"],
1974 runtime: "runtime",
1975 },
1976 shared: ["react", "react-dom"],
1977 },
1978 };
1979 "#;
1980 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1981 assert_eq!(
1982 result,
1983 vec![
1984 "./src/app.js",
1985 "./src/admin-polyfill.js",
1986 "./src/admin.js",
1987 "react",
1988 "react-dom"
1989 ]
1990 );
1991 }
1992
1993 #[test]
1994 fn string_or_array_nested_path() {
1995 let source = r#"
1996 export default {
1997 build: {
1998 rollupOptions: {
1999 input: ["./index.html", "./about.html"]
2000 }
2001 }
2002 };
2003 "#;
2004 let result = extract_config_string_or_array(
2005 source,
2006 &js_path(),
2007 &["build", "rollupOptions", "input"],
2008 );
2009 assert_eq!(result, vec!["./index.html", "./about.html"]);
2010 }
2011
2012 #[test]
2013 fn string_or_array_template_literal() {
2014 let source = r"export default { entry: `./src/index.js` };";
2015 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2016 assert_eq!(result, vec!["./src/index.js"]);
2017 }
2018
2019 #[test]
2020 fn string_or_array_object_path_helper_values() {
2021 let source = r#"
2024 import { resolve, join } from "node:path";
2025 import path from "node:path";
2026 export default {
2027 build: {
2028 rollupOptions: {
2029 input: {
2030 app: resolve(__dirname, "src/app.ts"),
2031 modal: path.resolve(__dirname, "src/modal.ts"),
2032 tabs: join(__dirname, "src/tabs.ts"),
2033 styles: resolve(__dirname, "src/index.css"),
2034 },
2035 },
2036 },
2037 };
2038 "#;
2039 let result = extract_config_string_or_array(
2040 source,
2041 &js_path(),
2042 &["build", "rollupOptions", "input"],
2043 );
2044 assert_eq!(
2045 result,
2046 vec!["src/app.ts", "src/modal.ts", "src/tabs.ts", "src/index.css"]
2047 );
2048 }
2049
2050 #[test]
2051 fn string_or_array_array_path_helper_values() {
2052 let source = r#"
2053 import { resolve } from "node:path";
2054 export default {
2055 build: {
2056 rollupOptions: {
2057 input: [resolve(__dirname, "src/a.ts"), "./src/b.ts"],
2058 },
2059 },
2060 };
2061 "#;
2062 let result = extract_config_string_or_array(
2063 source,
2064 &js_path(),
2065 &["build", "rollupOptions", "input"],
2066 );
2067 assert_eq!(result, vec!["src/a.ts", "./src/b.ts"]);
2068 }
2069
2070 #[test]
2071 fn string_or_array_top_level_path_helper_call() {
2072 let source = r#"
2073 import { resolve } from "node:path";
2074 export default { build: { lib: { entry: resolve(__dirname, "src/index.ts") } } };
2075 "#;
2076 let result = extract_config_string_or_array(source, &js_path(), &["build", "lib", "entry"]);
2077 assert_eq!(result, vec!["src/index.ts"]);
2078 }
2079
2080 #[test]
2081 fn string_or_array_import_meta_dirname_anchor() {
2082 let source = r#"
2083 import { resolve } from "node:path";
2084 export default {
2085 build: { lib: { entry: resolve(import.meta.dirname, "src/index.ts") } },
2086 };
2087 "#;
2088 let result = extract_config_string_or_array(source, &ts_path(), &["build", "lib", "entry"]);
2089 assert_eq!(result, vec!["src/index.ts"]);
2090 }
2091
2092 #[test]
2093 fn string_or_array_non_literal_path_helper_args_dropped() {
2094 let source = r#"
2097 import { resolve } from "node:path";
2098 export default { build: { lib: { entry: resolve(baseDir, "src/index.ts") } } };
2099 "#;
2100 let result = extract_config_string_or_array(source, &js_path(), &["build", "lib", "entry"]);
2101 assert!(
2102 result.is_empty(),
2103 "non-literal path-helper args must be dropped: {result:?}"
2104 );
2105 }
2106
2107 #[test]
2110 fn require_strings_array() {
2111 let source = r"
2112 module.exports = {
2113 plugins: [
2114 require('autoprefixer'),
2115 require('postcss-import')
2116 ]
2117 };
2118 ";
2119 let deps = extract_config_require_strings(source, &js_path(), "plugins");
2120 assert_eq!(deps, vec!["autoprefixer", "postcss-import"]);
2121 }
2122
2123 #[test]
2124 fn require_strings_with_tuples() {
2125 let source = r"
2126 module.exports = {
2127 plugins: [
2128 require('autoprefixer'),
2129 [require('postcss-preset-env'), { stage: 3 }]
2130 ]
2131 };
2132 ";
2133 let deps = extract_config_require_strings(source, &js_path(), "plugins");
2134 assert_eq!(deps, vec!["autoprefixer", "postcss-preset-env"]);
2135 }
2136
2137 #[test]
2138 fn require_strings_empty_array() {
2139 let source = r"module.exports = { plugins: [] };";
2140 let deps = extract_config_require_strings(source, &js_path(), "plugins");
2141 assert!(deps.is_empty());
2142 }
2143
2144 #[test]
2145 fn require_strings_no_require_calls() {
2146 let source = r#"module.exports = { plugins: ["a", "b"] };"#;
2147 let deps = extract_config_require_strings(source, &js_path(), "plugins");
2148 assert!(deps.is_empty());
2149 }
2150
2151 #[test]
2152 fn extract_aliases_from_object_with_file_url_to_path() {
2153 let source = r#"
2154 import { defineConfig } from 'vite';
2155 import { fileURLToPath, URL } from 'node:url';
2156
2157 export default defineConfig({
2158 resolve: {
2159 alias: {
2160 "@": fileURLToPath(new URL("./src", import.meta.url))
2161 }
2162 }
2163 });
2164 "#;
2165
2166 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
2167 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
2168 }
2169
2170 #[test]
2171 fn extract_aliases_from_array_form() {
2172 let source = r#"
2173 export default {
2174 resolve: {
2175 alias: [
2176 { find: "@", replacement: "./src" },
2177 { find: "$utils", replacement: "src/lib/utils" }
2178 ]
2179 }
2180 };
2181 "#;
2182
2183 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
2184 assert_eq!(
2185 aliases,
2186 vec![
2187 ("@".to_string(), "./src".to_string()),
2188 ("$utils".to_string(), "src/lib/utils".to_string())
2189 ]
2190 );
2191 }
2192
2193 #[test]
2194 fn extract_aliases_from_object_with_array_values() {
2195 let source = r#"
2196 ({
2197 compilerOptions: {
2198 paths: {
2199 "@/*": ["./src/*"],
2200 "@shared/*": ["./shared/*", "./fallback/*"]
2201 }
2202 }
2203 })
2204 "#;
2205
2206 let aliases = extract_config_aliases(source, &js_path(), &["compilerOptions", "paths"]);
2207 assert_eq!(
2208 aliases,
2209 vec![
2210 ("@/*".to_string(), "./src/*".to_string()),
2211 ("@shared/*".to_string(), "./shared/*".to_string())
2212 ]
2213 );
2214 }
2215
2216 #[test]
2217 fn extract_array_object_strings_mixed_forms() {
2218 let source = r#"
2219 export default {
2220 components: [
2221 "~/components",
2222 { path: "@/feature-components" }
2223 ]
2224 };
2225 "#;
2226
2227 let values =
2228 extract_config_array_object_strings(source, &ts_path(), &["components"], "path");
2229 assert_eq!(
2230 values,
2231 vec![
2232 "~/components".to_string(),
2233 "@/feature-components".to_string()
2234 ]
2235 );
2236 }
2237
2238 #[test]
2239 fn extract_array_object_string_pairs_with_and_without_secondary() {
2240 let source = r#"
2241 export default {
2242 webServer: [
2243 { command: "tsx scripts/api.ts", cwd: "packages/api" },
2244 { command: "tsx scripts/web.ts" }
2245 ]
2246 };
2247 "#;
2248
2249 let pairs = extract_config_array_object_string_pairs(
2250 source,
2251 &ts_path(),
2252 &["webServer"],
2253 "command",
2254 "cwd",
2255 );
2256 assert_eq!(
2257 pairs,
2258 vec![
2259 (
2260 "tsx scripts/api.ts".to_string(),
2261 Some("packages/api".to_string())
2262 ),
2263 ("tsx scripts/web.ts".to_string(), None),
2264 ]
2265 );
2266 }
2267
2268 #[test]
2269 fn extract_array_object_string_pairs_skips_elements_missing_primary() {
2270 let source = r#"
2271 export default {
2272 webServer: [
2273 { cwd: "packages/api" },
2274 { command: "srvx --port 3000" }
2275 ]
2276 };
2277 "#;
2278
2279 let pairs = extract_config_array_object_string_pairs(
2280 source,
2281 &ts_path(),
2282 &["webServer"],
2283 "command",
2284 "cwd",
2285 );
2286 assert_eq!(pairs, vec![("srvx --port 3000".to_string(), None)]);
2287 }
2288
2289 #[test]
2290 fn extract_array_object_string_pairs_empty_for_object_form() {
2291 let source = r#"
2294 export default {
2295 webServer: { command: "srvx --port 3000" }
2296 };
2297 "#;
2298
2299 let pairs = extract_config_array_object_string_pairs(
2300 source,
2301 &ts_path(),
2302 &["webServer"],
2303 "command",
2304 "cwd",
2305 );
2306 assert!(pairs.is_empty());
2307 }
2308
2309 #[test]
2310 fn extract_config_plugin_option_string_from_json() {
2311 let source = r#"{
2312 "expo": {
2313 "plugins": [
2314 ["expo-router", { "root": "src/app" }]
2315 ]
2316 }
2317 }"#;
2318
2319 let value = extract_config_plugin_option_string(
2320 source,
2321 &json_path(),
2322 &["expo", "plugins"],
2323 "expo-router",
2324 "root",
2325 );
2326
2327 assert_eq!(value, Some("src/app".to_string()));
2328 }
2329
2330 #[test]
2331 fn extract_config_plugin_option_string_from_top_level_plugins() {
2332 let source = r#"{
2333 "plugins": [
2334 ["expo-router", { "root": "./src/routes" }]
2335 ]
2336 }"#;
2337
2338 let value = extract_config_plugin_option_string_from_paths(
2339 source,
2340 &json_path(),
2341 &[&["plugins"], &["expo", "plugins"]],
2342 "expo-router",
2343 "root",
2344 );
2345
2346 assert_eq!(value, Some("./src/routes".to_string()));
2347 }
2348
2349 #[test]
2350 fn extract_config_plugin_option_string_from_ts_config() {
2351 let source = r"
2352 export default {
2353 expo: {
2354 plugins: [
2355 ['expo-router', { root: './src/app' }]
2356 ]
2357 }
2358 };
2359 ";
2360
2361 let value = extract_config_plugin_option_string(
2362 source,
2363 &ts_path(),
2364 &["expo", "plugins"],
2365 "expo-router",
2366 "root",
2367 );
2368
2369 assert_eq!(value, Some("./src/app".to_string()));
2370 }
2371
2372 #[test]
2373 fn extract_config_plugin_option_string_returns_none_when_plugin_missing() {
2374 let source = r#"{
2375 "expo": {
2376 "plugins": [
2377 ["expo-font", {}]
2378 ]
2379 }
2380 }"#;
2381
2382 let value = extract_config_plugin_option_string(
2383 source,
2384 &json_path(),
2385 &["expo", "plugins"],
2386 "expo-router",
2387 "root",
2388 );
2389
2390 assert_eq!(value, None);
2391 }
2392
2393 #[test]
2394 fn normalize_config_path_relative_to_root() {
2395 let config_path = PathBuf::from("/project/vite.config.ts");
2396 let root = PathBuf::from("/project");
2397
2398 assert_eq!(
2399 normalize_config_path("./src/lib", &config_path, &root),
2400 Some("src/lib".to_string())
2401 );
2402 assert_eq!(
2403 normalize_config_path("/src/lib", &config_path, &root),
2404 Some("src/lib".to_string())
2405 );
2406 }
2407
2408 #[test]
2411 fn json_wrapped_in_parens_string() {
2412 let source = r#"({"extends": "@tsconfig/node18/tsconfig.json"})"#;
2413 let val = extract_config_string(source, &js_path(), &["extends"]);
2414 assert_eq!(val, Some("@tsconfig/node18/tsconfig.json".to_string()));
2415 }
2416
2417 #[test]
2418 fn json_wrapped_in_parens_nested_array() {
2419 let source =
2420 r#"({"compilerOptions": {"types": ["node", "jest"]}, "include": ["src/**/*"]})"#;
2421 let types = extract_config_string_array(source, &js_path(), &["compilerOptions", "types"]);
2422 assert_eq!(types, vec!["node", "jest"]);
2423
2424 let include = extract_config_string_array(source, &js_path(), &["include"]);
2425 assert_eq!(include, vec!["src/**/*"]);
2426 }
2427
2428 #[test]
2429 fn json_wrapped_in_parens_object_keys() {
2430 let source = r#"({"plugins": {"autoprefixer": {}, "tailwindcss": {}}})"#;
2431 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
2432 assert_eq!(keys, vec!["autoprefixer", "tailwindcss"]);
2433 }
2434
2435 fn json_path() -> PathBuf {
2438 PathBuf::from("config.json")
2439 }
2440
2441 #[test]
2442 fn json_file_parsed_correctly() {
2443 let source = r#"{"key": "value", "list": ["a", "b"]}"#;
2444 let val = extract_config_string(source, &json_path(), &["key"]);
2445 assert_eq!(val, Some("value".to_string()));
2446
2447 let list = extract_config_string_array(source, &json_path(), &["list"]);
2448 assert_eq!(list, vec!["a", "b"]);
2449 }
2450
2451 #[test]
2452 fn jsonc_file_parsed_correctly() {
2453 let source = r#"{"key": "value"}"#;
2454 let path = PathBuf::from("tsconfig.jsonc");
2455 let val = extract_config_string(source, &path, &["key"]);
2456 assert_eq!(val, Some("value".to_string()));
2457 }
2458
2459 #[test]
2462 fn extract_define_config_arrow_function() {
2463 let source = r#"
2464 import { defineConfig } from 'vite';
2465 export default defineConfig(() => ({
2466 test: {
2467 include: ["**/*.test.ts"]
2468 }
2469 }));
2470 "#;
2471 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
2472 assert_eq!(include, vec!["**/*.test.ts"]);
2473 }
2474
2475 #[test]
2476 fn extract_config_from_default_export_function_declaration() {
2477 let source = r#"
2478 export default function createConfig() {
2479 return {
2480 clientModules: ["./src/client/global.js"]
2481 };
2482 }
2483 "#;
2484
2485 let client_modules = extract_config_string_array(source, &ts_path(), &["clientModules"]);
2486 assert_eq!(client_modules, vec!["./src/client/global.js"]);
2487 }
2488
2489 #[test]
2490 fn extract_config_from_default_export_async_function_declaration() {
2491 let source = r#"
2492 export default async function createConfigAsync() {
2493 return {
2494 docs: {
2495 path: "knowledge"
2496 }
2497 };
2498 }
2499 "#;
2500
2501 let docs_path = extract_config_string(source, &ts_path(), &["docs", "path"]);
2502 assert_eq!(docs_path, Some("knowledge".to_string()));
2503 }
2504
2505 #[test]
2506 fn extract_config_from_exported_arrow_function_identifier() {
2507 let source = r#"
2508 const config = async () => {
2509 return {
2510 themes: ["classic"]
2511 };
2512 };
2513
2514 export default config;
2515 "#;
2516
2517 let themes = extract_config_shallow_strings(source, &ts_path(), "themes");
2518 assert_eq!(themes, vec!["classic"]);
2519 }
2520
2521 #[test]
2524 fn module_exports_nested_string() {
2525 let source = r#"
2526 module.exports = {
2527 resolve: {
2528 alias: {
2529 "@": "./src"
2530 }
2531 }
2532 };
2533 "#;
2534 let val = extract_config_string(source, &js_path(), &["resolve", "alias", "@"]);
2535 assert_eq!(val, Some("./src".to_string()));
2536 }
2537
2538 #[test]
2541 fn property_strings_nested_objects() {
2542 let source = r#"
2543 export default {
2544 plugins: {
2545 group1: { a: "val-a" },
2546 group2: { b: "val-b" }
2547 }
2548 };
2549 "#;
2550 let values = extract_config_property_strings(source, &js_path(), "plugins");
2551 assert!(values.contains(&"val-a".to_string()));
2552 assert!(values.contains(&"val-b".to_string()));
2553 }
2554
2555 #[test]
2556 fn property_strings_missing_key_returns_empty() {
2557 let source = r#"export default { other: "value" };"#;
2558 let values = extract_config_property_strings(source, &js_path(), "missing");
2559 assert!(values.is_empty());
2560 }
2561
2562 #[test]
2565 fn shallow_strings_tuple_array() {
2566 let source = r#"
2567 module.exports = {
2568 reporters: ["default", ["jest-junit", { outputDirectory: "reports" }]]
2569 };
2570 "#;
2571 let values = extract_config_shallow_strings(source, &js_path(), "reporters");
2572 assert_eq!(values, vec!["default", "jest-junit"]);
2573 assert!(!values.contains(&"reports".to_string()));
2575 }
2576
2577 #[test]
2578 fn shallow_strings_single_string() {
2579 let source = r#"export default { preset: "ts-jest" };"#;
2580 let values = extract_config_shallow_strings(source, &js_path(), "preset");
2581 assert_eq!(values, vec!["ts-jest"]);
2582 }
2583
2584 #[test]
2585 fn shallow_strings_missing_key() {
2586 let source = r#"export default { other: "val" };"#;
2587 let values = extract_config_shallow_strings(source, &js_path(), "missing");
2588 assert!(values.is_empty());
2589 }
2590
2591 #[test]
2592 fn shallow_strings_or_object_property_alias_objects() {
2593 let source = r#"
2594 export default {
2595 jsPlugins: [
2596 "eslint-plugin-playwright",
2597 ["eslint-plugin-regexp", { rules: {} }],
2598 { name: "short", specifier: "eslint-plugin-with-long-name" }
2599 ]
2600 };
2601 "#;
2602 let values = extract_config_shallow_strings_or_object_property(
2603 source,
2604 &ts_path(),
2605 "jsPlugins",
2606 "specifier",
2607 );
2608 assert_eq!(
2609 values,
2610 vec![
2611 "eslint-plugin-playwright",
2612 "eslint-plugin-regexp",
2613 "eslint-plugin-with-long-name"
2614 ]
2615 );
2616 }
2617
2618 #[test]
2621 fn nested_shallow_strings_vitest_reporters() {
2622 let source = r#"
2623 export default {
2624 test: {
2625 reporters: ["default", "vitest-sonar-reporter"]
2626 }
2627 };
2628 "#;
2629 let values =
2630 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
2631 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
2632 }
2633
2634 #[test]
2635 fn nested_shallow_strings_tuple_format() {
2636 let source = r#"
2637 export default {
2638 test: {
2639 reporters: ["default", ["vitest-sonar-reporter", { outputFile: "report.xml" }]]
2640 }
2641 };
2642 "#;
2643 let values =
2644 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
2645 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
2646 }
2647
2648 #[test]
2649 fn nested_shallow_strings_missing_outer() {
2650 let source = r"export default { other: {} };";
2651 let values =
2652 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
2653 assert!(values.is_empty());
2654 }
2655
2656 #[test]
2657 fn nested_shallow_strings_missing_inner() {
2658 let source = r#"export default { test: { include: ["**/*.test.ts"] } };"#;
2659 let values =
2660 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
2661 assert!(values.is_empty());
2662 }
2663
2664 #[test]
2667 fn string_or_array_missing_path() {
2668 let source = r"export default {};";
2669 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2670 assert!(result.is_empty());
2671 }
2672
2673 #[test]
2674 fn string_or_array_non_string_values() {
2675 let source = r"export default { entry: [42, true] };";
2677 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2678 assert!(result.is_empty());
2679 }
2680
2681 #[test]
2684 fn array_nested_extraction() {
2685 let source = r#"
2686 export default defineConfig({
2687 test: {
2688 projects: [
2689 {
2690 test: {
2691 setupFiles: ["./test/setup-a.ts"]
2692 }
2693 },
2694 {
2695 test: {
2696 setupFiles: "./test/setup-b.ts"
2697 }
2698 }
2699 ]
2700 }
2701 });
2702 "#;
2703 let results = extract_config_array_nested_string_or_array(
2704 source,
2705 &ts_path(),
2706 &["test", "projects"],
2707 &["test", "setupFiles"],
2708 );
2709 assert!(results.contains(&"./test/setup-a.ts".to_string()));
2710 assert!(results.contains(&"./test/setup-b.ts".to_string()));
2711 }
2712
2713 #[test]
2714 fn array_nested_empty_when_no_array() {
2715 let source = r#"export default { test: { projects: "not-an-array" } };"#;
2716 let results = extract_config_array_nested_string_or_array(
2717 source,
2718 &js_path(),
2719 &["test", "projects"],
2720 &["test", "setupFiles"],
2721 );
2722 assert!(results.is_empty());
2723 }
2724
2725 #[test]
2728 fn object_nested_extraction() {
2729 let source = r#"{
2730 "projects": {
2731 "app-one": {
2732 "architect": {
2733 "build": {
2734 "options": {
2735 "styles": ["src/styles.css"]
2736 }
2737 }
2738 }
2739 }
2740 }
2741 }"#;
2742 let results = extract_config_object_nested_string_or_array(
2743 source,
2744 &json_path(),
2745 &["projects"],
2746 &["architect", "build", "options", "styles"],
2747 );
2748 assert_eq!(results, vec!["src/styles.css"]);
2749 }
2750
2751 #[test]
2752 fn array_with_object_input_form_extracted() {
2753 let source = r#"{
2759 "projects": {
2760 "app": {
2761 "architect": {
2762 "build": {
2763 "options": {
2764 "styles": [
2765 "src/styles.scss",
2766 { "input": "src/theme.scss", "bundleName": "theme", "inject": false },
2767 { "bundleName": "lazy-only" }
2768 ]
2769 }
2770 }
2771 }
2772 }
2773 }
2774 }"#;
2775 let results = extract_config_object_nested_string_or_array(
2776 source,
2777 &json_path(),
2778 &["projects"],
2779 &["architect", "build", "options", "styles"],
2780 );
2781 assert!(
2782 results.contains(&"src/styles.scss".to_string()),
2783 "string form must still work: {results:?}"
2784 );
2785 assert!(
2786 results.contains(&"src/theme.scss".to_string()),
2787 "object form with `input` must be extracted: {results:?}"
2788 );
2789 assert!(
2792 !results.contains(&"lazy-only".to_string()),
2793 "bundleName must not be misinterpreted as a path: {results:?}"
2794 );
2795 assert!(
2796 !results.contains(&"theme".to_string()),
2797 "bundleName from full object must not leak: {results:?}"
2798 );
2799 }
2800
2801 #[test]
2804 fn object_nested_strings_extraction() {
2805 let source = r#"{
2806 "targets": {
2807 "build": {
2808 "executor": "@angular/build:application"
2809 },
2810 "test": {
2811 "executor": "@nx/vite:test"
2812 }
2813 }
2814 }"#;
2815 let results =
2816 extract_config_object_nested_strings(source, &json_path(), &["targets"], &["executor"]);
2817 assert!(results.contains(&"@angular/build:application".to_string()));
2818 assert!(results.contains(&"@nx/vite:test".to_string()));
2819 }
2820
2821 #[test]
2824 fn require_strings_direct_call() {
2825 let source = r"module.exports = { adapter: require('@sveltejs/adapter-node') };";
2826 let deps = extract_config_require_strings(source, &js_path(), "adapter");
2827 assert_eq!(deps, vec!["@sveltejs/adapter-node"]);
2828 }
2829
2830 #[test]
2831 fn require_strings_no_matching_key() {
2832 let source = r"module.exports = { other: require('something') };";
2833 let deps = extract_config_require_strings(source, &js_path(), "plugins");
2834 assert!(deps.is_empty());
2835 }
2836
2837 #[test]
2840 fn extract_imports_no_imports() {
2841 let source = r"export default {};";
2842 let imports = extract_imports(source, &js_path());
2843 assert!(imports.is_empty());
2844 }
2845
2846 #[test]
2847 fn extract_imports_side_effect_import() {
2848 let source = r"
2849 import 'polyfill';
2850 import './local-setup';
2851 export default {};
2852 ";
2853 let imports = extract_imports(source, &js_path());
2854 assert_eq!(imports, vec!["polyfill", "./local-setup"]);
2855 }
2856
2857 #[test]
2858 fn extract_imports_mixed_specifiers() {
2859 let source = r"
2860 import defaultExport from 'module-a';
2861 import { named } from 'module-b';
2862 import * as ns from 'module-c';
2863 export default {};
2864 ";
2865 let imports = extract_imports(source, &js_path());
2866 assert_eq!(imports, vec!["module-a", "module-b", "module-c"]);
2867 }
2868
2869 #[test]
2872 fn template_literal_in_string_or_array() {
2873 let source = r"export default { entry: `./src/index.ts` };";
2874 let result = extract_config_string_or_array(source, &ts_path(), &["entry"]);
2875 assert_eq!(result, vec!["./src/index.ts"]);
2876 }
2877
2878 #[test]
2879 fn template_literal_in_config_string() {
2880 let source = r"export default { testDir: `./tests` };";
2881 let val = extract_config_string(source, &js_path(), &["testDir"]);
2882 assert_eq!(val, Some("./tests".to_string()));
2883 }
2884
2885 #[test]
2888 fn nested_string_array_empty_path() {
2889 let source = r#"export default { items: ["a", "b"] };"#;
2890 let result = extract_config_string_array(source, &js_path(), &[]);
2891 assert!(result.is_empty());
2892 }
2893
2894 #[test]
2895 fn nested_string_empty_path() {
2896 let source = r#"export default { key: "val" };"#;
2897 let result = extract_config_string(source, &js_path(), &[]);
2898 assert!(result.is_none());
2899 }
2900
2901 #[test]
2902 fn object_keys_empty_path() {
2903 let source = r"export default { plugins: {} };";
2904 let result = extract_config_object_keys(source, &js_path(), &[]);
2905 assert!(result.is_empty());
2906 }
2907
2908 #[test]
2911 fn no_config_object_returns_empty() {
2912 let source = r"const x = 42;";
2914 let result = extract_config_string(source, &js_path(), &["key"]);
2915 assert!(result.is_none());
2916
2917 let arr = extract_config_string_array(source, &js_path(), &["items"]);
2918 assert!(arr.is_empty());
2919
2920 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
2921 assert!(keys.is_empty());
2922 }
2923
2924 #[test]
2927 fn property_with_string_key() {
2928 let source = r#"export default { "string-key": "value" };"#;
2929 let val = extract_config_string(source, &js_path(), &["string-key"]);
2930 assert_eq!(val, Some("value".to_string()));
2931 }
2932
2933 #[test]
2934 fn nested_navigation_through_non_object() {
2935 let source = r#"export default { level1: "not-an-object" };"#;
2937 let val = extract_config_string(source, &js_path(), &["level1", "level2"]);
2938 assert!(val.is_none());
2939 }
2940
2941 #[test]
2944 fn variable_reference_untyped() {
2945 let source = r#"
2946 const config = {
2947 testDir: "./tests"
2948 };
2949 export default config;
2950 "#;
2951 let val = extract_config_string(source, &js_path(), &["testDir"]);
2952 assert_eq!(val, Some("./tests".to_string()));
2953 }
2954
2955 #[test]
2956 fn variable_reference_with_type_annotation() {
2957 let source = r#"
2958 import type { StorybookConfig } from '@storybook/react-vite';
2959 const config: StorybookConfig = {
2960 addons: ["@storybook/addon-a11y", "@storybook/addon-docs"],
2961 framework: "@storybook/react-vite"
2962 };
2963 export default config;
2964 "#;
2965 let addons = extract_config_shallow_strings(source, &ts_path(), "addons");
2966 assert_eq!(
2967 addons,
2968 vec!["@storybook/addon-a11y", "@storybook/addon-docs"]
2969 );
2970
2971 let framework = extract_config_string(source, &ts_path(), &["framework"]);
2972 assert_eq!(framework, Some("@storybook/react-vite".to_string()));
2973 }
2974
2975 #[test]
2976 fn variable_reference_with_define_config() {
2977 let source = r#"
2978 import { defineConfig } from 'vitest/config';
2979 const config = defineConfig({
2980 test: {
2981 include: ["**/*.test.ts"]
2982 }
2983 });
2984 export default config;
2985 "#;
2986 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
2987 assert_eq!(include, vec!["**/*.test.ts"]);
2988 }
2989
2990 #[test]
2993 fn ts_satisfies_direct_export() {
2994 let source = r#"
2995 export default {
2996 testDir: "./tests"
2997 } satisfies PlaywrightTestConfig;
2998 "#;
2999 let val = extract_config_string(source, &ts_path(), &["testDir"]);
3000 assert_eq!(val, Some("./tests".to_string()));
3001 }
3002
3003 #[test]
3004 fn ts_as_direct_export() {
3005 let source = r#"
3006 export default {
3007 testDir: "./tests"
3008 } as const;
3009 "#;
3010 let val = extract_config_string(source, &ts_path(), &["testDir"]);
3011 assert_eq!(val, Some("./tests".to_string()));
3012 }
3013}