Render literal escaping errors in hovers

This commit is contained in:
Lukas Wirth 2024-05-13 12:51:57 +02:00
parent 253929f6ae
commit a39c0493a1
10 changed files with 71 additions and 82 deletions

View file

@ -136,15 +136,15 @@ impl From<ast::LiteralKind> for Literal {
Literal::Float(FloatTypeWrapper::new(lit.value().unwrap_or(Default::default())), ty) Literal::Float(FloatTypeWrapper::new(lit.value().unwrap_or(Default::default())), ty)
} }
LiteralKind::ByteString(bs) => { LiteralKind::ByteString(bs) => {
let text = bs.value().map(Box::from).unwrap_or_else(Default::default); let text = bs.value().map_or_else(|_| Default::default(), Box::from);
Literal::ByteString(text) Literal::ByteString(text)
} }
LiteralKind::String(s) => { LiteralKind::String(s) => {
let text = s.value().map(Box::from).unwrap_or_else(Default::default); let text = s.value().map_or_else(|_| Default::default(), Box::from);
Literal::String(text) Literal::String(text)
} }
LiteralKind::CString(s) => { LiteralKind::CString(s) => {
let text = s.value().map(Box::from).unwrap_or_else(Default::default); let text = s.value().map_or_else(|_| Default::default(), Box::from);
Literal::CString(text) Literal::CString(text)
} }
LiteralKind::Byte(b) => { LiteralKind::Byte(b) => {

View file

@ -441,21 +441,21 @@ fn unquote_str(lit: &tt::Literal) -> Option<(String, Span)> {
let span = lit.span; let span = lit.span;
let lit = ast::make::tokens::literal(&lit.to_string()); let lit = ast::make::tokens::literal(&lit.to_string());
let token = ast::String::cast(lit)?; let token = ast::String::cast(lit)?;
token.value().map(|it| (it.into_owned(), span)) token.value().ok().map(|it| (it.into_owned(), span))
} }
fn unquote_char(lit: &tt::Literal) -> Option<(char, Span)> { fn unquote_char(lit: &tt::Literal) -> Option<(char, Span)> {
let span = lit.span; let span = lit.span;
let lit = ast::make::tokens::literal(&lit.to_string()); let lit = ast::make::tokens::literal(&lit.to_string());
let token = ast::Char::cast(lit)?; let token = ast::Char::cast(lit)?;
token.value().zip(Some(span)) token.value().ok().zip(Some(span))
} }
fn unquote_byte_string(lit: &tt::Literal) -> Option<(Vec<u8>, Span)> { fn unquote_byte_string(lit: &tt::Literal) -> Option<(Vec<u8>, Span)> {
let span = lit.span; let span = lit.span;
let lit = ast::make::tokens::literal(&lit.to_string()); let lit = ast::make::tokens::literal(&lit.to_string());
let token = ast::ByteString::cast(lit)?; let token = ast::ByteString::cast(lit)?;
token.value().map(|it| (it.into_owned(), span)) token.value().ok().map(|it| (it.into_owned(), span))
} }
fn compile_error_expand( fn compile_error_expand(

View file

@ -25,7 +25,7 @@ pub(crate) fn make_raw_string(acc: &mut Assists, ctx: &AssistContext<'_>) -> Opt
if token.is_raw() { if token.is_raw() {
return None; return None;
} }
let value = token.value()?; let value = token.value().ok()?;
let target = token.syntax().text_range(); let target = token.syntax().text_range();
acc.add( acc.add(
AssistId("make_raw_string", AssistKind::RefactorRewrite), AssistId("make_raw_string", AssistKind::RefactorRewrite),
@ -64,7 +64,7 @@ pub(crate) fn make_usual_string(acc: &mut Assists, ctx: &AssistContext<'_>) -> O
if !token.is_raw() { if !token.is_raw() {
return None; return None;
} }
let value = token.value()?; let value = token.value().ok()?;
let target = token.syntax().text_range(); let target = token.syntax().text_range();
acc.add( acc.add(
AssistId("make_usual_string", AssistKind::RefactorRewrite), AssistId("make_usual_string", AssistKind::RefactorRewrite),
@ -398,12 +398,12 @@ string"###;
} }
#[test] #[test]
fn remove_hash_doesnt_work() { fn remove_hash_does_not_work() {
check_assist_not_applicable(remove_hash, r#"fn f() { let s = $0"random string"; }"#); check_assist_not_applicable(remove_hash, r#"fn f() { let s = $0"random string"; }"#);
} }
#[test] #[test]
fn remove_hash_no_hash_doesnt_work() { fn remove_hash_no_hash_does_not_work() {
check_assist_not_applicable(remove_hash, r#"fn f() { let s = $0r"random string"; }"#); check_assist_not_applicable(remove_hash, r#"fn f() { let s = $0r"random string"; }"#);
} }

View file

@ -25,7 +25,7 @@ use crate::{AssistContext, AssistId, AssistKind, Assists};
// ``` // ```
pub(crate) fn replace_string_with_char(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> { pub(crate) fn replace_string_with_char(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
let token = ctx.find_token_syntax_at_offset(STRING).and_then(ast::String::cast)?; let token = ctx.find_token_syntax_at_offset(STRING).and_then(ast::String::cast)?;
let value = token.value()?; let value = token.value().ok()?;
let target = token.syntax().text_range(); let target = token.syntax().text_range();
if value.chars().take(2).count() != 1 { if value.chars().take(2).count() != 1 {

View file

@ -123,7 +123,7 @@ fn try_lookup_include_path(
{ {
return None; return None;
} }
let path = token.value()?; let path = token.value().ok()?;
let file_id = sema.db.resolve_path(AnchoredPath { anchor: file_id, path: &path })?; let file_id = sema.db.resolve_path(AnchoredPath { anchor: file_id, path: &path })?;
let size = sema.db.file_text(file_id).len().try_into().ok()?; let size = sema.db.file_text(file_id).len().try_into().ok()?;
@ -179,11 +179,11 @@ fn try_filter_trait_item_definition(
AssocItem::Const(..) | AssocItem::TypeAlias(..) => { AssocItem::Const(..) | AssocItem::TypeAlias(..) => {
let trait_ = assoc.implemented_trait(db)?; let trait_ = assoc.implemented_trait(db)?;
let name = def.name(db)?; let name = def.name(db)?;
let discri_value = discriminant(&assoc); let discriminant_value = discriminant(&assoc);
trait_ trait_
.items(db) .items(db)
.iter() .iter()
.filter(|itm| discriminant(*itm) == discri_value) .filter(|itm| discriminant(*itm) == discriminant_value)
.find_map(|itm| (itm.name(db)? == name).then(|| itm.try_to_nav(db)).flatten()) .find_map(|itm| (itm.name(db)? == name).then(|| itm.try_to_nav(db)).flatten())
.map(|it| it.collect()) .map(|it| it.collect())
} }

View file

@ -15,7 +15,7 @@ use ide_db::{
FxIndexSet, RootDatabase, FxIndexSet, RootDatabase,
}; };
use itertools::{multizip, Itertools}; use itertools::{multizip, Itertools};
use syntax::{ast, match_ast, AstNode, AstToken, SyntaxKind::*, SyntaxNode, T}; use syntax::{ast, AstNode, SyntaxKind::*, SyntaxNode, T};
use crate::{ use crate::{
doc_links::token_as_doc_comment, doc_links::token_as_doc_comment,

View file

@ -533,11 +533,11 @@ pub(super) fn literal(sema: &Semantics<'_, RootDatabase>, token: SyntaxToken) ->
let value = match_ast! { let value = match_ast! {
match token { match token {
ast::String(string) => Ok(string.value()?.to_string()), ast::String(string) => string.value().as_ref().map_err(|e| format!("{e:?}")).map(ToString::to_string),
ast::ByteString(string) => Ok(format_args!("{:?}", string.value()?).to_string()), ast::ByteString(string) => string.value().as_ref().map_err(|e| format!("{e:?}")).map(|it| format!("{it:?}")),
ast::CString(string) => Ok(std::str::from_utf8(string.value()?.as_ref()).ok()?.to_owned()), ast::CString(string) => string.value().as_ref().map_err(|e| format!("{e:?}")).map(|it| std::str::from_utf8(it).map_or_else(|e| format!("{e:?}"), ToOwned::to_owned)),
ast::Char(char) => Ok(char.value()?.to_string()), ast::Char(char) => char .value().as_ref().map_err(|e| format!("{e:?}")).map(ToString::to_string),
ast::Byte(byte) => Ok(format!("0x{:X}", byte.value()?)), ast::Byte(byte) => byte .value().as_ref().map_err(|e| format!("{e:?}")).map(|it| format!("0x{it:X}")),
ast::FloatNumber(num) => { ast::FloatNumber(num) => {
let (text, _) = num.split_into_parts(); let (text, _) = num.split_into_parts();
let text = text.replace('_', ""); let text = text.replace('_', "");

View file

@ -25,7 +25,7 @@ pub(super) fn highlight_escape_string<T: IsString>(
} }
pub(super) fn highlight_escape_char(stack: &mut Highlights, char: &Char, start: TextSize) { pub(super) fn highlight_escape_char(stack: &mut Highlights, char: &Char, start: TextSize) {
if char.value().is_none() { if char.value().is_err() {
// We do not emit invalid escapes highlighting here. The lexer would likely be in a bad // We do not emit invalid escapes highlighting here. The lexer would likely be in a bad
// state and this token contains junks, since `'` is not a reliable delimiter (consider // state and this token contains junks, since `'` is not a reliable delimiter (consider
// lifetimes). Nonetheless, parser errors should already be emitted. // lifetimes). Nonetheless, parser errors should already be emitted.
@ -48,7 +48,7 @@ pub(super) fn highlight_escape_char(stack: &mut Highlights, char: &Char, start:
} }
pub(super) fn highlight_escape_byte(stack: &mut Highlights, byte: &Byte, start: TextSize) { pub(super) fn highlight_escape_byte(stack: &mut Highlights, byte: &Byte, start: TextSize) {
if byte.value().is_none() { if byte.value().is_err() {
// See `highlight_escape_char` for why no error highlighting here. // See `highlight_escape_char` for why no error highlighting here.
return; return;
} }

View file

@ -30,7 +30,7 @@ pub(super) fn ra_fixture(
if !active_parameter.ident().map_or(false, |name| name.text().starts_with("ra_fixture")) { if !active_parameter.ident().map_or(false, |name| name.text().starts_with("ra_fixture")) {
return None; return None;
} }
let value = literal.value()?; let value = literal.value().ok()?;
if let Some(range) = literal.open_quote_text_range() { if let Some(range) = literal.open_quote_text_range() {
hl.add(HlRange { range, highlight: HlTag::StringLiteral.into(), binding_hash: None }) hl.add(HlRange { range, highlight: HlTag::StringLiteral.into(), binding_hash: None })

View file

@ -6,7 +6,7 @@ use std::{
}; };
use rustc_lexer::unescape::{ use rustc_lexer::unescape::{
unescape_byte, unescape_char, unescape_mixed, unescape_unicode, MixedUnit, Mode, unescape_byte, unescape_char, unescape_mixed, unescape_unicode, EscapeError, MixedUnit, Mode,
}; };
use crate::{ use crate::{
@ -180,10 +180,7 @@ pub trait IsString: AstToken {
fn close_quote_text_range(&self) -> Option<TextRange> { fn close_quote_text_range(&self) -> Option<TextRange> {
self.quote_offsets().map(|it| it.quotes.1) self.quote_offsets().map(|it| it.quotes.1)
} }
fn escaped_char_ranges( fn escaped_char_ranges(&self, cb: &mut dyn FnMut(TextRange, Result<char, EscapeError>)) {
&self,
cb: &mut dyn FnMut(TextRange, Result<char, rustc_lexer::unescape::EscapeError>),
) {
let text_range_no_quotes = match self.text_range_between_quotes() { let text_range_no_quotes = match self.text_range_between_quotes() {
Some(it) => it, Some(it) => it,
None => return, None => return,
@ -212,20 +209,17 @@ impl IsString for ast::String {
} }
impl ast::String { impl ast::String {
pub fn value(&self) -> Option<Cow<'_, str>> { pub fn value(&self) -> Result<Cow<'_, str>, EscapeError> {
let text = self.text();
let text_range = self.text_range_between_quotes().ok_or(EscapeError::LoneSlash)?;
let text = &text[text_range - self.syntax().text_range().start()];
if self.is_raw() { if self.is_raw() {
let text = self.text(); return Ok(Cow::Borrowed(text));
let text =
&text[self.text_range_between_quotes()? - self.syntax().text_range().start()];
return Some(Cow::Borrowed(text));
} }
let text = self.text();
let text = &text[self.text_range_between_quotes()? - self.syntax().text_range().start()];
let mut buf = String::new(); let mut buf = String::new();
let mut prev_end = 0; let mut prev_end = 0;
let mut has_error = false; let mut has_error = None;
unescape_unicode(text, Self::MODE, &mut |char_range, unescaped_char| match ( unescape_unicode(text, Self::MODE, &mut |char_range, unescaped_char| match (
unescaped_char, unescaped_char,
buf.capacity() == 0, buf.capacity() == 0,
@ -239,13 +233,13 @@ impl ast::String {
buf.push_str(&text[..prev_end]); buf.push_str(&text[..prev_end]);
buf.push(c); buf.push(c);
} }
(Err(_), _) => has_error = true, (Err(e), _) => has_error = Some(e),
}); });
match (has_error, buf.capacity() == 0) { match (has_error, buf.capacity() == 0) {
(true, _) => None, (Some(e), _) => Err(e),
(false, true) => Some(Cow::Borrowed(text)), (None, true) => Ok(Cow::Borrowed(text)),
(false, false) => Some(Cow::Owned(buf)), (None, false) => Ok(Cow::Owned(buf)),
} }
} }
} }
@ -256,20 +250,17 @@ impl IsString for ast::ByteString {
} }
impl ast::ByteString { impl ast::ByteString {
pub fn value(&self) -> Option<Cow<'_, [u8]>> { pub fn value(&self) -> Result<Cow<'_, [u8]>, EscapeError> {
let text = self.text();
let text_range = self.text_range_between_quotes().ok_or(EscapeError::LoneSlash)?;
let text = &text[text_range - self.syntax().text_range().start()];
if self.is_raw() { if self.is_raw() {
let text = self.text(); return Ok(Cow::Borrowed(text.as_bytes()));
let text =
&text[self.text_range_between_quotes()? - self.syntax().text_range().start()];
return Some(Cow::Borrowed(text.as_bytes()));
} }
let text = self.text();
let text = &text[self.text_range_between_quotes()? - self.syntax().text_range().start()];
let mut buf: Vec<u8> = Vec::new(); let mut buf: Vec<u8> = Vec::new();
let mut prev_end = 0; let mut prev_end = 0;
let mut has_error = false; let mut has_error = None;
unescape_unicode(text, Self::MODE, &mut |char_range, unescaped_char| match ( unescape_unicode(text, Self::MODE, &mut |char_range, unescaped_char| match (
unescaped_char, unescaped_char,
buf.capacity() == 0, buf.capacity() == 0,
@ -283,13 +274,13 @@ impl ast::ByteString {
buf.extend_from_slice(text[..prev_end].as_bytes()); buf.extend_from_slice(text[..prev_end].as_bytes());
buf.push(c as u8); buf.push(c as u8);
} }
(Err(_), _) => has_error = true, (Err(e), _) => has_error = Some(e),
}); });
match (has_error, buf.capacity() == 0) { match (has_error, buf.capacity() == 0) {
(true, _) => None, (Some(e), _) => Err(e),
(false, true) => Some(Cow::Borrowed(text.as_bytes())), (None, true) => Ok(Cow::Borrowed(text.as_bytes())),
(false, false) => Some(Cow::Owned(buf)), (None, false) => Ok(Cow::Owned(buf)),
} }
} }
} }
@ -298,10 +289,7 @@ impl IsString for ast::CString {
const RAW_PREFIX: &'static str = "cr"; const RAW_PREFIX: &'static str = "cr";
const MODE: Mode = Mode::CStr; const MODE: Mode = Mode::CStr;
fn escaped_char_ranges( fn escaped_char_ranges(&self, cb: &mut dyn FnMut(TextRange, Result<char, EscapeError>)) {
&self,
cb: &mut dyn FnMut(TextRange, Result<char, rustc_lexer::unescape::EscapeError>),
) {
let text_range_no_quotes = match self.text_range_between_quotes() { let text_range_no_quotes = match self.text_range_between_quotes() {
Some(it) => it, Some(it) => it,
None => return, None => return,
@ -322,20 +310,17 @@ impl IsString for ast::CString {
} }
impl ast::CString { impl ast::CString {
pub fn value(&self) -> Option<Cow<'_, [u8]>> { pub fn value(&self) -> Result<Cow<'_, [u8]>, EscapeError> {
let text = self.text();
let text_range = self.text_range_between_quotes().ok_or(EscapeError::LoneSlash)?;
let text = &text[text_range - self.syntax().text_range().start()];
if self.is_raw() { if self.is_raw() {
let text = self.text(); return Ok(Cow::Borrowed(text.as_bytes()));
let text =
&text[self.text_range_between_quotes()? - self.syntax().text_range().start()];
return Some(Cow::Borrowed(text.as_bytes()));
} }
let text = self.text();
let text = &text[self.text_range_between_quotes()? - self.syntax().text_range().start()];
let mut buf = Vec::new(); let mut buf = Vec::new();
let mut prev_end = 0; let mut prev_end = 0;
let mut has_error = false; let mut has_error = None;
let extend_unit = |buf: &mut Vec<u8>, unit: MixedUnit| match unit { let extend_unit = |buf: &mut Vec<u8>, unit: MixedUnit| match unit {
MixedUnit::Char(c) => buf.extend(c.encode_utf8(&mut [0; 4]).as_bytes()), MixedUnit::Char(c) => buf.extend(c.encode_utf8(&mut [0; 4]).as_bytes()),
MixedUnit::HighByte(b) => buf.push(b), MixedUnit::HighByte(b) => buf.push(b),
@ -353,13 +338,13 @@ impl ast::CString {
buf.extend(text[..prev_end].as_bytes()); buf.extend(text[..prev_end].as_bytes());
extend_unit(&mut buf, u); extend_unit(&mut buf, u);
} }
(Err(_), _) => has_error = true, (Err(e), _) => has_error = Some(e),
}); });
match (has_error, buf.capacity() == 0) { match (has_error, buf.capacity() == 0) {
(true, _) => None, (Some(e), _) => Err(e),
(false, true) => Some(Cow::Borrowed(text.as_bytes())), (None, true) => Ok(Cow::Borrowed(text.as_bytes())),
(false, false) => Some(Cow::Owned(buf)), (None, false) => Ok(Cow::Owned(buf)),
} }
} }
} }
@ -478,34 +463,34 @@ impl Radix {
} }
impl ast::Char { impl ast::Char {
pub fn value(&self) -> Option<char> { pub fn value(&self) -> Result<char, EscapeError> {
let mut text = self.text(); let mut text = self.text();
if text.starts_with('\'') { if text.starts_with('\'') {
text = &text[1..]; text = &text[1..];
} else { } else {
return None; return Err(EscapeError::ZeroChars);
} }
if text.ends_with('\'') { if text.ends_with('\'') {
text = &text[0..text.len() - 1]; text = &text[0..text.len() - 1];
} }
unescape_char(text).ok() unescape_char(text)
} }
} }
impl ast::Byte { impl ast::Byte {
pub fn value(&self) -> Option<u8> { pub fn value(&self) -> Result<u8, EscapeError> {
let mut text = self.text(); let mut text = self.text();
if text.starts_with("b\'") { if text.starts_with("b\'") {
text = &text[2..]; text = &text[2..];
} else { } else {
return None; return Err(EscapeError::ZeroChars);
} }
if text.ends_with('\'') { if text.ends_with('\'') {
text = &text[0..text.len() - 1]; text = &text[0..text.len() - 1];
} }
unescape_byte(text).ok() unescape_byte(text)
} }
} }
@ -559,7 +544,10 @@ mod tests {
fn check_string_value<'a>(lit: &str, expected: impl Into<Option<&'a str>>) { fn check_string_value<'a>(lit: &str, expected: impl Into<Option<&'a str>>) {
assert_eq!( assert_eq!(
ast::String { syntax: make::tokens::literal(&format!("\"{lit}\"")) }.value().as_deref(), ast::String { syntax: make::tokens::literal(&format!("\"{lit}\"")) }
.value()
.as_deref()
.ok(),
expected.into() expected.into()
); );
} }
@ -584,7 +572,8 @@ bcde", "abcde",
assert_eq!( assert_eq!(
ast::ByteString { syntax: make::tokens::literal(&format!("b\"{lit}\"")) } ast::ByteString { syntax: make::tokens::literal(&format!("b\"{lit}\"")) }
.value() .value()
.as_deref(), .as_deref()
.ok(),
expected.into().map(|value| &value[..]) expected.into().map(|value| &value[..])
); );
} }