mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
refactor(dynamic): enhance project file indexing for Go and Rust with recursive directory inclusion, refine migration adapters for Go and Rust, and expand test coverage
This commit is contained in:
parent
680fc6bd28
commit
9c323d0ed5
7 changed files with 395 additions and 51 deletions
|
|
@ -13,7 +13,7 @@
|
|||
//! mirror the Phase 21 binding-stealing audit applied to
|
||||
//! `migration_rails` and `migration_flyway`.
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding, FrameworkDetectionContext};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
|
|
@ -44,10 +44,25 @@ fn name_is_migration_entry(name: &str) -> bool {
|
|||
}
|
||||
|
||||
/// 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(<n>)` is called. Scan for the
|
||||
/// argument to a `Migrate(` call as a best-effort version hint.
|
||||
fn extract_version(file_bytes: &[u8]) -> Option<String> {
|
||||
/// / `000001_init.down.sql`). When the runner calls `Migrate(<n>)`,
|
||||
/// prefer the matching migration filename from the project index;
|
||||
/// otherwise fall back to a single discovered SQL migration or the
|
||||
/// numeric version itself.
|
||||
fn extract_version(
|
||||
file_bytes: &[u8],
|
||||
context: Option<FrameworkDetectionContext<'_>>,
|
||||
) -> Option<String> {
|
||||
let migrate_target = extract_migrate_target(file_bytes);
|
||||
if let Some(context) = context
|
||||
&& let Some(filename) =
|
||||
migration_filename_from_project_files(context, migrate_target.as_deref())
|
||||
{
|
||||
return Some(filename);
|
||||
}
|
||||
migrate_target
|
||||
}
|
||||
|
||||
fn extract_migrate_target(file_bytes: &[u8]) -> Option<String> {
|
||||
let text = std::str::from_utf8(file_bytes).unwrap_or("");
|
||||
let needle = ".Migrate(";
|
||||
let idx = text.find(needle)?;
|
||||
|
|
@ -60,6 +75,66 @@ fn extract_version(file_bytes: &[u8]) -> Option<String> {
|
|||
Some(raw.to_owned())
|
||||
}
|
||||
|
||||
fn migration_filename_from_project_files(
|
||||
context: FrameworkDetectionContext<'_>,
|
||||
target_version: Option<&str>,
|
||||
) -> Option<String> {
|
||||
let mut candidates: Vec<&str> = context
|
||||
.project_files
|
||||
.iter()
|
||||
.map(|(path, _)| path)
|
||||
.filter(|path| is_go_migrate_sql_file(path))
|
||||
.collect();
|
||||
candidates.sort_unstable();
|
||||
|
||||
if let Some(target_version) = target_version
|
||||
&& let Some(path) = candidates
|
||||
.iter()
|
||||
.find(|path| {
|
||||
path.ends_with(".up.sql")
|
||||
&& migration_file_version(path)
|
||||
.map(|version| migration_versions_equal(version, target_version))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.or_else(|| {
|
||||
candidates.iter().find(|path| {
|
||||
migration_file_version(path)
|
||||
.map(|version| migration_versions_equal(version, target_version))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
})
|
||||
{
|
||||
return Some((*path).to_owned());
|
||||
}
|
||||
|
||||
if candidates.len() == 1 {
|
||||
return candidates.first().map(|path| (*path).to_owned());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn is_go_migrate_sql_file(path: &str) -> bool {
|
||||
path.ends_with(".up.sql") || path.ends_with(".down.sql")
|
||||
}
|
||||
|
||||
fn migration_file_version(path: &str) -> Option<&str> {
|
||||
let filename = path.rsplit('/').next().unwrap_or(path);
|
||||
let (version, _) = filename.split_once('_')?;
|
||||
if version.is_empty() || !version.chars().all(|c| c.is_ascii_digit()) {
|
||||
return None;
|
||||
}
|
||||
Some(version)
|
||||
}
|
||||
|
||||
fn migration_versions_equal(left: &str, right: &str) -> bool {
|
||||
let left = left.trim_start_matches('0');
|
||||
let right = right.trim_start_matches('0');
|
||||
let left = if left.is_empty() { "0" } else { left };
|
||||
let right = if right.is_empty() { "0" } else { right };
|
||||
left == right
|
||||
}
|
||||
|
||||
impl FrameworkAdapter for MigrationGoMigrateAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
ADAPTER_NAME
|
||||
|
|
@ -75,29 +150,48 @@ impl FrameworkAdapter for MigrationGoMigrateAdapter {
|
|||
_ast: tree_sitter::Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
) -> Option<FrameworkBinding> {
|
||||
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(),
|
||||
})
|
||||
detect_go_migrate(summary, file_bytes, None)
|
||||
}
|
||||
|
||||
fn detect_with_project_context(
|
||||
&self,
|
||||
summary: &FuncSummary,
|
||||
context: FrameworkDetectionContext<'_>,
|
||||
_ast: tree_sitter::Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
) -> Option<FrameworkBinding> {
|
||||
detect_go_migrate(summary, file_bytes, Some(context))
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_go_migrate(
|
||||
summary: &FuncSummary,
|
||||
file_bytes: &[u8],
|
||||
context: Option<FrameworkDetectionContext<'_>>,
|
||||
) -> Option<FrameworkBinding> {
|
||||
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, context),
|
||||
},
|
||||
route: None,
|
||||
request_params: Vec::new(),
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dynamic::framework::ProjectFileIndex;
|
||||
use crate::summary::CalleeSite;
|
||||
|
||||
fn parse_go(src: &[u8]) -> tree_sitter::Tree {
|
||||
|
|
@ -203,4 +297,78 @@ mod tests {
|
|||
panic!("expected Migration entry kind");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stamps_matching_sql_migration_filename_from_project_files() {
|
||||
let src: &[u8] = b"package entry\n\
|
||||
import \"github.com/golang-migrate/migrate/v4\"\n\
|
||||
func RunTo() {\n\
|
||||
m, _ := migrate.New(\"file://./migrations\", \"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 mut project_files = ProjectFileIndex::new();
|
||||
project_files.insert(
|
||||
"migrations/000041_old.up.sql",
|
||||
b"CREATE TABLE old_users(id int);",
|
||||
);
|
||||
project_files.insert(
|
||||
"migrations/000042_init.up.sql",
|
||||
b"CREATE TABLE users(id int);",
|
||||
);
|
||||
project_files.insert("migrations/000042_init.down.sql", b"DROP TABLE users;");
|
||||
let context = FrameworkDetectionContext {
|
||||
ssa_summary: None,
|
||||
project_files: &project_files,
|
||||
};
|
||||
let binding = MigrationGoMigrateAdapter
|
||||
.detect_with_project_context(&summary, context, tree.root_node(), src)
|
||||
.expect("binds");
|
||||
if let EntryKind::Migration { version } = binding.kind {
|
||||
assert_eq!(version.as_deref(), Some("migrations/000042_init.up.sql"));
|
||||
} else {
|
||||
panic!("expected Migration entry kind");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stamps_single_sql_migration_filename_for_steps_runner() {
|
||||
let src: &[u8] = b"package entry\n\
|
||||
import \"github.com/golang-migrate/migrate/v4\"\n\
|
||||
func RunOne() {\n\
|
||||
m, _ := migrate.New(\"file://./migrations\", \"postgres://x\")\n\
|
||||
m.Steps(1)\n\
|
||||
}\n";
|
||||
let tree = parse_go(src);
|
||||
let summary = FuncSummary {
|
||||
name: "RunOne".into(),
|
||||
callees: vec![CalleeSite::bare("m.Steps")],
|
||||
..Default::default()
|
||||
};
|
||||
let mut project_files = ProjectFileIndex::new();
|
||||
project_files.insert(
|
||||
"db/migrations/000001_create_users.up.sql",
|
||||
b"CREATE TABLE users(id int);",
|
||||
);
|
||||
let context = FrameworkDetectionContext {
|
||||
ssa_summary: None,
|
||||
project_files: &project_files,
|
||||
};
|
||||
let binding = MigrationGoMigrateAdapter
|
||||
.detect_with_project_context(&summary, context, tree.root_node(), src)
|
||||
.expect("binds");
|
||||
if let EntryKind::Migration { version } = binding.kind {
|
||||
assert_eq!(
|
||||
version.as_deref(),
|
||||
Some("db/migrations/000001_create_users.up.sql")
|
||||
);
|
||||
} else {
|
||||
panic!("expected Migration entry kind");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,12 +7,6 @@
|
|||
//! 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;
|
||||
|
|
@ -38,8 +32,6 @@ fn source_imports_refinery(file_bytes: &[u8]) -> bool {
|
|||
b"embed_migrations!",
|
||||
b"refinery::Runner",
|
||||
b"refinery::Migration",
|
||||
b"sqlx::migrate!",
|
||||
b"use sqlx_cli",
|
||||
];
|
||||
NEEDLES
|
||||
.iter()
|
||||
|
|
@ -115,24 +107,6 @@ mod tests {
|
|||
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";
|
||||
|
|
|
|||
134
src/dynamic/framework/adapters/migration_sqlx.rs
Normal file
134
src/dynamic/framework/adapters/migration_sqlx.rs
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
//! sqlx migration adapter (Rust).
|
||||
//!
|
||||
//! Fires when the surrounding source invokes `sqlx::migrate!()` or
|
||||
//! imports the `sqlx-cli` migration runner and the function under
|
||||
//! analysis is the canonical migration runner.
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
|
||||
pub struct MigrationSqlxAdapter;
|
||||
|
||||
const ADAPTER_NAME: &str = "migration-sqlx";
|
||||
|
||||
fn callee_is_sqlx_migration(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, "migrate" | "run" | "run_direct" | "run_migration")
|
||||
}
|
||||
|
||||
fn source_imports_sqlx_migration(file_bytes: &[u8]) -> bool {
|
||||
const NEEDLES: &[&[u8]] = &[
|
||||
b"sqlx::migrate!",
|
||||
b"use sqlx::migrate",
|
||||
b"use sqlx_cli",
|
||||
b"sqlx_cli::migrate",
|
||||
];
|
||||
NEEDLES
|
||||
.iter()
|
||||
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
|
||||
}
|
||||
|
||||
fn name_is_migration_entry(name: &str) -> bool {
|
||||
matches!(name, "migrate" | "run" | "run_migration")
|
||||
}
|
||||
|
||||
impl FrameworkAdapter for MigrationSqlxAdapter {
|
||||
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<FrameworkBinding> {
|
||||
let has_shape = source_imports_sqlx_migration(file_bytes);
|
||||
let name_matches = name_is_migration_entry(&summary.name);
|
||||
let body_runs_runner = super::any_callee_matches(summary, callee_is_sqlx_migration);
|
||||
if !(has_shape && (name_matches || body_runs_runner)) {
|
||||
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_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(),
|
||||
callees: vec![CalleeSite::bare("run")],
|
||||
..Default::default()
|
||||
};
|
||||
let binding = MigrationSqlxAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.expect("sqlx migration binds");
|
||||
assert_eq!(binding.adapter, "migration-sqlx");
|
||||
assert!(matches!(binding.kind, EntryKind::Migration { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_helper_named_migrate_without_sqlx_marker() {
|
||||
let src: &[u8] = b"pub fn migrate() {}\n";
|
||||
let tree = parse_rust(src);
|
||||
let summary = FuncSummary {
|
||||
name: "migrate".into(),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
MigrationSqlxAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_none(),
|
||||
"helper named migrate without sqlx marker must not bind",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_unrelated_helper_in_sqlx_file() {
|
||||
let src: &[u8] = b"async fn migrate(pool: &PgPool) -> sqlx::Result<()> {\n\
|
||||
sqlx::migrate!(\"./migrations\").run(pool).await\n\
|
||||
}\n\
|
||||
pub fn helper() {}\n";
|
||||
let tree = parse_rust(src);
|
||||
let summary = FuncSummary {
|
||||
name: "helper".into(),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
MigrationSqlxAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_none(),
|
||||
"unrelated helper in sqlx migration file must not bind",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -76,6 +76,7 @@ pub mod migration_prisma;
|
|||
pub mod migration_rails;
|
||||
pub mod migration_refinery;
|
||||
pub mod migration_sequelize;
|
||||
pub mod migration_sqlx;
|
||||
pub mod nats_go;
|
||||
pub mod php_codeigniter;
|
||||
pub mod php_laravel;
|
||||
|
|
@ -198,6 +199,7 @@ pub use migration_prisma::MigrationPrismaAdapter;
|
|||
pub use migration_rails::MigrationRailsAdapter;
|
||||
pub use migration_refinery::MigrationRefineryAdapter;
|
||||
pub use migration_sequelize::MigrationSequelizeAdapter;
|
||||
pub use migration_sqlx::MigrationSqlxAdapter;
|
||||
pub use nats_go::NatsGoAdapter;
|
||||
pub use php_codeigniter::PhpCodeIgniterAdapter;
|
||||
pub use php_laravel::PhpLaravelAdapter;
|
||||
|
|
|
|||
|
|
@ -57,6 +57,16 @@ impl ProjectFileIndex {
|
|||
index
|
||||
}
|
||||
|
||||
/// Add files under each project-relative directory when their
|
||||
/// extension matches `extensions`. Missing directories are skipped.
|
||||
pub fn include_dirs(mut self, root: &Path, rel_dirs: &[&str], extensions: &[&str]) -> Self {
|
||||
for rel_dir in rel_dirs {
|
||||
let dir = root.join(rel_dir);
|
||||
self.insert_matching_files(root, &dir, extensions, 0);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Insert or replace a project-relative file.
|
||||
pub fn insert(&mut self, rel_path: impl Into<String>, bytes: impl Into<Vec<u8>>) {
|
||||
self.files
|
||||
|
|
@ -70,10 +80,61 @@ impl ProjectFileIndex {
|
|||
.map(Vec::as_slice)
|
||||
}
|
||||
|
||||
/// Iterate project-relative file paths and raw bytes.
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&str, &[u8])> {
|
||||
self.files
|
||||
.iter()
|
||||
.map(|(path, bytes)| (path.as_str(), bytes.as_slice()))
|
||||
}
|
||||
|
||||
/// True when the index has no files.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.files.is_empty()
|
||||
}
|
||||
|
||||
fn insert_matching_files(
|
||||
&mut self,
|
||||
root: &Path,
|
||||
dir: &Path,
|
||||
extensions: &[&str],
|
||||
depth: usize,
|
||||
) {
|
||||
const MAX_DEPTH: usize = 4;
|
||||
if depth > MAX_DEPTH {
|
||||
return;
|
||||
}
|
||||
let Ok(entries) = std::fs::read_dir(dir) else {
|
||||
return;
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
let Ok(file_type) = entry.file_type() else {
|
||||
continue;
|
||||
};
|
||||
if file_type.is_dir() {
|
||||
self.insert_matching_files(root, &path, extensions, depth + 1);
|
||||
continue;
|
||||
}
|
||||
if !file_type.is_file() {
|
||||
continue;
|
||||
}
|
||||
let Some(ext) = path.extension().and_then(|e| e.to_str()) else {
|
||||
continue;
|
||||
};
|
||||
if !extensions.iter().any(|want| ext.eq_ignore_ascii_case(want)) {
|
||||
continue;
|
||||
}
|
||||
let Ok(rel) = path.strip_prefix(root) else {
|
||||
continue;
|
||||
};
|
||||
let Some(rel) = rel.to_str() else {
|
||||
continue;
|
||||
};
|
||||
if let Ok(bytes) = std::fs::read(&path) {
|
||||
self.insert(rel, bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_project_rel(rel_path: impl Into<String>) -> String {
|
||||
|
|
@ -509,8 +570,8 @@ mod tests {
|
|||
let rust_registered = registry::adapters_for(Lang::Rust);
|
||||
assert_eq!(
|
||||
rust_registered.len(),
|
||||
10,
|
||||
"Rust must have Phase 20 baseline (6) + M.3 juniper (1) + refinery (1) + Track L.9 (CryptoRust, DataExfilRust)",
|
||||
11,
|
||||
"Rust must have Phase 20 baseline (6) + M.3 juniper/refinery/sqlx (3) + Track L.9 (CryptoRust, DataExfilRust)",
|
||||
);
|
||||
for adapter in rust_registered {
|
||||
assert_eq!(adapter.lang(), Lang::Rust);
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ static RUST: &[&dyn FrameworkAdapter] = &[
|
|||
&super::adapters::GraphqlJuniperAdapter,
|
||||
&super::adapters::HeaderRustAdapter,
|
||||
&super::adapters::MigrationRefineryAdapter,
|
||||
&super::adapters::MigrationSqlxAdapter,
|
||||
&super::adapters::RedirectRustAdapter,
|
||||
&super::adapters::RustActixAdapter,
|
||||
&super::adapters::RustAxumAdapter,
|
||||
|
|
|
|||
|
|
@ -1286,7 +1286,11 @@ fn framework_project_files_for_entry(entry_file: &str, lang: Lang) -> ProjectFil
|
|||
],
|
||||
_ => &[],
|
||||
};
|
||||
ProjectFileIndex::from_root(&root, rel_paths)
|
||||
let index = ProjectFileIndex::from_root(&root, rel_paths);
|
||||
match lang {
|
||||
Lang::Go => index.include_dirs(&root, &["migrations", "db/migrations"], &["sql"]),
|
||||
_ => index,
|
||||
}
|
||||
}
|
||||
|
||||
fn infer_framework_project_root(entry_path: &Path, lang: Lang) -> Option<PathBuf> {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue