diff --git a/crates/squawk_ide/src/code_actions.rs b/crates/squawk_ide/src/code_actions.rs index c3f16990..81a87091 100644 --- a/crates/squawk_ide/src/code_actions.rs +++ b/crates/squawk_ide/src/code_actions.rs @@ -1,4 +1,4 @@ -use rowan::TextSize; +use rowan::{TextRange, TextSize}; use squawk_linter::Edit; use squawk_syntax::{ SyntaxKind, @@ -9,6 +9,7 @@ use crate::{ column_name::ColumnName, offsets::token_from_offset, quote::{quote_column_alias, unquote_ident}, + symbols::Name, }; #[derive(Debug, Clone)] @@ -34,6 +35,7 @@ pub fn code_actions(file: ast::SourceFile, offset: TextSize) -> Option, + file: &ast::SourceFile, + offset: TextSize, +) -> Option<()> { + let token = token_from_offset(file, offset)?; + let target = token.parent_ancestors().find_map(ast::Target::cast)?; + + let as_name = target.as_name()?; + let alias_name = as_name.name()?; + + let (inferred_column, _) = ColumnName::inferred_from_target(target.clone())?; + let inferred_column_alias = inferred_column.to_string()?; + + let alias = alias_name.syntax().text().to_string(); + + if Name::new(alias) != Name::new(inferred_column_alias) { + return None; + } + + // TODO: + // This lets use remove any whitespace so we don't end up with: + // select x as x, b from t; + // becoming + // select x , b from t; + // but we probably want a better way to express this. + // Maybe a "Remove preceding whitespace" style option for edits. + let expr_end = target.expr()?.syntax().text_range().end(); + let alias_end = as_name.syntax().text_range().end(); + + actions.push(CodeAction { + title: "Remove redundant alias".to_owned(), + edits: vec![Edit::delete(TextRange::new(expr_end, alias_end))], + kind: ActionKind::QuickFix, + }); + + Some(()) +} + #[cfg(test)] mod test { use super::*; @@ -1045,4 +1086,65 @@ mod test { @r#"select 'foo' as "?column?" from t;"# ); } + + #[test] + fn remove_redundant_alias_simple() { + assert_snapshot!(apply_code_action( + remove_redundant_alias, + "select col_name as col_na$0me from t;"), + @"select col_name from t;" + ); + } + + #[test] + fn remove_redundant_alias_quoted() { + assert_snapshot!(apply_code_action( + remove_redundant_alias, + r#"select "x"$0 as x from t;"#), + @r#"select "x" from t;"# + ); + } + + #[test] + fn remove_redundant_alias_case_insensitive() { + assert_snapshot!(apply_code_action( + remove_redundant_alias, + "select col_name$0 as COL_NAME from t;"), + @"select col_name from t;" + ); + } + + #[test] + fn remove_redundant_alias_function() { + assert_snapshot!(apply_code_action( + remove_redundant_alias, + "select count(*)$0 as count from t;"), + @"select count(*) from t;" + ); + } + + #[test] + fn remove_redundant_alias_field_expr() { + assert_snapshot!(apply_code_action( + remove_redundant_alias, + "select t.col$0umn as column from t;"), + @"select t.column from t;" + ); + } + + #[test] + fn remove_redundant_alias_not_applicable_different_name() { + assert!(code_action_not_applicable( + remove_redundant_alias, + "select col_name$0 as foo from t;" + )); + } + + #[test] + fn remove_redundant_alias_not_applicable_no_alias() { + assert!(code_action_not_applicable( + remove_redundant_alias, + "select col_name$0 from t;" + )); + } } diff --git a/crates/squawk_ide/src/column_name.rs b/crates/squawk_ide/src/column_name.rs index 3c1d584d..dd3e2b86 100644 --- a/crates/squawk_ide/src/column_name.rs +++ b/crates/squawk_ide/src/column_name.rs @@ -3,13 +3,7 @@ use squawk_syntax::{ ast::{self, AstNode}, }; -fn normalize_identifier(text: &str) -> String { - if text.starts_with('"') && text.ends_with('"') { - text[1..text.len() - 1].to_string() - } else { - text.to_lowercase() - } -} +use crate::quote::normalize_identifier; #[derive(Clone, Debug, PartialEq)] pub(crate) enum ColumnName { @@ -30,6 +24,7 @@ pub(crate) enum ColumnName { } impl ColumnName { + // Get the alias, otherwise infer the column name. pub(crate) fn from_target(target: ast::Target) -> Option<(ColumnName, SyntaxNode)> { if let Some(as_name) = target.as_name() && let Some(name_node) = as_name.name() @@ -37,7 +32,13 @@ impl ColumnName { let text = name_node.text(); let normalized = normalize_identifier(&text); return Some((ColumnName::Column(normalized), name_node.syntax().clone())); - } else if let Some(expr) = target.expr() + } + Self::inferred_from_target(target) + } + + // Ignore any aliases, just infer the what the column name. + pub(crate) fn inferred_from_target(target: ast::Target) -> Option<(ColumnName, SyntaxNode)> { + if let Some(expr) = target.expr() && let Some(name) = name_from_expr(expr, false) { return Some(name); diff --git a/crates/squawk_ide/src/quote.rs b/crates/squawk_ide/src/quote.rs index 482e0797..b72ce7ee 100644 --- a/crates/squawk_ide/src/quote.rs +++ b/crates/squawk_ide/src/quote.rs @@ -77,6 +77,14 @@ pub(crate) fn is_reserved_word(text: &str) -> bool { .is_ok() } +pub(crate) fn normalize_identifier(text: &str) -> String { + if text.starts_with('"') && text.ends_with('"') && text.len() >= 2 { + text[1..text.len() - 1].to_string() + } else { + text.to_lowercase() + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/squawk_ide/src/symbols.rs b/crates/squawk_ide/src/symbols.rs index 53d8cf48..7f98f054 100644 --- a/crates/squawk_ide/src/symbols.rs +++ b/crates/squawk_ide/src/symbols.rs @@ -3,6 +3,8 @@ use smol_str::SmolStr; use squawk_syntax::SyntaxNodePtr; use std::fmt; +use crate::quote::normalize_identifier; + #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub(crate) struct Name(pub(crate) SmolStr); @@ -25,7 +27,7 @@ impl Name { pub(crate) fn new(text: impl Into) -> Self { let text = text.into(); let normalized = normalize_identifier(&text); - Name(normalized) + Name(normalized.into()) } } @@ -35,14 +37,6 @@ impl fmt::Display for Name { } } -fn normalize_identifier(text: &str) -> SmolStr { - if text.starts_with('"') && text.ends_with('"') && text.len() >= 2 { - text[1..text.len() - 1].into() - } else { - text.to_lowercase().into() - } -} - #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub(crate) enum SymbolKind { Table, @@ -59,3 +53,17 @@ pub(crate) struct Symbol { } pub(crate) type SymbolId = Idx; + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn name_case_insensitive_compare() { + assert_eq!(Name::new("foo"), Name::new("FOO")); + } + + #[test] + fn name_quote_comparing() { + assert_eq!(Name::new(r#""foo""#), Name::new("foo")); + } +}