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]
427pub fn extract_config_array_object_strings(
428 source: &str,
429 path: &Path,
430 array_path: &[&str],
431 key: &str,
432) -> Vec<String> {
433 extract_from_source(source, path, |program| {
434 let obj = find_config_object(program)?;
435 let array_expr = get_nested_expression(obj, array_path)?;
436 let Expression::ArrayExpression(arr) = array_expr else {
437 return None;
438 };
439
440 let mut results = Vec::new();
441 for element in &arr.elements {
442 let Some(expr) = element.as_expression() else {
443 continue;
444 };
445 match expr {
446 Expression::ObjectExpression(item) => {
447 if let Some(prop) = find_property(item, key)
448 && let Some(value) = expression_to_path_string(&prop.value)
449 {
450 results.push(value);
451 }
452 }
453 _ => {
454 if let Some(value) = expression_to_path_string(expr) {
455 results.push(value);
456 }
457 }
458 }
459 }
460
461 (!results.is_empty()).then_some(results)
462 })
463 .unwrap_or_default()
464}
465
466#[must_use]
512pub fn extract_lazy_imports_in_array(source: &str, path: &Path, prop_path: &[&str]) -> Vec<String> {
513 extract_from_source(source, path, |program| {
514 let obj = find_config_object(program)?;
515 let array_expr = get_nested_expression(obj, prop_path)?;
516 let Expression::ArrayExpression(arr) = array_expr else {
517 return None;
518 };
519 let mut specs = Vec::new();
520 for element in &arr.elements {
521 let Some(expr) = element.as_expression() else {
522 continue;
523 };
524 if let Some(spec) = lazy_import_specifier(expr) {
525 specs.push(spec);
526 }
527 }
528 (!specs.is_empty()).then_some(specs)
529 })
530 .unwrap_or_default()
531}
532
533fn lazy_import_specifier(expr: &Expression<'_>) -> Option<String> {
546 let callable = match expr {
547 Expression::ObjectExpression(obj) => &find_property(obj, "file")?.value,
548 _ => expr,
549 };
550 let import_expr = extract_import_from_callable(callable)?;
551 expression_to_string(&import_expr.source)
552}
553
554#[must_use]
561pub fn extract_config_plugin_option_string(
562 source: &str,
563 path: &Path,
564 plugins_path: &[&str],
565 plugin_name: &str,
566 option_key: &str,
567) -> Option<String> {
568 extract_from_source(source, path, |program| {
569 let obj = find_config_object(program)?;
570 let plugins_expr = get_nested_expression(obj, plugins_path)?;
571 let Expression::ArrayExpression(plugins) = plugins_expr else {
572 return None;
573 };
574
575 for entry in &plugins.elements {
576 let Some(Expression::ArrayExpression(tuple)) = entry.as_expression() else {
577 continue;
578 };
579 let Some(plugin_expr) = tuple
580 .elements
581 .first()
582 .and_then(ArrayExpressionElement::as_expression)
583 else {
584 continue;
585 };
586 if expression_to_string(plugin_expr).as_deref() != Some(plugin_name) {
587 continue;
588 }
589
590 let Some(options_expr) = tuple
591 .elements
592 .get(1)
593 .and_then(ArrayExpressionElement::as_expression)
594 else {
595 continue;
596 };
597 let Expression::ObjectExpression(options_obj) = options_expr else {
598 continue;
599 };
600 let option = find_property(options_obj, option_key)?;
601 return expression_to_path_string(&option.value);
602 }
603
604 None
605 })
606}
607
608#[must_use]
610pub fn extract_config_plugin_option_string_from_paths(
611 source: &str,
612 path: &Path,
613 plugin_paths: &[&[&str]],
614 plugin_name: &str,
615 option_key: &str,
616) -> Option<String> {
617 plugin_paths.iter().find_map(|plugins_path| {
618 extract_config_plugin_option_string(source, path, plugins_path, plugin_name, option_key)
619 })
620}
621
622#[must_use]
627pub fn normalize_config_path(raw: &str, config_path: &Path, root: &Path) -> Option<String> {
628 if raw.is_empty() {
629 return None;
630 }
631
632 let candidate = if let Some(stripped) = raw.strip_prefix('/') {
633 lexical_normalize(&root.join(stripped))
634 } else {
635 let path = Path::new(raw);
636 if path.is_absolute() {
637 lexical_normalize(path)
638 } else {
639 let base = config_path.parent().unwrap_or(root);
640 lexical_normalize(&base.join(path))
641 }
642 };
643
644 let relative = candidate.strip_prefix(root).ok()?;
645 let normalized = relative.to_string_lossy().replace('\\', "/");
646 (!normalized.is_empty()).then_some(normalized)
647}
648
649fn extract_from_source<T>(
658 source: &str,
659 path: &Path,
660 extractor: impl FnOnce(&Program) -> Option<T>,
661) -> Option<T> {
662 let source_type = SourceType::from_path(path).unwrap_or_default();
663 let alloc = Allocator::default();
664
665 let is_json = path
668 .extension()
669 .is_some_and(|ext| ext == "json" || ext == "jsonc");
670 if is_json {
671 let wrapped = format!("({source})");
672 let parsed = Parser::new(&alloc, &wrapped, SourceType::mjs()).parse();
673 return extractor(&parsed.program);
674 }
675
676 let parsed = Parser::new(&alloc, source, source_type).parse();
677 extractor(&parsed.program)
678}
679
680fn find_config_object<'a>(program: &'a Program) -> Option<&'a ObjectExpression<'a>> {
692 for stmt in &program.body {
693 match stmt {
694 Statement::ExportDefaultDeclaration(decl) => {
696 let expr: Option<&Expression> = match &decl.declaration {
698 ExportDefaultDeclarationKind::ObjectExpression(obj) => {
699 return Some(obj);
700 }
701 ExportDefaultDeclarationKind::FunctionDeclaration(func) => {
702 return extract_object_from_function(func);
703 }
704 _ => decl.declaration.as_expression(),
705 };
706 if let Some(expr) = expr {
707 if let Some(obj) = extract_object_from_expression(expr) {
709 return Some(obj);
710 }
711 if let Some(name) = unwrap_to_identifier_name(expr) {
714 return find_variable_init_object(program, name);
715 }
716 }
717 }
718 Statement::ExpressionStatement(expr_stmt) => {
720 if let Expression::AssignmentExpression(assign) = &expr_stmt.expression
721 && is_module_exports_target(&assign.left)
722 {
723 return extract_object_from_expression(&assign.right);
724 }
725 }
726 _ => {}
727 }
728 }
729
730 if program.body.len() == 1
733 && let Statement::ExpressionStatement(expr_stmt) = &program.body[0]
734 {
735 match &expr_stmt.expression {
736 Expression::ObjectExpression(obj) => return Some(obj),
737 Expression::ParenthesizedExpression(paren) => {
738 if let Expression::ObjectExpression(obj) = &paren.expression {
739 return Some(obj);
740 }
741 }
742 _ => {}
743 }
744 }
745
746 None
747}
748
749fn extract_object_from_expression<'a>(
751 expr: &'a Expression<'a>,
752) -> Option<&'a ObjectExpression<'a>> {
753 match expr {
754 Expression::ObjectExpression(obj) => Some(obj),
756 Expression::CallExpression(call) => {
758 for arg in &call.arguments {
760 match arg {
761 Argument::ObjectExpression(obj) => return Some(obj),
762 Argument::ArrowFunctionExpression(arrow) => {
764 if arrow.expression
765 && !arrow.body.statements.is_empty()
766 && let Statement::ExpressionStatement(expr_stmt) =
767 &arrow.body.statements[0]
768 {
769 return extract_object_from_expression(&expr_stmt.expression);
770 }
771 }
772 _ => {}
773 }
774 }
775 None
776 }
777 Expression::ParenthesizedExpression(paren) => {
779 extract_object_from_expression(&paren.expression)
780 }
781 Expression::TSSatisfiesExpression(ts_sat) => {
783 extract_object_from_expression(&ts_sat.expression)
784 }
785 Expression::TSAsExpression(ts_as) => extract_object_from_expression(&ts_as.expression),
786 Expression::ArrowFunctionExpression(arrow) => extract_object_from_arrow_function(arrow),
787 Expression::FunctionExpression(func) => extract_object_from_function(func),
788 _ => None,
789 }
790}
791
792fn extract_object_from_arrow_function<'a>(
793 arrow: &'a ArrowFunctionExpression<'a>,
794) -> Option<&'a ObjectExpression<'a>> {
795 if arrow.expression {
796 arrow.body.statements.first().and_then(|stmt| {
797 if let Statement::ExpressionStatement(expr_stmt) = stmt {
798 extract_object_from_expression(&expr_stmt.expression)
799 } else {
800 None
801 }
802 })
803 } else {
804 extract_object_from_function_body(&arrow.body)
805 }
806}
807
808fn extract_object_from_function<'a>(func: &'a Function<'a>) -> Option<&'a ObjectExpression<'a>> {
809 func.body
810 .as_ref()
811 .and_then(|body| extract_object_from_function_body(body))
812}
813
814fn extract_object_from_function_body<'a>(
815 body: &'a FunctionBody<'a>,
816) -> Option<&'a ObjectExpression<'a>> {
817 for stmt in &body.statements {
818 if let Statement::ReturnStatement(ret) = stmt
819 && let Some(argument) = &ret.argument
820 && let Some(obj) = extract_object_from_expression(argument)
821 {
822 return Some(obj);
823 }
824 }
825 None
826}
827
828fn is_module_exports_target(target: &AssignmentTarget) -> bool {
830 if let AssignmentTarget::StaticMemberExpression(member) = target
831 && let Expression::Identifier(obj) = &member.object
832 {
833 return obj.name == "module" && member.property.name == "exports";
834 }
835 false
836}
837
838fn unwrap_to_identifier_name<'a>(expr: &'a Expression<'a>) -> Option<&'a str> {
842 match expr {
843 Expression::Identifier(id) => Some(&id.name),
844 Expression::TSSatisfiesExpression(ts_sat) => unwrap_to_identifier_name(&ts_sat.expression),
845 Expression::TSAsExpression(ts_as) => unwrap_to_identifier_name(&ts_as.expression),
846 _ => None,
847 }
848}
849
850fn find_variable_init_object<'a>(
855 program: &'a Program,
856 name: &str,
857) -> Option<&'a ObjectExpression<'a>> {
858 for stmt in &program.body {
859 if let Statement::VariableDeclaration(decl) = stmt {
860 for declarator in &decl.declarations {
861 if let BindingPattern::BindingIdentifier(id) = &declarator.id
862 && id.name == name
863 && let Some(init) = &declarator.init
864 {
865 return extract_object_from_expression(init);
866 }
867 }
868 }
869 }
870 None
871}
872
873pub(crate) fn find_property<'a>(
875 obj: &'a ObjectExpression<'a>,
876 key: &str,
877) -> Option<&'a ObjectProperty<'a>> {
878 for prop in &obj.properties {
879 if let ObjectPropertyKind::ObjectProperty(p) = prop
880 && property_key_matches(&p.key, key)
881 {
882 return Some(p);
883 }
884 }
885 None
886}
887
888pub(crate) fn property_key_matches(key: &PropertyKey, name: &str) -> bool {
890 match key {
891 PropertyKey::StaticIdentifier(id) => id.name == name,
892 PropertyKey::StringLiteral(s) => s.value == name,
893 _ => false,
894 }
895}
896
897fn get_object_string_property(obj: &ObjectExpression, key: &str) -> Option<String> {
899 find_property(obj, key).and_then(|p| expression_to_string(&p.value))
900}
901
902fn get_object_string_array_property(obj: &ObjectExpression, key: &str) -> Vec<String> {
904 find_property(obj, key)
905 .map(|p| expression_to_string_array(&p.value))
906 .unwrap_or_default()
907}
908
909fn get_nested_string_array_from_object(
911 obj: &ObjectExpression,
912 path: &[&str],
913) -> Option<Vec<String>> {
914 if path.is_empty() {
915 return None;
916 }
917 if path.len() == 1 {
918 return Some(get_object_string_array_property(obj, path[0]));
919 }
920 let prop = find_property(obj, path[0])?;
922 if let Expression::ObjectExpression(nested) = &prop.value {
923 get_nested_string_array_from_object(nested, &path[1..])
924 } else {
925 None
926 }
927}
928
929fn get_nested_string_from_object(obj: &ObjectExpression, path: &[&str]) -> Option<String> {
931 if path.is_empty() {
932 return None;
933 }
934 if path.len() == 1 {
935 return get_object_string_property(obj, path[0]);
936 }
937 let prop = find_property(obj, path[0])?;
938 if let Expression::ObjectExpression(nested) = &prop.value {
939 get_nested_string_from_object(nested, &path[1..])
940 } else {
941 None
942 }
943}
944
945pub(crate) fn expression_to_string(expr: &Expression) -> Option<String> {
947 match expr {
948 Expression::StringLiteral(s) => Some(s.value.to_string()),
949 Expression::TemplateLiteral(t) if t.expressions.is_empty() => {
950 t.quasis.first().map(|q| q.value.raw.to_string())
952 }
953 _ => None,
954 }
955}
956
957pub(crate) fn expression_to_path_string(expr: &Expression) -> Option<String> {
959 match expr {
960 Expression::ParenthesizedExpression(paren) => expression_to_path_string(&paren.expression),
961 Expression::TSAsExpression(ts_as) => expression_to_path_string(&ts_as.expression),
962 Expression::TSSatisfiesExpression(ts_sat) => expression_to_path_string(&ts_sat.expression),
963 Expression::CallExpression(call) => call_expression_to_path_string(call),
964 Expression::NewExpression(new_expr) => new_expression_to_path_string(new_expr),
965 _ => expression_to_string(expr),
966 }
967}
968
969fn call_expression_to_path_string(call: &CallExpression) -> Option<String> {
970 if matches!(&call.callee, Expression::Identifier(id) if id.name == "fileURLToPath") {
971 return call
972 .arguments
973 .first()
974 .and_then(Argument::as_expression)
975 .and_then(expression_to_path_string);
976 }
977
978 let callee_name = match &call.callee {
979 Expression::Identifier(id) => Some(id.name.as_str()),
980 Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
981 _ => None,
982 }?;
983
984 if !matches!(callee_name, "resolve" | "join") {
985 return None;
986 }
987
988 let mut segments = Vec::new();
989 for (index, arg) in call.arguments.iter().enumerate() {
990 let expr = arg.as_expression()?;
991
992 if matches!(expr, Expression::Identifier(id) if id.name == "__dirname") {
993 if index == 0 {
994 continue;
995 }
996 return None;
997 }
998
999 segments.push(expression_to_string(expr)?);
1000 }
1001
1002 (!segments.is_empty()).then(|| join_path_segments(&segments))
1003}
1004
1005fn new_expression_to_path_string(new_expr: &NewExpression) -> Option<String> {
1006 if !matches!(&new_expr.callee, Expression::Identifier(id) if id.name == "URL") {
1007 return None;
1008 }
1009
1010 let source = new_expr
1011 .arguments
1012 .first()
1013 .and_then(Argument::as_expression)
1014 .and_then(expression_to_string)?;
1015
1016 let base = new_expr
1017 .arguments
1018 .get(1)
1019 .and_then(Argument::as_expression)?;
1020 is_import_meta_url_expression(base).then_some(source)
1021}
1022
1023fn is_import_meta_url_expression(expr: &Expression) -> bool {
1024 if let Expression::StaticMemberExpression(member) = expr {
1025 member.property.name == "url" && matches!(member.object, Expression::MetaProperty(_))
1026 } else {
1027 false
1028 }
1029}
1030
1031fn join_path_segments(segments: &[String]) -> String {
1032 let mut joined = PathBuf::new();
1033 for segment in segments {
1034 joined.push(segment);
1035 }
1036 joined.to_string_lossy().replace('\\', "/")
1037}
1038
1039fn expression_to_alias_pairs(expr: &Expression) -> Vec<(String, String)> {
1040 match expr {
1041 Expression::ObjectExpression(obj) => obj
1042 .properties
1043 .iter()
1044 .filter_map(|prop| {
1045 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
1046 return None;
1047 };
1048 let find = property_key_to_string(&prop.key)?;
1049 let replacement = expression_to_path_values(&prop.value).into_iter().next()?;
1050 Some((find, replacement))
1051 })
1052 .collect(),
1053 Expression::ArrayExpression(arr) => arr
1054 .elements
1055 .iter()
1056 .filter_map(|element| {
1057 let Expression::ObjectExpression(obj) = element.as_expression()? else {
1058 return None;
1059 };
1060 let find = find_property(obj, "find")
1061 .and_then(|prop| expression_to_string(&prop.value))?;
1062 let replacement = find_property(obj, "replacement")
1063 .and_then(|prop| expression_to_path_string(&prop.value))?;
1064 Some((find, replacement))
1065 })
1066 .collect(),
1067 _ => Vec::new(),
1068 }
1069}
1070
1071fn lexical_normalize(path: &Path) -> PathBuf {
1072 let mut normalized = PathBuf::new();
1073
1074 for component in path.components() {
1075 match component {
1076 std::path::Component::CurDir => {}
1077 std::path::Component::ParentDir => {
1078 normalized.pop();
1079 }
1080 _ => normalized.push(component.as_os_str()),
1081 }
1082 }
1083
1084 normalized
1085}
1086
1087fn expression_to_string_array(expr: &Expression) -> Vec<String> {
1089 match expr {
1090 Expression::ArrayExpression(arr) => arr
1091 .elements
1092 .iter()
1093 .filter_map(|el| match el {
1094 ArrayExpressionElement::SpreadElement(_) => None,
1095 _ => el.as_expression().and_then(expression_to_string),
1096 })
1097 .collect(),
1098 _ => vec![],
1099 }
1100}
1101
1102fn collect_shallow_string_values(expr: &Expression) -> Vec<String> {
1107 let mut values = Vec::new();
1108 match expr {
1109 Expression::StringLiteral(s) => {
1110 values.push(s.value.to_string());
1111 }
1112 Expression::ArrayExpression(arr) => {
1113 for el in &arr.elements {
1114 if let Some(inner) = el.as_expression() {
1115 match inner {
1116 Expression::StringLiteral(s) => {
1117 values.push(s.value.to_string());
1118 }
1119 Expression::ArrayExpression(sub_arr) => {
1121 if let Some(first) = sub_arr.elements.first()
1122 && let Some(first_expr) = first.as_expression()
1123 && let Some(s) = expression_to_string(first_expr)
1124 {
1125 values.push(s);
1126 }
1127 }
1128 _ => {}
1129 }
1130 }
1131 }
1132 }
1133 Expression::ObjectExpression(obj) => {
1135 for prop in &obj.properties {
1136 if let ObjectPropertyKind::ObjectProperty(p) = prop {
1137 match &p.value {
1138 Expression::StringLiteral(s) => {
1139 values.push(s.value.to_string());
1140 }
1141 Expression::ArrayExpression(sub_arr) => {
1143 if let Some(first) = sub_arr.elements.first()
1144 && let Some(first_expr) = first.as_expression()
1145 && let Some(s) = expression_to_string(first_expr)
1146 {
1147 values.push(s);
1148 }
1149 }
1150 _ => {}
1151 }
1152 }
1153 }
1154 }
1155 _ => {}
1156 }
1157 values
1158}
1159
1160fn collect_shallow_string_or_object_property_values(
1162 expr: &Expression,
1163 object_property: &str,
1164) -> Vec<String> {
1165 match expr {
1166 Expression::ArrayExpression(arr) => arr
1167 .elements
1168 .iter()
1169 .filter_map(|element| {
1170 element
1171 .as_expression()
1172 .and_then(|expr| shallow_string_or_object_property(expr, object_property))
1173 })
1174 .collect(),
1175 _ => shallow_string_or_object_property(expr, object_property)
1176 .into_iter()
1177 .collect(),
1178 }
1179}
1180
1181fn shallow_string_or_object_property(expr: &Expression, object_property: &str) -> Option<String> {
1182 match expr {
1183 Expression::ParenthesizedExpression(paren) => {
1184 shallow_string_or_object_property(&paren.expression, object_property)
1185 }
1186 Expression::TSSatisfiesExpression(ts_sat) => {
1187 shallow_string_or_object_property(&ts_sat.expression, object_property)
1188 }
1189 Expression::TSAsExpression(ts_as) => {
1190 shallow_string_or_object_property(&ts_as.expression, object_property)
1191 }
1192 Expression::ArrayExpression(sub_arr) => sub_arr
1193 .elements
1194 .first()
1195 .and_then(ArrayExpressionElement::as_expression)
1196 .and_then(expression_to_string),
1197 Expression::ObjectExpression(obj) => {
1198 find_property(obj, object_property).and_then(|prop| expression_to_string(&prop.value))
1199 }
1200 _ => expression_to_string(expr),
1201 }
1202}
1203
1204fn collect_all_string_values(expr: &Expression, values: &mut Vec<String>) {
1206 match expr {
1207 Expression::StringLiteral(s) => {
1208 values.push(s.value.to_string());
1209 }
1210 Expression::ArrayExpression(arr) => {
1211 for el in &arr.elements {
1212 if let Some(expr) = el.as_expression() {
1213 collect_all_string_values(expr, values);
1214 }
1215 }
1216 }
1217 Expression::ObjectExpression(obj) => {
1218 for prop in &obj.properties {
1219 if let ObjectPropertyKind::ObjectProperty(p) = prop {
1220 collect_all_string_values(&p.value, values);
1221 }
1222 }
1223 }
1224 _ => {}
1225 }
1226}
1227
1228fn property_key_to_string(key: &PropertyKey) -> Option<String> {
1230 match key {
1231 PropertyKey::StaticIdentifier(id) => Some(id.name.to_string()),
1232 PropertyKey::StringLiteral(s) => Some(s.value.to_string()),
1233 _ => None,
1234 }
1235}
1236
1237fn get_nested_object_keys(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
1239 if path.is_empty() {
1240 return None;
1241 }
1242 let prop = find_property(obj, path[0])?;
1243 if path.len() == 1 {
1244 if let Expression::ObjectExpression(nested) = &prop.value {
1245 let keys = nested
1246 .properties
1247 .iter()
1248 .filter_map(|p| {
1249 if let ObjectPropertyKind::ObjectProperty(p) = p {
1250 property_key_to_string(&p.key)
1251 } else {
1252 None
1253 }
1254 })
1255 .collect();
1256 return Some(keys);
1257 }
1258 return None;
1259 }
1260 if let Expression::ObjectExpression(nested) = &prop.value {
1261 get_nested_object_keys(nested, &path[1..])
1262 } else {
1263 None
1264 }
1265}
1266
1267fn get_nested_expression<'a>(
1269 obj: &'a ObjectExpression<'a>,
1270 path: &[&str],
1271) -> Option<&'a Expression<'a>> {
1272 if path.is_empty() {
1273 return None;
1274 }
1275 let prop = find_property(obj, path[0])?;
1276 if path.len() == 1 {
1277 return Some(&prop.value);
1278 }
1279 if let Expression::ObjectExpression(nested) = &prop.value {
1280 get_nested_expression(nested, &path[1..])
1281 } else {
1282 None
1283 }
1284}
1285
1286fn get_nested_string_or_array(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
1288 if path.is_empty() {
1289 return None;
1290 }
1291 if path.len() == 1 {
1292 let prop = find_property(obj, path[0])?;
1293 return Some(expression_to_string_or_array(&prop.value));
1294 }
1295 let prop = find_property(obj, path[0])?;
1296 if let Expression::ObjectExpression(nested) = &prop.value {
1297 get_nested_string_or_array(nested, &path[1..])
1298 } else {
1299 None
1300 }
1301}
1302
1303fn expression_to_string_or_array(expr: &Expression) -> Vec<String> {
1311 match expr {
1312 Expression::StringLiteral(s) => vec![s.value.to_string()],
1313 Expression::TemplateLiteral(t) if t.expressions.is_empty() => t
1314 .quasis
1315 .first()
1316 .map(|q| vec![q.value.raw.to_string()])
1317 .unwrap_or_default(),
1318 Expression::ArrayExpression(arr) => arr
1319 .elements
1320 .iter()
1321 .filter_map(|el| el.as_expression())
1322 .flat_map(|e| match e {
1323 Expression::ObjectExpression(obj) => find_property(obj, "input")
1324 .map(|p| expression_to_string_or_array(&p.value))
1325 .unwrap_or_default(),
1326 _ => expression_to_string(e).into_iter().collect(),
1327 })
1328 .collect(),
1329 Expression::ObjectExpression(obj) => obj
1330 .properties
1331 .iter()
1332 .flat_map(|p| {
1333 if let ObjectPropertyKind::ObjectProperty(p) = p {
1334 match &p.value {
1335 Expression::ArrayExpression(_) => expression_to_string_or_array(&p.value),
1336 Expression::ObjectExpression(value_obj) => {
1337 find_property(value_obj, "import")
1338 .map(|import_prop| {
1339 expression_to_string_or_array(&import_prop.value)
1340 })
1341 .unwrap_or_default()
1342 }
1343 _ => expression_to_string(&p.value).into_iter().collect(),
1344 }
1345 } else {
1346 Vec::new()
1347 }
1348 })
1349 .collect(),
1350 _ => vec![],
1351 }
1352}
1353
1354fn collect_require_sources(expr: &Expression) -> Vec<String> {
1356 let mut sources = Vec::new();
1357 match expr {
1358 Expression::CallExpression(call) if is_require_call(call) => {
1359 if let Some(s) = get_require_source(call) {
1360 sources.push(s);
1361 }
1362 }
1363 Expression::ArrayExpression(arr) => {
1364 for el in &arr.elements {
1365 if let Some(inner) = el.as_expression() {
1366 match inner {
1367 Expression::CallExpression(call) if is_require_call(call) => {
1368 if let Some(s) = get_require_source(call) {
1369 sources.push(s);
1370 }
1371 }
1372 Expression::ArrayExpression(sub_arr) => {
1374 if let Some(first) = sub_arr.elements.first()
1375 && let Some(Expression::CallExpression(call)) =
1376 first.as_expression()
1377 && is_require_call(call)
1378 && let Some(s) = get_require_source(call)
1379 {
1380 sources.push(s);
1381 }
1382 }
1383 _ => {}
1384 }
1385 }
1386 }
1387 }
1388 _ => {}
1389 }
1390 sources
1391}
1392
1393fn is_require_call(call: &CallExpression) -> bool {
1395 matches!(&call.callee, Expression::Identifier(id) if id.name == "require")
1396}
1397
1398fn get_require_source(call: &CallExpression) -> Option<String> {
1400 call.arguments.first().and_then(|arg| {
1401 if let Argument::StringLiteral(s) = arg {
1402 Some(s.value.to_string())
1403 } else {
1404 None
1405 }
1406 })
1407}
1408
1409#[cfg(test)]
1410mod tests {
1411 use super::*;
1412 use std::path::PathBuf;
1413
1414 fn js_path() -> PathBuf {
1415 PathBuf::from("config.js")
1416 }
1417
1418 fn ts_path() -> PathBuf {
1419 PathBuf::from("config.ts")
1420 }
1421
1422 #[test]
1423 fn extract_lazy_imports_bare_arrows() {
1424 let source = r"
1425 import { defineConfig } from '@adonisjs/core/app'
1426 export default defineConfig({
1427 preloads: [
1428 () => import('#start/routes'),
1429 () => import('#start/kernel'),
1430 ],
1431 })
1432 ";
1433 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["preloads"]);
1434 assert_eq!(specs, vec!["#start/routes", "#start/kernel"]);
1435 }
1436
1437 #[test]
1438 fn extract_lazy_imports_object_form_with_file_key() {
1439 let source = r"
1440 export default defineConfig({
1441 providers: [
1442 () => import('@adonisjs/core/providers/app_provider'),
1443 {
1444 file: () => import('@adonisjs/core/providers/repl_provider'),
1445 environment: ['repl', 'test'],
1446 },
1447 ],
1448 })
1449 ";
1450 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["providers"]);
1451 assert_eq!(
1452 specs,
1453 vec![
1454 "@adonisjs/core/providers/app_provider",
1455 "@adonisjs/core/providers/repl_provider",
1456 ]
1457 );
1458 }
1459
1460 #[test]
1461 fn extract_lazy_imports_block_body_with_return() {
1462 let source = r"
1464 export default defineConfig({
1465 commands: [
1466 () => { return import('@adonisjs/core/commands') },
1467 ],
1468 })
1469 ";
1470 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["commands"]);
1471 assert_eq!(specs, vec!["@adonisjs/core/commands"]);
1472 }
1473
1474 #[test]
1475 fn extract_lazy_imports_skips_unknown_element_shapes() {
1476 let source = r"
1479 export default defineConfig({
1480 commands: [
1481 'string-entry',
1482 42,
1483 { other: 'value' },
1484 () => import('@adonisjs/lucid/commands'),
1485 ],
1486 })
1487 ";
1488 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["commands"]);
1489 assert_eq!(specs, vec!["@adonisjs/lucid/commands"]);
1490 }
1491
1492 #[test]
1493 fn extract_lazy_imports_missing_property_returns_empty() {
1494 let source = r"
1495 export default defineConfig({
1496 preloads: [() => import('#start/routes')],
1497 })
1498 ";
1499 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["providers"]);
1500 assert!(specs.is_empty());
1501 }
1502
1503 #[test]
1504 fn extract_imports_basic() {
1505 let source = r"
1506 import foo from 'foo-pkg';
1507 import { bar } from '@scope/bar';
1508 export default {};
1509 ";
1510 let imports = extract_imports(source, &js_path());
1511 assert_eq!(imports, vec!["foo-pkg", "@scope/bar"]);
1512 }
1513
1514 #[test]
1515 fn extract_default_export_object_property() {
1516 let source = r#"export default { testDir: "./tests" };"#;
1517 let val = extract_config_string(source, &js_path(), &["testDir"]);
1518 assert_eq!(val, Some("./tests".to_string()));
1519 }
1520
1521 #[test]
1522 fn extract_define_config_property() {
1523 let source = r#"
1524 import { defineConfig } from 'vitest/config';
1525 export default defineConfig({
1526 test: {
1527 include: ["**/*.test.ts", "**/*.spec.ts"],
1528 setupFiles: ["./test/setup.ts"]
1529 }
1530 });
1531 "#;
1532 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
1533 assert_eq!(include, vec!["**/*.test.ts", "**/*.spec.ts"]);
1534
1535 let setup = extract_config_string_array(source, &ts_path(), &["test", "setupFiles"]);
1536 assert_eq!(setup, vec!["./test/setup.ts"]);
1537 }
1538
1539 #[test]
1540 fn extract_module_exports_property() {
1541 let source = r#"module.exports = { testEnvironment: "jsdom" };"#;
1542 let val = extract_config_string(source, &js_path(), &["testEnvironment"]);
1543 assert_eq!(val, Some("jsdom".to_string()));
1544 }
1545
1546 #[test]
1547 fn extract_nested_string_array() {
1548 let source = r#"
1549 export default {
1550 resolve: {
1551 alias: {
1552 "@": "./src"
1553 }
1554 },
1555 test: {
1556 include: ["src/**/*.test.ts"]
1557 }
1558 };
1559 "#;
1560 let include = extract_config_string_array(source, &js_path(), &["test", "include"]);
1561 assert_eq!(include, vec!["src/**/*.test.ts"]);
1562 }
1563
1564 #[test]
1565 fn extract_addons_array() {
1566 let source = r#"
1567 export default {
1568 addons: [
1569 "@storybook/addon-a11y",
1570 "@storybook/addon-docs",
1571 "@storybook/addon-links"
1572 ]
1573 };
1574 "#;
1575 let addons = extract_config_property_strings(source, &ts_path(), "addons");
1576 assert_eq!(
1577 addons,
1578 vec![
1579 "@storybook/addon-a11y",
1580 "@storybook/addon-docs",
1581 "@storybook/addon-links"
1582 ]
1583 );
1584 }
1585
1586 #[test]
1587 fn handle_empty_config() {
1588 let source = "";
1589 let result = extract_config_string(source, &js_path(), &["key"]);
1590 assert_eq!(result, None);
1591 }
1592
1593 #[test]
1596 fn object_keys_postcss_plugins() {
1597 let source = r"
1598 module.exports = {
1599 plugins: {
1600 autoprefixer: {},
1601 tailwindcss: {},
1602 'postcss-import': {}
1603 }
1604 };
1605 ";
1606 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1607 assert_eq!(keys, vec!["autoprefixer", "tailwindcss", "postcss-import"]);
1608 }
1609
1610 #[test]
1611 fn object_keys_nested_path() {
1612 let source = r"
1613 export default {
1614 build: {
1615 plugins: {
1616 minify: {},
1617 compress: {}
1618 }
1619 }
1620 };
1621 ";
1622 let keys = extract_config_object_keys(source, &js_path(), &["build", "plugins"]);
1623 assert_eq!(keys, vec!["minify", "compress"]);
1624 }
1625
1626 #[test]
1627 fn object_keys_empty_object() {
1628 let source = r"export default { plugins: {} };";
1629 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1630 assert!(keys.is_empty());
1631 }
1632
1633 #[test]
1634 fn object_keys_non_object_returns_empty() {
1635 let source = r#"export default { plugins: ["a", "b"] };"#;
1636 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1637 assert!(keys.is_empty());
1638 }
1639
1640 #[test]
1643 fn string_or_array_single_string() {
1644 let source = r#"export default { entry: "./src/index.js" };"#;
1645 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1646 assert_eq!(result, vec!["./src/index.js"]);
1647 }
1648
1649 #[test]
1650 fn string_or_array_array() {
1651 let source = r#"export default { entry: ["./src/a.js", "./src/b.js"] };"#;
1652 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1653 assert_eq!(result, vec!["./src/a.js", "./src/b.js"]);
1654 }
1655
1656 #[test]
1657 fn string_or_array_object_values() {
1658 let source =
1659 r#"export default { entry: { main: "./src/main.js", vendor: "./src/vendor.js" } };"#;
1660 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1661 assert_eq!(result, vec!["./src/main.js", "./src/vendor.js"]);
1662 }
1663
1664 #[test]
1665 fn string_or_array_object_array_values() {
1666 let source = r#"export default { entry: { app: ["./src/polyfill.js", "./src/app.js"] } };"#;
1667 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1668 assert_eq!(result, vec!["./src/polyfill.js", "./src/app.js"]);
1669 }
1670
1671 #[test]
1672 fn string_or_array_webpack_entry_descriptors() {
1673 let source = r#"
1674 export default {
1675 entry: {
1676 app: {
1677 import: "./src/app.js",
1678 filename: "pages/app.js",
1679 dependOn: "shared",
1680 },
1681 admin: {
1682 import: ["./src/admin-polyfill.js", "./src/admin.js"],
1683 runtime: "runtime",
1684 },
1685 shared: ["react", "react-dom"],
1686 },
1687 };
1688 "#;
1689 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1690 assert_eq!(
1691 result,
1692 vec![
1693 "./src/app.js",
1694 "./src/admin-polyfill.js",
1695 "./src/admin.js",
1696 "react",
1697 "react-dom"
1698 ]
1699 );
1700 }
1701
1702 #[test]
1703 fn string_or_array_nested_path() {
1704 let source = r#"
1705 export default {
1706 build: {
1707 rollupOptions: {
1708 input: ["./index.html", "./about.html"]
1709 }
1710 }
1711 };
1712 "#;
1713 let result = extract_config_string_or_array(
1714 source,
1715 &js_path(),
1716 &["build", "rollupOptions", "input"],
1717 );
1718 assert_eq!(result, vec!["./index.html", "./about.html"]);
1719 }
1720
1721 #[test]
1722 fn string_or_array_template_literal() {
1723 let source = r"export default { entry: `./src/index.js` };";
1724 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1725 assert_eq!(result, vec!["./src/index.js"]);
1726 }
1727
1728 #[test]
1731 fn require_strings_array() {
1732 let source = r"
1733 module.exports = {
1734 plugins: [
1735 require('autoprefixer'),
1736 require('postcss-import')
1737 ]
1738 };
1739 ";
1740 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1741 assert_eq!(deps, vec!["autoprefixer", "postcss-import"]);
1742 }
1743
1744 #[test]
1745 fn require_strings_with_tuples() {
1746 let source = r"
1747 module.exports = {
1748 plugins: [
1749 require('autoprefixer'),
1750 [require('postcss-preset-env'), { stage: 3 }]
1751 ]
1752 };
1753 ";
1754 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1755 assert_eq!(deps, vec!["autoprefixer", "postcss-preset-env"]);
1756 }
1757
1758 #[test]
1759 fn require_strings_empty_array() {
1760 let source = r"module.exports = { plugins: [] };";
1761 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1762 assert!(deps.is_empty());
1763 }
1764
1765 #[test]
1766 fn require_strings_no_require_calls() {
1767 let source = r#"module.exports = { plugins: ["a", "b"] };"#;
1768 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1769 assert!(deps.is_empty());
1770 }
1771
1772 #[test]
1773 fn extract_aliases_from_object_with_file_url_to_path() {
1774 let source = r#"
1775 import { defineConfig } from 'vite';
1776 import { fileURLToPath, URL } from 'node:url';
1777
1778 export default defineConfig({
1779 resolve: {
1780 alias: {
1781 "@": fileURLToPath(new URL("./src", import.meta.url))
1782 }
1783 }
1784 });
1785 "#;
1786
1787 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
1788 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
1789 }
1790
1791 #[test]
1792 fn extract_aliases_from_array_form() {
1793 let source = r#"
1794 export default {
1795 resolve: {
1796 alias: [
1797 { find: "@", replacement: "./src" },
1798 { find: "$utils", replacement: "src/lib/utils" }
1799 ]
1800 }
1801 };
1802 "#;
1803
1804 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
1805 assert_eq!(
1806 aliases,
1807 vec![
1808 ("@".to_string(), "./src".to_string()),
1809 ("$utils".to_string(), "src/lib/utils".to_string())
1810 ]
1811 );
1812 }
1813
1814 #[test]
1815 fn extract_aliases_from_object_with_array_values() {
1816 let source = r#"
1817 ({
1818 compilerOptions: {
1819 paths: {
1820 "@/*": ["./src/*"],
1821 "@shared/*": ["./shared/*", "./fallback/*"]
1822 }
1823 }
1824 })
1825 "#;
1826
1827 let aliases = extract_config_aliases(source, &js_path(), &["compilerOptions", "paths"]);
1828 assert_eq!(
1829 aliases,
1830 vec![
1831 ("@/*".to_string(), "./src/*".to_string()),
1832 ("@shared/*".to_string(), "./shared/*".to_string())
1833 ]
1834 );
1835 }
1836
1837 #[test]
1838 fn extract_array_object_strings_mixed_forms() {
1839 let source = r#"
1840 export default {
1841 components: [
1842 "~/components",
1843 { path: "@/feature-components" }
1844 ]
1845 };
1846 "#;
1847
1848 let values =
1849 extract_config_array_object_strings(source, &ts_path(), &["components"], "path");
1850 assert_eq!(
1851 values,
1852 vec![
1853 "~/components".to_string(),
1854 "@/feature-components".to_string()
1855 ]
1856 );
1857 }
1858
1859 #[test]
1860 fn extract_config_plugin_option_string_from_json() {
1861 let source = r#"{
1862 "expo": {
1863 "plugins": [
1864 ["expo-router", { "root": "src/app" }]
1865 ]
1866 }
1867 }"#;
1868
1869 let value = extract_config_plugin_option_string(
1870 source,
1871 &json_path(),
1872 &["expo", "plugins"],
1873 "expo-router",
1874 "root",
1875 );
1876
1877 assert_eq!(value, Some("src/app".to_string()));
1878 }
1879
1880 #[test]
1881 fn extract_config_plugin_option_string_from_top_level_plugins() {
1882 let source = r#"{
1883 "plugins": [
1884 ["expo-router", { "root": "./src/routes" }]
1885 ]
1886 }"#;
1887
1888 let value = extract_config_plugin_option_string_from_paths(
1889 source,
1890 &json_path(),
1891 &[&["plugins"], &["expo", "plugins"]],
1892 "expo-router",
1893 "root",
1894 );
1895
1896 assert_eq!(value, Some("./src/routes".to_string()));
1897 }
1898
1899 #[test]
1900 fn extract_config_plugin_option_string_from_ts_config() {
1901 let source = r"
1902 export default {
1903 expo: {
1904 plugins: [
1905 ['expo-router', { root: './src/app' }]
1906 ]
1907 }
1908 };
1909 ";
1910
1911 let value = extract_config_plugin_option_string(
1912 source,
1913 &ts_path(),
1914 &["expo", "plugins"],
1915 "expo-router",
1916 "root",
1917 );
1918
1919 assert_eq!(value, Some("./src/app".to_string()));
1920 }
1921
1922 #[test]
1923 fn extract_config_plugin_option_string_returns_none_when_plugin_missing() {
1924 let source = r#"{
1925 "expo": {
1926 "plugins": [
1927 ["expo-font", {}]
1928 ]
1929 }
1930 }"#;
1931
1932 let value = extract_config_plugin_option_string(
1933 source,
1934 &json_path(),
1935 &["expo", "plugins"],
1936 "expo-router",
1937 "root",
1938 );
1939
1940 assert_eq!(value, None);
1941 }
1942
1943 #[test]
1944 fn normalize_config_path_relative_to_root() {
1945 let config_path = PathBuf::from("/project/vite.config.ts");
1946 let root = PathBuf::from("/project");
1947
1948 assert_eq!(
1949 normalize_config_path("./src/lib", &config_path, &root),
1950 Some("src/lib".to_string())
1951 );
1952 assert_eq!(
1953 normalize_config_path("/src/lib", &config_path, &root),
1954 Some("src/lib".to_string())
1955 );
1956 }
1957
1958 #[test]
1961 fn json_wrapped_in_parens_string() {
1962 let source = r#"({"extends": "@tsconfig/node18/tsconfig.json"})"#;
1963 let val = extract_config_string(source, &js_path(), &["extends"]);
1964 assert_eq!(val, Some("@tsconfig/node18/tsconfig.json".to_string()));
1965 }
1966
1967 #[test]
1968 fn json_wrapped_in_parens_nested_array() {
1969 let source =
1970 r#"({"compilerOptions": {"types": ["node", "jest"]}, "include": ["src/**/*"]})"#;
1971 let types = extract_config_string_array(source, &js_path(), &["compilerOptions", "types"]);
1972 assert_eq!(types, vec!["node", "jest"]);
1973
1974 let include = extract_config_string_array(source, &js_path(), &["include"]);
1975 assert_eq!(include, vec!["src/**/*"]);
1976 }
1977
1978 #[test]
1979 fn json_wrapped_in_parens_object_keys() {
1980 let source = r#"({"plugins": {"autoprefixer": {}, "tailwindcss": {}}})"#;
1981 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1982 assert_eq!(keys, vec!["autoprefixer", "tailwindcss"]);
1983 }
1984
1985 fn json_path() -> PathBuf {
1988 PathBuf::from("config.json")
1989 }
1990
1991 #[test]
1992 fn json_file_parsed_correctly() {
1993 let source = r#"{"key": "value", "list": ["a", "b"]}"#;
1994 let val = extract_config_string(source, &json_path(), &["key"]);
1995 assert_eq!(val, Some("value".to_string()));
1996
1997 let list = extract_config_string_array(source, &json_path(), &["list"]);
1998 assert_eq!(list, vec!["a", "b"]);
1999 }
2000
2001 #[test]
2002 fn jsonc_file_parsed_correctly() {
2003 let source = r#"{"key": "value"}"#;
2004 let path = PathBuf::from("tsconfig.jsonc");
2005 let val = extract_config_string(source, &path, &["key"]);
2006 assert_eq!(val, Some("value".to_string()));
2007 }
2008
2009 #[test]
2012 fn extract_define_config_arrow_function() {
2013 let source = r#"
2014 import { defineConfig } from 'vite';
2015 export default defineConfig(() => ({
2016 test: {
2017 include: ["**/*.test.ts"]
2018 }
2019 }));
2020 "#;
2021 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
2022 assert_eq!(include, vec!["**/*.test.ts"]);
2023 }
2024
2025 #[test]
2026 fn extract_config_from_default_export_function_declaration() {
2027 let source = r#"
2028 export default function createConfig() {
2029 return {
2030 clientModules: ["./src/client/global.js"]
2031 };
2032 }
2033 "#;
2034
2035 let client_modules = extract_config_string_array(source, &ts_path(), &["clientModules"]);
2036 assert_eq!(client_modules, vec!["./src/client/global.js"]);
2037 }
2038
2039 #[test]
2040 fn extract_config_from_default_export_async_function_declaration() {
2041 let source = r#"
2042 export default async function createConfigAsync() {
2043 return {
2044 docs: {
2045 path: "knowledge"
2046 }
2047 };
2048 }
2049 "#;
2050
2051 let docs_path = extract_config_string(source, &ts_path(), &["docs", "path"]);
2052 assert_eq!(docs_path, Some("knowledge".to_string()));
2053 }
2054
2055 #[test]
2056 fn extract_config_from_exported_arrow_function_identifier() {
2057 let source = r#"
2058 const config = async () => {
2059 return {
2060 themes: ["classic"]
2061 };
2062 };
2063
2064 export default config;
2065 "#;
2066
2067 let themes = extract_config_shallow_strings(source, &ts_path(), "themes");
2068 assert_eq!(themes, vec!["classic"]);
2069 }
2070
2071 #[test]
2074 fn module_exports_nested_string() {
2075 let source = r#"
2076 module.exports = {
2077 resolve: {
2078 alias: {
2079 "@": "./src"
2080 }
2081 }
2082 };
2083 "#;
2084 let val = extract_config_string(source, &js_path(), &["resolve", "alias", "@"]);
2085 assert_eq!(val, Some("./src".to_string()));
2086 }
2087
2088 #[test]
2091 fn property_strings_nested_objects() {
2092 let source = r#"
2093 export default {
2094 plugins: {
2095 group1: { a: "val-a" },
2096 group2: { b: "val-b" }
2097 }
2098 };
2099 "#;
2100 let values = extract_config_property_strings(source, &js_path(), "plugins");
2101 assert!(values.contains(&"val-a".to_string()));
2102 assert!(values.contains(&"val-b".to_string()));
2103 }
2104
2105 #[test]
2106 fn property_strings_missing_key_returns_empty() {
2107 let source = r#"export default { other: "value" };"#;
2108 let values = extract_config_property_strings(source, &js_path(), "missing");
2109 assert!(values.is_empty());
2110 }
2111
2112 #[test]
2115 fn shallow_strings_tuple_array() {
2116 let source = r#"
2117 module.exports = {
2118 reporters: ["default", ["jest-junit", { outputDirectory: "reports" }]]
2119 };
2120 "#;
2121 let values = extract_config_shallow_strings(source, &js_path(), "reporters");
2122 assert_eq!(values, vec!["default", "jest-junit"]);
2123 assert!(!values.contains(&"reports".to_string()));
2125 }
2126
2127 #[test]
2128 fn shallow_strings_single_string() {
2129 let source = r#"export default { preset: "ts-jest" };"#;
2130 let values = extract_config_shallow_strings(source, &js_path(), "preset");
2131 assert_eq!(values, vec!["ts-jest"]);
2132 }
2133
2134 #[test]
2135 fn shallow_strings_missing_key() {
2136 let source = r#"export default { other: "val" };"#;
2137 let values = extract_config_shallow_strings(source, &js_path(), "missing");
2138 assert!(values.is_empty());
2139 }
2140
2141 #[test]
2142 fn shallow_strings_or_object_property_alias_objects() {
2143 let source = r#"
2144 export default {
2145 jsPlugins: [
2146 "eslint-plugin-playwright",
2147 ["eslint-plugin-regexp", { rules: {} }],
2148 { name: "short", specifier: "eslint-plugin-with-long-name" }
2149 ]
2150 };
2151 "#;
2152 let values = extract_config_shallow_strings_or_object_property(
2153 source,
2154 &ts_path(),
2155 "jsPlugins",
2156 "specifier",
2157 );
2158 assert_eq!(
2159 values,
2160 vec![
2161 "eslint-plugin-playwright",
2162 "eslint-plugin-regexp",
2163 "eslint-plugin-with-long-name"
2164 ]
2165 );
2166 }
2167
2168 #[test]
2171 fn nested_shallow_strings_vitest_reporters() {
2172 let source = r#"
2173 export default {
2174 test: {
2175 reporters: ["default", "vitest-sonar-reporter"]
2176 }
2177 };
2178 "#;
2179 let values =
2180 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
2181 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
2182 }
2183
2184 #[test]
2185 fn nested_shallow_strings_tuple_format() {
2186 let source = r#"
2187 export default {
2188 test: {
2189 reporters: ["default", ["vitest-sonar-reporter", { outputFile: "report.xml" }]]
2190 }
2191 };
2192 "#;
2193 let values =
2194 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
2195 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
2196 }
2197
2198 #[test]
2199 fn nested_shallow_strings_missing_outer() {
2200 let source = r"export default { other: {} };";
2201 let values =
2202 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
2203 assert!(values.is_empty());
2204 }
2205
2206 #[test]
2207 fn nested_shallow_strings_missing_inner() {
2208 let source = r#"export default { test: { include: ["**/*.test.ts"] } };"#;
2209 let values =
2210 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
2211 assert!(values.is_empty());
2212 }
2213
2214 #[test]
2217 fn string_or_array_missing_path() {
2218 let source = r"export default {};";
2219 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2220 assert!(result.is_empty());
2221 }
2222
2223 #[test]
2224 fn string_or_array_non_string_values() {
2225 let source = r"export default { entry: [42, true] };";
2227 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2228 assert!(result.is_empty());
2229 }
2230
2231 #[test]
2234 fn array_nested_extraction() {
2235 let source = r#"
2236 export default defineConfig({
2237 test: {
2238 projects: [
2239 {
2240 test: {
2241 setupFiles: ["./test/setup-a.ts"]
2242 }
2243 },
2244 {
2245 test: {
2246 setupFiles: "./test/setup-b.ts"
2247 }
2248 }
2249 ]
2250 }
2251 });
2252 "#;
2253 let results = extract_config_array_nested_string_or_array(
2254 source,
2255 &ts_path(),
2256 &["test", "projects"],
2257 &["test", "setupFiles"],
2258 );
2259 assert!(results.contains(&"./test/setup-a.ts".to_string()));
2260 assert!(results.contains(&"./test/setup-b.ts".to_string()));
2261 }
2262
2263 #[test]
2264 fn array_nested_empty_when_no_array() {
2265 let source = r#"export default { test: { projects: "not-an-array" } };"#;
2266 let results = extract_config_array_nested_string_or_array(
2267 source,
2268 &js_path(),
2269 &["test", "projects"],
2270 &["test", "setupFiles"],
2271 );
2272 assert!(results.is_empty());
2273 }
2274
2275 #[test]
2278 fn object_nested_extraction() {
2279 let source = r#"{
2280 "projects": {
2281 "app-one": {
2282 "architect": {
2283 "build": {
2284 "options": {
2285 "styles": ["src/styles.css"]
2286 }
2287 }
2288 }
2289 }
2290 }
2291 }"#;
2292 let results = extract_config_object_nested_string_or_array(
2293 source,
2294 &json_path(),
2295 &["projects"],
2296 &["architect", "build", "options", "styles"],
2297 );
2298 assert_eq!(results, vec!["src/styles.css"]);
2299 }
2300
2301 #[test]
2302 fn array_with_object_input_form_extracted() {
2303 let source = r#"{
2309 "projects": {
2310 "app": {
2311 "architect": {
2312 "build": {
2313 "options": {
2314 "styles": [
2315 "src/styles.scss",
2316 { "input": "src/theme.scss", "bundleName": "theme", "inject": false },
2317 { "bundleName": "lazy-only" }
2318 ]
2319 }
2320 }
2321 }
2322 }
2323 }
2324 }"#;
2325 let results = extract_config_object_nested_string_or_array(
2326 source,
2327 &json_path(),
2328 &["projects"],
2329 &["architect", "build", "options", "styles"],
2330 );
2331 assert!(
2332 results.contains(&"src/styles.scss".to_string()),
2333 "string form must still work: {results:?}"
2334 );
2335 assert!(
2336 results.contains(&"src/theme.scss".to_string()),
2337 "object form with `input` must be extracted: {results:?}"
2338 );
2339 assert!(
2342 !results.contains(&"lazy-only".to_string()),
2343 "bundleName must not be misinterpreted as a path: {results:?}"
2344 );
2345 assert!(
2346 !results.contains(&"theme".to_string()),
2347 "bundleName from full object must not leak: {results:?}"
2348 );
2349 }
2350
2351 #[test]
2354 fn object_nested_strings_extraction() {
2355 let source = r#"{
2356 "targets": {
2357 "build": {
2358 "executor": "@angular/build:application"
2359 },
2360 "test": {
2361 "executor": "@nx/vite:test"
2362 }
2363 }
2364 }"#;
2365 let results =
2366 extract_config_object_nested_strings(source, &json_path(), &["targets"], &["executor"]);
2367 assert!(results.contains(&"@angular/build:application".to_string()));
2368 assert!(results.contains(&"@nx/vite:test".to_string()));
2369 }
2370
2371 #[test]
2374 fn require_strings_direct_call() {
2375 let source = r"module.exports = { adapter: require('@sveltejs/adapter-node') };";
2376 let deps = extract_config_require_strings(source, &js_path(), "adapter");
2377 assert_eq!(deps, vec!["@sveltejs/adapter-node"]);
2378 }
2379
2380 #[test]
2381 fn require_strings_no_matching_key() {
2382 let source = r"module.exports = { other: require('something') };";
2383 let deps = extract_config_require_strings(source, &js_path(), "plugins");
2384 assert!(deps.is_empty());
2385 }
2386
2387 #[test]
2390 fn extract_imports_no_imports() {
2391 let source = r"export default {};";
2392 let imports = extract_imports(source, &js_path());
2393 assert!(imports.is_empty());
2394 }
2395
2396 #[test]
2397 fn extract_imports_side_effect_import() {
2398 let source = r"
2399 import 'polyfill';
2400 import './local-setup';
2401 export default {};
2402 ";
2403 let imports = extract_imports(source, &js_path());
2404 assert_eq!(imports, vec!["polyfill", "./local-setup"]);
2405 }
2406
2407 #[test]
2408 fn extract_imports_mixed_specifiers() {
2409 let source = r"
2410 import defaultExport from 'module-a';
2411 import { named } from 'module-b';
2412 import * as ns from 'module-c';
2413 export default {};
2414 ";
2415 let imports = extract_imports(source, &js_path());
2416 assert_eq!(imports, vec!["module-a", "module-b", "module-c"]);
2417 }
2418
2419 #[test]
2422 fn template_literal_in_string_or_array() {
2423 let source = r"export default { entry: `./src/index.ts` };";
2424 let result = extract_config_string_or_array(source, &ts_path(), &["entry"]);
2425 assert_eq!(result, vec!["./src/index.ts"]);
2426 }
2427
2428 #[test]
2429 fn template_literal_in_config_string() {
2430 let source = r"export default { testDir: `./tests` };";
2431 let val = extract_config_string(source, &js_path(), &["testDir"]);
2432 assert_eq!(val, Some("./tests".to_string()));
2433 }
2434
2435 #[test]
2438 fn nested_string_array_empty_path() {
2439 let source = r#"export default { items: ["a", "b"] };"#;
2440 let result = extract_config_string_array(source, &js_path(), &[]);
2441 assert!(result.is_empty());
2442 }
2443
2444 #[test]
2445 fn nested_string_empty_path() {
2446 let source = r#"export default { key: "val" };"#;
2447 let result = extract_config_string(source, &js_path(), &[]);
2448 assert!(result.is_none());
2449 }
2450
2451 #[test]
2452 fn object_keys_empty_path() {
2453 let source = r"export default { plugins: {} };";
2454 let result = extract_config_object_keys(source, &js_path(), &[]);
2455 assert!(result.is_empty());
2456 }
2457
2458 #[test]
2461 fn no_config_object_returns_empty() {
2462 let source = r"const x = 42;";
2464 let result = extract_config_string(source, &js_path(), &["key"]);
2465 assert!(result.is_none());
2466
2467 let arr = extract_config_string_array(source, &js_path(), &["items"]);
2468 assert!(arr.is_empty());
2469
2470 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
2471 assert!(keys.is_empty());
2472 }
2473
2474 #[test]
2477 fn property_with_string_key() {
2478 let source = r#"export default { "string-key": "value" };"#;
2479 let val = extract_config_string(source, &js_path(), &["string-key"]);
2480 assert_eq!(val, Some("value".to_string()));
2481 }
2482
2483 #[test]
2484 fn nested_navigation_through_non_object() {
2485 let source = r#"export default { level1: "not-an-object" };"#;
2487 let val = extract_config_string(source, &js_path(), &["level1", "level2"]);
2488 assert!(val.is_none());
2489 }
2490
2491 #[test]
2494 fn variable_reference_untyped() {
2495 let source = r#"
2496 const config = {
2497 testDir: "./tests"
2498 };
2499 export default config;
2500 "#;
2501 let val = extract_config_string(source, &js_path(), &["testDir"]);
2502 assert_eq!(val, Some("./tests".to_string()));
2503 }
2504
2505 #[test]
2506 fn variable_reference_with_type_annotation() {
2507 let source = r#"
2508 import type { StorybookConfig } from '@storybook/react-vite';
2509 const config: StorybookConfig = {
2510 addons: ["@storybook/addon-a11y", "@storybook/addon-docs"],
2511 framework: "@storybook/react-vite"
2512 };
2513 export default config;
2514 "#;
2515 let addons = extract_config_shallow_strings(source, &ts_path(), "addons");
2516 assert_eq!(
2517 addons,
2518 vec!["@storybook/addon-a11y", "@storybook/addon-docs"]
2519 );
2520
2521 let framework = extract_config_string(source, &ts_path(), &["framework"]);
2522 assert_eq!(framework, Some("@storybook/react-vite".to_string()));
2523 }
2524
2525 #[test]
2526 fn variable_reference_with_define_config() {
2527 let source = r#"
2528 import { defineConfig } from 'vitest/config';
2529 const config = defineConfig({
2530 test: {
2531 include: ["**/*.test.ts"]
2532 }
2533 });
2534 export default config;
2535 "#;
2536 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
2537 assert_eq!(include, vec!["**/*.test.ts"]);
2538 }
2539
2540 #[test]
2543 fn ts_satisfies_direct_export() {
2544 let source = r#"
2545 export default {
2546 testDir: "./tests"
2547 } satisfies PlaywrightTestConfig;
2548 "#;
2549 let val = extract_config_string(source, &ts_path(), &["testDir"]);
2550 assert_eq!(val, Some("./tests".to_string()));
2551 }
2552
2553 #[test]
2554 fn ts_as_direct_export() {
2555 let source = r#"
2556 export default {
2557 testDir: "./tests"
2558 } as const;
2559 "#;
2560 let val = extract_config_string(source, &ts_path(), &["testDir"]);
2561 assert_eq!(val, Some("./tests".to_string()));
2562 }
2563}