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]
236pub fn extract_config_string_or_array(
237 source: &str,
238 path: &Path,
239 prop_path: &[&str],
240) -> Vec<String> {
241 extract_from_source(source, path, |program| {
242 let obj = find_config_object(program)?;
243 get_nested_string_or_array(obj, prop_path)
244 })
245 .unwrap_or_default()
246}
247
248#[must_use]
250pub fn extract_config_path_string(source: &str, path: &Path, prop_path: &[&str]) -> Option<String> {
251 extract_from_source(source, path, |program| {
252 let obj = find_config_object(program)?;
253 let expr = get_nested_expression(obj, prop_path)?;
254 expression_to_path_string(expr)
255 })
256}
257
258#[must_use]
265pub fn extract_config_array_nested_string_or_array(
266 source: &str,
267 path: &Path,
268 array_path: &[&str],
269 inner_path: &[&str],
270) -> Vec<String> {
271 extract_from_source(source, path, |program| {
272 let obj = find_config_object(program)?;
273 let array_expr = get_nested_expression(obj, array_path)?;
274 let Expression::ArrayExpression(arr) = array_expr else {
275 return None;
276 };
277 let mut results = Vec::new();
278 for element in &arr.elements {
279 if let Some(Expression::ObjectExpression(element_obj)) = element.as_expression()
280 && let Some(values) = get_nested_string_or_array(element_obj, inner_path)
281 {
282 results.extend(values);
283 }
284 }
285 if results.is_empty() {
286 None
287 } else {
288 Some(results)
289 }
290 })
291 .unwrap_or_default()
292}
293
294#[must_use]
301pub fn extract_config_object_nested_string_or_array(
302 source: &str,
303 path: &Path,
304 object_path: &[&str],
305 inner_path: &[&str],
306) -> Vec<String> {
307 extract_config_object_nested(source, path, object_path, |value_obj| {
308 get_nested_string_or_array(value_obj, inner_path)
309 })
310}
311
312#[must_use]
317pub fn extract_config_object_nested_strings(
318 source: &str,
319 path: &Path,
320 object_path: &[&str],
321 inner_path: &[&str],
322) -> Vec<String> {
323 extract_config_object_nested(source, path, object_path, |value_obj| {
324 get_nested_string_from_object(value_obj, inner_path).map(|s| vec![s])
325 })
326}
327
328fn extract_config_object_nested(
333 source: &str,
334 path: &Path,
335 object_path: &[&str],
336 extract_fn: impl Fn(&ObjectExpression<'_>) -> Option<Vec<String>>,
337) -> Vec<String> {
338 extract_from_source(source, path, |program| {
339 let obj = find_config_object(program)?;
340 let obj_expr = get_nested_expression(obj, object_path)?;
341 let Expression::ObjectExpression(target_obj) = obj_expr else {
342 return None;
343 };
344 let mut results = Vec::new();
345 for prop in &target_obj.properties {
346 if let ObjectPropertyKind::ObjectProperty(p) = prop
347 && let Expression::ObjectExpression(value_obj) = &p.value
348 && let Some(values) = extract_fn(value_obj)
349 {
350 results.extend(values);
351 }
352 }
353 if results.is_empty() {
354 None
355 } else {
356 Some(results)
357 }
358 })
359 .unwrap_or_default()
360}
361
362#[must_use]
368pub fn extract_config_require_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
369 extract_from_source(source, path, |program| {
370 let obj = find_config_object(program)?;
371 let prop = find_property(obj, key)?;
372 Some(collect_require_sources(&prop.value))
373 })
374 .unwrap_or_default()
375}
376
377#[must_use]
384pub fn extract_config_aliases(
385 source: &str,
386 path: &Path,
387 prop_path: &[&str],
388) -> Vec<(String, String)> {
389 extract_from_source(source, path, |program| {
390 let obj = find_config_object(program)?;
391 let expr = get_nested_expression(obj, prop_path)?;
392 let aliases = expression_to_alias_pairs(expr);
393 (!aliases.is_empty()).then_some(aliases)
394 })
395 .unwrap_or_default()
396}
397
398#[must_use]
404pub fn extract_config_array_object_strings(
405 source: &str,
406 path: &Path,
407 array_path: &[&str],
408 key: &str,
409) -> Vec<String> {
410 extract_from_source(source, path, |program| {
411 let obj = find_config_object(program)?;
412 let array_expr = get_nested_expression(obj, array_path)?;
413 let Expression::ArrayExpression(arr) = array_expr else {
414 return None;
415 };
416
417 let mut results = Vec::new();
418 for element in &arr.elements {
419 let Some(expr) = element.as_expression() else {
420 continue;
421 };
422 match expr {
423 Expression::ObjectExpression(item) => {
424 if let Some(prop) = find_property(item, key)
425 && let Some(value) = expression_to_path_string(&prop.value)
426 {
427 results.push(value);
428 }
429 }
430 _ => {
431 if let Some(value) = expression_to_path_string(expr) {
432 results.push(value);
433 }
434 }
435 }
436 }
437
438 (!results.is_empty()).then_some(results)
439 })
440 .unwrap_or_default()
441}
442
443#[must_use]
450pub fn extract_config_plugin_option_string(
451 source: &str,
452 path: &Path,
453 plugins_path: &[&str],
454 plugin_name: &str,
455 option_key: &str,
456) -> Option<String> {
457 extract_from_source(source, path, |program| {
458 let obj = find_config_object(program)?;
459 let plugins_expr = get_nested_expression(obj, plugins_path)?;
460 let Expression::ArrayExpression(plugins) = plugins_expr else {
461 return None;
462 };
463
464 for entry in &plugins.elements {
465 let Some(Expression::ArrayExpression(tuple)) = entry.as_expression() else {
466 continue;
467 };
468 let Some(plugin_expr) = tuple
469 .elements
470 .first()
471 .and_then(ArrayExpressionElement::as_expression)
472 else {
473 continue;
474 };
475 if expression_to_string(plugin_expr).as_deref() != Some(plugin_name) {
476 continue;
477 }
478
479 let Some(options_expr) = tuple
480 .elements
481 .get(1)
482 .and_then(ArrayExpressionElement::as_expression)
483 else {
484 continue;
485 };
486 let Expression::ObjectExpression(options_obj) = options_expr else {
487 continue;
488 };
489 let option = find_property(options_obj, option_key)?;
490 return expression_to_path_string(&option.value);
491 }
492
493 None
494 })
495}
496
497#[must_use]
499pub fn extract_config_plugin_option_string_from_paths(
500 source: &str,
501 path: &Path,
502 plugin_paths: &[&[&str]],
503 plugin_name: &str,
504 option_key: &str,
505) -> Option<String> {
506 plugin_paths.iter().find_map(|plugins_path| {
507 extract_config_plugin_option_string(source, path, plugins_path, plugin_name, option_key)
508 })
509}
510
511#[must_use]
516pub fn normalize_config_path(raw: &str, config_path: &Path, root: &Path) -> Option<String> {
517 if raw.is_empty() {
518 return None;
519 }
520
521 let candidate = if let Some(stripped) = raw.strip_prefix('/') {
522 lexical_normalize(&root.join(stripped))
523 } else {
524 let path = Path::new(raw);
525 if path.is_absolute() {
526 lexical_normalize(path)
527 } else {
528 let base = config_path.parent().unwrap_or(root);
529 lexical_normalize(&base.join(path))
530 }
531 };
532
533 let relative = candidate.strip_prefix(root).ok()?;
534 let normalized = relative.to_string_lossy().replace('\\', "/");
535 (!normalized.is_empty()).then_some(normalized)
536}
537
538fn extract_from_source<T>(
547 source: &str,
548 path: &Path,
549 extractor: impl FnOnce(&Program) -> Option<T>,
550) -> Option<T> {
551 let source_type = SourceType::from_path(path).unwrap_or_default();
552 let alloc = Allocator::default();
553
554 let is_json = path
557 .extension()
558 .is_some_and(|ext| ext == "json" || ext == "jsonc");
559 if is_json {
560 let wrapped = format!("({source})");
561 let parsed = Parser::new(&alloc, &wrapped, SourceType::mjs()).parse();
562 return extractor(&parsed.program);
563 }
564
565 let parsed = Parser::new(&alloc, source, source_type).parse();
566 extractor(&parsed.program)
567}
568
569fn find_config_object<'a>(program: &'a Program) -> Option<&'a ObjectExpression<'a>> {
581 for stmt in &program.body {
582 match stmt {
583 Statement::ExportDefaultDeclaration(decl) => {
585 let expr: Option<&Expression> = match &decl.declaration {
587 ExportDefaultDeclarationKind::ObjectExpression(obj) => {
588 return Some(obj);
589 }
590 ExportDefaultDeclarationKind::FunctionDeclaration(func) => {
591 return extract_object_from_function(func);
592 }
593 _ => decl.declaration.as_expression(),
594 };
595 if let Some(expr) = expr {
596 if let Some(obj) = extract_object_from_expression(expr) {
598 return Some(obj);
599 }
600 if let Some(name) = unwrap_to_identifier_name(expr) {
603 return find_variable_init_object(program, name);
604 }
605 }
606 }
607 Statement::ExpressionStatement(expr_stmt) => {
609 if let Expression::AssignmentExpression(assign) = &expr_stmt.expression
610 && is_module_exports_target(&assign.left)
611 {
612 return extract_object_from_expression(&assign.right);
613 }
614 }
615 _ => {}
616 }
617 }
618
619 if program.body.len() == 1
622 && let Statement::ExpressionStatement(expr_stmt) = &program.body[0]
623 {
624 match &expr_stmt.expression {
625 Expression::ObjectExpression(obj) => return Some(obj),
626 Expression::ParenthesizedExpression(paren) => {
627 if let Expression::ObjectExpression(obj) = &paren.expression {
628 return Some(obj);
629 }
630 }
631 _ => {}
632 }
633 }
634
635 None
636}
637
638fn extract_object_from_expression<'a>(
640 expr: &'a Expression<'a>,
641) -> Option<&'a ObjectExpression<'a>> {
642 match expr {
643 Expression::ObjectExpression(obj) => Some(obj),
645 Expression::CallExpression(call) => {
647 for arg in &call.arguments {
649 match arg {
650 Argument::ObjectExpression(obj) => return Some(obj),
651 Argument::ArrowFunctionExpression(arrow) => {
653 if arrow.expression
654 && !arrow.body.statements.is_empty()
655 && let Statement::ExpressionStatement(expr_stmt) =
656 &arrow.body.statements[0]
657 {
658 return extract_object_from_expression(&expr_stmt.expression);
659 }
660 }
661 _ => {}
662 }
663 }
664 None
665 }
666 Expression::ParenthesizedExpression(paren) => {
668 extract_object_from_expression(&paren.expression)
669 }
670 Expression::TSSatisfiesExpression(ts_sat) => {
672 extract_object_from_expression(&ts_sat.expression)
673 }
674 Expression::TSAsExpression(ts_as) => extract_object_from_expression(&ts_as.expression),
675 Expression::ArrowFunctionExpression(arrow) => extract_object_from_arrow_function(arrow),
676 Expression::FunctionExpression(func) => extract_object_from_function(func),
677 _ => None,
678 }
679}
680
681fn extract_object_from_arrow_function<'a>(
682 arrow: &'a ArrowFunctionExpression<'a>,
683) -> Option<&'a ObjectExpression<'a>> {
684 if arrow.expression {
685 arrow.body.statements.first().and_then(|stmt| {
686 if let Statement::ExpressionStatement(expr_stmt) = stmt {
687 extract_object_from_expression(&expr_stmt.expression)
688 } else {
689 None
690 }
691 })
692 } else {
693 extract_object_from_function_body(&arrow.body)
694 }
695}
696
697fn extract_object_from_function<'a>(func: &'a Function<'a>) -> Option<&'a ObjectExpression<'a>> {
698 func.body
699 .as_ref()
700 .and_then(|body| extract_object_from_function_body(body))
701}
702
703fn extract_object_from_function_body<'a>(
704 body: &'a FunctionBody<'a>,
705) -> Option<&'a ObjectExpression<'a>> {
706 for stmt in &body.statements {
707 if let Statement::ReturnStatement(ret) = stmt
708 && let Some(argument) = &ret.argument
709 && let Some(obj) = extract_object_from_expression(argument)
710 {
711 return Some(obj);
712 }
713 }
714 None
715}
716
717fn is_module_exports_target(target: &AssignmentTarget) -> bool {
719 if let AssignmentTarget::StaticMemberExpression(member) = target
720 && let Expression::Identifier(obj) = &member.object
721 {
722 return obj.name == "module" && member.property.name == "exports";
723 }
724 false
725}
726
727fn unwrap_to_identifier_name<'a>(expr: &'a Expression<'a>) -> Option<&'a str> {
731 match expr {
732 Expression::Identifier(id) => Some(&id.name),
733 Expression::TSSatisfiesExpression(ts_sat) => unwrap_to_identifier_name(&ts_sat.expression),
734 Expression::TSAsExpression(ts_as) => unwrap_to_identifier_name(&ts_as.expression),
735 _ => None,
736 }
737}
738
739fn find_variable_init_object<'a>(
744 program: &'a Program,
745 name: &str,
746) -> Option<&'a ObjectExpression<'a>> {
747 for stmt in &program.body {
748 if let Statement::VariableDeclaration(decl) = stmt {
749 for declarator in &decl.declarations {
750 if let BindingPattern::BindingIdentifier(id) = &declarator.id
751 && id.name == name
752 && let Some(init) = &declarator.init
753 {
754 return extract_object_from_expression(init);
755 }
756 }
757 }
758 }
759 None
760}
761
762pub(crate) fn find_property<'a>(
764 obj: &'a ObjectExpression<'a>,
765 key: &str,
766) -> Option<&'a ObjectProperty<'a>> {
767 for prop in &obj.properties {
768 if let ObjectPropertyKind::ObjectProperty(p) = prop
769 && property_key_matches(&p.key, key)
770 {
771 return Some(p);
772 }
773 }
774 None
775}
776
777pub(crate) fn property_key_matches(key: &PropertyKey, name: &str) -> bool {
779 match key {
780 PropertyKey::StaticIdentifier(id) => id.name == name,
781 PropertyKey::StringLiteral(s) => s.value == name,
782 _ => false,
783 }
784}
785
786fn get_object_string_property(obj: &ObjectExpression, key: &str) -> Option<String> {
788 find_property(obj, key).and_then(|p| expression_to_string(&p.value))
789}
790
791fn get_object_string_array_property(obj: &ObjectExpression, key: &str) -> Vec<String> {
793 find_property(obj, key)
794 .map(|p| expression_to_string_array(&p.value))
795 .unwrap_or_default()
796}
797
798fn get_nested_string_array_from_object(
800 obj: &ObjectExpression,
801 path: &[&str],
802) -> Option<Vec<String>> {
803 if path.is_empty() {
804 return None;
805 }
806 if path.len() == 1 {
807 return Some(get_object_string_array_property(obj, path[0]));
808 }
809 let prop = find_property(obj, path[0])?;
811 if let Expression::ObjectExpression(nested) = &prop.value {
812 get_nested_string_array_from_object(nested, &path[1..])
813 } else {
814 None
815 }
816}
817
818fn get_nested_string_from_object(obj: &ObjectExpression, path: &[&str]) -> Option<String> {
820 if path.is_empty() {
821 return None;
822 }
823 if path.len() == 1 {
824 return get_object_string_property(obj, path[0]);
825 }
826 let prop = find_property(obj, path[0])?;
827 if let Expression::ObjectExpression(nested) = &prop.value {
828 get_nested_string_from_object(nested, &path[1..])
829 } else {
830 None
831 }
832}
833
834pub(crate) fn expression_to_string(expr: &Expression) -> Option<String> {
836 match expr {
837 Expression::StringLiteral(s) => Some(s.value.to_string()),
838 Expression::TemplateLiteral(t) if t.expressions.is_empty() => {
839 t.quasis.first().map(|q| q.value.raw.to_string())
841 }
842 _ => None,
843 }
844}
845
846pub(crate) fn expression_to_path_string(expr: &Expression) -> Option<String> {
848 match expr {
849 Expression::ParenthesizedExpression(paren) => expression_to_path_string(&paren.expression),
850 Expression::TSAsExpression(ts_as) => expression_to_path_string(&ts_as.expression),
851 Expression::TSSatisfiesExpression(ts_sat) => expression_to_path_string(&ts_sat.expression),
852 Expression::CallExpression(call) => call_expression_to_path_string(call),
853 Expression::NewExpression(new_expr) => new_expression_to_path_string(new_expr),
854 _ => expression_to_string(expr),
855 }
856}
857
858fn call_expression_to_path_string(call: &CallExpression) -> Option<String> {
859 if matches!(&call.callee, Expression::Identifier(id) if id.name == "fileURLToPath") {
860 return call
861 .arguments
862 .first()
863 .and_then(Argument::as_expression)
864 .and_then(expression_to_path_string);
865 }
866
867 let callee_name = match &call.callee {
868 Expression::Identifier(id) => Some(id.name.as_str()),
869 Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
870 _ => None,
871 }?;
872
873 if !matches!(callee_name, "resolve" | "join") {
874 return None;
875 }
876
877 let mut segments = Vec::new();
878 for (index, arg) in call.arguments.iter().enumerate() {
879 let expr = arg.as_expression()?;
880
881 if matches!(expr, Expression::Identifier(id) if id.name == "__dirname") {
882 if index == 0 {
883 continue;
884 }
885 return None;
886 }
887
888 segments.push(expression_to_string(expr)?);
889 }
890
891 (!segments.is_empty()).then(|| join_path_segments(&segments))
892}
893
894fn new_expression_to_path_string(new_expr: &NewExpression) -> Option<String> {
895 if !matches!(&new_expr.callee, Expression::Identifier(id) if id.name == "URL") {
896 return None;
897 }
898
899 let source = new_expr
900 .arguments
901 .first()
902 .and_then(Argument::as_expression)
903 .and_then(expression_to_string)?;
904
905 let base = new_expr
906 .arguments
907 .get(1)
908 .and_then(Argument::as_expression)?;
909 is_import_meta_url_expression(base).then_some(source)
910}
911
912fn is_import_meta_url_expression(expr: &Expression) -> bool {
913 if let Expression::StaticMemberExpression(member) = expr {
914 member.property.name == "url" && matches!(member.object, Expression::MetaProperty(_))
915 } else {
916 false
917 }
918}
919
920fn join_path_segments(segments: &[String]) -> String {
921 let mut joined = PathBuf::new();
922 for segment in segments {
923 joined.push(segment);
924 }
925 joined.to_string_lossy().replace('\\', "/")
926}
927
928fn expression_to_alias_pairs(expr: &Expression) -> Vec<(String, String)> {
929 match expr {
930 Expression::ObjectExpression(obj) => obj
931 .properties
932 .iter()
933 .filter_map(|prop| {
934 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
935 return None;
936 };
937 let find = property_key_to_string(&prop.key)?;
938 let replacement = expression_to_path_values(&prop.value).into_iter().next()?;
939 Some((find, replacement))
940 })
941 .collect(),
942 Expression::ArrayExpression(arr) => arr
943 .elements
944 .iter()
945 .filter_map(|element| {
946 let Expression::ObjectExpression(obj) = element.as_expression()? else {
947 return None;
948 };
949 let find = find_property(obj, "find")
950 .and_then(|prop| expression_to_string(&prop.value))?;
951 let replacement = find_property(obj, "replacement")
952 .and_then(|prop| expression_to_path_string(&prop.value))?;
953 Some((find, replacement))
954 })
955 .collect(),
956 _ => Vec::new(),
957 }
958}
959
960fn lexical_normalize(path: &Path) -> PathBuf {
961 let mut normalized = PathBuf::new();
962
963 for component in path.components() {
964 match component {
965 std::path::Component::CurDir => {}
966 std::path::Component::ParentDir => {
967 normalized.pop();
968 }
969 _ => normalized.push(component.as_os_str()),
970 }
971 }
972
973 normalized
974}
975
976fn expression_to_string_array(expr: &Expression) -> Vec<String> {
978 match expr {
979 Expression::ArrayExpression(arr) => arr
980 .elements
981 .iter()
982 .filter_map(|el| match el {
983 ArrayExpressionElement::SpreadElement(_) => None,
984 _ => el.as_expression().and_then(expression_to_string),
985 })
986 .collect(),
987 _ => vec![],
988 }
989}
990
991fn collect_shallow_string_values(expr: &Expression) -> Vec<String> {
996 let mut values = Vec::new();
997 match expr {
998 Expression::StringLiteral(s) => {
999 values.push(s.value.to_string());
1000 }
1001 Expression::ArrayExpression(arr) => {
1002 for el in &arr.elements {
1003 if let Some(inner) = el.as_expression() {
1004 match inner {
1005 Expression::StringLiteral(s) => {
1006 values.push(s.value.to_string());
1007 }
1008 Expression::ArrayExpression(sub_arr) => {
1010 if let Some(first) = sub_arr.elements.first()
1011 && let Some(first_expr) = first.as_expression()
1012 && let Some(s) = expression_to_string(first_expr)
1013 {
1014 values.push(s);
1015 }
1016 }
1017 _ => {}
1018 }
1019 }
1020 }
1021 }
1022 Expression::ObjectExpression(obj) => {
1024 for prop in &obj.properties {
1025 if let ObjectPropertyKind::ObjectProperty(p) = prop {
1026 match &p.value {
1027 Expression::StringLiteral(s) => {
1028 values.push(s.value.to_string());
1029 }
1030 Expression::ArrayExpression(sub_arr) => {
1032 if let Some(first) = sub_arr.elements.first()
1033 && let Some(first_expr) = first.as_expression()
1034 && let Some(s) = expression_to_string(first_expr)
1035 {
1036 values.push(s);
1037 }
1038 }
1039 _ => {}
1040 }
1041 }
1042 }
1043 }
1044 _ => {}
1045 }
1046 values
1047}
1048
1049fn collect_all_string_values(expr: &Expression, values: &mut Vec<String>) {
1051 match expr {
1052 Expression::StringLiteral(s) => {
1053 values.push(s.value.to_string());
1054 }
1055 Expression::ArrayExpression(arr) => {
1056 for el in &arr.elements {
1057 if let Some(expr) = el.as_expression() {
1058 collect_all_string_values(expr, values);
1059 }
1060 }
1061 }
1062 Expression::ObjectExpression(obj) => {
1063 for prop in &obj.properties {
1064 if let ObjectPropertyKind::ObjectProperty(p) = prop {
1065 collect_all_string_values(&p.value, values);
1066 }
1067 }
1068 }
1069 _ => {}
1070 }
1071}
1072
1073fn property_key_to_string(key: &PropertyKey) -> Option<String> {
1075 match key {
1076 PropertyKey::StaticIdentifier(id) => Some(id.name.to_string()),
1077 PropertyKey::StringLiteral(s) => Some(s.value.to_string()),
1078 _ => None,
1079 }
1080}
1081
1082fn get_nested_object_keys(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
1084 if path.is_empty() {
1085 return None;
1086 }
1087 let prop = find_property(obj, path[0])?;
1088 if path.len() == 1 {
1089 if let Expression::ObjectExpression(nested) = &prop.value {
1090 let keys = nested
1091 .properties
1092 .iter()
1093 .filter_map(|p| {
1094 if let ObjectPropertyKind::ObjectProperty(p) = p {
1095 property_key_to_string(&p.key)
1096 } else {
1097 None
1098 }
1099 })
1100 .collect();
1101 return Some(keys);
1102 }
1103 return None;
1104 }
1105 if let Expression::ObjectExpression(nested) = &prop.value {
1106 get_nested_object_keys(nested, &path[1..])
1107 } else {
1108 None
1109 }
1110}
1111
1112fn get_nested_expression<'a>(
1114 obj: &'a ObjectExpression<'a>,
1115 path: &[&str],
1116) -> Option<&'a Expression<'a>> {
1117 if path.is_empty() {
1118 return None;
1119 }
1120 let prop = find_property(obj, path[0])?;
1121 if path.len() == 1 {
1122 return Some(&prop.value);
1123 }
1124 if let Expression::ObjectExpression(nested) = &prop.value {
1125 get_nested_expression(nested, &path[1..])
1126 } else {
1127 None
1128 }
1129}
1130
1131fn get_nested_string_or_array(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
1133 if path.is_empty() {
1134 return None;
1135 }
1136 if path.len() == 1 {
1137 let prop = find_property(obj, path[0])?;
1138 return Some(expression_to_string_or_array(&prop.value));
1139 }
1140 let prop = find_property(obj, path[0])?;
1141 if let Expression::ObjectExpression(nested) = &prop.value {
1142 get_nested_string_or_array(nested, &path[1..])
1143 } else {
1144 None
1145 }
1146}
1147
1148fn expression_to_string_or_array(expr: &Expression) -> Vec<String> {
1156 match expr {
1157 Expression::StringLiteral(s) => vec![s.value.to_string()],
1158 Expression::TemplateLiteral(t) if t.expressions.is_empty() => t
1159 .quasis
1160 .first()
1161 .map(|q| vec![q.value.raw.to_string()])
1162 .unwrap_or_default(),
1163 Expression::ArrayExpression(arr) => arr
1164 .elements
1165 .iter()
1166 .filter_map(|el| el.as_expression())
1167 .flat_map(|e| match e {
1168 Expression::ObjectExpression(obj) => find_property(obj, "input")
1169 .map(|p| expression_to_string_or_array(&p.value))
1170 .unwrap_or_default(),
1171 _ => expression_to_string(e).into_iter().collect(),
1172 })
1173 .collect(),
1174 Expression::ObjectExpression(obj) => obj
1175 .properties
1176 .iter()
1177 .flat_map(|p| {
1178 if let ObjectPropertyKind::ObjectProperty(p) = p {
1179 match &p.value {
1180 Expression::ArrayExpression(_) => expression_to_string_or_array(&p.value),
1181 Expression::ObjectExpression(value_obj) => {
1182 find_property(value_obj, "import")
1183 .map(|import_prop| {
1184 expression_to_string_or_array(&import_prop.value)
1185 })
1186 .unwrap_or_default()
1187 }
1188 _ => expression_to_string(&p.value).into_iter().collect(),
1189 }
1190 } else {
1191 Vec::new()
1192 }
1193 })
1194 .collect(),
1195 _ => vec![],
1196 }
1197}
1198
1199fn collect_require_sources(expr: &Expression) -> Vec<String> {
1201 let mut sources = Vec::new();
1202 match expr {
1203 Expression::CallExpression(call) if is_require_call(call) => {
1204 if let Some(s) = get_require_source(call) {
1205 sources.push(s);
1206 }
1207 }
1208 Expression::ArrayExpression(arr) => {
1209 for el in &arr.elements {
1210 if let Some(inner) = el.as_expression() {
1211 match inner {
1212 Expression::CallExpression(call) if is_require_call(call) => {
1213 if let Some(s) = get_require_source(call) {
1214 sources.push(s);
1215 }
1216 }
1217 Expression::ArrayExpression(sub_arr) => {
1219 if let Some(first) = sub_arr.elements.first()
1220 && let Some(Expression::CallExpression(call)) =
1221 first.as_expression()
1222 && is_require_call(call)
1223 && let Some(s) = get_require_source(call)
1224 {
1225 sources.push(s);
1226 }
1227 }
1228 _ => {}
1229 }
1230 }
1231 }
1232 }
1233 _ => {}
1234 }
1235 sources
1236}
1237
1238fn is_require_call(call: &CallExpression) -> bool {
1240 matches!(&call.callee, Expression::Identifier(id) if id.name == "require")
1241}
1242
1243fn get_require_source(call: &CallExpression) -> Option<String> {
1245 call.arguments.first().and_then(|arg| {
1246 if let Argument::StringLiteral(s) = arg {
1247 Some(s.value.to_string())
1248 } else {
1249 None
1250 }
1251 })
1252}
1253
1254#[cfg(test)]
1255mod tests {
1256 use super::*;
1257 use std::path::PathBuf;
1258
1259 fn js_path() -> PathBuf {
1260 PathBuf::from("config.js")
1261 }
1262
1263 fn ts_path() -> PathBuf {
1264 PathBuf::from("config.ts")
1265 }
1266
1267 #[test]
1268 fn extract_imports_basic() {
1269 let source = r"
1270 import foo from 'foo-pkg';
1271 import { bar } from '@scope/bar';
1272 export default {};
1273 ";
1274 let imports = extract_imports(source, &js_path());
1275 assert_eq!(imports, vec!["foo-pkg", "@scope/bar"]);
1276 }
1277
1278 #[test]
1279 fn extract_default_export_object_property() {
1280 let source = r#"export default { testDir: "./tests" };"#;
1281 let val = extract_config_string(source, &js_path(), &["testDir"]);
1282 assert_eq!(val, Some("./tests".to_string()));
1283 }
1284
1285 #[test]
1286 fn extract_define_config_property() {
1287 let source = r#"
1288 import { defineConfig } from 'vitest/config';
1289 export default defineConfig({
1290 test: {
1291 include: ["**/*.test.ts", "**/*.spec.ts"],
1292 setupFiles: ["./test/setup.ts"]
1293 }
1294 });
1295 "#;
1296 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
1297 assert_eq!(include, vec!["**/*.test.ts", "**/*.spec.ts"]);
1298
1299 let setup = extract_config_string_array(source, &ts_path(), &["test", "setupFiles"]);
1300 assert_eq!(setup, vec!["./test/setup.ts"]);
1301 }
1302
1303 #[test]
1304 fn extract_module_exports_property() {
1305 let source = r#"module.exports = { testEnvironment: "jsdom" };"#;
1306 let val = extract_config_string(source, &js_path(), &["testEnvironment"]);
1307 assert_eq!(val, Some("jsdom".to_string()));
1308 }
1309
1310 #[test]
1311 fn extract_nested_string_array() {
1312 let source = r#"
1313 export default {
1314 resolve: {
1315 alias: {
1316 "@": "./src"
1317 }
1318 },
1319 test: {
1320 include: ["src/**/*.test.ts"]
1321 }
1322 };
1323 "#;
1324 let include = extract_config_string_array(source, &js_path(), &["test", "include"]);
1325 assert_eq!(include, vec!["src/**/*.test.ts"]);
1326 }
1327
1328 #[test]
1329 fn extract_addons_array() {
1330 let source = r#"
1331 export default {
1332 addons: [
1333 "@storybook/addon-a11y",
1334 "@storybook/addon-docs",
1335 "@storybook/addon-links"
1336 ]
1337 };
1338 "#;
1339 let addons = extract_config_property_strings(source, &ts_path(), "addons");
1340 assert_eq!(
1341 addons,
1342 vec![
1343 "@storybook/addon-a11y",
1344 "@storybook/addon-docs",
1345 "@storybook/addon-links"
1346 ]
1347 );
1348 }
1349
1350 #[test]
1351 fn handle_empty_config() {
1352 let source = "";
1353 let result = extract_config_string(source, &js_path(), &["key"]);
1354 assert_eq!(result, None);
1355 }
1356
1357 #[test]
1360 fn object_keys_postcss_plugins() {
1361 let source = r"
1362 module.exports = {
1363 plugins: {
1364 autoprefixer: {},
1365 tailwindcss: {},
1366 'postcss-import': {}
1367 }
1368 };
1369 ";
1370 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1371 assert_eq!(keys, vec!["autoprefixer", "tailwindcss", "postcss-import"]);
1372 }
1373
1374 #[test]
1375 fn object_keys_nested_path() {
1376 let source = r"
1377 export default {
1378 build: {
1379 plugins: {
1380 minify: {},
1381 compress: {}
1382 }
1383 }
1384 };
1385 ";
1386 let keys = extract_config_object_keys(source, &js_path(), &["build", "plugins"]);
1387 assert_eq!(keys, vec!["minify", "compress"]);
1388 }
1389
1390 #[test]
1391 fn object_keys_empty_object() {
1392 let source = r"export default { plugins: {} };";
1393 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1394 assert!(keys.is_empty());
1395 }
1396
1397 #[test]
1398 fn object_keys_non_object_returns_empty() {
1399 let source = r#"export default { plugins: ["a", "b"] };"#;
1400 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1401 assert!(keys.is_empty());
1402 }
1403
1404 #[test]
1407 fn string_or_array_single_string() {
1408 let source = r#"export default { entry: "./src/index.js" };"#;
1409 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1410 assert_eq!(result, vec!["./src/index.js"]);
1411 }
1412
1413 #[test]
1414 fn string_or_array_array() {
1415 let source = r#"export default { entry: ["./src/a.js", "./src/b.js"] };"#;
1416 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1417 assert_eq!(result, vec!["./src/a.js", "./src/b.js"]);
1418 }
1419
1420 #[test]
1421 fn string_or_array_object_values() {
1422 let source =
1423 r#"export default { entry: { main: "./src/main.js", vendor: "./src/vendor.js" } };"#;
1424 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1425 assert_eq!(result, vec!["./src/main.js", "./src/vendor.js"]);
1426 }
1427
1428 #[test]
1429 fn string_or_array_object_array_values() {
1430 let source = r#"export default { entry: { app: ["./src/polyfill.js", "./src/app.js"] } };"#;
1431 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1432 assert_eq!(result, vec!["./src/polyfill.js", "./src/app.js"]);
1433 }
1434
1435 #[test]
1436 fn string_or_array_webpack_entry_descriptors() {
1437 let source = r#"
1438 export default {
1439 entry: {
1440 app: {
1441 import: "./src/app.js",
1442 filename: "pages/app.js",
1443 dependOn: "shared",
1444 },
1445 admin: {
1446 import: ["./src/admin-polyfill.js", "./src/admin.js"],
1447 runtime: "runtime",
1448 },
1449 shared: ["react", "react-dom"],
1450 },
1451 };
1452 "#;
1453 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1454 assert_eq!(
1455 result,
1456 vec![
1457 "./src/app.js",
1458 "./src/admin-polyfill.js",
1459 "./src/admin.js",
1460 "react",
1461 "react-dom"
1462 ]
1463 );
1464 }
1465
1466 #[test]
1467 fn string_or_array_nested_path() {
1468 let source = r#"
1469 export default {
1470 build: {
1471 rollupOptions: {
1472 input: ["./index.html", "./about.html"]
1473 }
1474 }
1475 };
1476 "#;
1477 let result = extract_config_string_or_array(
1478 source,
1479 &js_path(),
1480 &["build", "rollupOptions", "input"],
1481 );
1482 assert_eq!(result, vec!["./index.html", "./about.html"]);
1483 }
1484
1485 #[test]
1486 fn string_or_array_template_literal() {
1487 let source = r"export default { entry: `./src/index.js` };";
1488 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1489 assert_eq!(result, vec!["./src/index.js"]);
1490 }
1491
1492 #[test]
1495 fn require_strings_array() {
1496 let source = r"
1497 module.exports = {
1498 plugins: [
1499 require('autoprefixer'),
1500 require('postcss-import')
1501 ]
1502 };
1503 ";
1504 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1505 assert_eq!(deps, vec!["autoprefixer", "postcss-import"]);
1506 }
1507
1508 #[test]
1509 fn require_strings_with_tuples() {
1510 let source = r"
1511 module.exports = {
1512 plugins: [
1513 require('autoprefixer'),
1514 [require('postcss-preset-env'), { stage: 3 }]
1515 ]
1516 };
1517 ";
1518 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1519 assert_eq!(deps, vec!["autoprefixer", "postcss-preset-env"]);
1520 }
1521
1522 #[test]
1523 fn require_strings_empty_array() {
1524 let source = r"module.exports = { plugins: [] };";
1525 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1526 assert!(deps.is_empty());
1527 }
1528
1529 #[test]
1530 fn require_strings_no_require_calls() {
1531 let source = r#"module.exports = { plugins: ["a", "b"] };"#;
1532 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1533 assert!(deps.is_empty());
1534 }
1535
1536 #[test]
1537 fn extract_aliases_from_object_with_file_url_to_path() {
1538 let source = r#"
1539 import { defineConfig } from 'vite';
1540 import { fileURLToPath, URL } from 'node:url';
1541
1542 export default defineConfig({
1543 resolve: {
1544 alias: {
1545 "@": fileURLToPath(new URL("./src", import.meta.url))
1546 }
1547 }
1548 });
1549 "#;
1550
1551 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
1552 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
1553 }
1554
1555 #[test]
1556 fn extract_aliases_from_array_form() {
1557 let source = r#"
1558 export default {
1559 resolve: {
1560 alias: [
1561 { find: "@", replacement: "./src" },
1562 { find: "$utils", replacement: "src/lib/utils" }
1563 ]
1564 }
1565 };
1566 "#;
1567
1568 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
1569 assert_eq!(
1570 aliases,
1571 vec![
1572 ("@".to_string(), "./src".to_string()),
1573 ("$utils".to_string(), "src/lib/utils".to_string())
1574 ]
1575 );
1576 }
1577
1578 #[test]
1579 fn extract_aliases_from_object_with_array_values() {
1580 let source = r#"
1581 ({
1582 compilerOptions: {
1583 paths: {
1584 "@/*": ["./src/*"],
1585 "@shared/*": ["./shared/*", "./fallback/*"]
1586 }
1587 }
1588 })
1589 "#;
1590
1591 let aliases = extract_config_aliases(source, &js_path(), &["compilerOptions", "paths"]);
1592 assert_eq!(
1593 aliases,
1594 vec![
1595 ("@/*".to_string(), "./src/*".to_string()),
1596 ("@shared/*".to_string(), "./shared/*".to_string())
1597 ]
1598 );
1599 }
1600
1601 #[test]
1602 fn extract_array_object_strings_mixed_forms() {
1603 let source = r#"
1604 export default {
1605 components: [
1606 "~/components",
1607 { path: "@/feature-components" }
1608 ]
1609 };
1610 "#;
1611
1612 let values =
1613 extract_config_array_object_strings(source, &ts_path(), &["components"], "path");
1614 assert_eq!(
1615 values,
1616 vec![
1617 "~/components".to_string(),
1618 "@/feature-components".to_string()
1619 ]
1620 );
1621 }
1622
1623 #[test]
1624 fn extract_config_plugin_option_string_from_json() {
1625 let source = r#"{
1626 "expo": {
1627 "plugins": [
1628 ["expo-router", { "root": "src/app" }]
1629 ]
1630 }
1631 }"#;
1632
1633 let value = extract_config_plugin_option_string(
1634 source,
1635 &json_path(),
1636 &["expo", "plugins"],
1637 "expo-router",
1638 "root",
1639 );
1640
1641 assert_eq!(value, Some("src/app".to_string()));
1642 }
1643
1644 #[test]
1645 fn extract_config_plugin_option_string_from_top_level_plugins() {
1646 let source = r#"{
1647 "plugins": [
1648 ["expo-router", { "root": "./src/routes" }]
1649 ]
1650 }"#;
1651
1652 let value = extract_config_plugin_option_string_from_paths(
1653 source,
1654 &json_path(),
1655 &[&["plugins"], &["expo", "plugins"]],
1656 "expo-router",
1657 "root",
1658 );
1659
1660 assert_eq!(value, Some("./src/routes".to_string()));
1661 }
1662
1663 #[test]
1664 fn extract_config_plugin_option_string_from_ts_config() {
1665 let source = r"
1666 export default {
1667 expo: {
1668 plugins: [
1669 ['expo-router', { root: './src/app' }]
1670 ]
1671 }
1672 };
1673 ";
1674
1675 let value = extract_config_plugin_option_string(
1676 source,
1677 &ts_path(),
1678 &["expo", "plugins"],
1679 "expo-router",
1680 "root",
1681 );
1682
1683 assert_eq!(value, Some("./src/app".to_string()));
1684 }
1685
1686 #[test]
1687 fn extract_config_plugin_option_string_returns_none_when_plugin_missing() {
1688 let source = r#"{
1689 "expo": {
1690 "plugins": [
1691 ["expo-font", {}]
1692 ]
1693 }
1694 }"#;
1695
1696 let value = extract_config_plugin_option_string(
1697 source,
1698 &json_path(),
1699 &["expo", "plugins"],
1700 "expo-router",
1701 "root",
1702 );
1703
1704 assert_eq!(value, None);
1705 }
1706
1707 #[test]
1708 fn normalize_config_path_relative_to_root() {
1709 let config_path = PathBuf::from("/project/vite.config.ts");
1710 let root = PathBuf::from("/project");
1711
1712 assert_eq!(
1713 normalize_config_path("./src/lib", &config_path, &root),
1714 Some("src/lib".to_string())
1715 );
1716 assert_eq!(
1717 normalize_config_path("/src/lib", &config_path, &root),
1718 Some("src/lib".to_string())
1719 );
1720 }
1721
1722 #[test]
1725 fn json_wrapped_in_parens_string() {
1726 let source = r#"({"extends": "@tsconfig/node18/tsconfig.json"})"#;
1727 let val = extract_config_string(source, &js_path(), &["extends"]);
1728 assert_eq!(val, Some("@tsconfig/node18/tsconfig.json".to_string()));
1729 }
1730
1731 #[test]
1732 fn json_wrapped_in_parens_nested_array() {
1733 let source =
1734 r#"({"compilerOptions": {"types": ["node", "jest"]}, "include": ["src/**/*"]})"#;
1735 let types = extract_config_string_array(source, &js_path(), &["compilerOptions", "types"]);
1736 assert_eq!(types, vec!["node", "jest"]);
1737
1738 let include = extract_config_string_array(source, &js_path(), &["include"]);
1739 assert_eq!(include, vec!["src/**/*"]);
1740 }
1741
1742 #[test]
1743 fn json_wrapped_in_parens_object_keys() {
1744 let source = r#"({"plugins": {"autoprefixer": {}, "tailwindcss": {}}})"#;
1745 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1746 assert_eq!(keys, vec!["autoprefixer", "tailwindcss"]);
1747 }
1748
1749 fn json_path() -> PathBuf {
1752 PathBuf::from("config.json")
1753 }
1754
1755 #[test]
1756 fn json_file_parsed_correctly() {
1757 let source = r#"{"key": "value", "list": ["a", "b"]}"#;
1758 let val = extract_config_string(source, &json_path(), &["key"]);
1759 assert_eq!(val, Some("value".to_string()));
1760
1761 let list = extract_config_string_array(source, &json_path(), &["list"]);
1762 assert_eq!(list, vec!["a", "b"]);
1763 }
1764
1765 #[test]
1766 fn jsonc_file_parsed_correctly() {
1767 let source = r#"{"key": "value"}"#;
1768 let path = PathBuf::from("tsconfig.jsonc");
1769 let val = extract_config_string(source, &path, &["key"]);
1770 assert_eq!(val, Some("value".to_string()));
1771 }
1772
1773 #[test]
1776 fn extract_define_config_arrow_function() {
1777 let source = r#"
1778 import { defineConfig } from 'vite';
1779 export default defineConfig(() => ({
1780 test: {
1781 include: ["**/*.test.ts"]
1782 }
1783 }));
1784 "#;
1785 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
1786 assert_eq!(include, vec!["**/*.test.ts"]);
1787 }
1788
1789 #[test]
1790 fn extract_config_from_default_export_function_declaration() {
1791 let source = r#"
1792 export default function createConfig() {
1793 return {
1794 clientModules: ["./src/client/global.js"]
1795 };
1796 }
1797 "#;
1798
1799 let client_modules = extract_config_string_array(source, &ts_path(), &["clientModules"]);
1800 assert_eq!(client_modules, vec!["./src/client/global.js"]);
1801 }
1802
1803 #[test]
1804 fn extract_config_from_default_export_async_function_declaration() {
1805 let source = r#"
1806 export default async function createConfigAsync() {
1807 return {
1808 docs: {
1809 path: "knowledge"
1810 }
1811 };
1812 }
1813 "#;
1814
1815 let docs_path = extract_config_string(source, &ts_path(), &["docs", "path"]);
1816 assert_eq!(docs_path, Some("knowledge".to_string()));
1817 }
1818
1819 #[test]
1820 fn extract_config_from_exported_arrow_function_identifier() {
1821 let source = r#"
1822 const config = async () => {
1823 return {
1824 themes: ["classic"]
1825 };
1826 };
1827
1828 export default config;
1829 "#;
1830
1831 let themes = extract_config_shallow_strings(source, &ts_path(), "themes");
1832 assert_eq!(themes, vec!["classic"]);
1833 }
1834
1835 #[test]
1838 fn module_exports_nested_string() {
1839 let source = r#"
1840 module.exports = {
1841 resolve: {
1842 alias: {
1843 "@": "./src"
1844 }
1845 }
1846 };
1847 "#;
1848 let val = extract_config_string(source, &js_path(), &["resolve", "alias", "@"]);
1849 assert_eq!(val, Some("./src".to_string()));
1850 }
1851
1852 #[test]
1855 fn property_strings_nested_objects() {
1856 let source = r#"
1857 export default {
1858 plugins: {
1859 group1: { a: "val-a" },
1860 group2: { b: "val-b" }
1861 }
1862 };
1863 "#;
1864 let values = extract_config_property_strings(source, &js_path(), "plugins");
1865 assert!(values.contains(&"val-a".to_string()));
1866 assert!(values.contains(&"val-b".to_string()));
1867 }
1868
1869 #[test]
1870 fn property_strings_missing_key_returns_empty() {
1871 let source = r#"export default { other: "value" };"#;
1872 let values = extract_config_property_strings(source, &js_path(), "missing");
1873 assert!(values.is_empty());
1874 }
1875
1876 #[test]
1879 fn shallow_strings_tuple_array() {
1880 let source = r#"
1881 module.exports = {
1882 reporters: ["default", ["jest-junit", { outputDirectory: "reports" }]]
1883 };
1884 "#;
1885 let values = extract_config_shallow_strings(source, &js_path(), "reporters");
1886 assert_eq!(values, vec!["default", "jest-junit"]);
1887 assert!(!values.contains(&"reports".to_string()));
1889 }
1890
1891 #[test]
1892 fn shallow_strings_single_string() {
1893 let source = r#"export default { preset: "ts-jest" };"#;
1894 let values = extract_config_shallow_strings(source, &js_path(), "preset");
1895 assert_eq!(values, vec!["ts-jest"]);
1896 }
1897
1898 #[test]
1899 fn shallow_strings_missing_key() {
1900 let source = r#"export default { other: "val" };"#;
1901 let values = extract_config_shallow_strings(source, &js_path(), "missing");
1902 assert!(values.is_empty());
1903 }
1904
1905 #[test]
1908 fn nested_shallow_strings_vitest_reporters() {
1909 let source = r#"
1910 export default {
1911 test: {
1912 reporters: ["default", "vitest-sonar-reporter"]
1913 }
1914 };
1915 "#;
1916 let values =
1917 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
1918 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
1919 }
1920
1921 #[test]
1922 fn nested_shallow_strings_tuple_format() {
1923 let source = r#"
1924 export default {
1925 test: {
1926 reporters: ["default", ["vitest-sonar-reporter", { outputFile: "report.xml" }]]
1927 }
1928 };
1929 "#;
1930 let values =
1931 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
1932 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
1933 }
1934
1935 #[test]
1936 fn nested_shallow_strings_missing_outer() {
1937 let source = r"export default { other: {} };";
1938 let values =
1939 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
1940 assert!(values.is_empty());
1941 }
1942
1943 #[test]
1944 fn nested_shallow_strings_missing_inner() {
1945 let source = r#"export default { test: { include: ["**/*.test.ts"] } };"#;
1946 let values =
1947 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
1948 assert!(values.is_empty());
1949 }
1950
1951 #[test]
1954 fn string_or_array_missing_path() {
1955 let source = r"export default {};";
1956 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1957 assert!(result.is_empty());
1958 }
1959
1960 #[test]
1961 fn string_or_array_non_string_values() {
1962 let source = r"export default { entry: [42, true] };";
1964 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1965 assert!(result.is_empty());
1966 }
1967
1968 #[test]
1971 fn array_nested_extraction() {
1972 let source = r#"
1973 export default defineConfig({
1974 test: {
1975 projects: [
1976 {
1977 test: {
1978 setupFiles: ["./test/setup-a.ts"]
1979 }
1980 },
1981 {
1982 test: {
1983 setupFiles: "./test/setup-b.ts"
1984 }
1985 }
1986 ]
1987 }
1988 });
1989 "#;
1990 let results = extract_config_array_nested_string_or_array(
1991 source,
1992 &ts_path(),
1993 &["test", "projects"],
1994 &["test", "setupFiles"],
1995 );
1996 assert!(results.contains(&"./test/setup-a.ts".to_string()));
1997 assert!(results.contains(&"./test/setup-b.ts".to_string()));
1998 }
1999
2000 #[test]
2001 fn array_nested_empty_when_no_array() {
2002 let source = r#"export default { test: { projects: "not-an-array" } };"#;
2003 let results = extract_config_array_nested_string_or_array(
2004 source,
2005 &js_path(),
2006 &["test", "projects"],
2007 &["test", "setupFiles"],
2008 );
2009 assert!(results.is_empty());
2010 }
2011
2012 #[test]
2015 fn object_nested_extraction() {
2016 let source = r#"{
2017 "projects": {
2018 "app-one": {
2019 "architect": {
2020 "build": {
2021 "options": {
2022 "styles": ["src/styles.css"]
2023 }
2024 }
2025 }
2026 }
2027 }
2028 }"#;
2029 let results = extract_config_object_nested_string_or_array(
2030 source,
2031 &json_path(),
2032 &["projects"],
2033 &["architect", "build", "options", "styles"],
2034 );
2035 assert_eq!(results, vec!["src/styles.css"]);
2036 }
2037
2038 #[test]
2039 fn array_with_object_input_form_extracted() {
2040 let source = r#"{
2046 "projects": {
2047 "app": {
2048 "architect": {
2049 "build": {
2050 "options": {
2051 "styles": [
2052 "src/styles.scss",
2053 { "input": "src/theme.scss", "bundleName": "theme", "inject": false },
2054 { "bundleName": "lazy-only" }
2055 ]
2056 }
2057 }
2058 }
2059 }
2060 }
2061 }"#;
2062 let results = extract_config_object_nested_string_or_array(
2063 source,
2064 &json_path(),
2065 &["projects"],
2066 &["architect", "build", "options", "styles"],
2067 );
2068 assert!(
2069 results.contains(&"src/styles.scss".to_string()),
2070 "string form must still work: {results:?}"
2071 );
2072 assert!(
2073 results.contains(&"src/theme.scss".to_string()),
2074 "object form with `input` must be extracted: {results:?}"
2075 );
2076 assert!(
2079 !results.contains(&"lazy-only".to_string()),
2080 "bundleName must not be misinterpreted as a path: {results:?}"
2081 );
2082 assert!(
2083 !results.contains(&"theme".to_string()),
2084 "bundleName from full object must not leak: {results:?}"
2085 );
2086 }
2087
2088 #[test]
2091 fn object_nested_strings_extraction() {
2092 let source = r#"{
2093 "targets": {
2094 "build": {
2095 "executor": "@angular/build:application"
2096 },
2097 "test": {
2098 "executor": "@nx/vite:test"
2099 }
2100 }
2101 }"#;
2102 let results =
2103 extract_config_object_nested_strings(source, &json_path(), &["targets"], &["executor"]);
2104 assert!(results.contains(&"@angular/build:application".to_string()));
2105 assert!(results.contains(&"@nx/vite:test".to_string()));
2106 }
2107
2108 #[test]
2111 fn require_strings_direct_call() {
2112 let source = r"module.exports = { adapter: require('@sveltejs/adapter-node') };";
2113 let deps = extract_config_require_strings(source, &js_path(), "adapter");
2114 assert_eq!(deps, vec!["@sveltejs/adapter-node"]);
2115 }
2116
2117 #[test]
2118 fn require_strings_no_matching_key() {
2119 let source = r"module.exports = { other: require('something') };";
2120 let deps = extract_config_require_strings(source, &js_path(), "plugins");
2121 assert!(deps.is_empty());
2122 }
2123
2124 #[test]
2127 fn extract_imports_no_imports() {
2128 let source = r"export default {};";
2129 let imports = extract_imports(source, &js_path());
2130 assert!(imports.is_empty());
2131 }
2132
2133 #[test]
2134 fn extract_imports_side_effect_import() {
2135 let source = r"
2136 import 'polyfill';
2137 import './local-setup';
2138 export default {};
2139 ";
2140 let imports = extract_imports(source, &js_path());
2141 assert_eq!(imports, vec!["polyfill", "./local-setup"]);
2142 }
2143
2144 #[test]
2145 fn extract_imports_mixed_specifiers() {
2146 let source = r"
2147 import defaultExport from 'module-a';
2148 import { named } from 'module-b';
2149 import * as ns from 'module-c';
2150 export default {};
2151 ";
2152 let imports = extract_imports(source, &js_path());
2153 assert_eq!(imports, vec!["module-a", "module-b", "module-c"]);
2154 }
2155
2156 #[test]
2159 fn template_literal_in_string_or_array() {
2160 let source = r"export default { entry: `./src/index.ts` };";
2161 let result = extract_config_string_or_array(source, &ts_path(), &["entry"]);
2162 assert_eq!(result, vec!["./src/index.ts"]);
2163 }
2164
2165 #[test]
2166 fn template_literal_in_config_string() {
2167 let source = r"export default { testDir: `./tests` };";
2168 let val = extract_config_string(source, &js_path(), &["testDir"]);
2169 assert_eq!(val, Some("./tests".to_string()));
2170 }
2171
2172 #[test]
2175 fn nested_string_array_empty_path() {
2176 let source = r#"export default { items: ["a", "b"] };"#;
2177 let result = extract_config_string_array(source, &js_path(), &[]);
2178 assert!(result.is_empty());
2179 }
2180
2181 #[test]
2182 fn nested_string_empty_path() {
2183 let source = r#"export default { key: "val" };"#;
2184 let result = extract_config_string(source, &js_path(), &[]);
2185 assert!(result.is_none());
2186 }
2187
2188 #[test]
2189 fn object_keys_empty_path() {
2190 let source = r"export default { plugins: {} };";
2191 let result = extract_config_object_keys(source, &js_path(), &[]);
2192 assert!(result.is_empty());
2193 }
2194
2195 #[test]
2198 fn no_config_object_returns_empty() {
2199 let source = r"const x = 42;";
2201 let result = extract_config_string(source, &js_path(), &["key"]);
2202 assert!(result.is_none());
2203
2204 let arr = extract_config_string_array(source, &js_path(), &["items"]);
2205 assert!(arr.is_empty());
2206
2207 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
2208 assert!(keys.is_empty());
2209 }
2210
2211 #[test]
2214 fn property_with_string_key() {
2215 let source = r#"export default { "string-key": "value" };"#;
2216 let val = extract_config_string(source, &js_path(), &["string-key"]);
2217 assert_eq!(val, Some("value".to_string()));
2218 }
2219
2220 #[test]
2221 fn nested_navigation_through_non_object() {
2222 let source = r#"export default { level1: "not-an-object" };"#;
2224 let val = extract_config_string(source, &js_path(), &["level1", "level2"]);
2225 assert!(val.is_none());
2226 }
2227
2228 #[test]
2231 fn variable_reference_untyped() {
2232 let source = r#"
2233 const config = {
2234 testDir: "./tests"
2235 };
2236 export default config;
2237 "#;
2238 let val = extract_config_string(source, &js_path(), &["testDir"]);
2239 assert_eq!(val, Some("./tests".to_string()));
2240 }
2241
2242 #[test]
2243 fn variable_reference_with_type_annotation() {
2244 let source = r#"
2245 import type { StorybookConfig } from '@storybook/react-vite';
2246 const config: StorybookConfig = {
2247 addons: ["@storybook/addon-a11y", "@storybook/addon-docs"],
2248 framework: "@storybook/react-vite"
2249 };
2250 export default config;
2251 "#;
2252 let addons = extract_config_shallow_strings(source, &ts_path(), "addons");
2253 assert_eq!(
2254 addons,
2255 vec!["@storybook/addon-a11y", "@storybook/addon-docs"]
2256 );
2257
2258 let framework = extract_config_string(source, &ts_path(), &["framework"]);
2259 assert_eq!(framework, Some("@storybook/react-vite".to_string()));
2260 }
2261
2262 #[test]
2263 fn variable_reference_with_define_config() {
2264 let source = r#"
2265 import { defineConfig } from 'vitest/config';
2266 const config = defineConfig({
2267 test: {
2268 include: ["**/*.test.ts"]
2269 }
2270 });
2271 export default config;
2272 "#;
2273 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
2274 assert_eq!(include, vec!["**/*.test.ts"]);
2275 }
2276
2277 #[test]
2280 fn ts_satisfies_direct_export() {
2281 let source = r#"
2282 export default {
2283 testDir: "./tests"
2284 } satisfies PlaywrightTestConfig;
2285 "#;
2286 let val = extract_config_string(source, &ts_path(), &["testDir"]);
2287 assert_eq!(val, Some("./tests".to_string()));
2288 }
2289
2290 #[test]
2291 fn ts_as_direct_export() {
2292 let source = r#"
2293 export default {
2294 testDir: "./tests"
2295 } as const;
2296 "#;
2297 let val = extract_config_string(source, &ts_path(), &["testDir"]);
2298 assert_eq!(val, Some("./tests".to_string()));
2299 }
2300}