diff --git a/.cargo/config.toml b/.cargo/config.toml index 3327840..fabd056 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -4,105 +4,3 @@ rustdocflags = [ "-Arustdoc::private_intra_doc_links", "-Drustdoc::broken_intra_doc_links", ] - -[target.'cfg(all())'] -rustflags = [ - # https://doc.rust-lang.org/rustc/lints/index.html - "-Wfuture_incompatible", - "-Wlet_underscore", - "-Wnonstandard-style", - "-Wrust_2018_compatibility", - "-Wrust_2018_idioms", - "-Wrust_2021_compatibility", - "-Wtrivial_casts", - "-Wtrivial_numeric_casts", - "-Wunsafe_code", - "-Wunused", - "-Wunused_import_braces", - "-Wunused_lifetimes", - "-Wunused_macro_rules", - "-Wunused_qualifications", - "-Wunused_tuple_struct_fields", - "-Wwarnings", - - # This list is based off Embarks clippy list - # https://github.com/EmbarkStudios/rust-ecosystem/blob/main/lints.rs - # - # You can lookup the motivation for each clippy here: - # https://rust-lang.github.io/rust-clippy/master/index.html - # - # The following lints are pending implementation, as they cause significant code churn - # - # "-Wclippy::string_lit_as_bytes", - # - # We also excluded the lint `clippy::map_unwrap_or` as it considers the pattern `map_or_else(.., ..)` more readable than `map(...).unwrap_or(..)` - # We do not. - - "-Aclippy::doc_markdown", - "-Wclippy::await_holding_lock", - "-Wclippy::char_lit_as_u8", - "-Wclippy::checked_conversions", - "-Wclippy::dbg_macro", - "-Wclippy::debug_assert_with_mut_call", - "-Wclippy::disallowed_macros", - "-Wclippy::disallowed_methods", - "-Wclippy::disallowed_types", - "-Wclippy::empty_enum", - "-Wclippy::enum_glob_use", - "-Wclippy::exit", - "-Wclippy::explicit_deref_methods", - "-Wclippy::explicit_into_iter_loop", - "-Wclippy::expl_impl_clone_on_copy", - "-Wclippy::fallible_impl_from", - "-Wclippy::filter_map_next", - "-Wclippy::flat_map_option", - "-Wclippy::float_cmp_const", - "-Wclippy::fn_params_excessive_bools", - "-Wclippy::from_iter_instead_of_collect", - "-Wclippy::if_let_mutex", - "-Wclippy::implicit_clone", - "-Wclippy::imprecise_flops", - "-Wclippy::inefficient_to_string", - "-Wclippy::invalid_upcast_comparisons", - "-Wclippy::large_digit_groups", - "-Wclippy::large_stack_arrays", - "-Wclippy::large_types_passed_by_value", - "-Wclippy::let_unit_value", - "-Wclippy::linkedlist", - "-Wclippy::lossy_float_literal", - "-Wclippy::macro_use_imports", - "-Wclippy::manual_ok_or", - "-Wclippy::map_flatten", - "-Wclippy::match_on_vec_items", - "-Wclippy::match_same_arms", - "-Wclippy::match_wildcard_for_single_variants", - "-Wclippy::match_wild_err_arm", - "-Wclippy::mem_forget", - "-Wclippy::mismatched_target_os", - "-Wclippy::missing_enforced_import_renames", - "-Wclippy::mutex_integer", - "-Wclippy::mut_mut", - "-Wclippy::needless_continue", - "-Wclippy::needless_for_each", - "-Wclippy::needless_pass_by_value", - "-Wclippy::option_option", - "-Wclippy::path_buf_push_overwrite", - "-Wclippy::ptr_as_ptr", - "-Wclippy::rc_mutex", - "-Wclippy::ref_option_ref", - "-Wclippy::rest_pat_in_fully_bound_structs", - "-Wclippy::same_functions_in_if_condition", - "-Wclippy::semicolon_if_nothing_returned", - "-Wclippy::single_match_else", - "-Wclippy::string_add", - "-Wclippy::string_add_assign", - "-Wclippy::string_to_string", - "-Wclippy::todo", - "-Wclippy::trait_duplication_in_bounds", - "-Wclippy::unimplemented", - "-Wclippy::unnested_or_patterns", - "-Wclippy::unused_self", - "-Wclippy::useless_transmute", - "-Wclippy::verbose_file_reads", - "-Wclippy::zero_sized_map_values", -] diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f251587..520292d 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -30,7 +30,7 @@ jobs: timeout-minutes: 45 steps: - uses: actions/checkout@v3 - - run: cargo clippy --workspace --all-targets --all-features + - run: cargo clippy --workspace --all-targets --all-features -- -Dwarnings format: runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index 6f792eb..fb2aab7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,48 @@ resolver = "2" chrono-tz = { version = "0.9.0", default-features = false, features = ["std"] } serde_json = { version = "1.0.117", default-features = false } serde = { version = "1.0.203", features = ["derive"] } + +# use only "allow" and "warn" for lints (both rustc and clippy) +# the Github CI task will fail on warnings but +# we only want the warnings during local development +[workspace.lints.rust] +# Lint groups are set to warn so new lints are used as they become available +future_incompatible = { level = "warn", priority = -1 } +let_underscore = { level = "warn", priority = -1 } +nonstandard-style = { level = "warn", priority = -1 } +rust_2018_compatibility = { level = "warn", priority = -1 } +rust_2018_idioms = { level = "warn", priority = -1 } +rust_2021_compatibility = { level = "warn", priority = -1 } +unused = { level = "warn", priority = -1 } +warnings = { level = "warn", priority = -1 } + +# 2024 compatibility is allow for now and will be fixed in a near-future PR +rust_2024_compatibility = { level = "allow", priority = -2 } + +# We also warn on a set of individual lints that are ont included in any group +async_fn_in_trait = "warn" +dead_code = "warn" +trivial_casts = "warn" +trivial_numeric_casts = "warn" +unsafe_code = "warn" +unused_import_braces = "warn" +unused_lifetimes = "warn" +unused_macro_rules = "warn" +unused_qualifications = "warn" + +[workspace.lints.clippy] +# Lint groups are set to warn so new lints are used as they become available +complexity = { level = "warn", priority = -1 } +correctness = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } +perf = { level = "warn", priority = -1 } +style = { level = "warn", priority = -1 } +suspicious = { level = "warn", priority = -1 } + +# These lints are explicitly allowed. +missing_errors_doc = "allow" # the Error type is self documenting +map_unwrap_or = "allow" # we prefer to `map(a).unwrap_or(b)` as it's clear what the fallback value is + +# These lints are allowed, but we want to deny them over time +missing_panics_doc = "allow" +module_name_repetitions = "allow" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index f12dbc5..b3191fa 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -19,3 +19,6 @@ console = { version = "0.15.8" } ocpi-tariffs = { version = "0.6.1", path = "../ocpi-tariffs", features = ["ocpi-v211"] } serde_json.workspace = true serde.workspace = true + +[lints] +workspace = true diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 77b3d9c..f12c01f 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -180,6 +180,7 @@ pub struct Validate { } impl Validate { + #[allow(clippy::too_many_lines)] fn run(self) -> Result<()> { let (report, cdr, _) = self.args.load_all()?; @@ -314,7 +315,7 @@ impl Validate { table.retain_rows(|v| !v[1].is_empty() || !v[2].is_empty()); - println!("{}", table); + println!("{table}"); if !is_valid { println!( @@ -323,13 +324,13 @@ impl Validate { ); exit(1); - } else { - println!( - "Calculation {} all totals in the CDR.\n", - style("matches").green().bold() - ); } + println!( + "Calculation {} all totals in the CDR.\n", + style("matches").green().bold() + ); + Ok(()) } } @@ -365,7 +366,7 @@ impl Analyze { "Flat", ]); - for period in report.periods.iter() { + for period in &report.periods { let start_time = period.start_date_time.with_timezone(&time_zone); let dim = &period.dimensions; @@ -379,7 +380,7 @@ impl Analyze { ]); table.row(&[ - "".to_string(), + String::new(), "Price".to_string(), to_string_or_default(dim.energy.price.map(|p| p.price)), to_string_or_default(dim.time.price.map(|p| p.price)), @@ -396,11 +397,11 @@ impl Analyze { report.total_energy.to_string(), report.total_time.to_string(), report.total_parking_time.to_string(), - "".to_string(), + String::new(), ]); table.row(&[ - "".to_string(), + String::new(), "Price".to_string(), to_string_or_default(report.total_energy_cost.map(|p| p.excl_vat)), to_string_or_default(report.total_time_cost.map(|p| p.excl_vat)), @@ -408,7 +409,7 @@ impl Analyze { to_string_or_default(report.total_fixed_cost.map(|p| p.excl_vat)), ]); - println!("{}", table); + println!("{table}"); Ok(()) } @@ -499,7 +500,7 @@ impl Display for Table { write!(f, "|")?; for (value, &width) in row.iter().zip(&self.widths) { - write!(f, " {0: <1$} |", value, width)?; + write!(f, " {value: , /// End time of day, for example 19:45, valid until this - /// time of the day. Same syntax as start_time + /// time of the day. Same syntax as `start_time` pub end_time: Option, /// Start date, for example: 2015-12-24, valid from this day diff --git a/ocpi-tariffs/src/ocpi/v221/tariff.rs b/ocpi-tariffs/src/ocpi/v221/tariff.rs index b91c13c..ad099d7 100644 --- a/ocpi-tariffs/src/ocpi/v221/tariff.rs +++ b/ocpi-tariffs/src/ocpi/v221/tariff.rs @@ -48,10 +48,10 @@ pub struct OcpiPriceComponent { /// Optionally specify a VAT percentage for this component. pub vat: CompatibilityVat, - /// Minimum amount to be billed. This unit will be billed in this step_size - /// blocks. For example: if type is time and step_size is 300, then time will + /// Minimum amount to be billed. This unit will be billed in this `step_size` + /// blocks. For example: if type is time and `step_size` is 300, then time will /// be billed in blocks of 5 minutes, so if 6 minutes is used, 10 minutes (2 - /// blocks of step_size) will be billed + /// blocks of `step_size`) will be billed pub step_size: u64, } @@ -104,13 +104,13 @@ pub struct OcpiTariffElement { #[derive(Debug, Copy, PartialEq, Eq, Clone, Hash, Deserialize, Serialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum TariffDimensionType { - /// Defined in kWh, step_size multiplier: 1 Wh + /// Defined in kWh, `step_size` multiplier: 1 Wh Energy, - /// Flat fee, no unit for step_size + /// Flat fee, no unit for `step_size` Flat, - /// Time not charging: defined in hours, step_size multiplier: 1 second + /// Time not charging: defined in hours, `step_size` multiplier: 1 second ParkingTime, - /// Time charging: defined in hours, step_size multiplier: 1 second + /// Time charging: defined in hours, `step_size` multiplier: 1 second Time, } @@ -122,7 +122,7 @@ pub struct OcpiTariffRestriction { pub start_time: Option, /// End time of day, for example 19:45, valid until this - /// time of the day. Same syntax as start_time + /// time of the day. Same syntax as `start_time` pub end_time: Option, /// Start date, for example: 2015-12-24, valid from this day diff --git a/ocpi-tariffs/src/pricer.rs b/ocpi-tariffs/src/pricer.rs index 029f54e..a00baaf 100644 --- a/ocpi-tariffs/src/pricer.rs +++ b/ocpi-tariffs/src/pricer.rs @@ -47,6 +47,7 @@ pub struct Pricer<'a> { impl<'a> Pricer<'a> { /// Create a new pricer instance using the specified [`Cdr`]. + #[must_use] pub fn new(cdr: &'a Cdr) -> Self { Self { cdr, @@ -57,6 +58,7 @@ impl<'a> Pricer<'a> { } /// Use a list of [`OcpiTariff`]'s for pricing instead of the tariffs found in the [`Cdr`]. + #[must_use] pub fn with_tariffs(mut self, tariffs: impl IntoIterator) -> Self { self.tariffs = Some(tariffs.into_iter().collect()); @@ -65,6 +67,7 @@ impl<'a> Pricer<'a> { /// Directly specify a time zone to use for the calculation. This overrides any time zones in /// the session or any detected time zones if [`Self::detect_time_zone`] is set to true. + #[must_use] pub fn with_time_zone(mut self, time_zone: Tz) -> Self { self.time_zone = Some(time_zone); @@ -75,6 +78,7 @@ impl<'a> Pricer<'a> { /// is missing. The detection will only succeed if the country has just one time-zone, /// nonetheless there are edge cases where the detection will be incorrect. Only use this /// feature as a fallback when a certain degree of inaccuracy is allowed. + #[must_use] pub fn detect_time_zone(mut self, detect: bool) -> Self { self.detect_time_zone = detect; @@ -83,6 +87,7 @@ impl<'a> Pricer<'a> { /// Attempt to apply the first applicable tariff to the charge session and build a report /// containing the results. + #[allow(clippy::too_many_lines)] pub fn build_report(self) -> Result { let cdr_tz = self.cdr.cdr_location.time_zone.as_ref(); @@ -452,6 +457,7 @@ impl PeriodReport { } /// The total cost of all dimensions in this period. + #[must_use] pub fn cost(&self) -> Option { [ self.dimensions.time.cost(), diff --git a/ocpi-tariffs/src/session.rs b/ocpi-tariffs/src/session.rs index 2b0c00f..d918944 100644 --- a/ocpi-tariffs/src/session.rs +++ b/ocpi-tariffs/src/session.rs @@ -169,7 +169,7 @@ impl PeriodData { energy: None, }; - for dimension in period.dimensions.iter() { + for dimension in &period.dimensions { match *dimension { OcpiCdrDimension::MinCurrent(volume) => inst.min_current = Some(volume), OcpiCdrDimension::MaxCurrent(volume) => inst.max_current = Some(volume), diff --git a/ocpi-tariffs/src/tariff.rs b/ocpi-tariffs/src/tariff.rs index f2ad40f..fe69aca 100644 --- a/ocpi-tariffs/src/tariff.rs +++ b/ocpi-tariffs/src/tariff.rs @@ -35,7 +35,7 @@ impl Tariff { pub fn active_components(&self, period: &ChargePeriod) -> PriceComponents { let mut components = PriceComponents::new(); - for tariff_element in self.elements.iter() { + for tariff_element in &self.elements { if !tariff_element.is_active(period) { continue; } @@ -90,7 +90,7 @@ impl TariffElement { let mut components = PriceComponents::new(); - for ocpi_component in ocpi_element.price_components.iter() { + for ocpi_component in &ocpi_element.price_components { let price_component = PriceComponent::new(ocpi_component, element_index); match ocpi_component.component_type { @@ -110,7 +110,7 @@ impl TariffElement { } pub fn is_active(&self, period: &ChargePeriod) -> bool { - for restriction in self.restrictions.iter() { + for restriction in &self.restrictions { if !restriction.instant_validity_exclusive(&period.start_instant) { return false; } @@ -126,7 +126,7 @@ impl TariffElement { // use this in the future to validate if a period is still valid when it ends. #[allow(dead_code)] pub fn is_active_at_end(&self, period: &ChargePeriod) -> bool { - for restriction in self.restrictions.iter() { + for restriction in &self.restrictions { if !restriction.instant_validity_inclusive(&period.end_instant) { return false; } diff --git a/ocpi-tariffs/src/types/electricity.rs b/ocpi-tariffs/src/types/electricity.rs index 65e7826..7f7d63a 100644 --- a/ocpi-tariffs/src/types/electricity.rs +++ b/ocpi-tariffs/src/types/electricity.rs @@ -15,11 +15,13 @@ impl Kwh { } /// Saturating addition + #[must_use] pub fn saturating_add(self, other: Self) -> Self { Self(self.0.saturating_add(other.0)) } /// Saturating subtraction + #[must_use] pub fn saturating_sub(self, other: Self) -> Self { Self(self.0.saturating_sub(other.0)) } @@ -36,6 +38,7 @@ impl Kwh { } /// Round this number to the OCPI specified amount of decimals. + #[must_use] pub fn with_scale(self) -> Self { Self(self.0.with_scale()) } diff --git a/ocpi-tariffs/src/types/money.rs b/ocpi-tariffs/src/types/money.rs index 2f5632e..d862857 100644 --- a/ocpi-tariffs/src/types/money.rs +++ b/ocpi-tariffs/src/types/money.rs @@ -11,7 +11,7 @@ pub struct Price { #[serde(default)] /// The price including VAT. /// - /// If no vat is applicable this value will be equal to the excl_vat. + /// If no vat is applicable this value will be equal to the `excl_vat`. /// /// If no vat could be determined (tariff is 2.1.1) this value will be `None`. pub incl_vat: Option, @@ -26,6 +26,7 @@ impl Price { } /// Round this number to the OCPI specified amount of decimals. + #[must_use] pub fn with_scale(self) -> Self { Self { excl_vat: self.excl_vat.with_scale(), @@ -34,6 +35,7 @@ impl Price { } /// Saturating addition. + #[must_use] pub fn saturating_add(self, rhs: Self) -> Self { Self { excl_vat: self.excl_vat.saturating_add(rhs.excl_vat), @@ -64,36 +66,43 @@ impl Money { } /// Round this number to the OCPI specified amount of decimals. + #[must_use] pub fn with_scale(self) -> Self { Self(self.0.with_scale()) } /// Saturating addition + #[must_use] pub fn saturating_add(self, other: Self) -> Self { Self(self.0.saturating_add(other.0)) } /// Saturating subtraction + #[must_use] pub fn saturating_sub(self, other: Self) -> Self { Self(self.0.saturating_sub(other.0)) } /// Saturating multiplication + #[must_use] pub fn saturating_mul(self, other: Self) -> Self { Self(self.0.saturating_mul(other.0)) } /// Apply a VAT percentage to this monetary amount. + #[must_use] pub fn apply_vat(self, vat: Vat) -> Self { Self(self.0.saturating_mul(vat.as_fraction())) } /// Cost of a certain amount of [`Kwh`] with this price. + #[must_use] pub fn kwh_cost(self, kwh: Kwh) -> Self { Self(self.0.saturating_mul(kwh.into())) } /// Cost of a certain amount of [`HoursDecimal`] with this price. + #[must_use] pub fn time_cost(self, hours: HoursDecimal) -> Self { Self(self.0.saturating_mul(hours.as_num_hours_number())) } diff --git a/ocpi-tariffs/src/types/time.rs b/ocpi-tariffs/src/types/time.rs index 30d7239..d2ce1f9 100644 --- a/ocpi-tariffs/src/types/time.rs +++ b/ocpi-tariffs/src/types/time.rs @@ -46,7 +46,7 @@ impl Display for HoursDecimal { let minutes = (duration.num_seconds() / SECS_IN_MIN) % MINS_IN_HOUR; let hours = duration.num_seconds() / (SECS_IN_MIN * MINS_IN_HOUR); - write!(f, "{:0>2}:{:0>2}:{:0>2}", hours, minutes, seconds) + write!(f, "{hours:0>2}:{minutes:0>2}:{seconds:0>2}") } } @@ -74,6 +74,7 @@ impl HoursDecimal { } /// Convert into decimal representation. + #[must_use] pub fn as_num_hours_decimal(&self) -> rust_decimal::Decimal { self.as_num_hours_number().into() } @@ -100,12 +101,12 @@ impl HoursDecimal { )) } - /// Saturating subtraction. + #[must_use] pub fn saturating_sub(self, other: Self) -> Self { Self(self.0.checked_sub(&other.0).unwrap_or_else(Duration::zero)) } - /// Saturating addition. + #[must_use] pub fn saturating_add(self, other: Self) -> Self { Self( self.0 diff --git a/ocpi-tariffs/tests/integration.rs b/ocpi-tariffs/tests/integration.rs index de2932b..d6c1fb3 100644 --- a/ocpi-tariffs/tests/integration.rs +++ b/ocpi-tariffs/tests/integration.rs @@ -5,6 +5,7 @@ use ocpi_tariffs::{ }; use std::path::Path; +#[allow(clippy::needless_pass_by_value)] #[test_each::file(glob = "ocpi-tariffs/test_data/*/cdr*.json", name(segments = 2))] fn test_json(cdr: &str, path: &Path) { let tariff = std::fs::read_to_string(path.parent().unwrap().join("tariff.json")).unwrap(); diff --git a/scripts/ci.sh b/scripts/ci.sh new file mode 100755 index 0000000..8896a0e --- /dev/null +++ b/scripts/ci.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +set -e + +export CARGO_TARGET_DIR="target" +export RUST_BACKTRACE="full" + +cargo deny --workspace --all-features -L info check +cargo check --workspace --all-features --verbose +cargo fmt --all --check +cargo clippy --workspace --all-features --all-targets +cargo doc --workspace --all-features --no-deps --document-private-items +cargo test --workspace --all-features --verbose