Skip to main content

fallow_core/plugins/
used_export_names.rs

1//! A path lookup over the plugin used-export rules.
2
3use std::collections::BTreeSet;
4
5use super::{AggregatedPluginResult, CompiledPathRule};
6
7/// The plugin used-export rules of one plugin run, compiled for path lookups.
8///
9/// A framework plugin marks some exports of a file as used by the framework,
10/// for example `revalidate` in a Next.js page. The unused-export analysis
11/// reads the same rules with the same path matcher.
12#[derive(Debug, Clone)]
13pub struct UsedExportNames {
14    rules: Vec<(CompiledPathRule, Vec<String>)>,
15}
16
17impl UsedExportNames {
18    /// The export names that a plugin marks as used in the file at the
19    /// project-root-relative `rel_path`, with forward slashes. The name `*`
20    /// means each export of the file.
21    #[must_use]
22    pub fn names_for(&self, rel_path: &str) -> BTreeSet<&str> {
23        self.rules
24            .iter()
25            .filter(|(path, _)| path.matches(rel_path))
26            .flat_map(|(_, exports)| exports.iter().map(String::as_str))
27            .collect()
28    }
29}
30
31impl AggregatedPluginResult {
32    /// Compiles the used-export rules for path lookups. A rule with an
33    /// invalid glob matches no file, as in the unused-export analysis.
34    #[must_use]
35    pub fn used_export_names(&self) -> UsedExportNames {
36        UsedExportNames {
37            rules: self
38                .used_exports
39                .iter()
40                .filter_map(|rule| {
41                    let path = CompiledPathRule::for_used_export_rule(
42                        &rule.rule.path,
43                        "used_exports pattern",
44                    )?;
45                    Some((path, rule.rule.exports.clone()))
46                })
47                .collect(),
48        }
49    }
50}
51
52#[cfg(test)]
53mod tests {
54    use super::super::{PluginUsedExportRule, UsedExportRule};
55    use super::*;
56
57    fn result(rules: Vec<PluginUsedExportRule>) -> AggregatedPluginResult {
58        AggregatedPluginResult {
59            used_exports: rules,
60            ..AggregatedPluginResult::default()
61        }
62    }
63
64    #[test]
65    fn names_for_a_path_join_each_matching_rule() {
66        let names = result(vec![
67            PluginUsedExportRule::new(
68                "nextjs",
69                UsedExportRule::new("app/**/page.{ts,tsx}", ["default", "revalidate"]),
70            ),
71            PluginUsedExportRule::new("nextjs", UsedExportRule::new("app/**/*.tsx", ["dynamic"])),
72            PluginUsedExportRule::new(
73                "tanstack-router",
74                UsedExportRule::new("src/routes/**/*.tsx", ["Route"]),
75            ),
76        ])
77        .used_export_names();
78
79        assert_eq!(
80            names.names_for("app/blog/page.tsx"),
81            BTreeSet::from(["default", "dynamic", "revalidate"])
82        );
83        assert_eq!(
84            names.names_for("src/routes/index.tsx"),
85            BTreeSet::from(["Route"])
86        );
87        assert!(names.names_for("src/lib/math.ts").is_empty());
88    }
89
90    #[test]
91    fn an_excluded_glob_and_an_invalid_glob_match_no_file() {
92        let names = result(vec![
93            PluginUsedExportRule::new(
94                "tanstack-router",
95                UsedExportRule::new("src/routes/**/*.tsx", ["Route"])
96                    .with_excluded_globs(["src/routes/**/-*.tsx"]),
97            ),
98            PluginUsedExportRule::new("broken", UsedExportRule::new("src/[", ["config"])),
99        ])
100        .used_export_names();
101
102        assert_eq!(
103            names.names_for("src/routes/posts.tsx"),
104            BTreeSet::from(["Route"])
105        );
106        assert!(names.names_for("src/routes/-helper.tsx").is_empty());
107        assert!(names.names_for("src/[").is_empty());
108    }
109}