From 03e797e339e96e2674efa3d15e43e52b5e6c50d3 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Wed, 15 Apr 2026 21:58:24 -0400 Subject: [PATCH] parser/ide: semantic highlighting more types + better param parsing --- crates/squawk_ide/src/semantic_tokens.rs | 114 +++++++++++++++++- crates/squawk_parser/src/grammar.rs | 4 +- .../tests/data/ok/create_function.sql | 5 + .../snapshots/tests__create_function_ok.snap | 45 ++++++- .../squawk_syntax/src/ast/generated/nodes.rs | 4 + crates/squawk_syntax/src/postgresql.ungram | 2 +- 6 files changed, 168 insertions(+), 6 deletions(-) diff --git a/crates/squawk_ide/src/semantic_tokens.rs b/crates/squawk_ide/src/semantic_tokens.rs index 6ea086f5..e45f21cb 100644 --- a/crates/squawk_ide/src/semantic_tokens.rs +++ b/crates/squawk_ide/src/semantic_tokens.rs @@ -43,11 +43,24 @@ fn highlight_type(out: &mut SemanticTokenBuilder, ty: ast::Type) { match ty { ast::Type::ArrayType(_) => (), ast::Type::BitType(bit_type) => { + if let Some(token) = bit_type.setof_token() { + out.push_type(token.into()); + } if let Some(token) = bit_type.bit_token() { out.push_type(token.into()); } + if let Some(token) = bit_type.varying_token() { + out.push_type(token.into()); + } } ast::Type::CharType(char_type) => { + if let Some(token) = char_type.setof_token() { + out.push_type(token.into()); + } + if let Some(token) = char_type.national_token() { + out.push_type(token.into()); + } + if let Some(token) = char_type .varchar_token() .or_else(|| char_type.nchar_token()) @@ -55,9 +68,15 @@ fn highlight_type(out: &mut SemanticTokenBuilder, ty: ast::Type) { .or_else(|| char_type.char_token()) { out.push_type(token.into()); - }; + } + if let Some(token) = char_type.varying_token() { + out.push_type(token.into()); + } } ast::Type::DoubleType(double_type) => { + if let Some(token) = double_type.setof_token() { + out.push_type(token.into()); + } if let Some(token) = double_type.double_token() { out.push_type(token.into()); } @@ -67,19 +86,56 @@ fn highlight_type(out: &mut SemanticTokenBuilder, ty: ast::Type) { } ast::Type::ExprType(_) => (), ast::Type::IntervalType(interval_type) => { + if let Some(token) = interval_type.setof_token() { + out.push_type(token.into()); + } if let Some(token) = interval_type.interval_token() { out.push_type(token.into()); } } - ast::Type::PathType(_) => (), + ast::Type::PathType(path_type) => { + if let Some(token) = path_type.setof_token() { + out.push_type(token.into()); + } + } ast::Type::PercentType(_) => (), ast::Type::TimeType(time_type) => { + if let Some(token) = time_type.setof_token() { + out.push_type(token.into()); + } if let Some(token) = time_type .timestamp_token() .or_else(|| time_type.time_token()) { out.push_type(token.into()); } + + if let Some(timezone) = time_type.timezone() { + match timezone { + ast::Timezone::WithTimezone(with_timezone) => { + if let Some(token) = with_timezone.with_token() { + out.push_type(token.into()); + } + if let Some(token) = with_timezone.time_token() { + out.push_type(token.into()); + } + if let Some(token) = with_timezone.zone_token() { + out.push_type(token.into()); + } + } + ast::Timezone::WithoutTimezone(without_timezone) => { + if let Some(token) = without_timezone.without_token() { + out.push_type(token.into()); + } + if let Some(token) = without_timezone.time_token() { + out.push_type(token.into()); + } + if let Some(token) = without_timezone.zone_token() { + out.push_type(token.into()); + } + } + } + } } } } @@ -520,6 +576,60 @@ select '1'::double precision; "#); } + #[test] + fn cast_time_and_timestamp_time_zone() { + assert_snapshot!(semantic_tokens( + " +select cast(1 as timestamp with time zone), cast(1 as timestamp without time zone), cast(1 as time with time zone), cast(1 as time without time zone); +", + ), @r#" + "timestamp" @ 18..27: Type + "with" @ 28..32: Type + "time" @ 33..37: Type + "zone" @ 38..42: Type + "timestamp" @ 55..64: Type + "without" @ 65..72: Type + "time" @ 73..77: Type + "zone" @ 78..82: Type + "time" @ 95..99: Type + "with" @ 100..104: Type + "time" @ 105..109: Type + "zone" @ 110..114: Type + "time" @ 127..131: Type + "without" @ 132..139: Type + "time" @ 140..144: Type + "zone" @ 145..149: Type + "#); + } + + #[test] + fn cast_national_character_varying_type() { + assert_snapshot!(semantic_tokens( + " +select 'foo'::national character varying; +", + ), @r#" + "national" @ 15..23: Type + "character" @ 24..33: Type + "varying" @ 34..41: Type + "#); + } + + #[test] + fn create_function_returns_setof_type() { + assert_snapshot!(semantic_tokens( + " +create function f() returns setof int +as 'select 1' +language sql; +", + ), @r#" + "f" @ 17..18: Function + "setof" @ 29..34: Type + "int" @ 35..38: Type + "#); + } + #[test] fn create_table_temporal_primary_key_column_types() { assert_snapshot!(semantic_tokens( diff --git a/crates/squawk_parser/src/grammar.rs b/crates/squawk_parser/src/grammar.rs index 4b08dfb6..1829612a 100644 --- a/crates/squawk_parser/src/grammar.rs +++ b/crates/squawk_parser/src/grammar.rs @@ -13749,6 +13749,9 @@ fn param(p: &mut Parser<'_>, kind: ParamKind) { // float8 order by // ^ ORDER_KW => true, + // double precision + // ^ + PRECISION_KW if p.at(DOUBLE_KW) => true, // we're at the end of the param, must be a type R_PAREN | EQ | DEFAULT_KW | COMMA => true, _ => false, @@ -14005,7 +14008,6 @@ fn opt_ret_type(p: &mut Parser<'_>) { p.error("expected table arg list"); } } else { - p.eat(SETOF_KW); type_name(p); } m.complete(p, RET_TYPE); diff --git a/crates/squawk_parser/tests/data/ok/create_function.sql b/crates/squawk_parser/tests/data/ok/create_function.sql index e8583995..a44562d5 100644 --- a/crates/squawk_parser/tests/data/ok/create_function.sql +++ b/crates/squawk_parser/tests/data/ok/create_function.sql @@ -123,6 +123,11 @@ returns null on null input as '' language sql; +-- regression +create function identity(double precision) returns double precision +as 'select $1' +language sql; + create function f() returns void strict diff --git a/crates/squawk_parser/tests/snapshots/tests__create_function_ok.snap b/crates/squawk_parser/tests/snapshots/tests__create_function_ok.snap index 40c1d68e..9cf32bbc 100644 --- a/crates/squawk_parser/tests/snapshots/tests__create_function_ok.snap +++ b/crates/squawk_parser/tests/snapshots/tests__create_function_ok.snap @@ -238,9 +238,9 @@ SOURCE_FILE RET_TYPE RETURNS_KW "returns" WHITESPACE " " - SETOF_KW "setof" - WHITESPACE " " PATH_TYPE + SETOF_KW "setof" + WHITESPACE " " PATH PATH PATH_SEGMENT @@ -976,6 +976,47 @@ SOURCE_FILE SQL_KW "sql" SEMICOLON ";" WHITESPACE "\n\n" + COMMENT "-- regression" + WHITESPACE "\n" + CREATE_FUNCTION + CREATE_KW "create" + WHITESPACE " " + FUNCTION_KW "function" + WHITESPACE " " + PATH + PATH_SEGMENT + NAME + IDENTITY_KW "identity" + PARAM_LIST + L_PAREN "(" + PARAM + DOUBLE_TYPE + DOUBLE_KW "double" + WHITESPACE " " + PRECISION_KW "precision" + R_PAREN ")" + WHITESPACE " " + RET_TYPE + RETURNS_KW "returns" + WHITESPACE " " + DOUBLE_TYPE + DOUBLE_KW "double" + WHITESPACE " " + PRECISION_KW "precision" + WHITESPACE "\n" + FUNC_OPTION_LIST + AS_FUNC_OPTION + AS_KW "as" + WHITESPACE " " + LITERAL + STRING "'select $1'" + WHITESPACE "\n" + LANGUAGE_FUNC_OPTION + LANGUAGE_KW "language" + WHITESPACE " " + SQL_KW "sql" + SEMICOLON ";" + WHITESPACE "\n\n" CREATE_FUNCTION CREATE_KW "create" WHITESPACE " " diff --git a/crates/squawk_syntax/src/ast/generated/nodes.rs b/crates/squawk_syntax/src/ast/generated/nodes.rs index 841255a1..4b81df93 100644 --- a/crates/squawk_syntax/src/ast/generated/nodes.rs +++ b/crates/squawk_syntax/src/ast/generated/nodes.rs @@ -2777,6 +2777,10 @@ impl CharType { support::token(&self.syntax, SyntaxKind::CHARACTER_KW) } #[inline] + pub fn national_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::NATIONAL_KW) + } + #[inline] pub fn nchar_token(&self) -> Option { support::token(&self.syntax, SyntaxKind::NCHAR_KW) } diff --git a/crates/squawk_syntax/src/postgresql.ungram b/crates/squawk_syntax/src/postgresql.ungram index a2d98e33..388a8aa1 100644 --- a/crates/squawk_syntax/src/postgresql.ungram +++ b/crates/squawk_syntax/src/postgresql.ungram @@ -567,7 +567,7 @@ PathType = CharType = 'setof'? - ('varchar' | ( 'character' | 'char' | 'nchar' ) 'varying'? ) + ('varchar' | 'national'? ( 'character' | 'char' | 'nchar' ) 'varying'? ) ArgList? BitType =