use std::collections::BTreeMap;
use crate::{
context_designator::resolve_context_designator, BindingTable, ComponentNode, ContextDesignator,
ContextEntity, ContextId, ExecutionBoundary, ExpressionGraph, ProviderId, SemanticId,
SemanticOwner, SemanticTypeId, SourceProvenance,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProviderEntity {
pub id: ProviderId,
pub owner: SemanticOwner,
pub authored_field: SemanticId,
pub name: String,
pub context_designator: ContextDesignator,
pub context: ContextId,
pub declared_type: crate::DeclaredStateType,
pub declared_type_id: SemanticTypeId,
pub value_expression: SemanticId,
pub execution_boundary: ExecutionBoundary,
pub provenance: SourceProvenance,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DuplicateProviderDeclaration {
pub owner: SemanticId,
pub context: ContextId,
pub retained_provider: ProviderId,
pub rejected_field: SemanticId,
pub provenance: SourceProvenance,
}
#[must_use]
pub fn collect_provider_entities(
components: &[ComponentNode],
contexts: &BTreeMap<ContextId, ContextEntity>,
expression_graph: &ExpressionGraph,
bindings: Option<&BindingTable>,
) -> (
BTreeMap<ProviderId, ProviderEntity>,
Vec<DuplicateProviderDeclaration>,
) {
let mut providers = BTreeMap::new();
let mut duplicates = Vec::new();
let mut declarations = Vec::new();
for component in components {
for declaration in &component.provider_declarations {
let Some(context) = resolve_context_designator(
&declaration.context_designator,
components,
contexts,
bindings,
) else {
continue;
};
let id = ProviderId::for_component(&component.id, &declaration.name);
let semantic_id = id.as_semantic_id().clone();
let Some(value_expression) = expression_graph.root_for(&semantic_id).cloned() else {
continue;
};
declarations.push((component, declaration, context, id, value_expression));
}
}
let mut counts = BTreeMap::<(SemanticId, ContextId), usize>::new();
for (component, _, context, _, _) in &declarations {
*counts
.entry((component.id.clone(), context.clone()))
.or_default() += 1;
}
for (component, declaration, context, id, value_expression) in declarations {
if counts[&(component.id.clone(), context.clone())] > 1 {
duplicates.push(DuplicateProviderDeclaration {
owner: component.id.clone(),
context,
retained_provider: id,
rejected_field: declaration.authored_field.clone(),
provenance: declaration.provenance.clone(),
});
continue;
}
let semantic_id = id.as_semantic_id().clone();
providers.insert(
id.clone(),
ProviderEntity {
declared_type_id: SemanticTypeId::for_subject(&semantic_id),
id,
owner: SemanticOwner::entity(component.id.clone()),
authored_field: declaration.authored_field.clone(),
name: declaration.name.clone(),
context_designator: declaration.context_designator.clone(),
context,
declared_type: declaration.declared_type.clone(),
value_expression,
execution_boundary: ExecutionBoundary::Client,
provenance: declaration.provenance.clone(),
},
);
}
(providers, duplicates)
}
#[cfg(test)]
mod tests {
use crate::{
build_application_semantic_model, build_application_semantic_model_for_unit,
CompilationUnit, ContextId, ProviderId, SemanticReferenceKind,
};
#[test]
fn lowers_same_component_provider_with_one_context_relation() {
let parsed = presolve_parser::parse_file(
"src/AppShell.tsx",
r#"
@component("x-app-shell")
class AppShell extends Component {
@context()
theme!: Theme;
@provide(AppShell.theme)
providedTheme: Theme = this.selectedTheme;
render() { return <main />; }
}
"#,
);
let asm = build_application_semantic_model(&parsed);
let component = &asm.components[0];
let id = ProviderId::for_component(&component.id, "providedTheme");
let provider = asm.provider(&id).expect("provider");
assert_eq!(asm.providers().len(), 1);
assert_eq!(
provider.context,
ContextId::for_component(&component.id, "theme")
);
assert_eq!(
asm.expression_owner(&provider.value_expression),
Some(id.as_semantic_id())
);
assert!(asm.references.iter().any(|reference| {
reference.kind == SemanticReferenceKind::ProvidesContext
&& reference.source == *id.as_semantic_id()
&& reference.target == *provider.context.as_semantic_id()
}));
assert_eq!(
asm.semantic_type_of(provider.id.as_semantic_id()),
Some(&crate::SemanticType::Unknown)
);
let diagnostics = crate::validate_application_semantic_model(&asm);
assert!(diagnostics.is_empty(), "{diagnostics:#?}");
}
#[test]
fn resolves_imported_context_owners_without_runtime_lookup() {
let unit = CompilationUnit::parse_sources([
(
"src/app-shell.tsx",
r#"
@component("x-app-shell")
class AppShell extends Component {
@context()
theme!: Theme;
render() { return <main />; }
}
export { AppShell };
"#,
),
(
"src/theme-boundary.tsx",
r#"
import { AppShell } from "./app-shell";
@component("x-theme-boundary")
class ThemeBoundary extends Component {
@provide(AppShell.theme)
providedTheme: Theme = this.theme;
render() { return <main />; }
}
"#,
),
]);
let asm = build_application_semantic_model_for_unit(&unit);
assert_eq!(asm.providers().len(), 1);
assert_eq!(
asm.providers()[0].context.as_str(),
"module:src/app-shell.tsx/component:x-app-shell/context:theme"
);
}
#[test]
fn retains_duplicate_providers_without_selecting_an_executable_winner() {
let parsed = presolve_parser::parse_file(
"src/AppShell.tsx",
r#"
@component("x-app-shell")
class AppShell extends Component {
@context()
theme: string;
@provide(AppShell.theme)
primary: string = "one";
@provide(AppShell.theme)
secondary: string = "two";
render() { return <main />; }
}
"#,
);
let asm = build_application_semantic_model(&parsed);
assert!(asm.providers().is_empty());
assert_eq!(asm.duplicate_provider_declarations.len(), 2);
assert_eq!(
asm.context_declaration_candidates()
.invalid_candidates()
.len(),
2
);
}
#[test]
fn retains_the_single_structurally_resolved_provider_before_expression_validation() {
let parsed = presolve_parser::parse_file(
"src/AppShell.tsx",
r#"
@component("x-app-shell")
class AppShell extends Component {
@context()
theme: string;
@provide("theme")
stringTarget: string = "light";
@provide(AppShell.theme)
missingType = "light";
@provide(AppShell.theme)
missingValue!: string;
@provide(AppShell.theme)
static staticProvider: string = "light";
@provide(AppShell.theme)
callProvider: string = createTheme();
@provide(AppShell.notAContext)
wrongTarget: string = "light";
render() { return <main />; }
}
"#,
);
let asm = build_application_semantic_model(&parsed);
let component = &asm.components[0];
let provider_id = ProviderId::for_component(&component.id, "callProvider");
assert_eq!(asm.contexts().len(), 1);
assert_eq!(asm.providers().len(), 1);
assert!(asm.provider(&provider_id).is_some());
assert!(asm.references.iter().any(|reference| {
reference.kind == SemanticReferenceKind::ProvidesContext
&& reference.source == *provider_id.as_semantic_id()
}));
assert!(asm.components[0].state_fields.is_empty());
}
}