ci: Add lint to check if all sub-crates features can be enabled from ruma crate

This commit is contained in:
Kévin Commaille 2024-05-10 17:41:44 +02:00 committed by Kévin Commaille
parent 30701596d0
commit 9b3f4a2c0f
4 changed files with 129 additions and 14 deletions

View File

@ -1,9 +1,14 @@
use std::path::PathBuf; #![allow(clippy::disallowed_types)]
use std::{collections::HashMap, path::PathBuf};
#[cfg(feature = "default")]
use reqwest::blocking::Client; use reqwest::blocking::Client;
use semver::Version; use semver::Version;
use serde::{de::IgnoredAny, Deserialize}; use serde::{de::IgnoredAny, Deserialize};
#[cfg(feature = "default")]
use toml_edit::{value, Document}; use toml_edit::{value, Document};
#[cfg(feature = "default")]
use xshell::{cmd, pushd, read_file, write_file}; use xshell::{cmd, pushd, read_file, write_file};
use crate::{util::ask_yes_no, Metadata, Result}; use crate::{util::ask_yes_no, Metadata, Result};
@ -22,11 +27,51 @@ pub struct Package {
/// The package's manifest path. /// The package's manifest path.
pub manifest_path: PathBuf, pub manifest_path: PathBuf,
/// A map of the package dependencies. /// A list of the package dependencies.
#[serde(default)] #[serde(default)]
pub dependencies: Vec<Dependency>, pub dependencies: Vec<Dependency>,
/// A map of the package features.
#[serde(default)]
pub features: HashMap<String, Vec<String>>,
} }
impl Package {
/// Whether this package has a way to enable the given feature from the given package.
pub fn can_enable_feature(&self, package_name: &str, feature_name: &str) -> bool {
for activated_feature in self.features.values().flatten() {
// Remove optional `dep:` at the start.
let remaining = activated_feature.trim_start_matches("dep:");
// Check that we have the package name.
let Some(remaining) = remaining.strip_prefix(package_name) else {
continue;
};
if remaining.is_empty() {
// The feature only enables the dependency.
continue;
}
// Remove optional `?`.
let remaining = remaining.trim_start_matches('?');
let Some(remaining) = remaining.strip_prefix('/') else {
// This is another package name starting with the same string.
continue;
};
// Finally, only the feature name is remaining.
if remaining == feature_name {
return true;
}
}
false
}
}
#[cfg(feature = "default")]
impl Package { impl Package {
/// Update the version of this crate. /// Update the version of this crate.
pub fn update_version(&mut self, version: &Version, dry_run: bool) -> Result<()> { pub fn update_version(&mut self, version: &Version, dry_run: bool) -> Result<()> {
@ -203,6 +248,7 @@ pub enum DependencyKind {
Build, Build,
} }
#[cfg(feature = "default")]
/// A crate from the `GET /crates/{crate}` endpoint of crates.io. /// A crate from the `GET /crates/{crate}` endpoint of crates.io.
#[derive(Deserialize)] #[derive(Deserialize)]
struct CratesIoCrate { struct CratesIoCrate {

View File

@ -1,15 +1,17 @@
// Triggers at the `#[clap(subcommand)]` line, but not easily reproducible outside this crate. // Triggers at the `#[clap(subcommand)]` line, but not easily reproducible outside this crate.
#![allow(unused_qualifications)] #![allow(unused_qualifications)]
use std::path::PathBuf; use std::path::Path;
use clap::{Args, Subcommand}; use clap::{Args, Subcommand};
use xshell::pushd; use xshell::pushd;
use crate::{cmd, Metadata, Result, NIGHTLY}; use crate::{cmd, Metadata, Result, NIGHTLY};
mod reexport_features;
mod spec_links; mod spec_links;
use reexport_features::check_reexport_features;
use spec_links::check_spec_links; use spec_links::check_spec_links;
const MSRV: &str = "1.75"; const MSRV: &str = "1.75";
@ -66,6 +68,8 @@ pub enum CiCmd {
Dependencies, Dependencies,
/// Check spec links point to a recent version (lint) /// Check spec links point to a recent version (lint)
SpecLinks, SpecLinks,
/// Check all cargo features of sub-crates can be enabled from ruma (lint)
ReexportFeatures,
/// Check typos /// Check typos
Typos, Typos,
} }
@ -75,18 +79,22 @@ pub struct CiTask {
/// Which command to run. /// Which command to run.
cmd: Option<CiCmd>, cmd: Option<CiCmd>,
/// The root of the workspace. /// The metadata of the workspace.
project_root: PathBuf, project_metadata: Metadata,
} }
impl CiTask { impl CiTask {
pub(crate) fn new(cmd: Option<CiCmd>) -> Result<Self> { pub(crate) fn new(cmd: Option<CiCmd>) -> Result<Self> {
let project_root = Metadata::load()?.workspace_root; let project_metadata = Metadata::load()?;
Ok(Self { cmd, project_root }) Ok(Self { cmd, project_metadata })
}
fn project_root(&self) -> &Path {
&self.project_metadata.workspace_root
} }
pub(crate) fn run(self) -> Result<()> { pub(crate) fn run(self) -> Result<()> {
let _p = pushd(&self.project_root)?; let _p = pushd(self.project_root())?;
match self.cmd { match self.cmd {
Some(CiCmd::Msrv) => self.msrv()?, Some(CiCmd::Msrv) => self.msrv()?,
@ -110,7 +118,8 @@ impl CiTask {
Some(CiCmd::ClippyAll) => self.clippy_all()?, Some(CiCmd::ClippyAll) => self.clippy_all()?,
Some(CiCmd::Lint) => self.lint()?, Some(CiCmd::Lint) => self.lint()?,
Some(CiCmd::Dependencies) => self.dependencies()?, Some(CiCmd::Dependencies) => self.dependencies()?,
Some(CiCmd::SpecLinks) => check_spec_links(&self.project_root.join("crates"))?, Some(CiCmd::SpecLinks) => check_spec_links(&self.project_root().join("crates"))?,
Some(CiCmd::ReexportFeatures) => check_reexport_features(&self.project_metadata)?,
Some(CiCmd::Typos) => self.typos()?, Some(CiCmd::Typos) => self.typos()?,
None => { None => {
self.msrv() self.msrv()
@ -301,9 +310,11 @@ impl CiTask {
// Check dependencies being sorted // Check dependencies being sorted
let dependencies_res = self.dependencies(); let dependencies_res = self.dependencies();
// Check that all links point to the same version of the spec // Check that all links point to the same version of the spec
let spec_links_res = check_spec_links(&self.project_root.join("crates")); let spec_links_res = check_spec_links(&self.project_root().join("crates"));
// Check that all cargo features of sub-crates can be enabled from ruma.
let reexport_features_res = check_reexport_features(&self.project_metadata);
dependencies_res.and(spec_links_res) dependencies_res.and(spec_links_res).and(reexport_features_res)
} }
/// Check the sorting of dependencies with the nightly version. /// Check the sorting of dependencies with the nightly version.

View File

@ -0,0 +1,54 @@
use crate::{Metadata, Result};
/// Check that the ruma crate allows to enable all the features of the other ruma-* crates.
///
/// For simplicity, this function assumes that:
///
/// - Those dependencies are not renamed.
/// - ruma does not use `default-features = false` on those dependencies.
///
/// This does not check if all features are re-exported individually, as that is not always wanted.
pub(crate) fn check_reexport_features(metadata: &Metadata) -> Result<()> {
println!("Checking all features can be enabled from ruma…");
let mut n_errors = 0;
let Some(ruma) = metadata.find_package("ruma") else {
return Err("ruma package not found in workspace".into());
};
for package in ruma.dependencies.iter().filter_map(|dep| metadata.find_package(&dep.name)) {
println!("Checking features of {}", package.name);
// Exclude ruma and xtask.
if !package.name.starts_with("ruma-") {
continue;
}
// Filter features that are enabled by other features of the same package.
let features = package.features.keys().filter(|feature_name| {
!package.features.values().flatten().any(|activated_feature| {
activated_feature.trim_start_matches("dep:") == *feature_name
})
});
for feature_name in features {
// Let's assume that ruma never has `default-features = false`.
if feature_name == "default" {
continue;
}
if !ruma.can_enable_feature(&package.name, feature_name) {
println!(r#" Missing feature "{}/{feature_name}""#, package.name);
n_errors += 1;
}
}
}
if n_errors > 0 {
// Visual aid to separate the end error message.
println!();
return Err(format!("Found {n_errors} missing features").into());
}
Ok(())
}

View File

@ -17,7 +17,6 @@ use serde_json::from_str as from_json_str;
// Keep in sync with version in `rust-toolchain.toml` and `.github/workflows/ci.yml` // Keep in sync with version in `rust-toolchain.toml` and `.github/workflows/ci.yml`
const NIGHTLY: &str = "nightly-2024-02-14"; const NIGHTLY: &str = "nightly-2024-02-14";
#[cfg(feature = "default")]
mod cargo; mod cargo;
mod ci; mod ci;
mod doc; mod doc;
@ -26,6 +25,7 @@ mod release;
#[cfg(feature = "default")] #[cfg(feature = "default")]
mod util; mod util;
use cargo::Package;
use ci::{CiArgs, CiTask}; use ci::{CiArgs, CiTask};
use doc::DocTask; use doc::DocTask;
#[cfg(feature = "default")] #[cfg(feature = "default")]
@ -70,8 +70,7 @@ fn main() -> Result<()> {
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
struct Metadata { struct Metadata {
pub workspace_root: PathBuf, pub workspace_root: PathBuf,
#[cfg(feature = "default")] pub packages: Vec<Package>,
pub packages: Vec<cargo::Package>,
} }
impl Metadata { impl Metadata {
@ -80,6 +79,11 @@ impl Metadata {
let metadata_json = cmd!("cargo metadata --no-deps --format-version 1").read()?; let metadata_json = cmd!("cargo metadata --no-deps --format-version 1").read()?;
Ok(from_json_str(&metadata_json)?) Ok(from_json_str(&metadata_json)?)
} }
/// Find the package with the given name.
pub fn find_package(&self, name: &str) -> Option<&Package> {
self.packages.iter().find(|p| p.name == name)
}
} }
#[cfg(feature = "default")] #[cfg(feature = "default")]