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}