selene_lib/lints/
bad_string_escape.rs

1use super::*;
2use std::convert::Infallible;
3
4use full_moon::{
5    ast::{self, Ast},
6    tokenizer,
7    visitors::Visitor,
8};
9use regex::Regex;
10
11lazy_static::lazy_static! {
12    static ref STRING_ESCAPE_REGEX: Regex = Regex::new(r"\\(u\{|.)([\da-fA-F]*)(\}?)").unwrap();
13}
14
15enum ReasonWhy {
16    CodepointTooHigh,
17    DecimalTooHigh,
18    DoubleInSingle,
19    Invalid,
20    Malformed,
21    SingleInDouble,
22}
23
24pub struct BadStringEscapeLint;
25
26impl Lint for BadStringEscapeLint {
27    type Config = ();
28    type Error = Infallible;
29
30    const SEVERITY: Severity = Severity::Warning;
31    const LINT_TYPE: LintType = LintType::Correctness;
32
33    fn new(_: Self::Config) -> Result<Self, Self::Error> {
34        Ok(BadStringEscapeLint)
35    }
36
37    fn pass(&self, ast: &Ast, context: &Context, _: &AstContext) -> Vec<Diagnostic> {
38        let mut visitor = BadStringEscapeVisitor {
39            sequences: Vec::new(),
40            roblox: context.is_roblox(),
41        };
42
43        visitor.visit_ast(ast);
44
45        visitor
46            .sequences
47            .iter()
48            .map(|sequence| match sequence.issue {
49                ReasonWhy::Invalid => Diagnostic::new(
50                    "bad_string_escape",
51                    "string escape sequence doesn't exist".to_owned(),
52                    Label::new(sequence.range.to_owned()),
53                ),
54                ReasonWhy::Malformed => Diagnostic::new(
55                    "bad_string_escape",
56                    "string escape sequence is malformed".to_owned(),
57                    Label::new(sequence.range.to_owned()),
58                ),
59                ReasonWhy::DecimalTooHigh => Diagnostic::new_complete(
60                    "bad_string_escape",
61                    "decimal escape is too high".to_owned(),
62                    Label::new(sequence.range.to_owned()),
63                    vec![
64                        "help: the maximum codepoint allowed in decimal escapes is `255`"
65                            .to_owned(),
66                    ],
67                    Vec::new(),
68                ),
69                ReasonWhy::CodepointTooHigh => Diagnostic::new_complete(
70                    "bad_string_escape",
71                    "unicode codepoint is too high for this escape sequence".to_owned(),
72                    Label::new(sequence.range.to_owned()),
73                    vec![
74                        "help: the maximum codepoint allowed in unicode escapes is `10ffff`"
75                            .to_owned(),
76                    ],
77                    Vec::new(),
78                ),
79                ReasonWhy::DoubleInSingle => Diagnostic::new(
80                    "bad_string_escape",
81                    "double quotes do not have to be escaped when inside single quoted strings"
82                        .to_owned(),
83                    Label::new(sequence.range.to_owned()),
84                ),
85                ReasonWhy::SingleInDouble => Diagnostic::new(
86                    "bad_string_escape",
87                    "single quotes do not have to be escaped when inside double quoted strings"
88                        .to_owned(),
89                    Label::new(sequence.range.to_owned()),
90                ),
91            })
92            .collect()
93    }
94}
95
96struct BadStringEscapeVisitor {
97    sequences: Vec<StringEscapeSequence>,
98    roblox: bool,
99}
100
101struct StringEscapeSequence {
102    range: (usize, usize),
103    issue: ReasonWhy,
104}
105
106impl Visitor for BadStringEscapeVisitor {
107    fn visit_expression(&mut self, node: &ast::Expression) {
108        if_chain::if_chain! {
109            if let ast::Expression::String(token) = node;
110            if let tokenizer::TokenType::StringLiteral { literal, quote_type, .. } = token.token_type();
111            if *quote_type != tokenizer::StringLiteralQuoteType::Brackets;
112            then {
113                let quote_type = *quote_type;
114                let value_start = node.range().unwrap().0.bytes();
115
116                for captures in STRING_ESCAPE_REGEX.captures_iter(literal) {
117                    let start = value_start + captures.get(1).unwrap().start();
118
119                    match &captures[1] {
120                        "a" | "b" | "f" | "n" | "r" | "t" | "v" | "\\" => {},
121                        "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" => {
122                            if captures[2].len() > 1 {
123                                let hundreds = captures[1].parse::<u16>().unwrap_or(0) * 100;
124                                let tens = captures[2][1..2].parse::<u16>().unwrap_or(0);
125                                if hundreds + tens > 0xff {
126                                    self.sequences.push(
127                                        StringEscapeSequence{
128                                            range: (start, start + 4),
129                                            issue: ReasonWhy::DecimalTooHigh,
130                                        }
131                                    );
132                                }
133                            }
134                        },
135                        "\"" => {
136                            if quote_type == tokenizer::StringLiteralQuoteType::Single {
137                                self.sequences.push(
138                                    StringEscapeSequence{
139                                        range: (start, start + 2),
140                                        issue: ReasonWhy::DoubleInSingle,
141                                    }
142                                );
143                            }
144                        },
145                        "'" => {
146                            if quote_type == tokenizer::StringLiteralQuoteType::Double {
147                                self.sequences.push(
148                                    StringEscapeSequence{
149                                        range: (start, start + 2),
150                                        issue: ReasonWhy::SingleInDouble,
151                                    }
152                                );
153                            }
154                        },
155                        "z" => {
156                            if !self.roblox {
157                                self.sequences.push(
158                                    StringEscapeSequence{
159                                        range: (start, start + 2),
160                                        issue: ReasonWhy::Invalid,
161                                    }
162                                );
163                            }
164                        },
165                        "x" => {
166                            if !self.roblox {
167                                self.sequences.push(
168                                    StringEscapeSequence{
169                                        range: (start, start + 2),
170                                        issue: ReasonWhy::Invalid,
171                                    }
172                                );
173                                continue;
174                            }
175                            let second_capture_len = captures[2].len();
176                            if second_capture_len != 2 {
177                                self.sequences.push(
178                                    StringEscapeSequence{
179                                        range: (start, start + second_capture_len + 2),
180                                        issue: ReasonWhy::Malformed
181                                    }
182                                );
183                            }
184                        },
185                        "u{" => {
186                            if !self.roblox {
187                                self.sequences.push(
188                                    StringEscapeSequence{
189                                        range: (start, start + 2),
190                                        issue: ReasonWhy::Invalid,
191                                    }
192                                );
193                                continue;
194                            }
195                            let second_capture_len = captures[2].len();
196                            if captures[3].is_empty() {
197                                self.sequences.push(
198                                    StringEscapeSequence{
199                                        range: (start, start + second_capture_len + 3),
200                                        issue: ReasonWhy::Malformed,
201                                    }
202                                );
203                                continue;
204                            }
205                            let codepoint = u32::from_str_radix(&captures[2], 16).unwrap_or(0x0011_0000);
206                            if codepoint > 0x0010_ffff {
207                                self.sequences.push(
208                                    StringEscapeSequence {
209                                        range: (start, start + second_capture_len + 4),
210                                        issue: ReasonWhy::CodepointTooHigh,
211                                    }
212                                );
213                            }
214                        },
215                        _ => {
216                            self.sequences.push(
217                                StringEscapeSequence{
218                                    range: (start, start + 2),
219                                    issue: ReasonWhy::Invalid,
220                                }
221                            );
222                        },
223                    }
224                }
225            }
226        }
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::{super::test_util::test_lint, *};
233
234    #[test]
235    fn test_bad_string_escape() {
236        test_lint(
237            BadStringEscapeLint::new(()).unwrap(),
238            "bad_string_escape",
239            "lua51_string_escapes",
240        );
241    }
242
243    #[test]
244    #[cfg(feature = "roblox")]
245    fn test_bad_string_escape_roblox() {
246        test_lint(
247            BadStringEscapeLint::new(()).unwrap(),
248            "bad_string_escape",
249            "roblox_string_escapes",
250        );
251    }
252}