diff --git a/crates/squawk_parser/src/plpgsql_grammar.rs b/crates/squawk_parser/src/plpgsql_grammar.rs index 56807b5d..be00e831 100644 --- a/crates/squawk_parser/src/plpgsql_grammar.rs +++ b/crates/squawk_parser/src/plpgsql_grammar.rs @@ -80,7 +80,6 @@ fn opt_block(p: &mut Parser) { opt_exception_section(p); p.expect(END_KW); opt_label_name_ref(p); - // TODO: add validation, sometimes this is required p.eat(SEMICOLON); m.complete(p, PLPGSQL_BLOCK); } @@ -236,16 +235,26 @@ fn decl_datatype(p: &mut Parser) { fn path_name_ref(p: &mut Parser) { assert!(at_path(p).is_some()); let m = p.start(); - name(p, PATH_SEGMENT_REF); + path_segment_ref(p); let mut path = m.complete(p, PATH_REF); while !p.at(EOF) && p.at(DOT) { let m = path.precede(p); p.bump(DOT); - name(p, PATH_SEGMENT_REF); + path_segment_ref(p); path = m.complete(p, PATH_REF); } } +fn path_segment_ref(p: &mut Parser) { + let m = p.start(); + let unicode_ident = p.at(IDENT); + p.bump_any(); + if unicode_ident && p.eat(UESCAPE_KW) { + p.expect(STRING); + } + m.complete(p, PATH_SEGMENT_REF); +} + fn at_percent_datatype(p: &Parser) -> bool { let Some(n) = at_path(p) else { return false; @@ -256,7 +265,11 @@ fn at_percent_datatype(p: &Parser) -> bool { fn at_path(p: &Parser) -> Option { let mut n = 0; while !p.nth_at(n, EOF) && at_name(p, n) { + let unicode_ident = p.nth_at(n, IDENT); n += 1; + if unicode_ident && p.nth_at(n, UESCAPE_KW) && p.nth_at(n + 1, STRING) { + n += 2; + } if !p.nth_at(n, DOT) { return Some(n); } diff --git a/crates/squawk_parser/tests/data/plpgsql/err/block.sql b/crates/squawk_parser/tests/data/plpgsql/err/block.sql new file mode 100644 index 00000000..46a483fb --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/err/block.sql @@ -0,0 +1,16 @@ +do $$ +begin + begin + null; + end +end +$$; + +do $$ +begin + <> + begin + null; + end inner +end +$$; diff --git a/crates/squawk_parser/tests/data/plpgsql/err/percent_type.sql b/crates/squawk_parser/tests/data/plpgsql/err/percent_type.sql new file mode 100644 index 00000000..81b98b4b --- /dev/null +++ b/crates/squawk_parser/tests/data/plpgsql/err/percent_type.sql @@ -0,0 +1,8 @@ +do $$ +declare + a U&"tbl" UESCAPE '!'%type; + b U&"tbl" UESCAPE '!'%rowtype; +begin + null; +end +$$; diff --git a/crates/squawk_parser/tests/data/plpgsql/ok/percent_type.sql b/crates/squawk_parser/tests/data/plpgsql/ok/percent_type.sql index 7d60d1fa..ca0afa46 100644 --- a/crates/squawk_parser/tests/data/plpgsql/ok/percent_type.sql +++ b/crates/squawk_parser/tests/data/plpgsql/ok/percent_type.sql @@ -13,6 +13,7 @@ declare j table%rowtype; k select%type; l $1%type; + m U&"int4" UESCAPE '!'; begin null; end diff --git a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_block_err.snap b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_block_err.snap new file mode 100644 index 00000000..0bdc5f8f --- /dev/null +++ b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_block_err.snap @@ -0,0 +1,59 @@ +--- +source: crates/squawk_syntax/src/test.rs +input_file: crates/squawk_parser/tests/data/plpgsql/err/block.sql +--- +PLPGSQL@0..35 + WHITESPACE@0..1 "\n" + PLPGSQL_BLOCK@1..34 + BEGIN_KW@1..6 "begin" + WHITESPACE@6..9 "\n " + PLPGSQL_BODY@9..30 + PLPGSQL_BLOCK@9..30 + BEGIN_KW@9..14 "begin" + WHITESPACE@14..19 "\n " + PLPGSQL_BODY@19..24 + PLPGSQL_NULL_STMT@19..24 + NULL_KW@19..23 "null" + SEMICOLON@23..24 ";" + WHITESPACE@24..27 "\n " + END_KW@27..30 "end" + WHITESPACE@30..31 "\n" + END_KW@31..34 "end" + WHITESPACE@34..35 "\n" +--- +PLPGSQL@0..53 + WHITESPACE@0..1 "\n" + PLPGSQL_BLOCK@1..52 + BEGIN_KW@1..6 "begin" + WHITESPACE@6..9 "\n " + PLPGSQL_BODY@9..48 + PLPGSQL_BLOCK@9..48 + PLPGSQL_LABEL@9..18 + LESS_LESS@9..11 "<<" + PLPGSQL_LABEL_NAME@11..16 + INNER_KW@11..16 "inner" + GREATER_GREATER@16..18 ">>" + WHITESPACE@18..21 "\n " + BEGIN_KW@21..26 "begin" + WHITESPACE@26..31 "\n " + PLPGSQL_BODY@31..36 + PLPGSQL_NULL_STMT@31..36 + NULL_KW@31..35 "null" + SEMICOLON@35..36 ";" + WHITESPACE@36..39 "\n " + END_KW@39..42 "end" + WHITESPACE@42..43 " " + PLPGSQL_LABEL_NAME_REF@43..48 + INNER_KW@43..48 "inner" + WHITESPACE@48..49 "\n" + END_KW@49..52 "end" + WHITESPACE@52..53 "\n" + +error[syntax-error]: Missing semicolon after block + ╭▸ +5 │ end + ╰╴ ━ +error[syntax-error]: Missing semicolon after block + ╭▸ +14 │ end inner + ╰╴ ━ diff --git a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_percent_type_err.snap b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_percent_type_err.snap new file mode 100644 index 00000000..b70d97b0 --- /dev/null +++ b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_percent_type_err.snap @@ -0,0 +1,61 @@ +--- +source: crates/squawk_syntax/src/test.rs +input_file: crates/squawk_parser/tests/data/plpgsql/err/percent_type.sql +--- +PLPGSQL@0..90 + WHITESPACE@0..1 "\n" + PLPGSQL_BLOCK@1..89 + PLPGSQL_DECLARE_SECTION@1..71 + DECLARE_KW@1..8 "declare" + WHITESPACE@8..11 "\n " + PLPGSQL_VAR_DECL@11..38 + PLPGSQL_VAR_NAME@11..12 + IDENT@11..12 "a" + WHITESPACE@12..13 " " + PERCENT_TYPE@13..37 + PATH_REF@13..32 + PATH_SEGMENT_REF@13..32 + IDENT@13..20 "U&\"tbl\"" + WHITESPACE@20..21 " " + UESCAPE_KW@21..28 "UESCAPE" + WHITESPACE@28..29 " " + STRING@29..32 "'!'" + PERCENT_TYPE_CLAUSE@32..37 + PERCENT@32..33 "%" + TYPE_KW@33..37 "type" + SEMICOLON@37..38 ";" + WHITESPACE@38..41 "\n " + PLPGSQL_VAR_DECL@41..71 + PLPGSQL_VAR_NAME@41..42 + IDENT@41..42 "b" + WHITESPACE@42..43 " " + PLPGSQL_PERCENT_ROWTYPE@43..70 + PATH_REF@43..62 + PATH_SEGMENT_REF@43..62 + IDENT@43..50 "U&\"tbl\"" + WHITESPACE@50..51 " " + UESCAPE_KW@51..58 "UESCAPE" + WHITESPACE@58..59 " " + STRING@59..62 "'!'" + PERCENT@62..63 "%" + ROWTYPE_KW@63..70 "rowtype" + SEMICOLON@70..71 ";" + WHITESPACE@71..72 "\n" + BEGIN_KW@72..77 "begin" + WHITESPACE@77..80 "\n " + PLPGSQL_BODY@80..85 + PLPGSQL_NULL_STMT@80..85 + NULL_KW@80..84 "null" + SEMICOLON@84..85 ";" + WHITESPACE@85..86 "\n" + END_KW@86..89 "end" + WHITESPACE@89..90 "\n" + +error[syntax-error]: UESCAPE is not allowed before %TYPE + ╭▸ +3 │ a U&"tbl" UESCAPE '!'%type; + ╰╴ ━ +error[syntax-error]: UESCAPE is not allowed before %ROWTYPE + ╭▸ +4 │ b U&"tbl" UESCAPE '!'%rowtype; + ╰╴ ━ diff --git a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_percent_type_ok.snap b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_percent_type_ok.snap index 8e15c43b..2edde30c 100644 --- a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_percent_type_ok.snap +++ b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_percent_type_ok.snap @@ -2,10 +2,10 @@ source: crates/squawk_syntax/src/test.rs input_file: crates/squawk_parser/tests/data/plpgsql/ok/percent_type.sql --- -PLPGSQL@0..265 +PLPGSQL@0..291 WHITESPACE@0..1 "\n" - PLPGSQL_BLOCK@1..264 - PLPGSQL_DECLARE_SECTION@1..246 + PLPGSQL_BLOCK@1..290 + PLPGSQL_DECLARE_SECTION@1..272 DECLARE_KW@1..8 "declare" WHITESPACE@8..11 "\n " PLPGSQL_VAR_DECL@11..17 @@ -205,13 +205,27 @@ PLPGSQL@0..265 PERCENT@240..241 "%" TYPE_KW@241..245 "type" SEMICOLON@245..246 ";" - WHITESPACE@246..247 "\n" - BEGIN_KW@247..252 "begin" - WHITESPACE@252..255 "\n " - PLPGSQL_BODY@255..260 - PLPGSQL_NULL_STMT@255..260 - NULL_KW@255..259 "null" - SEMICOLON@259..260 ";" - WHITESPACE@260..261 "\n" - END_KW@261..264 "end" - WHITESPACE@264..265 "\n" + WHITESPACE@246..249 "\n " + PLPGSQL_VAR_DECL@249..272 + PLPGSQL_VAR_NAME@249..250 + IDENT@249..250 "m" + WHITESPACE@250..251 " " + PATH_TYPE@251..271 + PATH_REF@251..271 + PATH_SEGMENT_REF@251..271 + IDENT@251..259 "U&\"int4\"" + WHITESPACE@259..260 " " + UESCAPE_KW@260..267 "UESCAPE" + WHITESPACE@267..268 " " + STRING@268..271 "'!'" + SEMICOLON@271..272 ";" + WHITESPACE@272..273 "\n" + BEGIN_KW@273..278 "begin" + WHITESPACE@278..281 "\n " + PLPGSQL_BODY@281..286 + PLPGSQL_NULL_STMT@281..286 + NULL_KW@281..285 "null" + SEMICOLON@285..286 ";" + WHITESPACE@286..287 "\n" + END_KW@287..290 "end" + WHITESPACE@290..291 "\n" diff --git a/crates/squawk_syntax/src/validation.rs b/crates/squawk_syntax/src/validation.rs index 7e086369..d6b002a9 100644 --- a/crates/squawk_syntax/src/validation.rs +++ b/crates/squawk_syntax/src/validation.rs @@ -49,6 +49,13 @@ pub(crate) fn validate(root: &SyntaxNode, errors: &mut Vec) { ast::NonStandardParam(it) => validate_non_standard_param(it, errors), ast::ParenFromItem(it) => validate_paren_from_item(it, errors), ast::PartitionForValuesWith(it) => validate_hash_partition_bounds(it, errors), + ast::PercentType(it) => validate_plpgsql_percent_type( + it.syntax(), + it.percent_type_clause().and_then(|it| it.percent_token()), + "%TYPE", + errors, + ), + ast::PlpgsqlBlock(it) => validate_plpgsql_block(it, errors), ast::PlpgsqlCaseStmt(it) => validate_no_bare_case(it.subject(), errors), ast::PlpgsqlCompOptionPrintStrictParams(it) => { validate_print_strict_params(it, errors) @@ -63,6 +70,12 @@ pub(crate) fn validate(root: &SyntaxNode, errors: &mut Vec) { ast::PlpgsqlForIStmt(it) => validate_for_i_single_var(it, errors), ast::PlpgsqlForQueryStmt(it) => validate_for_query_no_reverse(it, errors), ast::PlpgsqlIfStmt(it) => validate_no_bare_case(it.cond(), errors), + ast::PlpgsqlPercentRowtype(it) => validate_plpgsql_percent_type( + it.syntax(), + it.percent_token(), + "%ROWTYPE", + errors, + ), ast::RelationFromItem(it) => validate_relation_from_item(it, errors), ast::RuleStmtList(it) => validate_rule_stmt_list(it, errors), ast::Select(it) => validate_select(it, errors), @@ -357,6 +370,51 @@ fn validate_atomic_body(it: ast::AtomicBody, acc: &mut Vec) { } } +// -- err +// declare x U&"tbl" UESCAPE '!'%type; +// declare x U&"tbl" UESCAPE '!'%rowtype; +fn validate_plpgsql_percent_type( + syntax: &SyntaxNode, + percent: Option, + percent_type: &str, + acc: &mut Vec, +) { + if !syntax + .parent() + .is_some_and(|parent| matches!(parent.kind(), PLPGSQL_VAR_DECL | PLPGSQL_CURSOR_ARG)) + { + return; + } + let has_uescape = syntax + .descendants_with_tokens() + .any(|element| element.kind() == UESCAPE_KW); + if !has_uescape { + return; + } + let Some(percent) = percent else { + return; + }; + acc.push(SyntaxError::new( + format!("UESCAPE is not allowed before {percent_type}"), + percent.text_range(), + )); +} + +fn validate_plpgsql_block(it: ast::PlpgsqlBlock, acc: &mut Vec) { + if it.semicolon_token().is_some() + || it + .syntax() + .parent() + .is_none_or(|parent| parent.kind() != PLPGSQL_BODY) + { + return; + } + acc.push(SyntaxError::new( + "Missing semicolon after block", + TextRange::empty(it.syntax().text_range().end()), + )); +} + fn validate_print_strict_params( it: ast::PlpgsqlCompOptionPrintStrictParams, acc: &mut Vec,