Skip to content
Open
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
5 changes: 3 additions & 2 deletions src/tools/features-status-dump/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"] }
83 changes: 83 additions & 0 deletions src/tools/features-status-dump/src/display.rs
Original file line number Diff line number Diff line change
@@ -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<String, SourcedFeature>,
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::<Vec<_>>();
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!("<https://github.com/rust-lang/rust/issues/{}>", issue);
write!(f, " {:<w_issue$}", link, w_issue = 49)?;
}
if let Some(description) = &feature.feature.description {
write!(f, ": {}", description)?;
}
writeln!(f)?;
}
std::fmt::Result::Ok(())
}
}

impl NewFeaturesStatus {
pub fn new(
value: FeaturesStatus,
compare: for<'a, 'b> 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 }
}
}
21 changes: 21 additions & 0 deletions src/tools/features-status-dump/src/err.rs
Original file line number Diff line number Diff line change
@@ -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 {}
158 changes: 122 additions & 36 deletions src/tools/features-status-dump/src/main.rs
Original file line number Diff line number Diff line change
@@ -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<String, Feature>,
lib_features_status: HashMap<String, Feature>,
pub(crate) lang_features_status: HashMap<String, Feature>,
pub(crate) lib_features_status: HashMap<String, Feature>,
}

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<W>(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
}
Loading
Loading