selene_lib/lints/
global_usage.rs

1use super::*;
2use std::collections::HashSet;
3
4use full_moon::ast::Ast;
5use regex::Regex;
6use serde::Deserialize;
7
8fn is_global(name: &str, roblox: bool) -> bool {
9    (roblox && name == "shared") || name == "_G"
10}
11
12#[derive(Clone, Default, Deserialize)]
13#[serde(default)]
14pub struct GlobalConfig {
15    ignore_pattern: Option<String>,
16}
17
18pub struct GlobalLint {
19    ignore_pattern: Option<Regex>,
20}
21
22impl Lint for GlobalLint {
23    type Config = GlobalConfig;
24    type Error = regex::Error;
25
26    const SEVERITY: Severity = Severity::Warning;
27    const LINT_TYPE: LintType = LintType::Complexity;
28
29    fn new(config: Self::Config) -> Result<Self, Self::Error> {
30        Ok(GlobalLint {
31            ignore_pattern: config
32                .ignore_pattern
33                .map(|ignore_pattern| Regex::new(&ignore_pattern))
34                .transpose()?,
35        })
36    }
37
38    fn pass(&self, _: &Ast, context: &Context, ast_context: &AstContext) -> Vec<Diagnostic> {
39        let mut checked = HashSet::new(); // TODO: Fix ScopeManager having duplicate references
40
41        ast_context
42            .scope_manager
43            .references
44            .iter()
45            .filter(|(_, reference)| {
46                if !checked.contains(&reference.identifier) {
47                    checked.insert(reference.identifier);
48
49                    let matches_ignore_pattern = match &self.ignore_pattern {
50                        Some(ignore_pattern) => match reference
51                            .indexing
52                            .as_ref()
53                            .and_then(|indexing| indexing.first())
54                            .and_then(|index_entry| index_entry.static_name.as_ref())
55                        {
56                            // Trim whitespace at the end as `_G.a  = 1` yields `a  `
57                            Some(name) => ignore_pattern
58                                .is_match(name.to_string().trim_end_matches(char::is_whitespace)),
59                            None => false,
60                        },
61                        None => false,
62                    };
63
64                    is_global(&reference.name, context.is_roblox())
65                        && !matches_ignore_pattern
66                        && reference.resolved.is_none()
67                } else {
68                    false
69                }
70            })
71            .map(|(_, reference)| {
72                Diagnostic::new(
73                    "global_usage",
74                    format!(
75                        "use of `{}` is not allowed, structure your code in a more idiomatic way",
76                        reference.name
77                    ),
78                    Label::new(reference.identifier),
79                )
80            })
81            .collect()
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::{super::test_util::test_lint, *};
88
89    #[test]
90    fn test_global_usage() {
91        test_lint(
92            GlobalLint::new(GlobalConfig::default()).unwrap(),
93            "global_usage",
94            "global_usage",
95        );
96    }
97
98    #[test]
99    fn test_invalid_regex() {
100        assert!(GlobalLint::new(GlobalConfig {
101            ignore_pattern: Some("(".to_owned()),
102        })
103        .is_err());
104    }
105
106    #[test]
107    fn test_global_usage_ignore() {
108        test_lint(
109            GlobalLint::new(GlobalConfig {
110                ignore_pattern: Some("^_.*_$".to_owned()),
111            })
112            .unwrap(),
113            "global_usage",
114            "global_usage_ignore",
115        );
116    }
117}