use anyhow::Result;
use serde::Serialize;
use crate::graph::GraphStore;
#[derive(Debug, Clone, Serialize)]
pub struct ConcernMatch {
pub symbol_id: String,
pub kind: &'static str,
pub detail: String,
}
struct ConcernPattern {
kind: &'static str,
patterns: &'static [&'static str],
}
static CONCERN_PATTERNS: &[ConcernPattern] = &[
ConcernPattern {
kind: "Authorization",
patterns: &[
"@PreAuthorize(",
"@PostAuthorize(",
"@Secured(",
"@RolesAllowed(",
"@PermitAll",
"@DenyAll",
"@login_required",
"@permission_required(",
"@requires_auth",
"@UseGuards(",
"@Roles(",
"@SetMetadata('roles'",
"[Authorize(",
"[Authorize]",
"[AllowAnonymous]",
"#[guard(",
"#[authorize(",
],
},
ConcernPattern {
kind: "Validation",
patterns: &[
"@Valid",
"@Validated",
"@NotNull",
"@NotBlank",
"@NotEmpty",
"@Size(",
"@Pattern(",
"@Min(",
"@Max(",
"@validator(",
"@pydantic.validator(",
"@field_validator(",
"@UsePipes(",
"ValidationPipe",
"[ValidateAntiForgeryToken]",
"[Required]",
"[Range(",
"[StringLength(",
"#[validate(",
],
},
ConcernPattern {
kind: "Caching",
patterns: &[
"@Cacheable(",
"@CacheEvict(",
"@CachePut(",
"@Caching(",
"@cache",
"@lru_cache(",
"@cached_property",
"@memoize",
"@CacheKey(",
"@CacheTTL(",
"CacheInterceptor",
"[OutputCache(",
"[ResponseCache(",
"caches_action",
"caches_page",
"#[cached(",
],
},
ConcernPattern {
kind: "Transaction",
patterns: &[
"@Transactional(",
"@Transactional\n",
"@atomic",
"@transaction.atomic",
"@commit_on_success",
"@Transactional()",
"[Transaction]",
"#[transactional]",
],
},
ConcernPattern {
kind: "RateLimiting",
patterns: &[
"@RateLimiter(",
"@RateLimit(",
"@Bulkhead(",
"@rate_limit(",
"@throttle(",
"@ratelimit(",
"@Throttle(",
"@SkipThrottle(",
"[EnableRateLimiting(",
"[DisableRateLimiting(",
"#[rate_limit(",
],
},
ConcernPattern {
kind: "AuditLogging",
patterns: &[
"@Auditable(",
"@Audit(",
"@Logged",
"@audit_log(",
"@log_action(",
"LoggingInterceptor",
"[Audit]",
"#[instrument(",
],
},
ConcernPattern {
kind: "FeatureFlag",
patterns: &[
"@FeatureFlag(",
"@Toggle(",
"@Feature(",
"@feature_flag(",
"@feature_enabled(",
"[FeatureGate(",
"#[feature(",
],
},
ConcernPattern {
kind: "Cors",
patterns: &[
"@CrossOrigin(",
"@CrossOrigin\n",
"[EnableCors(",
"[DisableCors(",
"#[cors(",
],
},
ConcernPattern {
kind: "Async",
patterns: &[
"@Async",
"@Scheduled(",
"@EventListener(",
"@celery.task",
"@background_task(",
"@periodic_task(",
"@Cron(",
"@Interval(",
"@EventPattern(",
"[BackgroundService]",
"#[tokio::main]",
],
},
ConcernPattern {
kind: "Retry",
patterns: &[
"@Retry(",
"@Retryable(",
"@CircuitBreaker(",
"@retry(",
"@backoff(",
"@circuit_breaker(",
"RetryInterceptor",
"[Retry(",
"[CircuitBreaker(",
"#[retry(",
],
},
];
pub fn detect_cross_cutting(store: &GraphStore) -> Result<Vec<ConcernMatch>> {
let _lock = store.write_lock()?;
let conn = store.connection()?;
let result = conn
.query("MATCH (s:Symbol) WHERE s.docstring IS NOT NULL AND s.docstring <> '' RETURN s.id, s.docstring")
.map_err(|e| anyhow::anyhow!("query failed: {e}"))?;
let mut matches = Vec::new();
for row in result {
if row.len() < 2 {
continue;
}
let symbol_id = row[0].to_string();
let docstring = row[1].to_string();
for cp in CONCERN_PATTERNS {
for &pattern in cp.patterns {
if docstring.contains(pattern) {
let detail = extract_matched_line(&docstring, pattern);
matches.push(ConcernMatch {
symbol_id: symbol_id.clone(),
kind: cp.kind,
detail,
});
break;
}
}
}
}
if !matches.is_empty() {
write_concerns(store, &matches)?;
}
Ok(matches)
}
fn extract_matched_line(docstring: &str, pattern: &str) -> String {
for line in docstring.lines() {
if line.contains(pattern) {
return line.trim().to_string();
}
}
pattern.to_string()
}
fn write_concerns(store: &GraphStore, matches: &[ConcernMatch]) -> Result<()> {
let conn = store.connection()?;
conn.query("BEGIN TRANSACTION")
.map_err(|e| anyhow::anyhow!("begin txn: {e}"))?;
let _ = conn.query("MATCH (c:Concern) DETACH DELETE c");
for m in matches {
let sym_esc = crate::escape_str(&m.symbol_id);
let kind_esc = crate::escape_str(m.kind);
let detail_esc = crate::escape_str(&m.detail);
let concern_id = format!("{}::{}", m.symbol_id, m.kind);
let id_esc = crate::escape_str(&concern_id);
let _ = conn.query(&format!(
"CREATE (c:Concern {{id: '{id_esc}', kind: '{kind_esc}', detail: '{detail_esc}'}})"
));
let _ = conn.query(&format!(
"MATCH (s:Symbol), (c:Concern) WHERE s.id = '{sym_esc}' AND c.id = '{id_esc}' CREATE (s)-[:HAS_CONCERN]->(c)"
));
}
conn.query("COMMIT")
.map_err(|e| anyhow::anyhow!("commit txn: {e}"))?;
Ok(())
}
pub fn format_concerns(matches: &[ConcernMatch]) -> String {
if matches.is_empty() {
return "No cross-cutting concerns detected.".to_string();
}
let mut by_kind: std::collections::BTreeMap<&str, Vec<&ConcernMatch>> =
std::collections::BTreeMap::new();
for m in matches {
by_kind.entry(m.kind).or_default().push(m);
}
let mut out = format!("Cross-cutting concerns: {} total\n\n", matches.len());
for (kind, items) in &by_kind {
out.push_str(&format!("## {} ({} symbols)\n", kind, items.len()));
for item in items {
out.push_str(&format!(" {} — {}\n", item.symbol_id, item.detail));
}
out.push('\n');
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_java_authorization() {
let docstring = "@PreAuthorize(\"hasRole('ADMIN')\")\npublic void deleteUser() {}";
let mut found = Vec::new();
for cp in CONCERN_PATTERNS {
for &pattern in cp.patterns {
if docstring.contains(pattern) {
found.push(cp.kind);
break;
}
}
}
assert!(
found.contains(&"Authorization"),
"should detect @PreAuthorize"
);
}
#[test]
fn test_detect_python_caching() {
let docstring = "@lru_cache(maxsize=128)\ndef get_user(user_id):";
let mut found = Vec::new();
for cp in CONCERN_PATTERNS {
for &pattern in cp.patterns {
if docstring.contains(pattern) {
found.push(cp.kind);
break;
}
}
}
assert!(found.contains(&"Caching"), "should detect @lru_cache");
}
#[test]
fn test_detect_nestjs_throttle() {
let docstring = "@Throttle(10, 60)\n@Roles('admin')\nasync getUsers() {}";
let mut found = Vec::new();
for cp in CONCERN_PATTERNS {
for &pattern in cp.patterns {
if docstring.contains(pattern) {
found.push(cp.kind);
break;
}
}
}
assert!(found.contains(&"RateLimiting"), "should detect @Throttle");
assert!(found.contains(&"Authorization"), "should detect @Roles");
}
#[test]
fn test_detect_csharp_authorize() {
let docstring = "[Authorize(Roles=\"Admin\")]\n[ValidateAntiForgeryToken]\npublic IActionResult Delete()";
let mut found = Vec::new();
for cp in CONCERN_PATTERNS {
for &pattern in cp.patterns {
if docstring.contains(pattern) {
found.push(cp.kind);
break;
}
}
}
assert!(
found.contains(&"Authorization"),
"should detect [Authorize]"
);
assert!(
found.contains(&"Validation"),
"should detect [ValidateAntiForgeryToken]"
);
}
#[test]
fn test_detect_rust_instrument() {
let docstring = "#[instrument(skip(db))]\nasync fn handle_request()";
let mut found = Vec::new();
for cp in CONCERN_PATTERNS {
for &pattern in cp.patterns {
if docstring.contains(pattern) {
found.push(cp.kind);
break;
}
}
}
assert!(
found.contains(&"AuditLogging"),
"should detect #[instrument]"
);
}
#[test]
fn test_detect_spring_transactional() {
let docstring = "@Transactional(readOnly = true)\npublic List<User> findAll()";
let mut found = Vec::new();
for cp in CONCERN_PATTERNS {
for &pattern in cp.patterns {
if docstring.contains(pattern) {
found.push(cp.kind);
break;
}
}
}
assert!(
found.contains(&"Transaction"),
"should detect @Transactional"
);
}
#[test]
fn test_no_false_positive_on_plain_text() {
let docstring = "This function validates cacheable behavior for users";
let mut found = Vec::new();
for cp in CONCERN_PATTERNS {
for &pattern in cp.patterns {
if docstring.contains(pattern) {
found.push(cp.kind);
break;
}
}
}
assert!(
found.is_empty(),
"should not match plain text without annotation syntax: {:?}",
found
);
}
#[test]
fn test_extract_matched_line() {
let doc = "@PreAuthorize(\"hasRole('ADMIN')\")\npublic void delete()";
let line = extract_matched_line(doc, "@PreAuthorize(");
assert_eq!(line, "@PreAuthorize(\"hasRole('ADMIN')\")");
}
#[test]
fn test_detect_python_login_required() {
let docstring = "@login_required\ndef dashboard(request):";
let mut found = Vec::new();
for cp in CONCERN_PATTERNS {
for &pattern in cp.patterns {
if docstring.contains(pattern) {
found.push(cp.kind);
break;
}
}
}
assert!(
found.contains(&"Authorization"),
"should detect @login_required"
);
}
#[test]
fn test_detect_ruby_before_action() {
let docstring = "before_action :authenticate_user!\ndef index";
let mut found = Vec::new();
for cp in CONCERN_PATTERNS {
for &pattern in cp.patterns {
if docstring.contains(pattern) {
found.push(cp.kind);
break;
}
}
}
assert!(
found.is_empty() || found.contains(&"Authorization"),
"Ruby before_action pattern check: {:?}",
found
);
}
}