Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 8 additions & 15 deletions crates/assists/src/handlers/convert_integer_literal.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use syntax::{ast, ast::Radix, AstNode};
use syntax::{ast, ast::Radix, AstToken};

use crate::{AssistContext, AssistId, AssistKind, Assists, GroupLabel};

Expand All @@ -14,15 +14,13 @@ use crate::{AssistContext, AssistId, AssistKind, Assists, GroupLabel};
// const _: i32 = 0b1010;
// ```
pub(crate) fn convert_integer_literal(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let literal = ctx.find_node_at_offset::<ast::Literal>()?;
let (radix, value) = literal.int_value()?;
let literal = ctx.find_node_at_offset::<ast::Literal>()?.as_int_number()?;
let radix = literal.radix();
let value = literal.value()?;
let suffix = literal.suffix();

let range = literal.syntax().text_range();
let group_id = GroupLabel("Convert integer base".into());
let suffix = match literal.kind() {
ast::LiteralKind::IntNumber { suffix } => suffix,
_ => return None,
};

for &target_radix in Radix::ALL {
if target_radix == radix {
Expand All @@ -36,16 +34,11 @@ pub(crate) fn convert_integer_literal(acc: &mut Assists, ctx: &AssistContext) ->
Radix::Hexadecimal => format!("0x{:X}", value),
};

let label = format!(
"Convert {} to {}{}",
literal,
converted,
suffix.as_deref().unwrap_or_default()
);
let label = format!("Convert {} to {}{}", literal, converted, suffix.unwrap_or_default());

// Appends the type suffix back into the new literal if it exists.
if let Some(suffix) = &suffix {
converted.push_str(&suffix);
if let Some(suffix) = suffix {
converted.push_str(suffix);
}

acc.add_group(
Expand Down
2 changes: 1 addition & 1 deletion crates/syntax/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::{
};

pub use self::{
expr_ext::{ArrayExprKind, BinOp, Effect, ElseBranch, LiteralKind, PrefixOp, Radix, RangeOp},
expr_ext::{ArrayExprKind, BinOp, Effect, ElseBranch, LiteralKind, PrefixOp, RangeOp},
generated::{nodes::*, tokens::*},
node_ext::{
AttrKind, FieldKind, NameOrNameRef, PathSegmentKind, SelfParamKind, SlicePatComponents,
Expand Down
87 changes: 12 additions & 75 deletions crates/syntax/src/ast/expr_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use crate::{
ast::{self, support, AstChildren, AstNode},
SmolStr,
AstToken, SmolStr,
SyntaxKind::*,
SyntaxToken, T,
};
Expand Down Expand Up @@ -316,6 +316,10 @@ impl ast::Literal {
.unwrap()
}

pub fn as_int_number(&self) -> Option<ast::IntNumber> {
ast::IntNumber::cast(self.token())
}

fn find_suffix(text: &str, possible_suffixes: &[&str]) -> Option<SmolStr> {
possible_suffixes
.iter()
Expand All @@ -324,11 +328,6 @@ impl ast::Literal {
}

pub fn kind(&self) -> LiteralKind {
const INT_SUFFIXES: [&str; 12] = [
"u64", "u32", "u16", "u8", "usize", "isize", "i64", "i32", "i16", "i8", "u128", "i128",
];
const FLOAT_SUFFIXES: [&str; 2] = ["f32", "f64"];

let token = self.token();

match token.kind() {
Expand All @@ -337,17 +336,20 @@ impl ast::Literal {
// The lexer treats e.g. `1f64` as an integer literal. See
// https://github.com/rust-analyzer/rust-analyzer/issues/1592
// and the comments on the linked PR.

let text = token.text();
if let suffix @ Some(_) = Self::find_suffix(&text, &FLOAT_SUFFIXES) {
if let suffix @ Some(_) = Self::find_suffix(&text, &ast::FloatNumber::SUFFIXES) {
LiteralKind::FloatNumber { suffix }
} else {
LiteralKind::IntNumber { suffix: Self::find_suffix(&text, &INT_SUFFIXES) }
LiteralKind::IntNumber {
suffix: Self::find_suffix(&text, &ast::IntNumber::SUFFIXES),
}
}
}
FLOAT_NUMBER => {
let text = token.text();
LiteralKind::FloatNumber { suffix: Self::find_suffix(&text, &FLOAT_SUFFIXES) }
LiteralKind::FloatNumber {
suffix: Self::find_suffix(&text, &ast::FloatNumber::SUFFIXES),
}
}
STRING | RAW_STRING => LiteralKind::String,
T![true] => LiteralKind::Bool(true),
Expand All @@ -358,71 +360,6 @@ impl ast::Literal {
_ => unreachable!(),
}
}

// FIXME: should probably introduce string token type?
// https://github.com/rust-analyzer/rust-analyzer/issues/6308
pub fn int_value(&self) -> Option<(Radix, u128)> {
let suffix = match self.kind() {
LiteralKind::IntNumber { suffix } => suffix,
_ => return None,
};

let token = self.token();
let mut text = token.text().as_str();
text = &text[..text.len() - suffix.map_or(0, |it| it.len())];

let buf;
if text.contains("_") {
buf = text.replace('_', "");
text = buf.as_str();
};

let radix = Radix::identify(text)?;
let digits = &text[radix.prefix_len()..];
let value = u128::from_str_radix(digits, radix as u32).ok()?;
Some((radix, value))
}
}

#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum Radix {
Binary = 2,
Octal = 8,
Decimal = 10,
Hexadecimal = 16,
}

impl Radix {
pub const ALL: &'static [Radix] =
&[Radix::Binary, Radix::Octal, Radix::Decimal, Radix::Hexadecimal];

fn identify(literal_text: &str) -> Option<Self> {
// We cannot express a literal in anything other than decimal in under 3 characters, so we return here if possible.
if literal_text.len() < 3 && literal_text.chars().all(|c| c.is_digit(10)) {
return Some(Self::Decimal);
}

let res = match &literal_text[..2] {
"0b" => Radix::Binary,
"0o" => Radix::Octal,
"0x" => Radix::Hexadecimal,
_ => Radix::Decimal,
};

// Checks that all characters after the base prefix are all valid digits for that base.
if literal_text[res.prefix_len()..].chars().all(|c| c.is_digit(res as u32)) {
Some(res)
} else {
None
}
}

const fn prefix_len(&self) -> usize {
match self {
Self::Decimal => 0,
_ => 2,
}
}
}

#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down
42 changes: 42 additions & 0 deletions crates/syntax/src/ast/generated/tokens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,45 @@ impl AstToken for RawString {
}
fn syntax(&self) -> &SyntaxToken { &self.syntax }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct IntNumber {
pub(crate) syntax: SyntaxToken,
}
impl std::fmt::Display for IntNumber {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.syntax, f)
}
}
impl AstToken for IntNumber {
fn can_cast(kind: SyntaxKind) -> bool { kind == INT_NUMBER }
fn cast(syntax: SyntaxToken) -> Option<Self> {
if Self::can_cast(syntax.kind()) {
Some(Self { syntax })
} else {
None
}
}
fn syntax(&self) -> &SyntaxToken { &self.syntax }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct FloatNumber {
pub(crate) syntax: SyntaxToken,
}
impl std::fmt::Display for FloatNumber {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.syntax, f)
}
}
impl AstToken for FloatNumber {
fn can_cast(kind: SyntaxKind) -> bool { kind == FLOAT_NUMBER }
fn cast(syntax: SyntaxToken) -> Option<Self> {
if Self::can_cast(syntax.kind()) {
Some(Self { syntax })
} else {
None
}
}
fn syntax(&self) -> &SyntaxToken { &self.syntax }
}
97 changes: 86 additions & 11 deletions crates/syntax/src/ast/token_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ use std::{
use rustc_lexer::unescape::{unescape_literal, Mode};

use crate::{
ast::{AstToken, Comment, RawString, String, Whitespace},
ast::{self, AstToken},
TextRange, TextSize,
};

impl Comment {
impl ast::Comment {
pub fn kind(&self) -> CommentKind {
kind_by_prefix(self.text())
}
Expand Down Expand Up @@ -80,7 +80,7 @@ fn kind_by_prefix(text: &str) -> CommentKind {
panic!("bad comment text: {:?}", text)
}

impl Whitespace {
impl ast::Whitespace {
pub fn spans_multiple_lines(&self) -> bool {
let text = self.text();
text.find('\n').map_or(false, |idx| text[idx + 1..].contains('\n'))
Expand Down Expand Up @@ -138,19 +138,19 @@ pub trait HasQuotes: AstToken {
}
}

impl HasQuotes for String {}
impl HasQuotes for RawString {}
impl HasQuotes for ast::String {}
impl HasQuotes for ast::RawString {}

pub trait HasStringValue: HasQuotes {
fn value(&self) -> Option<Cow<'_, str>>;
}

impl HasStringValue for String {
impl HasStringValue for ast::String {
fn value(&self) -> Option<Cow<'_, str>> {
let text = self.text().as_str();
let text = &text[self.text_range_between_quotes()? - self.syntax().text_range().start()];

let mut buf = std::string::String::with_capacity(text.len());
let mut buf = String::with_capacity(text.len());
let mut has_error = false;
unescape_literal(text, Mode::Str, &mut |_, unescaped_char| match unescaped_char {
Ok(c) => buf.push(c),
Expand All @@ -166,15 +166,16 @@ impl HasStringValue for String {
}
}

impl HasStringValue for RawString {
// FIXME: merge `ast::RawString` and `ast::String`.
impl HasStringValue for ast::RawString {
fn value(&self) -> Option<Cow<'_, str>> {
let text = self.text().as_str();
let text = &text[self.text_range_between_quotes()? - self.syntax().text_range().start()];
Some(Cow::Borrowed(text))
}
}

impl RawString {
impl ast::RawString {
pub fn map_range_up(&self, range: TextRange) -> Option<TextRange> {
let contents_range = self.text_range_between_quotes()?;
assert!(TextRange::up_to(contents_range.len()).contains_range(range));
Expand Down Expand Up @@ -500,7 +501,7 @@ pub trait HasFormatSpecifier: AstToken {
}
}

impl HasFormatSpecifier for String {
impl HasFormatSpecifier for ast::String {
fn char_ranges(
&self,
) -> Option<Vec<(TextRange, Result<char, rustc_lexer::unescape::EscapeError>)>> {
Expand All @@ -521,7 +522,7 @@ impl HasFormatSpecifier for String {
}
}

impl HasFormatSpecifier for RawString {
impl HasFormatSpecifier for ast::RawString {
fn char_ranges(
&self,
) -> Option<Vec<(TextRange, Result<char, rustc_lexer::unescape::EscapeError>)>> {
Expand All @@ -536,3 +537,77 @@ impl HasFormatSpecifier for RawString {
Some(res)
}
}

impl ast::IntNumber {
#[rustfmt::skip]
pub(crate) const SUFFIXES: &'static [&'static str] = &[
"u8", "u16", "u32", "u64", "u128", "usize",
"i8", "i16", "i32", "i64", "i128", "isize",
];

pub fn radix(&self) -> Radix {
match self.text().get(..2).unwrap_or_default() {
"0b" => Radix::Binary,
"0o" => Radix::Octal,
"0x" => Radix::Hexadecimal,
_ => Radix::Decimal,
}
}

pub fn value(&self) -> Option<u128> {
let token = self.syntax();

let mut text = token.text().as_str();
if let Some(suffix) = self.suffix() {
text = &text[..text.len() - suffix.len()]
}

let radix = self.radix();
text = &text[radix.prefix_len()..];

let buf;
if text.contains("_") {
buf = text.replace('_', "");
text = buf.as_str();
};

let value = u128::from_str_radix(text, radix as u32).ok()?;
Some(value)
}

pub fn suffix(&self) -> Option<&str> {
let text = self.text();
// FIXME: don't check a fixed set of suffixes, `1_0_1___lol` is valid
// syntax, suffix is `lol`.
ast::IntNumber::SUFFIXES.iter().find_map(|suffix| {
if text.ends_with(suffix) {
return Some(&text[text.len() - suffix.len()..]);
}
None
})
}
}

impl ast::FloatNumber {
pub(crate) const SUFFIXES: &'static [&'static str] = &["f32", "f64"];
}

#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum Radix {
Binary = 2,
Octal = 8,
Decimal = 10,
Hexadecimal = 16,
}

impl Radix {
pub const ALL: &'static [Radix] =
&[Radix::Binary, Radix::Octal, Radix::Decimal, Radix::Hexadecimal];

const fn prefix_len(&self) -> usize {
match self {
Self::Decimal => 0,
_ => 2,
}
}
}
Loading