use std::collections::HashMap;
use std::sync::LazyLock;
use serde::{Deserialize, Serialize};
use super::{Finding, FindingType};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SuggestedFix {
pub pattern: String,
pub framework: String,
pub recommendation: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reference_url: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum Framework {
JavaJpa,
JavaWebFlux,
JavaQuarkusReactive,
JavaQuarkus,
JavaHelidonMp,
JavaHelidonSe,
JavaGeneric,
CsharpEfCore,
CsharpGeneric,
RustDiesel,
RustSeaOrm,
RustGeneric,
}
impl Framework {
const fn as_str(self) -> &'static str {
match self {
Self::JavaJpa => "java_jpa",
Self::JavaWebFlux => "java_webflux",
Self::JavaQuarkusReactive => "java_quarkus_reactive",
Self::JavaQuarkus => "java_quarkus",
Self::JavaHelidonMp => "java_helidon_mp",
Self::JavaHelidonSe => "java_helidon_se",
Self::JavaGeneric => "java_generic",
Self::CsharpEfCore => "csharp_ef_core",
Self::CsharpGeneric => "csharp_generic",
Self::RustDiesel => "rust_diesel",
Self::RustSeaOrm => "rust_sea_orm",
Self::RustGeneric => "rust_generic",
}
}
}
const JAVA_RULES: &[(Framework, &[&str])] = &[
(Framework::JavaHelidonMp, &["io.helidon.microprofile"]),
(Framework::JavaHelidonSe, &["io.helidon"]),
(
Framework::JavaQuarkusReactive,
&[
"io.quarkus.hibernate.reactive",
"io.quarkus.panache.reactive",
"io.quarkus.reactive",
"org.hibernate.reactive",
"io.smallrye.mutiny",
],
),
(
Framework::JavaQuarkus,
&[
"io.quarkus.hibernate.orm",
"io.quarkus.panache.common",
"io.quarkus",
],
),
(
Framework::JavaWebFlux,
&["org.springframework.web.reactive", "reactor.core"],
),
(
Framework::JavaJpa,
&[
"jakarta.persistence",
"javax.persistence",
"org.hibernate",
"org.springframework.data.jpa",
],
),
];
const CSHARP_RULES: &[(Framework, &[&str])] = &[(
Framework::CsharpEfCore,
&[
"Microsoft.EntityFrameworkCore",
"Pomelo.EntityFrameworkCore",
],
)];
const RUST_RULES: &[(Framework, &[&str])] = &[
(Framework::RustDiesel, &["diesel::"]),
(Framework::RustSeaOrm, &["sea_orm::"]),
];
#[derive(Debug, Clone, Copy)]
enum Language {
Java,
Csharp,
Rust,
}
impl Language {
const fn rules(self) -> &'static [(Framework, &'static [&'static str])] {
match self {
Self::Java => JAVA_RULES,
Self::Csharp => CSHARP_RULES,
Self::Rust => RUST_RULES,
}
}
const fn generic(self) -> Framework {
match self {
Self::Java => Framework::JavaGeneric,
Self::Csharp => Framework::CsharpGeneric,
Self::Rust => Framework::RustGeneric,
}
}
}
fn language_from_filepath(fp: &str) -> Option<Language> {
let ext = std::path::Path::new(fp).extension()?;
if ext.eq_ignore_ascii_case("java") {
Some(Language::Java)
} else if ext.eq_ignore_ascii_case("cs") {
Some(Language::Csharp)
} else if ext.eq_ignore_ascii_case("rs") {
Some(Language::Rust)
} else {
None
}
}
static FIXES: LazyLock<HashMap<(FindingType, Framework), SuggestedFix>> = LazyLock::new(|| {
use FindingType::{NPlusOneHttp, NPlusOneSql, RedundantSql};
use Framework::{
CsharpEfCore, CsharpGeneric, JavaGeneric, JavaHelidonMp, JavaHelidonSe, JavaJpa,
JavaQuarkus, JavaQuarkusReactive, JavaWebFlux, RustDiesel, RustGeneric, RustSeaOrm,
};
let entries: &[((FindingType, Framework), &str, Option<&str>)] = &[
(
(NPlusOneSql, JavaJpa),
"Use JOIN FETCH on the relationship or annotate the repository \
method with @EntityGraph to load associations in a single query.",
Some(
"https://docs.jboss.org/hibernate/orm/current/userguide/html_single/\
Hibernate_User_Guide.html#fetching-strategies-dynamic-fetching",
),
),
(
(NPlusOneSql, JavaQuarkusReactive),
"Use Mutiny's Hibernate Reactive Session.fetch() with @NamedEntityGraph, \
or join the relation in a Panache reactive query, to load associations \
in a single round-trip.",
Some("https://quarkus.io/guides/hibernate-reactive"),
),
(
(NPlusOneHttp, JavaWebFlux),
"Replace the sequential .flatMap() chain with Flux.merge() or Flux.zip() \
for parallel execution, or call a batch endpoint that returns the \
aggregated result in one round-trip.",
Some("https://docs.spring.io/spring-framework/reference/web/webflux-functional.html"),
),
(
(NPlusOneHttp, JavaQuarkusReactive),
"Replace chained Uni.chain() / Multi.onItem().transformToUni() calls with \
Uni.combine().all().unis(...) for parallel execution, or call a batch \
endpoint.",
Some("https://smallrye.io/smallrye-mutiny/latest/guides/combining-items/"),
),
(
(NPlusOneHttp, JavaGeneric),
"Coalesce the calls into a batch endpoint, or cache the per-request \
results with Spring's @Cacheable using a request-scoped cache.",
Some("https://docs.spring.io/spring-framework/reference/integration/cache.html"),
),
(
(RedundantSql, JavaQuarkusReactive),
"Use Quarkus' @CacheResult on the reactive method, or memoize the Uni \
with Mutiny's .memoize().indefinitely() to deduplicate within a request.",
Some("https://quarkus.io/guides/cache"),
),
(
(NPlusOneSql, JavaQuarkus),
"In Quarkus with Hibernate ORM, use a JOIN FETCH in your JPQL or Panache \
query, annotate the repository method with @EntityGraph, or call \
entityManager.unwrap(Session.class).fetchProfile(...) for a named fetch \
plan.",
Some("https://quarkus.io/guides/hibernate-orm-panache#fetching-and-loading"),
),
(
(NPlusOneHttp, JavaQuarkus),
"Use CompletableFuture.allOf(...) on the Quarkus ManagedExecutor for \
parallel calls, or invoke a batch endpoint via the Quarkus REST Client. \
For repeated reads, add @CacheResult on the client method.",
Some("https://quarkus.io/guides/rest-client-reactive"),
),
(
(RedundantSql, JavaQuarkus),
"Add @CacheResult on the @ApplicationScoped service method (Quarkus \
cache extension), or scope a HashMap on a @RequestScoped bean to \
deduplicate the query within the request.",
Some("https://quarkus.io/guides/cache"),
),
(
(NPlusOneSql, JavaHelidonSe),
"Replace the per-id loop with a single named Helidon DbClient query \
that performs JOIN, or pass a list of ids via the :ids JDBC parameter \
binding. Helidon SE has no JPA layer: the fix happens at the \
DbClient query level.",
Some("https://helidon.io/docs/latest/se/dbclient"),
),
(
(NPlusOneHttp, JavaHelidonSe),
"Fan out concurrent requests with Helidon WebClient using \
Single.zip(...) or Multi.merge(...). Or call a batch endpoint that \
returns the aggregated result in one round-trip.",
Some("https://helidon.io/docs/latest/se/webclient"),
),
(
(NPlusOneSql, JavaHelidonMp),
"Helidon MP entities are JPA-managed under Hibernate. Use \
@EntityGraph on the repository method or JPQL JOIN FETCH on the \
relationship to load associations in a single query.",
Some("https://helidon.io/docs/latest/mp/persistence"),
),
(
(NPlusOneHttp, JavaHelidonMp),
"Use the MicroProfile Rest Client with CompletableFuture.allOf(...) \
on the @ManagedExecutorConfig executor for parallel calls. Or call \
a batch endpoint that returns the aggregated result in one \
round-trip.",
Some(
"https://download.eclipse.org/microprofile/microprofile-rest-client-3.0/microprofile-rest-client-spec-3.0.html",
),
),
(
(RedundantSql, JavaGeneric),
"Add a service-level cache (Caffeine, Spring Cache) or deduplicate the \
query within the request scope.",
Some("https://docs.spring.io/spring-framework/reference/integration/cache.html"),
),
(
(NPlusOneSql, CsharpEfCore),
"Use .Include() (and .ThenInclude() for nested relations) to eager-load. \
Add .AsSplitQuery() when Include causes Cartesian explosion. Consider \
.AsNoTracking() for read-only queries.",
Some("https://learn.microsoft.com/en-us/ef/core/querying/related-data/eager"),
),
(
(RedundantSql, CsharpEfCore),
"Use IMemoryCache from Microsoft.Extensions.Caching.Memory, or add EF \
Core's second-level cache via a community extension. Within a request, \
scope the DbContext so identical reads short-circuit through the change \
tracker.",
Some("https://learn.microsoft.com/en-us/aspnet/core/performance/caching/memory"),
),
(
(NPlusOneHttp, CsharpGeneric),
"Use Task.WhenAll for parallel independent calls, or call a batch \
endpoint. For repeated identical calls, configure response caching on \
HttpClient via DelegatingHandler.",
Some(
"https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.whenall",
),
),
(
(NPlusOneSql, RustDiesel),
"Load associations with Diesel's belonging_to + grouped_by pattern \
(two queries instead of N+1), or use .inner_join() / .left_join() to \
fetch parent + children in a single query.",
Some("https://docs.diesel.rs/master/diesel/associations/index.html"),
),
(
(NPlusOneSql, RustSeaOrm),
"Use Entity::find().find_with_related(...) or .find_also_related(...) to \
fetch related entities in a single query, or load with a JOIN via \
QuerySelect::join().",
Some("https://www.sea-ql.org/SeaORM/docs/relation/select-related/"),
),
(
(RedundantSql, RustDiesel),
"Cache the result with the moka crate, or scope-deduplicate via a \
request-local OnceCell stored in axum/actix-web extensions.",
Some("https://docs.rs/moka"),
),
(
(RedundantSql, RustSeaOrm),
"Cache the result with the moka crate, or memoize per-request via a \
OnceCell stored in your handler state.",
Some("https://docs.rs/moka"),
),
(
(NPlusOneHttp, RustGeneric),
"Use tokio::join! or futures::future::join_all for parallel independent \
calls. Switch to a batch endpoint when the calls fan out from the same \
upstream input.",
Some("https://docs.rs/tokio/latest/tokio/macro.join.html"),
),
];
let mut m = HashMap::with_capacity(entries.len());
for ((ft, fw), recommendation, url) in entries {
m.insert(
(ft.clone(), *fw),
SuggestedFix {
pattern: ft.as_str().to_string(),
framework: fw.as_str().to_string(),
recommendation: (*recommendation).to_string(),
reference_url: url.map(ToString::to_string),
},
);
}
m
});
pub(crate) fn enrich(findings: &mut [Finding]) {
for finding in findings.iter_mut() {
if let Some(fix) = lookup_fix(finding) {
finding.suggested_fix = Some(fix.clone());
}
}
}
fn lookup_fix(finding: &Finding) -> Option<&'static SuggestedFix> {
let framework = detect_framework(finding)?;
FIXES.get(&(finding.finding_type.clone(), framework))
}
fn detect_framework(finding: &Finding) -> Option<Framework> {
let loc = finding.code_location.as_ref()?;
let filepath = loc.filepath.as_ref()?;
let language = language_from_filepath(filepath)?;
let ns = loc.namespace.as_deref().unwrap_or("");
for (framework, hints) in language.rules() {
if hints.iter().any(|hint| namespace_matches(ns, hint)) {
return Some(*framework);
}
}
Some(language.generic())
}
fn namespace_matches(ns: &str, hint: &str) -> bool {
let bytes = ns.as_bytes();
let mut start = 0;
while let Some(found) = ns[start..].find(hint) {
let abs = start + found;
let end = abs + hint.len();
let leading_ok = abs == 0
|| bytes[abs - 1] == b'.'
|| (bytes[abs - 1] == b':' && abs >= 2 && bytes[abs - 2] == b':');
let trailing_ok = end == ns.len()
|| bytes[end - 1] == b':'
|| bytes[end] == b'.'
|| (bytes[end] == b':' && end + 1 < ns.len() && bytes[end + 1] == b':');
if leading_ok && trailing_ok {
return true;
}
start = end;
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use crate::detect::{FindingType, Severity};
use crate::event::CodeLocation;
use crate::test_helpers::make_finding;
fn finding_with_location(ft: FindingType, loc: Option<CodeLocation>) -> Finding {
let mut f = make_finding(ft, Severity::Warning);
f.code_location = loc;
f.suggested_fix = None;
f
}
fn loc(filepath: &str, namespace: Option<&str>) -> CodeLocation {
CodeLocation {
function: None,
filepath: Some(filepath.to_string()),
lineno: None,
namespace: namespace.map(ToString::to_string),
}
}
#[test]
fn detects_java_jpa_via_jakarta_persistence() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc(
"src/main/java/com/example/OrderRepository.java",
Some("jakarta.persistence.EntityManager"),
)),
);
assert_eq!(detect_framework(&f), Some(Framework::JavaJpa));
}
#[test]
fn detects_java_jpa_via_hibernate() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc(
"src/main/java/com/example/OrderRepository.java",
Some("org.hibernate.SessionImpl"),
)),
);
assert_eq!(detect_framework(&f), Some(Framework::JavaJpa));
}
#[test]
fn detects_java_jpa_via_spring_data_jpa() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc(
"OrderRepository.java",
Some("org.springframework.data.jpa.repository.JpaRepository"),
)),
);
assert_eq!(detect_framework(&f), Some(Framework::JavaJpa));
}
#[test]
fn detects_java_webflux_via_reactor() {
let f = finding_with_location(
FindingType::NPlusOneHttp,
Some(loc(
"src/main/java/com/example/UserClient.java",
Some("reactor.core.publisher.Flux"),
)),
);
assert_eq!(detect_framework(&f), Some(Framework::JavaWebFlux));
}
#[test]
fn detects_java_webflux_via_spring_reactive() {
let f = finding_with_location(
FindingType::NPlusOneHttp,
Some(loc(
"UserHandler.java",
Some("org.springframework.web.reactive.function.client.WebClient"),
)),
);
assert_eq!(detect_framework(&f), Some(Framework::JavaWebFlux));
}
#[test]
fn detects_java_quarkus_reactive_via_mutiny() {
let f = finding_with_location(
FindingType::NPlusOneHttp,
Some(loc(
"src/main/java/com/acme/UserService.java",
Some("io.smallrye.mutiny.Uni"),
)),
);
assert_eq!(detect_framework(&f), Some(Framework::JavaQuarkusReactive));
}
#[test]
fn detects_java_quarkus_reactive_via_hibernate_reactive() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc(
"OrderRepository.java",
Some("org.hibernate.reactive.session.impl.ReactiveSessionImpl"),
)),
);
assert_eq!(detect_framework(&f), Some(Framework::JavaQuarkusReactive));
}
#[test]
fn detects_java_quarkus_reactive_via_quarkus_namespace() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc(
"OrderRepository.java",
Some("io.quarkus.hibernate.reactive.panache.PanacheRepository"),
)),
);
assert_eq!(detect_framework(&f), Some(Framework::JavaQuarkusReactive));
}
#[test]
fn detects_java_quarkus_reactive_via_panache_reactive_subpackage() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc(
"OrderRepository.java",
Some("io.quarkus.panache.reactive.PanacheRepository"),
)),
);
assert_eq!(detect_framework(&f), Some(Framework::JavaQuarkusReactive));
}
#[test]
fn detects_java_quarkus_non_reactive_via_hibernate_orm() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc(
"OrderRepository.java",
Some("io.quarkus.hibernate.orm.runtime.session.SessionImpl"),
)),
);
assert_eq!(detect_framework(&f), Some(Framework::JavaQuarkus));
}
#[test]
fn detects_java_quarkus_non_reactive_via_panache_common() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc(
"OrderRepository.java",
Some("io.quarkus.panache.common.runtime.AbstractJpaOperations"),
)),
);
assert_eq!(detect_framework(&f), Some(Framework::JavaQuarkus));
}
#[test]
fn detects_java_quarkus_non_reactive_via_generic_quarkus_namespace() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc(
"Scheduler.java",
Some("io.quarkus.scheduler.runtime.SchedulerImpl"),
)),
);
assert_eq!(detect_framework(&f), Some(Framework::JavaQuarkus));
}
#[test]
fn quarkus_reactive_wins_over_non_reactive_on_overlap() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc(
"OrderRepository.java",
Some("io.quarkus.hibernate.reactive.runtime.ReactiveSessionImpl"),
)),
);
assert_eq!(detect_framework(&f), Some(Framework::JavaQuarkusReactive));
}
#[test]
fn detects_java_helidon_se_via_dbclient() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc(
"OrderService.java",
Some("io.helidon.dbclient.jdbc.JdbcExecute"),
)),
);
assert_eq!(detect_framework(&f), Some(Framework::JavaHelidonSe));
}
#[test]
fn detects_java_helidon_se_via_webclient() {
let f = finding_with_location(
FindingType::NPlusOneHttp,
Some(loc(
"UserClient.java",
Some("io.helidon.webclient.WebClient"),
)),
);
assert_eq!(detect_framework(&f), Some(Framework::JavaHelidonSe));
}
#[test]
fn detects_java_helidon_mp_via_microprofile_namespace() {
let f = finding_with_location(
FindingType::NPlusOneHttp,
Some(loc(
"UserResource.java",
Some("io.helidon.microprofile.server.ServerImpl"),
)),
);
assert_eq!(detect_framework(&f), Some(Framework::JavaHelidonMp));
}
#[test]
fn helidon_mp_wins_over_helidon_se_on_overlap() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc(
"UserRepository.java",
Some("io.helidon.microprofile.cdi.HelidonContainerImpl"),
)),
);
assert_eq!(detect_framework(&f), Some(Framework::JavaHelidonMp));
}
#[test]
fn falls_back_to_java_generic_without_framework_hint() {
let f = finding_with_location(
FindingType::NPlusOneHttp,
Some(loc(
"src/main/java/com/example/UserClient.java",
Some("com.example.UserClient"),
)),
);
assert_eq!(detect_framework(&f), Some(Framework::JavaGeneric));
}
#[test]
fn case_insensitive_java_extension() {
let f = finding_with_location(FindingType::NPlusOneSql, Some(loc("Repository.JAVA", None)));
assert_eq!(detect_framework(&f), Some(Framework::JavaGeneric));
}
#[test]
fn detects_csharp_ef_core() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc(
"src/Orders/Repositories/OrderRepository.cs",
Some("Microsoft.EntityFrameworkCore.DbSet"),
)),
);
assert_eq!(detect_framework(&f), Some(Framework::CsharpEfCore));
}
#[test]
fn detects_csharp_ef_core_via_pomelo_provider() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc(
"OrderRepository.cs",
Some("Pomelo.EntityFrameworkCore.MySql.Query.Internal"),
)),
);
assert_eq!(detect_framework(&f), Some(Framework::CsharpEfCore));
}
#[test]
fn falls_back_to_csharp_generic_without_ef_hint() {
let f = finding_with_location(
FindingType::NPlusOneHttp,
Some(loc(
"src/Orders/UserClient.cs",
Some("Acme.Orders.UserClient"),
)),
);
assert_eq!(detect_framework(&f), Some(Framework::CsharpGeneric));
}
#[test]
fn detects_rust_diesel() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc(
"crates/orders/src/repository.rs",
Some("diesel::query_dsl::methods::FilterDsl"),
)),
);
assert_eq!(detect_framework(&f), Some(Framework::RustDiesel));
}
#[test]
fn detects_rust_sea_orm() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc(
"crates/orders/src/repository.rs",
Some("sea_orm::query::Selector"),
)),
);
assert_eq!(detect_framework(&f), Some(Framework::RustSeaOrm));
}
#[test]
fn rust_diesel_hint_does_not_match_user_module_named_diesel() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc(
"crates/orders/src/mydiesel.rs",
Some("orders::mydiesel::query"),
)),
);
assert_eq!(detect_framework(&f), Some(Framework::RustGeneric));
}
#[test]
fn falls_back_to_rust_generic_without_orm_hint() {
let f = finding_with_location(
FindingType::NPlusOneHttp,
Some(loc(
"crates/orders/src/user_client.rs",
Some("orders::user_client"),
)),
);
assert_eq!(detect_framework(&f), Some(Framework::RustGeneric));
}
#[test]
fn java_hint_requires_trailing_segment_boundary() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc("src/main/java/Foo.java", Some("io.helidongrpc.Foo"))),
);
assert_eq!(detect_framework(&f), Some(Framework::JavaGeneric));
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc("src/main/java/Bar.java", Some("org.hibernatefoo.Bar"))),
);
assert_eq!(detect_framework(&f), Some(Framework::JavaGeneric));
}
#[test]
fn csharp_hint_requires_trailing_segment_boundary() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc(
"src/Repo.cs",
Some("Microsoft.EntityFrameworkCoreCache.Provider"),
)),
);
assert_eq!(detect_framework(&f), Some(Framework::CsharpGeneric));
}
#[test]
fn returns_none_for_unsupported_extension() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc("repo.py", Some("django.db.models"))),
);
assert_eq!(detect_framework(&f), None);
}
#[test]
fn returns_none_when_code_location_missing() {
let f = finding_with_location(FindingType::NPlusOneSql, None);
assert_eq!(detect_framework(&f), None);
}
#[test]
fn returns_none_when_filepath_absent_even_if_namespace_present() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(CodeLocation {
function: Some("findById".to_string()),
filepath: None,
lineno: Some(7),
namespace: Some("org.hibernate.SessionImpl".to_string()),
}),
);
assert_eq!(detect_framework(&f), None);
}
#[test]
fn lookup_table_returns_jpa_fix_for_n_plus_one_sql() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc("Repository.java", Some("org.hibernate.SessionImpl"))),
);
let fix = lookup_fix(&f).expect("should have a fix");
assert_eq!(fix.framework, "java_jpa");
assert_eq!(fix.pattern, "n_plus_one_sql");
assert!(fix.recommendation.contains("JOIN FETCH"));
assert!(fix.reference_url.is_some());
}
#[test]
fn lookup_table_returns_csharp_ef_core_fix() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc(
"OrderRepository.cs",
Some("Microsoft.EntityFrameworkCore.DbSet"),
)),
);
let fix = lookup_fix(&f).expect("should have a fix");
assert_eq!(fix.framework, "csharp_ef_core");
assert!(fix.recommendation.contains(".Include()"));
}
#[test]
fn lookup_table_returns_rust_diesel_fix() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc(
"src/repo.rs",
Some("diesel::query_dsl::methods::FilterDsl"),
)),
);
let fix = lookup_fix(&f).expect("should have a fix");
assert_eq!(fix.framework, "rust_diesel");
assert!(fix.recommendation.contains("belonging_to"));
}
#[test]
fn lookup_table_returns_rust_sea_orm_fix() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc("src/repo.rs", Some("sea_orm::query::Selector"))),
);
let fix = lookup_fix(&f).expect("should have a fix");
assert_eq!(fix.framework, "rust_sea_orm");
assert!(fix.recommendation.contains("find_with_related"));
}
#[test]
fn lookup_table_returns_quarkus_reactive_http_fix() {
let f = finding_with_location(
FindingType::NPlusOneHttp,
Some(loc("UserService.java", Some("io.smallrye.mutiny.Uni"))),
);
let fix = lookup_fix(&f).expect("should have a fix");
assert_eq!(fix.framework, "java_quarkus_reactive");
assert!(fix.recommendation.contains("Uni.combine()"));
}
#[test]
fn lookup_table_returns_webflux_http_fix() {
let f = finding_with_location(
FindingType::NPlusOneHttp,
Some(loc("UserHandler.java", Some("reactor.core.publisher.Flux"))),
);
let fix = lookup_fix(&f).expect("should have a fix");
assert_eq!(fix.framework, "java_webflux");
assert!(
fix.recommendation.contains("Flux.zip()")
|| fix.recommendation.contains("Flux.merge()")
);
}
#[test]
fn lookup_table_returns_quarkus_non_reactive_sql_fix() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc(
"OrderRepository.java",
Some("io.quarkus.hibernate.orm.runtime.session.SessionImpl"),
)),
);
let fix = lookup_fix(&f).expect("should have a fix");
assert_eq!(fix.framework, "java_quarkus");
assert!(
fix.recommendation.contains("JOIN FETCH")
|| fix.recommendation.contains("@EntityGraph"),
"Quarkus non-reactive SQL fix should mention JOIN FETCH or @EntityGraph"
);
}
#[test]
fn lookup_table_returns_quarkus_non_reactive_redundant_fix() {
let f = finding_with_location(
FindingType::RedundantSql,
Some(loc(
"UserService.java",
Some("io.quarkus.hibernate.orm.runtime.session.SessionImpl"),
)),
);
let fix = lookup_fix(&f).expect("should have a fix");
assert_eq!(fix.framework, "java_quarkus");
assert!(fix.recommendation.contains("@CacheResult"));
}
#[test]
fn lookup_table_returns_helidon_se_sql_fix() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc(
"OrderService.java",
Some("io.helidon.dbclient.jdbc.JdbcExecute"),
)),
);
let fix = lookup_fix(&f).expect("should have a fix");
assert_eq!(fix.framework, "java_helidon_se");
assert!(
fix.recommendation.contains("DbClient"),
"Helidon SE SQL fix should reference DbClient"
);
}
#[test]
fn lookup_table_returns_helidon_se_http_fix() {
let f = finding_with_location(
FindingType::NPlusOneHttp,
Some(loc(
"UserClient.java",
Some("io.helidon.webclient.WebClient"),
)),
);
let fix = lookup_fix(&f).expect("should have a fix");
assert_eq!(fix.framework, "java_helidon_se");
assert!(
fix.recommendation.contains("Single.zip") || fix.recommendation.contains("Multi.merge"),
"Helidon SE HTTP fix should reference Single.zip or Multi.merge"
);
}
#[test]
fn lookup_table_returns_helidon_mp_sql_fix() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc(
"UserRepository.java",
Some("io.helidon.microprofile.cdi.HelidonContainerImpl"),
)),
);
let fix = lookup_fix(&f).expect("should have a fix");
assert_eq!(fix.framework, "java_helidon_mp");
assert!(
fix.recommendation.contains("@EntityGraph")
|| fix.recommendation.contains("JOIN FETCH"),
"Helidon MP SQL fix should reference @EntityGraph or JOIN FETCH"
);
}
#[test]
fn lookup_table_returns_helidon_mp_http_fix() {
let f = finding_with_location(
FindingType::NPlusOneHttp,
Some(loc(
"UserResource.java",
Some("io.helidon.microprofile.server.ServerImpl"),
)),
);
let fix = lookup_fix(&f).expect("should have a fix");
assert_eq!(fix.framework, "java_helidon_mp");
assert!(
fix.recommendation.contains("MicroProfile Rest Client")
&& fix.recommendation.contains("CompletableFuture"),
"Helidon MP HTTP fix should reference MicroProfile Rest Client + CompletableFuture"
);
}
#[test]
fn lookup_table_misses_for_unmapped_combination() {
let f = finding_with_location(
FindingType::SlowSql,
Some(loc("Repository.java", Some("org.hibernate.SessionImpl"))),
);
assert!(lookup_fix(&f).is_none());
}
#[test]
fn lookup_table_misses_for_unmapped_rust_generic_n_plus_one_sql() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc("src/repo.rs", Some("orders::repo"))),
);
assert!(lookup_fix(&f).is_none());
}
#[test]
fn enrich_populates_suggested_fix_when_match() {
let mut findings = vec![finding_with_location(
FindingType::NPlusOneSql,
Some(loc("Repository.java", Some("org.hibernate.SessionImpl"))),
)];
enrich(&mut findings);
let fix = findings[0]
.suggested_fix
.as_ref()
.expect("expected suggested_fix to be set");
assert_eq!(fix.framework, "java_jpa");
}
#[test]
fn enrich_leaves_suggested_fix_none_when_no_match() {
let mut findings = vec![finding_with_location(
FindingType::NPlusOneSql,
Some(loc("src/repo.rs", None)),
)];
enrich(&mut findings);
assert!(findings[0].suggested_fix.is_none());
}
#[test]
fn enrich_leaves_suggested_fix_none_for_unsupported_language() {
let mut findings = vec![finding_with_location(
FindingType::NPlusOneSql,
Some(loc("repo.py", Some("django.db.models"))),
)];
enrich(&mut findings);
assert!(findings[0].suggested_fix.is_none());
}
#[test]
fn suggested_fix_serializes_with_skip_when_url_absent() {
let fix = SuggestedFix {
pattern: "n_plus_one_sql".to_string(),
framework: "java_jpa".to_string(),
recommendation: "Use JOIN FETCH".to_string(),
reference_url: None,
};
let json = serde_json::to_string(&fix).unwrap();
assert!(!json.contains("reference_url"));
}
#[test]
fn fix_table_reference_urls_are_https_and_on_allowed_domains() {
const ALLOWED_DOMAIN_SUFFIXES: &[&str] = &[
"docs.jboss.org",
"quarkus.io",
"smallrye.io",
"helidon.io",
"docs.spring.io",
"download.eclipse.org",
"learn.microsoft.com",
"docs.diesel.rs",
"sea-ql.org",
"docs.rs",
];
for ((ft, fw), fix) in FIXES.iter() {
let Some(url) = fix.reference_url.as_deref() else {
continue;
};
assert!(
url.starts_with("https://"),
"({ft:?}, {fw:?}) reference_url must start with https://, got {url:?}"
);
let after_scheme = &url["https://".len()..];
let host = after_scheme
.split(['/', '?', '#'])
.next()
.expect("split has at least one element");
assert!(
ALLOWED_DOMAIN_SUFFIXES
.iter()
.any(|dom| host == *dom || host.ends_with(&format!(".{dom}"))),
"({ft:?}, {fw:?}) reference_url host {host:?} not in the allowlist; \
add it to ALLOWED_DOMAIN_SUFFIXES if intentional, otherwise fix the URL"
);
}
}
}