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]
130pub fn extract_config_nested_shallow_strings(
131 source: &str,
132 path: &Path,
133 outer_path: &[&str],
134 key: &str,
135) -> Vec<String> {
136 extract_from_source(source, path, |program| {
137 let obj = find_config_object(program)?;
138 let nested = get_nested_expression(obj, outer_path)?;
139 if let Expression::ObjectExpression(nested_obj) = nested {
140 let prop = find_property(nested_obj, key)?;
141 Some(collect_shallow_string_values(&prop.value))
142 } else {
143 None
144 }
145 })
146 .unwrap_or_default()
147}
148
149pub fn find_config_object_pub<'a>(program: &'a Program) -> Option<&'a ObjectExpression<'a>> {
151 find_config_object(program)
152}
153
154pub(crate) fn property_expr<'a>(
156 obj: &'a ObjectExpression<'a>,
157 key: &str,
158) -> Option<&'a Expression<'a>> {
159 find_property(obj, key).map(|prop| &prop.value)
160}
161
162pub(crate) fn property_object<'a>(
164 obj: &'a ObjectExpression<'a>,
165 key: &str,
166) -> Option<&'a ObjectExpression<'a>> {
167 property_expr(obj, key).and_then(object_expression)
168}
169
170pub(crate) fn property_string(obj: &ObjectExpression<'_>, key: &str) -> Option<String> {
172 property_expr(obj, key).and_then(expression_to_string)
173}
174
175pub(crate) fn object_expression<'a>(expr: &'a Expression<'a>) -> Option<&'a ObjectExpression<'a>> {
177 match expr {
178 Expression::ObjectExpression(obj) => Some(obj),
179 Expression::ParenthesizedExpression(paren) => object_expression(&paren.expression),
180 Expression::TSSatisfiesExpression(ts_sat) => object_expression(&ts_sat.expression),
181 Expression::TSAsExpression(ts_as) => object_expression(&ts_as.expression),
182 _ => None,
183 }
184}
185
186pub(crate) fn array_expression<'a>(expr: &'a Expression<'a>) -> Option<&'a ArrayExpression<'a>> {
188 match expr {
189 Expression::ArrayExpression(arr) => Some(arr),
190 Expression::ParenthesizedExpression(paren) => array_expression(&paren.expression),
191 Expression::TSSatisfiesExpression(ts_sat) => array_expression(&ts_sat.expression),
192 Expression::TSAsExpression(ts_as) => array_expression(&ts_as.expression),
193 _ => None,
194 }
195}
196
197pub(crate) fn expression_to_path_values(expr: &Expression<'_>) -> Vec<String> {
199 match expr {
200 Expression::ArrayExpression(arr) => arr
201 .elements
202 .iter()
203 .filter_map(|element| element.as_expression().and_then(expression_to_path_string))
204 .collect(),
205 _ => expression_to_path_string(expr).into_iter().collect(),
206 }
207}
208
209pub(crate) fn is_disabled_expression(expr: &Expression<'_>) -> bool {
211 matches!(expr, Expression::BooleanLiteral(boolean) if !boolean.value)
212 || matches!(expr, Expression::NullLiteral(_))
213}
214
215#[must_use]
220pub fn extract_config_object_keys(source: &str, path: &Path, prop_path: &[&str]) -> Vec<String> {
221 extract_from_source(source, path, |program| {
222 let obj = find_config_object(program)?;
223 get_nested_object_keys(obj, prop_path)
224 })
225 .unwrap_or_default()
226}
227
228#[must_use]
237pub fn extract_config_string_or_array(
238 source: &str,
239 path: &Path,
240 prop_path: &[&str],
241) -> Vec<String> {
242 extract_from_source(source, path, |program| {
243 let obj = find_config_object(program)?;
244 get_nested_string_or_array(obj, prop_path)
245 })
246 .unwrap_or_default()
247}
248
249#[must_use]
251pub fn extract_config_path_string(source: &str, path: &Path, prop_path: &[&str]) -> Option<String> {
252 extract_from_source(source, path, |program| {
253 let obj = find_config_object(program)?;
254 let expr = get_nested_expression(obj, prop_path)?;
255 expression_to_path_string(expr)
256 })
257}
258
259#[must_use]
266pub fn extract_config_array_nested_string_or_array(
267 source: &str,
268 path: &Path,
269 array_path: &[&str],
270 inner_path: &[&str],
271) -> Vec<String> {
272 extract_from_source(source, path, |program| {
273 let obj = find_config_object(program)?;
274 let array_expr = get_nested_expression(obj, array_path)?;
275 let Expression::ArrayExpression(arr) = array_expr else {
276 return None;
277 };
278 let mut results = Vec::new();
279 for element in &arr.elements {
280 if let Some(Expression::ObjectExpression(element_obj)) = element.as_expression()
281 && let Some(values) = get_nested_string_or_array(element_obj, inner_path)
282 {
283 results.extend(values);
284 }
285 }
286 if results.is_empty() {
287 None
288 } else {
289 Some(results)
290 }
291 })
292 .unwrap_or_default()
293}
294
295#[must_use]
302pub fn extract_config_object_nested_string_or_array(
303 source: &str,
304 path: &Path,
305 object_path: &[&str],
306 inner_path: &[&str],
307) -> Vec<String> {
308 extract_config_object_nested(source, path, object_path, |value_obj| {
309 get_nested_string_or_array(value_obj, inner_path)
310 })
311}
312
313#[must_use]
318pub fn extract_config_object_nested_strings(
319 source: &str,
320 path: &Path,
321 object_path: &[&str],
322 inner_path: &[&str],
323) -> Vec<String> {
324 extract_config_object_nested(source, path, object_path, |value_obj| {
325 get_nested_string_from_object(value_obj, inner_path).map(|s| vec![s])
326 })
327}
328
329fn extract_config_object_nested(
334 source: &str,
335 path: &Path,
336 object_path: &[&str],
337 extract_fn: impl Fn(&ObjectExpression<'_>) -> Option<Vec<String>>,
338) -> Vec<String> {
339 extract_from_source(source, path, |program| {
340 let obj = find_config_object(program)?;
341 let obj_expr = get_nested_expression(obj, object_path)?;
342 let Expression::ObjectExpression(target_obj) = obj_expr else {
343 return None;
344 };
345 let mut results = Vec::new();
346 for prop in &target_obj.properties {
347 if let ObjectPropertyKind::ObjectProperty(p) = prop
348 && let Expression::ObjectExpression(value_obj) = &p.value
349 && let Some(values) = extract_fn(value_obj)
350 {
351 results.extend(values);
352 }
353 }
354 if results.is_empty() {
355 None
356 } else {
357 Some(results)
358 }
359 })
360 .unwrap_or_default()
361}
362
363#[must_use]
369pub fn extract_config_require_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
370 extract_from_source(source, path, |program| {
371 let obj = find_config_object(program)?;
372 let prop = find_property(obj, key)?;
373 Some(collect_require_sources(&prop.value))
374 })
375 .unwrap_or_default()
376}
377
378#[must_use]
385pub fn extract_config_aliases(
386 source: &str,
387 path: &Path,
388 prop_path: &[&str],
389) -> Vec<(String, String)> {
390 extract_from_source(source, path, |program| {
391 let obj = find_config_object(program)?;
392 let expr = get_nested_expression(obj, prop_path)?;
393 let aliases = expression_to_alias_pairs(expr);
394 (!aliases.is_empty()).then_some(aliases)
395 })
396 .unwrap_or_default()
397}
398
399#[must_use]
405pub fn extract_config_array_object_strings(
406 source: &str,
407 path: &Path,
408 array_path: &[&str],
409 key: &str,
410) -> Vec<String> {
411 extract_from_source(source, path, |program| {
412 let obj = find_config_object(program)?;
413 let array_expr = get_nested_expression(obj, array_path)?;
414 let Expression::ArrayExpression(arr) = array_expr else {
415 return None;
416 };
417
418 let mut results = Vec::new();
419 for element in &arr.elements {
420 let Some(expr) = element.as_expression() else {
421 continue;
422 };
423 match expr {
424 Expression::ObjectExpression(item) => {
425 if let Some(prop) = find_property(item, key)
426 && let Some(value) = expression_to_path_string(&prop.value)
427 {
428 results.push(value);
429 }
430 }
431 _ => {
432 if let Some(value) = expression_to_path_string(expr) {
433 results.push(value);
434 }
435 }
436 }
437 }
438
439 (!results.is_empty()).then_some(results)
440 })
441 .unwrap_or_default()
442}
443
444#[must_use]
490pub fn extract_lazy_imports_in_array(source: &str, path: &Path, prop_path: &[&str]) -> Vec<String> {
491 extract_from_source(source, path, |program| {
492 let obj = find_config_object(program)?;
493 let array_expr = get_nested_expression(obj, prop_path)?;
494 let Expression::ArrayExpression(arr) = array_expr else {
495 return None;
496 };
497 let mut specs = Vec::new();
498 for element in &arr.elements {
499 let Some(expr) = element.as_expression() else {
500 continue;
501 };
502 if let Some(spec) = lazy_import_specifier(expr) {
503 specs.push(spec);
504 }
505 }
506 (!specs.is_empty()).then_some(specs)
507 })
508 .unwrap_or_default()
509}
510
511fn lazy_import_specifier(expr: &Expression<'_>) -> Option<String> {
524 let callable = match expr {
525 Expression::ObjectExpression(obj) => &find_property(obj, "file")?.value,
526 _ => expr,
527 };
528 let import_expr = extract_import_from_callable(callable)?;
529 expression_to_string(&import_expr.source)
530}
531
532#[must_use]
539pub fn extract_config_plugin_option_string(
540 source: &str,
541 path: &Path,
542 plugins_path: &[&str],
543 plugin_name: &str,
544 option_key: &str,
545) -> Option<String> {
546 extract_from_source(source, path, |program| {
547 let obj = find_config_object(program)?;
548 let plugins_expr = get_nested_expression(obj, plugins_path)?;
549 let Expression::ArrayExpression(plugins) = plugins_expr else {
550 return None;
551 };
552
553 for entry in &plugins.elements {
554 let Some(Expression::ArrayExpression(tuple)) = entry.as_expression() else {
555 continue;
556 };
557 let Some(plugin_expr) = tuple
558 .elements
559 .first()
560 .and_then(ArrayExpressionElement::as_expression)
561 else {
562 continue;
563 };
564 if expression_to_string(plugin_expr).as_deref() != Some(plugin_name) {
565 continue;
566 }
567
568 let Some(options_expr) = tuple
569 .elements
570 .get(1)
571 .and_then(ArrayExpressionElement::as_expression)
572 else {
573 continue;
574 };
575 let Expression::ObjectExpression(options_obj) = options_expr else {
576 continue;
577 };
578 let option = find_property(options_obj, option_key)?;
579 return expression_to_path_string(&option.value);
580 }
581
582 None
583 })
584}
585
586#[must_use]
588pub fn extract_config_plugin_option_string_from_paths(
589 source: &str,
590 path: &Path,
591 plugin_paths: &[&[&str]],
592 plugin_name: &str,
593 option_key: &str,
594) -> Option<String> {
595 plugin_paths.iter().find_map(|plugins_path| {
596 extract_config_plugin_option_string(source, path, plugins_path, plugin_name, option_key)
597 })
598}
599
600#[must_use]
605pub fn normalize_config_path(raw: &str, config_path: &Path, root: &Path) -> Option<String> {
606 if raw.is_empty() {
607 return None;
608 }
609
610 let candidate = if let Some(stripped) = raw.strip_prefix('/') {
611 lexical_normalize(&root.join(stripped))
612 } else {
613 let path = Path::new(raw);
614 if path.is_absolute() {
615 lexical_normalize(path)
616 } else {
617 let base = config_path.parent().unwrap_or(root);
618 lexical_normalize(&base.join(path))
619 }
620 };
621
622 let relative = candidate.strip_prefix(root).ok()?;
623 let normalized = relative.to_string_lossy().replace('\\', "/");
624 (!normalized.is_empty()).then_some(normalized)
625}
626
627fn extract_from_source<T>(
636 source: &str,
637 path: &Path,
638 extractor: impl FnOnce(&Program) -> Option<T>,
639) -> Option<T> {
640 let source_type = SourceType::from_path(path).unwrap_or_default();
641 let alloc = Allocator::default();
642
643 let is_json = path
646 .extension()
647 .is_some_and(|ext| ext == "json" || ext == "jsonc");
648 if is_json {
649 let wrapped = format!("({source})");
650 let parsed = Parser::new(&alloc, &wrapped, SourceType::mjs()).parse();
651 return extractor(&parsed.program);
652 }
653
654 let parsed = Parser::new(&alloc, source, source_type).parse();
655 extractor(&parsed.program)
656}
657
658fn find_config_object<'a>(program: &'a Program) -> Option<&'a ObjectExpression<'a>> {
670 for stmt in &program.body {
671 match stmt {
672 Statement::ExportDefaultDeclaration(decl) => {
674 let expr: Option<&Expression> = match &decl.declaration {
676 ExportDefaultDeclarationKind::ObjectExpression(obj) => {
677 return Some(obj);
678 }
679 ExportDefaultDeclarationKind::FunctionDeclaration(func) => {
680 return extract_object_from_function(func);
681 }
682 _ => decl.declaration.as_expression(),
683 };
684 if let Some(expr) = expr {
685 if let Some(obj) = extract_object_from_expression(expr) {
687 return Some(obj);
688 }
689 if let Some(name) = unwrap_to_identifier_name(expr) {
692 return find_variable_init_object(program, name);
693 }
694 }
695 }
696 Statement::ExpressionStatement(expr_stmt) => {
698 if let Expression::AssignmentExpression(assign) = &expr_stmt.expression
699 && is_module_exports_target(&assign.left)
700 {
701 return extract_object_from_expression(&assign.right);
702 }
703 }
704 _ => {}
705 }
706 }
707
708 if program.body.len() == 1
711 && let Statement::ExpressionStatement(expr_stmt) = &program.body[0]
712 {
713 match &expr_stmt.expression {
714 Expression::ObjectExpression(obj) => return Some(obj),
715 Expression::ParenthesizedExpression(paren) => {
716 if let Expression::ObjectExpression(obj) = &paren.expression {
717 return Some(obj);
718 }
719 }
720 _ => {}
721 }
722 }
723
724 None
725}
726
727fn extract_object_from_expression<'a>(
729 expr: &'a Expression<'a>,
730) -> Option<&'a ObjectExpression<'a>> {
731 match expr {
732 Expression::ObjectExpression(obj) => Some(obj),
734 Expression::CallExpression(call) => {
736 for arg in &call.arguments {
738 match arg {
739 Argument::ObjectExpression(obj) => return Some(obj),
740 Argument::ArrowFunctionExpression(arrow) => {
742 if arrow.expression
743 && !arrow.body.statements.is_empty()
744 && let Statement::ExpressionStatement(expr_stmt) =
745 &arrow.body.statements[0]
746 {
747 return extract_object_from_expression(&expr_stmt.expression);
748 }
749 }
750 _ => {}
751 }
752 }
753 None
754 }
755 Expression::ParenthesizedExpression(paren) => {
757 extract_object_from_expression(&paren.expression)
758 }
759 Expression::TSSatisfiesExpression(ts_sat) => {
761 extract_object_from_expression(&ts_sat.expression)
762 }
763 Expression::TSAsExpression(ts_as) => extract_object_from_expression(&ts_as.expression),
764 Expression::ArrowFunctionExpression(arrow) => extract_object_from_arrow_function(arrow),
765 Expression::FunctionExpression(func) => extract_object_from_function(func),
766 _ => None,
767 }
768}
769
770fn extract_object_from_arrow_function<'a>(
771 arrow: &'a ArrowFunctionExpression<'a>,
772) -> Option<&'a ObjectExpression<'a>> {
773 if arrow.expression {
774 arrow.body.statements.first().and_then(|stmt| {
775 if let Statement::ExpressionStatement(expr_stmt) = stmt {
776 extract_object_from_expression(&expr_stmt.expression)
777 } else {
778 None
779 }
780 })
781 } else {
782 extract_object_from_function_body(&arrow.body)
783 }
784}
785
786fn extract_object_from_function<'a>(func: &'a Function<'a>) -> Option<&'a ObjectExpression<'a>> {
787 func.body
788 .as_ref()
789 .and_then(|body| extract_object_from_function_body(body))
790}
791
792fn extract_object_from_function_body<'a>(
793 body: &'a FunctionBody<'a>,
794) -> Option<&'a ObjectExpression<'a>> {
795 for stmt in &body.statements {
796 if let Statement::ReturnStatement(ret) = stmt
797 && let Some(argument) = &ret.argument
798 && let Some(obj) = extract_object_from_expression(argument)
799 {
800 return Some(obj);
801 }
802 }
803 None
804}
805
806fn is_module_exports_target(target: &AssignmentTarget) -> bool {
808 if let AssignmentTarget::StaticMemberExpression(member) = target
809 && let Expression::Identifier(obj) = &member.object
810 {
811 return obj.name == "module" && member.property.name == "exports";
812 }
813 false
814}
815
816fn unwrap_to_identifier_name<'a>(expr: &'a Expression<'a>) -> Option<&'a str> {
820 match expr {
821 Expression::Identifier(id) => Some(&id.name),
822 Expression::TSSatisfiesExpression(ts_sat) => unwrap_to_identifier_name(&ts_sat.expression),
823 Expression::TSAsExpression(ts_as) => unwrap_to_identifier_name(&ts_as.expression),
824 _ => None,
825 }
826}
827
828fn find_variable_init_object<'a>(
833 program: &'a Program,
834 name: &str,
835) -> Option<&'a ObjectExpression<'a>> {
836 for stmt in &program.body {
837 if let Statement::VariableDeclaration(decl) = stmt {
838 for declarator in &decl.declarations {
839 if let BindingPattern::BindingIdentifier(id) = &declarator.id
840 && id.name == name
841 && let Some(init) = &declarator.init
842 {
843 return extract_object_from_expression(init);
844 }
845 }
846 }
847 }
848 None
849}
850
851pub(crate) fn find_property<'a>(
853 obj: &'a ObjectExpression<'a>,
854 key: &str,
855) -> Option<&'a ObjectProperty<'a>> {
856 for prop in &obj.properties {
857 if let ObjectPropertyKind::ObjectProperty(p) = prop
858 && property_key_matches(&p.key, key)
859 {
860 return Some(p);
861 }
862 }
863 None
864}
865
866pub(crate) fn property_key_matches(key: &PropertyKey, name: &str) -> bool {
868 match key {
869 PropertyKey::StaticIdentifier(id) => id.name == name,
870 PropertyKey::StringLiteral(s) => s.value == name,
871 _ => false,
872 }
873}
874
875fn get_object_string_property(obj: &ObjectExpression, key: &str) -> Option<String> {
877 find_property(obj, key).and_then(|p| expression_to_string(&p.value))
878}
879
880fn get_object_string_array_property(obj: &ObjectExpression, key: &str) -> Vec<String> {
882 find_property(obj, key)
883 .map(|p| expression_to_string_array(&p.value))
884 .unwrap_or_default()
885}
886
887fn get_nested_string_array_from_object(
889 obj: &ObjectExpression,
890 path: &[&str],
891) -> Option<Vec<String>> {
892 if path.is_empty() {
893 return None;
894 }
895 if path.len() == 1 {
896 return Some(get_object_string_array_property(obj, path[0]));
897 }
898 let prop = find_property(obj, path[0])?;
900 if let Expression::ObjectExpression(nested) = &prop.value {
901 get_nested_string_array_from_object(nested, &path[1..])
902 } else {
903 None
904 }
905}
906
907fn get_nested_string_from_object(obj: &ObjectExpression, path: &[&str]) -> Option<String> {
909 if path.is_empty() {
910 return None;
911 }
912 if path.len() == 1 {
913 return get_object_string_property(obj, path[0]);
914 }
915 let prop = find_property(obj, path[0])?;
916 if let Expression::ObjectExpression(nested) = &prop.value {
917 get_nested_string_from_object(nested, &path[1..])
918 } else {
919 None
920 }
921}
922
923pub(crate) fn expression_to_string(expr: &Expression) -> Option<String> {
925 match expr {
926 Expression::StringLiteral(s) => Some(s.value.to_string()),
927 Expression::TemplateLiteral(t) if t.expressions.is_empty() => {
928 t.quasis.first().map(|q| q.value.raw.to_string())
930 }
931 _ => None,
932 }
933}
934
935pub(crate) fn expression_to_path_string(expr: &Expression) -> Option<String> {
937 match expr {
938 Expression::ParenthesizedExpression(paren) => expression_to_path_string(&paren.expression),
939 Expression::TSAsExpression(ts_as) => expression_to_path_string(&ts_as.expression),
940 Expression::TSSatisfiesExpression(ts_sat) => expression_to_path_string(&ts_sat.expression),
941 Expression::CallExpression(call) => call_expression_to_path_string(call),
942 Expression::NewExpression(new_expr) => new_expression_to_path_string(new_expr),
943 _ => expression_to_string(expr),
944 }
945}
946
947fn call_expression_to_path_string(call: &CallExpression) -> Option<String> {
948 if matches!(&call.callee, Expression::Identifier(id) if id.name == "fileURLToPath") {
949 return call
950 .arguments
951 .first()
952 .and_then(Argument::as_expression)
953 .and_then(expression_to_path_string);
954 }
955
956 let callee_name = match &call.callee {
957 Expression::Identifier(id) => Some(id.name.as_str()),
958 Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
959 _ => None,
960 }?;
961
962 if !matches!(callee_name, "resolve" | "join") {
963 return None;
964 }
965
966 let mut segments = Vec::new();
967 for (index, arg) in call.arguments.iter().enumerate() {
968 let expr = arg.as_expression()?;
969
970 if matches!(expr, Expression::Identifier(id) if id.name == "__dirname") {
971 if index == 0 {
972 continue;
973 }
974 return None;
975 }
976
977 segments.push(expression_to_string(expr)?);
978 }
979
980 (!segments.is_empty()).then(|| join_path_segments(&segments))
981}
982
983fn new_expression_to_path_string(new_expr: &NewExpression) -> Option<String> {
984 if !matches!(&new_expr.callee, Expression::Identifier(id) if id.name == "URL") {
985 return None;
986 }
987
988 let source = new_expr
989 .arguments
990 .first()
991 .and_then(Argument::as_expression)
992 .and_then(expression_to_string)?;
993
994 let base = new_expr
995 .arguments
996 .get(1)
997 .and_then(Argument::as_expression)?;
998 is_import_meta_url_expression(base).then_some(source)
999}
1000
1001fn is_import_meta_url_expression(expr: &Expression) -> bool {
1002 if let Expression::StaticMemberExpression(member) = expr {
1003 member.property.name == "url" && matches!(member.object, Expression::MetaProperty(_))
1004 } else {
1005 false
1006 }
1007}
1008
1009fn join_path_segments(segments: &[String]) -> String {
1010 let mut joined = PathBuf::new();
1011 for segment in segments {
1012 joined.push(segment);
1013 }
1014 joined.to_string_lossy().replace('\\', "/")
1015}
1016
1017fn expression_to_alias_pairs(expr: &Expression) -> Vec<(String, String)> {
1018 match expr {
1019 Expression::ObjectExpression(obj) => obj
1020 .properties
1021 .iter()
1022 .filter_map(|prop| {
1023 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
1024 return None;
1025 };
1026 let find = property_key_to_string(&prop.key)?;
1027 let replacement = expression_to_path_values(&prop.value).into_iter().next()?;
1028 Some((find, replacement))
1029 })
1030 .collect(),
1031 Expression::ArrayExpression(arr) => arr
1032 .elements
1033 .iter()
1034 .filter_map(|element| {
1035 let Expression::ObjectExpression(obj) = element.as_expression()? else {
1036 return None;
1037 };
1038 let find = find_property(obj, "find")
1039 .and_then(|prop| expression_to_string(&prop.value))?;
1040 let replacement = find_property(obj, "replacement")
1041 .and_then(|prop| expression_to_path_string(&prop.value))?;
1042 Some((find, replacement))
1043 })
1044 .collect(),
1045 _ => Vec::new(),
1046 }
1047}
1048
1049fn lexical_normalize(path: &Path) -> PathBuf {
1050 let mut normalized = PathBuf::new();
1051
1052 for component in path.components() {
1053 match component {
1054 std::path::Component::CurDir => {}
1055 std::path::Component::ParentDir => {
1056 normalized.pop();
1057 }
1058 _ => normalized.push(component.as_os_str()),
1059 }
1060 }
1061
1062 normalized
1063}
1064
1065fn expression_to_string_array(expr: &Expression) -> Vec<String> {
1067 match expr {
1068 Expression::ArrayExpression(arr) => arr
1069 .elements
1070 .iter()
1071 .filter_map(|el| match el {
1072 ArrayExpressionElement::SpreadElement(_) => None,
1073 _ => el.as_expression().and_then(expression_to_string),
1074 })
1075 .collect(),
1076 _ => vec![],
1077 }
1078}
1079
1080fn collect_shallow_string_values(expr: &Expression) -> Vec<String> {
1085 let mut values = Vec::new();
1086 match expr {
1087 Expression::StringLiteral(s) => {
1088 values.push(s.value.to_string());
1089 }
1090 Expression::ArrayExpression(arr) => {
1091 for el in &arr.elements {
1092 if let Some(inner) = el.as_expression() {
1093 match inner {
1094 Expression::StringLiteral(s) => {
1095 values.push(s.value.to_string());
1096 }
1097 Expression::ArrayExpression(sub_arr) => {
1099 if let Some(first) = sub_arr.elements.first()
1100 && let Some(first_expr) = first.as_expression()
1101 && let Some(s) = expression_to_string(first_expr)
1102 {
1103 values.push(s);
1104 }
1105 }
1106 _ => {}
1107 }
1108 }
1109 }
1110 }
1111 Expression::ObjectExpression(obj) => {
1113 for prop in &obj.properties {
1114 if let ObjectPropertyKind::ObjectProperty(p) = prop {
1115 match &p.value {
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 _ => {}
1134 }
1135 values
1136}
1137
1138fn collect_all_string_values(expr: &Expression, values: &mut Vec<String>) {
1140 match expr {
1141 Expression::StringLiteral(s) => {
1142 values.push(s.value.to_string());
1143 }
1144 Expression::ArrayExpression(arr) => {
1145 for el in &arr.elements {
1146 if let Some(expr) = el.as_expression() {
1147 collect_all_string_values(expr, values);
1148 }
1149 }
1150 }
1151 Expression::ObjectExpression(obj) => {
1152 for prop in &obj.properties {
1153 if let ObjectPropertyKind::ObjectProperty(p) = prop {
1154 collect_all_string_values(&p.value, values);
1155 }
1156 }
1157 }
1158 _ => {}
1159 }
1160}
1161
1162fn property_key_to_string(key: &PropertyKey) -> Option<String> {
1164 match key {
1165 PropertyKey::StaticIdentifier(id) => Some(id.name.to_string()),
1166 PropertyKey::StringLiteral(s) => Some(s.value.to_string()),
1167 _ => None,
1168 }
1169}
1170
1171fn get_nested_object_keys(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
1173 if path.is_empty() {
1174 return None;
1175 }
1176 let prop = find_property(obj, path[0])?;
1177 if path.len() == 1 {
1178 if let Expression::ObjectExpression(nested) = &prop.value {
1179 let keys = nested
1180 .properties
1181 .iter()
1182 .filter_map(|p| {
1183 if let ObjectPropertyKind::ObjectProperty(p) = p {
1184 property_key_to_string(&p.key)
1185 } else {
1186 None
1187 }
1188 })
1189 .collect();
1190 return Some(keys);
1191 }
1192 return None;
1193 }
1194 if let Expression::ObjectExpression(nested) = &prop.value {
1195 get_nested_object_keys(nested, &path[1..])
1196 } else {
1197 None
1198 }
1199}
1200
1201fn get_nested_expression<'a>(
1203 obj: &'a ObjectExpression<'a>,
1204 path: &[&str],
1205) -> Option<&'a Expression<'a>> {
1206 if path.is_empty() {
1207 return None;
1208 }
1209 let prop = find_property(obj, path[0])?;
1210 if path.len() == 1 {
1211 return Some(&prop.value);
1212 }
1213 if let Expression::ObjectExpression(nested) = &prop.value {
1214 get_nested_expression(nested, &path[1..])
1215 } else {
1216 None
1217 }
1218}
1219
1220fn get_nested_string_or_array(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
1222 if path.is_empty() {
1223 return None;
1224 }
1225 if path.len() == 1 {
1226 let prop = find_property(obj, path[0])?;
1227 return Some(expression_to_string_or_array(&prop.value));
1228 }
1229 let prop = find_property(obj, path[0])?;
1230 if let Expression::ObjectExpression(nested) = &prop.value {
1231 get_nested_string_or_array(nested, &path[1..])
1232 } else {
1233 None
1234 }
1235}
1236
1237fn expression_to_string_or_array(expr: &Expression) -> Vec<String> {
1245 match expr {
1246 Expression::StringLiteral(s) => vec![s.value.to_string()],
1247 Expression::TemplateLiteral(t) if t.expressions.is_empty() => t
1248 .quasis
1249 .first()
1250 .map(|q| vec![q.value.raw.to_string()])
1251 .unwrap_or_default(),
1252 Expression::ArrayExpression(arr) => arr
1253 .elements
1254 .iter()
1255 .filter_map(|el| el.as_expression())
1256 .flat_map(|e| match e {
1257 Expression::ObjectExpression(obj) => find_property(obj, "input")
1258 .map(|p| expression_to_string_or_array(&p.value))
1259 .unwrap_or_default(),
1260 _ => expression_to_string(e).into_iter().collect(),
1261 })
1262 .collect(),
1263 Expression::ObjectExpression(obj) => obj
1264 .properties
1265 .iter()
1266 .flat_map(|p| {
1267 if let ObjectPropertyKind::ObjectProperty(p) = p {
1268 match &p.value {
1269 Expression::ArrayExpression(_) => expression_to_string_or_array(&p.value),
1270 Expression::ObjectExpression(value_obj) => {
1271 find_property(value_obj, "import")
1272 .map(|import_prop| {
1273 expression_to_string_or_array(&import_prop.value)
1274 })
1275 .unwrap_or_default()
1276 }
1277 _ => expression_to_string(&p.value).into_iter().collect(),
1278 }
1279 } else {
1280 Vec::new()
1281 }
1282 })
1283 .collect(),
1284 _ => vec![],
1285 }
1286}
1287
1288fn collect_require_sources(expr: &Expression) -> Vec<String> {
1290 let mut sources = Vec::new();
1291 match expr {
1292 Expression::CallExpression(call) if is_require_call(call) => {
1293 if let Some(s) = get_require_source(call) {
1294 sources.push(s);
1295 }
1296 }
1297 Expression::ArrayExpression(arr) => {
1298 for el in &arr.elements {
1299 if let Some(inner) = el.as_expression() {
1300 match inner {
1301 Expression::CallExpression(call) if is_require_call(call) => {
1302 if let Some(s) = get_require_source(call) {
1303 sources.push(s);
1304 }
1305 }
1306 Expression::ArrayExpression(sub_arr) => {
1308 if let Some(first) = sub_arr.elements.first()
1309 && let Some(Expression::CallExpression(call)) =
1310 first.as_expression()
1311 && is_require_call(call)
1312 && let Some(s) = get_require_source(call)
1313 {
1314 sources.push(s);
1315 }
1316 }
1317 _ => {}
1318 }
1319 }
1320 }
1321 }
1322 _ => {}
1323 }
1324 sources
1325}
1326
1327fn is_require_call(call: &CallExpression) -> bool {
1329 matches!(&call.callee, Expression::Identifier(id) if id.name == "require")
1330}
1331
1332fn get_require_source(call: &CallExpression) -> Option<String> {
1334 call.arguments.first().and_then(|arg| {
1335 if let Argument::StringLiteral(s) = arg {
1336 Some(s.value.to_string())
1337 } else {
1338 None
1339 }
1340 })
1341}
1342
1343#[cfg(test)]
1344mod tests {
1345 use super::*;
1346 use std::path::PathBuf;
1347
1348 fn js_path() -> PathBuf {
1349 PathBuf::from("config.js")
1350 }
1351
1352 fn ts_path() -> PathBuf {
1353 PathBuf::from("config.ts")
1354 }
1355
1356 #[test]
1357 fn extract_lazy_imports_bare_arrows() {
1358 let source = r"
1359 import { defineConfig } from '@adonisjs/core/app'
1360 export default defineConfig({
1361 preloads: [
1362 () => import('#start/routes'),
1363 () => import('#start/kernel'),
1364 ],
1365 })
1366 ";
1367 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["preloads"]);
1368 assert_eq!(specs, vec!["#start/routes", "#start/kernel"]);
1369 }
1370
1371 #[test]
1372 fn extract_lazy_imports_object_form_with_file_key() {
1373 let source = r"
1374 export default defineConfig({
1375 providers: [
1376 () => import('@adonisjs/core/providers/app_provider'),
1377 {
1378 file: () => import('@adonisjs/core/providers/repl_provider'),
1379 environment: ['repl', 'test'],
1380 },
1381 ],
1382 })
1383 ";
1384 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["providers"]);
1385 assert_eq!(
1386 specs,
1387 vec![
1388 "@adonisjs/core/providers/app_provider",
1389 "@adonisjs/core/providers/repl_provider",
1390 ]
1391 );
1392 }
1393
1394 #[test]
1395 fn extract_lazy_imports_block_body_with_return() {
1396 let source = r"
1398 export default defineConfig({
1399 commands: [
1400 () => { return import('@adonisjs/core/commands') },
1401 ],
1402 })
1403 ";
1404 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["commands"]);
1405 assert_eq!(specs, vec!["@adonisjs/core/commands"]);
1406 }
1407
1408 #[test]
1409 fn extract_lazy_imports_skips_unknown_element_shapes() {
1410 let source = r"
1413 export default defineConfig({
1414 commands: [
1415 'string-entry',
1416 42,
1417 { other: 'value' },
1418 () => import('@adonisjs/lucid/commands'),
1419 ],
1420 })
1421 ";
1422 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["commands"]);
1423 assert_eq!(specs, vec!["@adonisjs/lucid/commands"]);
1424 }
1425
1426 #[test]
1427 fn extract_lazy_imports_missing_property_returns_empty() {
1428 let source = r"
1429 export default defineConfig({
1430 preloads: [() => import('#start/routes')],
1431 })
1432 ";
1433 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["providers"]);
1434 assert!(specs.is_empty());
1435 }
1436
1437 #[test]
1438 fn extract_imports_basic() {
1439 let source = r"
1440 import foo from 'foo-pkg';
1441 import { bar } from '@scope/bar';
1442 export default {};
1443 ";
1444 let imports = extract_imports(source, &js_path());
1445 assert_eq!(imports, vec!["foo-pkg", "@scope/bar"]);
1446 }
1447
1448 #[test]
1449 fn extract_default_export_object_property() {
1450 let source = r#"export default { testDir: "./tests" };"#;
1451 let val = extract_config_string(source, &js_path(), &["testDir"]);
1452 assert_eq!(val, Some("./tests".to_string()));
1453 }
1454
1455 #[test]
1456 fn extract_define_config_property() {
1457 let source = r#"
1458 import { defineConfig } from 'vitest/config';
1459 export default defineConfig({
1460 test: {
1461 include: ["**/*.test.ts", "**/*.spec.ts"],
1462 setupFiles: ["./test/setup.ts"]
1463 }
1464 });
1465 "#;
1466 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
1467 assert_eq!(include, vec!["**/*.test.ts", "**/*.spec.ts"]);
1468
1469 let setup = extract_config_string_array(source, &ts_path(), &["test", "setupFiles"]);
1470 assert_eq!(setup, vec!["./test/setup.ts"]);
1471 }
1472
1473 #[test]
1474 fn extract_module_exports_property() {
1475 let source = r#"module.exports = { testEnvironment: "jsdom" };"#;
1476 let val = extract_config_string(source, &js_path(), &["testEnvironment"]);
1477 assert_eq!(val, Some("jsdom".to_string()));
1478 }
1479
1480 #[test]
1481 fn extract_nested_string_array() {
1482 let source = r#"
1483 export default {
1484 resolve: {
1485 alias: {
1486 "@": "./src"
1487 }
1488 },
1489 test: {
1490 include: ["src/**/*.test.ts"]
1491 }
1492 };
1493 "#;
1494 let include = extract_config_string_array(source, &js_path(), &["test", "include"]);
1495 assert_eq!(include, vec!["src/**/*.test.ts"]);
1496 }
1497
1498 #[test]
1499 fn extract_addons_array() {
1500 let source = r#"
1501 export default {
1502 addons: [
1503 "@storybook/addon-a11y",
1504 "@storybook/addon-docs",
1505 "@storybook/addon-links"
1506 ]
1507 };
1508 "#;
1509 let addons = extract_config_property_strings(source, &ts_path(), "addons");
1510 assert_eq!(
1511 addons,
1512 vec![
1513 "@storybook/addon-a11y",
1514 "@storybook/addon-docs",
1515 "@storybook/addon-links"
1516 ]
1517 );
1518 }
1519
1520 #[test]
1521 fn handle_empty_config() {
1522 let source = "";
1523 let result = extract_config_string(source, &js_path(), &["key"]);
1524 assert_eq!(result, None);
1525 }
1526
1527 #[test]
1530 fn object_keys_postcss_plugins() {
1531 let source = r"
1532 module.exports = {
1533 plugins: {
1534 autoprefixer: {},
1535 tailwindcss: {},
1536 'postcss-import': {}
1537 }
1538 };
1539 ";
1540 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1541 assert_eq!(keys, vec!["autoprefixer", "tailwindcss", "postcss-import"]);
1542 }
1543
1544 #[test]
1545 fn object_keys_nested_path() {
1546 let source = r"
1547 export default {
1548 build: {
1549 plugins: {
1550 minify: {},
1551 compress: {}
1552 }
1553 }
1554 };
1555 ";
1556 let keys = extract_config_object_keys(source, &js_path(), &["build", "plugins"]);
1557 assert_eq!(keys, vec!["minify", "compress"]);
1558 }
1559
1560 #[test]
1561 fn object_keys_empty_object() {
1562 let source = r"export default { plugins: {} };";
1563 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1564 assert!(keys.is_empty());
1565 }
1566
1567 #[test]
1568 fn object_keys_non_object_returns_empty() {
1569 let source = r#"export default { plugins: ["a", "b"] };"#;
1570 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1571 assert!(keys.is_empty());
1572 }
1573
1574 #[test]
1577 fn string_or_array_single_string() {
1578 let source = r#"export default { entry: "./src/index.js" };"#;
1579 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1580 assert_eq!(result, vec!["./src/index.js"]);
1581 }
1582
1583 #[test]
1584 fn string_or_array_array() {
1585 let source = r#"export default { entry: ["./src/a.js", "./src/b.js"] };"#;
1586 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1587 assert_eq!(result, vec!["./src/a.js", "./src/b.js"]);
1588 }
1589
1590 #[test]
1591 fn string_or_array_object_values() {
1592 let source =
1593 r#"export default { entry: { main: "./src/main.js", vendor: "./src/vendor.js" } };"#;
1594 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1595 assert_eq!(result, vec!["./src/main.js", "./src/vendor.js"]);
1596 }
1597
1598 #[test]
1599 fn string_or_array_object_array_values() {
1600 let source = r#"export default { entry: { app: ["./src/polyfill.js", "./src/app.js"] } };"#;
1601 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1602 assert_eq!(result, vec!["./src/polyfill.js", "./src/app.js"]);
1603 }
1604
1605 #[test]
1606 fn string_or_array_webpack_entry_descriptors() {
1607 let source = r#"
1608 export default {
1609 entry: {
1610 app: {
1611 import: "./src/app.js",
1612 filename: "pages/app.js",
1613 dependOn: "shared",
1614 },
1615 admin: {
1616 import: ["./src/admin-polyfill.js", "./src/admin.js"],
1617 runtime: "runtime",
1618 },
1619 shared: ["react", "react-dom"],
1620 },
1621 };
1622 "#;
1623 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1624 assert_eq!(
1625 result,
1626 vec![
1627 "./src/app.js",
1628 "./src/admin-polyfill.js",
1629 "./src/admin.js",
1630 "react",
1631 "react-dom"
1632 ]
1633 );
1634 }
1635
1636 #[test]
1637 fn string_or_array_nested_path() {
1638 let source = r#"
1639 export default {
1640 build: {
1641 rollupOptions: {
1642 input: ["./index.html", "./about.html"]
1643 }
1644 }
1645 };
1646 "#;
1647 let result = extract_config_string_or_array(
1648 source,
1649 &js_path(),
1650 &["build", "rollupOptions", "input"],
1651 );
1652 assert_eq!(result, vec!["./index.html", "./about.html"]);
1653 }
1654
1655 #[test]
1656 fn string_or_array_template_literal() {
1657 let source = r"export default { entry: `./src/index.js` };";
1658 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1659 assert_eq!(result, vec!["./src/index.js"]);
1660 }
1661
1662 #[test]
1665 fn require_strings_array() {
1666 let source = r"
1667 module.exports = {
1668 plugins: [
1669 require('autoprefixer'),
1670 require('postcss-import')
1671 ]
1672 };
1673 ";
1674 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1675 assert_eq!(deps, vec!["autoprefixer", "postcss-import"]);
1676 }
1677
1678 #[test]
1679 fn require_strings_with_tuples() {
1680 let source = r"
1681 module.exports = {
1682 plugins: [
1683 require('autoprefixer'),
1684 [require('postcss-preset-env'), { stage: 3 }]
1685 ]
1686 };
1687 ";
1688 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1689 assert_eq!(deps, vec!["autoprefixer", "postcss-preset-env"]);
1690 }
1691
1692 #[test]
1693 fn require_strings_empty_array() {
1694 let source = r"module.exports = { plugins: [] };";
1695 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1696 assert!(deps.is_empty());
1697 }
1698
1699 #[test]
1700 fn require_strings_no_require_calls() {
1701 let source = r#"module.exports = { plugins: ["a", "b"] };"#;
1702 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1703 assert!(deps.is_empty());
1704 }
1705
1706 #[test]
1707 fn extract_aliases_from_object_with_file_url_to_path() {
1708 let source = r#"
1709 import { defineConfig } from 'vite';
1710 import { fileURLToPath, URL } from 'node:url';
1711
1712 export default defineConfig({
1713 resolve: {
1714 alias: {
1715 "@": fileURLToPath(new URL("./src", import.meta.url))
1716 }
1717 }
1718 });
1719 "#;
1720
1721 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
1722 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
1723 }
1724
1725 #[test]
1726 fn extract_aliases_from_array_form() {
1727 let source = r#"
1728 export default {
1729 resolve: {
1730 alias: [
1731 { find: "@", replacement: "./src" },
1732 { find: "$utils", replacement: "src/lib/utils" }
1733 ]
1734 }
1735 };
1736 "#;
1737
1738 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
1739 assert_eq!(
1740 aliases,
1741 vec![
1742 ("@".to_string(), "./src".to_string()),
1743 ("$utils".to_string(), "src/lib/utils".to_string())
1744 ]
1745 );
1746 }
1747
1748 #[test]
1749 fn extract_aliases_from_object_with_array_values() {
1750 let source = r#"
1751 ({
1752 compilerOptions: {
1753 paths: {
1754 "@/*": ["./src/*"],
1755 "@shared/*": ["./shared/*", "./fallback/*"]
1756 }
1757 }
1758 })
1759 "#;
1760
1761 let aliases = extract_config_aliases(source, &js_path(), &["compilerOptions", "paths"]);
1762 assert_eq!(
1763 aliases,
1764 vec![
1765 ("@/*".to_string(), "./src/*".to_string()),
1766 ("@shared/*".to_string(), "./shared/*".to_string())
1767 ]
1768 );
1769 }
1770
1771 #[test]
1772 fn extract_array_object_strings_mixed_forms() {
1773 let source = r#"
1774 export default {
1775 components: [
1776 "~/components",
1777 { path: "@/feature-components" }
1778 ]
1779 };
1780 "#;
1781
1782 let values =
1783 extract_config_array_object_strings(source, &ts_path(), &["components"], "path");
1784 assert_eq!(
1785 values,
1786 vec![
1787 "~/components".to_string(),
1788 "@/feature-components".to_string()
1789 ]
1790 );
1791 }
1792
1793 #[test]
1794 fn extract_config_plugin_option_string_from_json() {
1795 let source = r#"{
1796 "expo": {
1797 "plugins": [
1798 ["expo-router", { "root": "src/app" }]
1799 ]
1800 }
1801 }"#;
1802
1803 let value = extract_config_plugin_option_string(
1804 source,
1805 &json_path(),
1806 &["expo", "plugins"],
1807 "expo-router",
1808 "root",
1809 );
1810
1811 assert_eq!(value, Some("src/app".to_string()));
1812 }
1813
1814 #[test]
1815 fn extract_config_plugin_option_string_from_top_level_plugins() {
1816 let source = r#"{
1817 "plugins": [
1818 ["expo-router", { "root": "./src/routes" }]
1819 ]
1820 }"#;
1821
1822 let value = extract_config_plugin_option_string_from_paths(
1823 source,
1824 &json_path(),
1825 &[&["plugins"], &["expo", "plugins"]],
1826 "expo-router",
1827 "root",
1828 );
1829
1830 assert_eq!(value, Some("./src/routes".to_string()));
1831 }
1832
1833 #[test]
1834 fn extract_config_plugin_option_string_from_ts_config() {
1835 let source = r"
1836 export default {
1837 expo: {
1838 plugins: [
1839 ['expo-router', { root: './src/app' }]
1840 ]
1841 }
1842 };
1843 ";
1844
1845 let value = extract_config_plugin_option_string(
1846 source,
1847 &ts_path(),
1848 &["expo", "plugins"],
1849 "expo-router",
1850 "root",
1851 );
1852
1853 assert_eq!(value, Some("./src/app".to_string()));
1854 }
1855
1856 #[test]
1857 fn extract_config_plugin_option_string_returns_none_when_plugin_missing() {
1858 let source = r#"{
1859 "expo": {
1860 "plugins": [
1861 ["expo-font", {}]
1862 ]
1863 }
1864 }"#;
1865
1866 let value = extract_config_plugin_option_string(
1867 source,
1868 &json_path(),
1869 &["expo", "plugins"],
1870 "expo-router",
1871 "root",
1872 );
1873
1874 assert_eq!(value, None);
1875 }
1876
1877 #[test]
1878 fn normalize_config_path_relative_to_root() {
1879 let config_path = PathBuf::from("/project/vite.config.ts");
1880 let root = PathBuf::from("/project");
1881
1882 assert_eq!(
1883 normalize_config_path("./src/lib", &config_path, &root),
1884 Some("src/lib".to_string())
1885 );
1886 assert_eq!(
1887 normalize_config_path("/src/lib", &config_path, &root),
1888 Some("src/lib".to_string())
1889 );
1890 }
1891
1892 #[test]
1895 fn json_wrapped_in_parens_string() {
1896 let source = r#"({"extends": "@tsconfig/node18/tsconfig.json"})"#;
1897 let val = extract_config_string(source, &js_path(), &["extends"]);
1898 assert_eq!(val, Some("@tsconfig/node18/tsconfig.json".to_string()));
1899 }
1900
1901 #[test]
1902 fn json_wrapped_in_parens_nested_array() {
1903 let source =
1904 r#"({"compilerOptions": {"types": ["node", "jest"]}, "include": ["src/**/*"]})"#;
1905 let types = extract_config_string_array(source, &js_path(), &["compilerOptions", "types"]);
1906 assert_eq!(types, vec!["node", "jest"]);
1907
1908 let include = extract_config_string_array(source, &js_path(), &["include"]);
1909 assert_eq!(include, vec!["src/**/*"]);
1910 }
1911
1912 #[test]
1913 fn json_wrapped_in_parens_object_keys() {
1914 let source = r#"({"plugins": {"autoprefixer": {}, "tailwindcss": {}}})"#;
1915 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1916 assert_eq!(keys, vec!["autoprefixer", "tailwindcss"]);
1917 }
1918
1919 fn json_path() -> PathBuf {
1922 PathBuf::from("config.json")
1923 }
1924
1925 #[test]
1926 fn json_file_parsed_correctly() {
1927 let source = r#"{"key": "value", "list": ["a", "b"]}"#;
1928 let val = extract_config_string(source, &json_path(), &["key"]);
1929 assert_eq!(val, Some("value".to_string()));
1930
1931 let list = extract_config_string_array(source, &json_path(), &["list"]);
1932 assert_eq!(list, vec!["a", "b"]);
1933 }
1934
1935 #[test]
1936 fn jsonc_file_parsed_correctly() {
1937 let source = r#"{"key": "value"}"#;
1938 let path = PathBuf::from("tsconfig.jsonc");
1939 let val = extract_config_string(source, &path, &["key"]);
1940 assert_eq!(val, Some("value".to_string()));
1941 }
1942
1943 #[test]
1946 fn extract_define_config_arrow_function() {
1947 let source = r#"
1948 import { defineConfig } from 'vite';
1949 export default defineConfig(() => ({
1950 test: {
1951 include: ["**/*.test.ts"]
1952 }
1953 }));
1954 "#;
1955 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
1956 assert_eq!(include, vec!["**/*.test.ts"]);
1957 }
1958
1959 #[test]
1960 fn extract_config_from_default_export_function_declaration() {
1961 let source = r#"
1962 export default function createConfig() {
1963 return {
1964 clientModules: ["./src/client/global.js"]
1965 };
1966 }
1967 "#;
1968
1969 let client_modules = extract_config_string_array(source, &ts_path(), &["clientModules"]);
1970 assert_eq!(client_modules, vec!["./src/client/global.js"]);
1971 }
1972
1973 #[test]
1974 fn extract_config_from_default_export_async_function_declaration() {
1975 let source = r#"
1976 export default async function createConfigAsync() {
1977 return {
1978 docs: {
1979 path: "knowledge"
1980 }
1981 };
1982 }
1983 "#;
1984
1985 let docs_path = extract_config_string(source, &ts_path(), &["docs", "path"]);
1986 assert_eq!(docs_path, Some("knowledge".to_string()));
1987 }
1988
1989 #[test]
1990 fn extract_config_from_exported_arrow_function_identifier() {
1991 let source = r#"
1992 const config = async () => {
1993 return {
1994 themes: ["classic"]
1995 };
1996 };
1997
1998 export default config;
1999 "#;
2000
2001 let themes = extract_config_shallow_strings(source, &ts_path(), "themes");
2002 assert_eq!(themes, vec!["classic"]);
2003 }
2004
2005 #[test]
2008 fn module_exports_nested_string() {
2009 let source = r#"
2010 module.exports = {
2011 resolve: {
2012 alias: {
2013 "@": "./src"
2014 }
2015 }
2016 };
2017 "#;
2018 let val = extract_config_string(source, &js_path(), &["resolve", "alias", "@"]);
2019 assert_eq!(val, Some("./src".to_string()));
2020 }
2021
2022 #[test]
2025 fn property_strings_nested_objects() {
2026 let source = r#"
2027 export default {
2028 plugins: {
2029 group1: { a: "val-a" },
2030 group2: { b: "val-b" }
2031 }
2032 };
2033 "#;
2034 let values = extract_config_property_strings(source, &js_path(), "plugins");
2035 assert!(values.contains(&"val-a".to_string()));
2036 assert!(values.contains(&"val-b".to_string()));
2037 }
2038
2039 #[test]
2040 fn property_strings_missing_key_returns_empty() {
2041 let source = r#"export default { other: "value" };"#;
2042 let values = extract_config_property_strings(source, &js_path(), "missing");
2043 assert!(values.is_empty());
2044 }
2045
2046 #[test]
2049 fn shallow_strings_tuple_array() {
2050 let source = r#"
2051 module.exports = {
2052 reporters: ["default", ["jest-junit", { outputDirectory: "reports" }]]
2053 };
2054 "#;
2055 let values = extract_config_shallow_strings(source, &js_path(), "reporters");
2056 assert_eq!(values, vec!["default", "jest-junit"]);
2057 assert!(!values.contains(&"reports".to_string()));
2059 }
2060
2061 #[test]
2062 fn shallow_strings_single_string() {
2063 let source = r#"export default { preset: "ts-jest" };"#;
2064 let values = extract_config_shallow_strings(source, &js_path(), "preset");
2065 assert_eq!(values, vec!["ts-jest"]);
2066 }
2067
2068 #[test]
2069 fn shallow_strings_missing_key() {
2070 let source = r#"export default { other: "val" };"#;
2071 let values = extract_config_shallow_strings(source, &js_path(), "missing");
2072 assert!(values.is_empty());
2073 }
2074
2075 #[test]
2078 fn nested_shallow_strings_vitest_reporters() {
2079 let source = r#"
2080 export default {
2081 test: {
2082 reporters: ["default", "vitest-sonar-reporter"]
2083 }
2084 };
2085 "#;
2086 let values =
2087 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
2088 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
2089 }
2090
2091 #[test]
2092 fn nested_shallow_strings_tuple_format() {
2093 let source = r#"
2094 export default {
2095 test: {
2096 reporters: ["default", ["vitest-sonar-reporter", { outputFile: "report.xml" }]]
2097 }
2098 };
2099 "#;
2100 let values =
2101 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
2102 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
2103 }
2104
2105 #[test]
2106 fn nested_shallow_strings_missing_outer() {
2107 let source = r"export default { other: {} };";
2108 let values =
2109 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
2110 assert!(values.is_empty());
2111 }
2112
2113 #[test]
2114 fn nested_shallow_strings_missing_inner() {
2115 let source = r#"export default { test: { include: ["**/*.test.ts"] } };"#;
2116 let values =
2117 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
2118 assert!(values.is_empty());
2119 }
2120
2121 #[test]
2124 fn string_or_array_missing_path() {
2125 let source = r"export default {};";
2126 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2127 assert!(result.is_empty());
2128 }
2129
2130 #[test]
2131 fn string_or_array_non_string_values() {
2132 let source = r"export default { entry: [42, true] };";
2134 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2135 assert!(result.is_empty());
2136 }
2137
2138 #[test]
2141 fn array_nested_extraction() {
2142 let source = r#"
2143 export default defineConfig({
2144 test: {
2145 projects: [
2146 {
2147 test: {
2148 setupFiles: ["./test/setup-a.ts"]
2149 }
2150 },
2151 {
2152 test: {
2153 setupFiles: "./test/setup-b.ts"
2154 }
2155 }
2156 ]
2157 }
2158 });
2159 "#;
2160 let results = extract_config_array_nested_string_or_array(
2161 source,
2162 &ts_path(),
2163 &["test", "projects"],
2164 &["test", "setupFiles"],
2165 );
2166 assert!(results.contains(&"./test/setup-a.ts".to_string()));
2167 assert!(results.contains(&"./test/setup-b.ts".to_string()));
2168 }
2169
2170 #[test]
2171 fn array_nested_empty_when_no_array() {
2172 let source = r#"export default { test: { projects: "not-an-array" } };"#;
2173 let results = extract_config_array_nested_string_or_array(
2174 source,
2175 &js_path(),
2176 &["test", "projects"],
2177 &["test", "setupFiles"],
2178 );
2179 assert!(results.is_empty());
2180 }
2181
2182 #[test]
2185 fn object_nested_extraction() {
2186 let source = r#"{
2187 "projects": {
2188 "app-one": {
2189 "architect": {
2190 "build": {
2191 "options": {
2192 "styles": ["src/styles.css"]
2193 }
2194 }
2195 }
2196 }
2197 }
2198 }"#;
2199 let results = extract_config_object_nested_string_or_array(
2200 source,
2201 &json_path(),
2202 &["projects"],
2203 &["architect", "build", "options", "styles"],
2204 );
2205 assert_eq!(results, vec!["src/styles.css"]);
2206 }
2207
2208 #[test]
2209 fn array_with_object_input_form_extracted() {
2210 let source = r#"{
2216 "projects": {
2217 "app": {
2218 "architect": {
2219 "build": {
2220 "options": {
2221 "styles": [
2222 "src/styles.scss",
2223 { "input": "src/theme.scss", "bundleName": "theme", "inject": false },
2224 { "bundleName": "lazy-only" }
2225 ]
2226 }
2227 }
2228 }
2229 }
2230 }
2231 }"#;
2232 let results = extract_config_object_nested_string_or_array(
2233 source,
2234 &json_path(),
2235 &["projects"],
2236 &["architect", "build", "options", "styles"],
2237 );
2238 assert!(
2239 results.contains(&"src/styles.scss".to_string()),
2240 "string form must still work: {results:?}"
2241 );
2242 assert!(
2243 results.contains(&"src/theme.scss".to_string()),
2244 "object form with `input` must be extracted: {results:?}"
2245 );
2246 assert!(
2249 !results.contains(&"lazy-only".to_string()),
2250 "bundleName must not be misinterpreted as a path: {results:?}"
2251 );
2252 assert!(
2253 !results.contains(&"theme".to_string()),
2254 "bundleName from full object must not leak: {results:?}"
2255 );
2256 }
2257
2258 #[test]
2261 fn object_nested_strings_extraction() {
2262 let source = r#"{
2263 "targets": {
2264 "build": {
2265 "executor": "@angular/build:application"
2266 },
2267 "test": {
2268 "executor": "@nx/vite:test"
2269 }
2270 }
2271 }"#;
2272 let results =
2273 extract_config_object_nested_strings(source, &json_path(), &["targets"], &["executor"]);
2274 assert!(results.contains(&"@angular/build:application".to_string()));
2275 assert!(results.contains(&"@nx/vite:test".to_string()));
2276 }
2277
2278 #[test]
2281 fn require_strings_direct_call() {
2282 let source = r"module.exports = { adapter: require('@sveltejs/adapter-node') };";
2283 let deps = extract_config_require_strings(source, &js_path(), "adapter");
2284 assert_eq!(deps, vec!["@sveltejs/adapter-node"]);
2285 }
2286
2287 #[test]
2288 fn require_strings_no_matching_key() {
2289 let source = r"module.exports = { other: require('something') };";
2290 let deps = extract_config_require_strings(source, &js_path(), "plugins");
2291 assert!(deps.is_empty());
2292 }
2293
2294 #[test]
2297 fn extract_imports_no_imports() {
2298 let source = r"export default {};";
2299 let imports = extract_imports(source, &js_path());
2300 assert!(imports.is_empty());
2301 }
2302
2303 #[test]
2304 fn extract_imports_side_effect_import() {
2305 let source = r"
2306 import 'polyfill';
2307 import './local-setup';
2308 export default {};
2309 ";
2310 let imports = extract_imports(source, &js_path());
2311 assert_eq!(imports, vec!["polyfill", "./local-setup"]);
2312 }
2313
2314 #[test]
2315 fn extract_imports_mixed_specifiers() {
2316 let source = r"
2317 import defaultExport from 'module-a';
2318 import { named } from 'module-b';
2319 import * as ns from 'module-c';
2320 export default {};
2321 ";
2322 let imports = extract_imports(source, &js_path());
2323 assert_eq!(imports, vec!["module-a", "module-b", "module-c"]);
2324 }
2325
2326 #[test]
2329 fn template_literal_in_string_or_array() {
2330 let source = r"export default { entry: `./src/index.ts` };";
2331 let result = extract_config_string_or_array(source, &ts_path(), &["entry"]);
2332 assert_eq!(result, vec!["./src/index.ts"]);
2333 }
2334
2335 #[test]
2336 fn template_literal_in_config_string() {
2337 let source = r"export default { testDir: `./tests` };";
2338 let val = extract_config_string(source, &js_path(), &["testDir"]);
2339 assert_eq!(val, Some("./tests".to_string()));
2340 }
2341
2342 #[test]
2345 fn nested_string_array_empty_path() {
2346 let source = r#"export default { items: ["a", "b"] };"#;
2347 let result = extract_config_string_array(source, &js_path(), &[]);
2348 assert!(result.is_empty());
2349 }
2350
2351 #[test]
2352 fn nested_string_empty_path() {
2353 let source = r#"export default { key: "val" };"#;
2354 let result = extract_config_string(source, &js_path(), &[]);
2355 assert!(result.is_none());
2356 }
2357
2358 #[test]
2359 fn object_keys_empty_path() {
2360 let source = r"export default { plugins: {} };";
2361 let result = extract_config_object_keys(source, &js_path(), &[]);
2362 assert!(result.is_empty());
2363 }
2364
2365 #[test]
2368 fn no_config_object_returns_empty() {
2369 let source = r"const x = 42;";
2371 let result = extract_config_string(source, &js_path(), &["key"]);
2372 assert!(result.is_none());
2373
2374 let arr = extract_config_string_array(source, &js_path(), &["items"]);
2375 assert!(arr.is_empty());
2376
2377 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
2378 assert!(keys.is_empty());
2379 }
2380
2381 #[test]
2384 fn property_with_string_key() {
2385 let source = r#"export default { "string-key": "value" };"#;
2386 let val = extract_config_string(source, &js_path(), &["string-key"]);
2387 assert_eq!(val, Some("value".to_string()));
2388 }
2389
2390 #[test]
2391 fn nested_navigation_through_non_object() {
2392 let source = r#"export default { level1: "not-an-object" };"#;
2394 let val = extract_config_string(source, &js_path(), &["level1", "level2"]);
2395 assert!(val.is_none());
2396 }
2397
2398 #[test]
2401 fn variable_reference_untyped() {
2402 let source = r#"
2403 const config = {
2404 testDir: "./tests"
2405 };
2406 export default config;
2407 "#;
2408 let val = extract_config_string(source, &js_path(), &["testDir"]);
2409 assert_eq!(val, Some("./tests".to_string()));
2410 }
2411
2412 #[test]
2413 fn variable_reference_with_type_annotation() {
2414 let source = r#"
2415 import type { StorybookConfig } from '@storybook/react-vite';
2416 const config: StorybookConfig = {
2417 addons: ["@storybook/addon-a11y", "@storybook/addon-docs"],
2418 framework: "@storybook/react-vite"
2419 };
2420 export default config;
2421 "#;
2422 let addons = extract_config_shallow_strings(source, &ts_path(), "addons");
2423 assert_eq!(
2424 addons,
2425 vec!["@storybook/addon-a11y", "@storybook/addon-docs"]
2426 );
2427
2428 let framework = extract_config_string(source, &ts_path(), &["framework"]);
2429 assert_eq!(framework, Some("@storybook/react-vite".to_string()));
2430 }
2431
2432 #[test]
2433 fn variable_reference_with_define_config() {
2434 let source = r#"
2435 import { defineConfig } from 'vitest/config';
2436 const config = defineConfig({
2437 test: {
2438 include: ["**/*.test.ts"]
2439 }
2440 });
2441 export default config;
2442 "#;
2443 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
2444 assert_eq!(include, vec!["**/*.test.ts"]);
2445 }
2446
2447 #[test]
2450 fn ts_satisfies_direct_export() {
2451 let source = r#"
2452 export default {
2453 testDir: "./tests"
2454 } satisfies PlaywrightTestConfig;
2455 "#;
2456 let val = extract_config_string(source, &ts_path(), &["testDir"]);
2457 assert_eq!(val, Some("./tests".to_string()));
2458 }
2459
2460 #[test]
2461 fn ts_as_direct_export() {
2462 let source = r#"
2463 export default {
2464 testDir: "./tests"
2465 } as const;
2466 "#;
2467 let val = extract_config_string(source, &ts_path(), &["testDir"]);
2468 assert_eq!(val, Some("./tests".to_string()));
2469 }
2470}