1use std::path::{Path, PathBuf};
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]
45pub fn extract_imports_and_requires(source: &str, path: &Path) -> Vec<String> {
46 extract_from_source(source, path, |program| {
47 let mut sources = Vec::new();
48 for stmt in &program.body {
49 match stmt {
50 Statement::ImportDeclaration(decl) => {
51 sources.push(decl.source.value.to_string());
52 }
53 Statement::ExpressionStatement(expr) => {
54 if let Expression::CallExpression(call) = &expr.expression
55 && is_require_call(call)
56 && let Some(s) = get_require_source(call)
57 {
58 sources.push(s);
59 }
60 }
61 _ => {}
62 }
63 }
64 Some(sources)
65 })
66 .unwrap_or_default()
67}
68
69#[must_use]
71pub fn extract_config_string_array(source: &str, path: &Path, prop_path: &[&str]) -> Vec<String> {
72 extract_from_source(source, path, |program| {
73 let obj = find_config_object(program)?;
74 get_nested_string_array_from_object(obj, prop_path)
75 })
76 .unwrap_or_default()
77}
78
79#[must_use]
81pub fn extract_config_string(source: &str, path: &Path, prop_path: &[&str]) -> Option<String> {
82 extract_from_source(source, path, |program| {
83 let obj = find_config_object(program)?;
84 get_nested_string_from_object(obj, prop_path)
85 })
86}
87
88#[must_use]
95pub fn extract_config_property_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
96 extract_from_source(source, path, |program| {
97 let obj = find_config_object(program)?;
98 let mut values = Vec::new();
99 if let Some(prop) = find_property(obj, key) {
100 collect_all_string_values(&prop.value, &mut values);
101 }
102 Some(values)
103 })
104 .unwrap_or_default()
105}
106
107#[must_use]
114pub fn extract_config_shallow_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
115 extract_from_source(source, path, |program| {
116 let obj = find_config_object(program)?;
117 let prop = find_property(obj, key)?;
118 Some(collect_shallow_string_values(&prop.value))
119 })
120 .unwrap_or_default()
121}
122
123#[must_use]
129pub fn extract_config_nested_shallow_strings(
130 source: &str,
131 path: &Path,
132 outer_path: &[&str],
133 key: &str,
134) -> Vec<String> {
135 extract_from_source(source, path, |program| {
136 let obj = find_config_object(program)?;
137 let nested = get_nested_expression(obj, outer_path)?;
138 if let Expression::ObjectExpression(nested_obj) = nested {
139 let prop = find_property(nested_obj, key)?;
140 Some(collect_shallow_string_values(&prop.value))
141 } else {
142 None
143 }
144 })
145 .unwrap_or_default()
146}
147
148pub fn find_config_object_pub<'a>(program: &'a Program) -> Option<&'a ObjectExpression<'a>> {
150 find_config_object(program)
151}
152
153pub(crate) fn property_expr<'a>(
155 obj: &'a ObjectExpression<'a>,
156 key: &str,
157) -> Option<&'a Expression<'a>> {
158 find_property(obj, key).map(|prop| &prop.value)
159}
160
161pub(crate) fn property_object<'a>(
163 obj: &'a ObjectExpression<'a>,
164 key: &str,
165) -> Option<&'a ObjectExpression<'a>> {
166 property_expr(obj, key).and_then(object_expression)
167}
168
169pub(crate) fn property_string(obj: &ObjectExpression<'_>, key: &str) -> Option<String> {
171 property_expr(obj, key).and_then(expression_to_string)
172}
173
174pub(crate) fn object_expression<'a>(expr: &'a Expression<'a>) -> Option<&'a ObjectExpression<'a>> {
176 match expr {
177 Expression::ObjectExpression(obj) => Some(obj),
178 Expression::ParenthesizedExpression(paren) => object_expression(&paren.expression),
179 Expression::TSSatisfiesExpression(ts_sat) => object_expression(&ts_sat.expression),
180 Expression::TSAsExpression(ts_as) => object_expression(&ts_as.expression),
181 _ => None,
182 }
183}
184
185pub(crate) fn array_expression<'a>(expr: &'a Expression<'a>) -> Option<&'a ArrayExpression<'a>> {
187 match expr {
188 Expression::ArrayExpression(arr) => Some(arr),
189 Expression::ParenthesizedExpression(paren) => array_expression(&paren.expression),
190 Expression::TSSatisfiesExpression(ts_sat) => array_expression(&ts_sat.expression),
191 Expression::TSAsExpression(ts_as) => array_expression(&ts_as.expression),
192 _ => None,
193 }
194}
195
196pub(crate) fn expression_to_path_values(expr: &Expression<'_>) -> Vec<String> {
198 match expr {
199 Expression::ArrayExpression(arr) => arr
200 .elements
201 .iter()
202 .filter_map(|element| element.as_expression().and_then(expression_to_path_string))
203 .collect(),
204 _ => expression_to_path_string(expr).into_iter().collect(),
205 }
206}
207
208pub(crate) fn is_disabled_expression(expr: &Expression<'_>) -> bool {
210 matches!(expr, Expression::BooleanLiteral(boolean) if !boolean.value)
211 || matches!(expr, Expression::NullLiteral(_))
212}
213
214#[must_use]
219pub fn extract_config_object_keys(source: &str, path: &Path, prop_path: &[&str]) -> Vec<String> {
220 extract_from_source(source, path, |program| {
221 let obj = find_config_object(program)?;
222 get_nested_object_keys(obj, prop_path)
223 })
224 .unwrap_or_default()
225}
226
227#[must_use]
234pub fn extract_config_string_or_array(
235 source: &str,
236 path: &Path,
237 prop_path: &[&str],
238) -> Vec<String> {
239 extract_from_source(source, path, |program| {
240 let obj = find_config_object(program)?;
241 get_nested_string_or_array(obj, prop_path)
242 })
243 .unwrap_or_default()
244}
245
246#[must_use]
253pub fn extract_config_array_nested_string_or_array(
254 source: &str,
255 path: &Path,
256 array_path: &[&str],
257 inner_path: &[&str],
258) -> Vec<String> {
259 extract_from_source(source, path, |program| {
260 let obj = find_config_object(program)?;
261 let array_expr = get_nested_expression(obj, array_path)?;
262 let Expression::ArrayExpression(arr) = array_expr else {
263 return None;
264 };
265 let mut results = Vec::new();
266 for element in &arr.elements {
267 if let Some(Expression::ObjectExpression(element_obj)) = element.as_expression()
268 && let Some(values) = get_nested_string_or_array(element_obj, inner_path)
269 {
270 results.extend(values);
271 }
272 }
273 if results.is_empty() {
274 None
275 } else {
276 Some(results)
277 }
278 })
279 .unwrap_or_default()
280}
281
282#[must_use]
289pub fn extract_config_object_nested_string_or_array(
290 source: &str,
291 path: &Path,
292 object_path: &[&str],
293 inner_path: &[&str],
294) -> Vec<String> {
295 extract_config_object_nested(source, path, object_path, |value_obj| {
296 get_nested_string_or_array(value_obj, inner_path)
297 })
298}
299
300#[must_use]
305pub fn extract_config_object_nested_strings(
306 source: &str,
307 path: &Path,
308 object_path: &[&str],
309 inner_path: &[&str],
310) -> Vec<String> {
311 extract_config_object_nested(source, path, object_path, |value_obj| {
312 get_nested_string_from_object(value_obj, inner_path).map(|s| vec![s])
313 })
314}
315
316fn extract_config_object_nested(
321 source: &str,
322 path: &Path,
323 object_path: &[&str],
324 extract_fn: impl Fn(&ObjectExpression<'_>) -> Option<Vec<String>>,
325) -> Vec<String> {
326 extract_from_source(source, path, |program| {
327 let obj = find_config_object(program)?;
328 let obj_expr = get_nested_expression(obj, object_path)?;
329 let Expression::ObjectExpression(target_obj) = obj_expr else {
330 return None;
331 };
332 let mut results = Vec::new();
333 for prop in &target_obj.properties {
334 if let ObjectPropertyKind::ObjectProperty(p) = prop
335 && let Expression::ObjectExpression(value_obj) = &p.value
336 && let Some(values) = extract_fn(value_obj)
337 {
338 results.extend(values);
339 }
340 }
341 if results.is_empty() {
342 None
343 } else {
344 Some(results)
345 }
346 })
347 .unwrap_or_default()
348}
349
350#[must_use]
356pub fn extract_config_require_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
357 extract_from_source(source, path, |program| {
358 let obj = find_config_object(program)?;
359 let prop = find_property(obj, key)?;
360 Some(collect_require_sources(&prop.value))
361 })
362 .unwrap_or_default()
363}
364
365#[must_use]
372pub fn extract_config_aliases(
373 source: &str,
374 path: &Path,
375 prop_path: &[&str],
376) -> Vec<(String, String)> {
377 extract_from_source(source, path, |program| {
378 let obj = find_config_object(program)?;
379 let expr = get_nested_expression(obj, prop_path)?;
380 let aliases = expression_to_alias_pairs(expr);
381 (!aliases.is_empty()).then_some(aliases)
382 })
383 .unwrap_or_default()
384}
385
386#[must_use]
392pub fn extract_config_array_object_strings(
393 source: &str,
394 path: &Path,
395 array_path: &[&str],
396 key: &str,
397) -> Vec<String> {
398 extract_from_source(source, path, |program| {
399 let obj = find_config_object(program)?;
400 let array_expr = get_nested_expression(obj, array_path)?;
401 let Expression::ArrayExpression(arr) = array_expr else {
402 return None;
403 };
404
405 let mut results = Vec::new();
406 for element in &arr.elements {
407 let Some(expr) = element.as_expression() else {
408 continue;
409 };
410 match expr {
411 Expression::ObjectExpression(item) => {
412 if let Some(prop) = find_property(item, key)
413 && let Some(value) = expression_to_path_string(&prop.value)
414 {
415 results.push(value);
416 }
417 }
418 _ => {
419 if let Some(value) = expression_to_path_string(expr) {
420 results.push(value);
421 }
422 }
423 }
424 }
425
426 (!results.is_empty()).then_some(results)
427 })
428 .unwrap_or_default()
429}
430
431#[must_use]
438pub fn extract_config_plugin_option_string(
439 source: &str,
440 path: &Path,
441 plugins_path: &[&str],
442 plugin_name: &str,
443 option_key: &str,
444) -> Option<String> {
445 extract_from_source(source, path, |program| {
446 let obj = find_config_object(program)?;
447 let plugins_expr = get_nested_expression(obj, plugins_path)?;
448 let Expression::ArrayExpression(plugins) = plugins_expr else {
449 return None;
450 };
451
452 for entry in &plugins.elements {
453 let Some(Expression::ArrayExpression(tuple)) = entry.as_expression() else {
454 continue;
455 };
456 let Some(plugin_expr) = tuple
457 .elements
458 .first()
459 .and_then(ArrayExpressionElement::as_expression)
460 else {
461 continue;
462 };
463 if expression_to_string(plugin_expr).as_deref() != Some(plugin_name) {
464 continue;
465 }
466
467 let Some(options_expr) = tuple
468 .elements
469 .get(1)
470 .and_then(ArrayExpressionElement::as_expression)
471 else {
472 continue;
473 };
474 let Expression::ObjectExpression(options_obj) = options_expr else {
475 continue;
476 };
477 let option = find_property(options_obj, option_key)?;
478 return expression_to_path_string(&option.value);
479 }
480
481 None
482 })
483}
484
485#[must_use]
487pub fn extract_config_plugin_option_string_from_paths(
488 source: &str,
489 path: &Path,
490 plugin_paths: &[&[&str]],
491 plugin_name: &str,
492 option_key: &str,
493) -> Option<String> {
494 plugin_paths.iter().find_map(|plugins_path| {
495 extract_config_plugin_option_string(source, path, plugins_path, plugin_name, option_key)
496 })
497}
498
499#[must_use]
504pub fn normalize_config_path(raw: &str, config_path: &Path, root: &Path) -> Option<String> {
505 if raw.is_empty() {
506 return None;
507 }
508
509 let candidate = if let Some(stripped) = raw.strip_prefix('/') {
510 lexical_normalize(&root.join(stripped))
511 } else {
512 let path = Path::new(raw);
513 if path.is_absolute() {
514 lexical_normalize(path)
515 } else {
516 let base = config_path.parent().unwrap_or(root);
517 lexical_normalize(&base.join(path))
518 }
519 };
520
521 let relative = candidate.strip_prefix(root).ok()?;
522 let normalized = relative.to_string_lossy().replace('\\', "/");
523 (!normalized.is_empty()).then_some(normalized)
524}
525
526fn extract_from_source<T>(
535 source: &str,
536 path: &Path,
537 extractor: impl FnOnce(&Program) -> Option<T>,
538) -> Option<T> {
539 let source_type = SourceType::from_path(path).unwrap_or_default();
540 let alloc = Allocator::default();
541
542 let is_json = path
545 .extension()
546 .is_some_and(|ext| ext == "json" || ext == "jsonc");
547 if is_json {
548 let wrapped = format!("({source})");
549 let parsed = Parser::new(&alloc, &wrapped, SourceType::mjs()).parse();
550 return extractor(&parsed.program);
551 }
552
553 let parsed = Parser::new(&alloc, source, source_type).parse();
554 extractor(&parsed.program)
555}
556
557fn find_config_object<'a>(program: &'a Program) -> Option<&'a ObjectExpression<'a>> {
569 for stmt in &program.body {
570 match stmt {
571 Statement::ExportDefaultDeclaration(decl) => {
573 let expr: Option<&Expression> = match &decl.declaration {
575 ExportDefaultDeclarationKind::ObjectExpression(obj) => {
576 return Some(obj);
577 }
578 ExportDefaultDeclarationKind::FunctionDeclaration(func) => {
579 return extract_object_from_function(func);
580 }
581 _ => decl.declaration.as_expression(),
582 };
583 if let Some(expr) = expr {
584 if let Some(obj) = extract_object_from_expression(expr) {
586 return Some(obj);
587 }
588 if let Some(name) = unwrap_to_identifier_name(expr) {
591 return find_variable_init_object(program, name);
592 }
593 }
594 }
595 Statement::ExpressionStatement(expr_stmt) => {
597 if let Expression::AssignmentExpression(assign) = &expr_stmt.expression
598 && is_module_exports_target(&assign.left)
599 {
600 return extract_object_from_expression(&assign.right);
601 }
602 }
603 _ => {}
604 }
605 }
606
607 if program.body.len() == 1
610 && let Statement::ExpressionStatement(expr_stmt) = &program.body[0]
611 {
612 match &expr_stmt.expression {
613 Expression::ObjectExpression(obj) => return Some(obj),
614 Expression::ParenthesizedExpression(paren) => {
615 if let Expression::ObjectExpression(obj) = &paren.expression {
616 return Some(obj);
617 }
618 }
619 _ => {}
620 }
621 }
622
623 None
624}
625
626fn extract_object_from_expression<'a>(
628 expr: &'a Expression<'a>,
629) -> Option<&'a ObjectExpression<'a>> {
630 match expr {
631 Expression::ObjectExpression(obj) => Some(obj),
633 Expression::CallExpression(call) => {
635 for arg in &call.arguments {
637 match arg {
638 Argument::ObjectExpression(obj) => return Some(obj),
639 Argument::ArrowFunctionExpression(arrow) => {
641 if arrow.expression
642 && !arrow.body.statements.is_empty()
643 && let Statement::ExpressionStatement(expr_stmt) =
644 &arrow.body.statements[0]
645 {
646 return extract_object_from_expression(&expr_stmt.expression);
647 }
648 }
649 _ => {}
650 }
651 }
652 None
653 }
654 Expression::ParenthesizedExpression(paren) => {
656 extract_object_from_expression(&paren.expression)
657 }
658 Expression::TSSatisfiesExpression(ts_sat) => {
660 extract_object_from_expression(&ts_sat.expression)
661 }
662 Expression::TSAsExpression(ts_as) => extract_object_from_expression(&ts_as.expression),
663 Expression::ArrowFunctionExpression(arrow) => extract_object_from_arrow_function(arrow),
664 Expression::FunctionExpression(func) => extract_object_from_function(func),
665 _ => None,
666 }
667}
668
669fn extract_object_from_arrow_function<'a>(
670 arrow: &'a ArrowFunctionExpression<'a>,
671) -> Option<&'a ObjectExpression<'a>> {
672 if arrow.expression {
673 arrow.body.statements.first().and_then(|stmt| {
674 if let Statement::ExpressionStatement(expr_stmt) = stmt {
675 extract_object_from_expression(&expr_stmt.expression)
676 } else {
677 None
678 }
679 })
680 } else {
681 extract_object_from_function_body(&arrow.body)
682 }
683}
684
685fn extract_object_from_function<'a>(func: &'a Function<'a>) -> Option<&'a ObjectExpression<'a>> {
686 func.body
687 .as_ref()
688 .and_then(|body| extract_object_from_function_body(body))
689}
690
691fn extract_object_from_function_body<'a>(
692 body: &'a FunctionBody<'a>,
693) -> Option<&'a ObjectExpression<'a>> {
694 for stmt in &body.statements {
695 if let Statement::ReturnStatement(ret) = stmt
696 && let Some(argument) = &ret.argument
697 && let Some(obj) = extract_object_from_expression(argument)
698 {
699 return Some(obj);
700 }
701 }
702 None
703}
704
705fn is_module_exports_target(target: &AssignmentTarget) -> bool {
707 if let AssignmentTarget::StaticMemberExpression(member) = target
708 && let Expression::Identifier(obj) = &member.object
709 {
710 return obj.name == "module" && member.property.name == "exports";
711 }
712 false
713}
714
715fn unwrap_to_identifier_name<'a>(expr: &'a Expression<'a>) -> Option<&'a str> {
719 match expr {
720 Expression::Identifier(id) => Some(&id.name),
721 Expression::TSSatisfiesExpression(ts_sat) => unwrap_to_identifier_name(&ts_sat.expression),
722 Expression::TSAsExpression(ts_as) => unwrap_to_identifier_name(&ts_as.expression),
723 _ => None,
724 }
725}
726
727fn find_variable_init_object<'a>(
732 program: &'a Program,
733 name: &str,
734) -> Option<&'a ObjectExpression<'a>> {
735 for stmt in &program.body {
736 if let Statement::VariableDeclaration(decl) = stmt {
737 for declarator in &decl.declarations {
738 if let BindingPattern::BindingIdentifier(id) = &declarator.id
739 && id.name == name
740 && let Some(init) = &declarator.init
741 {
742 return extract_object_from_expression(init);
743 }
744 }
745 }
746 }
747 None
748}
749
750pub(crate) fn find_property<'a>(
752 obj: &'a ObjectExpression<'a>,
753 key: &str,
754) -> Option<&'a ObjectProperty<'a>> {
755 for prop in &obj.properties {
756 if let ObjectPropertyKind::ObjectProperty(p) = prop
757 && property_key_matches(&p.key, key)
758 {
759 return Some(p);
760 }
761 }
762 None
763}
764
765pub(crate) fn property_key_matches(key: &PropertyKey, name: &str) -> bool {
767 match key {
768 PropertyKey::StaticIdentifier(id) => id.name == name,
769 PropertyKey::StringLiteral(s) => s.value == name,
770 _ => false,
771 }
772}
773
774fn get_object_string_property(obj: &ObjectExpression, key: &str) -> Option<String> {
776 find_property(obj, key).and_then(|p| expression_to_string(&p.value))
777}
778
779fn get_object_string_array_property(obj: &ObjectExpression, key: &str) -> Vec<String> {
781 find_property(obj, key)
782 .map(|p| expression_to_string_array(&p.value))
783 .unwrap_or_default()
784}
785
786fn get_nested_string_array_from_object(
788 obj: &ObjectExpression,
789 path: &[&str],
790) -> Option<Vec<String>> {
791 if path.is_empty() {
792 return None;
793 }
794 if path.len() == 1 {
795 return Some(get_object_string_array_property(obj, path[0]));
796 }
797 let prop = find_property(obj, path[0])?;
799 if let Expression::ObjectExpression(nested) = &prop.value {
800 get_nested_string_array_from_object(nested, &path[1..])
801 } else {
802 None
803 }
804}
805
806fn get_nested_string_from_object(obj: &ObjectExpression, path: &[&str]) -> Option<String> {
808 if path.is_empty() {
809 return None;
810 }
811 if path.len() == 1 {
812 return get_object_string_property(obj, path[0]);
813 }
814 let prop = find_property(obj, path[0])?;
815 if let Expression::ObjectExpression(nested) = &prop.value {
816 get_nested_string_from_object(nested, &path[1..])
817 } else {
818 None
819 }
820}
821
822pub(crate) fn expression_to_string(expr: &Expression) -> Option<String> {
824 match expr {
825 Expression::StringLiteral(s) => Some(s.value.to_string()),
826 Expression::TemplateLiteral(t) if t.expressions.is_empty() => {
827 t.quasis.first().map(|q| q.value.raw.to_string())
829 }
830 _ => None,
831 }
832}
833
834pub(crate) fn expression_to_path_string(expr: &Expression) -> Option<String> {
836 match expr {
837 Expression::ParenthesizedExpression(paren) => expression_to_path_string(&paren.expression),
838 Expression::TSAsExpression(ts_as) => expression_to_path_string(&ts_as.expression),
839 Expression::TSSatisfiesExpression(ts_sat) => expression_to_path_string(&ts_sat.expression),
840 Expression::CallExpression(call) => call_expression_to_path_string(call),
841 Expression::NewExpression(new_expr) => new_expression_to_path_string(new_expr),
842 _ => expression_to_string(expr),
843 }
844}
845
846fn call_expression_to_path_string(call: &CallExpression) -> Option<String> {
847 if matches!(&call.callee, Expression::Identifier(id) if id.name == "fileURLToPath") {
848 return call
849 .arguments
850 .first()
851 .and_then(Argument::as_expression)
852 .and_then(expression_to_path_string);
853 }
854
855 let callee_name = match &call.callee {
856 Expression::Identifier(id) => Some(id.name.as_str()),
857 Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
858 _ => None,
859 }?;
860
861 if !matches!(callee_name, "resolve" | "join") {
862 return None;
863 }
864
865 let mut segments = Vec::new();
866 for (index, arg) in call.arguments.iter().enumerate() {
867 let expr = arg.as_expression()?;
868
869 if matches!(expr, Expression::Identifier(id) if id.name == "__dirname") {
870 if index == 0 {
871 continue;
872 }
873 return None;
874 }
875
876 segments.push(expression_to_string(expr)?);
877 }
878
879 (!segments.is_empty()).then(|| join_path_segments(&segments))
880}
881
882fn new_expression_to_path_string(new_expr: &NewExpression) -> Option<String> {
883 if !matches!(&new_expr.callee, Expression::Identifier(id) if id.name == "URL") {
884 return None;
885 }
886
887 let source = new_expr
888 .arguments
889 .first()
890 .and_then(Argument::as_expression)
891 .and_then(expression_to_string)?;
892
893 let base = new_expr
894 .arguments
895 .get(1)
896 .and_then(Argument::as_expression)?;
897 is_import_meta_url_expression(base).then_some(source)
898}
899
900fn is_import_meta_url_expression(expr: &Expression) -> bool {
901 if let Expression::StaticMemberExpression(member) = expr {
902 member.property.name == "url" && matches!(member.object, Expression::MetaProperty(_))
903 } else {
904 false
905 }
906}
907
908fn join_path_segments(segments: &[String]) -> String {
909 let mut joined = PathBuf::new();
910 for segment in segments {
911 joined.push(segment);
912 }
913 joined.to_string_lossy().replace('\\', "/")
914}
915
916fn expression_to_alias_pairs(expr: &Expression) -> Vec<(String, String)> {
917 match expr {
918 Expression::ObjectExpression(obj) => obj
919 .properties
920 .iter()
921 .filter_map(|prop| {
922 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
923 return None;
924 };
925 let find = property_key_to_string(&prop.key)?;
926 let replacement = expression_to_path_string(&prop.value)?;
927 Some((find, replacement))
928 })
929 .collect(),
930 Expression::ArrayExpression(arr) => arr
931 .elements
932 .iter()
933 .filter_map(|element| {
934 let Expression::ObjectExpression(obj) = element.as_expression()? else {
935 return None;
936 };
937 let find = find_property(obj, "find")
938 .and_then(|prop| expression_to_string(&prop.value))?;
939 let replacement = find_property(obj, "replacement")
940 .and_then(|prop| expression_to_path_string(&prop.value))?;
941 Some((find, replacement))
942 })
943 .collect(),
944 _ => Vec::new(),
945 }
946}
947
948fn lexical_normalize(path: &Path) -> PathBuf {
949 let mut normalized = PathBuf::new();
950
951 for component in path.components() {
952 match component {
953 std::path::Component::CurDir => {}
954 std::path::Component::ParentDir => {
955 normalized.pop();
956 }
957 _ => normalized.push(component.as_os_str()),
958 }
959 }
960
961 normalized
962}
963
964fn expression_to_string_array(expr: &Expression) -> Vec<String> {
966 match expr {
967 Expression::ArrayExpression(arr) => arr
968 .elements
969 .iter()
970 .filter_map(|el| match el {
971 ArrayExpressionElement::SpreadElement(_) => None,
972 _ => el.as_expression().and_then(expression_to_string),
973 })
974 .collect(),
975 _ => vec![],
976 }
977}
978
979fn collect_shallow_string_values(expr: &Expression) -> Vec<String> {
984 let mut values = Vec::new();
985 match expr {
986 Expression::StringLiteral(s) => {
987 values.push(s.value.to_string());
988 }
989 Expression::ArrayExpression(arr) => {
990 for el in &arr.elements {
991 if let Some(inner) = el.as_expression() {
992 match inner {
993 Expression::StringLiteral(s) => {
994 values.push(s.value.to_string());
995 }
996 Expression::ArrayExpression(sub_arr) => {
998 if let Some(first) = sub_arr.elements.first()
999 && let Some(first_expr) = first.as_expression()
1000 && let Some(s) = expression_to_string(first_expr)
1001 {
1002 values.push(s);
1003 }
1004 }
1005 _ => {}
1006 }
1007 }
1008 }
1009 }
1010 Expression::ObjectExpression(obj) => {
1012 for prop in &obj.properties {
1013 if let ObjectPropertyKind::ObjectProperty(p) = prop {
1014 match &p.value {
1015 Expression::StringLiteral(s) => {
1016 values.push(s.value.to_string());
1017 }
1018 Expression::ArrayExpression(sub_arr) => {
1020 if let Some(first) = sub_arr.elements.first()
1021 && let Some(first_expr) = first.as_expression()
1022 && let Some(s) = expression_to_string(first_expr)
1023 {
1024 values.push(s);
1025 }
1026 }
1027 _ => {}
1028 }
1029 }
1030 }
1031 }
1032 _ => {}
1033 }
1034 values
1035}
1036
1037fn collect_all_string_values(expr: &Expression, values: &mut Vec<String>) {
1039 match expr {
1040 Expression::StringLiteral(s) => {
1041 values.push(s.value.to_string());
1042 }
1043 Expression::ArrayExpression(arr) => {
1044 for el in &arr.elements {
1045 if let Some(expr) = el.as_expression() {
1046 collect_all_string_values(expr, values);
1047 }
1048 }
1049 }
1050 Expression::ObjectExpression(obj) => {
1051 for prop in &obj.properties {
1052 if let ObjectPropertyKind::ObjectProperty(p) = prop {
1053 collect_all_string_values(&p.value, values);
1054 }
1055 }
1056 }
1057 _ => {}
1058 }
1059}
1060
1061fn property_key_to_string(key: &PropertyKey) -> Option<String> {
1063 match key {
1064 PropertyKey::StaticIdentifier(id) => Some(id.name.to_string()),
1065 PropertyKey::StringLiteral(s) => Some(s.value.to_string()),
1066 _ => None,
1067 }
1068}
1069
1070fn get_nested_object_keys(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
1072 if path.is_empty() {
1073 return None;
1074 }
1075 let prop = find_property(obj, path[0])?;
1076 if path.len() == 1 {
1077 if let Expression::ObjectExpression(nested) = &prop.value {
1078 let keys = nested
1079 .properties
1080 .iter()
1081 .filter_map(|p| {
1082 if let ObjectPropertyKind::ObjectProperty(p) = p {
1083 property_key_to_string(&p.key)
1084 } else {
1085 None
1086 }
1087 })
1088 .collect();
1089 return Some(keys);
1090 }
1091 return None;
1092 }
1093 if let Expression::ObjectExpression(nested) = &prop.value {
1094 get_nested_object_keys(nested, &path[1..])
1095 } else {
1096 None
1097 }
1098}
1099
1100fn get_nested_expression<'a>(
1102 obj: &'a ObjectExpression<'a>,
1103 path: &[&str],
1104) -> Option<&'a Expression<'a>> {
1105 if path.is_empty() {
1106 return None;
1107 }
1108 let prop = find_property(obj, path[0])?;
1109 if path.len() == 1 {
1110 return Some(&prop.value);
1111 }
1112 if let Expression::ObjectExpression(nested) = &prop.value {
1113 get_nested_expression(nested, &path[1..])
1114 } else {
1115 None
1116 }
1117}
1118
1119fn get_nested_string_or_array(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
1121 if path.is_empty() {
1122 return None;
1123 }
1124 if path.len() == 1 {
1125 let prop = find_property(obj, path[0])?;
1126 return Some(expression_to_string_or_array(&prop.value));
1127 }
1128 let prop = find_property(obj, path[0])?;
1129 if let Expression::ObjectExpression(nested) = &prop.value {
1130 get_nested_string_or_array(nested, &path[1..])
1131 } else {
1132 None
1133 }
1134}
1135
1136fn expression_to_string_or_array(expr: &Expression) -> Vec<String> {
1138 match expr {
1139 Expression::StringLiteral(s) => vec![s.value.to_string()],
1140 Expression::TemplateLiteral(t) if t.expressions.is_empty() => t
1141 .quasis
1142 .first()
1143 .map(|q| vec![q.value.raw.to_string()])
1144 .unwrap_or_default(),
1145 Expression::ArrayExpression(arr) => arr
1146 .elements
1147 .iter()
1148 .filter_map(|el| el.as_expression().and_then(expression_to_string))
1149 .collect(),
1150 Expression::ObjectExpression(obj) => obj
1151 .properties
1152 .iter()
1153 .filter_map(|p| {
1154 if let ObjectPropertyKind::ObjectProperty(p) = p {
1155 expression_to_string(&p.value)
1156 } else {
1157 None
1158 }
1159 })
1160 .collect(),
1161 _ => vec![],
1162 }
1163}
1164
1165fn collect_require_sources(expr: &Expression) -> Vec<String> {
1167 let mut sources = Vec::new();
1168 match expr {
1169 Expression::CallExpression(call) if is_require_call(call) => {
1170 if let Some(s) = get_require_source(call) {
1171 sources.push(s);
1172 }
1173 }
1174 Expression::ArrayExpression(arr) => {
1175 for el in &arr.elements {
1176 if let Some(inner) = el.as_expression() {
1177 match inner {
1178 Expression::CallExpression(call) if is_require_call(call) => {
1179 if let Some(s) = get_require_source(call) {
1180 sources.push(s);
1181 }
1182 }
1183 Expression::ArrayExpression(sub_arr) => {
1185 if let Some(first) = sub_arr.elements.first()
1186 && let Some(Expression::CallExpression(call)) =
1187 first.as_expression()
1188 && is_require_call(call)
1189 && let Some(s) = get_require_source(call)
1190 {
1191 sources.push(s);
1192 }
1193 }
1194 _ => {}
1195 }
1196 }
1197 }
1198 }
1199 _ => {}
1200 }
1201 sources
1202}
1203
1204fn is_require_call(call: &CallExpression) -> bool {
1206 matches!(&call.callee, Expression::Identifier(id) if id.name == "require")
1207}
1208
1209fn get_require_source(call: &CallExpression) -> Option<String> {
1211 call.arguments.first().and_then(|arg| {
1212 if let Argument::StringLiteral(s) = arg {
1213 Some(s.value.to_string())
1214 } else {
1215 None
1216 }
1217 })
1218}
1219
1220#[cfg(test)]
1221mod tests {
1222 use super::*;
1223 use std::path::PathBuf;
1224
1225 fn js_path() -> PathBuf {
1226 PathBuf::from("config.js")
1227 }
1228
1229 fn ts_path() -> PathBuf {
1230 PathBuf::from("config.ts")
1231 }
1232
1233 #[test]
1234 fn extract_imports_basic() {
1235 let source = r"
1236 import foo from 'foo-pkg';
1237 import { bar } from '@scope/bar';
1238 export default {};
1239 ";
1240 let imports = extract_imports(source, &js_path());
1241 assert_eq!(imports, vec!["foo-pkg", "@scope/bar"]);
1242 }
1243
1244 #[test]
1245 fn extract_default_export_object_property() {
1246 let source = r#"export default { testDir: "./tests" };"#;
1247 let val = extract_config_string(source, &js_path(), &["testDir"]);
1248 assert_eq!(val, Some("./tests".to_string()));
1249 }
1250
1251 #[test]
1252 fn extract_define_config_property() {
1253 let source = r#"
1254 import { defineConfig } from 'vitest/config';
1255 export default defineConfig({
1256 test: {
1257 include: ["**/*.test.ts", "**/*.spec.ts"],
1258 setupFiles: ["./test/setup.ts"]
1259 }
1260 });
1261 "#;
1262 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
1263 assert_eq!(include, vec!["**/*.test.ts", "**/*.spec.ts"]);
1264
1265 let setup = extract_config_string_array(source, &ts_path(), &["test", "setupFiles"]);
1266 assert_eq!(setup, vec!["./test/setup.ts"]);
1267 }
1268
1269 #[test]
1270 fn extract_module_exports_property() {
1271 let source = r#"module.exports = { testEnvironment: "jsdom" };"#;
1272 let val = extract_config_string(source, &js_path(), &["testEnvironment"]);
1273 assert_eq!(val, Some("jsdom".to_string()));
1274 }
1275
1276 #[test]
1277 fn extract_nested_string_array() {
1278 let source = r#"
1279 export default {
1280 resolve: {
1281 alias: {
1282 "@": "./src"
1283 }
1284 },
1285 test: {
1286 include: ["src/**/*.test.ts"]
1287 }
1288 };
1289 "#;
1290 let include = extract_config_string_array(source, &js_path(), &["test", "include"]);
1291 assert_eq!(include, vec!["src/**/*.test.ts"]);
1292 }
1293
1294 #[test]
1295 fn extract_addons_array() {
1296 let source = r#"
1297 export default {
1298 addons: [
1299 "@storybook/addon-a11y",
1300 "@storybook/addon-docs",
1301 "@storybook/addon-links"
1302 ]
1303 };
1304 "#;
1305 let addons = extract_config_property_strings(source, &ts_path(), "addons");
1306 assert_eq!(
1307 addons,
1308 vec![
1309 "@storybook/addon-a11y",
1310 "@storybook/addon-docs",
1311 "@storybook/addon-links"
1312 ]
1313 );
1314 }
1315
1316 #[test]
1317 fn handle_empty_config() {
1318 let source = "";
1319 let result = extract_config_string(source, &js_path(), &["key"]);
1320 assert_eq!(result, None);
1321 }
1322
1323 #[test]
1326 fn object_keys_postcss_plugins() {
1327 let source = r"
1328 module.exports = {
1329 plugins: {
1330 autoprefixer: {},
1331 tailwindcss: {},
1332 'postcss-import': {}
1333 }
1334 };
1335 ";
1336 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1337 assert_eq!(keys, vec!["autoprefixer", "tailwindcss", "postcss-import"]);
1338 }
1339
1340 #[test]
1341 fn object_keys_nested_path() {
1342 let source = r"
1343 export default {
1344 build: {
1345 plugins: {
1346 minify: {},
1347 compress: {}
1348 }
1349 }
1350 };
1351 ";
1352 let keys = extract_config_object_keys(source, &js_path(), &["build", "plugins"]);
1353 assert_eq!(keys, vec!["minify", "compress"]);
1354 }
1355
1356 #[test]
1357 fn object_keys_empty_object() {
1358 let source = r"export default { plugins: {} };";
1359 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1360 assert!(keys.is_empty());
1361 }
1362
1363 #[test]
1364 fn object_keys_non_object_returns_empty() {
1365 let source = r#"export default { plugins: ["a", "b"] };"#;
1366 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1367 assert!(keys.is_empty());
1368 }
1369
1370 #[test]
1373 fn string_or_array_single_string() {
1374 let source = r#"export default { entry: "./src/index.js" };"#;
1375 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1376 assert_eq!(result, vec!["./src/index.js"]);
1377 }
1378
1379 #[test]
1380 fn string_or_array_array() {
1381 let source = r#"export default { entry: ["./src/a.js", "./src/b.js"] };"#;
1382 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1383 assert_eq!(result, vec!["./src/a.js", "./src/b.js"]);
1384 }
1385
1386 #[test]
1387 fn string_or_array_object_values() {
1388 let source =
1389 r#"export default { entry: { main: "./src/main.js", vendor: "./src/vendor.js" } };"#;
1390 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1391 assert_eq!(result, vec!["./src/main.js", "./src/vendor.js"]);
1392 }
1393
1394 #[test]
1395 fn string_or_array_nested_path() {
1396 let source = r#"
1397 export default {
1398 build: {
1399 rollupOptions: {
1400 input: ["./index.html", "./about.html"]
1401 }
1402 }
1403 };
1404 "#;
1405 let result = extract_config_string_or_array(
1406 source,
1407 &js_path(),
1408 &["build", "rollupOptions", "input"],
1409 );
1410 assert_eq!(result, vec!["./index.html", "./about.html"]);
1411 }
1412
1413 #[test]
1414 fn string_or_array_template_literal() {
1415 let source = r"export default { entry: `./src/index.js` };";
1416 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1417 assert_eq!(result, vec!["./src/index.js"]);
1418 }
1419
1420 #[test]
1423 fn require_strings_array() {
1424 let source = r"
1425 module.exports = {
1426 plugins: [
1427 require('autoprefixer'),
1428 require('postcss-import')
1429 ]
1430 };
1431 ";
1432 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1433 assert_eq!(deps, vec!["autoprefixer", "postcss-import"]);
1434 }
1435
1436 #[test]
1437 fn require_strings_with_tuples() {
1438 let source = r"
1439 module.exports = {
1440 plugins: [
1441 require('autoprefixer'),
1442 [require('postcss-preset-env'), { stage: 3 }]
1443 ]
1444 };
1445 ";
1446 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1447 assert_eq!(deps, vec!["autoprefixer", "postcss-preset-env"]);
1448 }
1449
1450 #[test]
1451 fn require_strings_empty_array() {
1452 let source = r"module.exports = { plugins: [] };";
1453 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1454 assert!(deps.is_empty());
1455 }
1456
1457 #[test]
1458 fn require_strings_no_require_calls() {
1459 let source = r#"module.exports = { plugins: ["a", "b"] };"#;
1460 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1461 assert!(deps.is_empty());
1462 }
1463
1464 #[test]
1465 fn extract_aliases_from_object_with_file_url_to_path() {
1466 let source = r#"
1467 import { defineConfig } from 'vite';
1468 import { fileURLToPath, URL } from 'node:url';
1469
1470 export default defineConfig({
1471 resolve: {
1472 alias: {
1473 "@": fileURLToPath(new URL("./src", import.meta.url))
1474 }
1475 }
1476 });
1477 "#;
1478
1479 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
1480 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
1481 }
1482
1483 #[test]
1484 fn extract_aliases_from_array_form() {
1485 let source = r#"
1486 export default {
1487 resolve: {
1488 alias: [
1489 { find: "@", replacement: "./src" },
1490 { find: "$utils", replacement: "src/lib/utils" }
1491 ]
1492 }
1493 };
1494 "#;
1495
1496 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
1497 assert_eq!(
1498 aliases,
1499 vec![
1500 ("@".to_string(), "./src".to_string()),
1501 ("$utils".to_string(), "src/lib/utils".to_string())
1502 ]
1503 );
1504 }
1505
1506 #[test]
1507 fn extract_array_object_strings_mixed_forms() {
1508 let source = r#"
1509 export default {
1510 components: [
1511 "~/components",
1512 { path: "@/feature-components" }
1513 ]
1514 };
1515 "#;
1516
1517 let values =
1518 extract_config_array_object_strings(source, &ts_path(), &["components"], "path");
1519 assert_eq!(
1520 values,
1521 vec![
1522 "~/components".to_string(),
1523 "@/feature-components".to_string()
1524 ]
1525 );
1526 }
1527
1528 #[test]
1529 fn extract_config_plugin_option_string_from_json() {
1530 let source = r#"{
1531 "expo": {
1532 "plugins": [
1533 ["expo-router", { "root": "src/app" }]
1534 ]
1535 }
1536 }"#;
1537
1538 let value = extract_config_plugin_option_string(
1539 source,
1540 &json_path(),
1541 &["expo", "plugins"],
1542 "expo-router",
1543 "root",
1544 );
1545
1546 assert_eq!(value, Some("src/app".to_string()));
1547 }
1548
1549 #[test]
1550 fn extract_config_plugin_option_string_from_top_level_plugins() {
1551 let source = r#"{
1552 "plugins": [
1553 ["expo-router", { "root": "./src/routes" }]
1554 ]
1555 }"#;
1556
1557 let value = extract_config_plugin_option_string_from_paths(
1558 source,
1559 &json_path(),
1560 &[&["plugins"], &["expo", "plugins"]],
1561 "expo-router",
1562 "root",
1563 );
1564
1565 assert_eq!(value, Some("./src/routes".to_string()));
1566 }
1567
1568 #[test]
1569 fn extract_config_plugin_option_string_from_ts_config() {
1570 let source = r"
1571 export default {
1572 expo: {
1573 plugins: [
1574 ['expo-router', { root: './src/app' }]
1575 ]
1576 }
1577 };
1578 ";
1579
1580 let value = extract_config_plugin_option_string(
1581 source,
1582 &ts_path(),
1583 &["expo", "plugins"],
1584 "expo-router",
1585 "root",
1586 );
1587
1588 assert_eq!(value, Some("./src/app".to_string()));
1589 }
1590
1591 #[test]
1592 fn extract_config_plugin_option_string_returns_none_when_plugin_missing() {
1593 let source = r#"{
1594 "expo": {
1595 "plugins": [
1596 ["expo-font", {}]
1597 ]
1598 }
1599 }"#;
1600
1601 let value = extract_config_plugin_option_string(
1602 source,
1603 &json_path(),
1604 &["expo", "plugins"],
1605 "expo-router",
1606 "root",
1607 );
1608
1609 assert_eq!(value, None);
1610 }
1611
1612 #[test]
1613 fn normalize_config_path_relative_to_root() {
1614 let config_path = PathBuf::from("/project/vite.config.ts");
1615 let root = PathBuf::from("/project");
1616
1617 assert_eq!(
1618 normalize_config_path("./src/lib", &config_path, &root),
1619 Some("src/lib".to_string())
1620 );
1621 assert_eq!(
1622 normalize_config_path("/src/lib", &config_path, &root),
1623 Some("src/lib".to_string())
1624 );
1625 }
1626
1627 #[test]
1630 fn json_wrapped_in_parens_string() {
1631 let source = r#"({"extends": "@tsconfig/node18/tsconfig.json"})"#;
1632 let val = extract_config_string(source, &js_path(), &["extends"]);
1633 assert_eq!(val, Some("@tsconfig/node18/tsconfig.json".to_string()));
1634 }
1635
1636 #[test]
1637 fn json_wrapped_in_parens_nested_array() {
1638 let source =
1639 r#"({"compilerOptions": {"types": ["node", "jest"]}, "include": ["src/**/*"]})"#;
1640 let types = extract_config_string_array(source, &js_path(), &["compilerOptions", "types"]);
1641 assert_eq!(types, vec!["node", "jest"]);
1642
1643 let include = extract_config_string_array(source, &js_path(), &["include"]);
1644 assert_eq!(include, vec!["src/**/*"]);
1645 }
1646
1647 #[test]
1648 fn json_wrapped_in_parens_object_keys() {
1649 let source = r#"({"plugins": {"autoprefixer": {}, "tailwindcss": {}}})"#;
1650 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1651 assert_eq!(keys, vec!["autoprefixer", "tailwindcss"]);
1652 }
1653
1654 fn json_path() -> PathBuf {
1657 PathBuf::from("config.json")
1658 }
1659
1660 #[test]
1661 fn json_file_parsed_correctly() {
1662 let source = r#"{"key": "value", "list": ["a", "b"]}"#;
1663 let val = extract_config_string(source, &json_path(), &["key"]);
1664 assert_eq!(val, Some("value".to_string()));
1665
1666 let list = extract_config_string_array(source, &json_path(), &["list"]);
1667 assert_eq!(list, vec!["a", "b"]);
1668 }
1669
1670 #[test]
1671 fn jsonc_file_parsed_correctly() {
1672 let source = r#"{"key": "value"}"#;
1673 let path = PathBuf::from("tsconfig.jsonc");
1674 let val = extract_config_string(source, &path, &["key"]);
1675 assert_eq!(val, Some("value".to_string()));
1676 }
1677
1678 #[test]
1681 fn extract_define_config_arrow_function() {
1682 let source = r#"
1683 import { defineConfig } from 'vite';
1684 export default defineConfig(() => ({
1685 test: {
1686 include: ["**/*.test.ts"]
1687 }
1688 }));
1689 "#;
1690 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
1691 assert_eq!(include, vec!["**/*.test.ts"]);
1692 }
1693
1694 #[test]
1695 fn extract_config_from_default_export_function_declaration() {
1696 let source = r#"
1697 export default function createConfig() {
1698 return {
1699 clientModules: ["./src/client/global.js"]
1700 };
1701 }
1702 "#;
1703
1704 let client_modules = extract_config_string_array(source, &ts_path(), &["clientModules"]);
1705 assert_eq!(client_modules, vec!["./src/client/global.js"]);
1706 }
1707
1708 #[test]
1709 fn extract_config_from_default_export_async_function_declaration() {
1710 let source = r#"
1711 export default async function createConfigAsync() {
1712 return {
1713 docs: {
1714 path: "knowledge"
1715 }
1716 };
1717 }
1718 "#;
1719
1720 let docs_path = extract_config_string(source, &ts_path(), &["docs", "path"]);
1721 assert_eq!(docs_path, Some("knowledge".to_string()));
1722 }
1723
1724 #[test]
1725 fn extract_config_from_exported_arrow_function_identifier() {
1726 let source = r#"
1727 const config = async () => {
1728 return {
1729 themes: ["classic"]
1730 };
1731 };
1732
1733 export default config;
1734 "#;
1735
1736 let themes = extract_config_shallow_strings(source, &ts_path(), "themes");
1737 assert_eq!(themes, vec!["classic"]);
1738 }
1739
1740 #[test]
1743 fn module_exports_nested_string() {
1744 let source = r#"
1745 module.exports = {
1746 resolve: {
1747 alias: {
1748 "@": "./src"
1749 }
1750 }
1751 };
1752 "#;
1753 let val = extract_config_string(source, &js_path(), &["resolve", "alias", "@"]);
1754 assert_eq!(val, Some("./src".to_string()));
1755 }
1756
1757 #[test]
1760 fn property_strings_nested_objects() {
1761 let source = r#"
1762 export default {
1763 plugins: {
1764 group1: { a: "val-a" },
1765 group2: { b: "val-b" }
1766 }
1767 };
1768 "#;
1769 let values = extract_config_property_strings(source, &js_path(), "plugins");
1770 assert!(values.contains(&"val-a".to_string()));
1771 assert!(values.contains(&"val-b".to_string()));
1772 }
1773
1774 #[test]
1775 fn property_strings_missing_key_returns_empty() {
1776 let source = r#"export default { other: "value" };"#;
1777 let values = extract_config_property_strings(source, &js_path(), "missing");
1778 assert!(values.is_empty());
1779 }
1780
1781 #[test]
1784 fn shallow_strings_tuple_array() {
1785 let source = r#"
1786 module.exports = {
1787 reporters: ["default", ["jest-junit", { outputDirectory: "reports" }]]
1788 };
1789 "#;
1790 let values = extract_config_shallow_strings(source, &js_path(), "reporters");
1791 assert_eq!(values, vec!["default", "jest-junit"]);
1792 assert!(!values.contains(&"reports".to_string()));
1794 }
1795
1796 #[test]
1797 fn shallow_strings_single_string() {
1798 let source = r#"export default { preset: "ts-jest" };"#;
1799 let values = extract_config_shallow_strings(source, &js_path(), "preset");
1800 assert_eq!(values, vec!["ts-jest"]);
1801 }
1802
1803 #[test]
1804 fn shallow_strings_missing_key() {
1805 let source = r#"export default { other: "val" };"#;
1806 let values = extract_config_shallow_strings(source, &js_path(), "missing");
1807 assert!(values.is_empty());
1808 }
1809
1810 #[test]
1813 fn nested_shallow_strings_vitest_reporters() {
1814 let source = r#"
1815 export default {
1816 test: {
1817 reporters: ["default", "vitest-sonar-reporter"]
1818 }
1819 };
1820 "#;
1821 let values =
1822 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
1823 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
1824 }
1825
1826 #[test]
1827 fn nested_shallow_strings_tuple_format() {
1828 let source = r#"
1829 export default {
1830 test: {
1831 reporters: ["default", ["vitest-sonar-reporter", { outputFile: "report.xml" }]]
1832 }
1833 };
1834 "#;
1835 let values =
1836 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
1837 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
1838 }
1839
1840 #[test]
1841 fn nested_shallow_strings_missing_outer() {
1842 let source = r"export default { other: {} };";
1843 let values =
1844 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
1845 assert!(values.is_empty());
1846 }
1847
1848 #[test]
1849 fn nested_shallow_strings_missing_inner() {
1850 let source = r#"export default { test: { include: ["**/*.test.ts"] } };"#;
1851 let values =
1852 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
1853 assert!(values.is_empty());
1854 }
1855
1856 #[test]
1859 fn string_or_array_missing_path() {
1860 let source = r"export default {};";
1861 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1862 assert!(result.is_empty());
1863 }
1864
1865 #[test]
1866 fn string_or_array_non_string_values() {
1867 let source = r"export default { entry: [42, true] };";
1869 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1870 assert!(result.is_empty());
1871 }
1872
1873 #[test]
1876 fn array_nested_extraction() {
1877 let source = r#"
1878 export default defineConfig({
1879 test: {
1880 projects: [
1881 {
1882 test: {
1883 setupFiles: ["./test/setup-a.ts"]
1884 }
1885 },
1886 {
1887 test: {
1888 setupFiles: "./test/setup-b.ts"
1889 }
1890 }
1891 ]
1892 }
1893 });
1894 "#;
1895 let results = extract_config_array_nested_string_or_array(
1896 source,
1897 &ts_path(),
1898 &["test", "projects"],
1899 &["test", "setupFiles"],
1900 );
1901 assert!(results.contains(&"./test/setup-a.ts".to_string()));
1902 assert!(results.contains(&"./test/setup-b.ts".to_string()));
1903 }
1904
1905 #[test]
1906 fn array_nested_empty_when_no_array() {
1907 let source = r#"export default { test: { projects: "not-an-array" } };"#;
1908 let results = extract_config_array_nested_string_or_array(
1909 source,
1910 &js_path(),
1911 &["test", "projects"],
1912 &["test", "setupFiles"],
1913 );
1914 assert!(results.is_empty());
1915 }
1916
1917 #[test]
1920 fn object_nested_extraction() {
1921 let source = r#"{
1922 "projects": {
1923 "app-one": {
1924 "architect": {
1925 "build": {
1926 "options": {
1927 "styles": ["src/styles.css"]
1928 }
1929 }
1930 }
1931 }
1932 }
1933 }"#;
1934 let results = extract_config_object_nested_string_or_array(
1935 source,
1936 &json_path(),
1937 &["projects"],
1938 &["architect", "build", "options", "styles"],
1939 );
1940 assert_eq!(results, vec!["src/styles.css"]);
1941 }
1942
1943 #[test]
1946 fn object_nested_strings_extraction() {
1947 let source = r#"{
1948 "targets": {
1949 "build": {
1950 "executor": "@angular/build:application"
1951 },
1952 "test": {
1953 "executor": "@nx/vite:test"
1954 }
1955 }
1956 }"#;
1957 let results =
1958 extract_config_object_nested_strings(source, &json_path(), &["targets"], &["executor"]);
1959 assert!(results.contains(&"@angular/build:application".to_string()));
1960 assert!(results.contains(&"@nx/vite:test".to_string()));
1961 }
1962
1963 #[test]
1966 fn require_strings_direct_call() {
1967 let source = r"module.exports = { adapter: require('@sveltejs/adapter-node') };";
1968 let deps = extract_config_require_strings(source, &js_path(), "adapter");
1969 assert_eq!(deps, vec!["@sveltejs/adapter-node"]);
1970 }
1971
1972 #[test]
1973 fn require_strings_no_matching_key() {
1974 let source = r"module.exports = { other: require('something') };";
1975 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1976 assert!(deps.is_empty());
1977 }
1978
1979 #[test]
1982 fn extract_imports_no_imports() {
1983 let source = r"export default {};";
1984 let imports = extract_imports(source, &js_path());
1985 assert!(imports.is_empty());
1986 }
1987
1988 #[test]
1989 fn extract_imports_side_effect_import() {
1990 let source = r"
1991 import 'polyfill';
1992 import './local-setup';
1993 export default {};
1994 ";
1995 let imports = extract_imports(source, &js_path());
1996 assert_eq!(imports, vec!["polyfill", "./local-setup"]);
1997 }
1998
1999 #[test]
2000 fn extract_imports_mixed_specifiers() {
2001 let source = r"
2002 import defaultExport from 'module-a';
2003 import { named } from 'module-b';
2004 import * as ns from 'module-c';
2005 export default {};
2006 ";
2007 let imports = extract_imports(source, &js_path());
2008 assert_eq!(imports, vec!["module-a", "module-b", "module-c"]);
2009 }
2010
2011 #[test]
2014 fn template_literal_in_string_or_array() {
2015 let source = r"export default { entry: `./src/index.ts` };";
2016 let result = extract_config_string_or_array(source, &ts_path(), &["entry"]);
2017 assert_eq!(result, vec!["./src/index.ts"]);
2018 }
2019
2020 #[test]
2021 fn template_literal_in_config_string() {
2022 let source = r"export default { testDir: `./tests` };";
2023 let val = extract_config_string(source, &js_path(), &["testDir"]);
2024 assert_eq!(val, Some("./tests".to_string()));
2025 }
2026
2027 #[test]
2030 fn nested_string_array_empty_path() {
2031 let source = r#"export default { items: ["a", "b"] };"#;
2032 let result = extract_config_string_array(source, &js_path(), &[]);
2033 assert!(result.is_empty());
2034 }
2035
2036 #[test]
2037 fn nested_string_empty_path() {
2038 let source = r#"export default { key: "val" };"#;
2039 let result = extract_config_string(source, &js_path(), &[]);
2040 assert!(result.is_none());
2041 }
2042
2043 #[test]
2044 fn object_keys_empty_path() {
2045 let source = r"export default { plugins: {} };";
2046 let result = extract_config_object_keys(source, &js_path(), &[]);
2047 assert!(result.is_empty());
2048 }
2049
2050 #[test]
2053 fn no_config_object_returns_empty() {
2054 let source = r"const x = 42;";
2056 let result = extract_config_string(source, &js_path(), &["key"]);
2057 assert!(result.is_none());
2058
2059 let arr = extract_config_string_array(source, &js_path(), &["items"]);
2060 assert!(arr.is_empty());
2061
2062 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
2063 assert!(keys.is_empty());
2064 }
2065
2066 #[test]
2069 fn property_with_string_key() {
2070 let source = r#"export default { "string-key": "value" };"#;
2071 let val = extract_config_string(source, &js_path(), &["string-key"]);
2072 assert_eq!(val, Some("value".to_string()));
2073 }
2074
2075 #[test]
2076 fn nested_navigation_through_non_object() {
2077 let source = r#"export default { level1: "not-an-object" };"#;
2079 let val = extract_config_string(source, &js_path(), &["level1", "level2"]);
2080 assert!(val.is_none());
2081 }
2082
2083 #[test]
2086 fn variable_reference_untyped() {
2087 let source = r#"
2088 const config = {
2089 testDir: "./tests"
2090 };
2091 export default config;
2092 "#;
2093 let val = extract_config_string(source, &js_path(), &["testDir"]);
2094 assert_eq!(val, Some("./tests".to_string()));
2095 }
2096
2097 #[test]
2098 fn variable_reference_with_type_annotation() {
2099 let source = r#"
2100 import type { StorybookConfig } from '@storybook/react-vite';
2101 const config: StorybookConfig = {
2102 addons: ["@storybook/addon-a11y", "@storybook/addon-docs"],
2103 framework: "@storybook/react-vite"
2104 };
2105 export default config;
2106 "#;
2107 let addons = extract_config_shallow_strings(source, &ts_path(), "addons");
2108 assert_eq!(
2109 addons,
2110 vec!["@storybook/addon-a11y", "@storybook/addon-docs"]
2111 );
2112
2113 let framework = extract_config_string(source, &ts_path(), &["framework"]);
2114 assert_eq!(framework, Some("@storybook/react-vite".to_string()));
2115 }
2116
2117 #[test]
2118 fn variable_reference_with_define_config() {
2119 let source = r#"
2120 import { defineConfig } from 'vitest/config';
2121 const config = defineConfig({
2122 test: {
2123 include: ["**/*.test.ts"]
2124 }
2125 });
2126 export default config;
2127 "#;
2128 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
2129 assert_eq!(include, vec!["**/*.test.ts"]);
2130 }
2131
2132 #[test]
2135 fn ts_satisfies_direct_export() {
2136 let source = r#"
2137 export default {
2138 testDir: "./tests"
2139 } satisfies PlaywrightTestConfig;
2140 "#;
2141 let val = extract_config_string(source, &ts_path(), &["testDir"]);
2142 assert_eq!(val, Some("./tests".to_string()));
2143 }
2144
2145 #[test]
2146 fn ts_as_direct_export() {
2147 let source = r#"
2148 export default {
2149 testDir: "./tests"
2150 } as const;
2151 "#;
2152 let val = extract_config_string(source, &ts_path(), &["testDir"]);
2153 assert_eq!(val, Some("./tests".to_string()));
2154 }
2155}