Skip to main content

cargo_semver_checks/
query.rs

1use std::{collections::BTreeMap, sync::Arc};
2
3use ron::extensions::Extensions;
4use serde::{Deserialize, Serialize};
5use trustfall::{FieldValue, TransparentValue};
6
7use crate::ReleaseType;
8
9#[non_exhaustive]
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
11pub enum RequiredSemverUpdate {
12    #[serde(alias = "minor")]
13    Minor,
14    #[serde(alias = "major")]
15    Major,
16}
17
18impl RequiredSemverUpdate {
19    pub fn as_str(&self) -> &'static str {
20        match self {
21            Self::Major => "major",
22            Self::Minor => "minor",
23        }
24    }
25}
26
27impl From<RequiredSemverUpdate> for ReleaseType {
28    fn from(value: RequiredSemverUpdate) -> Self {
29        match value {
30            RequiredSemverUpdate::Major => Self::Major,
31            RequiredSemverUpdate::Minor => Self::Minor,
32        }
33    }
34}
35
36/// The level of intensity of the error when a lint occurs.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
38pub enum LintLevel {
39    /// If this lint occurs, do nothing.
40    #[serde(alias = "allow")]
41    Allow,
42    /// If this lint occurs, print a warning.
43    #[serde(alias = "warn")]
44    Warn,
45    /// If this lint occurs, raise an error.
46    #[serde(alias = "deny")]
47    Deny,
48}
49
50impl LintLevel {
51    pub fn as_str(self) -> &'static str {
52        match self {
53            LintLevel::Allow => "allow",
54            LintLevel::Warn => "warn",
55            LintLevel::Deny => "deny",
56        }
57    }
58}
59
60/// Kind of semver update.
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum ActualSemverUpdate {
63    Major,
64    Minor,
65    Patch,
66    NotChanged,
67}
68
69impl ActualSemverUpdate {
70    pub(crate) fn supports_requirement(&self, required: RequiredSemverUpdate) -> bool {
71        match (*self, required) {
72            (ActualSemverUpdate::Major, _) => true,
73            (ActualSemverUpdate::Minor, RequiredSemverUpdate::Major) => false,
74            (ActualSemverUpdate::Minor, _) => true,
75            (_, _) => false,
76        }
77    }
78}
79
80impl From<ReleaseType> for ActualSemverUpdate {
81    fn from(value: ReleaseType) -> Self {
82        match value {
83            ReleaseType::Major => Self::Major,
84            ReleaseType::Minor => Self::Minor,
85            ReleaseType::Patch => Self::Patch,
86        }
87    }
88}
89
90/// How the witness is expected to be used in running the lint.
91#[non_exhaustive]
92#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
93pub enum WitnessPurpose {
94    /// The query is expected to only flag true-positive cases,
95    /// so the witness is used to double-check its outputs for false-positives.
96    /// If the witness fails to validate an output,
97    /// then either the query or the witness generator has a bug.
98    #[default]
99    ConsistencyCheck,
100
101    /// The query is necessary but not sufficient to correctly identify true-positives.
102    /// A witness must be generated for each query output, and its outcome determines
103    /// whether the output is a true positive. The query is *not* considered buggy
104    /// even if the witness fails to validate the query's output.
105    RequiredForCorrectness,
106}
107
108impl WitnessPurpose {
109    pub(crate) const fn as_str(self) -> &'static str {
110        match self {
111            Self::ConsistencyCheck => "consistency_check",
112            Self::RequiredForCorrectness => "required_for_correctness",
113        }
114    }
115}
116
117/// A query that can be executed on a pair of rustdoc output files,
118/// returning instances of a particular kind of semver violation.
119#[non_exhaustive]
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct SemverQuery {
122    pub id: String,
123
124    pub(crate) human_readable_name: String,
125
126    pub description: String,
127
128    pub required_update: RequiredSemverUpdate,
129
130    /// The default lint level for when this lint occurs.
131    pub lint_level: LintLevel,
132
133    #[serde(default)]
134    pub reference: Option<String>,
135
136    #[serde(default)]
137    pub reference_link: Option<String>,
138
139    pub(crate) query: String,
140
141    #[serde(default)]
142    pub(crate) arguments: BTreeMap<String, TransparentValue>,
143
144    /// The top-level error describing the semver violation that was detected.
145    /// Even if multiple instances of this semver issue are found, this error
146    /// message is displayed only at most once.
147    pub(crate) error_message: String,
148
149    /// Optional template that can be combined with each query output to produce
150    /// a human-readable description of the specific semver violation that was discovered.
151    #[serde(default)]
152    pub(crate) per_result_error_template: Option<String>,
153
154    /// Optional data to create witness code for query output.  See the [`Witness`] struct for
155    /// more information.
156    #[serde(default)]
157    pub witness: Option<Witness>,
158}
159
160impl SemverQuery {
161    /// Deserializes a [`SemverQuery`] from a [`ron`]-encoded string slice.
162    ///
163    /// Returns an `Err` if the deserialization fails.
164    pub fn from_ron_str(query_text: &str) -> ron::Result<Self> {
165        let mut deserializer = ron::Deserializer::from_str_with_options(
166            query_text,
167            &ron::Options::default().with_default_extension(Extensions::IMPLICIT_SOME),
168        )?;
169
170        Self::deserialize(&mut deserializer)
171    }
172
173    pub fn all_queries() -> BTreeMap<String, SemverQuery> {
174        let mut queries = BTreeMap::default();
175        for (id, query_text) in get_queries() {
176            let query = Self::from_ron_str(query_text).unwrap_or_else(|e| {
177                panic!(
178                    "\
179                Failed to parse a query: {e}
180                ```ron
181                {query_text}
182                ```"
183                );
184            });
185            assert_eq!(id, query.id, "Query id must match file name");
186            let id_conflict = queries.insert(query.id.clone(), query);
187            assert!(id_conflict.is_none(), "{id_conflict:?}");
188        }
189
190        queries
191    }
192}
193
194/// Configured values for a [`SemverQuery`] that differ from the lint's defaults.
195#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
196#[serde(rename_all = "kebab-case")]
197pub struct QueryOverride {
198    /// The required version bump for this lint; see [`SemverQuery`].`required_update`.
199    ///
200    /// If this is `None`, use the query's default `required_update` when calculating
201    /// the effective required version bump.
202    #[serde(default)]
203    pub required_update: Option<RequiredSemverUpdate>,
204
205    /// The lint level for this lint; see [`SemverQuery`].`lint_level`.
206    ///
207    /// If this is `None`, use the query's default `lint_level` when calculating
208    /// the effective lint level.
209    #[serde(default)]
210    pub lint_level: Option<LintLevel>,
211}
212
213/// A mapping of lint ids to configured values that override that lint's defaults.
214pub type OverrideMap = BTreeMap<String, QueryOverride>;
215
216/// A stack of [`OverrideMap`] values capturing our precedence rules.
217///
218/// Items toward the top of the stack (later in the backing `Vec`) have *higher* precedence
219/// and override items lower in the stack. If an override is set and not `None` for a given lint
220/// in multiple maps in the stack, the value at the top of the stack will be used
221/// to calculate the effective lint level or required version update.
222#[derive(Debug, Clone, Default, PartialEq, Eq)]
223pub struct OverrideStack(Vec<OverrideMap>);
224
225impl OverrideStack {
226    /// Creates a new, empty [`OverrideStack`] instance.
227    #[must_use]
228    pub fn new() -> Self {
229        Self(Vec::new())
230    }
231
232    /// Inserts the given map at the top of the stack.
233    ///
234    /// The inserted overrides will take precedence over any lower item in the stack,
235    /// if both maps have a not-`None` entry for a given lint.
236    pub fn push(&mut self, item: &OverrideMap) {
237        self.0.push(item.clone());
238    }
239
240    /// Calculates the *effective* lint level of this query, by searching for an override
241    /// mapped to this query's id from the top of the stack first, returning the query's default
242    /// lint level if not overridden.
243    #[must_use]
244    pub fn effective_lint_level(&self, query: &SemverQuery) -> LintLevel {
245        self.0
246            .iter()
247            .rev()
248            .find_map(|x| x.get(&query.id).and_then(|y| y.lint_level))
249            .unwrap_or(query.lint_level)
250    }
251
252    /// Calculates the *effective* required version bump of this query, by searching for an override
253    /// mapped to this query's id from the top of the stack first, returning the query's default
254    /// required version bump if not overridden.
255    #[must_use]
256    pub fn effective_required_update(&self, query: &SemverQuery) -> RequiredSemverUpdate {
257        self.0
258            .iter()
259            .rev()
260            .find_map(|x| x.get(&query.id).and_then(|y| y.required_update))
261            .unwrap_or(query.required_update)
262    }
263}
264
265/// Data for generating a **witness** from the results of a [`SemverQuery`].
266///
267/// A witness is a minimal compilable example of how downstream code would
268/// break given this change.  See field documentation for more information
269/// on each member.
270///
271/// Fields besides [`hint_template`](Self::hint_template) are optional, as it is not
272/// always necessary to use an additional query [`witness_query`](Self::witness_query)
273/// or possible to build a compilable witness from [`witness_template`](Self::witness_template)
274/// for a given `SemverQuery`.
275#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct Witness {
277    /// The purpose this witness serves in the surrounding query.
278    #[serde(default)]
279    pub purpose: WitnessPurpose,
280
281    /// A [`handlebars`] template that renders a user-facing hint to give a quick
282    /// explanation of breakage.  This may not be a buildable example, but it should
283    /// show the idea of why downstream code could break.  It will be provided all
284    /// `@output` data from the [`SemverQuery`] query that contains this [`Witness`].
285    ///
286    /// Example for the `function_missing` lint, where `name` is the (re)moved function's
287    /// name and `path` is the importable path:
288    ///
289    /// ```no_run
290    /// # let _ = r#"
291    /// use {{join "::" path}};
292    /// {{name}}(...);
293    /// # "#;
294    /// ```
295    ///
296    /// Notice how this is not a compilable example, but it provides a distilled hint to the user
297    /// of how downstream code would break with this change.
298    pub hint_template: String,
299
300    /// A [`handlebars`] template that renders the compilable witness example of how
301    /// downstream code would break.
302    ///
303    /// This template will be provided any fields with `@output` directives in the
304    /// original [`SemverQuery`].  If [`witness_query`](Self::witness_query) is `Some`,
305    /// it will also be provided the `@output`s of that query. (The additional query's
306    /// outputs will take precedence over the original query if they share the same name.)
307    ///
308    /// Example for the `enum_variant_missing` lint, where `path` is the importable path of the enum,
309    /// `name` is the name of the enum, and `variant_name` is the name of the removed/renamed variant:
310    ///
311    /// ```no_run
312    /// # let _ = r#"
313    /// fn witness(item: {{path}}) {
314    ///     if let {{path}}::{{variant_name}} {..} = item {
315    ///
316    ///     }
317    /// }
318    /// # "#;
319    /// ```
320    #[serde(default)]
321    pub witness_template: Option<String>,
322
323    /// An optional query to collect more information that is necessary to render
324    /// the [`witness_template`](Self::witness_template).
325    ///
326    /// If `None`, no additional query will be run.
327    #[serde(default)]
328    pub witness_query: Option<WitnessQuery>,
329}
330
331/// A [`trustfall`] query, for [`Witness`] generation, containing the query
332/// string itself and a mapping of argument names to value types which are
333/// provided to the query.
334#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct WitnessQuery {
336    /// The string containing the Trustfall query.
337    pub query: String,
338
339    /// The mapping of argument names to values provided to the query.
340    ///
341    /// These can be inherited from a previous query ([`InheritedValue::Inherited`]) or
342    /// specified as [`InheritedValue::Constant`]s.
343    #[serde(default)]
344    pub arguments: BTreeMap<Arc<str>, InheritedValue>,
345}
346
347impl WitnessQuery {
348    /// Returns [`arguments`](Self::arguments), mapping the [`InheritedValue`]s from
349    /// the given output map, which is the map of output [`FieldValue`]s from the previous query.
350    ///
351    /// Fails with an [`anyhow::Error`] if any requested inheritance keys are missing.
352    pub fn inherit_arguments_from(
353        &self,
354        source_map: &BTreeMap<std::sync::Arc<str>, FieldValue>,
355    ) -> anyhow::Result<BTreeMap<Arc<str>, FieldValue>> {
356        let mut mapped = BTreeMap::new();
357
358        for (key, value) in self.arguments.iter() {
359            let mapped_value = match value {
360                // Inherit an output
361                InheritedValue::Inherited { inherit } => source_map
362                    .get(inherit.as_str())
363                    .cloned()
364                    .ok_or(anyhow::anyhow!(
365                        "inherited output key `{inherit}` does not exist in {source_map:?}"
366                    ))?,
367                // Set a constant
368                InheritedValue::Constant(value) => value.clone().into(),
369            };
370            mapped.insert(Arc::clone(key), mapped_value);
371        }
372
373        Ok(mapped)
374    }
375}
376
377/// Represents either a value inherited from a previous query, or a
378/// provided constant value.
379#[derive(Debug, Clone, Serialize, Deserialize)]
380#[serde(untagged, deny_unknown_fields)]
381pub enum InheritedValue {
382    /// Inherit the value from the previous output whose name is the given `String`.
383    Inherited { inherit: String },
384    /// Provide the constant value specified here.
385    Constant(TransparentValue),
386}
387
388#[cfg(test)]
389mod tests {
390    use std::borrow::Cow;
391    use std::collections::{BTreeSet, HashMap};
392    use std::ffi::OsStr;
393    use std::path::PathBuf;
394    use std::sync::{Arc, OnceLock};
395    use std::time::SystemTime;
396    use std::{collections::BTreeMap, path::Path};
397
398    use anyhow::Context;
399    use fs_err::PathExt;
400    use rayon::prelude::*;
401    use serde::{Deserialize, Serialize};
402    use toml::Value;
403    use trustfall::{FieldValue, TransparentValue};
404    use trustfall_core::ir::IndexedQuery;
405    use trustfall_rustdoc::{
406        VersionedIndex, VersionedRustdocAdapter, VersionedStorage, load_rustdoc,
407    };
408
409    use crate::query::{
410        InheritedValue, LintLevel, OverrideMap, OverrideStack, QueryOverride, RequiredSemverUpdate,
411        SemverQuery,
412    };
413    use crate::templating::make_handlebars_registry;
414
415    static TEST_CRATE_NAMES: OnceLock<Vec<String>> = OnceLock::new();
416
417    /// Mapping test crate (pair) name -> (old rustdoc, new rustdoc).
418    static TEST_CRATE_RUSTDOCS: OnceLock<BTreeMap<String, (VersionedStorage, VersionedStorage)>> =
419        OnceLock::new();
420
421    /// Mapping test crate (pair) name -> (old index, new index).
422    static TEST_CRATE_INDEXES: OnceLock<
423        BTreeMap<String, (VersionedIndex<'static>, VersionedIndex<'static>)>,
424    > = OnceLock::new();
425
426    fn get_test_crate_names() -> &'static [String] {
427        TEST_CRATE_NAMES.get_or_init(initialize_test_crate_names)
428    }
429
430    fn get_all_test_crates() -> &'static BTreeMap<String, (VersionedStorage, VersionedStorage)> {
431        TEST_CRATE_RUSTDOCS.get_or_init(initialize_test_crate_rustdocs)
432    }
433
434    #[test]
435    fn lint_files_have_matching_ids() {
436        let lints_dir = Path::new("src/lints");
437        let ron_files = collect_ron_files(lints_dir);
438
439        assert!(
440            !ron_files.is_empty(),
441            "expected at least one lint definition in {lints_dir:?}"
442        );
443
444        for path in ron_files {
445            let stem = path
446                .file_stem()
447                .and_then(OsStr::to_str)
448                .expect("lint file must have a valid UTF-8 stem");
449            assert!(
450                is_lower_snake_case(stem),
451                "lint file stem `{stem}` must be lower snake case"
452            );
453
454            let contents = fs_err::read_to_string(&path).expect("failed to read lint file");
455            let query = SemverQuery::from_ron_str(&contents).expect("failed to parse lint");
456            assert_eq!(
457                stem, query.id,
458                "lint id must match file stem for {:?}",
459                path
460            );
461        }
462    }
463
464    fn collect_ron_files(dir: &Path) -> Vec<PathBuf> {
465        let mut result = Vec::new();
466        let mut stack = vec![dir.to_path_buf()];
467
468        while let Some(current) = stack.pop() {
469            for entry in fs_err::read_dir(&current).expect("failed to read directory") {
470                let entry = entry.expect("failed to read directory entry");
471                let path = entry.path();
472                if entry
473                    .file_type()
474                    .expect("failed to determine file type")
475                    .is_dir()
476                {
477                    stack.push(path);
478                } else if path.extension() == Some(OsStr::new("ron")) {
479                    result.push(path);
480                }
481            }
482        }
483
484        result.sort();
485        result
486    }
487
488    fn is_lower_snake_case(value: &str) -> bool {
489        !value.is_empty()
490            && value.chars().all(|ch| ch.is_ascii_lowercase() || ch == '_')
491            && !value.starts_with('_')
492            && !value.ends_with('_')
493            && !value.contains("__")
494    }
495
496    fn get_all_test_crate_indexes()
497    -> &'static BTreeMap<String, (VersionedIndex<'static>, VersionedIndex<'static>)> {
498        TEST_CRATE_INDEXES.get_or_init(initialize_test_crate_indexes)
499    }
500
501    fn get_test_crate_indexes(
502        test_crate: &str,
503    ) -> &'static (VersionedIndex<'static>, VersionedIndex<'static>) {
504        &get_all_test_crate_indexes()[test_crate]
505    }
506
507    fn initialize_test_crate_names() -> Vec<String> {
508        std::fs::read_dir("./test_crates/")
509            .expect("directory test_crates/ not found")
510            .map(|dir_entry| dir_entry.expect("failed to list test_crates/"))
511            .filter(|dir_entry| {
512                // Only return directories inside `test_crates/` that contain
513                // an `old/Cargo.toml` file. This works around finicky git + cargo behavior:
514                // - Create a git branch, commit a new test case, and generate its rustdoc.
515                // - Cargo will then create `Cargo.lock` files for the crate,
516                //   which are ignored by git.
517                // - Check out another branch, and git won't delete the `Cargo.lock` files
518                //   since they aren't tracked. But we don't want to run tests on those crates!
519                if !dir_entry
520                    .metadata()
521                    .expect("failed to retrieve test_crates/* metadata")
522                    .is_dir()
523                {
524                    return false;
525                }
526
527                let mut test_crate_cargo_toml = dir_entry.path();
528                test_crate_cargo_toml.extend(["old", "Cargo.toml"]);
529                test_crate_cargo_toml.as_path().is_file()
530            })
531            .map(|dir_entry| {
532                String::from(
533                    String::from(
534                        dir_entry
535                            .path()
536                            .to_str()
537                            .expect("failed to convert dir_entry to String"),
538                    )
539                    .strip_prefix("./test_crates/")
540                    .expect(
541                        "the dir_entry doesn't start with './test_crates/', which is unexpected",
542                    ),
543                )
544            })
545            .collect()
546    }
547
548    fn initialize_test_crate_rustdocs() -> BTreeMap<String, (VersionedStorage, VersionedStorage)> {
549        get_test_crate_names()
550            .par_iter()
551            .map(|crate_pair| {
552                let old_rustdoc = load_pregenerated_rustdoc(crate_pair.as_str(), "old");
553                let new_rustdoc = load_pregenerated_rustdoc(crate_pair, "new");
554
555                (crate_pair.clone(), (old_rustdoc, new_rustdoc))
556            })
557            .collect()
558    }
559
560    fn initialize_test_crate_indexes()
561    -> BTreeMap<String, (VersionedIndex<'static>, VersionedIndex<'static>)> {
562        get_all_test_crates()
563            .par_iter()
564            .map(|(key, (old_crate, new_crate))| {
565                let old_index = VersionedIndex::from_storage(old_crate);
566                let new_index = VersionedIndex::from_storage(new_crate);
567                (key.clone(), (old_index, new_index))
568            })
569            .collect()
570    }
571
572    fn load_pregenerated_rustdoc(crate_pair: &str, crate_version: &str) -> VersionedStorage {
573        let rustdoc_path =
574            format!("./localdata/test_data/{crate_pair}/{crate_version}/rustdoc.json");
575        let metadata_path =
576            format!("./localdata/test_data/{crate_pair}/{crate_version}/metadata.json");
577        let metadata_text = std::fs::read_to_string(&metadata_path).map_err(|e| anyhow::anyhow!(e).context(
578            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");
579        let metadata = serde_json::from_str(&metadata_text).expect("failed to parse metadata file");
580        load_rustdoc(Path::new(&rustdoc_path), Some(metadata))
581            .with_context(|| format!("Could not load {rustdoc_path} file, did you forget to run ./scripts/regenerate_test_rustdocs.sh ?"))
582            .expect("failed to load rustdoc")
583    }
584
585    #[derive(Debug, PartialEq, Eq)]
586    struct PackageManifest {
587        name: String,
588        version: String,
589        edition: String,
590    }
591
592    fn load_package_manifest(manifest_dir: &Path) -> PackageManifest {
593        let manifest_path = manifest_dir.join("Cargo.toml");
594        let manifest_text =
595            fs_err::read_to_string(&manifest_path).expect("failed to load manifest for test crate");
596        let manifest: Value = toml::from_str(&manifest_text)
597            .unwrap_or_else(|e| panic!("failed to parse {}: {e}", manifest_path.display()));
598
599        let package_table = manifest
600            .get("package")
601            .and_then(Value::as_table)
602            .unwrap_or_else(|| {
603                panic!(
604                    "manifest at {} missing [package] table",
605                    manifest_path.display()
606                )
607            });
608
609        let name = package_table
610            .get("name")
611            .and_then(Value::as_str)
612            .unwrap_or_else(|| {
613                panic!(
614                    "manifest at {} missing package.name",
615                    manifest_path.display()
616                )
617            })
618            .to_owned();
619        let version = package_table
620            .get("version")
621            .and_then(Value::as_str)
622            .unwrap_or_else(|| {
623                panic!(
624                    "manifest at {} missing package.version",
625                    manifest_path.display()
626                )
627            })
628            .to_owned();
629        let edition = package_table
630            .get("edition")
631            .and_then(Value::as_str)
632            .unwrap_or_else(|| {
633                panic!(
634                    "manifest at {} missing package.edition",
635                    manifest_path.display()
636                )
637            })
638            .to_owned();
639
640        let publish_value = package_table.get("publish").unwrap_or_else(|| {
641            panic!(
642                "manifest at {} missing package.publish",
643                manifest_path.display()
644            )
645        });
646        assert!(
647            matches!(publish_value, Value::Boolean(false)),
648            "manifest at {} must set package.publish = false",
649            manifest_path.display()
650        );
651
652        PackageManifest {
653            name,
654            version,
655            edition,
656        }
657    }
658
659    const VERSION_MISMATCH_ALLOWED: &[&str] = &[
660        "semver_trick_self_referential",
661        "trait_missing_with_major_bump",
662    ];
663
664    #[test]
665    fn test_crates_have_consistent_manifests() {
666        let base_path = Path::new("./test_crates");
667        let entries = fs_err::read_dir(base_path).expect("directory test_crates/ not found");
668        let mut checked_pairs = 0usize;
669
670        for entry in entries {
671            let entry = entry.expect("failed to read test_crates entry");
672            let path = entry.path();
673            if !entry
674                .metadata()
675                .expect("failed to read metadata for test_crates entry")
676                .is_dir()
677            {
678                continue;
679            }
680
681            let old_dir = path.join("old");
682            let new_dir = path.join("new");
683            let old_dir_manifest = old_dir.join("Cargo.toml");
684            let new_dir_manifest = new_dir.join("Cargo.toml");
685            if !(old_dir.is_dir()
686                && new_dir.is_dir()
687                && old_dir_manifest.is_file()
688                && new_dir_manifest.is_file())
689            {
690                continue;
691            }
692
693            let dir_name = path
694                .file_name()
695                .and_then(|name| name.to_str())
696                .expect("test_crate directory must be valid UTF-8");
697
698            let old_manifest = load_package_manifest(&old_dir);
699            let new_manifest = load_package_manifest(&new_dir);
700
701            let PackageManifest {
702                name: old_name,
703                version: old_version,
704                edition: old_edition,
705            } = old_manifest;
706            let PackageManifest {
707                name: new_name,
708                version: new_version,
709                edition: new_edition,
710            } = new_manifest;
711
712            assert_eq!(
713                old_name, dir_name,
714                "manifest name must match directory name for {dir_name}"
715            );
716            assert_eq!(
717                new_name, dir_name,
718                "manifest name must match directory name for {dir_name}"
719            );
720            assert_eq!(
721                old_edition, new_edition,
722                "old and new editions differ for {dir_name}"
723            );
724
725            if !VERSION_MISMATCH_ALLOWED.contains(&dir_name) {
726                assert_eq!(
727                    old_version, new_version,
728                    "old and new versions differ for {dir_name}"
729                );
730            }
731
732            checked_pairs += 1;
733        }
734
735        assert!(
736            checked_pairs > 0,
737            "expected to check at least one test crate pair"
738        );
739    }
740
741    #[test]
742    fn all_queries_are_valid() {
743        let (_baseline, current) = get_test_crate_indexes("template");
744
745        let adapter =
746            VersionedRustdocAdapter::new(current, Some(current)).expect("failed to create adapter");
747        for semver_query in SemverQuery::all_queries().into_values() {
748            let _ = adapter
749                .run_query(&semver_query.query, semver_query.arguments)
750                .expect("not a valid query");
751        }
752    }
753
754    #[test]
755    fn pub_use_handling() {
756        let (_baseline, current) = get_test_crate_indexes("pub_use_handling");
757
758        let query = r#"
759            {
760                Crate {
761                    item {
762                        ... on Struct {
763                            name @filter(op: "=", value: ["$struct"])
764
765                            canonical_path {
766                                canonical_path: path @output
767                            }
768
769                            importable_path @fold {
770                                path @output
771                            }
772                        }
773                    }
774                }
775            }"#;
776        let mut arguments = BTreeMap::new();
777        arguments.insert("struct", "CheckPubUseHandling");
778
779        let adapter =
780            VersionedRustdocAdapter::new(current, None).expect("could not create adapter");
781
782        let results_iter = adapter
783            .run_query(query, arguments)
784            .expect("failed to run query");
785        let actual_results: Vec<BTreeMap<_, _>> = results_iter
786            .map(|res| res.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
787            .collect();
788
789        let expected_result: FieldValue =
790            vec!["pub_use_handling", "inner", "CheckPubUseHandling"].into();
791        assert_eq!(1, actual_results.len(), "{actual_results:?}");
792        assert_eq!(
793            expected_result, actual_results[0]["canonical_path"],
794            "{actual_results:?}"
795        );
796
797        let mut actual_paths = actual_results[0]["path"]
798            .as_vec_with(|val| val.as_vec_with(FieldValue::as_str))
799            .expect("not a Vec<Vec<&str>>");
800        actual_paths.sort_unstable();
801
802        let expected_paths = vec![
803            vec!["pub_use_handling", "CheckPubUseHandling"],
804            vec!["pub_use_handling", "inner", "CheckPubUseHandling"],
805        ];
806        assert_eq!(expected_paths, actual_paths);
807    }
808
809    type TestOutput = BTreeMap<String, Vec<BTreeMap<String, FieldValue>>>;
810
811    #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
812    #[non_exhaustive]
813    struct WitnessOutput {
814        filename: String,
815        begin_line: usize,
816        hint: String,
817    }
818
819    impl PartialOrd for WitnessOutput {
820        fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
821            Some(self.cmp(other))
822        }
823    }
824
825    /// Sorts by span (filename, begin_line)
826    impl Ord for WitnessOutput {
827        fn cmp(&self, other: &Self) -> std::cmp::Ordering {
828            (&self.filename, self.begin_line).cmp(&(&other.filename, other.begin_line))
829        }
830    }
831
832    fn pretty_format_output_difference(
833        query_name: &str,
834        output_name1: &'static str,
835        output1: TestOutput,
836        output_name2: &'static str,
837        output2: TestOutput,
838    ) -> String {
839        let output_ron1 =
840            ron::ser::to_string_pretty(&output1, ron::ser::PrettyConfig::default()).unwrap();
841        let output_ron2 =
842            ron::ser::to_string_pretty(&output2, ron::ser::PrettyConfig::default()).unwrap();
843        let diff = similar_asserts::SimpleDiff::from_str(
844            &output_ron1,
845            &output_ron2,
846            output_name1,
847            output_name2,
848        );
849        [
850            format!("Query {query_name} produced incorrect output (./src/lints/{query_name}.ron)."),
851            diff.to_string(),
852            "Remember that result output order matters, and remember to re-run \
853            ./scripts/regenerate_test_rustdocs.sh when needed."
854                .to_string(),
855        ]
856        .join("\n\n")
857    }
858
859    fn run_query_on_crate_pair(
860        semver_query: &SemverQuery,
861        parsed_query: Arc<IndexedQuery>, // The parsed version of semver_query.
862        crate_pair_name: &String,
863        indexed_crate_new: &VersionedIndex<'_>,
864        indexed_crate_old: &VersionedIndex<'_>,
865    ) -> (String, Vec<BTreeMap<String, FieldValue>>) {
866        let adapter = VersionedRustdocAdapter::new(indexed_crate_new, Some(indexed_crate_old))
867            .expect("could not create adapter");
868
869        let results_iter = adapter
870            .run_query_with_indexed_query(parsed_query.clone(), semver_query.arguments.clone())
871            .unwrap();
872
873        // Ensure span data inside `@fold` blocks is deterministically ordered,
874        // since the underlying adapter is non-deterministic due to its iteration over hashtables.
875        // Our heuristic for detecting spans inside `@fold` is to look for:
876        // - list-typed outputs
877        // - with names ending in `_begin_line`
878        // - located inside *one* `@fold` level (i.e. their component is directly under the root).
879        let fold_keys_and_targets: BTreeMap<&str, Vec<Arc<str>>> = parsed_query
880            .outputs
881            .iter()
882            .filter_map(|(name, output)| {
883                if name.as_ref().ends_with("_begin_line") && output.value_type.is_list() {
884                    if let Some(fold) = parsed_query
885                        .ir_query
886                        .root_component
887                        .folds
888                        .values()
889                        .find(|fold| fold.component.root == parsed_query.vids[&output.vid].root)
890                    {
891                        let targets = parsed_query
892                            .outputs
893                            .values()
894                            .filter_map(|o| {
895                                fold.component
896                                    .vertices
897                                    .contains_key(&o.vid)
898                                    .then_some(Arc::clone(&o.name))
899                            })
900                            .collect();
901                        Some((name.as_ref(), targets))
902                    } else {
903                        None
904                    }
905                } else {
906                    None
907                }
908            })
909            .collect();
910
911        let results = results_iter
912            .map(move |mut res| {
913                // Reorder `@fold`-ed span data in increasing `begin_line` order.
914                for (fold_key, targets) in &fold_keys_and_targets {
915                    let mut data: Vec<(u64, usize)> = res[*fold_key]
916                        .as_vec_with(FieldValue::as_u64)
917                        .expect("fold key was not a list of u64")
918                        .into_iter()
919                        .enumerate()
920                        .map(|(idx, val)| (val, idx))
921                        .collect();
922                    data.sort_unstable();
923                    for target in targets {
924                        res.entry(Arc::clone(target)).and_modify(|value| {
925                            // The output of a `@fold @transform(op: "count")` might not be a list here,
926                            // so ignore such outputs. They don't need reordering anyway.
927                            if let Some(slice) = value.as_slice() {
928                                let new_order = data
929                                    .iter()
930                                    .map(|(_, idx)| slice[*idx].clone())
931                                    .collect::<Vec<_>>()
932                                    .into();
933                                *value = new_order;
934                            }
935                        });
936                    }
937                }
938
939                // Turn the output keys into regular strings.
940                res.into_iter().map(|(k, v)| (k.to_string(), v)).collect()
941            })
942            .collect::<Vec<BTreeMap<_, _>>>();
943        (format!("./test_crates/{crate_pair_name}/"), results)
944    }
945
946    fn assert_no_false_positives_in_nonchanged_crate(
947        query_name: &str,
948        semver_query: &SemverQuery,
949        indexed_query: Arc<IndexedQuery>, // The parsed version of semver_query.
950        indexed_crate: &VersionedIndex<'_>,
951        crate_pair_name: &String,
952        crate_version: &str,
953    ) {
954        let (crate_pair_path, output) = run_query_on_crate_pair(
955            semver_query,
956            indexed_query,
957            crate_pair_name,
958            indexed_crate,
959            indexed_crate,
960        );
961        if !output.is_empty() {
962            // This `if` statement means that a false positive happened.
963            // The query was ran on two identical crates (with the same rustdoc)
964            // and it produced a non-empty output, which means that it found issues
965            // in a crate pair that definitely has no semver breaks.
966            let actual_output_name = Box::leak(Box::new(format!(
967                "actual ({crate_pair_name}/{crate_version})"
968            )));
969            let output_difference = pretty_format_output_difference(
970                query_name,
971                "expected (empty)",
972                BTreeMap::new(),
973                actual_output_name,
974                BTreeMap::from([(crate_pair_path, output)]),
975            );
976            panic!(
977                "The query produced a non-empty output when it compared two crates with the same rustdoc.\n{output_difference}\n"
978            );
979        }
980    }
981
982    pub(in crate::query) fn check_query_execution(query_name: &str) {
983        let query_text = std::fs::read_to_string(format!("./src/lints/{query_name}.ron")).unwrap();
984        let semver_query = SemverQuery::from_ron_str(&query_text).unwrap();
985
986        // Map of rustdoc version to parsed query.
987        let mut parsed_query_cache: HashMap<u32, Arc<IndexedQuery>> = HashMap::new();
988
989        let mut query_execution_results: TestOutput = get_test_crate_names()
990            .iter()
991            .map(|crate_pair_name| {
992                let (baseline, current) = get_test_crate_indexes(crate_pair_name);
993
994                let adapter = VersionedRustdocAdapter::new(current, Some(baseline))
995                    .expect("could not create adapter");
996
997                let indexed_query =
998                    parsed_query_cache
999                        .entry(adapter.version())
1000                        .or_insert_with(|| {
1001                            trustfall_core::frontend::parse(adapter.schema(), &semver_query.query)
1002                                .expect("Query failed to parse.")
1003                        });
1004
1005                assert_no_false_positives_in_nonchanged_crate(
1006                    query_name,
1007                    &semver_query,
1008                    indexed_query.clone(),
1009                    current,
1010                    crate_pair_name,
1011                    "new",
1012                );
1013                assert_no_false_positives_in_nonchanged_crate(
1014                    query_name,
1015                    &semver_query,
1016                    indexed_query.clone(),
1017                    baseline,
1018                    crate_pair_name,
1019                    "old",
1020                );
1021
1022                run_query_on_crate_pair(
1023                    &semver_query,
1024                    indexed_query.clone(),
1025                    crate_pair_name,
1026                    current,
1027                    baseline,
1028                )
1029            })
1030            .filter(|(_crate_pair_name, output)| !output.is_empty())
1031            .collect();
1032
1033        // Reorder vector of results into a deterministic order that will compensate for
1034        // nondeterminism in how the results are ordered.
1035        #[derive(Clone, Eq, PartialEq, Ord, PartialOrd)]
1036        enum SortKey {
1037            Span(Arc<str>, usize),
1038            Explicit(Vec<Arc<str>>),
1039        }
1040
1041        let key_func = |elem: &BTreeMap<String, FieldValue>| {
1042            // Queries should either:
1043            // - define `span_filename` and `span_begin_line` values where the lint is being raised,
1044            //   which will then define a total order of results for that query on that crate.
1045            // - define explicit ordering keys, canonically named `ordering_key`,
1046            //   `ordering_key1`, `ordering_key2`, etc., even though any output name
1047            //   with the `ordering_key` prefix works in practice. Those keys form a
1048            //   composite ordering key by being sorted lexicographically by name,
1049            //   then compared lexicographically by their string values, or
1050            if elem.contains_key("ordering_key") {
1051                let mut ordering_key_names: Vec<_> = elem
1052                    .keys()
1053                    .filter(|key| key.starts_with("ordering_key"))
1054                    .collect();
1055                ordering_key_names.sort_unstable();
1056                let ordering_keys = ordering_key_names
1057                    .into_iter()
1058                    .map(|key| {
1059                        let value = elem
1060                            .get(key)
1061                            .unwrap_or_else(|| panic!("{key} output missing from result"));
1062                        Arc::clone(
1063                            value
1064                                .as_arc_str()
1065                                .expect("ordering_key output was not a string"),
1066                        )
1067                    })
1068                    .collect();
1069                SortKey::Explicit(ordering_keys)
1070            } else {
1071                let filename = elem.get("span_filename").map(|value| {
1072                    value
1073                        .as_arc_str()
1074                        .expect("`span_filename` was not a string")
1075                });
1076                let line = elem
1077                    .get("span_begin_line")
1078                    .map(|value: &FieldValue| value.as_usize().expect("begin line was not an int"));
1079                match (filename, line) {
1080                    (Some(filename), Some(line)) => SortKey::Span(Arc::clone(filename), line),
1081                    (Some(_filename), None) => panic!(
1082                        "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."
1083                    ),
1084                    (None, Some(_line)) => panic!(
1085                        "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."
1086                    ),
1087                    (None, None) => panic!(
1088                        "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."
1089                    ),
1090                }
1091            }
1092        };
1093        for value in query_execution_results.values_mut() {
1094            value.sort_unstable_by_key(key_func);
1095        }
1096
1097        insta::with_settings!(
1098            {
1099                prepend_module_to_snapshot => false,
1100                snapshot_path => "../test_outputs/query_execution",
1101                omit_expression => true,
1102            },
1103            {
1104                insta::assert_ron_snapshot!(query_name, &query_execution_results);
1105            }
1106        );
1107
1108        let transparent_results: BTreeMap<_, Vec<BTreeMap<_, TransparentValue>>> =
1109            query_execution_results
1110                .into_iter()
1111                .map(|(k, v)| {
1112                    (
1113                        k,
1114                        v.into_iter()
1115                            .map(|x| x.into_iter().map(|(k, v)| (k, v.into())).collect())
1116                            .collect(),
1117                    )
1118                })
1119                .collect();
1120
1121        let registry = make_handlebars_registry();
1122        if let Some(template) = semver_query.per_result_error_template {
1123            assert!(!transparent_results.is_empty());
1124
1125            let flattened_actual_results: Vec<_> = transparent_results.values().flatten().collect();
1126            for semver_violation_result in flattened_actual_results {
1127                registry
1128                    .render_template(&template, semver_violation_result)
1129                    .with_context(|| "Error instantiating semver query template.")
1130                    .expect("could not materialize template");
1131            }
1132        }
1133
1134        if let Some(witness) = semver_query.witness {
1135            let actual_witnesses: BTreeMap<_, BTreeSet<_>> = transparent_results
1136                .iter()
1137                .map(|(k, v)| {
1138                    (
1139                        Cow::Borrowed(k.as_str()),
1140                        v.iter()
1141                            .map(|values| {
1142                                let Some(TransparentValue::String(filename)) = values.get("span_filename") else {
1143                                    unreachable!("Missing span_filename String, this should be validated above")
1144                                };
1145                                let begin_line = match values.get("span_begin_line") {
1146                                    Some(TransparentValue::Int64(i)) => *i as usize,
1147                                    Some(TransparentValue::Uint64(n)) => *n as usize,
1148                                    _ => unreachable!("Missing span_begin_line Int, this should be validated above"),
1149                                };
1150
1151                                // TODO: Run witness queries and generate full witness here.
1152                                WitnessOutput {
1153                                    filename: filename.to_string(),
1154                                    begin_line,
1155                                    hint: registry
1156                                        .render_template(&witness.hint_template, values)
1157                                        .expect("error rendering hint template"),
1158                                }
1159                            })
1160                            .collect(),
1161                    )
1162                })
1163                .collect();
1164
1165            insta::with_settings!(
1166                {
1167                    prepend_module_to_snapshot => false,
1168                    snapshot_path => "../test_outputs/witnesses",
1169                    omit_expression => true,
1170                    description => format!(
1171                        "Lint `{query_name}` did not have the expected witness output.\n\
1172                        See https://github.com/obi1kenobi/cargo-semver-checks/blob/main/CONTRIBUTING.md#testing-witnesses\n\
1173                        for more information."
1174                    ),
1175                },
1176                {
1177                    let formatted_witnesses = toml::to_string_pretty(&actual_witnesses)
1178                        .expect("failed to serialize witness snapshots as TOML");
1179                    insta::assert_snapshot!(query_name, formatted_witnesses);
1180                }
1181            );
1182        }
1183    }
1184
1185    /// Helper function to construct a blank query with a given id, lint level, and required
1186    /// version bump.
1187    #[must_use]
1188    fn make_blank_query(
1189        id: String,
1190        lint_level: LintLevel,
1191        required_update: RequiredSemverUpdate,
1192    ) -> SemverQuery {
1193        SemverQuery {
1194            id,
1195            lint_level,
1196            required_update,
1197            human_readable_name: String::new(),
1198            description: String::new(),
1199            reference: None,
1200            reference_link: None,
1201            query: String::new(),
1202            arguments: BTreeMap::new(),
1203            error_message: String::new(),
1204            per_result_error_template: None,
1205            witness: None,
1206        }
1207    }
1208
1209    #[test]
1210    fn test_overrides() {
1211        let mut stack = OverrideStack::new();
1212        stack.push(&OverrideMap::from_iter([
1213            (
1214                "query1".into(),
1215                QueryOverride {
1216                    lint_level: Some(LintLevel::Allow),
1217                    required_update: Some(RequiredSemverUpdate::Minor),
1218                },
1219            ),
1220            (
1221                "query2".into(),
1222                QueryOverride {
1223                    lint_level: None,
1224                    required_update: Some(RequiredSemverUpdate::Minor),
1225                },
1226            ),
1227        ]));
1228
1229        let q1 = make_blank_query(
1230            "query1".into(),
1231            LintLevel::Deny,
1232            RequiredSemverUpdate::Major,
1233        );
1234        let q2 = make_blank_query(
1235            "query2".into(),
1236            LintLevel::Warn,
1237            RequiredSemverUpdate::Major,
1238        );
1239
1240        // Should pick overridden values.
1241        assert_eq!(stack.effective_lint_level(&q1), LintLevel::Allow);
1242        assert_eq!(
1243            stack.effective_required_update(&q1),
1244            RequiredSemverUpdate::Minor
1245        );
1246
1247        // Should pick overridden value for semver and fall back to default lint level
1248        // which is not overridden
1249        assert_eq!(stack.effective_lint_level(&q2), LintLevel::Warn);
1250        assert_eq!(
1251            stack.effective_required_update(&q2),
1252            RequiredSemverUpdate::Minor
1253        );
1254    }
1255
1256    #[test]
1257    fn test_override_precedence() {
1258        let mut stack = OverrideStack::new();
1259        stack.push(&OverrideMap::from_iter([
1260            (
1261                "query1".into(),
1262                QueryOverride {
1263                    lint_level: Some(LintLevel::Allow),
1264                    required_update: Some(RequiredSemverUpdate::Minor),
1265                },
1266            ),
1267            (
1268                ("query2".into()),
1269                QueryOverride {
1270                    lint_level: None,
1271                    required_update: Some(RequiredSemverUpdate::Minor),
1272                },
1273            ),
1274        ]));
1275
1276        stack.push(&OverrideMap::from_iter([(
1277            "query1".into(),
1278            QueryOverride {
1279                required_update: None,
1280                lint_level: Some(LintLevel::Warn),
1281            },
1282        )]));
1283
1284        let q1 = make_blank_query(
1285            "query1".into(),
1286            LintLevel::Deny,
1287            RequiredSemverUpdate::Major,
1288        );
1289        let q2 = make_blank_query(
1290            "query2".into(),
1291            LintLevel::Warn,
1292            RequiredSemverUpdate::Major,
1293        );
1294
1295        // Should choose overridden value at the top of the stack
1296        assert_eq!(stack.effective_lint_level(&q1), LintLevel::Warn);
1297        // Should fall back to a configured value lower in the stack because
1298        // top is not set.
1299        assert_eq!(
1300            stack.effective_required_update(&q1),
1301            RequiredSemverUpdate::Minor
1302        );
1303
1304        // Should pick overridden value for semver and fall back to default lint level
1305        // which is not overridden
1306        assert_eq!(stack.effective_lint_level(&q2), LintLevel::Warn);
1307        assert_eq!(
1308            stack.effective_required_update(&q2),
1309            RequiredSemverUpdate::Minor
1310        );
1311    }
1312
1313    /// Makes sure we can specify [`InheritedValue`]s with `Inherited(...)`
1314    /// and untagged variants as [`TransparentValue`]s.
1315    #[test]
1316    fn test_inherited_value_deserialization() {
1317        let my_map: BTreeMap<String, InheritedValue> = ron::from_str(
1318            r#"{
1319                "abc": (inherit: "abc"),
1320                "string": "literal_string",
1321                "int": -30,
1322                "int_list": [-30, -2],
1323                "string_list": ["abc", "123"],
1324                }"#,
1325        )
1326        .expect("deserialization failed");
1327
1328        let Some(InheritedValue::Inherited { inherit: abc }) = my_map.get("abc") else {
1329            panic!("Expected Inherited, got {:?}", my_map.get("abc"));
1330        };
1331
1332        assert_eq!(abc, "abc");
1333
1334        let Some(InheritedValue::Constant(TransparentValue::String(string))) = my_map.get("string")
1335        else {
1336            panic!("Expected Constant(String), got {:?}", my_map.get("string"));
1337        };
1338
1339        assert_eq!(&**string, "literal_string");
1340
1341        let Some(InheritedValue::Constant(TransparentValue::Int64(int))) = my_map.get("int") else {
1342            panic!("Expected Constant(Int64), got {:?}", my_map.get("int"));
1343        };
1344
1345        assert_eq!(*int, -30);
1346
1347        let Some(InheritedValue::Constant(TransparentValue::List(ints))) = my_map.get("int_list")
1348        else {
1349            panic!("Expected Constant(List), got {:?}", my_map.get("lint_list"));
1350        };
1351
1352        let Some(TransparentValue::Int64(-30)) = ints.first() else {
1353            panic!("Expected Int64(-30), got {:?}", ints.first());
1354        };
1355
1356        let Some(TransparentValue::Int64(-2)) = ints.get(1) else {
1357            panic!("Expected Int64(-30), got {:?}", ints.get(1));
1358        };
1359
1360        let Some(InheritedValue::Constant(TransparentValue::List(strs))) =
1361            my_map.get("string_list")
1362        else {
1363            panic!(
1364                "Expected Constant(List), got {:?}",
1365                my_map.get("string_list")
1366            );
1367        };
1368
1369        let Some(TransparentValue::String(s)) = strs.first() else {
1370            panic!("Expected String, got {:?}", strs.first());
1371        };
1372
1373        assert_eq!(&**s, "abc");
1374
1375        let Some(TransparentValue::String(s)) = strs.get(1) else {
1376            panic!("Expected String, got {:?}", strs.get(1));
1377        };
1378
1379        assert_eq!(&**s, "123");
1380
1381        ron::from_str::<InheritedValue>(r#"[(inherit: "invalid")]"#)
1382            .expect_err("nested values should be TransparentValues, not InheritedValues");
1383    }
1384
1385    pub(super) fn check_all_lint_files_are_used_in_add_lints(added_lints: &[&str]) {
1386        let mut lints_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1387        lints_dir.push("src");
1388        lints_dir.push("lints");
1389
1390        let expected_lints: BTreeSet<_> = added_lints.iter().copied().collect();
1391        let mut missing_lints: BTreeSet<String> = Default::default();
1392
1393        let dir_contents =
1394            fs_err::read_dir(lints_dir).expect("failed to read 'src/lints' directory");
1395        for file in dir_contents {
1396            let file = file.expect("failed to examine file");
1397            let path = file.path();
1398
1399            // Check if we found a `*.ron` file. If so, that's a lint.
1400            if path.extension().map(|x| x.to_string_lossy()) == Some(Cow::Borrowed("ron")) {
1401                let stem = path
1402                    .file_stem()
1403                    .map(|x| x.to_string_lossy())
1404                    .expect("failed to get file name as utf-8");
1405
1406                // Check if the lint was added using our `add_lints!()` macro.
1407                // If not, that's an error.
1408                if !expected_lints.contains(stem.as_ref()) {
1409                    missing_lints.insert(stem.to_string());
1410                }
1411            }
1412        }
1413
1414        assert!(
1415            missing_lints.is_empty(),
1416            "some lints in 'src/lints/' haven't been registered using the `add_lints!()` macro, \
1417            so they won't be part of cargo-semver-checks: {missing_lints:?}"
1418        )
1419    }
1420
1421    #[test]
1422    fn lint_file_names_and_ids_match() {
1423        let mut lints_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1424        lints_dir.push("src");
1425        lints_dir.push("lints");
1426
1427        for entry in fs_err::read_dir(&lints_dir).expect("failed to read 'src/lints' directory") {
1428            let entry = entry.expect("failed to examine file");
1429            let path = entry.path();
1430
1431            if path.extension().and_then(OsStr::to_str) != Some("ron") {
1432                continue;
1433            }
1434
1435            let stem = path
1436                .file_stem()
1437                .and_then(OsStr::to_str)
1438                .expect("failed to get file name as utf-8");
1439
1440            assert!(
1441                stem.chars().all(|ch| ch.is_ascii_lowercase() || ch == '_'),
1442                "lint file name '{stem}' is not snake_case"
1443            );
1444            assert!(
1445                !stem.starts_with('_'),
1446                "lint file name '{stem}' must not start with '_'"
1447            );
1448            assert!(
1449                !stem.ends_with('_'),
1450                "lint file name '{stem}' must not end with '_'"
1451            );
1452            assert!(
1453                !stem.contains("__"),
1454                "lint file name '{stem}' must not contain '__'"
1455            );
1456
1457            let query_text =
1458                fs_err::read_to_string(&path).expect("failed to read lint definition file");
1459            let semver_query =
1460                SemverQuery::from_ron_str(&query_text).expect("failed to parse lint definition");
1461
1462            assert_eq!(
1463                stem,
1464                semver_query.id,
1465                "lint id does not match file name for {}",
1466                path.display()
1467            );
1468        }
1469    }
1470
1471    #[test]
1472    fn witness_hint_templates_have_whitespace_hygiene() {
1473        let mut issues = Vec::new();
1474
1475        for (query_name, semver_query) in SemverQuery::all_queries() {
1476            let Some(witness) = semver_query.witness else {
1477                continue;
1478            };
1479
1480            for (line_index, line) in witness.hint_template.lines().enumerate() {
1481                if line.ends_with([' ', '\t']) {
1482                    issues.push(format!(
1483                        "witness hint template for {query_name} has trailing whitespace on line {}: {:?}",
1484                        line_index + 1,
1485                        line
1486                    ));
1487                }
1488            }
1489
1490            if witness.hint_template.contains('\n') || witness.hint_template.contains('\r') {
1491                let trimmed = witness.hint_template.trim_end_matches(['\r', '\n']);
1492                let newline_suffix = &witness.hint_template[trimmed.len()..];
1493                if newline_suffix != "\n" && newline_suffix != "\r\n" {
1494                    issues.push(format!(
1495                        "witness hint template for {query_name} must end with exactly one trailing newline"
1496                    ));
1497                }
1498            }
1499        }
1500
1501        assert!(
1502            issues.is_empty(),
1503            "witness hint templates have whitespace hygiene issues:\n{}",
1504            issues.join("\n")
1505        );
1506    }
1507
1508    #[test]
1509    fn test_data_is_fresh() -> anyhow::Result<()> {
1510        // Adds the modification time of all files in `{dir}/**/*.{rs,toml,json}` to `set`, excluding
1511        // the `target` directory.
1512        fn recursive_file_times<P: Into<PathBuf>>(
1513            dir: P,
1514            set: &mut BTreeSet<SystemTime>,
1515        ) -> std::io::Result<()> {
1516            for item in fs_err::read_dir(dir)? {
1517                let item = item?;
1518                let metadata = item.metadata()?;
1519                if metadata.is_dir() {
1520                    // Don't recurse into the `target` directory.
1521                    if item.file_name() == "target" {
1522                        continue;
1523                    }
1524                    recursive_file_times(item.path(), set)?;
1525                } else if let Some("rs" | "toml" | "json") =
1526                    item.path().extension().and_then(OsStr::to_str)
1527                {
1528                    set.insert(metadata.modified()?);
1529                }
1530            }
1531
1532            Ok(())
1533        }
1534
1535        let test_crate_dir = Path::new("test_crates");
1536        let localdata_dir = Path::new("localdata").join("test_data");
1537
1538        if !localdata_dir.fs_err_try_exists()? {
1539            panic!(
1540                "The localdata directory '{}' does not exist yet.\n\
1541                Please run `scripts/regenerate_test_rustdocs.sh`.",
1542                localdata_dir.display()
1543            );
1544        }
1545
1546        for test_crate in fs_err::read_dir(test_crate_dir)? {
1547            let test_crate = test_crate?;
1548
1549            if !test_crate.metadata()?.is_dir() {
1550                continue;
1551            }
1552
1553            if !test_crate
1554                .path()
1555                .join("new")
1556                .join("Cargo.toml")
1557                .fs_err_try_exists()?
1558                || !test_crate
1559                    .path()
1560                    .join("old")
1561                    .join("Cargo.toml")
1562                    .fs_err_try_exists()?
1563            {
1564                continue;
1565            }
1566
1567            for version in ["new", "old"] {
1568                let test_crate_path = test_crate.path().join(version);
1569
1570                let mut test_crate_times = BTreeSet::new();
1571                recursive_file_times(test_crate_path.clone(), &mut test_crate_times)?;
1572
1573                let localdata_path = localdata_dir.join(test_crate.file_name()).join(version);
1574                let mut localdata_times = BTreeSet::new();
1575
1576                recursive_file_times(localdata_path.clone(), &mut localdata_times).context(
1577                    "If this directory doesn't exist, run `scripts/regenerate_test_rustdocs.sh`",
1578                )?;
1579
1580                // if the most recently modified test crate file comes after the earliest localdata
1581                // file, it is potentially stale
1582                if let (Some(test_max), Some(local_min)) =
1583                    (test_crate_times.last(), localdata_times.first())
1584                    && test_max > local_min
1585                {
1586                    panic!(
1587                        "Files in the '{}' directory are newer than the local data generated by \n\
1588                            scripts/regenerate_test_rustdocs.sh in '{}'.\n\n\
1589                            Run `scripts/regenerate_test_rustdocs.sh` to generate fresh local data.",
1590                        test_crate_path.display(),
1591                        localdata_path.display()
1592                    )
1593                }
1594            }
1595        }
1596
1597        Ok(())
1598    }
1599}
1600
1601#[cfg(test)]
1602macro_rules! lint_test {
1603    // instantiates a lint test without the optional configuration predicate
1604    ($name:ident) => {
1605        #[test]
1606        fn $name() {
1607            super::tests::check_query_execution(stringify!($name))
1608        }
1609    };
1610    // instantiates a lint test, ignoring the test if the given configuration predicate (the second
1611    // argument) is _not_ met
1612    (($name:ident, $conf_pred:meta)) => {
1613        #[test]
1614        #[cfg_attr(not($conf_pred), ignore)]
1615        fn $name() {
1616            super::tests::check_query_execution(stringify!($name))
1617        }
1618    };
1619}
1620
1621macro_rules! lint_name {
1622    ($name:ident) => {
1623        stringify!($name)
1624    };
1625    (($name:ident, $conf_pred:meta)) => {
1626        stringify!($name)
1627    };
1628}
1629
1630macro_rules! add_lints {
1631    ($($args:tt,)+) => {
1632        #[cfg(test)]
1633        mod tests_lints {
1634            $(
1635                lint_test!($args);
1636            )*
1637
1638            #[test]
1639            fn all_lint_files_are_used_in_add_lints() {
1640                let added_lints = [
1641                    $(
1642                        lint_name!($args),
1643                    )*
1644                ];
1645
1646                super::tests::check_all_lint_files_are_used_in_add_lints(&added_lints);
1647            }
1648        }
1649
1650        fn get_queries() -> Vec<(&'static str, &'static str)> {
1651            vec![
1652                $(
1653                    (
1654                        lint_name!($args),
1655                        include_str!(concat!("lints/", lint_name!($args), ".ron")),
1656                    ),
1657                )*
1658            ]
1659        }
1660    };
1661    ($($args:tt),*) => {
1662        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.");
1663    }
1664}
1665
1666// The following add_lints! invocation is programmatically edited by scripts/make_new_lint.sh
1667// If you must manually edit it, be sure to read the "Requirements" comments in that script first
1668#[rustfmt::skip] // to keep lints with config predicates on a single line
1669add_lints!(
1670    (exported_function_requires_more_target_features, any(target_arch = "x86", target_arch = "x86_64")),
1671    (exported_function_target_feature_added, any(target_arch = "x86", target_arch = "x86_64")),
1672    (safe_function_requires_more_target_features, any(target_arch = "x86", target_arch = "x86_64")),
1673    (safe_function_target_feature_added, any(target_arch = "x86", target_arch = "x86_64")),
1674    (safe_inherent_method_requires_more_target_features, any(target_arch = "x86", target_arch = "x86_64")),
1675    (safe_inherent_method_target_feature_added, any(target_arch = "x86", target_arch = "x86_64")),
1676    (trait_method_target_feature_removed, any(target_arch = "x86", target_arch = "x86_64")),
1677    (unsafe_function_requires_more_target_features, any(target_arch = "x86", target_arch = "x86_64")),
1678    (unsafe_function_target_feature_added, any(target_arch = "x86", target_arch = "x86_64")),
1679    (unsafe_inherent_method_requires_more_target_features, any(target_arch = "x86", target_arch = "x86_64")),
1680    (unsafe_inherent_method_target_feature_added, any(target_arch = "x86", target_arch = "x86_64")),
1681    (unsafe_trait_method_requires_more_target_features, any(target_arch = "x86", target_arch = "x86_64")),
1682    (unsafe_trait_method_target_feature_added, any(target_arch = "x86", target_arch = "x86_64")),
1683    attribute_proc_macro_missing,
1684    auto_trait_impl_added,
1685    auto_trait_impl_removed,
1686    constructible_struct_adds_field,
1687    constructible_struct_adds_private_field,
1688    constructible_struct_changed_type,
1689    copy_impl_added,
1690    declarative_macro_missing,
1691    derive_helper_attr_removed,
1692    derive_proc_macro_missing,
1693    derive_trait_impl_removed,
1694    enum_changed_kind,
1695    enum_discriminants_undefined_non_exhaustive_variant,
1696    enum_discriminants_undefined_non_unit_variant,
1697    enum_marked_non_exhaustive,
1698    enum_missing,
1699    enum_must_use_added,
1700    enum_must_use_removed,
1701    enum_no_longer_non_exhaustive,
1702    enum_no_repr_variant_discriminant_changed,
1703    enum_non_exhaustive_struct_variant_field_added,
1704    enum_non_exhaustive_tuple_variant_changed_kind,
1705    enum_non_exhaustive_tuple_variant_field_added,
1706    enum_now_doc_hidden,
1707    enum_repr_int_added,
1708    enum_repr_int_changed,
1709    enum_repr_int_removed,
1710    enum_repr_transparent_removed,
1711    enum_repr_variant_discriminant_changed,
1712    enum_struct_variant_changed_kind,
1713    enum_struct_variant_field_added,
1714    enum_struct_variant_field_marked_deprecated,
1715    enum_struct_variant_field_missing,
1716    enum_struct_variant_field_now_doc_hidden,
1717    enum_tuple_variant_changed_kind,
1718    enum_tuple_variant_field_added,
1719    enum_tuple_variant_field_marked_deprecated,
1720    enum_tuple_variant_field_missing,
1721    enum_tuple_variant_field_now_doc_hidden,
1722    enum_unit_variant_changed_kind,
1723    enum_variant_added,
1724    enum_variant_marked_deprecated,
1725    enum_variant_marked_non_exhaustive,
1726    enum_variant_missing,
1727    enum_variant_no_longer_non_exhaustive,
1728    exhaustive_enum_added,
1729    exhaustive_struct_added,
1730    exhaustive_struct_with_doc_hidden_fields_added,
1731    exhaustive_struct_with_private_fields_added,
1732    exported_function_abi_no_longer_unwind,
1733    exported_function_abi_now_unwind,
1734    exported_function_changed_abi,
1735    exported_function_now_returns_unit,
1736    exported_function_parameter_count_changed,
1737    exported_function_return_value_added,
1738    feature_missing,
1739    feature_newly_enables_feature,
1740    feature_no_longer_enables_feature,
1741    feature_not_enabled_by_default,
1742    function_abi_no_longer_unwind,
1743    function_abi_now_unwind,
1744    function_changed_abi,
1745    function_const_generic_reordered,
1746    function_const_removed,
1747    function_export_name_changed,
1748    function_generic_type_reordered,
1749    function_like_proc_macro_missing,
1750    function_marked_deprecated,
1751    function_missing,
1752    function_must_use_added,
1753    function_must_use_removed,
1754    function_no_longer_unsafe,
1755    function_now_const,
1756    function_now_doc_hidden,
1757    function_now_returns_unit,
1758    function_parameter_count_changed,
1759    function_requires_different_const_generic_params,
1760    function_requires_different_generic_type_params,
1761    function_unsafe_added,
1762    global_value_marked_deprecated,
1763    inherent_associated_const_now_doc_hidden,
1764    inherent_associated_pub_const_added,
1765    inherent_associated_pub_const_missing,
1766    inherent_method_added,
1767    inherent_method_changed_abi,
1768    inherent_method_const_generic_reordered,
1769    inherent_method_const_removed,
1770    inherent_method_generic_type_reordered,
1771    inherent_method_missing,
1772    inherent_method_must_use_added,
1773    inherent_method_must_use_removed,
1774    inherent_method_no_longer_unsafe,
1775    inherent_method_no_longer_unwind,
1776    inherent_method_now_const,
1777    inherent_method_now_doc_hidden,
1778    inherent_method_now_returns_unit,
1779    inherent_method_now_unwind,
1780    inherent_method_unsafe_added,
1781    macro_marked_deprecated,
1782    macro_no_longer_exported,
1783    macro_now_doc_hidden,
1784    method_export_name_changed,
1785    method_no_longer_has_receiver,
1786    method_parameter_count_changed,
1787    method_receiver_mut_ref_became_owned,
1788    method_receiver_ref_became_mut,
1789    method_receiver_ref_became_owned,
1790    method_receiver_type_changed,
1791    method_requires_different_const_generic_params,
1792    method_requires_different_generic_type_params,
1793    module_missing,
1794    non_exhaustive_enum_added,
1795    non_exhaustive_struct_added,
1796    non_exhaustive_struct_changed_type,
1797    partial_ord_enum_struct_variant_fields_reordered,
1798    partial_ord_enum_variants_reordered,
1799    partial_ord_struct_fields_reordered,
1800    proc_macro_marked_deprecated,
1801    proc_macro_now_doc_hidden,
1802    pub_api_sealed_trait_became_unconditionally_sealed,
1803    pub_api_sealed_trait_became_unsealed,
1804    pub_api_sealed_trait_method_receiver_added,
1805    pub_api_sealed_trait_method_receiver_mut_ref_became_ref,
1806    pub_api_sealed_trait_method_return_value_added,
1807    pub_api_sealed_trait_method_target_feature_removed,
1808    pub_const_added,
1809    pub_module_level_const_missing,
1810    pub_module_level_const_now_doc_hidden,
1811    pub_static_added,
1812    pub_static_missing,
1813    pub_static_mut_now_immutable,
1814    pub_static_now_doc_hidden,
1815    pub_static_now_mutable,
1816    repr_align_added,
1817    repr_align_changed,
1818    repr_align_removed,
1819    repr_c_added,
1820    repr_c_enum_struct_variant_fields_reordered,
1821    repr_c_plain_struct_fields_reordered,
1822    repr_c_removed,
1823    repr_packed_added,
1824    repr_packed_changed,
1825    repr_packed_removed,
1826    repr_transparent_added,
1827    sized_impl_removed,
1828    static_became_unsafe,
1829    struct_field_marked_deprecated,
1830    struct_marked_non_exhaustive,
1831    struct_missing,
1832    struct_must_use_added,
1833    struct_must_use_removed,
1834    struct_no_longer_has_non_pub_fields,
1835    struct_no_longer_non_exhaustive,
1836    struct_now_doc_hidden,
1837    struct_pub_field_missing,
1838    struct_pub_field_now_doc_hidden,
1839    struct_repr_transparent_removed,
1840    struct_with_no_pub_fields_changed_type,
1841    struct_with_pub_fields_changed_type,
1842    trait_added_supertrait,
1843    trait_allows_fewer_const_generic_params,
1844    trait_allows_fewer_generic_type_params,
1845    trait_associated_const_added,
1846    trait_associated_const_default_removed,
1847    trait_associated_const_marked_deprecated,
1848    trait_associated_const_now_doc_hidden,
1849    trait_associated_type_added,
1850    trait_associated_type_default_removed,
1851    trait_associated_type_marked_deprecated,
1852    trait_associated_type_now_doc_hidden,
1853    trait_changed_kind,
1854    trait_const_generic_reordered,
1855    trait_generic_type_reordered,
1856    trait_marked_deprecated,
1857    trait_method_added,
1858    trait_method_changed_abi,
1859    trait_method_const_generic_reordered,
1860    trait_method_default_impl_removed,
1861    trait_method_generic_type_reordered,
1862    trait_method_marked_deprecated,
1863    trait_method_missing,
1864    trait_method_no_longer_has_receiver,
1865    trait_method_no_longer_unwind,
1866    trait_method_now_doc_hidden,
1867    trait_method_now_returns_unit,
1868    trait_method_now_unwind,
1869    trait_method_parameter_count_changed,
1870    trait_method_receiver_added,
1871    trait_method_receiver_mut_ref_became_owned,
1872    trait_method_receiver_mut_ref_became_ref,
1873    trait_method_receiver_owned_became_mut_ref,
1874    trait_method_receiver_owned_became_ref,
1875    trait_method_receiver_ref_became_mut,
1876    trait_method_receiver_ref_became_owned,
1877    trait_method_receiver_type_changed,
1878    trait_method_requires_different_const_generic_params,
1879    trait_method_requires_different_generic_type_params,
1880    trait_method_return_value_added,
1881    trait_method_unsafe_added,
1882    trait_method_unsafe_removed,
1883    trait_mismatched_generic_lifetimes,
1884    trait_missing,
1885    trait_must_use_added,
1886    trait_must_use_removed,
1887    trait_newly_sealed,
1888    trait_no_longer_dyn_compatible,
1889    trait_now_doc_hidden,
1890    trait_removed_associated_constant,
1891    trait_removed_associated_type,
1892    trait_removed_supertrait,
1893    trait_requires_more_const_generic_params,
1894    trait_requires_more_generic_type_params,
1895    trait_unsafe_added,
1896    trait_unsafe_removed,
1897    tuple_struct_to_plain_struct,
1898    type_allows_fewer_const_generic_params,
1899    type_allows_fewer_generic_type_params,
1900    type_associated_const_marked_deprecated,
1901    type_const_generic_reordered,
1902    type_generic_type_reordered,
1903    type_marked_deprecated,
1904    type_method_marked_deprecated,
1905    type_mismatched_generic_lifetimes,
1906    type_requires_more_const_generic_params,
1907    type_requires_more_generic_type_params,
1908    unconditionally_sealed_trait_became_pub_api_sealed,
1909    unconditionally_sealed_trait_became_unsealed,
1910    union_added,
1911    union_changed_kind,
1912    union_changed_to_incompatible_struct,
1913    union_field_added_with_all_pub_fields,
1914    union_field_added_with_non_pub_fields,
1915    union_field_marked_deprecated,
1916    union_field_missing,
1917    union_missing,
1918    union_must_use_added,
1919    union_must_use_removed,
1920    union_now_doc_hidden,
1921    union_pub_field_now_doc_hidden,
1922    union_with_multiple_pub_fields_changed_to_struct,
1923    unit_struct_changed_kind,
1924);