Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions libs/pavex_macros/src/prebuilt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ pub struct InputSchema {
pub id: Option<syn::Ident>,
pub clone_if_necessary: Flag,
pub never_clone: Flag,
pub allow: Option<PrebuiltAllows>,
}

#[derive(darling::FromMeta, Debug, Clone)]
pub struct PrebuiltAllows {
unused: Flag,
}

impl TryFrom<InputSchema> for Properties {
Expand All @@ -21,6 +27,7 @@ impl TryFrom<InputSchema> for Properties {
id,
clone_if_necessary,
never_clone,
allow,
} = input;
let Ok(cloning_policy) = CloningPolicyFlags {
clone_if_necessary,
Expand All @@ -32,14 +39,20 @@ impl TryFrom<InputSchema> for Properties {
));
};

Ok(Properties { id, cloning_policy })
let allow_unused = allow.as_ref().map(|a| a.unused.is_present());
Ok(Properties {
id,
cloning_policy,
allow_unused,
})
}
}

#[derive(darling::FromMeta, Debug, Clone, PartialEq, Eq)]
pub struct Properties {
pub id: Option<syn::Ident>,
pub cloning_policy: Option<CloningPolicy>,
pub allow_unused: Option<bool>,
}

pub struct PrebuiltAnnotation;
Expand All @@ -63,7 +76,11 @@ impl TypeAnnotation for PrebuiltAnnotation {
/// Decorate the input with a `#[diagnostic::pavex::prebuilt]` attribute
/// that matches the provided properties.
fn emit(item: TypeItem, properties: Properties) -> Result<AnnotationCodegen, darling::Error> {
let Properties { cloning_policy, id } = properties;
let Properties {
cloning_policy,
id,
allow_unused,
} = properties;

let name = item.name();
// Use the span of the type name if no identifier is provided.
Expand All @@ -89,6 +106,11 @@ fn emit(item: TypeItem, properties: Properties) -> Result<AnnotationCodegen, dar
cloning_policy = #cloning_policy,
});
}
if let Some(allow_unused) = allow_unused {
properties.extend(quote! {
allow_unused = #allow_unused,
});
}

let id_docs = format!(
r#"A strongly-typed id to register [`{name}`] as a prebuilt type for your Pavex application.
Expand Down
6 changes: 5 additions & 1 deletion libs/pavex_test_runner/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,11 @@ debug = "none""##
writeln!(&mut cargo_toml, "resolver = \"3\"").unwrap();
writeln!(&mut cargo_toml, "[workspace.package]\nedition = \"2024\"")?;
writeln!(&mut cargo_toml, "[workspace.dependencies]").unwrap();
writeln!(&mut cargo_toml, "pavex = {{ path = \"../pavex\" }}").unwrap();
writeln!(
&mut cargo_toml,
"pavex = {{ path = \"../pavex\", features = [\"server\"] }}"
)
.unwrap();
writeln!(
&mut cargo_toml,
"pavex_cli_client = {{ path = \"../pavex_cli_client\" }}"
Expand Down
31 changes: 21 additions & 10 deletions libs/pavex_test_runner/src/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,16 @@ impl SnapshotTest {
let actual = actual.replace(&self.app_name_with_hash, "app");

let expected = match fs_err::read_to_string(&self.expectation_path) {
Ok(s) => s,
Err(e) if e.kind() == ErrorKind::NotFound => "".into(),
outcome @ Err(_) => {
outcome.expect("Failed to load the expected value for a snapshot test")
Ok(s) => Some(s),
Err(e) if e.kind() == ErrorKind::NotFound => None,
Err(e) => {
panic!(
"Failed to load the expected value for a snapshot test: {e}"
)
}
};

let expected = Self::sanitize_output(&expected);
let expected = expected.map(|s| Self::sanitize_output(&s));
let actual = Self::sanitize_output(&actual);

let expectation_directory = self.expectation_path.parent().unwrap();
Expand All @@ -45,14 +47,23 @@ impl SnapshotTest {
self.expectation_path.file_name().unwrap().to_string_lossy()
));

if expected != actual {
print_changeset(&expected, &actual);
if let Some(expected) = expected {
if expected != actual {
print_changeset(&expected, &actual);
fs_err::write(last_snapshot_path, actual)
.expect("Failed to save the actual value for a failed snapshot test");
Err(())
} else {
let _ = fs_err::remove_file(last_snapshot_path);
Ok(())
}
} else if actual.is_empty() {
Err(())
} else {
print_changeset("", &actual);
fs_err::write(last_snapshot_path, actual)
.expect("Failed to save the actual value for a failed snapshot test");
Err(())
} else {
let _ = fs_err::remove_file(last_snapshot_path);
Ok(())
}
}

Expand Down
87 changes: 53 additions & 34 deletions libs/pavexc/src/compiler/analyses/unused.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ use crate::compiler::analyses::components::HydratedComponent;
use crate::compiler::analyses::components::{ComponentDb, ComponentId};
use crate::compiler::analyses::computations::ComputationDb;
use crate::compiler::analyses::processing_pipeline::RequestHandlerPipeline;
use crate::compiler::computation::Computation;
use crate::compiler::utils::get_ok_variant;
use crate::diagnostic::{CompilerDiagnostic, DiagnosticSink};
use indexmap::IndexSet;
use miette::Severity;
use pavex_bp_schema::{Lint, LintSetting};

/// Emit a warning for each user-registered constructor that hasn't
use super::components::component::Component;

/// Emit a warning for each user-registered constructor and prebuilt type that hasn't
/// been used in the code-generated pipelines.
pub(crate) fn detect_unused<'a, I>(
pipelines: I,
Expand All @@ -21,11 +22,11 @@ pub(crate) fn detect_unused<'a, I>(
) where
I: Iterator<Item = &'a RequestHandlerPipeline>,
{
// Some user-registered constructors are never used directly—e.g. a constructor with
// Some user-registered components are never used directly—e.g. a constructor with
// unassigned generic parameters.
// We consider the original user-registered constructor as "used" if one of its derivations
// We consider the original user-registered component as "used" if one of its derivations
// is used.
let mut used_user_constructor_ids = IndexSet::new();
let mut used_user_components_ids = IndexSet::new();

let graphs = pipelines
.flat_map(|p| p.graph_iter())
Expand All @@ -36,25 +37,32 @@ pub(crate) fn detect_unused<'a, I>(
let Some(component_id) = node.component_id() else {
continue;
};
let HydratedComponent::Constructor(_) =
component_db.hydrated_component(component_id, computation_db)
else {
continue;
};
used_user_constructor_ids.insert(
match component_db.hydrated_component(component_id, computation_db) {
HydratedComponent::Constructor(_) | HydratedComponent::PrebuiltType(_) => {}
_ => {
continue;
}
}
used_user_components_ids.insert(
component_db
.derived_from(&component_id)
.unwrap_or(component_id),
);
}
}

for (id, _) in component_db.constructors(computation_db) {
for (id, c) in component_db.iter() {
if !matches!(
c,
Component::Constructor { .. } | Component::PrebuiltType { .. }
) {
continue;
}
if component_db.derived_from(&id).is_some() || component_db.is_framework_primitive(&id) {
// It's not a user-registered constructor.
// It's not a user-registered component..
continue;
}
if used_user_constructor_ids.contains(&id) {
if used_user_components_ids.contains(&id) {
continue;
}

Expand All @@ -78,34 +86,45 @@ fn emit_unused_warning(
let Some(user_id) = db.user_component_id(id) else {
return;
};
let component_kind = db.user_db()[user_id].kind();
let source = diagnostics.annotated(
db.registration_target(user_id),
"The unused constructor was registered here",
format!("The unused {component_kind} was registered here"),
);
let HydratedComponent::Constructor(constructor) = db.hydrated_component(id, computation_db)
else {
return;
};
let output_type = constructor.output_type();
let output_type = if output_type.is_result() {
get_ok_variant(output_type)
} else {
output_type
};
let Computation::Callable(callable) = &constructor.0 else {
return;
let component = db.hydrated_component(id, computation_db);
let (err_msg, ty_) = match component {
HydratedComponent::Constructor(constructor) => {
let output_type = constructor.output_type();
let ty_ = if output_type.is_result() {
get_ok_variant(output_type)
} else {
output_type
};
let err_msg = format!(
"You registered a constructor for `{}`, but it's never used.",
ty_.display_for_error()
);
(err_msg, ty_.to_owned())
}
HydratedComponent::PrebuiltType(ty_) => {
let err_msg = format!(
"You marked `{}` as prebuilt, but it's never used.",
ty_.display_for_error()
);
(err_msg, ty_.into_owned())
}
_ => unreachable!(),
};
let output_type = output_type.display_for_error();
let error = anyhow::anyhow!(
"You registered a constructor for `{output_type}`, \
but it's never used.\n\
`{}` is never invoked since no component is asking for `{output_type}` to be injected as one of its inputs.",
&callable.path,
"{err_msg}\n\
No component is asking for `{}` to be injected as one of its inputs.",
ty_.display_for_error()
);

let help = if db.registration(user_id).kind.is_blueprint() {
"If you want to ignore this warning, call `.ignore(Lint::Unused)` on the registered constructor."
"If you want to ignore this warning, invoke `.allow(Lint::Unused)` on the component."
} else {
"If you want to ignore this warning, add `allow(unused)` to your constructor attribute."
"If you want to ignore this warning, add `allow(unused)` to the attribute you used to define the component."
}
.to_string();
let builder = CompilerDiagnostic::builder(error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,22 @@ pub(crate) fn resolve_annotation_coordinates(
}

// Retrieve prebuilt properties for prebuilt types that have been registered directly against the blueprint
if let AnnotationProperties::Prebuilt { cloning_policy, .. } = &annotation.properties {
if let AnnotationProperties::Prebuilt {
cloning_policy,
allow_unused,
id: _,
} = &annotation.properties
{
assert!(matches!(
aux.component_interner[component_id],
UserComponent::PrebuiltType { .. }
));

let lints = aux.id2lints.entry(component_id).or_default();
if let Some(true) = allow_unused {
lints.entry(Lint::Unused).or_insert(LintSetting::Allow);
}

// Use the behaviour specified in the annotation, unless the user has overridden
// it when registering the prebuilt directly with the blueprint.
aux.id2cloning_policy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ fn intern_annotated(
}
AnnotationProperties::Prebuilt {
cloning_policy,
allow_unused,
id: _,
} => {
let prebuilt = UserComponent::PrebuiltType { source };
Expand All @@ -404,6 +405,20 @@ fn intern_annotated(
cloning_policy.unwrap_or(CloningPolicy::NeverClone),
);

let lints = aux.id2lints.entry(prebuilt_id).or_default();

if let Some(true) = allow_unused {
lints.insert(Lint::Unused, LintSetting::Allow);
} else if !krate_collection
.package_graph()
.metadata(&krate.core.package_id)
.unwrap()
.in_workspace()
{
// Ignore unused prebuilt types imported from crates defined outside the current workspace
lints.insert(Lint::Unused, LintSetting::Allow);
}

let ty = annotated_item2type(item, krate, krate_collection, diagnostics)?;
match PrebuiltType::new(ty) {
Ok(prebuilt) => {
Expand Down
1 change: 1 addition & 0 deletions libs/pavexc_attr_parser/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ pub enum AnnotationProperties {
},
Prebuilt {
id: String,
allow_unused: Option<bool>,
cloning_policy: Option<CloningPolicy>,
},
Config {
Expand Down
2 changes: 2 additions & 0 deletions libs/pavexc_attr_parser/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,13 +205,15 @@ impl From<RouteProperties> for AnnotationProperties {
/// `pavex::diagnostic::prebuilt`.
pub struct PrebuiltProperties {
pub id: String,
pub allow_unused: Option<bool>,
pub cloning_policy: Option<CloningPolicy>,
}

impl From<PrebuiltProperties> for AnnotationProperties {
fn from(value: PrebuiltProperties) -> Self {
AnnotationProperties::Prebuilt {
id: value.id,
allow_unused: value.allow_unused,
cloning_policy: value.cloning_policy.map(Into::into),
}
}
Expand Down
2 changes: 1 addition & 1 deletion libs/ui_tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ resolver = "3"
[workspace.package]
edition = "2024"
[workspace.dependencies]
pavex = { path = "../pavex" }
pavex = { path = "../pavex", features = ["server"] }
pavex_cli_client = { path = "../pavex_cli_client" }
workspace_hack = { path = "workspace_hack" }
reqwest = "0.12"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
WARNING:
⚠ You registered a constructor for `app::Unused`, but it's never
│ used.
│ `app::Unused::new` is never invoked since no component is asking
│ for `app::Unused` to be injected as one of its inputs.
│ No component is asking for `app::Unused` to be injected as one of
│ its inputs.
│
│ ╭─[blueprint/constructors/a_warning_is_emitted_for_unused_constructors/src/lib.rs:6:1]
│ 6 │ impl Unused {
Expand All @@ -11,5 +11,5 @@
│ · ╰──── The unused constructor was registered here
│ 9 │ todo!()
│ ╰────
│ help: If you want to ignore this warning, add `allow(unused)` to your
│ constructor attribute.
│ help: If you want to ignore this warning, add `allow(unused)` to the
│ attribute you used to define the component.
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ digraph "* * - 1" {
}
digraph app_state {
0 [ label = "0| crate::ApplicationState() -> crate::ApplicationState"]
}
}
Loading