diff --git a/src/dynamic/framework/adapters/migration_go_migrate.rs b/src/dynamic/framework/adapters/migration_go_migrate.rs new file mode 100644 index 00000000..2f2b1702 --- /dev/null +++ b/src/dynamic/framework/adapters/migration_go_migrate.rs @@ -0,0 +1,209 @@ +//! golang-migrate migration adapter (Go). +//! +//! Fires when the surrounding source imports the +//! `github.com/golang-migrate/migrate` driver and the function under +//! analysis is the canonical migration runner (drives `m.Up()` / +//! `m.Down()` / `m.Steps(n)` / `m.Migrate(version)` against a +//! `migrate.Migrate` instance) or itself names one of those entry +//! verbs. +//! +//! Notably does NOT fire just because a helper function is named +//! `Up` / `Down` in a file that has no golang-migrate import marker. +//! The source-shape needle plus the entry-name / driver-callee gate +//! mirror the Phase 21 binding-stealing audit applied to +//! `migration_rails` and `migration_flyway`. + +use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding}; +use crate::evidence::EntryKind; +use crate::summary::FuncSummary; +use crate::symbol::Lang; + +pub struct MigrationGoMigrateAdapter; + +const ADAPTER_NAME: &str = "migration-go-migrate"; + +fn callee_is_go_migrate(name: &str) -> bool { + let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name); + matches!( + last, + "Up" | "Down" | "Steps" | "Migrate" | "Force" | "Drop" + ) +} + +fn source_imports_go_migrate(file_bytes: &[u8]) -> bool { + const NEEDLES: &[&[u8]] = &[ + b"github.com/golang-migrate/migrate", + b"migrate.New(", + b"migrate.NewWithDatabaseInstance(", + b"migrate.NewWithSourceInstance(", + ]; + NEEDLES + .iter() + .any(|n| file_bytes.windows(n.len()).any(|w| w == *n)) +} + +fn name_is_migration_entry(name: &str) -> bool { + matches!(name, "Up" | "Down" | "Steps" | "Migrate" | "Force") +} + +/// golang-migrate uses filename-encoded versions (`000001_init.up.sql` +/// / `000001_init.down.sql`); the Go-side runner only carries the +/// numeric version when `m.Migrate()` is called. Scan for the +/// argument to a `Migrate(` call as a best-effort version hint. +fn extract_version(file_bytes: &[u8]) -> Option { + let text = std::str::from_utf8(file_bytes).unwrap_or(""); + let needle = ".Migrate("; + let idx = text.find(needle)?; + let after = &text[idx + needle.len()..]; + let end = after.find(')')?; + let raw = after[..end].trim(); + if raw.is_empty() || !raw.chars().all(|c| c.is_ascii_digit()) { + return None; + } + Some(raw.to_owned()) +} + +impl FrameworkAdapter for MigrationGoMigrateAdapter { + fn name(&self) -> &'static str { + ADAPTER_NAME + } + + fn lang(&self) -> Lang { + Lang::Go + } + + fn detect( + &self, + summary: &FuncSummary, + _ast: tree_sitter::Node<'_>, + file_bytes: &[u8], + ) -> Option { + let has_shape = source_imports_go_migrate(file_bytes); + let name_matches = name_is_migration_entry(&summary.name); + let body_runs_driver = super::any_callee_matches(summary, callee_is_go_migrate); + let binds = has_shape && (name_matches || body_runs_driver); + if !binds { + return None; + } + Some(FrameworkBinding { + adapter: ADAPTER_NAME.to_owned(), + kind: EntryKind::Migration { + version: extract_version(file_bytes), + }, + route: None, + request_params: Vec::new(), + response_writer: None, + middleware: Vec::new(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::summary::CalleeSite; + + fn parse_go(src: &[u8]) -> tree_sitter::Tree { + let mut parser = tree_sitter::Parser::new(); + let lang = tree_sitter::Language::from(tree_sitter_go::LANGUAGE); + parser.set_language(&lang).unwrap(); + parser.parse(src, None).unwrap() + } + + #[test] + fn fires_on_go_migrate_up_runner() { + let src: &[u8] = b"package entry\n\ + import \"github.com/golang-migrate/migrate/v4\"\n\ + func RunMigrations() {\n\ + m, _ := migrate.New(\"file://./migrations\", \"postgres://x\")\n\ + m.Up()\n\ + }\n"; + let tree = parse_go(src); + let summary = FuncSummary { + name: "RunMigrations".into(), + callees: vec![CalleeSite::bare("m.Up")], + ..Default::default() + }; + let binding = MigrationGoMigrateAdapter + .detect(&summary, tree.root_node(), src) + .expect("golang-migrate runner binds"); + assert_eq!(binding.adapter, "migration-go-migrate"); + assert!(matches!(binding.kind, EntryKind::Migration { .. })); + } + + #[test] + fn fires_on_entry_named_up() { + let src: &[u8] = b"package entry\n\ + import \"github.com/golang-migrate/migrate/v4\"\n\ + func Up(m *migrate.Migrate) error { return m.Up() }\n"; + let tree = parse_go(src); + let summary = FuncSummary { + name: "Up".into(), + ..Default::default() + }; + assert!( + MigrationGoMigrateAdapter + .detect(&summary, tree.root_node(), src) + .is_some(), + "function named Up in a golang-migrate file must bind", + ); + } + + #[test] + fn skips_helper_named_up_without_go_migrate_import() { + let src: &[u8] = b"package entry\nfunc Up() {}\n"; + let tree = parse_go(src); + let summary = FuncSummary { + name: "Up".into(), + ..Default::default() + }; + assert!( + MigrationGoMigrateAdapter + .detect(&summary, tree.root_node(), src) + .is_none(), + "helper named Up without golang-migrate import must not bind", + ); + } + + #[test] + fn skips_unrelated_method_in_go_migrate_file() { + let src: &[u8] = b"package entry\n\ + import \"github.com/golang-migrate/migrate/v4\"\n\ + func helper() {}\n"; + let tree = parse_go(src); + let summary = FuncSummary { + name: "helper".into(), + ..Default::default() + }; + assert!( + MigrationGoMigrateAdapter + .detect(&summary, tree.root_node(), src) + .is_none(), + "helper without driver callee must not bind in a golang-migrate file", + ); + } + + #[test] + fn extracts_numeric_version_from_migrate_call() { + let src: &[u8] = b"package entry\n\ + import \"github.com/golang-migrate/migrate/v4\"\n\ + func RunTo() {\n\ + m, _ := migrate.New(\"file://./m\", \"postgres://x\")\n\ + m.Migrate(42)\n\ + }\n"; + let tree = parse_go(src); + let summary = FuncSummary { + name: "RunTo".into(), + callees: vec![CalleeSite::bare("m.Migrate")], + ..Default::default() + }; + let binding = MigrationGoMigrateAdapter + .detect(&summary, tree.root_node(), src) + .expect("binds"); + if let EntryKind::Migration { version } = binding.kind { + assert_eq!(version.as_deref(), Some("42")); + } else { + panic!("expected Migration entry kind"); + } + } +} diff --git a/src/dynamic/framework/adapters/migration_knex.rs b/src/dynamic/framework/adapters/migration_knex.rs new file mode 100644 index 00000000..7e1b1290 --- /dev/null +++ b/src/dynamic/framework/adapters/migration_knex.rs @@ -0,0 +1,176 @@ +//! Knex.js migration adapter (JS). +//! +//! Fires when the surrounding source declares the canonical Knex +//! migration export pair (`exports.up` / `exports.down` against a +//! `knex` instance) or imports the `knex` package directly. The +//! source-shape needle plus the entry-name / DDL-callee gate mirror +//! the Phase 21 binding-stealing audit applied to +//! `migration_sequelize` and `migration_flyway`. +//! +//! Notably does NOT collide with Sequelize migration files (which use +//! `(queryInterface, Sequelize)` formals and live in +//! `migration_sequelize.rs`). Knex migration files use the bare +//! `knex` argument and call into `knex.schema.*` builders or +//! `knex.raw(...)` for DDL. + +use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding}; +use crate::evidence::EntryKind; +use crate::summary::FuncSummary; +use crate::symbol::Lang; + +pub struct MigrationKnexAdapter; + +const ADAPTER_NAME: &str = "migration-knex"; + +fn callee_is_knex_ddl(name: &str) -> bool { + let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name); + matches!( + last, + "createTable" + | "createTableIfNotExists" + | "dropTable" + | "dropTableIfExists" + | "alterTable" + | "renameTable" + | "hasTable" + | "hasColumn" + | "raw" + | "schema" + ) +} + +fn source_imports_knex(file_bytes: &[u8]) -> bool { + const NEEDLES: &[&[u8]] = &[ + b"require('knex')", + b"require(\"knex\")", + b"from 'knex'", + b"from \"knex\"", + b"knex.schema.createTable", + b"knex.schema.dropTable", + b"knex.schema.alterTable", + b"knex.raw(", + ]; + NEEDLES + .iter() + .any(|n| file_bytes.windows(n.len()).any(|w| w == *n)) +} + +fn name_is_migration_entry(name: &str) -> bool { + matches!(name, "up" | "down") +} + +impl FrameworkAdapter for MigrationKnexAdapter { + fn name(&self) -> &'static str { + ADAPTER_NAME + } + + fn lang(&self) -> Lang { + Lang::JavaScript + } + + fn detect( + &self, + summary: &FuncSummary, + _ast: tree_sitter::Node<'_>, + file_bytes: &[u8], + ) -> Option { + let has_shape = source_imports_knex(file_bytes); + let name_matches = name_is_migration_entry(&summary.name); + let body_runs_ddl = super::any_callee_matches(summary, callee_is_knex_ddl); + let binds = has_shape && (name_matches || body_runs_ddl); + if !binds { + return None; + } + Some(FrameworkBinding { + adapter: ADAPTER_NAME.to_owned(), + kind: EntryKind::Migration { version: None }, + route: None, + request_params: Vec::new(), + response_writer: None, + middleware: Vec::new(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::summary::CalleeSite; + + fn parse_js(src: &[u8]) -> tree_sitter::Tree { + let mut parser = tree_sitter::Parser::new(); + let lang = tree_sitter::Language::from(tree_sitter_javascript::LANGUAGE); + parser.set_language(&lang).unwrap(); + parser.parse(src, None).unwrap() + } + + #[test] + fn fires_on_knex_up_export() { + let src: &[u8] = b"exports.up = function(knex) {\n\ + return knex.schema.createTable('users', function (table) { table.string('name'); });\n\ + };\n\ + exports.down = function(knex) { return knex.schema.dropTable('users'); };\n"; + let tree = parse_js(src); + let summary = FuncSummary { + name: "up".into(), + callees: vec![CalleeSite::bare("knex.schema.createTable")], + ..Default::default() + }; + let binding = MigrationKnexAdapter + .detect(&summary, tree.root_node(), src) + .expect("knex migration binds"); + assert_eq!(binding.adapter, "migration-knex"); + assert!(matches!(binding.kind, EntryKind::Migration { .. })); + } + + #[test] + fn fires_on_knex_raw_runner() { + let src: &[u8] = b"const knex = require('knex');\n\ + exports.up = async function(knex) { await knex.raw('CREATE TABLE u(id int)'); };\n"; + let tree = parse_js(src); + let summary = FuncSummary { + name: "up".into(), + callees: vec![CalleeSite::bare("knex.raw")], + ..Default::default() + }; + assert!( + MigrationKnexAdapter + .detect(&summary, tree.root_node(), src) + .is_some(), + "knex.raw DDL must bind", + ); + } + + #[test] + fn skips_helper_named_up_without_knex_import() { + let src: &[u8] = b"exports.up = function(ctx) { return ctx; };\n"; + let tree = parse_js(src); + let summary = FuncSummary { + name: "up".into(), + ..Default::default() + }; + assert!( + MigrationKnexAdapter + .detect(&summary, tree.root_node(), src) + .is_none(), + "helper named `up` without knex import must not bind", + ); + } + + #[test] + fn skips_unrelated_method_in_knex_file() { + let src: &[u8] = b"const knex = require('knex');\n\ + function helper() {}\n"; + let tree = parse_js(src); + let summary = FuncSummary { + name: "helper".into(), + ..Default::default() + }; + assert!( + MigrationKnexAdapter + .detect(&summary, tree.root_node(), src) + .is_none(), + "helper without DDL callee must not bind in a knex file", + ); + } +} diff --git a/src/dynamic/framework/adapters/migration_liquibase.rs b/src/dynamic/framework/adapters/migration_liquibase.rs new file mode 100644 index 00000000..629704e5 --- /dev/null +++ b/src/dynamic/framework/adapters/migration_liquibase.rs @@ -0,0 +1,233 @@ +//! Liquibase migration adapter (Java). +//! +//! Fires when the surrounding source declares a Java class implementing +//! `liquibase.change.custom.CustomTaskChange` / +//! `liquibase.change.custom.CustomSqlChange` (the canonical +//! programmatic-changeset interfaces) and the function under analysis +//! is the canonical `execute(Database)` / `generateStatements(Database)` +//! entry point or runs JDBC DDL through the supplied database handle. +//! +//! Notably does NOT fire just because a helper method is named +//! `execute` in a file that has no Liquibase import marker. The +//! source-shape needle plus the entry-name / DDL-callee gate together +//! mirror the Phase 21 binding-stealing audit applied to +//! `migration_flyway` and `migration_rails`. + +use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding}; +use crate::evidence::EntryKind; +use crate::summary::FuncSummary; +use crate::symbol::Lang; + +pub struct MigrationLiquibaseAdapter; + +const ADAPTER_NAME: &str = "migration-liquibase"; + +fn callee_is_liquibase_ddl(name: &str) -> bool { + let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name); + matches!( + last, + "execute" + | "executeUpdate" + | "executeStatement" + | "executeQuery" + | "executeLargeUpdate" + | "prepareStatement" + | "createStatement" + | "getJdbcExecutor" + | "addBatch" + | "executeBatch" + ) +} + +fn source_has_liquibase_shape(file_bytes: &[u8]) -> bool { + const NEEDLES: &[&[u8]] = &[ + b"liquibase.change.custom.CustomTaskChange", + b"liquibase.change.custom.CustomSqlChange", + b"liquibase.database.Database", + b"liquibase.statement.SqlStatement", + b"implements CustomTaskChange", + b"implements CustomSqlChange", + ]; + NEEDLES + .iter() + .any(|n| file_bytes.windows(n.len()).any(|w| w == *n)) +} + +fn name_is_migration_entry(name: &str) -> bool { + matches!(name, "execute" | "generateStatements") +} + +/// Liquibase changeset IDs travel in the surrounding XML / YAML / SQL +/// metadata, not in the Java changeset class itself. The closest +/// in-source signal is a `@DatabaseChange(name = "", ...)` +/// annotation on the change-class declaration. Scan for it; absent +/// annotation, return `None` so the verifier can fall back to filename +/// derivation later in the pipeline. +fn extract_version(file_bytes: &[u8]) -> Option { + let text = std::str::from_utf8(file_bytes).unwrap_or(""); + let needle = "@DatabaseChange("; + let idx = text.find(needle)?; + let after = &text[idx + needle.len()..]; + let name_key = "name"; + let name_idx = after.find(name_key)?; + let tail = &after[name_idx + name_key.len()..]; + let eq = tail.find('=')?; + let quoted = tail[eq + 1..].trim_start(); + let quote = quoted.chars().next()?; + if quote != '"' && quote != '\'' { + return None; + } + let body = "ed[1..]; + let end = body.find(quote)?; + let raw = body[..end].trim(); + if raw.is_empty() { + None + } else { + Some(raw.to_owned()) + } +} + +impl FrameworkAdapter for MigrationLiquibaseAdapter { + fn name(&self) -> &'static str { + ADAPTER_NAME + } + + fn lang(&self) -> Lang { + Lang::Java + } + + fn detect( + &self, + summary: &FuncSummary, + _ast: tree_sitter::Node<'_>, + file_bytes: &[u8], + ) -> Option { + let has_shape = source_has_liquibase_shape(file_bytes); + let name_matches = name_is_migration_entry(&summary.name); + let body_runs_ddl = super::any_callee_matches(summary, callee_is_liquibase_ddl); + let binds = has_shape && (name_matches || body_runs_ddl); + if !binds { + return None; + } + Some(FrameworkBinding { + adapter: ADAPTER_NAME.to_owned(), + kind: EntryKind::Migration { + version: extract_version(file_bytes), + }, + route: None, + request_params: Vec::new(), + response_writer: None, + middleware: Vec::new(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn parse_java(src: &[u8]) -> tree_sitter::Tree { + let mut parser = tree_sitter::Parser::new(); + let lang = tree_sitter::Language::from(tree_sitter_java::LANGUAGE); + parser.set_language(&lang).unwrap(); + parser.parse(src, None).unwrap() + } + + #[test] + fn fires_on_custom_task_change() { + let src: &[u8] = b"import liquibase.change.custom.CustomTaskChange;\n\ + import liquibase.database.Database;\n\ + public class AddIndex implements CustomTaskChange {\n\ + public void execute(Database database) throws Exception { }\n\ + }\n"; + let tree = parse_java(src); + let summary = FuncSummary { + name: "execute".into(), + ..Default::default() + }; + let binding = MigrationLiquibaseAdapter + .detect(&summary, tree.root_node(), src) + .expect("liquibase migration binds"); + assert_eq!(binding.adapter, "migration-liquibase"); + assert!(matches!(binding.kind, EntryKind::Migration { .. })); + } + + #[test] + fn fires_on_custom_sql_change_generate_statements() { + let src: &[u8] = b"import liquibase.change.custom.CustomSqlChange;\n\ + public class SeedRows implements CustomSqlChange {\n\ + public SqlStatement[] generateStatements(Database db) { return null; }\n\ + }\n"; + let tree = parse_java(src); + let summary = FuncSummary { + name: "generateStatements".into(), + ..Default::default() + }; + assert!( + MigrationLiquibaseAdapter + .detect(&summary, tree.root_node(), src) + .is_some(), + "CustomSqlChange.generateStatements must bind", + ); + } + + #[test] + fn skips_helper_named_execute_without_liquibase_import() { + let src: &[u8] = b"public class Helper {\n\ + public void execute(Object ctx) { }\n\ + }\n"; + let tree = parse_java(src); + let summary = FuncSummary { + name: "execute".into(), + ..Default::default() + }; + assert!( + MigrationLiquibaseAdapter + .detect(&summary, tree.root_node(), src) + .is_none(), + "helper named `execute` without Liquibase import must not bind", + ); + } + + #[test] + fn skips_unrelated_method_in_liquibase_file() { + let src: &[u8] = b"import liquibase.change.custom.CustomTaskChange;\n\ + public class AddIndex implements CustomTaskChange {\n\ + public void helper() { }\n\ + public void execute(Object ctx) { }\n\ + }\n"; + let tree = parse_java(src); + let summary = FuncSummary { + name: "helper".into(), + ..Default::default() + }; + assert!( + MigrationLiquibaseAdapter + .detect(&summary, tree.root_node(), src) + .is_none(), + "helper method that does not run DDL must not bind even inside a Liquibase file", + ); + } + + #[test] + fn extracts_changeset_name_from_database_change_annotation() { + let src: &[u8] = b"import liquibase.change.custom.CustomTaskChange;\n\ + @DatabaseChange(name = \"add-users-index\", description = \"x\")\n\ + public class AddIndex implements CustomTaskChange {\n\ + public void execute(Object ctx) { }\n\ + }\n"; + let tree = parse_java(src); + let summary = FuncSummary { + name: "execute".into(), + ..Default::default() + }; + let binding = MigrationLiquibaseAdapter + .detect(&summary, tree.root_node(), src) + .expect("binds"); + if let EntryKind::Migration { version } = binding.kind { + assert_eq!(version.as_deref(), Some("add-users-index")); + } else { + panic!("expected Migration entry kind"); + } + } +} diff --git a/src/dynamic/framework/adapters/migration_refinery.rs b/src/dynamic/framework/adapters/migration_refinery.rs new file mode 100644 index 00000000..ad5e994c --- /dev/null +++ b/src/dynamic/framework/adapters/migration_refinery.rs @@ -0,0 +1,168 @@ +//! refinery migration adapter (Rust). +//! +//! Fires when the surrounding source imports the `refinery` crate or +//! invokes the `embed_migrations!` macro, and the function under +//! analysis is the canonical migration runner (drives +//! `runner().run(&mut conn)` / `runner().run_async(&mut conn).await` +//! against the macro-generated module) or itself names one of those +//! entry verbs. +//! +//! Also recognises the parallel `sqlx::migrate!()` runner so a single +//! adapter covers both the `refinery` + `sqlx-cli` shapes mentioned in +//! the Phase 21 deferred audit — both projects ship `.sql` migration +//! files driven by a Rust-side runner, and the source-import discriminator +//! cleanly distinguishes them from arbitrary code. + +use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding}; +use crate::evidence::EntryKind; +use crate::summary::FuncSummary; +use crate::symbol::Lang; + +pub struct MigrationRefineryAdapter; + +const ADAPTER_NAME: &str = "migration-refinery"; + +fn callee_is_refinery(name: &str) -> bool { + let last = name.rsplit_once("::").map(|(_, s)| s).unwrap_or(name); + let last = last.rsplit_once('.').map(|(_, s)| s).unwrap_or(last); + matches!( + last, + "run" | "run_async" | "runner" | "embed_migrations" | "migrate" + ) +} + +fn source_imports_refinery(file_bytes: &[u8]) -> bool { + const NEEDLES: &[&[u8]] = &[ + b"use refinery", + b"refinery::embed_migrations", + b"embed_migrations!", + b"refinery::Runner", + b"refinery::Migration", + b"sqlx::migrate!", + b"use sqlx_cli", + ]; + NEEDLES + .iter() + .any(|n| file_bytes.windows(n.len()).any(|w| w == *n)) +} + +fn name_is_migration_entry(name: &str) -> bool { + matches!(name, "run" | "run_async" | "runner" | "migrate") +} + +impl FrameworkAdapter for MigrationRefineryAdapter { + fn name(&self) -> &'static str { + ADAPTER_NAME + } + + fn lang(&self) -> Lang { + Lang::Rust + } + + fn detect( + &self, + summary: &FuncSummary, + _ast: tree_sitter::Node<'_>, + file_bytes: &[u8], + ) -> Option { + let has_shape = source_imports_refinery(file_bytes); + let name_matches = name_is_migration_entry(&summary.name); + let body_runs_runner = super::any_callee_matches(summary, callee_is_refinery); + let binds = has_shape && (name_matches || body_runs_runner); + if !binds { + return None; + } + Some(FrameworkBinding { + adapter: ADAPTER_NAME.to_owned(), + kind: EntryKind::Migration { version: None }, + route: None, + request_params: Vec::new(), + response_writer: None, + middleware: Vec::new(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::summary::CalleeSite; + + fn parse_rust(src: &[u8]) -> tree_sitter::Tree { + let mut parser = tree_sitter::Parser::new(); + let lang = tree_sitter::Language::from(tree_sitter_rust::LANGUAGE); + parser.set_language(&lang).unwrap(); + parser.parse(src, None).unwrap() + } + + #[test] + fn fires_on_refinery_runner() { + let src: &[u8] = b"use refinery::embed_migrations;\n\ + embed_migrations!(\"./migrations\");\n\ + pub fn run(conn: &mut postgres::Client) {\n\ + migrations::runner().run(conn).unwrap();\n\ + }\n"; + let tree = parse_rust(src); + let summary = FuncSummary { + name: "run".into(), + callees: vec![CalleeSite::bare("migrations::runner")], + ..Default::default() + }; + let binding = MigrationRefineryAdapter + .detect(&summary, tree.root_node(), src) + .expect("refinery runner binds"); + assert_eq!(binding.adapter, "migration-refinery"); + assert!(matches!(binding.kind, EntryKind::Migration { .. })); + } + + #[test] + fn fires_on_sqlx_migrate_macro() { + let src: &[u8] = b"async fn migrate(pool: &PgPool) -> sqlx::Result<()> {\n\ + sqlx::migrate!(\"./migrations\").run(pool).await\n\ + }\n"; + let tree = parse_rust(src); + let summary = FuncSummary { + name: "migrate".into(), + ..Default::default() + }; + assert!( + MigrationRefineryAdapter + .detect(&summary, tree.root_node(), src) + .is_some(), + "sqlx::migrate! macro must bind", + ); + } + + #[test] + fn skips_helper_named_run_without_refinery_import() { + let src: &[u8] = b"pub fn run() {}\n"; + let tree = parse_rust(src); + let summary = FuncSummary { + name: "run".into(), + ..Default::default() + }; + assert!( + MigrationRefineryAdapter + .detect(&summary, tree.root_node(), src) + .is_none(), + "helper named `run` without refinery import must not bind", + ); + } + + #[test] + fn skips_unrelated_method_in_refinery_file() { + let src: &[u8] = b"use refinery::embed_migrations;\n\ + pub fn helper() {}\n"; + let tree = parse_rust(src); + let summary = FuncSummary { + name: "helper".into(), + ..Default::default() + }; + assert!( + MigrationRefineryAdapter + .detect(&summary, tree.root_node(), src) + .is_none(), + "helper without runner callee must not bind in a refinery file", + ); + } +} diff --git a/src/dynamic/framework/adapters/mod.rs b/src/dynamic/framework/adapters/mod.rs index 49d010fe..bca42eda 100644 --- a/src/dynamic/framework/adapters/mod.rs +++ b/src/dynamic/framework/adapters/mod.rs @@ -54,9 +54,13 @@ pub mod middleware_spring; pub mod migration_django; pub mod migration_flask; pub mod migration_flyway; +pub mod migration_go_migrate; +pub mod migration_knex; pub mod migration_laravel; +pub mod migration_liquibase; pub mod migration_prisma; pub mod migration_rails; +pub mod migration_refinery; pub mod migration_sequelize; pub mod nats_go; pub mod php_codeigniter; @@ -158,9 +162,13 @@ pub use middleware_spring::MiddlewareSpringAdapter; pub use migration_django::MigrationDjangoAdapter; pub use migration_flask::MigrationFlaskAdapter; pub use migration_flyway::MigrationFlywayAdapter; +pub use migration_go_migrate::MigrationGoMigrateAdapter; +pub use migration_knex::MigrationKnexAdapter; pub use migration_laravel::MigrationLaravelAdapter; +pub use migration_liquibase::MigrationLiquibaseAdapter; pub use migration_prisma::MigrationPrismaAdapter; pub use migration_rails::MigrationRailsAdapter; +pub use migration_refinery::MigrationRefineryAdapter; pub use migration_sequelize::MigrationSequelizeAdapter; pub use nats_go::NatsGoAdapter; pub use php_codeigniter::PhpCodeIgniterAdapter; diff --git a/src/dynamic/framework/mod.rs b/src/dynamic/framework/mod.rs index 7caf36a1..6b964772 100644 --- a/src/dynamic/framework/mod.rs +++ b/src/dynamic/framework/mod.rs @@ -238,8 +238,8 @@ mod tests { let java_registered = registry::adapters_for(Lang::Java); assert_eq!( java_registered.len(), - 17, - "Java must have Phase 20 baseline (14) + M.3 Quartz/Spring-middleware (2) + Flyway (1)", + 18, + "Java must have Phase 20 baseline (14) + M.3 Quartz/Spring-middleware (2) + Flyway (1) + Liquibase (1)", ); for adapter in java_registered { assert_eq!(adapter.lang(), Lang::Java); @@ -274,8 +274,8 @@ mod tests { let js_registered = registry::adapters_for(Lang::JavaScript); assert_eq!( js_registered.len(), - 19, - "JavaScript must have Phase 20 baseline (12) + M.3 Phase-21 (7)", + 20, + "JavaScript must have Phase 20 baseline (12) + M.3 Phase-21 (7) + Knex (1)", ); for adapter in js_registered { assert_eq!(adapter.lang(), Lang::JavaScript); @@ -292,8 +292,8 @@ mod tests { let go_registered = registry::adapters_for(Lang::Go); assert_eq!( go_registered.len(), - 10, - "Go must have Phase 20 baseline (9) + M.3 gqlgen (1)", + 11, + "Go must have Phase 20 baseline (9) + M.3 gqlgen (1) + golang-migrate (1)", ); for adapter in go_registered { assert_eq!(adapter.lang(), Lang::Go); @@ -301,8 +301,8 @@ mod tests { let rust_registered = registry::adapters_for(Lang::Rust); assert_eq!( rust_registered.len(), - 7, - "Rust must have Phase 20 baseline (6) + M.3 juniper (1)", + 8, + "Rust must have Phase 20 baseline (6) + M.3 juniper (1) + refinery (1)", ); for adapter in rust_registered { assert_eq!(adapter.lang(), Lang::Rust); diff --git a/src/dynamic/framework/registry.rs b/src/dynamic/framework/registry.rs index e38a399a..f6238693 100644 --- a/src/dynamic/framework/registry.rs +++ b/src/dynamic/framework/registry.rs @@ -47,6 +47,7 @@ pub fn adapters_for(lang: Lang) -> &'static [&'static dyn FrameworkAdapter] { static RUST: &[&dyn FrameworkAdapter] = &[ &super::adapters::GraphqlJuniperAdapter, &super::adapters::HeaderRustAdapter, + &super::adapters::MigrationRefineryAdapter, &super::adapters::RedirectRustAdapter, &super::adapters::RustActixAdapter, &super::adapters::RustAxumAdapter, @@ -67,6 +68,7 @@ static JAVA: &[&dyn FrameworkAdapter] = &[ &super::adapters::LdapSpringAdapter, &super::adapters::MiddlewareSpringAdapter, &super::adapters::MigrationFlywayAdapter, + &super::adapters::MigrationLiquibaseAdapter, &super::adapters::RabbitJavaAdapter, &super::adapters::RedirectJavaAdapter, &super::adapters::ScheduledQuartzAdapter, @@ -81,6 +83,7 @@ static GO: &[&dyn FrameworkAdapter] = &[ &super::adapters::GoGinAdapter, &super::adapters::GraphqlGqlgenAdapter, &super::adapters::HeaderGoAdapter, + &super::adapters::MigrationGoMigrateAdapter, &super::adapters::NatsGoAdapter, &super::adapters::PubsubGoAdapter, &super::adapters::RedirectGoAdapter, @@ -154,6 +157,7 @@ static JAVASCRIPT: &[&dyn FrameworkAdapter] = &[ &super::adapters::JsKoaAdapter, &super::adapters::JsNestAdapter, &super::adapters::MiddlewareExpressAdapter, + &super::adapters::MigrationKnexAdapter, &super::adapters::MigrationPrismaAdapter, &super::adapters::MigrationSequelizeAdapter, &super::adapters::PpJsonDeepAssignJsAdapter,