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,
PythonDjango,
PythonSqlAlchemy,
PythonGeneric,
RustDiesel,
RustSeaOrm,
RustGeneric,
GoGorm,
GoGeneric,
NodePrisma,
NodeGeneric,
}
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::PythonDjango => "python_django",
Self::PythonSqlAlchemy => "python_sqlalchemy",
Self::PythonGeneric => "python_generic",
Self::RustDiesel => "rust_diesel",
Self::RustSeaOrm => "rust_sea_orm",
Self::RustGeneric => "rust_generic",
Self::GoGorm => "go_gorm",
Self::GoGeneric => "go_generic",
Self::NodePrisma => "node_prisma",
Self::NodeGeneric => "node_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 PYTHON_RULES: &[(Framework, &[Hint])] = &[
(Framework::PythonDjango, &[Hint::Substring("django")]),
(
Framework::PythonSqlAlchemy,
&[Hint::Substring("sqlalchemy")],
),
];
const RUST_RULES: &[(Framework, &[Hint])] = &[
(Framework::RustDiesel, &[Hint::Substring("diesel::")]),
(Framework::RustSeaOrm, &[Hint::Substring("sea_orm::")]),
];
const GO_RULES: &[(Framework, &[Hint])] = &[(Framework::GoGorm, &[Hint::Substring("gorm")])];
const JS_RULES: &[(Framework, &[Hint])] = &[(Framework::NodePrisma, &[Hint::Substring("prisma")])];
const SERVICE_NAME_RULES: &[(Framework, &[&str])] = &[
(Framework::JavaHelidonMp, &["helidon-mp", "helidon.mp"]),
(Framework::JavaHelidonSe, &["helidon"]),
];
const SCOPE_RULES: &[(Framework, &[&str])] = &[
(Framework::JavaQuarkusReactive, &["hibernate-reactive"]),
(Framework::JavaQuarkus, &["quarkus"]),
(Framework::JavaWebFlux, &["spring-webflux", "r2dbc"]),
(Framework::JavaJpa, &["spring-data", "hibernate"]),
(Framework::JavaHelidonSe, &["helidon"]),
(Framework::PythonDjango, &["django"]),
(Framework::PythonSqlAlchemy, &["sqlalchemy"]),
];
const VENDOR_SCOPE_RULES: &[(Framework, &[&str])] = &[
(
Framework::CsharpEfCore,
&[
"OpenTelemetry.Instrumentation.EntityFrameworkCore",
"Microsoft.EntityFrameworkCore",
],
),
(
Framework::JavaQuarkusReactive,
&[
"io.quarkus.hibernate.reactive",
"io.quarkus.panache.reactive",
"io.quarkus.reactive",
],
),
(Framework::JavaQuarkus, &["io.quarkus"]),
];
fn vendor_prefix_matches(scope: &str, prefix: &str) -> bool {
scope.starts_with(prefix)
&& (scope.len() == prefix.len() || scope.as_bytes()[prefix.len()] == b'.')
}
fn detect_framework_from_service_name(service: &str) -> Option<Framework> {
let lower = service.to_ascii_lowercase();
for (framework, needles) in SERVICE_NAME_RULES {
if needles.iter().any(|n| lower.contains(n)) {
return Some(*framework);
}
}
None
}
fn detect_framework_from_scopes(scopes: &[String]) -> Option<Framework> {
for (framework, prefixes) in VENDOR_SCOPE_RULES {
if scopes
.iter()
.any(|scope| prefixes.iter().any(|p| vendor_prefix_matches(scope, p)))
{
return Some(*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.")
.or_else(|| scope.strip_prefix("opentelemetry.instrumentation."))
.or_else(|| scope.strip_prefix("@opentelemetry/instrumentation-"))
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,
Python,
Rust,
Go,
JavaScript,
}
impl Language {
const fn rules(self) -> &'static [(Framework, &'static [Hint])] {
match self {
Self::Java => JAVA_RULES,
Self::Csharp => CSHARP_RULES,
Self::Python => PYTHON_RULES,
Self::Rust => RUST_RULES,
Self::Go => GO_RULES,
Self::JavaScript => JS_RULES,
}
}
const fn generic(self) -> Framework {
match self {
Self::Java => Framework::JavaGeneric,
Self::Csharp => Framework::CsharpGeneric,
Self::Python => Framework::PythonGeneric,
Self::Rust => Framework::RustGeneric,
Self::Go => Framework::GoGeneric,
Self::JavaScript => Framework::NodeGeneric,
}
}
}
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("py") {
Some(Language::Python)
} else if ext.eq_ignore_ascii_case("rs") {
Some(Language::Rust)
} else if ext.eq_ignore_ascii_case("go") {
Some(Language::Go)
} else if ext.eq_ignore_ascii_case("js")
|| ext.eq_ignore_ascii_case("ts")
|| ext.eq_ignore_ascii_case("jsx")
|| ext.eq_ignore_ascii_case("tsx")
|| ext.eq_ignore_ascii_case("mjs")
|| ext.eq_ignore_ascii_case("mts")
|| ext.eq_ignore_ascii_case("cjs")
|| ext.eq_ignore_ascii_case("cts")
{
Some(Language::JavaScript)
} else {
None
}
}
static FIXES: LazyLock<HashMap<(FindingType, Framework), SuggestedFix>> = LazyLock::new(|| {
use FindingType::{
ChattyService, ExcessiveFanout, NPlusOneHttp, NPlusOneSql, PoolSaturation, RedundantHttp,
RedundantSql, SerializedCalls, SlowHttp, SlowSql,
};
use Framework::{
CsharpEfCore, CsharpGeneric, GoGeneric, GoGorm, JavaGeneric, JavaHelidonMp, JavaHelidonSe,
JavaJpa, JavaQuarkus, JavaQuarkusReactive, JavaWebFlux, NodeGeneric, NodePrisma,
PythonDjango, PythonGeneric, PythonSqlAlchemy, 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"),
),
(
(NPlusOneSql, PythonDjango),
"Use select_related() for foreign-key joins or prefetch_related() for \
reverse/M2M relations to eager-load associations in one or two queries \
instead of N+1.",
Some("https://docs.djangoproject.com/en/5.2/ref/models/querysets/#select-related"),
),
(
(NPlusOneSql, PythonSqlAlchemy),
"Add joinedload() or subqueryload() in the query options to eager-load \
the relationship, or rewrite with an explicit join().",
Some(
"https://docs.sqlalchemy.org/en/21/orm/queryguide/relationships.html#joined-eager-loading",
),
),
(
(RedundantSql, PythonDjango),
"Cache the queryset result with Django's cache framework (@cache_page \
for views, cache.get/set for manual memoization), or share the result \
via a request-local variable to deduplicate within the request.",
Some("https://docs.djangoproject.com/en/5.2/topics/cache/"),
),
(
(RedundantSql, PythonSqlAlchemy),
"Use dogpile.cache or a scoped-session memoization pattern to \
deduplicate identical queries within the request. Share the result \
via the session's identity map when the same row is loaded twice.",
Some(
"https://docs.sqlalchemy.org/en/21/orm/session_basics.html#is-the-session-a-cache",
),
),
(
(NPlusOneHttp, PythonGeneric),
"Use asyncio.gather() or concurrent.futures.ThreadPoolExecutor for \
parallel independent calls. Switch to a batch endpoint when the calls \
fan out from the same upstream input.",
Some("https://docs.python.org/3/library/asyncio-task.html#asyncio.gather"),
),
(
(RedundantHttp, JavaGeneric),
"Wrap the HTTP client in a request-scoped memoization layer (Caffeine, \
a HashMap on a request-scoped bean, or Spring's @Cacheable with a \
request-scoped key) so identical calls return the cached response \
within the request.",
Some("https://github.com/ben-manes/caffeine"),
),
(
(RedundantHttp, CsharpEfCore),
"Insert a DelegatingHandler on HttpClient that memoizes responses by \
request key (URL + headers) inside IMemoryCache for the request's \
lifetime. Be explicit about the cache key to avoid serving stale \
data across users.",
Some("https://learn.microsoft.com/en-us/aspnet/core/performance/caching/memory"),
),
(
(RedundantHttp, CsharpGeneric),
"Add a DelegatingHandler on HttpClient that memoizes by request URI \
inside IMemoryCache for the request scope, or share the response via \
AsyncLazy<T> when the duplication is concurrent.",
Some("https://learn.microsoft.com/en-us/aspnet/core/performance/caching/memory"),
),
(
(RedundantHttp, RustGeneric),
"Memoize per-request with the moka crate or a request-local OnceCell \
stored in your handler state. For concurrent duplicates within one \
handler, share the in-flight future via futures::future::Shared.",
Some("https://docs.rs/moka"),
),
(
(RedundantHttp, PythonGeneric),
"Memoize per-request with functools.lru_cache (sync) or an async \
memoization decorator. For Django, wrap the view with @cache_page or \
share the response via a request-local dict.",
Some("https://docs.python.org/3/library/functools.html#functools.lru_cache"),
),
(
(SlowSql, JavaJpa),
"Profile the rendered SQL with EXPLAIN ANALYZE. Common JPA fixes: add \
a composite index matching the WHERE + ORDER BY columns, switch to \
a DTO projection via @Query(\"select new ...\") to avoid hydrating \
the full entity graph, or paginate with Slice/keyset pagination when \
OFFSET grows linearly.",
Some("https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html"),
),
(
(SlowSql, JavaQuarkus),
"Enable hibernate.generate_statistics + the slow query log to confirm \
the offender. Add a composite index, project to a DTO via Panache's \
.project(Class) or a native query, or paginate with .range(...) on \
a keyset-friendly column.",
Some("https://quarkus.io/guides/hibernate-orm-panache"),
),
(
(SlowSql, JavaGeneric),
"Profile with EXPLAIN ANALYZE. Add an index on the columns used in \
WHERE + ORDER BY when selectivity justifies it. If the plan is \
dominated by a nested loop on a large outer, rewrite to push the \
selective predicate first.",
Some("https://www.postgresql.org/docs/current/using-explain.html"),
),
(
(SlowSql, CsharpEfCore),
"Inspect the generated SQL via .ToQueryString() and EXPLAIN ANALYZE \
it. Add an index via Fluent API (HasIndex). For Include explosion, \
switch to .AsSplitQuery(). For read-only paths add .AsNoTracking() \
to remove change-tracker overhead.",
Some("https://learn.microsoft.com/en-us/ef/core/performance/efficient-querying"),
),
(
(SlowSql, CsharpGeneric),
"Run EXPLAIN ANALYZE against the rendered query and add an index when \
the plan shows a sequential scan over a large table. Prefer parameterized \
queries so the plan cache stays hot.",
Some("https://www.postgresql.org/docs/current/using-explain.html"),
),
(
(SlowSql, RustDiesel),
"Print the rendered SQL with diesel::debug_query, EXPLAIN ANALYZE it, \
and add the missing index. For wide rows, use a .select((col_a, col_b)) \
projection so postgres can use an index-only scan.",
Some("https://docs.diesel.rs/master/diesel/fn.debug_query.html"),
),
(
(SlowSql, RustSeaOrm),
"Capture the query via the SQL logger feature, EXPLAIN ANALYZE the \
output, and add an index when the plan does a sequential scan. \
Project to a partial model with .select_only().column(...) when \
only a few columns are read.",
Some("https://www.sea-ql.org/SeaORM/docs/index/"),
),
(
(SlowSql, RustGeneric),
"Run EXPLAIN ANALYZE on the slow query. Add a composite index \
matching the WHERE + ORDER BY columns, or rewrite to push the \
selective predicate before any join.",
Some("https://www.postgresql.org/docs/current/using-explain.html"),
),
(
(SlowSql, PythonDjango),
"Run EXPLAIN ANALYZE on the rendered query (django-debug-toolbar or \
connection.queries). Add a db_index=True on the model field, or use \
.only()/.defer() to limit fetched columns.",
Some("https://docs.djangoproject.com/en/5.2/ref/models/options/#indexes"),
),
(
(SlowSql, PythonSqlAlchemy),
"Capture the rendered SQL via echo=True and EXPLAIN ANALYZE it. Add \
an index via Index() in the table metadata, or project with \
.with_only_columns() to reduce transferred data.",
Some("https://docs.sqlalchemy.org/en/21/core/metadata.html#sqlalchemy.schema.Index"),
),
(
(SlowSql, PythonGeneric),
"Run EXPLAIN ANALYZE on the slow query. Add an index on the WHERE + \
ORDER BY columns, or rewrite to push the selective predicate first.",
Some("https://www.postgresql.org/docs/current/using-explain.html"),
),
(
(SlowHttp, JavaGeneric),
"Profile the upstream's own slow_sql / slow_http findings first \
(the latency is usually upstream-side). On the client, wrap the \
call in a Resilience4j circuit breaker with a tight timeout, and \
cache the response when staleness tolerance allows.",
Some("https://resilience4j.readme.io/docs/circuitbreaker"),
),
(
(SlowHttp, CsharpGeneric),
"Set a tight HttpClient.Timeout and wrap calls in a Polly retry + \
circuit breaker. If the latency is structural (slow upstream), \
cache the response with IMemoryCache or move the call off the \
request hot path via background hosted services.",
Some(
"https://learn.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/",
),
),
(
(SlowHttp, RustGeneric),
"Set a per-request timeout on reqwest / hyper / surf, wrap with the \
tower circuit-breaker layer, and cache the response with moka when \
staleness is acceptable.",
Some("https://docs.rs/tower"),
),
(
(SlowHttp, PythonGeneric),
"Set a per-request timeout on httpx / aiohttp, add a tenacity retry \
with exponential backoff, and cache the response with Django cache \
or dogpile.cache when staleness is acceptable.",
Some("https://docs.python.org/3/library/asyncio-task.html#asyncio.wait_for"),
),
(
(ExcessiveFanout, JavaWebFlux),
"Bound the fan-out width with Flux.flatMap(concurrency = N) (default \
concurrency is unbounded). Add a Resilience4j bulkhead so a slow \
upstream cannot saturate the reactor scheduler.",
Some("https://projectreactor.io/docs/core/release/reference/#which-operator"),
),
(
(ExcessiveFanout, JavaQuarkusReactive),
"Bound the fan-out with Multi.onItem().transformToUniAndConcatenate() \
when ordering matters, or .transformToUniAndMerge(concurrency = N) \
when independent. Add a Resilience4j bulkhead on the downstream \
client.",
Some("https://smallrye.io/smallrye-mutiny/latest/guides/combining-items/"),
),
(
(ExcessiveFanout, JavaGeneric),
"Replace the fan-out with a single bulk endpoint when the downstream \
supports it. Otherwise apply the bulkhead pattern (Resilience4j or a \
dedicated thread pool) to bound the blast radius of a slow \
dependency.",
Some("https://resilience4j.readme.io/docs/bulkhead"),
),
(
(ExcessiveFanout, CsharpGeneric),
"Cap parallelism with Parallel.ForEachAsync(MaxDegreeOfParallelism = N) \
or a SemaphoreSlim, and prefer a batch endpoint when the upstream \
offers one. Layer Polly's RateLimiter strategy on the HttpClient \
to enforce a hard ceiling (Polly v8 subsumes the v7 Bulkhead).",
Some("https://www.pollydocs.org/strategies/rate-limiter.html"),
),
(
(ExcessiveFanout, RustGeneric),
"Use futures::stream::iter(...).buffer_unordered(N) to cap concurrent \
requests, or replace the fan-out with a batch endpoint. Layer a \
tower::ConcurrencyLimit on the downstream client to enforce a hard \
ceiling.",
Some(
"https://docs.rs/futures/latest/futures/stream/trait.StreamExt.html#method.buffer_unordered",
),
),
(
(ExcessiveFanout, PythonGeneric),
"Cap parallelism with asyncio.Semaphore or \
concurrent.futures.ThreadPoolExecutor(max_workers=N), and prefer a \
batch endpoint when the upstream offers one.",
Some("https://docs.python.org/3/library/asyncio-sync.html#asyncio.Semaphore"),
),
(
(ChattyService, JavaGeneric),
"Coalesce the chatty interactions into a single bulk endpoint, or \
move the orchestration upstream so the calls happen inside one \
service. When the chattiness is between services and a bulk \
endpoint is impossible, add a CQRS-style read model populated \
asynchronously.",
Some("https://martinfowler.com/articles/microservices.html#SmartEndpointsAndDumbPipes"),
),
(
(ChattyService, CsharpGeneric),
"Combine the calls into a single bulk endpoint, or introduce a \
gRPC streaming RPC that returns the aggregated payload in one \
round-trip. As a stopgap, fan-in with Task.WhenAll and an \
AsyncLazy<T> per-key cache.",
Some("https://learn.microsoft.com/en-us/aspnet/core/grpc/protobuf"),
),
(
(ChattyService, RustGeneric),
"Coalesce the calls behind a single bulk endpoint, or expose a \
tonic streaming RPC that returns the aggregated payload. As an \
intermediate, batch with futures::future::join_all and a moka \
per-key cache.",
Some("https://docs.rs/tonic"),
),
(
(ChattyService, PythonGeneric),
"Coalesce the chatty interactions into a single bulk endpoint, or \
fan-in with asyncio.gather and a per-key cache (Django cache or \
dogpile.cache). Reduce round-trips by moving orchestration upstream.",
Some("https://docs.python.org/3/library/asyncio-task.html#asyncio.gather"),
),
(
(PoolSaturation, JavaQuarkus),
"Raise quarkus.datasource.jdbc.max-size only after confirming the \
root cause: usually slow queries hold connections too long. Profile \
slow_sql findings on the same service first, then size the pool to \
handle the corrected workload.",
Some("https://quarkus.io/guides/datasource#jdbc-configuration-reference"),
),
(
(PoolSaturation, JavaGeneric),
"Inspect slow_sql findings on the same service: pool saturation is \
usually a symptom, not the disease. After speeding up the slow \
queries, raise the HikariCP maximumPoolSize only if the corrected \
workload still saturates.",
Some("https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing"),
),
(
(PoolSaturation, CsharpEfCore),
"Check Npgsql Max Pool Size in the connection string and the \
DbContext lifetime (a per-request DbContext should release \
connections to the pool on Dispose). Slow queries on the same \
service usually drive the saturation, fix those first.",
Some("https://www.npgsql.org/doc/connection-string-parameters.html"),
),
(
(PoolSaturation, CsharpGeneric),
"Profile the slow_sql findings on the same service; pool saturation \
follows from connections being held during slow work. Raise the \
pool size only after the slow path is corrected.",
Some(
"https://learn.microsoft.com/en-us/sql/connect/ado-net/sql-server-connection-pooling",
),
),
(
(PoolSaturation, RustGeneric),
"Inspect slow_sql findings first: sqlx and deadpool pools usually \
saturate because connections are held during slow work. Tune \
max_connections on PoolOptions only after the slow queries are \
addressed.",
Some("https://docs.rs/sqlx/latest/sqlx/pool/struct.PoolOptions.html"),
),
(
(PoolSaturation, PythonGeneric),
"Inspect slow_sql findings on the same service: pool saturation usually \
follows from connections held during slow work. Tune pool_size on \
SQLAlchemy's create_engine or Django's CONN_MAX_AGE only after the \
slow path is corrected.",
Some(
"https://docs.sqlalchemy.org/en/21/core/engines.html#sqlalchemy.create_engine.params.pool_size",
),
),
(
(SerializedCalls, JavaWebFlux),
"Replace the sequential Mono.flatMap chain with Mono.zip(m1, m2, ...) \
or Flux.merge(...) when the calls are independent. Keep the \
sequential chain only when one call's output feeds the next.",
Some("https://projectreactor.io/docs/core/release/reference/#which.combining"),
),
(
(SerializedCalls, JavaQuarkusReactive),
"Switch the sequential Uni.chain() to \
Uni.combine().all().unis(u1, u2, ...).asTuple() when the calls do \
not depend on each other. Drop back to chain only when the \
next call needs the previous result.",
Some("https://smallrye.io/smallrye-mutiny/latest/guides/combining-items/"),
),
(
(SerializedCalls, JavaGeneric),
"Parallelize independent calls with CompletableFuture.allOf(...) on \
the ManagedExecutor (Quarkus) or @Async (Spring). Keep the calls \
serial only when one's output feeds the next.",
Some(
"https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/CompletableFuture.html",
),
),
(
(SerializedCalls, CsharpGeneric),
"Replace the sequential await chain with Task.WhenAll(t1, t2, ...) \
when the calls do not depend on each other. Use ValueTask for \
completed-synchronously paths to avoid allocating Task wrappers.",
Some(
"https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.whenall",
),
),
(
(SerializedCalls, RustGeneric),
"Replace sequential .await chains with tokio::join!(f1, f2, ...) or \
futures::future::join_all(iter) when the calls are independent. \
Use try_join! when any failure should short-circuit the rest.",
Some("https://docs.rs/tokio/latest/tokio/macro.join.html"),
),
(
(SerializedCalls, PythonGeneric),
"Replace sequential awaits with asyncio.gather(t1, t2, ...) when the \
calls are independent. For sync code, use \
concurrent.futures.ThreadPoolExecutor to run independent calls in \
parallel. Keep the sequential form only when one call's output feeds \
the next.",
Some("https://docs.python.org/3/library/asyncio-task.html#asyncio.gather"),
),
(
(NPlusOneSql, GoGorm),
"Use Preload() or Joins() to eager-load the association in a \
single query instead of N+1 lazy loads.",
Some("https://gorm.io/docs/preload.html"),
),
(
(NPlusOneSql, GoGeneric),
"Rewrite the per-id loop as a single query with a JOIN or WHERE id \
IN ($1, $2, ...). With pgx, use pgx.NamedArgs for named parameters \
or pass an array via ANY($1::int[]) for bulk lookups.",
Some("https://pkg.go.dev/github.com/jackc/pgx/v5"),
),
(
(RedundantSql, GoGorm),
"Cache the result with go-cache, sync.Map, or a request-scoped map \
stored in the context via context.WithValue. GORM does not deduplicate \
reads automatically, the caching layer must be explicit.",
Some("https://pkg.go.dev/github.com/patrickmn/go-cache"),
),
(
(RedundantSql, GoGeneric),
"Cache the result with go-cache or a sync.Map, or pass a \
request-scoped map via context.WithValue to deduplicate within \
the request.",
Some("https://pkg.go.dev/github.com/patrickmn/go-cache"),
),
(
(NPlusOneHttp, GoGeneric),
"Use errgroup.Go for parallel independent calls, or call a batch \
endpoint that returns the aggregated result in one round-trip.",
Some("https://pkg.go.dev/golang.org/x/sync/errgroup"),
),
(
(RedundantHttp, GoGeneric),
"Memoize per-request with singleflight.Do (for concurrent identical \
calls) or a sync.Map keyed by request URL stored in the context.",
Some("https://pkg.go.dev/golang.org/x/sync/singleflight"),
),
(
(SlowSql, GoGorm),
"Enable GORM's Logger in Info mode, capture the rendered SQL, and \
EXPLAIN ANALYZE it. Add an index via AutoMigrate or a raw migration. \
Use .Select() to limit fetched columns.",
Some("https://gorm.io/docs/performance.html"),
),
(
(SlowSql, GoGeneric),
"Run EXPLAIN ANALYZE on the slow query. Add a composite index \
matching the WHERE + ORDER BY columns. With pgx, use \
.QueryRow().Scan() with only the needed columns.",
Some("https://www.postgresql.org/docs/current/using-explain.html"),
),
(
(SlowHttp, GoGeneric),
"Set a per-request timeout via http.Client.Timeout or \
context.WithTimeout. Add a circuit breaker (sony/gobreaker) and \
cache the response with go-cache when staleness is acceptable.",
Some("https://pkg.go.dev/github.com/sony/gobreaker"),
),
(
(ExcessiveFanout, GoGeneric),
"Bound goroutine count with a semaphore channel (make(chan struct{}, N)) \
or errgroup with SetLimit(N). Prefer a batch endpoint when the \
downstream supports it.",
Some("https://pkg.go.dev/golang.org/x/sync/errgroup#Group.SetLimit"),
),
(
(ChattyService, GoGeneric),
"Coalesce the chatty interactions into a single bulk endpoint, or \
fan-in with errgroup and a per-key go-cache. Reduce round-trips by \
moving orchestration upstream.",
Some("https://pkg.go.dev/golang.org/x/sync/errgroup"),
),
(
(PoolSaturation, GoGeneric),
"Inspect slow_sql findings first: pgxpool usually saturates because \
connections are held during slow work. Tune MaxConns on \
pgxpool.Config only after the slow queries are addressed.",
Some("https://pkg.go.dev/github.com/jackc/pgx/v5/pgxpool#Config"),
),
(
(SerializedCalls, GoGeneric),
"Replace sequential calls with errgroup.Go for parallel execution. \
Keep the sequential form only when one call's output feeds the next.",
Some("https://pkg.go.dev/golang.org/x/sync/errgroup"),
),
(
(NPlusOneSql, NodePrisma),
"Use include:{} for eager loading, or rewrite with a findMany() \
that uses a WHERE id IN filter instead of N separate findUnique() \
calls.",
Some("https://www.prisma.io/docs/orm/prisma-client/queries/relation-queries"),
),
(
(NPlusOneSql, NodeGeneric),
"Rewrite the per-id loop as a single query with a JOIN or WHERE id \
IN ($1, $2, ...). With the pg client, use a parameterized query \
with ANY($1::int[]).",
Some("https://node-postgres.com/features/queries"),
),
(
(RedundantSql, NodePrisma),
"Wrap queries in a request-scoped Map to deduplicate identical reads \
within the request, or use a Prisma client extension that memoizes \
by query key.",
Some("https://www.prisma.io/docs/orm/prisma-client/client-extensions"),
),
(
(RedundantSql, NodeGeneric),
"Cache the result with node-cache or a request-scoped Map stored \
in Express/Fastify request locals. For concurrent identical \
queries, use p-memoize.",
Some("https://www.npmjs.com/package/node-cache"),
),
(
(NPlusOneHttp, NodeGeneric),
"Use Promise.all for parallel independent calls, or call a batch \
endpoint that returns the aggregated result in one round-trip.",
Some(
"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all",
),
),
(
(RedundantHttp, NodeGeneric),
"Memoize per-request with p-memoize or a Map stored in request \
locals. For concurrent identical calls, share the in-flight \
Promise.",
Some("https://www.npmjs.com/package/p-memoize"),
),
(
(SlowSql, NodePrisma),
"Enable Prisma's query logging, capture the rendered SQL, and \
EXPLAIN ANALYZE it. Add an @@index in the schema.prisma model. \
Use .select() to limit fetched columns.",
Some("https://www.prisma.io/docs/orm/prisma-schema/data-model/indexes"),
),
(
(SlowSql, NodeGeneric),
"Run EXPLAIN ANALYZE on the slow query. Add a composite index \
matching the WHERE + ORDER BY columns.",
Some("https://www.postgresql.org/docs/current/using-explain.html"),
),
(
(SlowHttp, NodeGeneric),
"Set a per-request timeout via AbortController + setTimeout. Add a \
circuit breaker (opossum) and cache the response with node-cache \
when staleness is acceptable.",
Some("https://www.npmjs.com/package/opossum"),
),
(
(ExcessiveFanout, NodeGeneric),
"Use p-limit to cap concurrency (const limit = pLimit(N); \
await Promise.all(urls.map(u => limit(() => fetch(u))))). Prefer \
a batch endpoint when the downstream supports it.",
Some("https://www.npmjs.com/package/p-limit"),
),
(
(ChattyService, NodeGeneric),
"Coalesce the chatty interactions into a single bulk endpoint, or \
fan-in with Promise.all and a per-key Map cache. For GraphQL, use \
DataLoader to batch and deduplicate.",
Some("https://www.npmjs.com/package/dataloader"),
),
(
(PoolSaturation, NodeGeneric),
"Inspect slow_sql findings first: the pg Pool usually saturates \
because connections are held during slow work. Tune max on \
new Pool({ max: N }) only after the slow queries are addressed.",
Some("https://node-postgres.com/apis/pool"),
),
(
(SerializedCalls, NodeGeneric),
"Replace sequential awaits with Promise.all([p1, p2, ...]) when \
the calls are independent. Keep the sequential form only when one \
call's output feeds the next.",
Some(
"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all",
),
),
];
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),
},
);
}
debug_assert_eq!(
entries.len(),
m.len(),
"duplicate (FindingType, Framework) key in FIXES entries"
);
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);
}
if let Some(language) = language_from_scope_prefix(&finding.instrumentation_scopes) {
let ns = finding
.code_location
.as_ref()
.and_then(|loc| loc.namespace.as_deref())
.unwrap_or("");
return Some(match_namespace_against_language(ns, language).unwrap_or(language.generic()));
}
if let Some(loc) = finding.code_location.as_ref() {
let ns = loc.namespace.as_deref().unwrap_or("");
if let Some(language) = loc.filepath.as_deref().and_then(language_from_filepath) {
return Some(
match_namespace_against_language(ns, language).unwrap_or(language.generic()),
);
}
if let Some(fw) = (!ns.is_empty())
.then(|| {
[
Language::Java,
Language::Csharp,
Language::Python,
Language::Rust,
Language::Go,
Language::JavaScript,
]
.into_iter()
.find_map(|language| match_namespace_against_language(ns, language))
})
.flatten()
{
return Some(fw);
}
}
detect_framework_from_service_name(&finding.service)
}
fn language_from_scope_prefix(scopes: &[String]) -> Option<Language> {
for scope in scopes {
if scope.starts_with("github.com/") {
return Some(Language::Go);
}
if scope.starts_with("@opentelemetry/instrumentation-")
|| scope.starts_with("@prisma/")
|| scope.starts_with("@nestjs/")
{
return Some(Language::JavaScript);
}
if scope == "Microsoft.EntityFrameworkCore"
|| scope.starts_with("Microsoft.EntityFrameworkCore.")
|| scope.starts_with("OpenTelemetry.Instrumentation.")
{
return Some(Language::Csharp);
}
}
None
}
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 vendor_scope_dotnet_ef_core() {
let f = finding_with_scopes(
FindingType::NPlusOneSql,
&["OpenTelemetry.Instrumentation.EntityFrameworkCore"],
);
assert_eq!(detect_framework(&f), Some(Framework::CsharpEfCore));
}
#[test]
fn vendor_scope_dotnet_ef_core_wins_over_namespace() {
let mut f = finding_with_scopes(
FindingType::NPlusOneSql,
&["OpenTelemetry.Instrumentation.EntityFrameworkCore"],
);
f.code_location = Some(loc("OrderController.cs", Some("MyApp.Controllers")));
assert_eq!(detect_framework(&f), Some(Framework::CsharpEfCore));
}
#[test]
fn vendor_scope_quarkus_generic() {
let f = finding_with_scopes(FindingType::NPlusOneSql, &["io.quarkus.opentelemetry"]);
assert_eq!(detect_framework(&f), Some(Framework::JavaQuarkus));
}
#[test]
fn vendor_scope_quarkus_reactive_wins_over_generic() {
let f = finding_with_scopes(
FindingType::NPlusOneSql,
&["io.quarkus.hibernate.reactive", "io.quarkus.opentelemetry"],
);
assert_eq!(detect_framework(&f), Some(Framework::JavaQuarkusReactive));
}
#[test]
fn vendor_scope_quarkus_panache_reactive() {
let f = finding_with_scopes(FindingType::NPlusOneSql, &["io.quarkus.panache.reactive"]);
assert_eq!(detect_framework(&f), Some(Framework::JavaQuarkusReactive));
}
#[test]
fn vendor_scope_wins_over_standard_scope() {
let f = finding_with_scopes(
FindingType::NPlusOneSql,
&["io.quarkus.opentelemetry", "io.opentelemetry.hibernate-6.0"],
);
assert_eq!(detect_framework(&f), Some(Framework::JavaQuarkus));
}
#[test]
fn vendor_scope_unknown_dotnet_instrumentation_falls_to_csharp_generic() {
let f = finding_with_scopes(
FindingType::NPlusOneSql,
&["OpenTelemetry.Instrumentation.SqlClient"],
);
assert_eq!(detect_framework(&f), Some(Framework::CsharpGeneric));
}
#[test]
fn vendor_scope_quarkus_prefix_requires_dot_boundary() {
let f = finding_with_scopes(FindingType::NPlusOneSql, &["io.quarkusbridge.acme"]);
assert_eq!(detect_framework(&f), None);
}
#[test]
fn vendor_scope_reactive_prefix_does_not_match_across_underscore() {
let f = finding_with_scopes(
FindingType::NPlusOneSql,
&["io.quarkus.reactive_streams.vendor"],
);
assert_eq!(detect_framework(&f), Some(Framework::JavaQuarkus));
}
#[test]
fn service_name_fallback_detects_helidon_se() {
let mut f = make_finding(FindingType::NPlusOneSql, Severity::Warning);
f.service = "helidon-se-svc".to_string();
f.code_location = None;
f.instrumentation_scopes = vec![];
f.suggested_fix = None;
assert_eq!(detect_framework(&f), Some(Framework::JavaHelidonSe));
}
#[test]
fn service_name_fallback_does_not_match_generic_names() {
let mut f = make_finding(FindingType::NPlusOneSql, Severity::Warning);
f.service = "diesel-svc".to_string();
f.code_location = None;
f.instrumentation_scopes = vec![];
f.suggested_fix = None;
assert_eq!(detect_framework(&f), None);
}
#[test]
fn service_name_fallback_returns_none_for_unknown() {
let mut f = make_finding(FindingType::NPlusOneSql, Severity::Warning);
f.service = "myapp-svc".to_string();
f.code_location = None;
f.instrumentation_scopes = vec![];
f.suggested_fix = None;
assert_eq!(detect_framework(&f), None);
}
#[test]
fn service_name_fallback_not_reached_when_scope_matches() {
let mut f = finding_with_scopes(
FindingType::NPlusOneSql,
&["io.opentelemetry.spring-data-3.0"],
);
f.service = "diesel-svc".to_string();
assert_eq!(detect_framework(&f), Some(Framework::JavaJpa));
}
#[test]
fn service_name_fallback_helidon_mp_wins_over_se() {
let mut f = make_finding(FindingType::NPlusOneSql, Severity::Warning);
f.service = "helidon-mp-svc".to_string();
f.code_location = None;
f.instrumentation_scopes = vec![];
f.suggested_fix = None;
assert_eq!(detect_framework(&f), Some(Framework::JavaHelidonMp));
}
#[test]
fn returns_python_django_for_py_extension_with_django_namespace() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc("repo.py", Some("django.db.models"))),
);
assert_eq!(detect_framework(&f), Some(Framework::PythonDjango));
}
#[test]
fn returns_python_sqlalchemy_for_py_extension_with_sqlalchemy_namespace() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc("models.py", Some("sqlalchemy.orm.session"))),
);
assert_eq!(detect_framework(&f), Some(Framework::PythonSqlAlchemy));
}
#[test]
fn returns_python_generic_for_py_extension_with_unknown_namespace() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc("handlers.py", Some("myapp.orders.views"))),
);
assert_eq!(detect_framework(&f), Some(Framework::PythonGeneric));
}
#[test]
fn returns_python_django_from_otel_scope_without_filepath() {
let mut f = finding_with_location(FindingType::NPlusOneSql, None);
f.instrumentation_scopes = vec!["opentelemetry.instrumentation.django".to_string()];
assert_eq!(detect_framework(&f), Some(Framework::PythonDjango));
}
#[test]
fn returns_none_for_unsupported_extension() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc("query.rb", Some("ActiveRecord::Base"))),
);
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_returns_slow_sql_jpa_fix() {
let f = finding_with_location(
FindingType::SlowSql,
Some(loc("Repository.java", Some("org.hibernate.SessionImpl"))),
);
let fix = lookup_fix(&f).expect("(SlowSql, JavaJpa) should have a fix");
assert_eq!(fix.framework, "java_jpa");
assert!(fix.recommendation.contains("EXPLAIN ANALYZE"));
assert!(fix.recommendation.contains("index"));
}
#[test]
fn lookup_table_returns_slow_sql_csharp_ef_core_fix() {
let f = finding_with_location(
FindingType::SlowSql,
Some(loc(
"OrderQueries.cs",
Some("Microsoft.EntityFrameworkCore.DbContext"),
)),
);
let fix = lookup_fix(&f).expect("(SlowSql, CsharpEfCore) should have a fix");
assert_eq!(fix.framework, "csharp_ef_core");
assert!(fix.recommendation.contains("ToQueryString"));
assert!(fix.recommendation.contains("HasIndex"));
}
#[test]
fn lookup_table_returns_redundant_http_java_generic_fix() {
let f = finding_with_location(
FindingType::RedundantHttp,
Some(loc(
"UserClient.java",
Some("org.springframework.web.client"),
)),
);
let fix = lookup_fix(&f).expect("(RedundantHttp, JavaGeneric) should have a fix");
assert_eq!(fix.framework, "java_generic");
assert!(
fix.recommendation.contains("memoization") || fix.recommendation.contains("@Cacheable")
);
}
#[test]
fn lookup_table_returns_excessive_fanout_webflux_fix() {
let f = finding_with_location(
FindingType::ExcessiveFanout,
Some(loc(
"OrderRouter.java",
Some("org.springframework.web.reactive"),
)),
);
let fix = lookup_fix(&f).expect("(ExcessiveFanout, JavaWebFlux) should have a fix");
assert_eq!(fix.framework, "java_webflux");
assert!(fix.recommendation.contains("concurrency"));
assert!(fix.recommendation.contains("bulkhead"));
}
#[test]
fn lookup_table_returns_chatty_service_csharp_generic_fix() {
let f = finding_with_location(
FindingType::ChattyService,
Some(loc("OrderOrchestrator.cs", Some("MyApp.Orders.Sync"))),
);
let fix = lookup_fix(&f).expect("(ChattyService, CsharpGeneric) should have a fix");
assert_eq!(fix.framework, "csharp_generic");
assert!(
fix.recommendation.contains("bulk endpoint") || fix.recommendation.contains("gRPC")
);
}
#[test]
fn lookup_table_returns_pool_saturation_quarkus_fix() {
let f = finding_with_location(
FindingType::PoolSaturation,
Some(loc(
"OrderService.java",
Some("io.quarkus.hibernate.orm.runtime"),
)),
);
let fix = lookup_fix(&f).expect("(PoolSaturation, JavaQuarkus) should have a fix");
assert_eq!(fix.framework, "java_quarkus");
assert!(fix.recommendation.contains("jdbc.max-size"));
assert!(fix.recommendation.contains("slow"));
}
#[test]
fn lookup_table_returns_serialized_calls_rust_generic_fix() {
let f = finding_with_location(
FindingType::SerializedCalls,
Some(loc("src/checkout.rs", Some("myapp::checkout"))),
);
let fix = lookup_fix(&f).expect("(SerializedCalls, RustGeneric) should have a fix");
assert_eq!(fix.framework, "rust_generic");
assert!(fix.recommendation.contains("tokio::join"));
}
#[test]
fn go_generic_from_scope_prefix_without_code_location() {
let mut f = finding_with_location(FindingType::NPlusOneSql, None);
f.instrumentation_scopes = vec!["github.com/exaring/otelpgx".to_string()];
let fix = lookup_fix(&f).expect("GoGeneric via scope prefix");
assert_eq!(fix.framework, "go_generic");
}
#[test]
fn node_generic_from_scope_prefix_without_code_location() {
let mut f = finding_with_location(FindingType::NPlusOneSql, None);
f.instrumentation_scopes = vec!["@opentelemetry/instrumentation-pg".to_string()];
let fix = lookup_fix(&f).expect("NodeGeneric via scope prefix");
assert_eq!(fix.framework, "node_generic");
}
#[test]
fn python_bare_driver_scope_returns_none_without_filepath() {
let mut f = finding_with_location(FindingType::RedundantSql, None);
f.instrumentation_scopes = vec!["opentelemetry.instrumentation.psycopg".to_string()];
assert!(lookup_fix(&f).is_none());
}
#[test]
fn go_gorm_detected_from_filepath_and_namespace() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc("handler.go", Some("gorm.io/gorm"))),
);
let fix = lookup_fix(&f).expect("(NPlusOneSql, GoGorm) should have a fix");
assert_eq!(fix.framework, "go_gorm");
assert!(fix.recommendation.contains("Preload"));
}
#[test]
fn go_generic_fallback_from_filepath_without_gorm_namespace() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc("handler.go", Some("main"))),
);
let fix = lookup_fix(&f).expect("(NPlusOneSql, GoGeneric) should have a fix");
assert_eq!(fix.framework, "go_generic");
}
#[test]
fn go_gorm_detected_from_namespace_without_filepath() {
let f = finding_with_location(
FindingType::RedundantSql,
Some(CodeLocation {
function: Some("List".to_string()),
filepath: None,
lineno: None,
namespace: Some("gorm.io/gorm".to_string()),
}),
);
let fix = lookup_fix(&f).expect("GORM should be detected from namespace alone");
assert_eq!(fix.framework, "go_gorm");
}
#[test]
fn node_prisma_detected_from_ts_filepath_and_namespace() {
let f = finding_with_location(
FindingType::NPlusOneSql,
Some(loc("service.ts", Some("prisma"))),
);
let fix = lookup_fix(&f).expect("(NPlusOneSql, NodePrisma) should have a fix");
assert_eq!(fix.framework, "node_prisma");
assert!(fix.recommendation.contains("include"));
}
#[test]
fn node_generic_from_tsx_filepath() {
let f = finding_with_location(
FindingType::SlowHttp,
Some(loc("OrderList.tsx", Some("components.OrderList"))),
);
let fix = lookup_fix(&f).expect("(SlowHttp, NodeGeneric) should have a fix");
assert_eq!(fix.framework, "node_generic");
}
#[test]
fn node_generic_from_mjs_filepath() {
let f = finding_with_location(
FindingType::NPlusOneHttp,
Some(loc("handler.mjs", Some("routes.orders"))),
);
let fix = lookup_fix(&f).expect("(NPlusOneHttp, NodeGeneric) should have a fix");
assert_eq!(fix.framework, "node_generic");
}
#[test]
fn fix_table_cardinality_is_pinned() {
assert_eq!(FIXES.len(), 96);
for anchor in [
(FindingType::NPlusOneSql, Framework::JavaJpa),
(FindingType::NPlusOneSql, Framework::CsharpEfCore),
(FindingType::NPlusOneSql, Framework::RustDiesel),
(FindingType::SlowSql, Framework::JavaJpa),
(FindingType::SerializedCalls, Framework::RustGeneric),
(FindingType::PoolSaturation, Framework::JavaQuarkus),
(FindingType::NPlusOneSql, Framework::GoGorm),
(FindingType::NPlusOneSql, Framework::NodePrisma),
] {
assert!(
FIXES.contains_key(&anchor),
"FIXES anchor entry {anchor:?} is missing; was it dropped or renamed?"
);
}
}
#[test]
fn lookup_table_misses_for_pool_saturation_under_webflux() {
let f = finding_with_location(
FindingType::PoolSaturation,
Some(loc(
"OrderRouter.java",
Some("org.springframework.web.reactive.function.client"),
)),
);
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_populates_suggested_fix_for_python_django() {
let mut findings = vec![finding_with_location(
FindingType::NPlusOneSql,
Some(loc("repo.py", Some("django.db.models"))),
)];
enrich(&mut findings);
let fix = findings[0]
.suggested_fix
.as_ref()
.expect("Python Django should be enriched with suggested_fix");
assert_eq!(fix.framework, "python_django");
assert!(fix.recommendation.contains("select_related"));
}
#[test]
fn enrich_leaves_suggested_fix_none_for_unsupported_language() {
let mut findings = vec![finding_with_location(
FindingType::NPlusOneSql,
Some(loc("query.rb", Some("ActiveRecord::Base"))),
)];
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",
"docs.oracle.com",
"projectreactor.io",
"resilience4j.readme.io",
"learn.microsoft.com",
"www.pollydocs.org",
"www.npgsql.org",
"docs.djangoproject.com",
"docs.sqlalchemy.org",
"docs.python.org",
"docs.diesel.rs",
"sea-ql.org",
"docs.rs",
"pkg.go.dev",
"gorm.io",
"www.npmjs.com",
"node-postgres.com",
"www.prisma.io",
"developer.mozilla.org",
"www.postgresql.org",
"martinfowler.com",
];
const ALLOWED_GITHUB_PREFIXES: &[&str] = &[
"github.com/ben-manes/caffeine",
"github.com/brettwooldridge/HikariCP",
"github.com/jackc/pgx",
"github.com/patrickmn/go-cache",
"github.com/sony/gobreaker",
];
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 authority_end = after_scheme
.find(['/', '?', '#'])
.unwrap_or(after_scheme.len());
assert!(
!after_scheme[..authority_end].contains('@'),
"({ft:?}, {fw:?}) reference_url must not carry userinfo, got {url:?}"
);
let authority = &after_scheme[..authority_end];
let host = authority.split(':').next().unwrap_or(authority);
let host_ok = ALLOWED_DOMAIN_SUFFIXES
.iter()
.any(|dom| host == *dom || host.ends_with(&format!(".{dom}")));
let github_path_ok = host == "github.com"
&& ALLOWED_GITHUB_PREFIXES.iter().any(|prefix| {
after_scheme
.strip_prefix(prefix)
.is_some_and(|rest| rest.is_empty() || rest.starts_with(['/', '?', '#']))
});
assert!(
host_ok || github_path_ok,
"({ft:?}, {fw:?}) reference_url {url:?} not allowed; add the host \
to ALLOWED_DOMAIN_SUFFIXES or the `<org>/<repo>` path to \
ALLOWED_GITHUB_PREFIXES"
);
}
}
}