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