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",
}
}
}
#[derive(Clone, Copy)]
enum Hint {
Substring(&'static str),
LastSegmentEndsWith(&'static str),
}
const JAVA_RULES: &[(Framework, &[Hint])] = &[
(
Framework::JavaHelidonMp,
&[Hint::Substring("io.helidon.microprofile")],
),
(Framework::JavaHelidonSe, &[Hint::Substring("io.helidon")]),
(
Framework::JavaQuarkusReactive,
&[
Hint::Substring("io.quarkus.hibernate.reactive"),
Hint::Substring("io.quarkus.panache.reactive"),
Hint::Substring("io.quarkus.reactive"),
Hint::Substring("org.hibernate.reactive"),
Hint::Substring("io.smallrye.mutiny"),
],
),
(
Framework::JavaQuarkus,
&[
Hint::Substring("io.quarkus.hibernate.orm"),
Hint::Substring("io.quarkus.panache.common"),
Hint::Substring("io.quarkus"),
],
),
(
Framework::JavaWebFlux,
&[
Hint::Substring("org.springframework.web.reactive"),
Hint::Substring("reactor.core"),
],
),
(
Framework::JavaJpa,
&[
Hint::Substring("jakarta.persistence"),
Hint::Substring("javax.persistence"),
Hint::Substring("org.hibernate"),
Hint::Substring("org.springframework.data.jpa"),
Hint::LastSegmentEndsWith("Repository"),
Hint::LastSegmentEndsWith("Repo"),
Hint::LastSegmentEndsWith("Dao"),
],
),
];
const CSHARP_RULES: &[(Framework, &[Hint])] = &[(
Framework::CsharpEfCore,
&[
Hint::Substring("Microsoft.EntityFrameworkCore"),
Hint::Substring("Pomelo.EntityFrameworkCore"),
],
)];
const RUST_RULES: &[(Framework, &[Hint])] = &[
(Framework::RustDiesel, &[Hint::Substring("diesel::")]),
(Framework::RustSeaOrm, &[Hint::Substring("sea_orm::")]),
];
const SCOPE_RULES: &[(Framework, &[&str])] = &[
(Framework::JavaQuarkusReactive, &["hibernate-reactive"]),
(Framework::JavaQuarkus, &["quarkus"]),
(Framework::JavaWebFlux, &["spring-webflux", "r2dbc"]),
(Framework::JavaJpa, &["spring-data", "hibernate"]),
];
fn detect_framework_from_scopes(scopes: &[String]) -> Option<Framework> {
for (framework, needles) in SCOPE_RULES {
if scopes
.iter()
.any(|scope| needles.iter().any(|needle| scope_matches(scope, needle)))
{
return Some(*framework);
}
}
None
}
fn scope_matches(scope: &str, needle: &str) -> bool {
let Some(rest) = scope.strip_prefix("io.opentelemetry.") else {
return false;
};
let Some(after) = rest.strip_prefix(needle) else {
return false;
};
after.is_empty() || after.starts_with('-')
}
#[derive(Debug, Clone, Copy)]
enum Language {
Java,
Csharp,
Rust,
}
impl Language {
const fn rules(self) -> &'static [(Framework, &'static [Hint])] {
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",
),
),
(
(RedundantSql, JavaJpa),
"Add Spring's @Cacheable on the repository or service method, \
or share the EntityManager within the request via @Transactional \
so Hibernate's first-level cache deduplicates the read.",
Some("https://docs.spring.io/spring-framework/reference/integration/cache.html"),
),
(
(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> {
if let Some(framework) = detect_framework_from_scopes(&finding.instrumentation_scopes) {
return Some(framework);
}
let loc = finding.code_location.as_ref()?;
let ns = loc.namespace.as_deref().unwrap_or("");
if let Some(filepath) = loc.filepath.as_deref() {
let language = language_from_filepath(filepath)?;
return Some(match_namespace_against_language(ns, language).unwrap_or(language.generic()));
}
if ns.is_empty() {
return None;
}
[Language::Java, Language::Csharp, Language::Rust]
.into_iter()
.find_map(|language| match_namespace_against_language(ns, language))
}
fn match_namespace_against_language(ns: &str, language: Language) -> Option<Framework> {
for (framework, hints) in language.rules() {
if hints.iter().any(|hint| hint_matches(ns, *hint)) {
return Some(*framework);
}
}
None
}
fn hint_matches(ns: &str, hint: Hint) -> bool {
match hint {
Hint::Substring(needle) => namespace_contains_segment(ns, needle),
Hint::LastSegmentEndsWith(suffix) => last_segment(ns).ends_with(suffix),
}
}
fn last_segment(ns: &str) -> &str {
let last_dot = ns.rfind('.').map(|i| i + 1);
let last_colon = ns.rfind("::").map(|i| i + 2);
match (last_dot, last_colon) {
(Some(a), Some(b)) => &ns[a.max(b)..],
(Some(a), None) => &ns[a..],
(None, Some(b)) => &ns[b..],
(None, None) => ns,
}
}
fn namespace_contains_segment(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 finding_with_scopes(ft: FindingType, scopes: &[&str]) -> Finding {
let mut f = make_finding(ft, Severity::Warning);
f.code_location = None;
f.instrumentation_scopes = scopes.iter().map(|s| (*s).to_string()).collect();
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 scope_detects_jpa_from_spring_data() {
let f = finding_with_scopes(
FindingType::RedundantSql,
&[
"io.opentelemetry.jdbc",
"io.opentelemetry.hibernate-6.0",
"io.opentelemetry.spring-data-3.0",
],
);
assert_eq!(detect_framework(&f), Some(Framework::JavaJpa));
}
#[test]
fn scope_detects_jpa_from_hibernate_alone() {
let f = finding_with_scopes(
FindingType::NPlusOneSql,
&["io.opentelemetry.jdbc", "io.opentelemetry.hibernate-6.0"],
);
assert_eq!(detect_framework(&f), Some(Framework::JavaJpa));
}
#[test]
fn scope_detects_quarkus_reactive_via_hibernate_reactive() {
let f = finding_with_scopes(
FindingType::NPlusOneSql,
&[
"io.opentelemetry.hibernate-reactive-1.0",
"io.opentelemetry.quarkus-resteasy-reactive-3.0",
],
);
assert_eq!(detect_framework(&f), Some(Framework::JavaQuarkusReactive));
}
#[test]
fn scope_detects_quarkus_non_reactive_via_quarkus_short_name() {
let f = finding_with_scopes(
FindingType::NPlusOneSql,
&[
"io.opentelemetry.jdbc",
"io.opentelemetry.hibernate-6.0",
"io.opentelemetry.quarkus-resteasy-reactive-3.0",
],
);
assert_eq!(detect_framework(&f), Some(Framework::JavaQuarkus));
}
#[test]
fn scope_detects_webflux_via_r2dbc() {
let f = finding_with_scopes(
FindingType::NPlusOneSql,
&[
"io.opentelemetry.r2dbc-1.0",
"io.opentelemetry.spring-webflux-5.0",
],
);
assert_eq!(detect_framework(&f), Some(Framework::JavaWebFlux));
}
#[test]
fn scope_detects_webflux_via_spring_webflux() {
let f = finding_with_scopes(
FindingType::NPlusOneHttp,
&["io.opentelemetry.spring-webflux-5.0"],
);
assert_eq!(detect_framework(&f), Some(Framework::JavaWebFlux));
}
#[test]
fn scope_wins_over_namespace_user_code() {
let mut f = finding_with_scopes(
FindingType::RedundantSql,
&["io.opentelemetry.spring-data-3.0"],
);
f.code_location = Some(loc(
"OrderRepository.java",
Some("com.example.OrderRepository"),
));
assert_eq!(detect_framework(&f), Some(Framework::JavaJpa));
}
#[test]
fn scope_falls_back_to_namespace_when_no_scope_rule_matches() {
let mut f = finding_with_scopes(FindingType::NPlusOneSql, &["io.opentelemetry.jdbc"]);
f.code_location = Some(loc("Repository.java", Some("org.hibernate.SessionImpl")));
assert_eq!(detect_framework(&f), Some(Framework::JavaJpa));
}
#[test]
fn scope_falls_back_to_namespace_when_scope_chain_empty() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc(
"Repository.java",
Some("org.springframework.data.jpa.repository.JpaRepository"),
)),
);
assert_eq!(detect_framework(&f), Some(Framework::JavaJpa));
}
#[test]
fn scope_unknown_falls_back_to_namespace() {
let mut f = finding_with_scopes(FindingType::NPlusOneSql, &["com.example.custom-tracer"]);
f.code_location = Some(loc("Repository.java", Some("org.hibernate.SessionImpl")));
assert_eq!(detect_framework(&f), Some(Framework::JavaJpa));
}
#[test]
fn scope_third_party_tracer_named_after_framework_does_not_match() {
let f = finding_with_scopes(FindingType::NPlusOneSql, &["com.acme.quarkus-monitoring"]);
assert_eq!(detect_framework(&f), None);
}
#[test]
fn scope_partial_segment_does_not_match() {
let f = finding_with_scopes(
FindingType::NPlusOneSql,
&["io.opentelemetry.quarkusextension-1.0"],
);
assert_eq!(detect_framework(&f), None);
}
#[test]
fn scope_matches_canonical_versioned_form() {
let f = finding_with_scopes(
FindingType::NPlusOneSql,
&["io.opentelemetry.spring-data-3.0"],
);
assert_eq!(detect_framework(&f), Some(Framework::JavaJpa));
}
#[test]
fn scope_matches_canonical_bare_form() {
let f = finding_with_scopes(FindingType::NPlusOneSql, &["io.opentelemetry.spring-data"]);
assert_eq!(detect_framework(&f), Some(Framework::JavaJpa));
}
#[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 detects_framework_via_namespace_when_filepath_absent() {
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), Some(Framework::JavaJpa));
}
#[test]
fn returns_none_when_filepath_absent_and_namespace_unrecognized() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(CodeLocation {
function: Some("processPayment".to_string()),
filepath: None,
lineno: None,
namespace: Some("custom.PaymentEngine".to_string()),
}),
);
assert_eq!(detect_framework(&f), None);
}
#[test]
fn detects_jpa_from_user_repository_class_without_filepath() {
let f = finding_with_location(
FindingType::RedundantSql,
Some(CodeLocation {
function: Some("slowQuery".to_string()),
filepath: None,
lineno: None,
namespace: Some("com.perfsim.order.domain.OrderRepository".to_string()),
}),
);
assert_eq!(detect_framework(&f), Some(Framework::JavaJpa));
}
#[test]
fn detects_jpa_from_user_dao_class_without_filepath() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(CodeLocation {
function: Some("findAll".to_string()),
filepath: None,
lineno: None,
namespace: Some("com.example.legacy.OrderDao".to_string()),
}),
);
assert_eq!(detect_framework(&f), Some(Framework::JavaJpa));
}
#[test]
fn user_code_suffix_does_not_match_unrelated_class_without_filepath() {
let f = finding_with_location(
FindingType::RedundantSql,
Some(CodeLocation {
function: Some("send".to_string()),
filepath: None,
lineno: None,
namespace: Some("com.example.HttpClientWrapper".to_string()),
}),
);
assert_eq!(detect_framework(&f), None);
}
#[test]
fn enrich_populates_jpa_fix_for_user_repository_without_filepath() {
let mut findings = vec![finding_with_location(
FindingType::RedundantSql,
Some(CodeLocation {
function: Some("slowQuery".to_string()),
filepath: None,
lineno: None,
namespace: Some("com.perfsim.order.domain.OrderRepository".to_string()),
}),
)];
enrich(&mut findings);
let fix = findings[0]
.suggested_fix
.as_ref()
.expect("expected suggested_fix to be set");
assert_eq!(fix.framework, "java_jpa");
assert_eq!(fix.pattern, "redundant_sql");
assert!(
fix.recommendation.contains("@Cacheable")
|| fix.recommendation.contains("EntityManager"),
"redundant_sql JPA fix should reference @Cacheable or EntityManager"
);
}
#[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"
);
}
}
}