diff --git a/crates/squawk_linter/src/analyze.rs b/crates/squawk_linter/src/analyze.rs index a496acb4..b65c7305 100644 --- a/crates/squawk_linter/src/analyze.rs +++ b/crates/squawk_linter/src/analyze.rs @@ -184,6 +184,7 @@ pub fn possibly_slow_stmt(stmt: &ast::Stmt) -> bool { | ast::Stmt::Delete(_) | ast::Stmt::Discard(_) | ast::Stmt::Do(_) + | ast::Stmt::EmptyStmt(_) | ast::Stmt::Execute(_) | ast::Stmt::Explain(_) | ast::Stmt::Fetch(_) diff --git a/crates/squawk_parser/src/generated/syntax_kind.rs b/crates/squawk_parser/src/generated/syntax_kind.rs index 4555b0d7..24b2cacf 100644 --- a/crates/squawk_parser/src/generated/syntax_kind.rs +++ b/crates/squawk_parser/src/generated/syntax_kind.rs @@ -816,6 +816,7 @@ pub enum SyntaxKind { EDGE_TABLES, EDGE_TABLE_DEF, ELSE_CLAUSE, + EMPTY_STMT, ENABLE_ALWAYS_RULE, ENABLE_ALWAYS_TRIGGER, ENABLE_REPLICA_RULE, diff --git a/crates/squawk_parser/src/grammar.rs b/crates/squawk_parser/src/grammar.rs index 19062fb2..e7984fa8 100644 --- a/crates/squawk_parser/src/grammar.rs +++ b/crates/squawk_parser/src/grammar.rs @@ -5677,6 +5677,7 @@ struct StmtRestrictions { fn stmt(p: &mut Parser, r: &StmtRestrictions) -> Option { match (p.current(), p.nth(1)) { + (SEMICOLON, _) => Some(empty_stmt(p)), (ABORT_KW, _) => Some(rollback(p)), (ALTER_KW, AGGREGATE_KW) => Some(alter_aggregate(p)), (ALTER_KW, COLLATION_KW) => Some(alter_collation(p)), @@ -5959,6 +5960,14 @@ fn stmt(p: &mut Parser, r: &StmtRestrictions) -> Option { } } +// handle things like: ;;;select 1 +fn empty_stmt(p: &mut Parser<'_>) -> CompletedMarker { + assert!(p.at(SEMICOLON)); + let m = p.start(); + p.bump(SEMICOLON); + m.complete(p, EMPTY_STMT) +} + // ALTER STATISTICS name OWNER TO { new_owner | CURRENT_ROLE | CURRENT_USER | SESSION_USER } // ALTER STATISTICS name RENAME TO new_name // ALTER STATISTICS name SET SCHEMA new_schema @@ -15293,17 +15302,16 @@ fn set_data_type(p: &mut Parser<'_>) { pub(crate) fn entry_point(p: &mut Parser) { let m = p.start(); while !p.at(EOF) { - // handle things like: ;;;select 1 - if p.eat(SEMICOLON) { - continue; - } let parsed_stmt = stmt( p, &StmtRestrictions { begin_end_allowed: true, }, ); - if !p.at(EOF) && parsed_stmt.is_some() { + if !p.at(EOF) + && let Some(marker) = parsed_stmt + && marker.kind() != EMPTY_STMT + { p.expect(SEMICOLON); } } diff --git a/crates/squawk_parser/tests/snapshots/tests__alter_table_err.snap b/crates/squawk_parser/tests/snapshots/tests__alter_table_err.snap index 56597b55..855ab63e 100644 --- a/crates/squawk_parser/tests/snapshots/tests__alter_table_err.snap +++ b/crates/squawk_parser/tests/snapshots/tests__alter_table_err.snap @@ -16,7 +16,8 @@ SOURCE_FILE WHITESPACE " " ERROR BOOLEAN_KW "boolean" - SEMICOLON ";" + EMPTY_STMT + SEMICOLON ";" WHITESPACE "\n\n" COMMENT "-- mismatch options" WHITESPACE "\n" diff --git a/crates/squawk_parser/tests/snapshots/tests__drop_rule_ok.snap b/crates/squawk_parser/tests/snapshots/tests__drop_rule_ok.snap index b10c0ee2..82af3b9b 100644 --- a/crates/squawk_parser/tests/snapshots/tests__drop_rule_ok.snap +++ b/crates/squawk_parser/tests/snapshots/tests__drop_rule_ok.snap @@ -21,7 +21,8 @@ SOURCE_FILE NAME_REF IDENT "t" SEMICOLON ";" - SEMICOLON ";" + EMPTY_STMT + SEMICOLON ";" WHITESPACE "\n\n" COMMENT "-- full" WHITESPACE "\n" diff --git a/crates/squawk_syntax/src/ast/generated/nodes.rs b/crates/squawk_syntax/src/ast/generated/nodes.rs index 661cffe2..e18539f3 100644 --- a/crates/squawk_syntax/src/ast/generated/nodes.rs +++ b/crates/squawk_syntax/src/ast/generated/nodes.rs @@ -8242,6 +8242,17 @@ impl ElseClause { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct EmptyStmt { + pub(crate) syntax: SyntaxNode, +} +impl EmptyStmt { + #[inline] + pub fn semicolon_token(&self) -> Option { + support::token(&self.syntax, SyntaxKind::SEMICOLON) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct EnableAlwaysRule { pub(crate) syntax: SyntaxNode, @@ -18963,6 +18974,7 @@ pub enum Stmt { DropUser(DropUser), DropUserMapping(DropUserMapping), DropView(DropView), + EmptyStmt(EmptyStmt), Execute(Execute), Explain(Explain), Fetch(Fetch), @@ -23606,6 +23618,24 @@ impl AstNode for ElseClause { &self.syntax } } +impl AstNode for EmptyStmt { + #[inline] + fn can_cast(kind: SyntaxKind) -> bool { + kind == SyntaxKind::EMPTY_STMT + } + #[inline] + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + #[inline] + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} impl AstNode for EnableAlwaysRule { #[inline] fn can_cast(kind: SyntaxKind) -> bool { @@ -34736,6 +34766,7 @@ impl AstNode for Stmt { | SyntaxKind::DROP_USER | SyntaxKind::DROP_USER_MAPPING | SyntaxKind::DROP_VIEW + | SyntaxKind::EMPTY_STMT | SyntaxKind::EXECUTE | SyntaxKind::EXPLAIN | SyntaxKind::FETCH @@ -34995,6 +35026,7 @@ impl AstNode for Stmt { SyntaxKind::DROP_USER => Stmt::DropUser(DropUser { syntax }), SyntaxKind::DROP_USER_MAPPING => Stmt::DropUserMapping(DropUserMapping { syntax }), SyntaxKind::DROP_VIEW => Stmt::DropView(DropView { syntax }), + SyntaxKind::EMPTY_STMT => Stmt::EmptyStmt(EmptyStmt { syntax }), SyntaxKind::EXECUTE => Stmt::Execute(Execute { syntax }), SyntaxKind::EXPLAIN => Stmt::Explain(Explain { syntax }), SyntaxKind::FETCH => Stmt::Fetch(Fetch { syntax }), @@ -35192,6 +35224,7 @@ impl AstNode for Stmt { Stmt::DropUser(it) => &it.syntax, Stmt::DropUserMapping(it) => &it.syntax, Stmt::DropView(it) => &it.syntax, + Stmt::EmptyStmt(it) => &it.syntax, Stmt::Execute(it) => &it.syntax, Stmt::Explain(it) => &it.syntax, Stmt::Fetch(it) => &it.syntax, @@ -36099,6 +36132,12 @@ impl From for Stmt { Stmt::DropView(node) } } +impl From for Stmt { + #[inline] + fn from(node: EmptyStmt) -> Stmt { + Stmt::EmptyStmt(node) + } +} impl From for Stmt { #[inline] fn from(node: Execute) -> Stmt { diff --git a/crates/squawk_syntax/src/postgresql.ungram b/crates/squawk_syntax/src/postgresql.ungram index e9a2f1f2..46e5ec6c 100644 --- a/crates/squawk_syntax/src/postgresql.ungram +++ b/crates/squawk_syntax/src/postgresql.ungram @@ -861,6 +861,9 @@ Truncate = ('restart' 'identity' | 'continue' 'identity')? ('cascade' | 'restrict')? +EmptyStmt = + ';' + LikeOption = ('including' | 'excluding') ('comments' @@ -3618,7 +3621,7 @@ Stmt = | Vacuum | Values | ParenSelect -| ';' +| EmptyStmt SourceFile =