From 3b4c02c19e4af645fd37e8bff774b05d546dc0b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Ochagav=C3=ADa?= Date: Thu, 8 Nov 2018 15:42:00 +0100 Subject: Validate string literals --- crates/ra_syntax/src/validation/char.rs | 270 ++++++++++++++++++++++++++++++ crates/ra_syntax/src/validation/mod.rs | 20 +++ crates/ra_syntax/src/validation/string.rs | 168 +++++++++++++++++++ 3 files changed, 458 insertions(+) create mode 100644 crates/ra_syntax/src/validation/char.rs create mode 100644 crates/ra_syntax/src/validation/mod.rs create mode 100644 crates/ra_syntax/src/validation/string.rs (limited to 'crates/ra_syntax/src/validation') diff --git a/crates/ra_syntax/src/validation/char.rs b/crates/ra_syntax/src/validation/char.rs new file mode 100644 index 000000000..63f9bad24 --- /dev/null +++ b/crates/ra_syntax/src/validation/char.rs @@ -0,0 +1,270 @@ +use std::u32; + +use arrayvec::ArrayString; + +use crate::{ + ast::{self, AstNode}, + string_lexing::{self, CharComponentKind}, + TextRange, + yellow::{ + SyntaxError, + SyntaxErrorKind::*, + }, +}; + +pub(crate) fn validate_char_node(node: ast::Char, errors: &mut Vec) { + let literal_text = node.text(); + let literal_range = node.syntax().range(); + let mut components = string_lexing::parse_char_literal(literal_text); + let mut len = 0; + for component in &mut components { + len += 1; + let text = &literal_text[component.range]; + let range = component.range + literal_range.start(); + validate_char_component(text, component.kind, range, errors); + } + + if !components.has_closing_quote { + errors.push(SyntaxError::new(UnclosedChar, literal_range)); + } + + if len == 0 { + errors.push(SyntaxError::new(EmptyChar, literal_range)); + } + + if len > 1 { + errors.push(SyntaxError::new(OverlongChar, literal_range)); + } +} + +pub(crate) fn validate_char_component( + text: &str, + kind: CharComponentKind, + range: TextRange, + errors: &mut Vec, +) { + // Validate escapes + use self::CharComponentKind::*; + match kind { + AsciiEscape => { + if text.len() == 1 { + // Escape sequence consists only of leading `\` + errors.push(SyntaxError::new(EmptyAsciiEscape, range)); + } else { + let escape_code = text.chars().skip(1).next().unwrap(); + if !is_ascii_escape(escape_code) { + errors.push(SyntaxError::new(InvalidAsciiEscape, range)); + } + } + } + AsciiCodeEscape => { + // An AsciiCodeEscape has 4 chars, example: `\xDD` + if text.len() < 4 { + errors.push(SyntaxError::new(TooShortAsciiCodeEscape, range)); + } else { + assert!( + text.chars().count() == 4, + "AsciiCodeEscape cannot be longer than 4 chars" + ); + + match u8::from_str_radix(&text[2..], 16) { + Ok(code) if code < 128 => { /* Escape code is valid */ } + Ok(_) => errors.push(SyntaxError::new(AsciiCodeEscapeOutOfRange, range)), + Err(_) => errors.push(SyntaxError::new(MalformedAsciiCodeEscape, range)), + } + } + } + UnicodeEscape => { + assert!(&text[..2] == "\\u", "UnicodeEscape always starts with \\u"); + + if text.len() == 2 { + // No starting `{` + errors.push(SyntaxError::new(MalformedUnicodeEscape, range)); + return; + } + + if text.len() == 3 { + // Only starting `{` + errors.push(SyntaxError::new(UnclosedUnicodeEscape, range)); + return; + } + + let mut code = ArrayString::<[_; 6]>::new(); + let mut closed = false; + for c in text[3..].chars() { + assert!(!closed, "no characters after escape is closed"); + + if c.is_digit(16) { + if code.len() == 6 { + errors.push(SyntaxError::new(OverlongUnicodeEscape, range)); + return; + } + + code.push(c); + } else if c == '_' { + // Reject leading _ + if code.len() == 0 { + errors.push(SyntaxError::new(MalformedUnicodeEscape, range)); + return; + } + } else if c == '}' { + closed = true; + } else { + errors.push(SyntaxError::new(MalformedUnicodeEscape, range)); + return; + } + } + + if !closed { + errors.push(SyntaxError::new(UnclosedUnicodeEscape, range)) + } + + if code.len() == 0 { + errors.push(SyntaxError::new(EmptyUnicodeEcape, range)); + return; + } + + match u32::from_str_radix(&code, 16) { + Ok(code_u32) if code_u32 > 0x10FFFF => { + errors.push(SyntaxError::new(UnicodeEscapeOutOfRange, range)); + } + Ok(_) => { + // Valid escape code + } + Err(_) => { + errors.push(SyntaxError::new(MalformedUnicodeEscape, range)); + } + } + } + CodePoint => { + // These code points must always be escaped + if text == "\t" || text == "\r" { + errors.push(SyntaxError::new(UnescapedCodepoint, range)); + } + } + } +} + +fn is_ascii_escape(code: char) -> bool { + match code { + '\\' | '\'' | '"' | 'n' | 'r' | 't' | '0' => true, + _ => false, + } +} + +#[cfg(test)] +mod test { + use crate::SourceFileNode; + + fn build_file(literal: &str) -> SourceFileNode { + let src = format!("const C: char = '{}';", literal); + SourceFileNode::parse(&src) + } + + fn assert_valid_char(literal: &str) { + let file = build_file(literal); + assert!( + file.errors().len() == 0, + "Errors for literal '{}': {:?}", + literal, + file.errors() + ); + } + + fn assert_invalid_char(literal: &str) { + let file = build_file(literal); + assert!(file.errors().len() > 0); + } + + #[test] + fn test_ansi_codepoints() { + for byte in 0..=255u8 { + match byte { + b'\n' | b'\r' | b'\t' => assert_invalid_char(&(byte as char).to_string()), + b'\'' | b'\\' => { /* Ignore character close and backslash */ } + _ => assert_valid_char(&(byte as char).to_string()), + } + } + } + + #[test] + fn test_unicode_codepoints() { + let valid = ["Ƒ", "バ", "メ", "﷽"]; + for c in &valid { + assert_valid_char(c); + } + } + + #[test] + fn test_unicode_multiple_codepoints() { + let invalid = ["नी", "👨‍👨‍"]; + for c in &invalid { + assert_invalid_char(c); + } + } + + #[test] + fn test_valid_ascii_escape() { + let valid = [ + r"\'", "\"", "\\\\", "\\\"", r"\n", r"\r", r"\t", r"\0", "a", "b", + ]; + for c in &valid { + assert_valid_char(c); + } + } + + #[test] + fn test_invalid_ascii_escape() { + let invalid = [r"\a", r"\?", r"\"]; + for c in &invalid { + assert_invalid_char(c); + } + } + + #[test] + fn test_valid_ascii_code_escape() { + let valid = [r"\x00", r"\x7F", r"\x55"]; + for c in &valid { + assert_valid_char(c); + } + } + + #[test] + fn test_invalid_ascii_code_escape() { + let invalid = [r"\x", r"\x7", r"\xF0"]; + for c in &invalid { + assert_invalid_char(c); + } + } + + #[test] + fn test_valid_unicode_escape() { + let valid = [ + r"\u{FF}", + r"\u{0}", + r"\u{F}", + r"\u{10FFFF}", + r"\u{1_0__FF___FF_____}", + ]; + for c in &valid { + assert_valid_char(c); + } + } + + #[test] + fn test_invalid_unicode_escape() { + let invalid = [ + r"\u", + r"\u{}", + r"\u{", + r"\u{FF", + r"\u{FFFFFF}", + r"\u{_F}", + r"\u{00FFFFF}", + r"\u{110000}", + ]; + for c in &invalid { + assert_invalid_char(c); + } + } +} diff --git a/crates/ra_syntax/src/validation/mod.rs b/crates/ra_syntax/src/validation/mod.rs new file mode 100644 index 000000000..2ff0bc26d --- /dev/null +++ b/crates/ra_syntax/src/validation/mod.rs @@ -0,0 +1,20 @@ +use crate::{ + algo::visit::{visitor_ctx, VisitorCtx}, + ast, + SourceFileNode, + yellow::SyntaxError, +}; + +mod char; +mod string; + +pub(crate) fn validate(file: &SourceFileNode) -> Vec { + let mut errors = Vec::new(); + for node in file.syntax().descendants() { + let _ = visitor_ctx(&mut errors) + .visit::(self::char::validate_char_node) + .visit::(self::string::validate_string_node) + .accept(node); + } + errors +} diff --git a/crates/ra_syntax/src/validation/string.rs b/crates/ra_syntax/src/validation/string.rs new file mode 100644 index 000000000..089879d15 --- /dev/null +++ b/crates/ra_syntax/src/validation/string.rs @@ -0,0 +1,168 @@ +use crate::{ + ast::{self, AstNode}, + string_lexing::{self, StringComponentKind}, + yellow::{ + SyntaxError, + SyntaxErrorKind::*, + }, +}; + +use super::char; + +pub(crate) fn validate_string_node(node: ast::String, errors: &mut Vec) { + let literal_text = node.text(); + let literal_range = node.syntax().range(); + let mut components = string_lexing::parse_string_literal(literal_text); + for component in &mut components { + let range = component.range + literal_range.start(); + + match component.kind { + StringComponentKind::Char(kind) => { + // Chars must escape \t, \n and \r codepoints, but strings don't + let text = &literal_text[component.range]; + match text { + "\t" | "\n" | "\r" => { /* always valid */ } + _ => char::validate_char_component(text, kind, range, errors), + } + } + StringComponentKind::IgnoreNewline => { /* always valid */ } + } + } + + if !components.has_closing_quote { + errors.push(SyntaxError::new(UnclosedString, literal_range)); + } +} + +#[cfg(test)] +mod test { + use crate::SourceFileNode; + + fn build_file(literal: &str) -> SourceFileNode { + let src = format!(r#"const S: &'static str = "{}";"#, literal); + println!("Source: {}", src); + SourceFileNode::parse(&src) + } + + fn assert_valid_str(literal: &str) { + let file = build_file(literal); + assert!( + file.errors().len() == 0, + "Errors for literal '{}': {:?}", + literal, + file.errors() + ); + } + + fn assert_invalid_str(literal: &str) { + let file = build_file(literal); + assert!(file.errors().len() > 0); + } + + #[test] + fn test_ansi_codepoints() { + for byte in 0..=255u8 { + match byte { + b'\"' | b'\\' => { /* Ignore string close and backslash */ } + _ => assert_valid_str(&(byte as char).to_string()), + } + } + } + + #[test] + fn test_unicode_codepoints() { + let valid = ["Ƒ", "バ", "メ", "﷽"]; + for c in &valid { + assert_valid_str(c); + } + } + + #[test] + fn test_unicode_multiple_codepoints() { + let valid = ["नी", "👨‍👨‍"]; + for c in &valid { + assert_valid_str(c); + } + } + + #[test] + fn test_valid_ascii_escape() { + let valid = [r"\'", r#"\""#, r"\\", r"\n", r"\r", r"\t", r"\0", "a", "b"]; + for c in &valid { + assert_valid_str(c); + } + } + + #[test] + fn test_invalid_ascii_escape() { + let invalid = [r"\a", r"\?", r"\"]; + for c in &invalid { + assert_invalid_str(c); + } + } + + #[test] + fn test_valid_ascii_code_escape() { + let valid = [r"\x00", r"\x7F", r"\x55"]; + for c in &valid { + assert_valid_str(c); + } + } + + #[test] + fn test_invalid_ascii_code_escape() { + let invalid = [r"\x", r"\x7", r"\xF0"]; + for c in &invalid { + assert_invalid_str(c); + } + } + + #[test] + fn test_valid_unicode_escape() { + let valid = [ + r"\u{FF}", + r"\u{0}", + r"\u{F}", + r"\u{10FFFF}", + r"\u{1_0__FF___FF_____}", + ]; + for c in &valid { + assert_valid_str(c); + } + } + + #[test] + fn test_invalid_unicode_escape() { + let invalid = [ + r"\u", + r"\u{}", + r"\u{", + r"\u{FF", + r"\u{FFFFFF}", + r"\u{_F}", + r"\u{00FFFFF}", + r"\u{110000}", + ]; + for c in &invalid { + assert_invalid_str(c); + } + } + + #[test] + fn test_mixed() { + assert_valid_str( + r"This is the tale of a string +with a newline in between, some emoji (👨‍👨‍) here and there, +unicode escapes like this: \u{1FFBB} and weird stuff like +this ﷽", + ); + } + + #[test] + fn test_ignore_newline() { + assert_valid_str( + "Hello \ + World", + ); + } +} -- cgit v1.2.3