commit 4a84e6ec6a9e6a008977fa48c761d479285c495c
parent 0a7fb27cbcc4c2114f63423d777737d7119d6196
Author: Zack Newman <zack@philomathiclife.com>
Date: Fri, 6 Sep 2024 18:22:34 -0600
lint reasons
Diffstat:
5 files changed, 114 insertions(+), 66 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
@@ -9,14 +9,14 @@ license = "MIT OR Apache-2.0"
name = "ci"
readme = "README.md"
repository = "https://git.philomathiclife.com/repos/ci/"
-rust-version = "1.77.0"
-version = "0.1.0"
+rust-version = "1.81.0"
+version = "0.1.1"
[badges]
maintenance = { status = "actively-developed" }
[dependencies]
-serde = { version = "1.0.208", default-features = false }
+serde = { version = "1.0.210", default-features = false }
toml = { version = "0.8.19", default-features = false, features = ["parse"] }
[profile.release]
diff --git a/README.md b/README.md
@@ -33,3 +33,17 @@ When `clippy`, `doc_tests`, and `tests` are not passed; then all three are invok
## Limitations
Any use of `compile_error` _not_ related to incompatible features will be silently ignored.
+
+## License
+
+Licensed under either of
+
+* Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0).
+* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT).
+
+at your option.
+
+## Contribution
+
+Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you,
+as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
diff --git a/src/args.rs b/src/args.rs
@@ -1,15 +1,16 @@
use super::E;
-use core::fmt::{self, Display, Formatter};
+use core::{
+ error::Error,
+ fmt::{self, Display, Formatter},
+};
use std::{
collections::HashSet,
- env,
- error::Error,
- fs,
+ env, fs,
path::PathBuf,
process::{Command, Stdio},
};
/// Error returned when parsing arguments passed to the application.
-#[allow(clippy::module_name_repetitions)]
+#[expect(clippy::module_name_repetitions, reason = "prefer this name")]
#[derive(Clone, Debug)]
pub enum ArgsErr {
/// Error when no arguments exist.
@@ -22,7 +23,6 @@ pub enum ArgsErr {
MissingPath,
}
impl Display for ArgsErr {
- #[allow(clippy::ref_patterns)]
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
Self::NoArgs => write!(f, "no arguments exist including the name of the process itself"),
@@ -141,7 +141,7 @@ impl Opts {
///
/// `self` is mutated iff `Self::None(_, false)` and `cargo t -q --doc` errors
/// due to a lack of library target; in which case, the second `bool` becomes `true`.
- #[allow(clippy::too_many_lines)]
+ #[expect(clippy::too_many_lines, reason = "not too many")]
pub fn run_cmd(
&mut self,
features: &str,
@@ -263,7 +263,7 @@ impl Opts {
}
}
/// Returns `Opts` and the directory `ci` should run in based on arguments passed to the application.
- #[allow(clippy::redundant_else)]
+ #[expect(clippy::redundant_else, reason = "when else-if is used, prefer else")]
pub fn from_args() -> Result<(Self, Option<PathBuf>), E> {
let mut args = env::args();
if args.next().is_none() {
diff --git a/src/main.rs b/src/main.rs
@@ -2,6 +2,9 @@
//!
//! `ci` is a CLI application that runs [`cargo`](https://doc.rust-lang.org/cargo/index.html) with
//! `clippy -q`, `t -q --doc`, and `t -q --tests` with all possible combinations of features defined in `Cargo.toml`.
+//! Additionally `ci` always updates the dependencies in `Cargo.toml` to the newest stable version even if such
+//! version is not [SemVer](https://semver.org/) compatible. If any dependencies are upgraded, then they will
+//! be written to `stderr`. This occurs before any other command is performed.
//!
//! `ci` avoids superfluous combinations of features. For example if feature `foo` automatically enables
//! feature `bar` and `bar` automatically enables feature `fizz`, then no combination of features that contains
@@ -45,6 +48,7 @@
rust_2024_compatibility,
unsafe_code,
unused,
+ unused_crate_dependencies,
warnings,
clippy::all,
clippy::cargo,
@@ -57,15 +61,17 @@
clippy::style,
clippy::suspicious
)]
-#![allow(
+#![expect(
clippy::blanket_clippy_restriction_lints,
clippy::implicit_return,
clippy::min_ident_chars,
clippy::missing_trait_methods,
clippy::question_mark_used,
+ clippy::ref_patterns,
clippy::single_call_fn,
clippy::single_char_lifetime_names,
- clippy::unseparated_literal_suffix
+ clippy::unseparated_literal_suffix,
+ reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs"
)]
extern crate alloc;
/// Functionality related to the passed arguments.
@@ -74,13 +80,15 @@ mod args;
mod manifest;
use alloc::string::FromUtf8Error;
use args::{ArgsErr, Opts, Success};
-use core::fmt::{self, Display, Formatter};
+use core::{
+ error::Error,
+ fmt::{self, Display, Formatter},
+};
use std::{
collections::HashSet,
- env,
- error::Error,
- fs,
+ env, fs,
io::{self, Write},
+ process::{Command, Stdio},
};
use toml::de::Error as TomlErr;
/// Application error.
@@ -101,7 +109,6 @@ enum E {
NoLibraryTargets,
}
impl Display for E {
- #[allow(clippy::ref_patterns)]
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
Self::Args(ref err) => err.fmt(f),
@@ -119,52 +126,76 @@ impl fmt::Debug for E {
}
}
impl Error for E {}
+#[expect(
+ clippy::panic_in_result_fn,
+ reason = "asserts are fine when they indicate a bug"
+)]
fn main() -> Result<(), E> {
Opts::from_args().and_then(|(mut opt, path)| {
path.map_or(Ok(()), |p| env::set_current_dir(p).map_err(E::Io)).and_then(|()| {
- fs::read_to_string("Cargo.toml")
- .map_err(E::Io)
- .and_then(|toml| manifest::from_toml(toml.as_str()).map_err(E::Toml))
- .and_then(|features_powerset| {
- features_powerset
- .into_iter()
- .try_fold(HashSet::new(), |mut msgs, features| {
- opt.run_cmd(features.as_str(), &mut msgs)
- .and_then(|success| {
- if matches!(success, Success::NoLibraryTargets) {
- // We don't want to bother continuing to call `cargo t -q --doc` once we
- // know there is no library target.
- assert!(matches!(opt, Opts::DocTests(_)), "Opts::DocTests should be the only variant that can return Success::NoLibraryTargets when Opts::run_cmd is called");
- Err(E::NoLibraryTargets)
- } else {
- Ok(msgs)
- }
- })
- })
- .or_else(|e| {
- if matches!(e, E::NoLibraryTargets) {
- Ok(HashSet::new())
- } else {
- Err(e)
- }
- })
- .and_then(|msgs| {
- if msgs.is_empty() {
- Ok(())
- } else {
- io::stderr()
- .lock()
- .write_all(
- msgs.into_iter()
- .fold(String::new(), |mut buffer, msg| {
- buffer.push_str(msg.as_str());
- buffer
- })
- .as_bytes(),
- )
- .map_err(E::Io)
- }
+ Command::new("cargo").stderr(Stdio::piped()).stdin(Stdio::null()).stdout(Stdio::piped()).args(["upgrade", "-i", "allow"]).output().map_err(E::Io).and_then(|output| {
+ if output.status.success() {
+ if output.stdout.is_empty() {
+ Ok(())
+ } else {
+ String::from_utf8(output.stdout).map_err(E::Utf8).and_then(|out| {
+ io::stderr()
+ .lock()
+ .write_all(out.as_bytes())
+ .map_err(E::Io)
})
+ }
+ } else {
+ String::from_utf8(output.stderr).map_err(E::Utf8).and_then(|mut err| {
+ err.push_str("\ncargo upgrade -i allow");
+ Err(E::Cmd(err))
+ })
+ }
+ }).and_then(|()| {
+ fs::read_to_string("Cargo.toml")
+ .map_err(E::Io)
+ .and_then(|toml| manifest::from_toml(toml.as_str()).map_err(E::Toml))
+ .and_then(|features_powerset| {
+ features_powerset
+ .into_iter()
+ .try_fold(HashSet::new(), |mut msgs, features| {
+ opt.run_cmd(features.as_str(), &mut msgs)
+ .and_then(|success| {
+ if matches!(success, Success::NoLibraryTargets) {
+ // We don't want to bother continuing to call `cargo t -q --doc` once we
+ // know there is no library target.
+ assert!(matches!(opt, Opts::DocTests(_)), "Opts::DocTests should be the only variant that can return Success::NoLibraryTargets when Opts::run_cmd is called");
+ Err(E::NoLibraryTargets)
+ } else {
+ Ok(msgs)
+ }
+ })
+ })
+ .or_else(|e| {
+ if matches!(e, E::NoLibraryTargets) {
+ Ok(HashSet::new())
+ } else {
+ Err(e)
+ }
+ })
+ .and_then(|msgs| {
+ if msgs.is_empty() {
+ Ok(())
+ } else {
+ io::stderr()
+ .lock()
+ .write_all(
+ msgs.into_iter()
+ .fold(String::new(), |mut buffer, msg| {
+ buffer.push_str(msg.as_str());
+ buffer
+ })
+ .as_bytes(),
+ )
+ .map_err(E::Io)
+ }
+ })
+ })
})
})
})
diff --git a/src/manifest.rs b/src/manifest.rs
@@ -59,12 +59,13 @@ impl<'de> Deserialize<'de> for ImpliedFeatures {
/// Features with the features that are enabled.
struct Features(HashMap<String, ImpliedFeatures>);
impl Features {
- #[allow(
+ #[expect(
clippy::arithmetic_side_effects,
clippy::as_conversions,
clippy::cast_possible_truncation,
clippy::indexing_slicing,
- clippy::unreachable
+ clippy::unreachable,
+ reason = "comments in code explain their correctness"
)]
/// Returns all possible combinations of features.
fn powerset(self) -> Vec<String> {
@@ -237,14 +238,14 @@ impl<'de> Deserialize<'de> for Manifest {
impl<'f> Visitor<'f> for FieldVisitor {
type Value = Field;
fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
- formatter.write_str("'features'")
+ write!(formatter, "'{FEATURES}'")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: Error,
{
Ok(match v {
- "features" => Field::Features,
+ FEATURES => Field::Features,
_ => Field::Other,
})
}
@@ -257,7 +258,7 @@ impl<'de> Deserialize<'de> for Manifest {
match key {
Field::Features => {
if features.is_some() {
- return Err(Error::duplicate_field("features"));
+ return Err(Error::duplicate_field(FEATURES));
}
features = Some(map.next_value()?);
}
@@ -269,10 +270,12 @@ impl<'de> Deserialize<'de> for Manifest {
))
}
}
+ /// `features`.
+ const FEATURES: &str = "features";
deserializer.deserialize_map(ManifestVisitor)
}
}
-/// Returns all possible combinations of features.
+/// Returns possible combinations of features.
pub fn from_toml(val: &str) -> Result<Vec<String>, TomlErr> {
toml::from_str::<Manifest>(val).map(|man| man.0.powerset())
}