diff --git a/src/tools/features-status-dump/Cargo.toml b/src/tools/features-status-dump/Cargo.toml index b2976f14a01a4..be9f7b8dea89a 100644 --- a/src/tools/features-status-dump/Cargo.toml +++ b/src/tools/features-status-dump/Cargo.toml @@ -1,12 +1,13 @@ [package] name = "features-status-dump" +description = "Dumps info about rustc's features to JSON." version = "0.1.0" license = "MIT OR Apache-2.0" -edition = "2021" +edition = "2024" [dependencies] anyhow = { version = "1" } clap = { version = "4", features = ["derive"] } -serde = { version = "1.0.125", features = [ "derive" ] } +serde = { version = "1.0.125", features = ["derive"] } serde_json = "1.0.59" tidy = { path = "../tidy", features = ["build-metrics"] } diff --git a/src/tools/features-status-dump/src/display.rs b/src/tools/features-status-dump/src/display.rs new file mode 100644 index 0000000000000..78b5a9ae79a23 --- /dev/null +++ b/src/tools/features-status-dump/src/display.rs @@ -0,0 +1,83 @@ +use std::collections::HashMap; +use std::fmt::Display; + +use tidy::features::Feature; + +use crate::FeaturesStatus; + +// Newtype because orphan rule + +enum Source { + Library, + Compiler, +} + +pub struct SourcedFeature { + source: Source, + pub feature: Feature, +} + +// A new struct is defined rather than changing the old one, to keep serialization behaving the same. +pub struct NewFeaturesStatus { + features: HashMap, + compare: for<'a, 'b> fn( + &'a (&String, &SourcedFeature), + &'b (&String, &SourcedFeature), + ) -> std::cmp::Ordering, +} + +impl Display for Source { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let string = match self { + Source::Library => "[Library]", + Source::Compiler => "[Compiler]", + }; + f.pad(string) + } +} + +impl Display for NewFeaturesStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut sorted = self.features.iter().collect::>(); + sorted.sort_by(self.compare); + for (name, feature) in sorted { + write!(f, "{:w_tag$}", feature.source, w_tag = 10)?; + write!(f, " {:w_name$}", name, w_name = 40)?; + write!(f, " is {:w_status$}", feature.feature.level, w_status = 10)?; + if let Some(since) = feature.feature.since { + write!(f, " since {:w_since$}", since, w_since = 7)?; + } + if let Some(issue) = &feature.feature.tracking_issue { + let link = format!("", issue); + write!(f, " {: fn( + &'a (&String, &SourcedFeature), + &'b (&String, &SourcedFeature), + ) -> std::cmp::Ordering, + ) -> Self { + let compiler_features = value + .lang_features_status + .into_iter() + .map(|(name, feature)| (name, SourcedFeature { feature, source: Source::Compiler })); + let library_features = value + .lib_features_status + .into_iter() + .map(|(name, feature)| (name, SourcedFeature { feature, source: Source::Library })); + let features = compiler_features.chain(library_features).collect(); + + Self { features, compare } + } +} diff --git a/src/tools/features-status-dump/src/err.rs b/src/tools/features-status-dump/src/err.rs new file mode 100644 index 0000000000000..9b106b5cdb606 --- /dev/null +++ b/src/tools/features-status-dump/src/err.rs @@ -0,0 +1,21 @@ +#[derive(Debug)] +pub(crate) enum DumpError { + NoSources, + NoVersions, +} + +impl std::fmt::Display for DumpError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let msg = match self { + DumpError::NoSources => { + "No feature sources given. Specify at least one with --library-path or --compiler-path" + } + DumpError::NoVersions => { + "Empty version range. first_version is older than last_version." + } + }; + f.pad(msg) + } +} + +impl std::error::Error for DumpError {} diff --git a/src/tools/features-status-dump/src/main.rs b/src/tools/features-status-dump/src/main.rs index a4f88362ab816..ffba13bf3e425 100644 --- a/src/tools/features-status-dump/src/main.rs +++ b/src/tools/features-status-dump/src/main.rs @@ -1,54 +1,140 @@ +use std::cmp::Ordering; +// For behaviour, see parse.rs use std::collections::HashMap; use std::fs::File; -use std::io::BufWriter; -use std::path::PathBuf; +use std::io::{self, BufWriter}; use anyhow::{Context, Result}; -use clap::Parser; use tidy::diagnostics::RunningCheck; -use tidy::features::{Feature, collect_lang_features, collect_lib_features}; - -#[derive(Debug, Parser)] -struct Cli { - /// Path to `library/` directory. - #[arg(long)] - library_path: PathBuf, - /// Path to `compiler/` directory. - #[arg(long)] - compiler_path: PathBuf, - /// Path to `output/` directory. - #[arg(long)] - output_path: PathBuf, -} +use tidy::features::{Feature, Status, collect_lang_features, collect_lib_features}; + +use crate::display::{NewFeaturesStatus, SourcedFeature}; +use crate::parse::{Cli, Tristate}; + +mod display; +mod err; +mod parse; +// Placing this into a structure makes it easier to serialize #[derive(Debug, serde::Serialize)] struct FeaturesStatus { - lang_features_status: HashMap, - lib_features_status: HashMap, + pub(crate) lang_features_status: HashMap, + pub(crate) lib_features_status: HashMap, } fn main() -> Result<()> { - let Cli { compiler_path, library_path, output_path } = Cli::parse(); + let args = crate::parse::parse()?; + + let lang_features_status: HashMap<_, _> = args + .compiler_path + .iter() + .flat_map(|compiler_path| { + collect_lang_features(&compiler_path, &mut RunningCheck::new_noop()) + }) + .filter(|(_, feature)| include(feature, &args)) + .collect(); - let lang_features_status = collect_lang_features(&compiler_path, &mut RunningCheck::new_noop()); - let lib_features_status = collect_lib_features(&library_path) - .into_iter() + let lib_features_status = args + .library_path + .iter() + .flat_map(|library_path| collect_lib_features(&library_path).into_iter()) + // The library contains less info on their features. Prefer the ones found in the compiler. .filter(|&(ref name, _)| !lang_features_status.contains_key(name)) + .filter(|(_, feature)| include(feature, &args)) .collect(); + let features_status = FeaturesStatus { lang_features_status, lib_features_status }; - let output_dir = output_path.parent().with_context(|| { - format!("failed to get parent dir of output path `{}`", output_path.display()) - })?; - std::fs::create_dir_all(output_dir).with_context(|| { - format!("failed to create output directory at `{}`", output_dir.display()) - })?; - - let output_file = File::create(&output_path).with_context(|| { - format!("failed to create file at given output path `{}`", output_path.display()) - })?; - let writer = BufWriter::new(output_file); - serde_json::to_writer_pretty(writer, &features_status) - .context("failed to write json output")?; + match &args.output_path { + Some(output_path) => { + let output_dir = output_path.parent().with_context(|| { + format!("failed to get parent dir of output path `{}`", output_path.display()) + })?; + std::fs::create_dir_all(output_dir).with_context(|| { + format!("failed to create output directory at `{}`", output_dir.display()) + })?; + + let output_file = File::create(&output_path).with_context(|| { + format!("failed to create file at given output path `{}`", output_path.display()) + })?; + let writer = BufWriter::new(output_file); + write_output(writer, features_status, &args)?; + } + None => { + let writer = BufWriter::new(std::io::stdout()); + write_output(writer, features_status, &args)?; + } + }; Ok(()) } + +fn write_output(mut writer: W, features_status: FeaturesStatus, args: &Cli) -> Result<()> +where + W: io::Write, +{ + match args.format { + parse::Format::JSON => serde_json::to_writer_pretty(writer, &features_status) + .context("failed to write json output"), + parse::Format::Text => { + let compare: for<'a, 'b> fn( + &'a (&String, &SourcedFeature), + &'b (&String, &SourcedFeature), + ) -> Ordering = match args.sort_by { + parse::SortBy::Newest => |a, b| compare_ascending(a, b).reverse(), + parse::SortBy::Oldest => compare_ascending, + }; + let new_features = NewFeaturesStatus::new(features_status, compare); + + write!(writer, "{}", new_features).map_err(Into::into) + } + } +} + +fn compare_ascending<'a, 'b>( + a: &'a (&String, &SourcedFeature), + b: &'b (&String, &SourcedFeature), +) -> Ordering { + match a.1.feature.since.cmp(&b.1.feature.since) { + Ordering::Equal => (), + other => return other, + }; + a.1.feature.tracking_issue.cmp(&b.1.feature.tracking_issue) +} + +fn include(feature: &Feature, args: &Cli) -> bool { + let accept = match args.accepted { + Tristate::Require => feature.level == Status::Accepted, + Tristate::Allow => true, + Tristate::Deny => feature.level != Status::Accepted, + }; + let remove = match args.removed { + Tristate::Require => feature.level == Status::Removed, + Tristate::Allow => true, + Tristate::Deny => feature.level != Status::Removed, + }; + let unstable = match args.unstable { + Tristate::Require => feature.level == Status::Unstable, + Tristate::Allow => true, + Tristate::Deny => feature.level != Status::Unstable, + }; + let tracking_issue = match args.tracking_issue { + Tristate::Require => feature.tracking_issue.is_some(), + Tristate::Allow => true, + Tristate::Deny => feature.tracking_issue.is_none(), + }; + let since = match args.since { + Tristate::Require => feature.since.is_some(), + Tristate::Allow => true, + Tristate::Deny => feature.since.is_none(), + }; + + let last_version = args + .last_version + .is_none_or(|last_version| feature.since.is_some_and(|version| version <= last_version)); + + let first_version = args + .first_version + .is_none_or(|last_version| feature.since.is_some_and(|version| version >= last_version)); + + accept && remove && unstable && tracking_issue && since && last_version && first_version +} diff --git a/src/tools/features-status-dump/src/parse.rs b/src/tools/features-status-dump/src/parse.rs new file mode 100644 index 0000000000000..7b7cb05d14f63 --- /dev/null +++ b/src/tools/features-status-dump/src/parse.rs @@ -0,0 +1,124 @@ +use std::path::PathBuf; +use std::str::FromStr; + +use anyhow::Result; +use clap::{Parser, ValueEnum}; +use tidy::features::Version; + +use crate::err::DumpError; + +#[derive(Debug, Parser)] +#[command(version, about)] +pub struct Cli { + /// Path to `library/` directory. Use this flag to read features from the standard library. + #[arg(long)] + pub library_path: Option, + /// Path to `compiler/` directory. Use this flag to read language features. + #[arg(long)] + pub compiler_path: Option, + /// Which file to write to. If none, writes to stdout. + #[arg(long)] + pub output_path: Option, + + /// What file format to write to. Text is the human-readable option. + #[arg(long)] + #[arg(default_value = "json")] + pub format: Format, + + /// Which features to show first. Only has effect when `format = text`. + /// Features two features with equal versions are ordered by issue number. + /// Features with no version are considered old. + #[arg(long)] + #[arg(default_value = "newest")] + pub sort_by: SortBy, + + /// How to filter unstable features. + #[arg(long)] + #[arg(default_value = "allow")] + #[arg(conflicts_with_all = ["accepted", "removed"])] + pub unstable: Tristate, + + /// How to filter accepted (stable) features. + #[arg(long)] + #[arg(default_value = "allow")] + #[arg(conflicts_with_all = ["removed", "unstable"])] + pub accepted: Tristate, + + /// How to filter removed features. + #[arg(long)] + #[arg(default_value = "allow")] + #[arg(conflicts_with_all = ["accepted", "unstable"])] + pub removed: Tristate, + + /// How to filter issues with(out) a tracking issue. + #[arg(long)] + #[arg(default_value = "allow")] + pub tracking_issue: Tristate, + + /// How to filter issues with(out) `since` version. + #[arg(long)] + #[arg(default_value = "allow")] + #[arg(conflicts_with_all(["first_version", "last_version"]))] + pub since: Tristate, + + /// Only show features introduced after or in this version (semver triple) + /// Features without known version are filtered out using this flag. + /// Features notated with `Current Version` are considered newer than any concrete semver. + #[arg(long)] + #[arg(value_parser = Version::from_str)] + pub first_version: Option, + + /// Only show features introduced before or in this version (semver triple). + /// Features without known version are filtered out using this flag. + /// Features notated with `Current Version` are considered newer than any concrete semver. + #[arg(long)] + #[arg(value_parser = Version::from_str)] + pub last_version: Option, +} + +#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)] +pub enum Tristate { + /// Only show these features. + Require, + /// Has no effect. + Allow, + /// Do not show these features. + Deny, +} + +#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)] +pub enum Format { + /// Formats into JSON. + /// Contains two objects "lang_features_status" and "lib_features_status", + /// each containing strings (feature names) mapping to tidy::features::Feature objects. + JSON, + /// formats each feature into a line like + /// + /// > [SOURCE] NAME is STATUS since VERSION : DESCRIPTION + /// + /// Leaving out the unknown parts. + Text, +} + +#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)] +pub enum SortBy { + Oldest, + Newest, +} + +pub fn parse() -> Result { + let cli = Cli::parse(); + + if cli.compiler_path == None && cli.library_path == None { + return Err(DumpError::NoSources.into()); + } + + if let Some(first_version) = cli.first_version + && let Some(last_version) = cli.last_version + && first_version > last_version + { + return Err(DumpError::NoVersions.into()); + } + + Ok(cli) +} diff --git a/src/tools/tidy/src/features/version.rs b/src/tools/tidy/src/features/version.rs index 0e0629a48e218..4c9560e2784de 100644 --- a/src/tools/tidy/src/features/version.rs +++ b/src/tools/tidy/src/features/version.rs @@ -37,6 +37,22 @@ impl From for ParseVersionError { } } +impl std::fmt::Display for ParseVersionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let msg = match self { + ParseVersionError::ParseIntError(parse_int_error) => { + &format!("Invalid semver. Part is not an integer: {}", parse_int_error) + } + ParseVersionError::WrongNumberOfParts => { + "Invalid semver. Must contain exactly three parts." + } + }; + f.pad(msg) + } +} + +impl std::error::Error for ParseVersionError {} + impl FromStr for Version { type Err = ParseVersionError;