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