use super::{CrossFileAnalyzer, CrossFileOptions};
use crate::AnalyzerOptions;
use std::path::Path;
#[test]
fn test_provide_inject_basic_match() {
let mut analyzer =
CrossFileAnalyzer::new(CrossFileOptions::default().with_provide_inject(true));
analyzer.add_file(
Path::new("Parent.ts"),
r#"import { provide, reactive } from 'vue'
const state = reactive({ count: 0 })
provide('state', state)"#,
);
analyzer.add_file(
Path::new("Child.ts"),
r#"import { inject } from 'vue'
const state = inject('state')"#,
);
let result = analyzer.analyze();
assert_eq!(result.stats.files_analyzed, 2);
}
#[test]
fn test_provide_inject_with_type_assertion() {
let mut analyzer =
CrossFileAnalyzer::new(CrossFileOptions::default().with_provide_inject(true));
analyzer.add_file(
Path::new("Child.ts"),
r#"import { inject } from 'vue'
const state = inject('state') as { count: number; user: { name: string } }"#,
);
let _result = analyzer.analyze();
let child_analysis = analyzer.get_analysis(analyzer.registry().iter().next().unwrap().id);
assert!(child_analysis.is_some());
let analysis = child_analysis.unwrap();
assert_eq!(analysis.provide_inject.injects().len(), 1);
assert_eq!(
analysis.provide_inject.injects()[0].key,
crate::provide::ProvideKey::String(vize_carton::CompactString::new("state"))
);
}
#[test]
fn test_provide_inject_with_satisfies() {
let mut analyzer =
CrossFileAnalyzer::new(CrossFileOptions::default().with_provide_inject(true));
analyzer.add_file(
Path::new("Child.ts"),
r#"import { inject } from 'vue'
const theme = inject('theme') satisfies string | undefined"#,
);
let _result = analyzer.analyze();
let child_analysis = analyzer.get_analysis(analyzer.registry().iter().next().unwrap().id);
assert!(child_analysis.is_some());
let analysis = child_analysis.unwrap();
assert_eq!(analysis.provide_inject.injects().len(), 1);
}
#[test]
fn test_provide_with_symbol_key() {
let mut analyzer =
CrossFileAnalyzer::new(CrossFileOptions::default().with_provide_inject(true));
analyzer.add_file(
Path::new("Parent.ts"),
r#"import { provide } from 'vue'
const ThemeKey = Symbol('theme')
provide(ThemeKey, 'dark')"#,
);
let _result = analyzer.analyze();
let parent_analysis = analyzer.get_analysis(analyzer.registry().iter().next().unwrap().id);
assert!(parent_analysis.is_some());
let analysis = parent_analysis.unwrap();
assert_eq!(analysis.provide_inject.provides().len(), 1);
}
#[test]
fn test_inject_with_default_value() {
let mut analyzer =
CrossFileAnalyzer::new(CrossFileOptions::default().with_provide_inject(true));
analyzer.add_file(
Path::new("Child.ts"),
r#"import { inject } from 'vue'
const theme = inject('theme', 'light')"#,
);
let _result = analyzer.analyze();
let child_analysis = analyzer.get_analysis(analyzer.registry().iter().next().unwrap().id);
assert!(child_analysis.is_some());
let analysis = child_analysis.unwrap();
let injects = analysis.provide_inject.injects();
assert_eq!(injects.len(), 1);
assert!(injects[0].default_value.is_some());
}
#[test]
fn test_multiple_provides_and_injects() {
let mut analyzer =
CrossFileAnalyzer::new(CrossFileOptions::default().with_provide_inject(true));
analyzer.add_file(
Path::new("Mixed.ts"),
r#"import { provide, inject, ref } from 'vue'
// Inject from ancestor
const theme = inject('theme', 'light')
const user = inject('user')
// Provide for descendants
const count = ref(0)
provide('count', count)
provide('config', { debug: true })"#,
);
let _result = analyzer.analyze();
let analysis = analyzer
.get_analysis(analyzer.registry().iter().next().unwrap().id)
.unwrap();
assert_eq!(analysis.provide_inject.provides().len(), 2);
assert_eq!(analysis.provide_inject.injects().len(), 2);
}
#[test]
fn test_inject_object_destructure_pattern() {
use crate::provide::InjectPattern;
let mut analyzer =
CrossFileAnalyzer::new(CrossFileOptions::default().with_reactivity_tracking(true));
analyzer.add_file(
Path::new("Child.ts"),
r#"import { inject } from 'vue'
const { count, name } = inject('state') as { count: number; name: string }"#,
);
let _result = analyzer.analyze();
let analysis = analyzer
.get_analysis(analyzer.registry().iter().next().unwrap().id)
.unwrap();
let injects = analysis.provide_inject.injects();
assert_eq!(injects.len(), 1, "Should have 1 inject");
match &injects[0].pattern {
InjectPattern::ObjectDestructure(props) => {
assert!(props.contains(&vize_carton::CompactString::new("count")));
assert!(props.contains(&vize_carton::CompactString::new("name")));
}
_ => panic!(
"Expected ObjectDestructure pattern, got {:?}",
injects[0].pattern
),
}
}
#[test]
fn test_inject_simple_pattern() {
use crate::provide::InjectPattern;
let mut analyzer =
CrossFileAnalyzer::new(CrossFileOptions::default().with_provide_inject(true));
analyzer.add_file(
Path::new("Child.ts"),
r#"import { inject } from 'vue'
const state = inject('state')"#,
);
let _result = analyzer.analyze();
let analysis = analyzer
.get_analysis(analyzer.registry().iter().next().unwrap().id)
.unwrap();
let injects = analysis.provide_inject.injects();
assert_eq!(injects.len(), 1);
assert!(matches!(injects[0].pattern, InjectPattern::Simple));
}
#[test]
fn test_inject_destructure_with_type_assertion() {
use crate::provide::InjectPattern;
let mut analyzer =
CrossFileAnalyzer::new(CrossFileOptions::default().with_reactivity_tracking(true));
analyzer.add_file(
Path::new("Child.ts"),
r#"import { inject } from 'vue'
const { foo } = inject('data') as { foo: string }"#,
);
let _result = analyzer.analyze();
let analysis = analyzer
.get_analysis(analyzer.registry().iter().next().unwrap().id)
.unwrap();
let injects = analysis.provide_inject.injects();
assert_eq!(injects.len(), 1);
match &injects[0].pattern {
InjectPattern::ObjectDestructure(props) => {
assert!(props.contains(&vize_carton::CompactString::new("foo")));
}
_ => panic!("Expected ObjectDestructure pattern"),
}
}
#[test]
fn test_inject_destructure_in_vue_sfc() {
use crate::provide::InjectPattern;
let mut analyzer =
CrossFileAnalyzer::new(CrossFileOptions::default().with_reactivity_tracking(true));
analyzer.add_file(
Path::new("Child.vue"),
r#"import { inject } from 'vue'
const { name } = inject('user') as { name: string; id: number }"#,
);
let _result = analyzer.analyze();
let analysis = analyzer
.get_analysis(analyzer.registry().iter().next().unwrap().id)
.unwrap();
let injects = analysis.provide_inject.injects();
assert_eq!(injects.len(), 1, "Should have 1 inject");
match &injects[0].pattern {
InjectPattern::ObjectDestructure(props) => {
assert!(
props.contains(&vize_carton::CompactString::new("name")),
"Should contain 'name' prop"
);
}
other => panic!("Expected ObjectDestructure pattern, got {:?}", other),
}
}
#[test]
fn test_playground_style_provide_inject() {
use crate::cross_file::diagnostics::CrossFileDiagnosticKind;
let mut analyzer = CrossFileAnalyzer::new(
CrossFileOptions::default()
.with_provide_inject(true)
.with_fallthrough_attrs(true)
.with_component_emits(true)
.with_reactivity_tracking(true),
);
let app_script = r#"import { provide, ref } from 'vue'
import ParentComponent from './ParentComponent.vue'
const theme = ref('dark')
provide('theme', theme)
provide('user', { name: 'John', id: 1 })"#;
let mut app_analyzer = crate::Analyzer::with_options(AnalyzerOptions::full());
app_analyzer.analyze_script_setup(app_script);
app_analyzer
.croquis_mut()
.used_components
.insert(vize_carton::CompactString::new("ParentComponent"));
let app_analysis = app_analyzer.finish();
eprintln!(
"App.vue used_components: {:?}",
app_analysis.used_components
);
let parent_script = r#"import { inject, ref, onMounted } from 'vue'
import ChildComponent from './ChildComponent.vue'
const theme = inject('theme')
const { name } = inject('user')"#;
let mut parent_analyzer = crate::Analyzer::with_options(AnalyzerOptions::full());
parent_analyzer.analyze_script_setup(parent_script);
parent_analyzer
.croquis_mut()
.used_components
.insert(vize_carton::CompactString::new("ChildComponent"));
let parent_analysis = parent_analyzer.finish();
eprintln!(
"ParentComponent.vue used_components: {:?}",
parent_analysis.used_components
);
let child_script = r#"const emit = defineEmits(['change'])"#;
let mut child_analyzer = crate::Analyzer::with_options(AnalyzerOptions::full());
child_analyzer.analyze_script_setup(child_script);
let child_analysis = child_analyzer.finish();
analyzer.add_file_with_analysis(Path::new("App.vue"), app_script, app_analysis);
analyzer.add_file_with_analysis(
Path::new("ParentComponent.vue"),
parent_script,
parent_analysis,
);
analyzer.add_file_with_analysis(
Path::new("ChildComponent.vue"),
child_script,
child_analysis,
);
analyzer.rebuild_component_edges();
eprintln!("Graph nodes: {}", analyzer.graph().nodes().count());
for node in analyzer.graph().nodes() {
eprintln!(
" {} (component_name={:?}): imports={:?}",
node.path, node.component_name, node.imports
);
}
let result = analyzer.analyze();
eprintln!("Diagnostics count: {}", result.diagnostics.len());
for d in &result.diagnostics {
eprintln!(" - {:?}: {}", d.kind, d.message);
}
assert!(
!result.provide_inject_matches.is_empty(),
"Should have at least 1 match (theme), got: {:?}",
result.provide_inject_matches
);
let unmatched_theme: Vec<_> = result
.diagnostics
.iter()
.filter(|d| matches!(&d.kind, CrossFileDiagnosticKind::UnmatchedInject { key } if key == "theme"))
.collect();
assert_eq!(
unmatched_theme.len(),
0,
"Should have no unmatched inject for 'theme'"
);
}
#[test]
fn test_provide_inject_with_component_usage_edge() {
use crate::cross_file::diagnostics::CrossFileDiagnosticKind;
let mut analyzer =
CrossFileAnalyzer::new(CrossFileOptions::default().with_provide_inject(true));
let mut app_analyzer = crate::Analyzer::with_options(AnalyzerOptions::full());
app_analyzer.analyze_script_setup(
r#"import { provide, ref } from 'vue'
const theme = ref('dark')
const user = ref({ name: 'Test' })
provide('theme', theme)
provide('user', user)"#,
);
app_analyzer
.croquis_mut()
.used_components
.insert(vize_carton::CompactString::new("Child"));
let app_analysis = app_analyzer.finish();
let mut child_analyzer = crate::Analyzer::with_options(AnalyzerOptions::full());
child_analyzer.analyze_script_setup(
r#"import { inject } from 'vue'
const theme = inject('theme')
const user = inject('user')"#,
);
let child_analysis = child_analyzer.finish();
let _app_id =
analyzer.add_file_with_analysis(Path::new("App.vue"), "script content", app_analysis);
let _child_id = analyzer.add_file_with_analysis(
Path::new("Child.vue"),
"script content",
child_analysis,
);
analyzer.rebuild_component_edges();
let result = analyzer.analyze();
assert_eq!(
result.provide_inject_matches.len(),
2,
"Should have 2 matches (theme and user)"
);
let unmatched_inject_errors: Vec<_> = result
.diagnostics
.iter()
.filter(|d| matches!(d.kind, CrossFileDiagnosticKind::UnmatchedInject { .. }))
.filter(|d| d.is_error())
.collect();
assert_eq!(
unmatched_inject_errors.len(),
0,
"Should have no unmatched inject errors, but got: {:?}",
unmatched_inject_errors
.iter()
.map(|d| &d.message)
.collect::<Vec<_>>()
);
let unused_provide_warnings: Vec<_> = result
.diagnostics
.iter()
.filter(|d| matches!(d.kind, CrossFileDiagnosticKind::UnusedProvide { .. }))
.collect();
assert_eq!(
unused_provide_warnings.len(),
0,
"Should have no unused provide warnings, but got: {:?}",
unused_provide_warnings
.iter()
.map(|d| &d.message)
.collect::<Vec<_>>()
);
}
#[test]
fn test_provide_inject_multiple_levels() {
use crate::cross_file::diagnostics::CrossFileDiagnosticKind;
let mut analyzer =
CrossFileAnalyzer::new(CrossFileOptions::default().with_provide_inject(true));
let mut gp_analyzer = crate::Analyzer::with_options(AnalyzerOptions::full());
gp_analyzer.analyze_script_setup(
r#"import { provide } from 'vue'
provide('globalState', { app: 'test' })"#,
);
gp_analyzer
.croquis_mut()
.used_components
.insert(vize_carton::CompactString::new("Parent"));
let gp_analysis = gp_analyzer.finish();
let mut parent_analyzer = crate::Analyzer::with_options(AnalyzerOptions::full());
parent_analyzer.analyze_script_setup(r#"// No provide/inject"#);
parent_analyzer
.croquis_mut()
.used_components
.insert(vize_carton::CompactString::new("Child"));
let parent_analysis = parent_analyzer.finish();
let mut child_analyzer = crate::Analyzer::with_options(AnalyzerOptions::full());
child_analyzer.analyze_script_setup(
r#"import { inject } from 'vue'
const state = inject('globalState')"#,
);
let child_analysis = child_analyzer.finish();
analyzer.add_file_with_analysis(Path::new("Grandparent.vue"), "", gp_analysis);
analyzer.add_file_with_analysis(Path::new("Parent.vue"), "", parent_analysis);
analyzer.add_file_with_analysis(Path::new("Child.vue"), "", child_analysis);
analyzer.rebuild_component_edges();
let result = analyzer.analyze();
assert_eq!(
result.provide_inject_matches.len(),
1,
"Should have 1 match for globalState"
);
let errors: Vec<_> = result
.diagnostics
.iter()
.filter(|d| d.is_error())
.filter(|d| {
matches!(
d.kind,
CrossFileDiagnosticKind::UnmatchedInject { .. }
| CrossFileDiagnosticKind::UnusedProvide { .. }
)
})
.collect();
assert_eq!(errors.len(), 0, "Should have no provide/inject errors");
}