Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
crates/squawk_parser/src/generated/* linguist-generated=true
crates/squawk_syntax/src/ast/generated/* linguist-generated=true
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,8 @@
},
"[sql]": {
"editor.tabSize": 2
},
"[ungrammar]": {
"editor.tabSize": 2
}
}
29 changes: 27 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ annotate-snippets = "0.11.5"
anyhow = "1.0.94"
convert_case = "0.7.1"
clap = { version = "4.5.8", features = ["derive"] }
ungrammar = "1.1.4"
quote = "1.0.40"
xshell = "0.2.7"
proc-macro2 = "1.0.95"

# local
squawk_github = { version = "0.0.0", path = "./crates/squawk_github" }
Expand Down
10 changes: 5 additions & 5 deletions crates/squawk_linter/src/rules/adding_field_with_default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use lazy_static::lazy_static;
use std::collections::HashSet;

use squawk_syntax::ast;
use squawk_syntax::ast::{AstNode, HasArgList};
use squawk_syntax::{ast::HasModuleItem, Parse, SourceFile};
use squawk_syntax::ast::AstNode;
use squawk_syntax::{Parse, SourceFile};

use crate::{Linter, Rule, Violation};

Expand Down Expand Up @@ -57,8 +57,8 @@ pub(crate) fn adding_field_with_default(ctx: &mut Linter, parse: &Parse<SourceFi
let help = "Add the column as nullable, backfill existing rows, and add a trigger to update the column on write instead.";
let file = parse.tree();
// TODO: use match_ast! like in #api_walkthrough
for item in file.items() {
if let ast::Stmt::AlterTable(alter_table) = item {
for stmt in file.stmts() {
if let ast::Stmt::AlterTable(alter_table) = stmt {
for action in alter_table.actions() {
if let ast::AlterTableAction::AddColumn(add_column) = action {
for constraint in add_column.constraints() {
Expand Down Expand Up @@ -273,14 +273,14 @@ ALTER TABLE "core_recipe" ADD COLUMN "foo" timestamptz DEFAULT now();

#[test]
fn add_numbers_ok() {
// This should be okay, but we don't handle expressions like this at the moment.
let sql = r#"
alter table account_metadata add column blah integer default 2 + 2;
"#;

let file = squawk_syntax::SourceFile::parse(sql);
let mut linter = Linter::from([Rule::AddingFieldWithDefault]);
let errors = linter.lint(file, sql);
assert!(errors.is_empty());
assert_debug_snapshot!(errors);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use squawk_syntax::{
ast::{self, AstNode, HasModuleItem},
ast::{self, AstNode},
Parse, SourceFile,
};

Expand All @@ -10,8 +10,8 @@ pub(crate) fn adding_foreign_key_constraint(ctx: &mut Linter, parse: &Parse<Sour
let help = "Add `NOT VALID` to the constraint in one transaction and then VALIDATE the constraint in a separate transaction.";
let file = parse.tree();
// TODO: use match_ast! like in #api_walkthrough
for item in file.items() {
if let ast::Stmt::AlterTable(alter_table) = item {
for stmt in file.stmts() {
if let ast::Stmt::AlterTable(alter_table) = stmt {
for action in alter_table.actions() {
match action {
ast::AlterTableAction::AddConstraint(add_constraint) => {
Expand Down
6 changes: 3 additions & 3 deletions crates/squawk_linter/src/rules/adding_not_null_field.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use squawk_syntax::{
ast::{self, AstNode, HasModuleItem},
ast::{self, AstNode},
Parse, SourceFile,
};

Expand All @@ -10,8 +10,8 @@ pub(crate) fn adding_not_null_field(ctx: &mut Linter, parse: &Parse<SourceFile>)
return;
}
let file = parse.tree();
for item in file.items() {
if let ast::Stmt::AlterTable(alter_table) = item {
for stmt in file.stmts() {
if let ast::Stmt::AlterTable(alter_table) = stmt {
for action in alter_table.actions() {
if let ast::AlterTableAction::AlterColumn(alter_column) = action {
let Some(option) = alter_column.option() else {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use squawk_syntax::{
ast::{self, AstNode, HasModuleItem},
ast::{self, AstNode},
Parse, SourceFile,
};

Expand All @@ -9,8 +9,8 @@ pub(crate) fn adding_primary_key_constraint(ctx: &mut Linter, parse: &Parse<Sour
let message = "Adding a primary key constraint requires an `ACCESS EXCLUSIVE` lock that will block all reads and writes to the table while the primary key index is built.";
let help = "Add the `PRIMARY KEY` constraint `USING` an index.";
let file = parse.tree();
for item in file.items() {
if let ast::Stmt::AlterTable(alter_table) = item {
for stmt in file.stmts() {
if let ast::Stmt::AlterTable(alter_table) = stmt {
for action in alter_table.actions() {
match action {
ast::AlterTableAction::AddConstraint(add_constraint) => {
Expand Down
6 changes: 3 additions & 3 deletions crates/squawk_linter/src/rules/adding_required_field.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
use squawk_syntax::{
ast::{self, AstNode, HasModuleItem},
ast::{self, AstNode},
Parse, SourceFile,
};

use crate::{Linter, Rule, Violation};

pub(crate) fn adding_required_field(ctx: &mut Linter, parse: &Parse<SourceFile>) {
let file = parse.tree();
for item in file.items() {
if let ast::Stmt::AlterTable(alter_table) = item {
for stmt in file.stmts() {
if let ast::Stmt::AlterTable(alter_table) = stmt {
for action in alter_table.actions() {
if let ast::AlterTableAction::AddColumn(add_column) = action {
if has_generated_constrait(add_column.constraints()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
use squawk_syntax::{
ast::{self, AstNode, HasModuleItem},
ast::{self, AstNode},
Parse, SourceFile,
};

use crate::{Linter, Rule, Violation};

pub(crate) fn ban_alter_domain_with_add_constraint(ctx: &mut Linter, parse: &Parse<SourceFile>) {
let file = parse.tree();
for item in file.items() {
if let ast::Stmt::AlterDomain(alter_domain) = item {
let actions = alter_domain.actions();
for action in actions {
if let ast::AlterDomainAction::AddConstraint(add_constraint) = action {
ctx.report(Violation::new(
for stmt in file.stmts() {
if let ast::Stmt::AlterDomain(alter_domain) = stmt {
if let Some(ast::AlterDomainAction::AddConstraint(add_constraint)) =
alter_domain.action()
{
ctx.report(Violation::new(
Rule::BanAlterDomainWithAddConstraint,
"Domains with constraints have poor support for online migrations. Use table and column constraints instead.".into(),
add_constraint.syntax().text_range(),
None,
))
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
use squawk_syntax::{
ast::{self, HasModuleItem},
Parse, SourceFile,
};
use squawk_syntax::{ast, Parse, SourceFile};

use crate::{Linter, Rule, Violation};

Expand All @@ -13,9 +10,9 @@ pub(crate) fn ban_concurrent_index_creation_in_transaction(
let file = parse.tree();
let mut errors = vec![];
let mut stmt_count = 0;
for item in file.items() {
for stmt in file.stmts() {
stmt_count += 1;
match item {
match stmt {
ast::Stmt::Begin(_) => {
in_transaction = true;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
use rowan::TextRange;
use squawk_syntax::{
ast::{self, AstNode, HasModuleItem},
ast::{self, AstNode},
Parse, SourceFile,
};

use crate::{Linter, Rule, Violation};

pub(crate) fn ban_create_domain_with_constraint(ctx: &mut Linter, parse: &Parse<SourceFile>) {
let file = parse.tree();
for item in file.items() {
if let ast::Stmt::CreateDomain(domain) = item {
for stmt in file.stmts() {
if let ast::Stmt::CreateDomain(domain) = stmt {
let range =
domain
.constraints()
Expand Down
6 changes: 3 additions & 3 deletions crates/squawk_linter/src/rules/ban_drop_column.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
use squawk_syntax::{
ast::{self, AstNode, HasModuleItem},
ast::{self, AstNode},
Parse, SourceFile,
};

use crate::{Linter, Rule, Violation};

pub(crate) fn ban_drop_column(ctx: &mut Linter, parse: &Parse<SourceFile>) {
let file = parse.tree();
for item in file.items() {
if let ast::Stmt::AlterTable(alter_table) = item {
for stmt in file.stmts() {
if let ast::Stmt::AlterTable(alter_table) = stmt {
for action in alter_table.actions() {
if let ast::AlterTableAction::DropColumn(drop_column) = action {
ctx.report(Violation::new(
Expand Down
6 changes: 3 additions & 3 deletions crates/squawk_linter/src/rules/ban_drop_database.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use squawk_syntax::{
ast::{self, AstNode, HasModuleItem},
ast::{self, AstNode},
Parse, SourceFile,
};

Expand All @@ -8,8 +8,8 @@ use crate::{Linter, Rule, Violation};
/// Brad's Rule aka ban dropping database statements.
pub(crate) fn ban_drop_database(ctx: &mut Linter, parse: &Parse<SourceFile>) {
let file = parse.tree();
for item in file.items() {
if let ast::Stmt::DropDatabase(drop_database) = item {
for stmt in file.stmts() {
if let ast::Stmt::DropDatabase(drop_database) = stmt {
ctx.report(Violation::new(
Rule::BanDropDatabase,
"Dropping a database may break existing clients.".into(),
Expand Down
6 changes: 3 additions & 3 deletions crates/squawk_linter/src/rules/ban_drop_not_null.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
use squawk_syntax::{
ast::{self, AstNode, HasModuleItem},
ast::{self, AstNode},
Parse, SourceFile,
};

use crate::{Linter, Rule, Violation};

pub(crate) fn ban_drop_not_null(ctx: &mut Linter, parse: &Parse<SourceFile>) {
let file = parse.tree();
for item in file.items() {
if let ast::Stmt::AlterTable(alter_table) = item {
for stmt in file.stmts() {
if let ast::Stmt::AlterTable(alter_table) = stmt {
for action in alter_table.actions() {
if let ast::AlterTableAction::AlterColumn(alter_column) = action {
if let Some(ast::AlterColumnOption::DropNotNull(drop_not_null)) =
Expand Down
6 changes: 3 additions & 3 deletions crates/squawk_linter/src/rules/ban_drop_table.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
use squawk_syntax::{
ast::{self, AstNode, HasModuleItem},
ast::{self, AstNode},
Parse, SourceFile,
};

use crate::{Linter, Rule, Violation};

pub(crate) fn ban_drop_table(ctx: &mut Linter, parse: &Parse<SourceFile>) {
let file = parse.tree();
for item in file.items() {
if let ast::Stmt::DropTable(drop_table) = item {
for stmt in file.stmts() {
if let ast::Stmt::DropTable(drop_table) = stmt {
ctx.report(Violation::new(
Rule::BanDropTable,
"Dropping a table may break existing clients.".into(),
Expand Down
32 changes: 13 additions & 19 deletions crates/squawk_linter/src/rules/ban_truncate_cascade.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,22 @@
use squawk_syntax::{
ast::{self, HasModuleItem},
Parse, SourceFile,
};
use squawk_syntax::{ast, Parse, SourceFile};

use crate::{Linter, Rule, Violation};

pub(crate) fn ban_truncate_cascade(ctx: &mut Linter, parse: &Parse<SourceFile>) {
let file = parse.tree();
for item in file.items() {
match item {
ast::Stmt::Truncate(truncate) => {
if let Some(cascade) = truncate.cascade_token() {
// TODO: if we had knowledge about the entire schema, we
// could be more precise here and actually navigate the
// foreign keys.
ctx.report(Violation::new(
Rule::BanTruncateCascade,
format!("Using `CASCADE` will recursively truncate any tables that foreign key to the referenced tables! So if you had foreign keys setup as `a <- b <- c` and truncated `a`, then `b` & `c` would also be truncated!"),
cascade.text_range(),
"Remove the `CASCADE` and specify exactly which tables you want to truncate.".to_string(),
));
}
for stmt in file.stmts() {
if let ast::Stmt::Truncate(truncate) = stmt {
if let Some(cascade) = truncate.cascade_token() {
// TODO: if we had knowledge about the entire schema, we
// could be more precise here and actually navigate the
// foreign keys.
ctx.report(Violation::new(
Rule::BanTruncateCascade,
"Using `CASCADE` will recursively truncate any tables that foreign key to the referenced tables! So if you had foreign keys setup as `a <- b <- c` and truncated `a`, then `b` & `c` would also be truncated!".to_string(),
cascade.text_range(),
"Remove the `CASCADE` and specify exactly which tables you want to truncate.".to_string(),
));
}
_ => (),
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions crates/squawk_linter/src/rules/changing_column_type.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
use squawk_syntax::{
ast::{self, AstNode, HasModuleItem},
ast::{self, AstNode},
Parse, SourceFile,
};

use crate::{Linter, Rule, Violation};

pub(crate) fn changing_column_type(ctx: &mut Linter, parse: &Parse<SourceFile>) {
let file = parse.tree();
for item in file.items() {
if let ast::Stmt::AlterTable(alter_table) = item {
for stmt in file.stmts() {
if let ast::Stmt::AlterTable(alter_table) = stmt {
for action in alter_table.actions() {
if let ast::AlterTableAction::AlterColumn(alter_column) = action {
if let Some(ast::AlterColumnOption::SetType(set_type)) = alter_column.option() {
Expand Down
Loading
Loading