1use std::path::Path;
15
16use oxc_allocator::Allocator;
17#[allow(clippy::wildcard_imports, reason = "many AST types used")]
18use oxc_ast::ast::*;
19use oxc_parser::Parser;
20use oxc_span::SourceType;
21
22#[must_use]
24pub fn extract_imports(source: &str, path: &Path) -> Vec<String> {
25 extract_from_source(source, path, |program| {
26 let mut sources = Vec::new();
27 for stmt in &program.body {
28 if let Statement::ImportDeclaration(decl) = stmt {
29 sources.push(decl.source.value.to_string());
30 }
31 }
32 Some(sources)
33 })
34 .unwrap_or_default()
35}
36
37#[must_use]
39pub fn extract_config_string_array(source: &str, path: &Path, prop_path: &[&str]) -> Vec<String> {
40 extract_from_source(source, path, |program| {
41 let obj = find_config_object(program)?;
42 get_nested_string_array_from_object(obj, prop_path)
43 })
44 .unwrap_or_default()
45}
46
47#[must_use]
49pub fn extract_config_string(source: &str, path: &Path, prop_path: &[&str]) -> Option<String> {
50 extract_from_source(source, path, |program| {
51 let obj = find_config_object(program)?;
52 get_nested_string_from_object(obj, prop_path)
53 })
54}
55
56#[must_use]
63pub fn extract_config_property_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
64 extract_from_source(source, path, |program| {
65 let obj = find_config_object(program)?;
66 let mut values = Vec::new();
67 if let Some(prop) = find_property(obj, key) {
68 collect_all_string_values(&prop.value, &mut values);
69 }
70 Some(values)
71 })
72 .unwrap_or_default()
73}
74
75#[must_use]
82pub fn extract_config_shallow_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
83 extract_from_source(source, path, |program| {
84 let obj = find_config_object(program)?;
85 let prop = find_property(obj, key)?;
86 Some(collect_shallow_string_values(&prop.value))
87 })
88 .unwrap_or_default()
89}
90
91#[must_use]
97pub fn extract_config_nested_shallow_strings(
98 source: &str,
99 path: &Path,
100 outer_path: &[&str],
101 key: &str,
102) -> Vec<String> {
103 extract_from_source(source, path, |program| {
104 let obj = find_config_object(program)?;
105 let nested = get_nested_expression(obj, outer_path)?;
106 if let Expression::ObjectExpression(nested_obj) = nested {
107 let prop = find_property(nested_obj, key)?;
108 Some(collect_shallow_string_values(&prop.value))
109 } else {
110 None
111 }
112 })
113 .unwrap_or_default()
114}
115
116pub fn find_config_object_pub<'a>(program: &'a Program) -> Option<&'a ObjectExpression<'a>> {
118 find_config_object(program)
119}
120
121#[must_use]
126pub fn extract_config_object_keys(source: &str, path: &Path, prop_path: &[&str]) -> Vec<String> {
127 extract_from_source(source, path, |program| {
128 let obj = find_config_object(program)?;
129 get_nested_object_keys(obj, prop_path)
130 })
131 .unwrap_or_default()
132}
133
134#[must_use]
141pub fn extract_config_string_or_array(
142 source: &str,
143 path: &Path,
144 prop_path: &[&str],
145) -> Vec<String> {
146 extract_from_source(source, path, |program| {
147 let obj = find_config_object(program)?;
148 get_nested_string_or_array(obj, prop_path)
149 })
150 .unwrap_or_default()
151}
152
153#[must_use]
160pub fn extract_config_array_nested_string_or_array(
161 source: &str,
162 path: &Path,
163 array_path: &[&str],
164 inner_path: &[&str],
165) -> Vec<String> {
166 extract_from_source(source, path, |program| {
167 let obj = find_config_object(program)?;
168 let array_expr = get_nested_expression(obj, array_path)?;
169 let Expression::ArrayExpression(arr) = array_expr else {
170 return None;
171 };
172 let mut results = Vec::new();
173 for element in &arr.elements {
174 if let Some(Expression::ObjectExpression(element_obj)) = element.as_expression()
175 && let Some(values) = get_nested_string_or_array(element_obj, inner_path)
176 {
177 results.extend(values);
178 }
179 }
180 if results.is_empty() {
181 None
182 } else {
183 Some(results)
184 }
185 })
186 .unwrap_or_default()
187}
188
189#[must_use]
196pub fn extract_config_object_nested_string_or_array(
197 source: &str,
198 path: &Path,
199 object_path: &[&str],
200 inner_path: &[&str],
201) -> Vec<String> {
202 extract_config_object_nested(source, path, object_path, |value_obj| {
203 get_nested_string_or_array(value_obj, inner_path)
204 })
205}
206
207#[must_use]
212pub fn extract_config_object_nested_strings(
213 source: &str,
214 path: &Path,
215 object_path: &[&str],
216 inner_path: &[&str],
217) -> Vec<String> {
218 extract_config_object_nested(source, path, object_path, |value_obj| {
219 get_nested_string_from_object(value_obj, inner_path).map(|s| vec![s])
220 })
221}
222
223fn extract_config_object_nested(
228 source: &str,
229 path: &Path,
230 object_path: &[&str],
231 extract_fn: impl Fn(&ObjectExpression<'_>) -> Option<Vec<String>>,
232) -> Vec<String> {
233 extract_from_source(source, path, |program| {
234 let obj = find_config_object(program)?;
235 let obj_expr = get_nested_expression(obj, object_path)?;
236 let Expression::ObjectExpression(target_obj) = obj_expr else {
237 return None;
238 };
239 let mut results = Vec::new();
240 for prop in &target_obj.properties {
241 if let ObjectPropertyKind::ObjectProperty(p) = prop
242 && let Expression::ObjectExpression(value_obj) = &p.value
243 && let Some(values) = extract_fn(value_obj)
244 {
245 results.extend(values);
246 }
247 }
248 if results.is_empty() {
249 None
250 } else {
251 Some(results)
252 }
253 })
254 .unwrap_or_default()
255}
256
257#[must_use]
263pub fn extract_config_require_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
264 extract_from_source(source, path, |program| {
265 let obj = find_config_object(program)?;
266 let prop = find_property(obj, key)?;
267 Some(collect_require_sources(&prop.value))
268 })
269 .unwrap_or_default()
270}
271
272fn extract_from_source<T>(
281 source: &str,
282 path: &Path,
283 extractor: impl FnOnce(&Program) -> Option<T>,
284) -> Option<T> {
285 let source_type = SourceType::from_path(path).unwrap_or_default();
286 let alloc = Allocator::default();
287
288 let is_json = path
291 .extension()
292 .is_some_and(|ext| ext == "json" || ext == "jsonc");
293 if is_json {
294 let wrapped = format!("({source})");
295 let parsed = Parser::new(&alloc, &wrapped, SourceType::mjs()).parse();
296 return extractor(&parsed.program);
297 }
298
299 let parsed = Parser::new(&alloc, source, source_type).parse();
300 extractor(&parsed.program)
301}
302
303fn find_config_object<'a>(program: &'a Program) -> Option<&'a ObjectExpression<'a>> {
315 for stmt in &program.body {
316 match stmt {
317 Statement::ExportDefaultDeclaration(decl) => {
319 let expr: Option<&Expression> = match &decl.declaration {
321 ExportDefaultDeclarationKind::ObjectExpression(obj) => {
322 return Some(obj);
323 }
324 _ => decl.declaration.as_expression(),
325 };
326 if let Some(expr) = expr {
327 if let Some(obj) = extract_object_from_expression(expr) {
329 return Some(obj);
330 }
331 if let Some(name) = unwrap_to_identifier_name(expr) {
334 return find_variable_init_object(program, name);
335 }
336 }
337 }
338 Statement::ExpressionStatement(expr_stmt) => {
340 if let Expression::AssignmentExpression(assign) = &expr_stmt.expression
341 && is_module_exports_target(&assign.left)
342 {
343 return extract_object_from_expression(&assign.right);
344 }
345 }
346 _ => {}
347 }
348 }
349
350 if program.body.len() == 1
353 && let Statement::ExpressionStatement(expr_stmt) = &program.body[0]
354 {
355 match &expr_stmt.expression {
356 Expression::ObjectExpression(obj) => return Some(obj),
357 Expression::ParenthesizedExpression(paren) => {
358 if let Expression::ObjectExpression(obj) = &paren.expression {
359 return Some(obj);
360 }
361 }
362 _ => {}
363 }
364 }
365
366 None
367}
368
369fn extract_object_from_expression<'a>(
371 expr: &'a Expression<'a>,
372) -> Option<&'a ObjectExpression<'a>> {
373 match expr {
374 Expression::ObjectExpression(obj) => Some(obj),
376 Expression::CallExpression(call) => {
378 for arg in &call.arguments {
380 match arg {
381 Argument::ObjectExpression(obj) => return Some(obj),
382 Argument::ArrowFunctionExpression(arrow) => {
384 if arrow.expression
385 && !arrow.body.statements.is_empty()
386 && let Statement::ExpressionStatement(expr_stmt) =
387 &arrow.body.statements[0]
388 {
389 return extract_object_from_expression(&expr_stmt.expression);
390 }
391 }
392 _ => {}
393 }
394 }
395 None
396 }
397 Expression::ParenthesizedExpression(paren) => {
399 extract_object_from_expression(&paren.expression)
400 }
401 Expression::TSSatisfiesExpression(ts_sat) => {
403 extract_object_from_expression(&ts_sat.expression)
404 }
405 Expression::TSAsExpression(ts_as) => extract_object_from_expression(&ts_as.expression),
406 _ => None,
407 }
408}
409
410fn is_module_exports_target(target: &AssignmentTarget) -> bool {
412 if let AssignmentTarget::StaticMemberExpression(member) = target
413 && let Expression::Identifier(obj) = &member.object
414 {
415 return obj.name == "module" && member.property.name == "exports";
416 }
417 false
418}
419
420fn unwrap_to_identifier_name<'a>(expr: &'a Expression<'a>) -> Option<&'a str> {
424 match expr {
425 Expression::Identifier(id) => Some(&id.name),
426 Expression::TSSatisfiesExpression(ts_sat) => unwrap_to_identifier_name(&ts_sat.expression),
427 Expression::TSAsExpression(ts_as) => unwrap_to_identifier_name(&ts_as.expression),
428 _ => None,
429 }
430}
431
432fn find_variable_init_object<'a>(
437 program: &'a Program,
438 name: &str,
439) -> Option<&'a ObjectExpression<'a>> {
440 for stmt in &program.body {
441 if let Statement::VariableDeclaration(decl) = stmt {
442 for declarator in &decl.declarations {
443 if let BindingPattern::BindingIdentifier(id) = &declarator.id
444 && id.name == name
445 && let Some(init) = &declarator.init
446 {
447 return extract_object_from_expression(init);
448 }
449 }
450 }
451 }
452 None
453}
454
455fn find_property<'a>(obj: &'a ObjectExpression<'a>, key: &str) -> Option<&'a ObjectProperty<'a>> {
457 for prop in &obj.properties {
458 if let ObjectPropertyKind::ObjectProperty(p) = prop
459 && property_key_matches(&p.key, key)
460 {
461 return Some(p);
462 }
463 }
464 None
465}
466
467fn property_key_matches(key: &PropertyKey, name: &str) -> bool {
469 match key {
470 PropertyKey::StaticIdentifier(id) => id.name == name,
471 PropertyKey::StringLiteral(s) => s.value == name,
472 _ => false,
473 }
474}
475
476fn get_object_string_property(obj: &ObjectExpression, key: &str) -> Option<String> {
478 find_property(obj, key).and_then(|p| expression_to_string(&p.value))
479}
480
481fn get_object_string_array_property(obj: &ObjectExpression, key: &str) -> Vec<String> {
483 find_property(obj, key)
484 .map(|p| expression_to_string_array(&p.value))
485 .unwrap_or_default()
486}
487
488fn get_nested_string_array_from_object(
490 obj: &ObjectExpression,
491 path: &[&str],
492) -> Option<Vec<String>> {
493 if path.is_empty() {
494 return None;
495 }
496 if path.len() == 1 {
497 return Some(get_object_string_array_property(obj, path[0]));
498 }
499 let prop = find_property(obj, path[0])?;
501 if let Expression::ObjectExpression(nested) = &prop.value {
502 get_nested_string_array_from_object(nested, &path[1..])
503 } else {
504 None
505 }
506}
507
508fn get_nested_string_from_object(obj: &ObjectExpression, path: &[&str]) -> Option<String> {
510 if path.is_empty() {
511 return None;
512 }
513 if path.len() == 1 {
514 return get_object_string_property(obj, path[0]);
515 }
516 let prop = find_property(obj, path[0])?;
517 if let Expression::ObjectExpression(nested) = &prop.value {
518 get_nested_string_from_object(nested, &path[1..])
519 } else {
520 None
521 }
522}
523
524fn expression_to_string(expr: &Expression) -> Option<String> {
526 match expr {
527 Expression::StringLiteral(s) => Some(s.value.to_string()),
528 Expression::TemplateLiteral(t) if t.expressions.is_empty() => {
529 t.quasis.first().map(|q| q.value.raw.to_string())
531 }
532 _ => None,
533 }
534}
535
536fn expression_to_string_array(expr: &Expression) -> Vec<String> {
538 match expr {
539 Expression::ArrayExpression(arr) => arr
540 .elements
541 .iter()
542 .filter_map(|el| match el {
543 ArrayExpressionElement::SpreadElement(_) => None,
544 _ => el.as_expression().and_then(expression_to_string),
545 })
546 .collect(),
547 _ => vec![],
548 }
549}
550
551fn collect_shallow_string_values(expr: &Expression) -> Vec<String> {
556 let mut values = Vec::new();
557 match expr {
558 Expression::StringLiteral(s) => {
559 values.push(s.value.to_string());
560 }
561 Expression::ArrayExpression(arr) => {
562 for el in &arr.elements {
563 if let Some(inner) = el.as_expression() {
564 match inner {
565 Expression::StringLiteral(s) => {
566 values.push(s.value.to_string());
567 }
568 Expression::ArrayExpression(sub_arr) => {
570 if let Some(first) = sub_arr.elements.first()
571 && let Some(first_expr) = first.as_expression()
572 && let Some(s) = expression_to_string(first_expr)
573 {
574 values.push(s);
575 }
576 }
577 _ => {}
578 }
579 }
580 }
581 }
582 _ => {}
583 }
584 values
585}
586
587fn collect_all_string_values(expr: &Expression, values: &mut Vec<String>) {
589 match expr {
590 Expression::StringLiteral(s) => {
591 values.push(s.value.to_string());
592 }
593 Expression::ArrayExpression(arr) => {
594 for el in &arr.elements {
595 if let Some(expr) = el.as_expression() {
596 collect_all_string_values(expr, values);
597 }
598 }
599 }
600 Expression::ObjectExpression(obj) => {
601 for prop in &obj.properties {
602 if let ObjectPropertyKind::ObjectProperty(p) = prop {
603 collect_all_string_values(&p.value, values);
604 }
605 }
606 }
607 _ => {}
608 }
609}
610
611fn property_key_to_string(key: &PropertyKey) -> Option<String> {
613 match key {
614 PropertyKey::StaticIdentifier(id) => Some(id.name.to_string()),
615 PropertyKey::StringLiteral(s) => Some(s.value.to_string()),
616 _ => None,
617 }
618}
619
620fn get_nested_object_keys(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
622 if path.is_empty() {
623 return None;
624 }
625 let prop = find_property(obj, path[0])?;
626 if path.len() == 1 {
627 if let Expression::ObjectExpression(nested) = &prop.value {
628 let keys = nested
629 .properties
630 .iter()
631 .filter_map(|p| {
632 if let ObjectPropertyKind::ObjectProperty(p) = p {
633 property_key_to_string(&p.key)
634 } else {
635 None
636 }
637 })
638 .collect();
639 return Some(keys);
640 }
641 return None;
642 }
643 if let Expression::ObjectExpression(nested) = &prop.value {
644 get_nested_object_keys(nested, &path[1..])
645 } else {
646 None
647 }
648}
649
650fn get_nested_expression<'a>(
652 obj: &'a ObjectExpression<'a>,
653 path: &[&str],
654) -> Option<&'a Expression<'a>> {
655 if path.is_empty() {
656 return None;
657 }
658 let prop = find_property(obj, path[0])?;
659 if path.len() == 1 {
660 return Some(&prop.value);
661 }
662 if let Expression::ObjectExpression(nested) = &prop.value {
663 get_nested_expression(nested, &path[1..])
664 } else {
665 None
666 }
667}
668
669fn get_nested_string_or_array(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
671 if path.is_empty() {
672 return None;
673 }
674 if path.len() == 1 {
675 let prop = find_property(obj, path[0])?;
676 return Some(expression_to_string_or_array(&prop.value));
677 }
678 let prop = find_property(obj, path[0])?;
679 if let Expression::ObjectExpression(nested) = &prop.value {
680 get_nested_string_or_array(nested, &path[1..])
681 } else {
682 None
683 }
684}
685
686fn expression_to_string_or_array(expr: &Expression) -> Vec<String> {
688 match expr {
689 Expression::StringLiteral(s) => vec![s.value.to_string()],
690 Expression::TemplateLiteral(t) if t.expressions.is_empty() => t
691 .quasis
692 .first()
693 .map(|q| vec![q.value.raw.to_string()])
694 .unwrap_or_default(),
695 Expression::ArrayExpression(arr) => arr
696 .elements
697 .iter()
698 .filter_map(|el| el.as_expression().and_then(expression_to_string))
699 .collect(),
700 Expression::ObjectExpression(obj) => obj
701 .properties
702 .iter()
703 .filter_map(|p| {
704 if let ObjectPropertyKind::ObjectProperty(p) = p {
705 expression_to_string(&p.value)
706 } else {
707 None
708 }
709 })
710 .collect(),
711 _ => vec![],
712 }
713}
714
715fn collect_require_sources(expr: &Expression) -> Vec<String> {
717 let mut sources = Vec::new();
718 match expr {
719 Expression::CallExpression(call) if is_require_call(call) => {
720 if let Some(s) = get_require_source(call) {
721 sources.push(s);
722 }
723 }
724 Expression::ArrayExpression(arr) => {
725 for el in &arr.elements {
726 if let Some(inner) = el.as_expression() {
727 match inner {
728 Expression::CallExpression(call) if is_require_call(call) => {
729 if let Some(s) = get_require_source(call) {
730 sources.push(s);
731 }
732 }
733 Expression::ArrayExpression(sub_arr) => {
735 if let Some(first) = sub_arr.elements.first()
736 && let Some(Expression::CallExpression(call)) =
737 first.as_expression()
738 && is_require_call(call)
739 && let Some(s) = get_require_source(call)
740 {
741 sources.push(s);
742 }
743 }
744 _ => {}
745 }
746 }
747 }
748 }
749 _ => {}
750 }
751 sources
752}
753
754fn is_require_call(call: &CallExpression) -> bool {
756 matches!(&call.callee, Expression::Identifier(id) if id.name == "require")
757}
758
759fn get_require_source(call: &CallExpression) -> Option<String> {
761 call.arguments.first().and_then(|arg| {
762 if let Argument::StringLiteral(s) = arg {
763 Some(s.value.to_string())
764 } else {
765 None
766 }
767 })
768}
769
770#[cfg(test)]
771mod tests {
772 use super::*;
773 use std::path::PathBuf;
774
775 fn js_path() -> PathBuf {
776 PathBuf::from("config.js")
777 }
778
779 fn ts_path() -> PathBuf {
780 PathBuf::from("config.ts")
781 }
782
783 #[test]
784 fn extract_imports_basic() {
785 let source = r"
786 import foo from 'foo-pkg';
787 import { bar } from '@scope/bar';
788 export default {};
789 ";
790 let imports = extract_imports(source, &js_path());
791 assert_eq!(imports, vec!["foo-pkg", "@scope/bar"]);
792 }
793
794 #[test]
795 fn extract_default_export_object_property() {
796 let source = r#"export default { testDir: "./tests" };"#;
797 let val = extract_config_string(source, &js_path(), &["testDir"]);
798 assert_eq!(val, Some("./tests".to_string()));
799 }
800
801 #[test]
802 fn extract_define_config_property() {
803 let source = r#"
804 import { defineConfig } from 'vitest/config';
805 export default defineConfig({
806 test: {
807 include: ["**/*.test.ts", "**/*.spec.ts"],
808 setupFiles: ["./test/setup.ts"]
809 }
810 });
811 "#;
812 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
813 assert_eq!(include, vec!["**/*.test.ts", "**/*.spec.ts"]);
814
815 let setup = extract_config_string_array(source, &ts_path(), &["test", "setupFiles"]);
816 assert_eq!(setup, vec!["./test/setup.ts"]);
817 }
818
819 #[test]
820 fn extract_module_exports_property() {
821 let source = r#"module.exports = { testEnvironment: "jsdom" };"#;
822 let val = extract_config_string(source, &js_path(), &["testEnvironment"]);
823 assert_eq!(val, Some("jsdom".to_string()));
824 }
825
826 #[test]
827 fn extract_nested_string_array() {
828 let source = r#"
829 export default {
830 resolve: {
831 alias: {
832 "@": "./src"
833 }
834 },
835 test: {
836 include: ["src/**/*.test.ts"]
837 }
838 };
839 "#;
840 let include = extract_config_string_array(source, &js_path(), &["test", "include"]);
841 assert_eq!(include, vec!["src/**/*.test.ts"]);
842 }
843
844 #[test]
845 fn extract_addons_array() {
846 let source = r#"
847 export default {
848 addons: [
849 "@storybook/addon-a11y",
850 "@storybook/addon-docs",
851 "@storybook/addon-links"
852 ]
853 };
854 "#;
855 let addons = extract_config_property_strings(source, &ts_path(), "addons");
856 assert_eq!(
857 addons,
858 vec![
859 "@storybook/addon-a11y",
860 "@storybook/addon-docs",
861 "@storybook/addon-links"
862 ]
863 );
864 }
865
866 #[test]
867 fn handle_empty_config() {
868 let source = "";
869 let result = extract_config_string(source, &js_path(), &["key"]);
870 assert_eq!(result, None);
871 }
872
873 #[test]
876 fn object_keys_postcss_plugins() {
877 let source = r"
878 module.exports = {
879 plugins: {
880 autoprefixer: {},
881 tailwindcss: {},
882 'postcss-import': {}
883 }
884 };
885 ";
886 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
887 assert_eq!(keys, vec!["autoprefixer", "tailwindcss", "postcss-import"]);
888 }
889
890 #[test]
891 fn object_keys_nested_path() {
892 let source = r"
893 export default {
894 build: {
895 plugins: {
896 minify: {},
897 compress: {}
898 }
899 }
900 };
901 ";
902 let keys = extract_config_object_keys(source, &js_path(), &["build", "plugins"]);
903 assert_eq!(keys, vec!["minify", "compress"]);
904 }
905
906 #[test]
907 fn object_keys_empty_object() {
908 let source = r"export default { plugins: {} };";
909 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
910 assert!(keys.is_empty());
911 }
912
913 #[test]
914 fn object_keys_non_object_returns_empty() {
915 let source = r#"export default { plugins: ["a", "b"] };"#;
916 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
917 assert!(keys.is_empty());
918 }
919
920 #[test]
923 fn string_or_array_single_string() {
924 let source = r#"export default { entry: "./src/index.js" };"#;
925 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
926 assert_eq!(result, vec!["./src/index.js"]);
927 }
928
929 #[test]
930 fn string_or_array_array() {
931 let source = r#"export default { entry: ["./src/a.js", "./src/b.js"] };"#;
932 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
933 assert_eq!(result, vec!["./src/a.js", "./src/b.js"]);
934 }
935
936 #[test]
937 fn string_or_array_object_values() {
938 let source =
939 r#"export default { entry: { main: "./src/main.js", vendor: "./src/vendor.js" } };"#;
940 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
941 assert_eq!(result, vec!["./src/main.js", "./src/vendor.js"]);
942 }
943
944 #[test]
945 fn string_or_array_nested_path() {
946 let source = r#"
947 export default {
948 build: {
949 rollupOptions: {
950 input: ["./index.html", "./about.html"]
951 }
952 }
953 };
954 "#;
955 let result = extract_config_string_or_array(
956 source,
957 &js_path(),
958 &["build", "rollupOptions", "input"],
959 );
960 assert_eq!(result, vec!["./index.html", "./about.html"]);
961 }
962
963 #[test]
964 fn string_or_array_template_literal() {
965 let source = r"export default { entry: `./src/index.js` };";
966 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
967 assert_eq!(result, vec!["./src/index.js"]);
968 }
969
970 #[test]
973 fn require_strings_array() {
974 let source = r"
975 module.exports = {
976 plugins: [
977 require('autoprefixer'),
978 require('postcss-import')
979 ]
980 };
981 ";
982 let deps = extract_config_require_strings(source, &js_path(), "plugins");
983 assert_eq!(deps, vec!["autoprefixer", "postcss-import"]);
984 }
985
986 #[test]
987 fn require_strings_with_tuples() {
988 let source = r"
989 module.exports = {
990 plugins: [
991 require('autoprefixer'),
992 [require('postcss-preset-env'), { stage: 3 }]
993 ]
994 };
995 ";
996 let deps = extract_config_require_strings(source, &js_path(), "plugins");
997 assert_eq!(deps, vec!["autoprefixer", "postcss-preset-env"]);
998 }
999
1000 #[test]
1001 fn require_strings_empty_array() {
1002 let source = r"module.exports = { plugins: [] };";
1003 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1004 assert!(deps.is_empty());
1005 }
1006
1007 #[test]
1008 fn require_strings_no_require_calls() {
1009 let source = r#"module.exports = { plugins: ["a", "b"] };"#;
1010 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1011 assert!(deps.is_empty());
1012 }
1013
1014 #[test]
1017 fn json_wrapped_in_parens_string() {
1018 let source = r#"({"extends": "@tsconfig/node18/tsconfig.json"})"#;
1019 let val = extract_config_string(source, &js_path(), &["extends"]);
1020 assert_eq!(val, Some("@tsconfig/node18/tsconfig.json".to_string()));
1021 }
1022
1023 #[test]
1024 fn json_wrapped_in_parens_nested_array() {
1025 let source =
1026 r#"({"compilerOptions": {"types": ["node", "jest"]}, "include": ["src/**/*"]})"#;
1027 let types = extract_config_string_array(source, &js_path(), &["compilerOptions", "types"]);
1028 assert_eq!(types, vec!["node", "jest"]);
1029
1030 let include = extract_config_string_array(source, &js_path(), &["include"]);
1031 assert_eq!(include, vec!["src/**/*"]);
1032 }
1033
1034 #[test]
1035 fn json_wrapped_in_parens_object_keys() {
1036 let source = r#"({"plugins": {"autoprefixer": {}, "tailwindcss": {}}})"#;
1037 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1038 assert_eq!(keys, vec!["autoprefixer", "tailwindcss"]);
1039 }
1040
1041 fn json_path() -> PathBuf {
1044 PathBuf::from("config.json")
1045 }
1046
1047 #[test]
1048 fn json_file_parsed_correctly() {
1049 let source = r#"{"key": "value", "list": ["a", "b"]}"#;
1050 let val = extract_config_string(source, &json_path(), &["key"]);
1051 assert_eq!(val, Some("value".to_string()));
1052
1053 let list = extract_config_string_array(source, &json_path(), &["list"]);
1054 assert_eq!(list, vec!["a", "b"]);
1055 }
1056
1057 #[test]
1058 fn jsonc_file_parsed_correctly() {
1059 let source = r#"{"key": "value"}"#;
1060 let path = PathBuf::from("tsconfig.jsonc");
1061 let val = extract_config_string(source, &path, &["key"]);
1062 assert_eq!(val, Some("value".to_string()));
1063 }
1064
1065 #[test]
1068 fn extract_define_config_arrow_function() {
1069 let source = r#"
1070 import { defineConfig } from 'vite';
1071 export default defineConfig(() => ({
1072 test: {
1073 include: ["**/*.test.ts"]
1074 }
1075 }));
1076 "#;
1077 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
1078 assert_eq!(include, vec!["**/*.test.ts"]);
1079 }
1080
1081 #[test]
1084 fn module_exports_nested_string() {
1085 let source = r#"
1086 module.exports = {
1087 resolve: {
1088 alias: {
1089 "@": "./src"
1090 }
1091 }
1092 };
1093 "#;
1094 let val = extract_config_string(source, &js_path(), &["resolve", "alias", "@"]);
1095 assert_eq!(val, Some("./src".to_string()));
1096 }
1097
1098 #[test]
1101 fn property_strings_nested_objects() {
1102 let source = r#"
1103 export default {
1104 plugins: {
1105 group1: { a: "val-a" },
1106 group2: { b: "val-b" }
1107 }
1108 };
1109 "#;
1110 let values = extract_config_property_strings(source, &js_path(), "plugins");
1111 assert!(values.contains(&"val-a".to_string()));
1112 assert!(values.contains(&"val-b".to_string()));
1113 }
1114
1115 #[test]
1116 fn property_strings_missing_key_returns_empty() {
1117 let source = r#"export default { other: "value" };"#;
1118 let values = extract_config_property_strings(source, &js_path(), "missing");
1119 assert!(values.is_empty());
1120 }
1121
1122 #[test]
1125 fn shallow_strings_tuple_array() {
1126 let source = r#"
1127 module.exports = {
1128 reporters: ["default", ["jest-junit", { outputDirectory: "reports" }]]
1129 };
1130 "#;
1131 let values = extract_config_shallow_strings(source, &js_path(), "reporters");
1132 assert_eq!(values, vec!["default", "jest-junit"]);
1133 assert!(!values.contains(&"reports".to_string()));
1135 }
1136
1137 #[test]
1138 fn shallow_strings_single_string() {
1139 let source = r#"export default { preset: "ts-jest" };"#;
1140 let values = extract_config_shallow_strings(source, &js_path(), "preset");
1141 assert_eq!(values, vec!["ts-jest"]);
1142 }
1143
1144 #[test]
1145 fn shallow_strings_missing_key() {
1146 let source = r#"export default { other: "val" };"#;
1147 let values = extract_config_shallow_strings(source, &js_path(), "missing");
1148 assert!(values.is_empty());
1149 }
1150
1151 #[test]
1154 fn nested_shallow_strings_vitest_reporters() {
1155 let source = r#"
1156 export default {
1157 test: {
1158 reporters: ["default", "vitest-sonar-reporter"]
1159 }
1160 };
1161 "#;
1162 let values =
1163 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
1164 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
1165 }
1166
1167 #[test]
1168 fn nested_shallow_strings_tuple_format() {
1169 let source = r#"
1170 export default {
1171 test: {
1172 reporters: ["default", ["vitest-sonar-reporter", { outputFile: "report.xml" }]]
1173 }
1174 };
1175 "#;
1176 let values =
1177 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
1178 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
1179 }
1180
1181 #[test]
1182 fn nested_shallow_strings_missing_outer() {
1183 let source = r"export default { other: {} };";
1184 let values =
1185 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
1186 assert!(values.is_empty());
1187 }
1188
1189 #[test]
1190 fn nested_shallow_strings_missing_inner() {
1191 let source = r#"export default { test: { include: ["**/*.test.ts"] } };"#;
1192 let values =
1193 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
1194 assert!(values.is_empty());
1195 }
1196
1197 #[test]
1200 fn string_or_array_missing_path() {
1201 let source = r"export default {};";
1202 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1203 assert!(result.is_empty());
1204 }
1205
1206 #[test]
1207 fn string_or_array_non_string_values() {
1208 let source = r"export default { entry: [42, true] };";
1210 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1211 assert!(result.is_empty());
1212 }
1213
1214 #[test]
1217 fn array_nested_extraction() {
1218 let source = r#"
1219 export default defineConfig({
1220 test: {
1221 projects: [
1222 {
1223 test: {
1224 setupFiles: ["./test/setup-a.ts"]
1225 }
1226 },
1227 {
1228 test: {
1229 setupFiles: "./test/setup-b.ts"
1230 }
1231 }
1232 ]
1233 }
1234 });
1235 "#;
1236 let results = extract_config_array_nested_string_or_array(
1237 source,
1238 &ts_path(),
1239 &["test", "projects"],
1240 &["test", "setupFiles"],
1241 );
1242 assert!(results.contains(&"./test/setup-a.ts".to_string()));
1243 assert!(results.contains(&"./test/setup-b.ts".to_string()));
1244 }
1245
1246 #[test]
1247 fn array_nested_empty_when_no_array() {
1248 let source = r#"export default { test: { projects: "not-an-array" } };"#;
1249 let results = extract_config_array_nested_string_or_array(
1250 source,
1251 &js_path(),
1252 &["test", "projects"],
1253 &["test", "setupFiles"],
1254 );
1255 assert!(results.is_empty());
1256 }
1257
1258 #[test]
1261 fn object_nested_extraction() {
1262 let source = r#"{
1263 "projects": {
1264 "app-one": {
1265 "architect": {
1266 "build": {
1267 "options": {
1268 "styles": ["src/styles.css"]
1269 }
1270 }
1271 }
1272 }
1273 }
1274 }"#;
1275 let results = extract_config_object_nested_string_or_array(
1276 source,
1277 &json_path(),
1278 &["projects"],
1279 &["architect", "build", "options", "styles"],
1280 );
1281 assert_eq!(results, vec!["src/styles.css"]);
1282 }
1283
1284 #[test]
1287 fn object_nested_strings_extraction() {
1288 let source = r#"{
1289 "targets": {
1290 "build": {
1291 "executor": "@angular/build:application"
1292 },
1293 "test": {
1294 "executor": "@nx/vite:test"
1295 }
1296 }
1297 }"#;
1298 let results =
1299 extract_config_object_nested_strings(source, &json_path(), &["targets"], &["executor"]);
1300 assert!(results.contains(&"@angular/build:application".to_string()));
1301 assert!(results.contains(&"@nx/vite:test".to_string()));
1302 }
1303
1304 #[test]
1307 fn require_strings_direct_call() {
1308 let source = r"module.exports = { adapter: require('@sveltejs/adapter-node') };";
1309 let deps = extract_config_require_strings(source, &js_path(), "adapter");
1310 assert_eq!(deps, vec!["@sveltejs/adapter-node"]);
1311 }
1312
1313 #[test]
1314 fn require_strings_no_matching_key() {
1315 let source = r"module.exports = { other: require('something') };";
1316 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1317 assert!(deps.is_empty());
1318 }
1319
1320 #[test]
1323 fn extract_imports_no_imports() {
1324 let source = r"export default {};";
1325 let imports = extract_imports(source, &js_path());
1326 assert!(imports.is_empty());
1327 }
1328
1329 #[test]
1330 fn extract_imports_side_effect_import() {
1331 let source = r"
1332 import 'polyfill';
1333 import './local-setup';
1334 export default {};
1335 ";
1336 let imports = extract_imports(source, &js_path());
1337 assert_eq!(imports, vec!["polyfill", "./local-setup"]);
1338 }
1339
1340 #[test]
1341 fn extract_imports_mixed_specifiers() {
1342 let source = r"
1343 import defaultExport from 'module-a';
1344 import { named } from 'module-b';
1345 import * as ns from 'module-c';
1346 export default {};
1347 ";
1348 let imports = extract_imports(source, &js_path());
1349 assert_eq!(imports, vec!["module-a", "module-b", "module-c"]);
1350 }
1351
1352 #[test]
1355 fn template_literal_in_string_or_array() {
1356 let source = r"export default { entry: `./src/index.ts` };";
1357 let result = extract_config_string_or_array(source, &ts_path(), &["entry"]);
1358 assert_eq!(result, vec!["./src/index.ts"]);
1359 }
1360
1361 #[test]
1362 fn template_literal_in_config_string() {
1363 let source = r"export default { testDir: `./tests` };";
1364 let val = extract_config_string(source, &js_path(), &["testDir"]);
1365 assert_eq!(val, Some("./tests".to_string()));
1366 }
1367
1368 #[test]
1371 fn nested_string_array_empty_path() {
1372 let source = r#"export default { items: ["a", "b"] };"#;
1373 let result = extract_config_string_array(source, &js_path(), &[]);
1374 assert!(result.is_empty());
1375 }
1376
1377 #[test]
1378 fn nested_string_empty_path() {
1379 let source = r#"export default { key: "val" };"#;
1380 let result = extract_config_string(source, &js_path(), &[]);
1381 assert!(result.is_none());
1382 }
1383
1384 #[test]
1385 fn object_keys_empty_path() {
1386 let source = r"export default { plugins: {} };";
1387 let result = extract_config_object_keys(source, &js_path(), &[]);
1388 assert!(result.is_empty());
1389 }
1390
1391 #[test]
1394 fn no_config_object_returns_empty() {
1395 let source = r"const x = 42;";
1397 let result = extract_config_string(source, &js_path(), &["key"]);
1398 assert!(result.is_none());
1399
1400 let arr = extract_config_string_array(source, &js_path(), &["items"]);
1401 assert!(arr.is_empty());
1402
1403 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1404 assert!(keys.is_empty());
1405 }
1406
1407 #[test]
1410 fn property_with_string_key() {
1411 let source = r#"export default { "string-key": "value" };"#;
1412 let val = extract_config_string(source, &js_path(), &["string-key"]);
1413 assert_eq!(val, Some("value".to_string()));
1414 }
1415
1416 #[test]
1417 fn nested_navigation_through_non_object() {
1418 let source = r#"export default { level1: "not-an-object" };"#;
1420 let val = extract_config_string(source, &js_path(), &["level1", "level2"]);
1421 assert!(val.is_none());
1422 }
1423
1424 #[test]
1427 fn variable_reference_untyped() {
1428 let source = r#"
1429 const config = {
1430 testDir: "./tests"
1431 };
1432 export default config;
1433 "#;
1434 let val = extract_config_string(source, &js_path(), &["testDir"]);
1435 assert_eq!(val, Some("./tests".to_string()));
1436 }
1437
1438 #[test]
1439 fn variable_reference_with_type_annotation() {
1440 let source = r#"
1441 import type { StorybookConfig } from '@storybook/react-vite';
1442 const config: StorybookConfig = {
1443 addons: ["@storybook/addon-a11y", "@storybook/addon-docs"],
1444 framework: "@storybook/react-vite"
1445 };
1446 export default config;
1447 "#;
1448 let addons = extract_config_shallow_strings(source, &ts_path(), "addons");
1449 assert_eq!(
1450 addons,
1451 vec!["@storybook/addon-a11y", "@storybook/addon-docs"]
1452 );
1453
1454 let framework = extract_config_string(source, &ts_path(), &["framework"]);
1455 assert_eq!(framework, Some("@storybook/react-vite".to_string()));
1456 }
1457
1458 #[test]
1459 fn variable_reference_with_define_config() {
1460 let source = r#"
1461 import { defineConfig } from 'vitest/config';
1462 const config = defineConfig({
1463 test: {
1464 include: ["**/*.test.ts"]
1465 }
1466 });
1467 export default config;
1468 "#;
1469 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
1470 assert_eq!(include, vec!["**/*.test.ts"]);
1471 }
1472
1473 #[test]
1476 fn ts_satisfies_direct_export() {
1477 let source = r#"
1478 export default {
1479 testDir: "./tests"
1480 } satisfies PlaywrightTestConfig;
1481 "#;
1482 let val = extract_config_string(source, &ts_path(), &["testDir"]);
1483 assert_eq!(val, Some("./tests".to_string()));
1484 }
1485
1486 #[test]
1487 fn ts_as_direct_export() {
1488 let source = r#"
1489 export default {
1490 testDir: "./tests"
1491 } as const;
1492 "#;
1493 let val = extract_config_string(source, &ts_path(), &["testDir"]);
1494 assert_eq!(val, Some("./tests".to_string()));
1495 }
1496}