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>> {
312 for stmt in &program.body {
313 match stmt {
314 Statement::ExportDefaultDeclaration(decl) => {
316 let expr: Option<&Expression> = match &decl.declaration {
318 ExportDefaultDeclarationKind::ObjectExpression(obj) => {
319 return Some(obj);
320 }
321 ExportDefaultDeclarationKind::CallExpression(_)
322 | ExportDefaultDeclarationKind::ParenthesizedExpression(_) => {
323 decl.declaration.as_expression()
325 }
326 _ => None,
327 };
328 if let Some(expr) = expr {
329 return extract_object_from_expression(expr);
330 }
331 }
332 Statement::ExpressionStatement(expr_stmt) => {
334 if let Expression::AssignmentExpression(assign) = &expr_stmt.expression
335 && is_module_exports_target(&assign.left)
336 {
337 return extract_object_from_expression(&assign.right);
338 }
339 }
340 _ => {}
341 }
342 }
343
344 if program.body.len() == 1
347 && let Statement::ExpressionStatement(expr_stmt) = &program.body[0]
348 {
349 match &expr_stmt.expression {
350 Expression::ObjectExpression(obj) => return Some(obj),
351 Expression::ParenthesizedExpression(paren) => {
352 if let Expression::ObjectExpression(obj) = &paren.expression {
353 return Some(obj);
354 }
355 }
356 _ => {}
357 }
358 }
359
360 None
361}
362
363fn extract_object_from_expression<'a>(
365 expr: &'a Expression<'a>,
366) -> Option<&'a ObjectExpression<'a>> {
367 match expr {
368 Expression::ObjectExpression(obj) => Some(obj),
370 Expression::CallExpression(call) => {
372 for arg in &call.arguments {
374 match arg {
375 Argument::ObjectExpression(obj) => return Some(obj),
376 Argument::ArrowFunctionExpression(arrow) => {
378 if arrow.expression
379 && !arrow.body.statements.is_empty()
380 && let Statement::ExpressionStatement(expr_stmt) =
381 &arrow.body.statements[0]
382 {
383 return extract_object_from_expression(&expr_stmt.expression);
384 }
385 }
386 _ => {}
387 }
388 }
389 None
390 }
391 Expression::ParenthesizedExpression(paren) => {
393 extract_object_from_expression(&paren.expression)
394 }
395 _ => None,
396 }
397}
398
399fn is_module_exports_target(target: &AssignmentTarget) -> bool {
401 if let AssignmentTarget::StaticMemberExpression(member) = target
402 && let Expression::Identifier(obj) = &member.object
403 {
404 return obj.name == "module" && member.property.name == "exports";
405 }
406 false
407}
408
409fn find_property<'a>(obj: &'a ObjectExpression<'a>, key: &str) -> Option<&'a ObjectProperty<'a>> {
411 for prop in &obj.properties {
412 if let ObjectPropertyKind::ObjectProperty(p) = prop
413 && property_key_matches(&p.key, key)
414 {
415 return Some(p);
416 }
417 }
418 None
419}
420
421fn property_key_matches(key: &PropertyKey, name: &str) -> bool {
423 match key {
424 PropertyKey::StaticIdentifier(id) => id.name == name,
425 PropertyKey::StringLiteral(s) => s.value == name,
426 _ => false,
427 }
428}
429
430fn get_object_string_property(obj: &ObjectExpression, key: &str) -> Option<String> {
432 find_property(obj, key).and_then(|p| expression_to_string(&p.value))
433}
434
435fn get_object_string_array_property(obj: &ObjectExpression, key: &str) -> Vec<String> {
437 find_property(obj, key)
438 .map(|p| expression_to_string_array(&p.value))
439 .unwrap_or_default()
440}
441
442fn get_nested_string_array_from_object(
444 obj: &ObjectExpression,
445 path: &[&str],
446) -> Option<Vec<String>> {
447 if path.is_empty() {
448 return None;
449 }
450 if path.len() == 1 {
451 return Some(get_object_string_array_property(obj, path[0]));
452 }
453 let prop = find_property(obj, path[0])?;
455 if let Expression::ObjectExpression(nested) = &prop.value {
456 get_nested_string_array_from_object(nested, &path[1..])
457 } else {
458 None
459 }
460}
461
462fn get_nested_string_from_object(obj: &ObjectExpression, path: &[&str]) -> Option<String> {
464 if path.is_empty() {
465 return None;
466 }
467 if path.len() == 1 {
468 return get_object_string_property(obj, path[0]);
469 }
470 let prop = find_property(obj, path[0])?;
471 if let Expression::ObjectExpression(nested) = &prop.value {
472 get_nested_string_from_object(nested, &path[1..])
473 } else {
474 None
475 }
476}
477
478fn expression_to_string(expr: &Expression) -> Option<String> {
480 match expr {
481 Expression::StringLiteral(s) => Some(s.value.to_string()),
482 Expression::TemplateLiteral(t) if t.expressions.is_empty() => {
483 t.quasis.first().map(|q| q.value.raw.to_string())
485 }
486 _ => None,
487 }
488}
489
490fn expression_to_string_array(expr: &Expression) -> Vec<String> {
492 match expr {
493 Expression::ArrayExpression(arr) => arr
494 .elements
495 .iter()
496 .filter_map(|el| match el {
497 ArrayExpressionElement::SpreadElement(_) => None,
498 _ => el.as_expression().and_then(expression_to_string),
499 })
500 .collect(),
501 _ => vec![],
502 }
503}
504
505fn collect_shallow_string_values(expr: &Expression) -> Vec<String> {
510 let mut values = Vec::new();
511 match expr {
512 Expression::StringLiteral(s) => {
513 values.push(s.value.to_string());
514 }
515 Expression::ArrayExpression(arr) => {
516 for el in &arr.elements {
517 if let Some(inner) = el.as_expression() {
518 match inner {
519 Expression::StringLiteral(s) => {
520 values.push(s.value.to_string());
521 }
522 Expression::ArrayExpression(sub_arr) => {
524 if let Some(first) = sub_arr.elements.first()
525 && let Some(first_expr) = first.as_expression()
526 && let Some(s) = expression_to_string(first_expr)
527 {
528 values.push(s);
529 }
530 }
531 _ => {}
532 }
533 }
534 }
535 }
536 _ => {}
537 }
538 values
539}
540
541fn collect_all_string_values(expr: &Expression, values: &mut Vec<String>) {
543 match expr {
544 Expression::StringLiteral(s) => {
545 values.push(s.value.to_string());
546 }
547 Expression::ArrayExpression(arr) => {
548 for el in &arr.elements {
549 if let Some(expr) = el.as_expression() {
550 collect_all_string_values(expr, values);
551 }
552 }
553 }
554 Expression::ObjectExpression(obj) => {
555 for prop in &obj.properties {
556 if let ObjectPropertyKind::ObjectProperty(p) = prop {
557 collect_all_string_values(&p.value, values);
558 }
559 }
560 }
561 _ => {}
562 }
563}
564
565fn property_key_to_string(key: &PropertyKey) -> Option<String> {
567 match key {
568 PropertyKey::StaticIdentifier(id) => Some(id.name.to_string()),
569 PropertyKey::StringLiteral(s) => Some(s.value.to_string()),
570 _ => None,
571 }
572}
573
574fn get_nested_object_keys(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
576 if path.is_empty() {
577 return None;
578 }
579 let prop = find_property(obj, path[0])?;
580 if path.len() == 1 {
581 if let Expression::ObjectExpression(nested) = &prop.value {
582 let keys = nested
583 .properties
584 .iter()
585 .filter_map(|p| {
586 if let ObjectPropertyKind::ObjectProperty(p) = p {
587 property_key_to_string(&p.key)
588 } else {
589 None
590 }
591 })
592 .collect();
593 return Some(keys);
594 }
595 return None;
596 }
597 if let Expression::ObjectExpression(nested) = &prop.value {
598 get_nested_object_keys(nested, &path[1..])
599 } else {
600 None
601 }
602}
603
604fn get_nested_expression<'a>(
606 obj: &'a ObjectExpression<'a>,
607 path: &[&str],
608) -> Option<&'a Expression<'a>> {
609 if path.is_empty() {
610 return None;
611 }
612 let prop = find_property(obj, path[0])?;
613 if path.len() == 1 {
614 return Some(&prop.value);
615 }
616 if let Expression::ObjectExpression(nested) = &prop.value {
617 get_nested_expression(nested, &path[1..])
618 } else {
619 None
620 }
621}
622
623fn get_nested_string_or_array(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
625 if path.is_empty() {
626 return None;
627 }
628 if path.len() == 1 {
629 let prop = find_property(obj, path[0])?;
630 return Some(expression_to_string_or_array(&prop.value));
631 }
632 let prop = find_property(obj, path[0])?;
633 if let Expression::ObjectExpression(nested) = &prop.value {
634 get_nested_string_or_array(nested, &path[1..])
635 } else {
636 None
637 }
638}
639
640fn expression_to_string_or_array(expr: &Expression) -> Vec<String> {
642 match expr {
643 Expression::StringLiteral(s) => vec![s.value.to_string()],
644 Expression::TemplateLiteral(t) if t.expressions.is_empty() => t
645 .quasis
646 .first()
647 .map(|q| vec![q.value.raw.to_string()])
648 .unwrap_or_default(),
649 Expression::ArrayExpression(arr) => arr
650 .elements
651 .iter()
652 .filter_map(|el| el.as_expression().and_then(expression_to_string))
653 .collect(),
654 Expression::ObjectExpression(obj) => obj
655 .properties
656 .iter()
657 .filter_map(|p| {
658 if let ObjectPropertyKind::ObjectProperty(p) = p {
659 expression_to_string(&p.value)
660 } else {
661 None
662 }
663 })
664 .collect(),
665 _ => vec![],
666 }
667}
668
669fn collect_require_sources(expr: &Expression) -> Vec<String> {
671 let mut sources = Vec::new();
672 match expr {
673 Expression::CallExpression(call) if is_require_call(call) => {
674 if let Some(s) = get_require_source(call) {
675 sources.push(s);
676 }
677 }
678 Expression::ArrayExpression(arr) => {
679 for el in &arr.elements {
680 if let Some(inner) = el.as_expression() {
681 match inner {
682 Expression::CallExpression(call) if is_require_call(call) => {
683 if let Some(s) = get_require_source(call) {
684 sources.push(s);
685 }
686 }
687 Expression::ArrayExpression(sub_arr) => {
689 if let Some(first) = sub_arr.elements.first()
690 && let Some(Expression::CallExpression(call)) =
691 first.as_expression()
692 && is_require_call(call)
693 && let Some(s) = get_require_source(call)
694 {
695 sources.push(s);
696 }
697 }
698 _ => {}
699 }
700 }
701 }
702 }
703 _ => {}
704 }
705 sources
706}
707
708fn is_require_call(call: &CallExpression) -> bool {
710 matches!(&call.callee, Expression::Identifier(id) if id.name == "require")
711}
712
713fn get_require_source(call: &CallExpression) -> Option<String> {
715 call.arguments.first().and_then(|arg| {
716 if let Argument::StringLiteral(s) = arg {
717 Some(s.value.to_string())
718 } else {
719 None
720 }
721 })
722}
723
724#[cfg(test)]
725mod tests {
726 use super::*;
727 use std::path::PathBuf;
728
729 fn js_path() -> PathBuf {
730 PathBuf::from("config.js")
731 }
732
733 fn ts_path() -> PathBuf {
734 PathBuf::from("config.ts")
735 }
736
737 #[test]
738 fn extract_imports_basic() {
739 let source = r"
740 import foo from 'foo-pkg';
741 import { bar } from '@scope/bar';
742 export default {};
743 ";
744 let imports = extract_imports(source, &js_path());
745 assert_eq!(imports, vec!["foo-pkg", "@scope/bar"]);
746 }
747
748 #[test]
749 fn extract_default_export_object_property() {
750 let source = r#"export default { testDir: "./tests" };"#;
751 let val = extract_config_string(source, &js_path(), &["testDir"]);
752 assert_eq!(val, Some("./tests".to_string()));
753 }
754
755 #[test]
756 fn extract_define_config_property() {
757 let source = r#"
758 import { defineConfig } from 'vitest/config';
759 export default defineConfig({
760 test: {
761 include: ["**/*.test.ts", "**/*.spec.ts"],
762 setupFiles: ["./test/setup.ts"]
763 }
764 });
765 "#;
766 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
767 assert_eq!(include, vec!["**/*.test.ts", "**/*.spec.ts"]);
768
769 let setup = extract_config_string_array(source, &ts_path(), &["test", "setupFiles"]);
770 assert_eq!(setup, vec!["./test/setup.ts"]);
771 }
772
773 #[test]
774 fn extract_module_exports_property() {
775 let source = r#"module.exports = { testEnvironment: "jsdom" };"#;
776 let val = extract_config_string(source, &js_path(), &["testEnvironment"]);
777 assert_eq!(val, Some("jsdom".to_string()));
778 }
779
780 #[test]
781 fn extract_nested_string_array() {
782 let source = r#"
783 export default {
784 resolve: {
785 alias: {
786 "@": "./src"
787 }
788 },
789 test: {
790 include: ["src/**/*.test.ts"]
791 }
792 };
793 "#;
794 let include = extract_config_string_array(source, &js_path(), &["test", "include"]);
795 assert_eq!(include, vec!["src/**/*.test.ts"]);
796 }
797
798 #[test]
799 fn extract_addons_array() {
800 let source = r#"
801 export default {
802 addons: [
803 "@storybook/addon-a11y",
804 "@storybook/addon-docs",
805 "@storybook/addon-links"
806 ]
807 };
808 "#;
809 let addons = extract_config_property_strings(source, &ts_path(), "addons");
810 assert_eq!(
811 addons,
812 vec![
813 "@storybook/addon-a11y",
814 "@storybook/addon-docs",
815 "@storybook/addon-links"
816 ]
817 );
818 }
819
820 #[test]
821 fn handle_empty_config() {
822 let source = "";
823 let result = extract_config_string(source, &js_path(), &["key"]);
824 assert_eq!(result, None);
825 }
826
827 #[test]
830 fn object_keys_postcss_plugins() {
831 let source = r"
832 module.exports = {
833 plugins: {
834 autoprefixer: {},
835 tailwindcss: {},
836 'postcss-import': {}
837 }
838 };
839 ";
840 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
841 assert_eq!(keys, vec!["autoprefixer", "tailwindcss", "postcss-import"]);
842 }
843
844 #[test]
845 fn object_keys_nested_path() {
846 let source = r"
847 export default {
848 build: {
849 plugins: {
850 minify: {},
851 compress: {}
852 }
853 }
854 };
855 ";
856 let keys = extract_config_object_keys(source, &js_path(), &["build", "plugins"]);
857 assert_eq!(keys, vec!["minify", "compress"]);
858 }
859
860 #[test]
861 fn object_keys_empty_object() {
862 let source = r"export default { plugins: {} };";
863 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
864 assert!(keys.is_empty());
865 }
866
867 #[test]
868 fn object_keys_non_object_returns_empty() {
869 let source = r#"export default { plugins: ["a", "b"] };"#;
870 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
871 assert!(keys.is_empty());
872 }
873
874 #[test]
877 fn string_or_array_single_string() {
878 let source = r#"export default { entry: "./src/index.js" };"#;
879 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
880 assert_eq!(result, vec!["./src/index.js"]);
881 }
882
883 #[test]
884 fn string_or_array_array() {
885 let source = r#"export default { entry: ["./src/a.js", "./src/b.js"] };"#;
886 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
887 assert_eq!(result, vec!["./src/a.js", "./src/b.js"]);
888 }
889
890 #[test]
891 fn string_or_array_object_values() {
892 let source =
893 r#"export default { entry: { main: "./src/main.js", vendor: "./src/vendor.js" } };"#;
894 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
895 assert_eq!(result, vec!["./src/main.js", "./src/vendor.js"]);
896 }
897
898 #[test]
899 fn string_or_array_nested_path() {
900 let source = r#"
901 export default {
902 build: {
903 rollupOptions: {
904 input: ["./index.html", "./about.html"]
905 }
906 }
907 };
908 "#;
909 let result = extract_config_string_or_array(
910 source,
911 &js_path(),
912 &["build", "rollupOptions", "input"],
913 );
914 assert_eq!(result, vec!["./index.html", "./about.html"]);
915 }
916
917 #[test]
918 fn string_or_array_template_literal() {
919 let source = r"export default { entry: `./src/index.js` };";
920 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
921 assert_eq!(result, vec!["./src/index.js"]);
922 }
923
924 #[test]
927 fn require_strings_array() {
928 let source = r"
929 module.exports = {
930 plugins: [
931 require('autoprefixer'),
932 require('postcss-import')
933 ]
934 };
935 ";
936 let deps = extract_config_require_strings(source, &js_path(), "plugins");
937 assert_eq!(deps, vec!["autoprefixer", "postcss-import"]);
938 }
939
940 #[test]
941 fn require_strings_with_tuples() {
942 let source = r"
943 module.exports = {
944 plugins: [
945 require('autoprefixer'),
946 [require('postcss-preset-env'), { stage: 3 }]
947 ]
948 };
949 ";
950 let deps = extract_config_require_strings(source, &js_path(), "plugins");
951 assert_eq!(deps, vec!["autoprefixer", "postcss-preset-env"]);
952 }
953
954 #[test]
955 fn require_strings_empty_array() {
956 let source = r"module.exports = { plugins: [] };";
957 let deps = extract_config_require_strings(source, &js_path(), "plugins");
958 assert!(deps.is_empty());
959 }
960
961 #[test]
962 fn require_strings_no_require_calls() {
963 let source = r#"module.exports = { plugins: ["a", "b"] };"#;
964 let deps = extract_config_require_strings(source, &js_path(), "plugins");
965 assert!(deps.is_empty());
966 }
967
968 #[test]
971 fn json_wrapped_in_parens_string() {
972 let source = r#"({"extends": "@tsconfig/node18/tsconfig.json"})"#;
973 let val = extract_config_string(source, &js_path(), &["extends"]);
974 assert_eq!(val, Some("@tsconfig/node18/tsconfig.json".to_string()));
975 }
976
977 #[test]
978 fn json_wrapped_in_parens_nested_array() {
979 let source =
980 r#"({"compilerOptions": {"types": ["node", "jest"]}, "include": ["src/**/*"]})"#;
981 let types = extract_config_string_array(source, &js_path(), &["compilerOptions", "types"]);
982 assert_eq!(types, vec!["node", "jest"]);
983
984 let include = extract_config_string_array(source, &js_path(), &["include"]);
985 assert_eq!(include, vec!["src/**/*"]);
986 }
987
988 #[test]
989 fn json_wrapped_in_parens_object_keys() {
990 let source = r#"({"plugins": {"autoprefixer": {}, "tailwindcss": {}}})"#;
991 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
992 assert_eq!(keys, vec!["autoprefixer", "tailwindcss"]);
993 }
994
995 fn json_path() -> PathBuf {
998 PathBuf::from("config.json")
999 }
1000
1001 #[test]
1002 fn json_file_parsed_correctly() {
1003 let source = r#"{"key": "value", "list": ["a", "b"]}"#;
1004 let val = extract_config_string(source, &json_path(), &["key"]);
1005 assert_eq!(val, Some("value".to_string()));
1006
1007 let list = extract_config_string_array(source, &json_path(), &["list"]);
1008 assert_eq!(list, vec!["a", "b"]);
1009 }
1010
1011 #[test]
1012 fn jsonc_file_parsed_correctly() {
1013 let source = r#"{"key": "value"}"#;
1014 let path = PathBuf::from("tsconfig.jsonc");
1015 let val = extract_config_string(source, &path, &["key"]);
1016 assert_eq!(val, Some("value".to_string()));
1017 }
1018
1019 #[test]
1022 fn extract_define_config_arrow_function() {
1023 let source = r#"
1024 import { defineConfig } from 'vite';
1025 export default defineConfig(() => ({
1026 test: {
1027 include: ["**/*.test.ts"]
1028 }
1029 }));
1030 "#;
1031 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
1032 assert_eq!(include, vec!["**/*.test.ts"]);
1033 }
1034
1035 #[test]
1038 fn module_exports_nested_string() {
1039 let source = r#"
1040 module.exports = {
1041 resolve: {
1042 alias: {
1043 "@": "./src"
1044 }
1045 }
1046 };
1047 "#;
1048 let val = extract_config_string(source, &js_path(), &["resolve", "alias", "@"]);
1049 assert_eq!(val, Some("./src".to_string()));
1050 }
1051
1052 #[test]
1055 fn property_strings_nested_objects() {
1056 let source = r#"
1057 export default {
1058 plugins: {
1059 group1: { a: "val-a" },
1060 group2: { b: "val-b" }
1061 }
1062 };
1063 "#;
1064 let values = extract_config_property_strings(source, &js_path(), "plugins");
1065 assert!(values.contains(&"val-a".to_string()));
1066 assert!(values.contains(&"val-b".to_string()));
1067 }
1068
1069 #[test]
1070 fn property_strings_missing_key_returns_empty() {
1071 let source = r#"export default { other: "value" };"#;
1072 let values = extract_config_property_strings(source, &js_path(), "missing");
1073 assert!(values.is_empty());
1074 }
1075
1076 #[test]
1079 fn shallow_strings_tuple_array() {
1080 let source = r#"
1081 module.exports = {
1082 reporters: ["default", ["jest-junit", { outputDirectory: "reports" }]]
1083 };
1084 "#;
1085 let values = extract_config_shallow_strings(source, &js_path(), "reporters");
1086 assert_eq!(values, vec!["default", "jest-junit"]);
1087 assert!(!values.contains(&"reports".to_string()));
1089 }
1090
1091 #[test]
1092 fn shallow_strings_single_string() {
1093 let source = r#"export default { preset: "ts-jest" };"#;
1094 let values = extract_config_shallow_strings(source, &js_path(), "preset");
1095 assert_eq!(values, vec!["ts-jest"]);
1096 }
1097
1098 #[test]
1099 fn shallow_strings_missing_key() {
1100 let source = r#"export default { other: "val" };"#;
1101 let values = extract_config_shallow_strings(source, &js_path(), "missing");
1102 assert!(values.is_empty());
1103 }
1104
1105 #[test]
1108 fn nested_shallow_strings_vitest_reporters() {
1109 let source = r#"
1110 export default {
1111 test: {
1112 reporters: ["default", "vitest-sonar-reporter"]
1113 }
1114 };
1115 "#;
1116 let values =
1117 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
1118 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
1119 }
1120
1121 #[test]
1122 fn nested_shallow_strings_tuple_format() {
1123 let source = r#"
1124 export default {
1125 test: {
1126 reporters: ["default", ["vitest-sonar-reporter", { outputFile: "report.xml" }]]
1127 }
1128 };
1129 "#;
1130 let values =
1131 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
1132 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
1133 }
1134
1135 #[test]
1136 fn nested_shallow_strings_missing_outer() {
1137 let source = r"export default { other: {} };";
1138 let values =
1139 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
1140 assert!(values.is_empty());
1141 }
1142
1143 #[test]
1144 fn nested_shallow_strings_missing_inner() {
1145 let source = r#"export default { test: { include: ["**/*.test.ts"] } };"#;
1146 let values =
1147 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
1148 assert!(values.is_empty());
1149 }
1150
1151 #[test]
1154 fn string_or_array_missing_path() {
1155 let source = r"export default {};";
1156 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1157 assert!(result.is_empty());
1158 }
1159
1160 #[test]
1161 fn string_or_array_non_string_values() {
1162 let source = r"export default { entry: [42, true] };";
1164 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1165 assert!(result.is_empty());
1166 }
1167
1168 #[test]
1171 fn array_nested_extraction() {
1172 let source = r#"
1173 export default defineConfig({
1174 test: {
1175 projects: [
1176 {
1177 test: {
1178 setupFiles: ["./test/setup-a.ts"]
1179 }
1180 },
1181 {
1182 test: {
1183 setupFiles: "./test/setup-b.ts"
1184 }
1185 }
1186 ]
1187 }
1188 });
1189 "#;
1190 let results = extract_config_array_nested_string_or_array(
1191 source,
1192 &ts_path(),
1193 &["test", "projects"],
1194 &["test", "setupFiles"],
1195 );
1196 assert!(results.contains(&"./test/setup-a.ts".to_string()));
1197 assert!(results.contains(&"./test/setup-b.ts".to_string()));
1198 }
1199
1200 #[test]
1201 fn array_nested_empty_when_no_array() {
1202 let source = r#"export default { test: { projects: "not-an-array" } };"#;
1203 let results = extract_config_array_nested_string_or_array(
1204 source,
1205 &js_path(),
1206 &["test", "projects"],
1207 &["test", "setupFiles"],
1208 );
1209 assert!(results.is_empty());
1210 }
1211
1212 #[test]
1215 fn object_nested_extraction() {
1216 let source = r#"{
1217 "projects": {
1218 "app-one": {
1219 "architect": {
1220 "build": {
1221 "options": {
1222 "styles": ["src/styles.css"]
1223 }
1224 }
1225 }
1226 }
1227 }
1228 }"#;
1229 let results = extract_config_object_nested_string_or_array(
1230 source,
1231 &json_path(),
1232 &["projects"],
1233 &["architect", "build", "options", "styles"],
1234 );
1235 assert_eq!(results, vec!["src/styles.css"]);
1236 }
1237
1238 #[test]
1241 fn object_nested_strings_extraction() {
1242 let source = r#"{
1243 "targets": {
1244 "build": {
1245 "executor": "@angular/build:application"
1246 },
1247 "test": {
1248 "executor": "@nx/vite:test"
1249 }
1250 }
1251 }"#;
1252 let results =
1253 extract_config_object_nested_strings(source, &json_path(), &["targets"], &["executor"]);
1254 assert!(results.contains(&"@angular/build:application".to_string()));
1255 assert!(results.contains(&"@nx/vite:test".to_string()));
1256 }
1257
1258 #[test]
1261 fn require_strings_direct_call() {
1262 let source = r"module.exports = { adapter: require('@sveltejs/adapter-node') };";
1263 let deps = extract_config_require_strings(source, &js_path(), "adapter");
1264 assert_eq!(deps, vec!["@sveltejs/adapter-node"]);
1265 }
1266
1267 #[test]
1268 fn require_strings_no_matching_key() {
1269 let source = r"module.exports = { other: require('something') };";
1270 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1271 assert!(deps.is_empty());
1272 }
1273
1274 #[test]
1277 fn extract_imports_no_imports() {
1278 let source = r"export default {};";
1279 let imports = extract_imports(source, &js_path());
1280 assert!(imports.is_empty());
1281 }
1282
1283 #[test]
1284 fn extract_imports_side_effect_import() {
1285 let source = r"
1286 import 'polyfill';
1287 import './local-setup';
1288 export default {};
1289 ";
1290 let imports = extract_imports(source, &js_path());
1291 assert_eq!(imports, vec!["polyfill", "./local-setup"]);
1292 }
1293
1294 #[test]
1295 fn extract_imports_mixed_specifiers() {
1296 let source = r"
1297 import defaultExport from 'module-a';
1298 import { named } from 'module-b';
1299 import * as ns from 'module-c';
1300 export default {};
1301 ";
1302 let imports = extract_imports(source, &js_path());
1303 assert_eq!(imports, vec!["module-a", "module-b", "module-c"]);
1304 }
1305
1306 #[test]
1309 fn template_literal_in_string_or_array() {
1310 let source = r"export default { entry: `./src/index.ts` };";
1311 let result = extract_config_string_or_array(source, &ts_path(), &["entry"]);
1312 assert_eq!(result, vec!["./src/index.ts"]);
1313 }
1314
1315 #[test]
1316 fn template_literal_in_config_string() {
1317 let source = r"export default { testDir: `./tests` };";
1318 let val = extract_config_string(source, &js_path(), &["testDir"]);
1319 assert_eq!(val, Some("./tests".to_string()));
1320 }
1321
1322 #[test]
1325 fn nested_string_array_empty_path() {
1326 let source = r#"export default { items: ["a", "b"] };"#;
1327 let result = extract_config_string_array(source, &js_path(), &[]);
1328 assert!(result.is_empty());
1329 }
1330
1331 #[test]
1332 fn nested_string_empty_path() {
1333 let source = r#"export default { key: "val" };"#;
1334 let result = extract_config_string(source, &js_path(), &[]);
1335 assert!(result.is_none());
1336 }
1337
1338 #[test]
1339 fn object_keys_empty_path() {
1340 let source = r"export default { plugins: {} };";
1341 let result = extract_config_object_keys(source, &js_path(), &[]);
1342 assert!(result.is_empty());
1343 }
1344
1345 #[test]
1348 fn no_config_object_returns_empty() {
1349 let source = r"const x = 42;";
1351 let result = extract_config_string(source, &js_path(), &["key"]);
1352 assert!(result.is_none());
1353
1354 let arr = extract_config_string_array(source, &js_path(), &["items"]);
1355 assert!(arr.is_empty());
1356
1357 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1358 assert!(keys.is_empty());
1359 }
1360
1361 #[test]
1364 fn property_with_string_key() {
1365 let source = r#"export default { "string-key": "value" };"#;
1366 let val = extract_config_string(source, &js_path(), &["string-key"]);
1367 assert_eq!(val, Some("value".to_string()));
1368 }
1369
1370 #[test]
1371 fn nested_navigation_through_non_object() {
1372 let source = r#"export default { level1: "not-an-object" };"#;
1374 let val = extract_config_string(source, &js_path(), &["level1", "level2"]);
1375 assert!(val.is_none());
1376 }
1377}