Skip to content

chore: expose schema_cache & file_context in lint rules #449

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Jul 15, 2025
Merged
Show file tree
Hide file tree
Changes from 13 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
4 changes: 4 additions & 0 deletions Cargo.lock

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

9 changes: 5 additions & 4 deletions crates/pgt_analyse/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ version = "0.0.0"


[dependencies]
pgt_console.workspace = true
pgt_diagnostics.workspace = true
pgt_query_ext.workspace = true
rustc-hash = { workspace = true }
pgt_console.workspace = true
pgt_diagnostics.workspace = true
pgt_query_ext.workspace = true
pgt_schema_cache.workspace = true
rustc-hash = { workspace = true }

biome_deserialize = { workspace = true, optional = true }
biome_deserialize_macros = { workspace = true, optional = true }
Expand Down
7 changes: 7 additions & 0 deletions crates/pgt_analyse/src/analysed_file_context.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#[derive(Default)]
pub struct AnalysedFileContext {}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this for?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's just a scaffold for the file context.
We can add properties in future linting PRs.
eugene works the same way: link


impl AnalysedFileContext {
#[allow(unused)]
pub fn update_from(&mut self, stmt_root: &pgt_query_ext::NodeEnum) {}
}
2 changes: 2 additions & 0 deletions crates/pgt_analyse/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod analysed_file_context;
mod categories;
pub mod context;
mod filter;
Expand All @@ -9,6 +10,7 @@ mod rule;
// Re-exported for use in the `declare_group` macro
pub use pgt_diagnostics::category_concat;

pub use crate::analysed_file_context::AnalysedFileContext;
pub use crate::categories::{
ActionCategory, RefactorKind, RuleCategories, RuleCategoriesBuilder, RuleCategory,
SUPPRESSION_ACTION_CATEGORY, SourceActionKind,
Expand Down
5 changes: 4 additions & 1 deletion crates/pgt_analyse/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::{borrow, collections::BTreeSet};

use crate::{
AnalyserOptions,
analysed_file_context::AnalysedFileContext,
context::RuleContext,
filter::{AnalysisFilter, GroupKey, RuleKey},
rule::{GroupCategory, Rule, RuleDiagnostic, RuleGroup},
Expand Down Expand Up @@ -158,6 +159,8 @@ impl RuleRegistry {
pub struct RegistryRuleParams<'a> {
pub root: &'a pgt_query_ext::NodeEnum,
pub options: &'a AnalyserOptions,
pub analysed_file_context: &'a AnalysedFileContext,
pub schema_cache: Option<&'a pgt_schema_cache::SchemaCache>,
}

/// Executor for rule as a generic function pointer
Expand All @@ -175,7 +178,7 @@ impl RegistryRule {
{
let options = params.options.rule_options::<R>().unwrap_or_default();
let ctx = RuleContext::new(params.root, &options);
R::run(&ctx)
R::run(&ctx, params.analysed_file_context, params.schema_cache)
}

Self { run: run::<R> }
Expand Down
15 changes: 14 additions & 1 deletion crates/pgt_analyse/src/rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ use pgt_diagnostics::{
Advices, Category, Diagnostic, DiagnosticTags, Location, LogCategory, MessageAndDescription,
Severity, Visit,
};
use pgt_schema_cache::SchemaCache;
use pgt_text_size::TextRange;
use std::cmp::Ordering;
use std::fmt::Debug;

use crate::analysed_file_context::AnalysedFileContext;
use crate::{categories::RuleCategory, context::RuleContext, registry::RegistryVisitor};

#[derive(Clone, Debug)]
Expand Down Expand Up @@ -102,7 +104,12 @@ pub trait GroupCategory {
pub trait Rule: RuleMeta + Sized {
type Options: Default + Clone + Debug;

fn run(ctx: &RuleContext<Self>) -> Vec<RuleDiagnostic>;
/// `schema_cache` will only be available if the user has a working database connection.
fn run(
rule_context: &RuleContext<Self>,
file_context: &AnalysedFileContext,
schema_cache: Option<&SchemaCache>,
) -> Vec<RuleDiagnostic>;
}

/// Diagnostic object returned by a single analysis rule
Expand Down Expand Up @@ -208,6 +215,12 @@ impl RuleDiagnostic {
self
}

/// Sets the span of this diagnostic.
pub fn span(mut self, span: TextRange) -> Self {
self.span = Some(span);
self
}

/// Marks this diagnostic as unnecessary code, which will
/// be displayed in the language server.
///
Expand Down
12 changes: 7 additions & 5 deletions crates/pgt_analyser/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ repository.workspace = true
version = "0.0.0"

[dependencies]
pgt_analyse = { workspace = true }
pgt_console = { workspace = true }
pgt_diagnostics = { workspace = true }
pgt_query_ext = { workspace = true }
serde = { workspace = true }
pgt_analyse = { workspace = true }
pgt_console = { workspace = true }
pgt_diagnostics = { workspace = true }
pgt_query_ext = { workspace = true }
pgt_schema_cache = { workspace = true }
pgt_text_size = { workspace = true }
serde = { workspace = true }

[dev-dependencies]
insta = { version = "1.42.1" }
Expand Down
58 changes: 42 additions & 16 deletions crates/pgt_analyser/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use std::{ops::Deref, sync::LazyLock};

use pgt_analyse::{
AnalyserOptions, AnalysisFilter, MetadataRegistry, RegistryRuleParams, RuleDiagnostic,
RuleRegistry,
AnalysedFileContext, AnalyserOptions, AnalysisFilter, MetadataRegistry, RegistryRuleParams,
RuleDiagnostic, RuleRegistry,
};
pub use registry::visit_registry;

Expand Down Expand Up @@ -30,8 +30,15 @@ pub struct Analyser<'a> {
registry: RuleRegistry,
}

pub struct AnalyserContext<'a> {
pub root: &'a pgt_query_ext::NodeEnum,
#[derive(Debug)]
pub struct AnalysableStatement {
pub root: pgt_query_ext::NodeEnum,
pub range: pgt_text_size::TextRange,
}

pub struct AnalyserParams<'a> {
pub stmts: Vec<AnalysableStatement>,
pub schema_cache: Option<&'a pgt_schema_cache::SchemaCache>,
}

pub struct AnalyserConfig<'a> {
Expand All @@ -52,17 +59,31 @@ impl<'a> Analyser<'a> {
}
}

pub fn run(&self, ctx: AnalyserContext) -> Vec<RuleDiagnostic> {
let params = RegistryRuleParams {
root: ctx.root,
options: self.options,
};
pub fn run(&self, params: AnalyserParams) -> Vec<RuleDiagnostic> {
let mut diagnostics = vec![];

let mut file_context = AnalysedFileContext::default();

for stmt in params.stmts {
let rule_params = RegistryRuleParams {
root: &stmt.root,
options: self.options,
analysed_file_context: &file_context,
schema_cache: params.schema_cache,
};

self.registry
.rules
.iter()
.flat_map(|rule| (rule.run)(&params))
.collect::<Vec<_>>()
diagnostics.extend(
self.registry
.rules
.iter()
.flat_map(|rule| (rule.run)(&rule_params))
.map(|r| r.span(stmt.range)),
);

file_context.update_from(&stmt.root);
}

diagnostics
}
}

Expand All @@ -77,9 +98,10 @@ mod tests {
markup,
};
use pgt_diagnostics::PrintDiagnostic;
use pgt_text_size::TextRange;
use termcolor::NoColor;

use crate::Analyser;
use crate::{AnalysableStatement, Analyser};

#[ignore]
#[test]
Expand All @@ -102,6 +124,7 @@ mod tests {
};

let ast = pgt_query_ext::parse(SQL).expect("failed to parse SQL");
let range = TextRange::new(0.into(), u32::try_from(SQL.len()).unwrap().into());

let options = AnalyserOptions::default();

Expand All @@ -110,7 +133,10 @@ mod tests {
filter,
});

let results = analyser.run(crate::AnalyserContext { root: &ast });
let results = analyser.run(crate::AnalyserParams {
stmts: vec![AnalysableStatement { root: ast, range }],
schema_cache: None,
});

println!("*******************");
for result in &results {
Expand Down
11 changes: 9 additions & 2 deletions crates/pgt_analyser/src/lint/safety/adding_required_field.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use pgt_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule};
use pgt_analyse::{
AnalysedFileContext, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule,
};
use pgt_console::markup;
use pgt_diagnostics::Severity;
use pgt_schema_cache::SchemaCache;

declare_lint_rule! {
/// Adding a new column that is NOT NULL and has no default value to an existing table effectively makes it required.
Expand All @@ -27,7 +30,11 @@ declare_lint_rule! {
impl Rule for AddingRequiredField {
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Vec<RuleDiagnostic> {
fn run(
ctx: &RuleContext<Self>,
_file_context: &AnalysedFileContext,
_schema_cache: Option<&SchemaCache>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from an API standpoint, I would put these two into the RuleContext. After all, they are "context" info.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I felt the RuleContext held static metadata about the rule, while the other stuff is related to the db/data model and the parsed sql file.

But I don't have a hard opinion

) -> Vec<RuleDiagnostic> {
let mut diagnostics = vec![];

if let pgt_query_ext::NodeEnum::AlterTableStmt(stmt) = ctx.stmt() {
Expand Down
11 changes: 9 additions & 2 deletions crates/pgt_analyser/src/lint/safety/ban_drop_column.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use pgt_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule};
use pgt_analyse::{
AnalysedFileContext, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule,
};
use pgt_console::markup;
use pgt_diagnostics::Severity;
use pgt_schema_cache::SchemaCache;

declare_lint_rule! {
/// Dropping a column may break existing clients.
Expand Down Expand Up @@ -29,7 +32,11 @@ declare_lint_rule! {
impl Rule for BanDropColumn {
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Vec<RuleDiagnostic> {
fn run(
ctx: &RuleContext<Self>,
_file_context: &AnalysedFileContext,
_schema_cache: Option<&SchemaCache>,
) -> Vec<RuleDiagnostic> {
let mut diagnostics = Vec::new();

if let pgt_query_ext::NodeEnum::AlterTableStmt(stmt) = &ctx.stmt() {
Expand Down
11 changes: 9 additions & 2 deletions crates/pgt_analyser/src/lint/safety/ban_drop_database.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use pgt_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule};
use pgt_analyse::{
AnalysedFileContext, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule,
};
use pgt_console::markup;
use pgt_diagnostics::Severity;
use pgt_schema_cache::SchemaCache;

declare_lint_rule! {
/// Dropping a database may break existing clients (and everything else, really).
Expand All @@ -18,7 +21,11 @@ declare_lint_rule! {
impl Rule for BanDropDatabase {
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Vec<RuleDiagnostic> {
fn run(
ctx: &RuleContext<Self>,
_file_context: &AnalysedFileContext,
_schema_cache: Option<&SchemaCache>,
) -> Vec<RuleDiagnostic> {
let mut diagnostics = vec![];

if let pgt_query_ext::NodeEnum::DropdbStmt(_) = &ctx.stmt() {
Expand Down
11 changes: 9 additions & 2 deletions crates/pgt_analyser/src/lint/safety/ban_drop_not_null.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use pgt_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule};
use pgt_analyse::{
AnalysedFileContext, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule,
};
use pgt_console::markup;
use pgt_diagnostics::Severity;
use pgt_schema_cache::SchemaCache;

declare_lint_rule! {
/// Dropping a NOT NULL constraint may break existing clients.
Expand Down Expand Up @@ -29,7 +32,11 @@ declare_lint_rule! {
impl Rule for BanDropNotNull {
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Vec<RuleDiagnostic> {
fn run(
ctx: &RuleContext<Self>,
_file_context: &AnalysedFileContext,
_schema_cache: Option<&SchemaCache>,
) -> Vec<RuleDiagnostic> {
let mut diagnostics = Vec::new();

if let pgt_query_ext::NodeEnum::AlterTableStmt(stmt) = &ctx.stmt() {
Expand Down
11 changes: 9 additions & 2 deletions crates/pgt_analyser/src/lint/safety/ban_drop_table.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use pgt_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule};
use pgt_analyse::{
AnalysedFileContext, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule,
};
use pgt_console::markup;
use pgt_diagnostics::Severity;
use pgt_schema_cache::SchemaCache;

declare_lint_rule! {
/// Dropping a table may break existing clients.
Expand Down Expand Up @@ -28,7 +31,11 @@ declare_lint_rule! {
impl Rule for BanDropTable {
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Vec<RuleDiagnostic> {
fn run(
ctx: &RuleContext<Self>,
_file_context: &AnalysedFileContext,
_schema_cache: Option<&SchemaCache>,
) -> Vec<RuleDiagnostic> {
let mut diagnostics = vec![];

if let pgt_query_ext::NodeEnum::DropStmt(stmt) = &ctx.stmt() {
Expand Down
11 changes: 9 additions & 2 deletions crates/pgt_analyser/src/lint/safety/ban_truncate_cascade.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use pgt_analyse::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule};
use pgt_analyse::{
AnalysedFileContext, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule,
};
use pgt_console::markup;
use pgt_diagnostics::Severity;
use pgt_query_ext::protobuf::DropBehavior;
use pgt_schema_cache::SchemaCache;

declare_lint_rule! {
/// Using `TRUNCATE`'s `CASCADE` option will truncate any tables that are also foreign-keyed to the specified tables.
Expand Down Expand Up @@ -31,7 +34,11 @@ declare_lint_rule! {
impl Rule for BanTruncateCascade {
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Vec<RuleDiagnostic> {
fn run(
ctx: &RuleContext<Self>,
_file_context: &AnalysedFileContext,
_schema_cache: Option<&SchemaCache>,
) -> Vec<RuleDiagnostic> {
let mut diagnostics = Vec::new();

if let pgt_query_ext::NodeEnum::TruncateStmt(stmt) = &ctx.stmt() {
Expand Down
Loading