-
Notifications
You must be signed in to change notification settings - Fork 653
fix(rome_js_analyzer): escape dollar signs and backticks properly for useTemplate rule #3477
fix(rome_js_analyzer): escape dollar signs and backticks properly for useTemplate rule #3477
Conversation
✅ Deploy Preview for docs-rometools ready!
To edit notification comments on pull requests, go to your Netlify site settings. |
@@ -336,3 +336,16 @@ fn collect_binary_add_expression(node: &JsBinaryExpression) -> Option<Vec<JsAnyE | |||
}; | |||
Some(result) | |||
} | |||
|
|||
/// Escape dollar sign in raw string. | |||
fn escape_dollar_sign(unescaped_string: &str) -> String { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: It would be nice if the function returns a Cow
to avoid the extra allocation if the string doesn't contain any ${
(which is the case for the majority of strings).
for (i, _) in unescaped_string.match_indices("${") { | ||
if i == 0 { | ||
chars.insert(0, '\\'); | ||
} else if chars[i - 1] != '\\' { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice that you thought of that edge case!
There is one more edge case. It is still necessary to add the \
if the input is "\\${"
where the first backslash escapes the second.
This should be converted to \\\${
where the third \
escapes the ${
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@MichaReiser Thanks for telling me more edge cases! I have investigated again based on your advice and it looks like I need to escape backticks as well as dollar signs. (ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals)
In implementing these escaping operations, I would like to use regex crate. Is it OK?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. An alternative would be to not error on strings that contain backticks because it having a template literal with many escapes may as well be less readable than using +
(which we probably can also say is true if the string contains any ${
It should be possible to implement this functionality without using the regex crate as it is only necessary to test for the presence of specific characters. We do something similar in normalize_newlines
where we match on a set of characters and then push the result to a new string. The code doesn't allocate for the cases where the input string contains no new lines because an empty string doesn't allocate in Rust.
tools/crates/rome_formatter/src/format_element.rs
Lines 176 to 200 in 2935526
pub fn normalize_newlines<const N: usize>(text: &str, terminators: [char; N]) -> Cow<str> { | |
let mut result = String::new(); | |
let mut last_end = 0; | |
for (start, part) in text.match_indices(terminators) { | |
result.push_str(&text[last_end..start]); | |
result.push('\n'); | |
last_end = start + part.len(); | |
// If the current character is \r and the | |
// next is \n, skip over the entire sequence | |
if part == "\r" && text[last_end..].starts_with('\n') { | |
last_end += 1; | |
} | |
} | |
// If the result is empty no line terminators were matched, | |
// return the entire input text without allocating a new String | |
if result.is_empty() { | |
Cow::Borrowed(text) | |
} else { | |
result.push_str(&text[last_end..text.len()]); | |
Cow::Owned(result) | |
} | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did a quick test here on a possible generic implementation. I have not spent to much time testing.
We can move this to a util folder and create an extension method for strings.
It is important to implement the already-escaped looking forward. Looking backward doing i-1
it is not UTF8 safe.
fn escape<'a>(unescaped_string: &'a str, needs_escaping: &[&str], escaping_char: char) -> std::borrow::Cow<'a, str> {
let mut escaped = String::new();
let escaping_char_len = escaping_char.len_utf8();
let mut iter = unescaped_string.char_indices();
'unescaped: while let Some((idx, chr)) = iter.next() {
'candidates: for candidate in needs_escaping {
if chr == escaping_char && unescaped_string[idx + escaping_char_len..].starts_with(candidate) {
for _ in candidate.chars().skip(1) {
let _ = iter.next();
}
continue 'candidates;
}
if unescaped_string[idx..].starts_with(candidate) {
if escaped.is_empty() {
escaped = String::with_capacity(unescaped_string.len() * 2);
escaped.push_str(&unescaped_string[0..idx]);
}
escaped.push(escaping_char);
escaped.push_str(candidate);
for _ in candidate.chars().skip(1) {
let _ = iter.next();
}
continue 'unescaped;
}
}
if !escaped.is_empty() {
escaped.push(chr);
}
}
if escaped.is_empty() {
std::borrow::Cow::Borrowed(unescaped_string)
} else {
std::borrow::Cow::Owned(escaped)
}
}
@xunilrj @MichaReiser Thanks you for your reviews and sorry for my late response! I refered to your comments and reimplemented escape method. I would like you to review again. |
${
properly for useTemplate ruleThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking good to me and we can get this merged after adding some documentation to escape
and potential simplification of the escape
implementation
Excellent work. Thank you so much for your contribution! I'll wait for the build to finish and give others some time to chime in. I plan to merge this tomorrow morning (in about 16h ;)). |
Summary
Closes #3451 Escaped
${
properly for useTemplate rule.Test Plan
I add two test cases and confirmed properly escaped
${
.