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
397 changes: 395 additions & 2 deletions crates/squawk_ide/src/hover.rs

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion crates/squawk_ide/src/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,13 @@ pub(crate) fn infer_type_from_ty(ty: &ast::Type) -> Option<Type> {
}
}

fn infer_type_from_literal(literal: &ast::Literal) -> Option<Type> {
pub(crate) fn infer_type_from_literal(literal: &ast::Literal) -> Option<Type> {
let token = literal.syntax().first_token()?;
match token.kind() {
SyntaxKind::INT_NUMBER => Some(Type::Integer),
SyntaxKind::FLOAT_NUMBER => Some(Type::Numeric),
// TODO: this isn't necessarily text, e.g., select 1 + '1';
// We need to look at the context of the string's usage to be sure.
SyntaxKind::STRING
| SyntaxKind::DOLLAR_QUOTED_STRING
| SyntaxKind::ESC_STRING
Expand Down
1 change: 1 addition & 0 deletions crates/squawk_ide/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub mod goto_definition;
pub mod hover;
mod infer;
pub mod inlay_hints;
mod literals;
pub mod location;
mod name;
mod offsets;
Expand Down
134 changes: 134 additions & 0 deletions crates/squawk_ide/src/literals.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
use squawk_syntax::{
SyntaxKind,
ast::{self, AstNode},
quote::{strip_dollar_quotes, strip_prefixed_quotes, strip_quotes, strip_unicode_esc_prefix},
unescape::{decode_esc_string, decode_plain_string, decode_unicode_esc_string, uescape_char},
};

pub(crate) fn binary_digits_to_hex(digits: &str) -> Option<String> {
const HEX_DIGITS: &[u8; 16] = b"0123456789ABCDEF";

if digits.is_empty() {
return Some("".to_string());
}

let mut out = String::with_capacity(digits.len().div_ceil(4));
let mut start = 0;

while start < digits.len() {
let chunk_len = if start == 0 {
match digits.len() % 4 {
0 => 4,
n => n,
}
} else {
4
};
let end = start + chunk_len;
let value = u8::from_str_radix(&digits[start..end], 2).ok()?;
out.push(HEX_DIGITS[value as usize] as char);
start = end;
}

Some(out)
}

pub(crate) fn hex_digits_to_binary(digits: &str) -> Option<String> {
const BINARY_DIGITS: [&str; 16] = [
"0000", "0001", "0010", "0011", "0100", "0101", "0110", "0111", "1000", "1001", "1010",
"1011", "1100", "1101", "1110", "1111",
];

if digits.is_empty() {
return Some("".to_string());
}

let mut out = String::with_capacity(digits.len() * 4);
for ch in digits.chars() {
let value = ch.to_digit(16)? as usize;
out.push_str(BINARY_DIGITS[value]);
}

Some(out)
}

#[derive(Clone, Copy, PartialEq, Eq)]
enum StringDecoding {
BitOrByte,
EscString,
UnicodeEscString,
}

pub(crate) fn literal_string_value(literal: &ast::Literal) -> Option<String> {
let escape_char = unicode_escape_char(literal);
let mut out = String::with_capacity(literal.syntax().text().len().into());
let mut decoding: Option<StringDecoding> = None;

for element in literal.syntax().children_with_tokens() {
let Some(token) = element.into_token() else {
continue;
};
match token.kind() {
SyntaxKind::ESC_STRING => {
let inner = strip_prefixed_quotes(token.text(), ['e', 'E'])?;
decode_esc_string(inner, &mut out);
decoding = Some(StringDecoding::EscString);
}
SyntaxKind::UNICODE_ESC_STRING => {
let inner = strip_unicode_esc_prefix(token.text())?;
decode_unicode_esc_string(inner, escape_char, &mut out);
decoding = Some(StringDecoding::UnicodeEscString);
}
SyntaxKind::BIT_STRING => {
let inner = strip_prefixed_quotes(token.text(), ['b', 'B'])?;
out.push_str(inner);
decoding = Some(StringDecoding::BitOrByte);
}
SyntaxKind::BYTE_STRING => {
let inner = strip_prefixed_quotes(token.text(), ['x', 'X'])?;
out.push_str(inner);
decoding = Some(StringDecoding::BitOrByte);
}
SyntaxKind::DOLLAR_QUOTED_STRING => {
let inner = strip_dollar_quotes(token.text())?;
out.push_str(inner);
return Some(out);
}
SyntaxKind::STRING => {
let inner = strip_quotes(token.text())?;
match decoding {
Some(StringDecoding::EscString) => decode_esc_string(inner, &mut out),
Some(StringDecoding::UnicodeEscString) => {
decode_unicode_esc_string(inner, escape_char, &mut out)
}
Some(StringDecoding::BitOrByte) => out.push_str(inner),
None => decode_plain_string(inner, &mut out),
}
}
SyntaxKind::UESCAPE_KW => break,
_ => (),
}
}

Some(out)
}

fn unicode_escape_char(literal: &ast::Literal) -> char {
let mut seen_uescape = false;
for element in literal.syntax().children_with_tokens() {
let Some(token) = element.into_token() else {
continue;
};
match token.kind() {
SyntaxKind::UESCAPE_KW => seen_uescape = true,
SyntaxKind::STRING if seen_uescape => {
if let Some(ch) = uescape_char(token.text()) {
return ch;
}
return '\\';
}
_ => (),
}
}
'\\'
}
24 changes: 24 additions & 0 deletions crates/squawk_syntax/src/ast/node_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,30 @@ fn name_ref() {
}
}

#[test]
fn unicode_quoted_name_keeps_doubled_single_quotes() {
let parse = SourceFile::parse(r#"select 1 U&"a''b""#);
assert!(parse.errors().is_empty());
let stmt = parse.tree().stmts().next().unwrap();
let ast::Stmt::Select(select) = stmt else {
unreachable!()
};
let name = select
.select_clause()
.unwrap()
.target_list()
.unwrap()
.targets()
.next()
.unwrap()
.as_name()
.unwrap()
.name()
.unwrap();

assert_snapshot!(name.text().to_string(), @"a''b");
}

#[test]
fn index_expr() {
let source_code = "
Expand Down
2 changes: 1 addition & 1 deletion crates/squawk_syntax/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub mod quote;
pub mod syntax_error;
mod syntax_node;
mod token_text;
mod unescape;
pub mod unescape;
mod validation;

#[cfg(test)]
Expand Down
21 changes: 21 additions & 0 deletions crates/squawk_syntax/src/quote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,27 @@ pub fn is_reserved_word(text: &str) -> bool {
.is_ok()
}

pub fn strip_quotes(text: &str) -> Option<&str> {
text.strip_prefix('\'')?.strip_suffix('\'')
}

pub fn strip_prefixed_quotes(text: &str, prefix: [char; 2]) -> Option<&str> {
strip_quotes(text.strip_prefix(prefix)?)
}

pub fn strip_unicode_esc_prefix(text: &str) -> Option<&str> {
strip_quotes(text.strip_prefix(['u', 'U'])?.strip_prefix('&')?)
}

pub fn strip_dollar_quotes(text: &str) -> Option<&str> {
let after_first = text.strip_prefix('$')?;
let tag_end = after_first.find('$')?;
let tag = &after_first[..tag_end];
let body = &after_first[tag_end + 1..];
let closing = format!("${tag}$");
body.strip_suffix(&closing)
}

#[cfg(test)]
mod tests {
use insta::assert_snapshot;
Expand Down
Loading
Loading