diff --git a/crates/squawk_ide/src/code_actions/unquote_identifier.rs b/crates/squawk_ide/src/code_actions/unquote_identifier.rs index 1779eddeb..2419eadc6 100644 --- a/crates/squawk_ide/src/code_actions/unquote_identifier.rs +++ b/crates/squawk_ide/src/code_actions/unquote_identifier.rs @@ -144,6 +144,11 @@ mod test { unquote_identifier, r#"select "select"$0 from t;"# )); + // type or function name word + assert!(code_action_not_applicable( + unquote_identifier, + r#"create table t ("left"$0 int);"# + )); } #[test] diff --git a/crates/squawk_syntax/src/generated/keywords.rs b/crates/squawk_syntax/src/generated/keywords.rs index adee7a42f..1c4082dec 100644 --- a/crates/squawk_syntax/src/generated/keywords.rs +++ b/crates/squawk_syntax/src/generated/keywords.rs @@ -81,3 +81,73 @@ pub(crate) const RESERVED_KEYWORDS: &[&str] = &[ "window", "with", ]; + +pub(crate) const TYPE_FUNC_NAME_KEYWORDS: &[&str] = &[ + "authorization", + "binary", + "collation", + "concurrently", + "cross", + "current_schema", + "freeze", + "full", + "ilike", + "inner", + "is", + "isnull", + "join", + "left", + "like", + "natural", + "notnull", + "outer", + "overlaps", + "right", + "similar", + "tablesample", + "verbose", +]; + +pub(crate) const AS_LABEL_KEYWORDS: &[&str] = &[ + "array", + "as", + "char", + "character", + "create", + "day", + "except", + "fetch", + "filter", + "for", + "from", + "grant", + "group", + "having", + "hour", + "ignore", + "intersect", + "into", + "isnull", + "limit", + "minute", + "month", + "notnull", + "offset", + "on", + "order", + "over", + "overlaps", + "precision", + "respect", + "returning", + "second", + "to", + "union", + "varying", + "where", + "window", + "with", + "within", + "without", + "year", +]; diff --git a/crates/squawk_syntax/src/quote.rs b/crates/squawk_syntax/src/quote.rs index 93dad3225..8d7f0d691 100644 --- a/crates/squawk_syntax/src/quote.rs +++ b/crates/squawk_syntax/src/quote.rs @@ -1,13 +1,33 @@ use crate::SyntaxNode; -use crate::generated::keywords::RESERVED_KEYWORDS; +use crate::generated::keywords::{AS_LABEL_KEYWORDS, RESERVED_KEYWORDS, TYPE_FUNC_NAME_KEYWORDS}; pub fn quote_string_literal(text: &str) -> String { format!("'{}'", text.replace('\'', "''")) } +fn quote(text: &str) -> String { + format!(r#""{}""#, text.replace('"', r#""""#)) +} + pub fn quote_column_alias(text: &str) -> String { if needs_quoting(text) { - format!(r#""{}""#, text.replace('"', r#""""#)) + quote(text) + } else { + text.to_string() + } +} + +pub fn quote_bare_column_alias(text: &str) -> String { + if needs_quoting(text) || is_as_label_word(text) { + quote(text) + } else { + text.to_string() + } +} + +pub fn quote_ident(text: &str) -> String { + if needs_quoting(text) || is_reserved_word(text) || is_type_func_name_word(text) { + quote(text) } else { text.to_string() } @@ -22,7 +42,7 @@ pub fn unquote_ident(node: &SyntaxNode) -> Option { let text = &text[1..text.len() - 1]; - if is_reserved_word(text) { + if is_reserved_word(text) || is_type_func_name_word(text) { return None; } @@ -80,6 +100,18 @@ pub fn is_reserved_word(text: &str) -> bool { .is_ok() } +fn is_type_func_name_word(text: &str) -> bool { + TYPE_FUNC_NAME_KEYWORDS + .binary_search(&text.to_ascii_lowercase().as_str()) + .is_ok() +} + +fn is_as_label_word(text: &str) -> bool { + AS_LABEL_KEYWORDS + .binary_search(&text.to_ascii_lowercase().as_str()) + .is_ok() +} + pub fn strip_quotes(text: &str) -> Option<&str> { text.strip_prefix('\'')?.strip_suffix('\'') } @@ -135,4 +167,56 @@ mod tests { fn quote_column_alias_handles_special_column_name() { assert_snapshot!(quote_column_alias("?column?"), @r#""?column?""#); } + + #[test] + fn quote_bare_column_alias_quotes_keywords_that_need_an_as() { + assert_snapshot!(quote_bare_column_alias("filter"), @r#""filter""#); + assert_snapshot!(quote_bare_column_alias("day"), @r#""day""#); + // also reserved + assert_snapshot!(quote_bare_column_alias("array"), @r#""array""#); + } + + #[test] + fn quote_bare_column_alias_doesnt_quote_bare_label_keywords() { + assert_snapshot!(quote_bare_column_alias("between"), @"between"); + assert_snapshot!(quote_bare_column_alias("all"), @"all"); + assert_snapshot!(quote_bare_column_alias("left"), @"left"); + assert_snapshot!(quote_bare_column_alias("col_name"), @"col_name"); + } + + #[test] + fn quote_ident_doesnt_quote_simple_identifiers() { + assert_snapshot!(quote_ident("col_name"), @"col_name"); + assert_snapshot!(quote_ident("users"), @"users"); + assert_snapshot!(quote_ident("t2$"), @"t2$"); + } + + #[test] + fn quote_ident_doesnt_quote_column_or_table_keywords() { + // unreserved + assert_snapshot!(quote_ident("data"), @"data"); + assert_snapshot!(quote_ident("value"), @"value"); + // col name + assert_snapshot!(quote_ident("int"), @"int"); + } + + #[test] + fn quote_ident_quotes_reserved_words() { + assert_snapshot!(quote_ident("select"), @r#""select""#); + assert_snapshot!(quote_ident("array"), @r#""array""#); + } + + #[test] + fn quote_ident_quotes_type_func_name_words() { + assert_snapshot!(quote_ident("left"), @r#""left""#); + assert_snapshot!(quote_ident("join"), @r#""join""#); + } + + #[test] + fn quote_ident_quotes_names_that_dont_fold_to_themselves() { + assert_snapshot!(quote_ident("Mixed"), @r#""Mixed""#); + assert_snapshot!(quote_ident("has space"), @r#""has space""#); + assert_snapshot!(quote_ident(""), @r#""""#); + assert_snapshot!(quote_ident(r#"foo"bar"#), @r#""foo""bar""#); + } } diff --git a/crates/xtask/src/codegen.rs b/crates/xtask/src/codegen.rs index ffae6d109..8ae06aad8 100644 --- a/crates/xtask/src/codegen.rs +++ b/crates/xtask/src/codegen.rs @@ -88,6 +88,10 @@ pub(crate) fn codegen() -> Result<()> { std::fs::write(playground_keywords_file, playground_keywords) .context("problem writing playground keywords")?; + let syntax_keywords = project_root().join("crates/squawk_syntax/src/generated/keywords.rs"); + let keyword_arrays = generate_keyword_arrays(&keyword_kinds)?; + std::fs::write(syntax_keywords, keyword_arrays).context("problem writing keyword arrays")?; + let kinds = generate_kind_src(&ast_src.nodes, &grammar, keyword_kinds.all_keywords); let syntax_kinds = generate_syntax_kinds(kinds)?; @@ -95,12 +99,6 @@ pub(crate) fn codegen() -> Result<()> { project_root().join("crates/squawk_parser/src/generated/syntax_kind.rs"); std::fs::write(syntax_kinds_file, syntax_kinds).context("problem writing syntax kinds")?; - let ide_reserved_keywords = - project_root().join("crates/squawk_syntax/src/generated/keywords.rs"); - let reserved_keywords = generate_reserved_keywords_array(&keyword_kinds.reserved_keywords)?; - std::fs::write(ide_reserved_keywords, reserved_keywords) - .context("problem writing reserved keywords")?; - Ok(()) } @@ -237,23 +235,38 @@ const PRELUDE: &str = "\ "; -fn generate_reserved_keywords_array(reserved_keywords: &[String]) -> Result { - let mut reserved_keywords = reserved_keywords - .iter() - .map(|x| x.to_ascii_lowercase()) - .collect::>(); - reserved_keywords.sort(); +fn generate_keyword_arrays(keyword_kinds: &KeywordKinds) -> Result { + let sorted = |keywords: &[String]| { + let mut keywords = keywords + .iter() + .map(|x| x.to_ascii_lowercase()) + .collect::>(); + keywords.sort(); + keywords + }; + let reserved_keywords = sorted(&keyword_kinds.reserved_keywords); + let type_func_name_keywords = sorted(&keyword_kinds.type_func_name_keywords); + let as_label_keywords = sorted(&keyword_kinds.as_label_keywords); let output = reformat( quote! { pub(crate) const RESERVED_KEYWORDS: &[&str] = &[ #(#reserved_keywords),* ]; + + pub(crate) const TYPE_FUNC_NAME_KEYWORDS: &[&str] = &[ + #(#type_func_name_keywords),* + ]; + + pub(crate) const AS_LABEL_KEYWORDS: &[&str] = &[ + #(#as_label_keywords),* + ]; } .to_string(), - ); + ) + .replace("pub(crate)", "\npub(crate)"); - Ok(format!("{PRELUDE}{output}")) + Ok(format!("{PRELUDE}{}", output.trim_start())) } fn generate_syntax_kinds(grammar: KindsSrc) -> Result { diff --git a/crates/xtask/src/keywords.rs b/crates/xtask/src/keywords.rs index 283f65f16..925556876 100644 --- a/crates/xtask/src/keywords.rs +++ b/crates/xtask/src/keywords.rs @@ -121,6 +121,7 @@ fn parse_header() -> Result> { pub(crate) struct KeywordKinds { pub(crate) all_keywords: Vec, pub(crate) bare_label_keywords: Vec, + pub(crate) as_label_keywords: Vec, pub(crate) unreserved_keywords: Vec, pub(crate) reserved_keywords: Vec, pub(crate) col_name_keywords: Vec, @@ -141,6 +142,16 @@ pub(crate) fn keyword_kinds() -> Result { .collect::>(); bare_label_keywords.sort(); + let mut as_label_keywords = keywords + .iter() + .filter(|(_key, value)| match value.label { + KeywordLabel::As => true, + KeywordLabel::Bare => false, + }) + .map(|(key, _value)| key.to_owned()) + .collect::>(); + as_label_keywords.sort(); + let mut unreserved_keywords = keywords .iter() .filter(|(_key, value)| matches!(value.category, KeywordCategory::Unreserved)) @@ -208,6 +219,7 @@ pub(crate) fn keyword_kinds() -> Result { Ok(KeywordKinds { all_keywords, bare_label_keywords, + as_label_keywords, unreserved_keywords, reserved_keywords, col_name_keywords,