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