use std::{collections::BTreeMap, sync::Arc};
use ron::extensions::Extensions;
use serde::{Deserialize, Serialize};
use trustfall::{FieldValue, TransparentValue};
use crate::ReleaseType;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum RequiredSemverUpdate {
#[serde(alias = "minor")]
Minor,
#[serde(alias = "major")]
Major,
}
impl RequiredSemverUpdate {
pub fn as_str(&self) -> &'static str {
match self {
Self::Major => "major",
Self::Minor => "minor",
}
}
}
impl From<RequiredSemverUpdate> for ReleaseType {
fn from(value: RequiredSemverUpdate) -> Self {
match value {
RequiredSemverUpdate::Major => Self::Major,
RequiredSemverUpdate::Minor => Self::Minor,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum LintLevel {
#[serde(alias = "allow")]
Allow,
#[serde(alias = "warn")]
Warn,
#[serde(alias = "deny")]
Deny,
}
impl LintLevel {
pub fn as_str(self) -> &'static str {
match self {
LintLevel::Allow => "allow",
LintLevel::Warn => "warn",
LintLevel::Deny => "deny",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ActualSemverUpdate {
Major,
Minor,
Patch,
NotChanged,
}
impl ActualSemverUpdate {
pub(crate) fn supports_requirement(&self, required: RequiredSemverUpdate) -> bool {
match (*self, required) {
(ActualSemverUpdate::Major, _) => true,
(ActualSemverUpdate::Minor, RequiredSemverUpdate::Major) => false,
(ActualSemverUpdate::Minor, _) => true,
(_, _) => false,
}
}
}
impl From<ReleaseType> for ActualSemverUpdate {
fn from(value: ReleaseType) -> Self {
match value {
ReleaseType::Major => Self::Major,
ReleaseType::Minor => Self::Minor,
ReleaseType::Patch => Self::Patch,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum WitnessPurpose {
#[default]
ConsistencyCheck,
RequiredForCorrectness,
}
impl WitnessPurpose {
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::ConsistencyCheck => "consistency_check",
Self::RequiredForCorrectness => "required_for_correctness",
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SemverQuery {
pub id: String,
pub(crate) human_readable_name: String,
pub description: String,
pub required_update: RequiredSemverUpdate,
pub lint_level: LintLevel,
#[serde(default)]
pub reference: Option<String>,
#[serde(default)]
pub reference_link: Option<String>,
pub(crate) query: String,
#[serde(default)]
pub(crate) arguments: BTreeMap<String, TransparentValue>,
pub(crate) error_message: String,
#[serde(default)]
pub(crate) per_result_error_template: Option<String>,
#[serde(default)]
pub witness: Option<Witness>,
}
impl SemverQuery {
pub fn from_ron_str(query_text: &str) -> ron::Result<Self> {
let mut deserializer = ron::Deserializer::from_str_with_options(
query_text,
&ron::Options::default().with_default_extension(Extensions::IMPLICIT_SOME),
)?;
Self::deserialize(&mut deserializer)
}
pub fn all_queries() -> BTreeMap<String, SemverQuery> {
let mut queries = BTreeMap::default();
for (id, query_text) in get_queries() {
let query = Self::from_ron_str(query_text).unwrap_or_else(|e| {
panic!(
"\
Failed to parse a query: {e}
```ron
{query_text}
```"
);
});
assert_eq!(id, query.id, "Query id must match file name");
let id_conflict = queries.insert(query.id.clone(), query);
assert!(id_conflict.is_none(), "{id_conflict:?}");
}
queries
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct QueryOverride {
#[serde(default)]
pub required_update: Option<RequiredSemverUpdate>,
#[serde(default)]
pub lint_level: Option<LintLevel>,
}
pub type OverrideMap = BTreeMap<String, QueryOverride>;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct OverrideStack(Vec<OverrideMap>);
impl OverrideStack {
#[must_use]
pub fn new() -> Self {
Self(Vec::new())
}
pub fn push(&mut self, item: &OverrideMap) {
self.0.push(item.clone());
}
#[must_use]
pub fn effective_lint_level(&self, query: &SemverQuery) -> LintLevel {
self.0
.iter()
.rev()
.find_map(|x| x.get(&query.id).and_then(|y| y.lint_level))
.unwrap_or(query.lint_level)
}
#[must_use]
pub fn effective_required_update(&self, query: &SemverQuery) -> RequiredSemverUpdate {
self.0
.iter()
.rev()
.find_map(|x| x.get(&query.id).and_then(|y| y.required_update))
.unwrap_or(query.required_update)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Witness {
#[serde(default)]
pub purpose: WitnessPurpose,
pub hint_template: String,
#[serde(default)]
pub witness_template: Option<String>,
#[serde(default)]
pub witness_query: Option<WitnessQuery>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WitnessQuery {
pub query: String,
#[serde(default)]
pub arguments: BTreeMap<Arc<str>, InheritedValue>,
}
impl WitnessQuery {
pub fn inherit_arguments_from(
&self,
source_map: &BTreeMap<std::sync::Arc<str>, FieldValue>,
) -> anyhow::Result<BTreeMap<Arc<str>, FieldValue>> {
let mut mapped = BTreeMap::new();
for (key, value) in self.arguments.iter() {
let mapped_value = match value {
InheritedValue::Inherited { inherit } => source_map
.get(inherit.as_str())
.cloned()
.ok_or(anyhow::anyhow!(
"inherited output key `{inherit}` does not exist in {source_map:?}"
))?,
InheritedValue::Constant(value) => value.clone().into(),
};
mapped.insert(Arc::clone(key), mapped_value);
}
Ok(mapped)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged, deny_unknown_fields)]
pub enum InheritedValue {
Inherited { inherit: String },
Constant(TransparentValue),
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use std::collections::{BTreeSet, HashMap};
use std::ffi::OsStr;
use std::path::PathBuf;
use std::sync::{Arc, OnceLock};
use std::time::SystemTime;
use std::{collections::BTreeMap, path::Path};
use anyhow::Context;
use fs_err::PathExt;
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use toml::Value;
use trustfall::{FieldValue, TransparentValue};
use trustfall_core::ir::IndexedQuery;
use trustfall_rustdoc::{
VersionedIndex, VersionedRustdocAdapter, VersionedStorage, load_rustdoc,
};
use crate::query::{
InheritedValue, LintLevel, OverrideMap, OverrideStack, QueryOverride, RequiredSemverUpdate,
SemverQuery,
};
use crate::templating::make_handlebars_registry;
static TEST_CRATE_NAMES: OnceLock<Vec<String>> = OnceLock::new();
static TEST_CRATE_RUSTDOCS: OnceLock<BTreeMap<String, (VersionedStorage, VersionedStorage)>> =
OnceLock::new();
static TEST_CRATE_INDEXES: OnceLock<
BTreeMap<String, (VersionedIndex<'static>, VersionedIndex<'static>)>,
> = OnceLock::new();
fn get_test_crate_names() -> &'static [String] {
TEST_CRATE_NAMES.get_or_init(initialize_test_crate_names)
}
fn get_all_test_crates() -> &'static BTreeMap<String, (VersionedStorage, VersionedStorage)> {
TEST_CRATE_RUSTDOCS.get_or_init(initialize_test_crate_rustdocs)
}
#[test]
fn lint_files_have_matching_ids() {
let lints_dir = Path::new("src/lints");
let ron_files = collect_ron_files(lints_dir);
assert!(
!ron_files.is_empty(),
"expected at least one lint definition in {lints_dir:?}"
);
for path in ron_files {
let stem = path
.file_stem()
.and_then(OsStr::to_str)
.expect("lint file must have a valid UTF-8 stem");
assert!(
is_lower_snake_case(stem),
"lint file stem `{stem}` must be lower snake case"
);
let contents = fs_err::read_to_string(&path).expect("failed to read lint file");
let query = SemverQuery::from_ron_str(&contents).expect("failed to parse lint");
assert_eq!(
stem, query.id,
"lint id must match file stem for {:?}",
path
);
}
}
fn collect_ron_files(dir: &Path) -> Vec<PathBuf> {
let mut result = Vec::new();
let mut stack = vec![dir.to_path_buf()];
while let Some(current) = stack.pop() {
for entry in fs_err::read_dir(¤t).expect("failed to read directory") {
let entry = entry.expect("failed to read directory entry");
let path = entry.path();
if entry
.file_type()
.expect("failed to determine file type")
.is_dir()
{
stack.push(path);
} else if path.extension() == Some(OsStr::new("ron")) {
result.push(path);
}
}
}
result.sort();
result
}
fn is_lower_snake_case(value: &str) -> bool {
!value.is_empty()
&& value.chars().all(|ch| ch.is_ascii_lowercase() || ch == '_')
&& !value.starts_with('_')
&& !value.ends_with('_')
&& !value.contains("__")
}
fn get_all_test_crate_indexes()
-> &'static BTreeMap<String, (VersionedIndex<'static>, VersionedIndex<'static>)> {
TEST_CRATE_INDEXES.get_or_init(initialize_test_crate_indexes)
}
fn get_test_crate_indexes(
test_crate: &str,
) -> &'static (VersionedIndex<'static>, VersionedIndex<'static>) {
&get_all_test_crate_indexes()[test_crate]
}
fn initialize_test_crate_names() -> Vec<String> {
std::fs::read_dir("./test_crates/")
.expect("directory test_crates/ not found")
.map(|dir_entry| dir_entry.expect("failed to list test_crates/"))
.filter(|dir_entry| {
if !dir_entry
.metadata()
.expect("failed to retrieve test_crates/* metadata")
.is_dir()
{
return false;
}
let mut test_crate_cargo_toml = dir_entry.path();
test_crate_cargo_toml.extend(["old", "Cargo.toml"]);
test_crate_cargo_toml.as_path().is_file()
})
.map(|dir_entry| {
String::from(
String::from(
dir_entry
.path()
.to_str()
.expect("failed to convert dir_entry to String"),
)
.strip_prefix("./test_crates/")
.expect(
"the dir_entry doesn't start with './test_crates/', which is unexpected",
),
)
})
.collect()
}
fn initialize_test_crate_rustdocs() -> BTreeMap<String, (VersionedStorage, VersionedStorage)> {
get_test_crate_names()
.par_iter()
.map(|crate_pair| {
let old_rustdoc = load_pregenerated_rustdoc(crate_pair.as_str(), "old");
let new_rustdoc = load_pregenerated_rustdoc(crate_pair, "new");
(crate_pair.clone(), (old_rustdoc, new_rustdoc))
})
.collect()
}
fn initialize_test_crate_indexes()
-> BTreeMap<String, (VersionedIndex<'static>, VersionedIndex<'static>)> {
get_all_test_crates()
.par_iter()
.map(|(key, (old_crate, new_crate))| {
let old_index = VersionedIndex::from_storage(old_crate);
let new_index = VersionedIndex::from_storage(new_crate);
(key.clone(), (old_index, new_index))
})
.collect()
}
fn load_pregenerated_rustdoc(crate_pair: &str, crate_version: &str) -> VersionedStorage {
let rustdoc_path =
format!("./localdata/test_data/{crate_pair}/{crate_version}/rustdoc.json");
let metadata_path =
format!("./localdata/test_data/{crate_pair}/{crate_version}/metadata.json");
let metadata_text = std::fs::read_to_string(&metadata_path).map_err(|e| anyhow::anyhow!(e).context(
format!("Could not load {metadata_path} file. These files are newly required as of PR#1007. Please re-run ./scripts/regenerate_test_rustdocs.sh"))).expect("failed to load metadata");
let metadata = serde_json::from_str(&metadata_text).expect("failed to parse metadata file");
load_rustdoc(Path::new(&rustdoc_path), Some(metadata))
.with_context(|| format!("Could not load {rustdoc_path} file, did you forget to run ./scripts/regenerate_test_rustdocs.sh ?"))
.expect("failed to load rustdoc")
}
#[derive(Debug, PartialEq, Eq)]
struct PackageManifest {
name: String,
version: String,
edition: String,
}
fn load_package_manifest(manifest_dir: &Path) -> PackageManifest {
let manifest_path = manifest_dir.join("Cargo.toml");
let manifest_text =
fs_err::read_to_string(&manifest_path).expect("failed to load manifest for test crate");
let manifest: Value = toml::from_str(&manifest_text)
.unwrap_or_else(|e| panic!("failed to parse {}: {e}", manifest_path.display()));
let package_table = manifest
.get("package")
.and_then(Value::as_table)
.unwrap_or_else(|| {
panic!(
"manifest at {} missing [package] table",
manifest_path.display()
)
});
let name = package_table
.get("name")
.and_then(Value::as_str)
.unwrap_or_else(|| {
panic!(
"manifest at {} missing package.name",
manifest_path.display()
)
})
.to_owned();
let version = package_table
.get("version")
.and_then(Value::as_str)
.unwrap_or_else(|| {
panic!(
"manifest at {} missing package.version",
manifest_path.display()
)
})
.to_owned();
let edition = package_table
.get("edition")
.and_then(Value::as_str)
.unwrap_or_else(|| {
panic!(
"manifest at {} missing package.edition",
manifest_path.display()
)
})
.to_owned();
let publish_value = package_table.get("publish").unwrap_or_else(|| {
panic!(
"manifest at {} missing package.publish",
manifest_path.display()
)
});
assert!(
matches!(publish_value, Value::Boolean(false)),
"manifest at {} must set package.publish = false",
manifest_path.display()
);
PackageManifest {
name,
version,
edition,
}
}
const VERSION_MISMATCH_ALLOWED: &[&str] = &[
"semver_trick_self_referential",
"trait_missing_with_major_bump",
];
#[test]
fn test_crates_have_consistent_manifests() {
let base_path = Path::new("./test_crates");
let entries = fs_err::read_dir(base_path).expect("directory test_crates/ not found");
let mut checked_pairs = 0usize;
for entry in entries {
let entry = entry.expect("failed to read test_crates entry");
let path = entry.path();
if !entry
.metadata()
.expect("failed to read metadata for test_crates entry")
.is_dir()
{
continue;
}
let old_dir = path.join("old");
let new_dir = path.join("new");
let old_dir_manifest = old_dir.join("Cargo.toml");
let new_dir_manifest = new_dir.join("Cargo.toml");
if !(old_dir.is_dir()
&& new_dir.is_dir()
&& old_dir_manifest.is_file()
&& new_dir_manifest.is_file())
{
continue;
}
let dir_name = path
.file_name()
.and_then(|name| name.to_str())
.expect("test_crate directory must be valid UTF-8");
let old_manifest = load_package_manifest(&old_dir);
let new_manifest = load_package_manifest(&new_dir);
let PackageManifest {
name: old_name,
version: old_version,
edition: old_edition,
} = old_manifest;
let PackageManifest {
name: new_name,
version: new_version,
edition: new_edition,
} = new_manifest;
assert_eq!(
old_name, dir_name,
"manifest name must match directory name for {dir_name}"
);
assert_eq!(
new_name, dir_name,
"manifest name must match directory name for {dir_name}"
);
assert_eq!(
old_edition, new_edition,
"old and new editions differ for {dir_name}"
);
if !VERSION_MISMATCH_ALLOWED.contains(&dir_name) {
assert_eq!(
old_version, new_version,
"old and new versions differ for {dir_name}"
);
}
checked_pairs += 1;
}
assert!(
checked_pairs > 0,
"expected to check at least one test crate pair"
);
}
#[test]
fn all_queries_are_valid() {
let (_baseline, current) = get_test_crate_indexes("template");
let adapter =
VersionedRustdocAdapter::new(current, Some(current)).expect("failed to create adapter");
for semver_query in SemverQuery::all_queries().into_values() {
let _ = adapter
.run_query(&semver_query.query, semver_query.arguments)
.expect("not a valid query");
}
}
#[test]
fn pub_use_handling() {
let (_baseline, current) = get_test_crate_indexes("pub_use_handling");
let query = r#"
{
Crate {
item {
... on Struct {
name @filter(op: "=", value: ["$struct"])
canonical_path {
canonical_path: path @output
}
importable_path @fold {
path @output
}
}
}
}
}"#;
let mut arguments = BTreeMap::new();
arguments.insert("struct", "CheckPubUseHandling");
let adapter =
VersionedRustdocAdapter::new(current, None).expect("could not create adapter");
let results_iter = adapter
.run_query(query, arguments)
.expect("failed to run query");
let actual_results: Vec<BTreeMap<_, _>> = results_iter
.map(|res| res.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
.collect();
let expected_result: FieldValue =
vec!["pub_use_handling", "inner", "CheckPubUseHandling"].into();
assert_eq!(1, actual_results.len(), "{actual_results:?}");
assert_eq!(
expected_result, actual_results[0]["canonical_path"],
"{actual_results:?}"
);
let mut actual_paths = actual_results[0]["path"]
.as_vec_with(|val| val.as_vec_with(FieldValue::as_str))
.expect("not a Vec<Vec<&str>>");
actual_paths.sort_unstable();
let expected_paths = vec![
vec!["pub_use_handling", "CheckPubUseHandling"],
vec!["pub_use_handling", "inner", "CheckPubUseHandling"],
];
assert_eq!(expected_paths, actual_paths);
}
type TestOutput = BTreeMap<String, Vec<BTreeMap<String, FieldValue>>>;
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
struct WitnessOutput {
filename: String,
begin_line: usize,
hint: String,
}
impl PartialOrd for WitnessOutput {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for WitnessOutput {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
(&self.filename, self.begin_line).cmp(&(&other.filename, other.begin_line))
}
}
fn pretty_format_output_difference(
query_name: &str,
output_name1: &'static str,
output1: TestOutput,
output_name2: &'static str,
output2: TestOutput,
) -> String {
let output_ron1 =
ron::ser::to_string_pretty(&output1, ron::ser::PrettyConfig::default()).unwrap();
let output_ron2 =
ron::ser::to_string_pretty(&output2, ron::ser::PrettyConfig::default()).unwrap();
let diff = similar_asserts::SimpleDiff::from_str(
&output_ron1,
&output_ron2,
output_name1,
output_name2,
);
[
format!("Query {query_name} produced incorrect output (./src/lints/{query_name}.ron)."),
diff.to_string(),
"Remember that result output order matters, and remember to re-run \
./scripts/regenerate_test_rustdocs.sh when needed."
.to_string(),
]
.join("\n\n")
}
fn run_query_on_crate_pair(
semver_query: &SemverQuery,
parsed_query: Arc<IndexedQuery>, crate_pair_name: &String,
indexed_crate_new: &VersionedIndex<'_>,
indexed_crate_old: &VersionedIndex<'_>,
) -> (String, Vec<BTreeMap<String, FieldValue>>) {
let adapter = VersionedRustdocAdapter::new(indexed_crate_new, Some(indexed_crate_old))
.expect("could not create adapter");
let results_iter = adapter
.run_query_with_indexed_query(parsed_query.clone(), semver_query.arguments.clone())
.unwrap();
let fold_keys_and_targets: BTreeMap<&str, Vec<Arc<str>>> = parsed_query
.outputs
.iter()
.filter_map(|(name, output)| {
if name.as_ref().ends_with("_begin_line") && output.value_type.is_list() {
if let Some(fold) = parsed_query
.ir_query
.root_component
.folds
.values()
.find(|fold| fold.component.root == parsed_query.vids[&output.vid].root)
{
let targets = parsed_query
.outputs
.values()
.filter_map(|o| {
fold.component
.vertices
.contains_key(&o.vid)
.then_some(Arc::clone(&o.name))
})
.collect();
Some((name.as_ref(), targets))
} else {
None
}
} else {
None
}
})
.collect();
let results = results_iter
.map(move |mut res| {
for (fold_key, targets) in &fold_keys_and_targets {
let mut data: Vec<(u64, usize)> = res[*fold_key]
.as_vec_with(FieldValue::as_u64)
.expect("fold key was not a list of u64")
.into_iter()
.enumerate()
.map(|(idx, val)| (val, idx))
.collect();
data.sort_unstable();
for target in targets {
res.entry(Arc::clone(target)).and_modify(|value| {
if let Some(slice) = value.as_slice() {
let new_order = data
.iter()
.map(|(_, idx)| slice[*idx].clone())
.collect::<Vec<_>>()
.into();
*value = new_order;
}
});
}
}
res.into_iter().map(|(k, v)| (k.to_string(), v)).collect()
})
.collect::<Vec<BTreeMap<_, _>>>();
(format!("./test_crates/{crate_pair_name}/"), results)
}
fn assert_no_false_positives_in_nonchanged_crate(
query_name: &str,
semver_query: &SemverQuery,
indexed_query: Arc<IndexedQuery>, indexed_crate: &VersionedIndex<'_>,
crate_pair_name: &String,
crate_version: &str,
) {
let (crate_pair_path, output) = run_query_on_crate_pair(
semver_query,
indexed_query,
crate_pair_name,
indexed_crate,
indexed_crate,
);
if !output.is_empty() {
let actual_output_name = Box::leak(Box::new(format!(
"actual ({crate_pair_name}/{crate_version})"
)));
let output_difference = pretty_format_output_difference(
query_name,
"expected (empty)",
BTreeMap::new(),
actual_output_name,
BTreeMap::from([(crate_pair_path, output)]),
);
panic!(
"The query produced a non-empty output when it compared two crates with the same rustdoc.\n{output_difference}\n"
);
}
}
pub(in crate::query) fn check_query_execution(query_name: &str) {
let query_text = std::fs::read_to_string(format!("./src/lints/{query_name}.ron")).unwrap();
let semver_query = SemverQuery::from_ron_str(&query_text).unwrap();
let mut parsed_query_cache: HashMap<u32, Arc<IndexedQuery>> = HashMap::new();
let mut query_execution_results: TestOutput = get_test_crate_names()
.iter()
.map(|crate_pair_name| {
let (baseline, current) = get_test_crate_indexes(crate_pair_name);
let adapter = VersionedRustdocAdapter::new(current, Some(baseline))
.expect("could not create adapter");
let indexed_query =
parsed_query_cache
.entry(adapter.version())
.or_insert_with(|| {
trustfall_core::frontend::parse(adapter.schema(), &semver_query.query)
.expect("Query failed to parse.")
});
assert_no_false_positives_in_nonchanged_crate(
query_name,
&semver_query,
indexed_query.clone(),
current,
crate_pair_name,
"new",
);
assert_no_false_positives_in_nonchanged_crate(
query_name,
&semver_query,
indexed_query.clone(),
baseline,
crate_pair_name,
"old",
);
run_query_on_crate_pair(
&semver_query,
indexed_query.clone(),
crate_pair_name,
current,
baseline,
)
})
.filter(|(_crate_pair_name, output)| !output.is_empty())
.collect();
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd)]
enum SortKey {
Span(Arc<str>, usize),
Explicit(Vec<Arc<str>>),
}
let key_func = |elem: &BTreeMap<String, FieldValue>| {
if elem.contains_key("ordering_key") {
let mut ordering_key_names: Vec<_> = elem
.keys()
.filter(|key| key.starts_with("ordering_key"))
.collect();
ordering_key_names.sort_unstable();
let ordering_keys = ordering_key_names
.into_iter()
.map(|key| {
let value = elem
.get(key)
.unwrap_or_else(|| panic!("{key} output missing from result"));
Arc::clone(
value
.as_arc_str()
.expect("ordering_key output was not a string"),
)
})
.collect();
SortKey::Explicit(ordering_keys)
} else {
let filename = elem.get("span_filename").map(|value| {
value
.as_arc_str()
.expect("`span_filename` was not a string")
});
let line = elem
.get("span_begin_line")
.map(|value: &FieldValue| value.as_usize().expect("begin line was not an int"));
match (filename, line) {
(Some(filename), Some(line)) => SortKey::Span(Arc::clone(filename), line),
(Some(_filename), None) => panic!(
"No `span_begin_line` was returned by the query, even though `span_filename` was present. A valid query must either output an explicit `ordering_key`, or output both `span_filename` and `span_begin_line`. See https://github.com/obi1kenobi/cargo-semver-checks/blob/main/CONTRIBUTING.md for details."
),
(None, Some(_line)) => panic!(
"No `span_filename` was returned by the query, even though `span_begin_line` was present. A valid query must either output an explicit `ordering_key`, or output both `span_filename` and `span_begin_line`. See https://github.com/obi1kenobi/cargo-semver-checks/blob/main/CONTRIBUTING.md for details."
),
(None, None) => panic!(
"A valid query must either output an explicit `ordering_key`, or output both `span_filename` and `span_begin_line`. See https://github.com/obi1kenobi/cargo-semver-checks/blob/main/CONTRIBUTING.md for details."
),
}
}
};
for value in query_execution_results.values_mut() {
value.sort_unstable_by_key(key_func);
}
insta::with_settings!(
{
prepend_module_to_snapshot => false,
snapshot_path => "../test_outputs/query_execution",
omit_expression => true,
},
{
insta::assert_ron_snapshot!(query_name, &query_execution_results);
}
);
let transparent_results: BTreeMap<_, Vec<BTreeMap<_, TransparentValue>>> =
query_execution_results
.into_iter()
.map(|(k, v)| {
(
k,
v.into_iter()
.map(|x| x.into_iter().map(|(k, v)| (k, v.into())).collect())
.collect(),
)
})
.collect();
let registry = make_handlebars_registry();
if let Some(template) = semver_query.per_result_error_template {
assert!(!transparent_results.is_empty());
let flattened_actual_results: Vec<_> = transparent_results.values().flatten().collect();
for semver_violation_result in flattened_actual_results {
registry
.render_template(&template, semver_violation_result)
.with_context(|| "Error instantiating semver query template.")
.expect("could not materialize template");
}
}
if let Some(witness) = semver_query.witness {
let actual_witnesses: BTreeMap<_, BTreeSet<_>> = transparent_results
.iter()
.map(|(k, v)| {
(
Cow::Borrowed(k.as_str()),
v.iter()
.map(|values| {
let Some(TransparentValue::String(filename)) = values.get("span_filename") else {
unreachable!("Missing span_filename String, this should be validated above")
};
let begin_line = match values.get("span_begin_line") {
Some(TransparentValue::Int64(i)) => *i as usize,
Some(TransparentValue::Uint64(n)) => *n as usize,
_ => unreachable!("Missing span_begin_line Int, this should be validated above"),
};
WitnessOutput {
filename: filename.to_string(),
begin_line,
hint: registry
.render_template(&witness.hint_template, values)
.expect("error rendering hint template"),
}
})
.collect(),
)
})
.collect();
insta::with_settings!(
{
prepend_module_to_snapshot => false,
snapshot_path => "../test_outputs/witnesses",
omit_expression => true,
description => format!(
"Lint `{query_name}` did not have the expected witness output.\n\
See https://github.com/obi1kenobi/cargo-semver-checks/blob/main/CONTRIBUTING.md#testing-witnesses\n\
for more information."
),
},
{
let formatted_witnesses = toml::to_string_pretty(&actual_witnesses)
.expect("failed to serialize witness snapshots as TOML");
insta::assert_snapshot!(query_name, formatted_witnesses);
}
);
}
}
#[must_use]
fn make_blank_query(
id: String,
lint_level: LintLevel,
required_update: RequiredSemverUpdate,
) -> SemverQuery {
SemverQuery {
id,
lint_level,
required_update,
human_readable_name: String::new(),
description: String::new(),
reference: None,
reference_link: None,
query: String::new(),
arguments: BTreeMap::new(),
error_message: String::new(),
per_result_error_template: None,
witness: None,
}
}
#[test]
fn test_overrides() {
let mut stack = OverrideStack::new();
stack.push(&OverrideMap::from_iter([
(
"query1".into(),
QueryOverride {
lint_level: Some(LintLevel::Allow),
required_update: Some(RequiredSemverUpdate::Minor),
},
),
(
"query2".into(),
QueryOverride {
lint_level: None,
required_update: Some(RequiredSemverUpdate::Minor),
},
),
]));
let q1 = make_blank_query(
"query1".into(),
LintLevel::Deny,
RequiredSemverUpdate::Major,
);
let q2 = make_blank_query(
"query2".into(),
LintLevel::Warn,
RequiredSemverUpdate::Major,
);
assert_eq!(stack.effective_lint_level(&q1), LintLevel::Allow);
assert_eq!(
stack.effective_required_update(&q1),
RequiredSemverUpdate::Minor
);
assert_eq!(stack.effective_lint_level(&q2), LintLevel::Warn);
assert_eq!(
stack.effective_required_update(&q2),
RequiredSemverUpdate::Minor
);
}
#[test]
fn test_override_precedence() {
let mut stack = OverrideStack::new();
stack.push(&OverrideMap::from_iter([
(
"query1".into(),
QueryOverride {
lint_level: Some(LintLevel::Allow),
required_update: Some(RequiredSemverUpdate::Minor),
},
),
(
("query2".into()),
QueryOverride {
lint_level: None,
required_update: Some(RequiredSemverUpdate::Minor),
},
),
]));
stack.push(&OverrideMap::from_iter([(
"query1".into(),
QueryOverride {
required_update: None,
lint_level: Some(LintLevel::Warn),
},
)]));
let q1 = make_blank_query(
"query1".into(),
LintLevel::Deny,
RequiredSemverUpdate::Major,
);
let q2 = make_blank_query(
"query2".into(),
LintLevel::Warn,
RequiredSemverUpdate::Major,
);
assert_eq!(stack.effective_lint_level(&q1), LintLevel::Warn);
assert_eq!(
stack.effective_required_update(&q1),
RequiredSemverUpdate::Minor
);
assert_eq!(stack.effective_lint_level(&q2), LintLevel::Warn);
assert_eq!(
stack.effective_required_update(&q2),
RequiredSemverUpdate::Minor
);
}
#[test]
fn test_inherited_value_deserialization() {
let my_map: BTreeMap<String, InheritedValue> = ron::from_str(
r#"{
"abc": (inherit: "abc"),
"string": "literal_string",
"int": -30,
"int_list": [-30, -2],
"string_list": ["abc", "123"],
}"#,
)
.expect("deserialization failed");
let Some(InheritedValue::Inherited { inherit: abc }) = my_map.get("abc") else {
panic!("Expected Inherited, got {:?}", my_map.get("abc"));
};
assert_eq!(abc, "abc");
let Some(InheritedValue::Constant(TransparentValue::String(string))) = my_map.get("string")
else {
panic!("Expected Constant(String), got {:?}", my_map.get("string"));
};
assert_eq!(&**string, "literal_string");
let Some(InheritedValue::Constant(TransparentValue::Int64(int))) = my_map.get("int") else {
panic!("Expected Constant(Int64), got {:?}", my_map.get("int"));
};
assert_eq!(*int, -30);
let Some(InheritedValue::Constant(TransparentValue::List(ints))) = my_map.get("int_list")
else {
panic!("Expected Constant(List), got {:?}", my_map.get("lint_list"));
};
let Some(TransparentValue::Int64(-30)) = ints.first() else {
panic!("Expected Int64(-30), got {:?}", ints.first());
};
let Some(TransparentValue::Int64(-2)) = ints.get(1) else {
panic!("Expected Int64(-30), got {:?}", ints.get(1));
};
let Some(InheritedValue::Constant(TransparentValue::List(strs))) =
my_map.get("string_list")
else {
panic!(
"Expected Constant(List), got {:?}",
my_map.get("string_list")
);
};
let Some(TransparentValue::String(s)) = strs.first() else {
panic!("Expected String, got {:?}", strs.first());
};
assert_eq!(&**s, "abc");
let Some(TransparentValue::String(s)) = strs.get(1) else {
panic!("Expected String, got {:?}", strs.get(1));
};
assert_eq!(&**s, "123");
ron::from_str::<InheritedValue>(r#"[(inherit: "invalid")]"#)
.expect_err("nested values should be TransparentValues, not InheritedValues");
}
pub(super) fn check_all_lint_files_are_used_in_add_lints(added_lints: &[&str]) {
let mut lints_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
lints_dir.push("src");
lints_dir.push("lints");
let expected_lints: BTreeSet<_> = added_lints.iter().copied().collect();
let mut missing_lints: BTreeSet<String> = Default::default();
let dir_contents =
fs_err::read_dir(lints_dir).expect("failed to read 'src/lints' directory");
for file in dir_contents {
let file = file.expect("failed to examine file");
let path = file.path();
if path.extension().map(|x| x.to_string_lossy()) == Some(Cow::Borrowed("ron")) {
let stem = path
.file_stem()
.map(|x| x.to_string_lossy())
.expect("failed to get file name as utf-8");
if !expected_lints.contains(stem.as_ref()) {
missing_lints.insert(stem.to_string());
}
}
}
assert!(
missing_lints.is_empty(),
"some lints in 'src/lints/' haven't been registered using the `add_lints!()` macro, \
so they won't be part of cargo-semver-checks: {missing_lints:?}"
)
}
#[test]
fn lint_file_names_and_ids_match() {
let mut lints_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
lints_dir.push("src");
lints_dir.push("lints");
for entry in fs_err::read_dir(&lints_dir).expect("failed to read 'src/lints' directory") {
let entry = entry.expect("failed to examine file");
let path = entry.path();
if path.extension().and_then(OsStr::to_str) != Some("ron") {
continue;
}
let stem = path
.file_stem()
.and_then(OsStr::to_str)
.expect("failed to get file name as utf-8");
assert!(
stem.chars().all(|ch| ch.is_ascii_lowercase() || ch == '_'),
"lint file name '{stem}' is not snake_case"
);
assert!(
!stem.starts_with('_'),
"lint file name '{stem}' must not start with '_'"
);
assert!(
!stem.ends_with('_'),
"lint file name '{stem}' must not end with '_'"
);
assert!(
!stem.contains("__"),
"lint file name '{stem}' must not contain '__'"
);
let query_text =
fs_err::read_to_string(&path).expect("failed to read lint definition file");
let semver_query =
SemverQuery::from_ron_str(&query_text).expect("failed to parse lint definition");
assert_eq!(
stem,
semver_query.id,
"lint id does not match file name for {}",
path.display()
);
}
}
#[test]
fn witness_hint_templates_have_whitespace_hygiene() {
let mut issues = Vec::new();
for (query_name, semver_query) in SemverQuery::all_queries() {
let Some(witness) = semver_query.witness else {
continue;
};
for (line_index, line) in witness.hint_template.lines().enumerate() {
if line.ends_with([' ', '\t']) {
issues.push(format!(
"witness hint template for {query_name} has trailing whitespace on line {}: {:?}",
line_index + 1,
line
));
}
}
if witness.hint_template.contains('\n') || witness.hint_template.contains('\r') {
let trimmed = witness.hint_template.trim_end_matches(['\r', '\n']);
let newline_suffix = &witness.hint_template[trimmed.len()..];
if newline_suffix != "\n" && newline_suffix != "\r\n" {
issues.push(format!(
"witness hint template for {query_name} must end with exactly one trailing newline"
));
}
}
}
assert!(
issues.is_empty(),
"witness hint templates have whitespace hygiene issues:\n{}",
issues.join("\n")
);
}
#[test]
fn test_data_is_fresh() -> anyhow::Result<()> {
fn recursive_file_times<P: Into<PathBuf>>(
dir: P,
set: &mut BTreeSet<SystemTime>,
) -> std::io::Result<()> {
for item in fs_err::read_dir(dir)? {
let item = item?;
let metadata = item.metadata()?;
if metadata.is_dir() {
if item.file_name() == "target" {
continue;
}
recursive_file_times(item.path(), set)?;
} else if let Some("rs" | "toml" | "json") =
item.path().extension().and_then(OsStr::to_str)
{
set.insert(metadata.modified()?);
}
}
Ok(())
}
let test_crate_dir = Path::new("test_crates");
let localdata_dir = Path::new("localdata").join("test_data");
if !localdata_dir.fs_err_try_exists()? {
panic!(
"The localdata directory '{}' does not exist yet.\n\
Please run `scripts/regenerate_test_rustdocs.sh`.",
localdata_dir.display()
);
}
for test_crate in fs_err::read_dir(test_crate_dir)? {
let test_crate = test_crate?;
if !test_crate.metadata()?.is_dir() {
continue;
}
if !test_crate
.path()
.join("new")
.join("Cargo.toml")
.fs_err_try_exists()?
|| !test_crate
.path()
.join("old")
.join("Cargo.toml")
.fs_err_try_exists()?
{
continue;
}
for version in ["new", "old"] {
let test_crate_path = test_crate.path().join(version);
let mut test_crate_times = BTreeSet::new();
recursive_file_times(test_crate_path.clone(), &mut test_crate_times)?;
let localdata_path = localdata_dir.join(test_crate.file_name()).join(version);
let mut localdata_times = BTreeSet::new();
recursive_file_times(localdata_path.clone(), &mut localdata_times).context(
"If this directory doesn't exist, run `scripts/regenerate_test_rustdocs.sh`",
)?;
if let (Some(test_max), Some(local_min)) =
(test_crate_times.last(), localdata_times.first())
&& test_max > local_min
{
panic!(
"Files in the '{}' directory are newer than the local data generated by \n\
scripts/regenerate_test_rustdocs.sh in '{}'.\n\n\
Run `scripts/regenerate_test_rustdocs.sh` to generate fresh local data.",
test_crate_path.display(),
localdata_path.display()
)
}
}
}
Ok(())
}
}
#[cfg(test)]
macro_rules! lint_test {
($name:ident) => {
#[test]
fn $name() {
super::tests::check_query_execution(stringify!($name))
}
};
(($name:ident, $conf_pred:meta)) => {
#[test]
#[cfg_attr(not($conf_pred), ignore)]
fn $name() {
super::tests::check_query_execution(stringify!($name))
}
};
}
macro_rules! lint_name {
($name:ident) => {
stringify!($name)
};
(($name:ident, $conf_pred:meta)) => {
stringify!($name)
};
}
macro_rules! add_lints {
($($args:tt,)+) => {
#[cfg(test)]
mod tests_lints {
$(
lint_test!($args);
)*
#[test]
fn all_lint_files_are_used_in_add_lints() {
let added_lints = [
$(
lint_name!($args),
)*
];
super::tests::check_all_lint_files_are_used_in_add_lints(&added_lints);
}
}
fn get_queries() -> Vec<(&'static str, &'static str)> {
vec![
$(
(
lint_name!($args),
include_str!(concat!("lints/", lint_name!($args), ".ron")),
),
)*
]
}
};
($($args:tt),*) => {
compile_error!("Please add a trailing comma after each lint identifier. This ensures our scripts like 'make_new_lint.sh' can safely edit invocations of this macro as needed.");
}
}
#[rustfmt::skip] add_lints!(
(exported_function_requires_more_target_features, any(target_arch = "x86", target_arch = "x86_64")),
(exported_function_target_feature_added, any(target_arch = "x86", target_arch = "x86_64")),
(safe_function_requires_more_target_features, any(target_arch = "x86", target_arch = "x86_64")),
(safe_function_target_feature_added, any(target_arch = "x86", target_arch = "x86_64")),
(safe_inherent_method_requires_more_target_features, any(target_arch = "x86", target_arch = "x86_64")),
(safe_inherent_method_target_feature_added, any(target_arch = "x86", target_arch = "x86_64")),
(trait_method_target_feature_removed, any(target_arch = "x86", target_arch = "x86_64")),
(unsafe_function_requires_more_target_features, any(target_arch = "x86", target_arch = "x86_64")),
(unsafe_function_target_feature_added, any(target_arch = "x86", target_arch = "x86_64")),
(unsafe_inherent_method_requires_more_target_features, any(target_arch = "x86", target_arch = "x86_64")),
(unsafe_inherent_method_target_feature_added, any(target_arch = "x86", target_arch = "x86_64")),
(unsafe_trait_method_requires_more_target_features, any(target_arch = "x86", target_arch = "x86_64")),
(unsafe_trait_method_target_feature_added, any(target_arch = "x86", target_arch = "x86_64")),
attribute_proc_macro_missing,
auto_trait_impl_removed,
constructible_struct_adds_field,
constructible_struct_adds_private_field,
constructible_struct_changed_type,
copy_impl_added,
declarative_macro_missing,
derive_helper_attr_removed,
derive_proc_macro_missing,
derive_trait_impl_removed,
enum_changed_kind,
enum_discriminants_undefined_non_exhaustive_variant,
enum_discriminants_undefined_non_unit_variant,
enum_marked_non_exhaustive,
enum_missing,
enum_must_use_added,
enum_must_use_removed,
enum_no_longer_non_exhaustive,
enum_no_repr_variant_discriminant_changed,
enum_non_exhaustive_struct_variant_field_added,
enum_non_exhaustive_tuple_variant_changed_kind,
enum_non_exhaustive_tuple_variant_field_added,
enum_now_doc_hidden,
enum_repr_int_added,
enum_repr_int_changed,
enum_repr_int_removed,
enum_repr_transparent_removed,
enum_repr_variant_discriminant_changed,
enum_struct_variant_changed_kind,
enum_struct_variant_field_added,
enum_struct_variant_field_marked_deprecated,
enum_struct_variant_field_missing,
enum_struct_variant_field_now_doc_hidden,
enum_tuple_variant_changed_kind,
enum_tuple_variant_field_added,
enum_tuple_variant_field_marked_deprecated,
enum_tuple_variant_field_missing,
enum_tuple_variant_field_now_doc_hidden,
enum_unit_variant_changed_kind,
enum_variant_added,
enum_variant_marked_deprecated,
enum_variant_marked_non_exhaustive,
enum_variant_missing,
enum_variant_no_longer_non_exhaustive,
exhaustive_enum_added,
exhaustive_struct_added,
exhaustive_struct_with_doc_hidden_fields_added,
exhaustive_struct_with_private_fields_added,
exported_function_abi_no_longer_unwind,
exported_function_abi_now_unwind,
exported_function_changed_abi,
exported_function_now_returns_unit,
exported_function_parameter_count_changed,
exported_function_return_value_added,
feature_missing,
feature_newly_enables_feature,
feature_no_longer_enables_feature,
feature_not_enabled_by_default,
function_abi_no_longer_unwind,
function_abi_now_unwind,
function_changed_abi,
function_const_generic_reordered,
function_const_removed,
function_export_name_changed,
function_generic_type_reordered,
function_like_proc_macro_missing,
function_marked_deprecated,
function_missing,
function_must_use_added,
function_must_use_removed,
function_no_longer_unsafe,
function_now_const,
function_now_doc_hidden,
function_now_returns_unit,
function_parameter_count_changed,
function_requires_different_const_generic_params,
function_requires_different_generic_type_params,
function_unsafe_added,
global_value_marked_deprecated,
inherent_associated_const_now_doc_hidden,
inherent_associated_pub_const_added,
inherent_associated_pub_const_missing,
inherent_method_added,
inherent_method_changed_abi,
inherent_method_const_generic_reordered,
inherent_method_const_removed,
inherent_method_generic_type_reordered,
inherent_method_missing,
inherent_method_must_use_added,
inherent_method_must_use_removed,
inherent_method_no_longer_unsafe,
inherent_method_no_longer_unwind,
inherent_method_now_const,
inherent_method_now_doc_hidden,
inherent_method_now_returns_unit,
inherent_method_now_unwind,
inherent_method_unsafe_added,
macro_marked_deprecated,
macro_no_longer_exported,
macro_now_doc_hidden,
method_export_name_changed,
method_no_longer_has_receiver,
method_parameter_count_changed,
method_receiver_mut_ref_became_owned,
method_receiver_ref_became_mut,
method_receiver_ref_became_owned,
method_receiver_type_changed,
method_requires_different_const_generic_params,
method_requires_different_generic_type_params,
module_missing,
non_exhaustive_enum_added,
non_exhaustive_struct_added,
non_exhaustive_struct_changed_type,
partial_ord_enum_struct_variant_fields_reordered,
partial_ord_enum_variants_reordered,
partial_ord_struct_fields_reordered,
proc_macro_marked_deprecated,
proc_macro_now_doc_hidden,
pub_api_sealed_trait_became_unconditionally_sealed,
pub_api_sealed_trait_became_unsealed,
pub_api_sealed_trait_method_receiver_added,
pub_api_sealed_trait_method_receiver_mut_ref_became_ref,
pub_api_sealed_trait_method_return_value_added,
pub_api_sealed_trait_method_target_feature_removed,
pub_const_added,
pub_module_level_const_missing,
pub_module_level_const_now_doc_hidden,
pub_static_added,
pub_static_missing,
pub_static_mut_now_immutable,
pub_static_now_doc_hidden,
pub_static_now_mutable,
repr_align_added,
repr_align_changed,
repr_align_removed,
repr_c_added,
repr_c_enum_struct_variant_fields_reordered,
repr_c_plain_struct_fields_reordered,
repr_c_removed,
repr_packed_added,
repr_packed_changed,
repr_packed_removed,
repr_transparent_added,
sized_impl_removed,
static_became_unsafe,
struct_field_marked_deprecated,
struct_marked_non_exhaustive,
struct_missing,
struct_must_use_added,
struct_must_use_removed,
struct_no_longer_has_non_pub_fields,
struct_no_longer_non_exhaustive,
struct_now_doc_hidden,
struct_pub_field_missing,
struct_pub_field_now_doc_hidden,
struct_repr_transparent_removed,
struct_with_no_pub_fields_changed_type,
struct_with_pub_fields_changed_type,
trait_added_supertrait,
trait_allows_fewer_const_generic_params,
trait_allows_fewer_generic_type_params,
trait_associated_const_added,
trait_associated_const_default_removed,
trait_associated_const_marked_deprecated,
trait_associated_const_now_doc_hidden,
trait_associated_type_added,
trait_associated_type_default_removed,
trait_associated_type_marked_deprecated,
trait_associated_type_now_doc_hidden,
trait_changed_kind,
trait_const_generic_reordered,
trait_generic_type_reordered,
trait_marked_deprecated,
trait_method_added,
trait_method_changed_abi,
trait_method_const_generic_reordered,
trait_method_default_impl_removed,
trait_method_generic_type_reordered,
trait_method_marked_deprecated,
trait_method_missing,
trait_method_no_longer_has_receiver,
trait_method_no_longer_unwind,
trait_method_now_doc_hidden,
trait_method_now_returns_unit,
trait_method_now_unwind,
trait_method_parameter_count_changed,
trait_method_receiver_added,
trait_method_receiver_mut_ref_became_owned,
trait_method_receiver_mut_ref_became_ref,
trait_method_receiver_owned_became_mut_ref,
trait_method_receiver_owned_became_ref,
trait_method_receiver_ref_became_mut,
trait_method_receiver_ref_became_owned,
trait_method_receiver_type_changed,
trait_method_requires_different_const_generic_params,
trait_method_requires_different_generic_type_params,
trait_method_return_value_added,
trait_method_unsafe_added,
trait_method_unsafe_removed,
trait_mismatched_generic_lifetimes,
trait_missing,
trait_must_use_added,
trait_must_use_removed,
trait_newly_sealed,
trait_no_longer_dyn_compatible,
trait_now_doc_hidden,
trait_removed_associated_constant,
trait_removed_associated_type,
trait_removed_supertrait,
trait_requires_more_const_generic_params,
trait_requires_more_generic_type_params,
trait_unsafe_added,
trait_unsafe_removed,
tuple_struct_to_plain_struct,
type_allows_fewer_const_generic_params,
type_allows_fewer_generic_type_params,
type_associated_const_marked_deprecated,
type_const_generic_reordered,
type_generic_type_reordered,
type_marked_deprecated,
type_method_marked_deprecated,
type_mismatched_generic_lifetimes,
type_requires_more_const_generic_params,
type_requires_more_generic_type_params,
unconditionally_sealed_trait_became_pub_api_sealed,
unconditionally_sealed_trait_became_unsealed,
union_added,
union_changed_kind,
union_changed_to_incompatible_struct,
union_field_added_with_all_pub_fields,
union_field_added_with_non_pub_fields,
union_field_marked_deprecated,
union_field_missing,
union_missing,
union_must_use_added,
union_must_use_removed,
union_now_doc_hidden,
union_pub_field_now_doc_hidden,
union_with_multiple_pub_fields_changed_to_struct,
unit_struct_changed_kind,
);