From ae758b583c9505fdcecead1b331a79c57bd338d9 Mon Sep 17 00:00:00 2001 From: Xabier Aldama Date: Mon, 20 Apr 2026 15:00:24 +0200 Subject: [PATCH 01/19] Add EnclosingFunction field to Violation --- .../src/analysis/ddsa_lib/js/violation.rs | 1 + .../src/analysis/ddsa_lib/runtime.rs | 10 ++++++++++ .../static-analysis-kernel/src/model/violation.rs | 14 ++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/crates/static-analysis-kernel/src/analysis/ddsa_lib/js/violation.rs b/crates/static-analysis-kernel/src/analysis/ddsa_lib/js/violation.rs index 5dcb41e6f..d0bb1a289 100644 --- a/crates/static-analysis-kernel/src/analysis/ddsa_lib/js/violation.rs +++ b/crates/static-analysis-kernel/src/analysis/ddsa_lib/js/violation.rs @@ -55,6 +55,7 @@ impl Violation { fixes, taint_flow, is_suppressed: false, + enclosing_function: None, } } } diff --git a/crates/static-analysis-kernel/src/analysis/ddsa_lib/runtime.rs b/crates/static-analysis-kernel/src/analysis/ddsa_lib/runtime.rs index 6ce95a692..c0994630e 100644 --- a/crates/static-analysis-kernel/src/analysis/ddsa_lib/runtime.rs +++ b/crates/static-analysis-kernel/src/analysis/ddsa_lib/runtime.rs @@ -200,6 +200,16 @@ impl JsRuntime { let violations = js_violations .into_iter() .map(|v| v.into_violation(rule.severity, rule.category)) + .map(|mut v| { + v.enclosing_function = analysis::languages::find_enclosing_function_with_tree( + source_text.as_ref(), + source_tree.as_ref(), + v.start.line, + v.start.col, + &rule.language, + ); + v + }) .collect::>(); let timing = ExecutionTimingCompat { diff --git a/crates/static-analysis-kernel/src/model/violation.rs b/crates/static-analysis-kernel/src/model/violation.rs index 574a7a15a..2e2c4b915 100644 --- a/crates/static-analysis-kernel/src/model/violation.rs +++ b/crates/static-analysis-kernel/src/model/violation.rs @@ -4,6 +4,16 @@ use common::model::position::{Position, Region}; use derive_builder::Builder; use serde::{Deserialize, Serialize}; +/// The function or method that encloses a violation. +#[derive(Deserialize, Debug, Serialize, Clone, PartialEq)] +pub struct EnclosingFunction { + /// Simple identifier (e.g. `handle`, `doSomething`). + pub name: String, + /// Service-definition qualified name: `ClassName.methodName` when the function belongs to a + /// class or struct, or just `methodName` for top-level functions. + pub fully_qualified_name: String, +} + #[derive(Copy, Clone, Deserialize, Debug, Serialize, Eq, PartialEq)] pub enum EditType { #[serde(rename = "ADD")] @@ -42,4 +52,8 @@ pub struct Violation { #[serde(default)] #[builder(default)] pub is_suppressed: bool, + /// The function or method enclosing this violation, if any. + #[serde(default)] + #[builder(default)] + pub enclosing_function: Option, } From 371567bb7453b02b254bead2c5fa4326853ff7c2 Mon Sep 17 00:00:00 2001 From: Xabier Aldama Date: Mon, 20 Apr 2026 15:00:31 +0200 Subject: [PATCH 02/19] Detect enclosing function for Java --- .../src/analysis/languages.rs | 100 ++++ .../src/analysis/languages/java.rs | 1 + .../src/analysis/languages/java/methods.rs | 472 ++++++++++++++++++ 3 files changed, 573 insertions(+) create mode 100644 crates/static-analysis-kernel/src/analysis/languages/java/methods.rs diff --git a/crates/static-analysis-kernel/src/analysis/languages.rs b/crates/static-analysis-kernel/src/analysis/languages.rs index 9a4a724b3..9a0ccd420 100644 --- a/crates/static-analysis-kernel/src/analysis/languages.rs +++ b/crates/static-analysis-kernel/src/analysis/languages.rs @@ -9,6 +9,60 @@ pub mod javascript; pub mod python; pub mod typescript; +use crate::model::common::Language; +use crate::model::violation::EnclosingFunction; + +/// Returns the enclosing function for the given source position, or `None` if the position +/// is not inside any named function or the language has no implementation. +/// +/// This function parses the source code from scratch. +/// If you already have a parsed tree, use [`find_enclosing_function_with_tree`]. +pub fn find_enclosing_function( + source_code: &str, + line: u32, + col: u32, + language: &Language, +) -> Option { + match language { + Language::Java => java::methods::find_enclosing_function(source_code, line, col), + _ => None, + } +} + +/// Returns the enclosing function for the given source position, reusing an already-parsed tree. +/// See [`find_enclosing_function`] for documentation. +pub fn find_enclosing_function_with_tree( + source_code: &str, + tree: &tree_sitter::Tree, + line: u32, + col: u32, + language: &Language, +) -> Option { + match language { + Language::Java => { + java::methods::find_enclosing_function_with_tree(source_code, tree, line, col) + } + _ => None, + } +} + +/// Walks up from `node` looking for an ancestor whose `kind()` is one of `class_kinds`. +/// Returns the text of that ancestor's `name` field, or `None` if not found. +pub(crate) fn enclosing_class_name<'s>( + source_code: &'s str, + mut node: tree_sitter::Node<'_>, + class_kinds: &[&str], +) -> Option<&'s str> { + loop { + node = node.parent()?; + if class_kinds.contains(&node.kind()) { + return node + .child_by_field_name("name") + .map(|n| ts_node_text(source_code, n)); + } + } +} + /// Returns the text that `node` spans. /// /// This is simply a wrapper around [`tree_sitter::Node::utf8_text`] @@ -21,3 +75,49 @@ pub(crate) fn ts_node_text<'text>(parsed_text: &'text str, node: tree_sitter::No node.utf8_text(parsed_text.as_bytes()) .expect("node should be from `parsed_text`'s tree") } + +#[cfg(test)] +mod tests { + use crate::model::common::{Language, ALL_LANGUAGES}; + + // Languages with an enclosing-function implementation in this module. + const SUPPORTED: &[Language] = &[Language::Java]; + + // Languages that intentionally have no implementation yet. + // When adding a new language to the analyzer, add it here (no detection) or to + // SUPPORTED (detection implemented) — leaving it out causes this test to fail. + const NOT_IMPLEMENTED: &[Language] = &[ + Language::Python, + Language::Go, + Language::JavaScript, + Language::TypeScript, + Language::Csharp, + Language::Dockerfile, + Language::Elixir, + Language::Json, + Language::Kotlin, + Language::Ruby, + Language::Rust, + Language::Swift, + Language::Terraform, + Language::Yaml, + Language::Starlark, + Language::Bash, + Language::PHP, + Language::Markdown, + Language::Apex, + Language::R, + Language::SQL, + ]; + + #[test] + fn all_languages_accounted_for() { + for lang in ALL_LANGUAGES { + assert!( + SUPPORTED.contains(lang) || NOT_IMPLEMENTED.contains(lang), + "{lang:?} is not listed in SUPPORTED or NOT_IMPLEMENTED — \ + either add enclosing-function detection for it or add it to NOT_IMPLEMENTED" + ); + } + } +} diff --git a/crates/static-analysis-kernel/src/analysis/languages/java.rs b/crates/static-analysis-kernel/src/analysis/languages/java.rs index c42b51a10..5d6134254 100644 --- a/crates/static-analysis-kernel/src/analysis/languages/java.rs +++ b/crates/static-analysis-kernel/src/analysis/languages/java.rs @@ -4,3 +4,4 @@ mod imports; pub use imports::*; +pub mod methods; diff --git a/crates/static-analysis-kernel/src/analysis/languages/java/methods.rs b/crates/static-analysis-kernel/src/analysis/languages/java/methods.rs new file mode 100644 index 000000000..b950b5a55 --- /dev/null +++ b/crates/static-analysis-kernel/src/analysis/languages/java/methods.rs @@ -0,0 +1,472 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024 Datadog, Inc. + +use std::collections::HashMap; + +use crate::analysis::languages::{enclosing_class_name, ts_node_text}; +use crate::analysis::tree_sitter::get_tree; +use crate::model::common::Language; +use crate::model::violation::EnclosingFunction; + +/// Returns the enclosing method or constructor for the given source position, or `None` if the +/// position is not inside any method. +/// +/// This function parses the source code from scratch. +/// If you already have a parsed tree, use [`find_enclosing_function_with_tree`]. +pub fn find_enclosing_function( + source_code: &str, + line: u32, + col: u32, +) -> Option { + get_tree(source_code, &Language::Java) + .and_then(|tree| find_enclosing_function_with_tree(source_code, &tree, line, col)) +} + +/// Returns the enclosing method or constructor for the given source position. +/// See [`find_enclosing_function`] for documentation. +/// +/// The `fullyQualifiedName` follows the standard Java FQN format: +/// `package.ClassName#methodName(ParamType1, ParamType2):ReturnType` +/// +/// Types are resolved to fully qualified names using the file's import declarations. +/// Types from `java.lang` (String, Integer, etc.) are always resolved. Types only +/// reachable via wildcard imports are returned as simple names. +pub fn find_enclosing_function_with_tree( + source_code: &str, + tree: &tree_sitter::Tree, + line: u32, + col: u32, +) -> Option { + let point = tree_sitter::Point { + row: line.saturating_sub(1) as usize, + column: col.saturating_sub(1) as usize, + }; + let mut node = tree + .root_node() + .named_descendant_for_point_range(point, point)?; + loop { + match node.kind() { + "method_declaration" | "constructor_declaration" => { + let name = node + .child_by_field_name("name") + .map(|n| ts_node_text(source_code, n).to_owned())?; + let fully_qualified_name = build_fqn(source_code, tree, node, &name); + return Some(EnclosingFunction { + name, + fully_qualified_name, + }); + } + _ => {} + } + node = node.parent()?; + } +} + +/// Builds the fully qualified method name in the format: +/// `package.ClassName#methodName(ParamType1, ParamType2):ReturnType` +/// +/// Constructors have no return type and omit the `:ReturnType` suffix. +fn build_fqn( + source_code: &str, + tree: &tree_sitter::Tree, + method_node: tree_sitter::Node, + method_name: &str, +) -> String { + let root = tree.root_node(); + let package = find_package(source_code, root); + let class_kinds = &[ + "class_declaration", + "interface_declaration", + "enum_declaration", + ]; + let class_name = enclosing_class_name(source_code, method_node, class_kinds); + + let fqn_class = match (package.as_deref(), class_name) { + (Some(pkg), Some(cls)) => format!("{pkg}.{cls}"), + (None, Some(cls)) => cls.to_string(), + (Some(pkg), None) => pkg.to_string(), + (None, None) => String::new(), + }; + + let import_map = build_import_map(source_code, root); + let pkg = package.as_deref(); + + // method_declaration has a `type` field; constructor_declaration does not. + let return_type = method_node + .child_by_field_name("type") + .map(|t| resolve_type(source_code, t, &import_map, pkg)); + + let param_types = method_node + .child_by_field_name("parameters") + .map(|p| extract_param_types(source_code, p, &import_map, pkg)) + .unwrap_or_default(); + + let params_str = param_types.join(", "); + + match return_type { + Some(rt) => format!("{fqn_class}#{method_name}({params_str}):{rt}"), + None => format!("{fqn_class}#{method_name}({params_str})"), + } +} + +/// Returns the package name declared at the top of the compilation unit, if any. +fn find_package(source_code: &str, root: tree_sitter::Node) -> Option { + for i in 0..root.named_child_count() { + let Some(child) = root.named_child(i) else { + continue; + }; + if child.kind() == "package_declaration" { + for j in 0..child.named_child_count() { + let Some(pkg_child) = child.named_child(j) else { + continue; + }; + if matches!(pkg_child.kind(), "scoped_identifier" | "identifier") { + return Some(ts_node_text(source_code, pkg_child).to_owned()); + } + } + } + } + None +} + +/// Builds a map of simple class name → fully qualified name from explicit (non-wildcard, +/// non-static) import declarations in the file. +fn build_import_map(source_code: &str, root: tree_sitter::Node) -> HashMap { + let mut map = HashMap::new(); + for i in 0..root.named_child_count() { + let Some(child) = root.named_child(i) else { + continue; + }; + if child.kind() != "import_declaration" { + continue; + } + let text = ts_node_text(source_code, child); + // Strip "import " prefix and ";" suffix, then trim whitespace + let body = text + .trim_start_matches("import") + .trim() + .trim_end_matches(';') + .trim(); + // Skip static and wildcard imports — they can't be resolved without a classpath + if body.starts_with("static ") || body.ends_with('*') { + continue; + } + let simple_name = body.split('.').next_back().unwrap_or("").to_string(); + if !simple_name.is_empty() { + map.insert(simple_name, body.to_string()); + } + } + map +} + +/// Resolves a tree-sitter type node to a fully qualified type name where possible. +/// +/// - Explicit imports: `MultipartFile` → `org.springframework.web.multipart.MultipartFile` +/// - java.lang types: `String` → `java.lang.String` +/// - Everything else: returned as the simple name from source +fn resolve_type( + source_code: &str, + type_node: tree_sitter::Node, + import_map: &HashMap, + package: Option<&str>, +) -> String { + match type_node.kind() { + // Primitives and void — always use as-is + "void_type" | "integral_type" | "floating_point_type" | "boolean_type" => { + ts_node_text(source_code, type_node).to_owned() + } + // Simple class name reference + "type_identifier" => { + resolve_class_name(ts_node_text(source_code, type_node), import_map, package) + } + // Already fully qualified in source (rare) + "scoped_type_identifier" => ts_node_text(source_code, type_node).to_owned(), + // Generic type: List → java.util.List + "generic_type" => { + let base = type_node + .named_child(0) + .map(|n| resolve_type(source_code, n, import_map, package)) + .unwrap_or_default(); + let type_args = type_node + .named_child(1) + .filter(|n| n.kind() == "type_arguments"); + match type_args { + Some(args_node) => { + let args: Vec = (0..args_node.named_child_count()) + .filter_map(|i| args_node.named_child(i)) + .map(|n| resolve_type(source_code, n, import_map, package)) + .collect(); + if args.is_empty() { + base + } else { + format!("{base}<{}>", args.join(", ")) + } + } + None => base, + } + } + // Array type: String[] — resolve element type and keep dimensions + "array_type" => { + let element = type_node + .child_by_field_name("element") + .map(|n| resolve_type(source_code, n, import_map, package)) + .unwrap_or_default(); + let dims = type_node + .child_by_field_name("dimensions") + .map(|n| ts_node_text(source_code, n)) + .unwrap_or("[]"); + format!("{element}{dims}") + } + // Annotated type: @NotNull String — strip annotation and resolve the underlying type + "annotated_type" => { + // The last named child is the actual type + let count = type_node.named_child_count(); + type_node + .named_child(count.saturating_sub(1)) + .map(|n| resolve_type(source_code, n, import_map, package)) + .unwrap_or_else(|| ts_node_text(source_code, type_node).to_owned()) + } + // Wildcard (? extends Foo, ? super Bar) — keep as raw text + "wildcard" => ts_node_text(source_code, type_node).to_owned(), + _ => ts_node_text(source_code, type_node).to_owned(), + } +} + +/// Resolves a simple class name to its fully qualified form using explicit imports and +/// the well-known `java.lang` package that is always implicitly available. +fn resolve_class_name( + name: &str, + import_map: &HashMap, + _package: Option<&str>, +) -> String { + if let Some(fqn) = import_map.get(name) { + return fqn.clone(); + } + if JAVA_LANG_TYPES.contains(&name) { + return format!("java.lang.{name}"); + } + name.to_owned() +} + +/// Extracts the ordered list of parameter types from a `formal_parameters` node. +/// Annotations and variable names are excluded; only the type is kept. +fn extract_param_types( + source_code: &str, + params_node: tree_sitter::Node, + import_map: &HashMap, + package: Option<&str>, +) -> Vec { + let mut types = vec![]; + for i in 0..params_node.named_child_count() { + let Some(child) = params_node.named_child(i) else { + continue; + }; + match child.kind() { + "formal_parameter" => { + if let Some(type_node) = child.child_by_field_name("type") { + types.push(resolve_type(source_code, type_node, import_map, package)); + } + } + "spread_parameter" => { + // Varargs: `Type... name` — type field exists on spread_parameter too + if let Some(type_node) = child.child_by_field_name("type") { + let t = resolve_type(source_code, type_node, import_map, package); + types.push(format!("{t}...")); + } + } + _ => {} + } + } + types +} + +// Types always in scope from java.lang.* (implicit import in every Java file) +const JAVA_LANG_TYPES: &[&str] = &[ + "AutoCloseable", + "Boolean", + "Byte", + "CharSequence", + "Character", + "Class", + "ClassLoader", + "Cloneable", + "Comparable", + "Double", + "Enum", + "Error", + "Exception", + "Float", + "Integer", + "Iterable", + "Long", + "Math", + "Number", + "Object", + "Process", + "Runnable", + "Runtime", + "RuntimeException", + "Short", + "String", + "StringBuffer", + "StringBuilder", + "System", + "Thread", + "Throwable", + "Void", +]; + +#[cfg(test)] +mod tests { + use super::find_enclosing_function_with_tree; + use crate::analysis::tree_sitter::get_tree; + use crate::model::common::Language; + use crate::model::violation::EnclosingFunction; + + fn find(source: &str, line: u32, col: u32) -> Option { + let tree = get_tree(source, &Language::Java).unwrap(); + find_enclosing_function_with_tree(source, &tree, line, col) + } + + fn ef(name: &str, sig: &str) -> Option { + Some(EnclosingFunction { + name: name.to_string(), + fully_qualified_name: sig.to_string(), + }) + } + + #[test] + fn inside_method() { + let src = "\ +class Foo { + public void doSomething() { + int x = 1; + } +} +"; + assert_eq!(find(src, 3, 9), ef("doSomething", "Foo#doSomething():void")); + } + + #[test] + fn inside_constructor() { + let src = "\ +class Foo { + public Foo() { + this.x = 0; + } +} +"; + assert_eq!(find(src, 3, 9), ef("Foo", "Foo#Foo()")); + } + + #[test] + fn with_package() { + let src = "\ +package com.example; +class Foo { + public void doSomething() { + int x = 1; + } +} +"; + assert_eq!( + find(src, 4, 9), + ef("doSomething", "com.example.Foo#doSomething():void") + ); + } + + #[test] + fn java_lang_type_resolved() { + // String is in java.lang and always resolved without an explicit import + let src = "\ +class Foo { + public void handle(String s) { + int x = 1; + } +} +"; + assert_eq!( + find(src, 3, 9), + ef("handle", "Foo#handle(java.lang.String):void") + ); + } + + #[test] + fn explicit_import_resolved() { + let src = "\ +import org.springframework.web.multipart.MultipartFile; +import org.springframework.ui.Model; +class Foo { + public String process(MultipartFile file, Model model) { + return \"ok\"; + } +} +"; + assert_eq!( + find(src, 4, 9), + ef( + "process", + "Foo#process(org.springframework.web.multipart.MultipartFile, org.springframework.ui.Model):java.lang.String" + ) + ); + } + + #[test] + fn full_fqn_with_package_and_imports() { + let src = "\ +package org.hdivsamples.controllers; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.ui.Model; +class DashboardController { + public String processSimple(MultipartFile file, Model model) { + return \"ok\"; + } +} +"; + assert_eq!( + find(src, 5, 9), + ef( + "processSimple", + "org.hdivsamples.controllers.DashboardController#processSimple(org.springframework.web.multipart.MultipartFile, org.springframework.ui.Model):java.lang.String" + ) + ); + } + + #[test] + fn annotations_not_in_fqn() { + // Method and parameter annotations are excluded from the FQN + let src = "\ +class Foo { + @Override + public void doSomething() { + int x = 1; + } +} +"; + assert_eq!(find(src, 4, 9), ef("doSomething", "Foo#doSomething():void")); + } + + #[test] + fn throws_not_in_fqn() { + // throws clause is not part of the standard Java FQN + let src = "\ +class Foo { + public void parse() throws IOException { + int x = 1; + } +} +"; + assert_eq!(find(src, 3, 9), ef("parse", "Foo#parse():void")); + } + + #[test] + fn top_level_field() { + let src = "\ +class Foo { + int x = 1; +} +"; + assert_eq!(find(src, 2, 9), None); + } +} From fc3e8c98c31e34df8e15df01f04f4c562f933fd6 Mon Sep 17 00:00:00 2001 From: Xabier Aldama Date: Mon, 20 Apr 2026 15:03:32 +0200 Subject: [PATCH 03/19] Export enclosing function in SARIF logicalLocations --- crates/cli/src/csv.rs | 1 + crates/cli/src/file_utils.rs | 3 + crates/cli/src/rule_utils.rs | 6 ++ crates/cli/src/sarif/sarif_utils.rs | 156 ++++++++++++++++++++++++---- 4 files changed, 147 insertions(+), 19 deletions(-) diff --git a/crates/cli/src/csv.rs b/crates/cli/src/csv.rs index b3da79d4f..0320d0a85 100644 --- a/crates/cli/src/csv.rs +++ b/crates/cli/src/csv.rs @@ -85,6 +85,7 @@ mod tests { fixes: vec![], taint_flow: None, is_suppressed: false, + enclosing_function: None, }], errors: vec![], execution_error: None, diff --git a/crates/cli/src/file_utils.rs b/crates/cli/src/file_utils.rs index aa6c9c280..fab2fcb43 100644 --- a/crates/cli/src/file_utils.rs +++ b/crates/cli/src/file_utils.rs @@ -452,6 +452,7 @@ mod tests { fixes: vec![], taint_flow: None, is_suppressed: false, + enclosing_function: None, }; let directory_string = d.into_os_string().into_string().unwrap(); let fingerprint = get_fingerprint_for_violation( @@ -497,6 +498,7 @@ mod tests { fixes: vec![], taint_flow: Some(vec![region0, region1]), is_suppressed: false, + enclosing_function: None, }; let fingerprint = get_fingerprint_for_violation( "taint_flow_rule".to_string(), @@ -528,6 +530,7 @@ mod tests { fixes: vec![], taint_flow: None, is_suppressed: false, + enclosing_function: None, }; let directory_string = d.into_os_string().into_string().unwrap(); diff --git a/crates/cli/src/rule_utils.rs b/crates/cli/src/rule_utils.rs index 3b2539352..1e3c109fb 100644 --- a/crates/cli/src/rule_utils.rs +++ b/crates/cli/src/rule_utils.rs @@ -81,6 +81,7 @@ pub fn convert_secret_result_to_rule_result(secret_result: &SecretResult) -> Rul fixes: vec![], taint_flow: None, is_suppressed: v.is_suppressed, + enclosing_function: None, }) .collect(), } @@ -163,6 +164,7 @@ mod tests { fixes: vec![], taint_flow: None, is_suppressed: false, + enclosing_function: None, }, Violation { start: Position { line: 10, col: 12 }, @@ -173,6 +175,7 @@ mod tests { fixes: vec![], taint_flow: None, is_suppressed: false, + enclosing_function: None, }, Violation { start: Position { line: 10, col: 12 }, @@ -183,6 +186,7 @@ mod tests { fixes: vec![], taint_flow: None, is_suppressed: false, + enclosing_function: None, }, ], errors: vec![], @@ -227,6 +231,7 @@ mod tests { fixes: vec![], taint_flow: None, is_suppressed: false, + enclosing_function: None, }, Violation { start: Position { line: 20, col: 1 }, @@ -237,6 +242,7 @@ mod tests { fixes: vec![], taint_flow: None, is_suppressed: true, + enclosing_function: None, }, ], errors: vec![], diff --git a/crates/cli/src/sarif/sarif_utils.rs b/crates/cli/src/sarif/sarif_utils.rs index 5575fa1c8..822bf3c0b 100644 --- a/crates/cli/src/sarif/sarif_utils.rs +++ b/crates/cli/src/sarif/sarif_utils.rs @@ -22,9 +22,10 @@ use secrets::model::secret_result::{SecretResult, SecretValidationStatus, Valida use secrets::model::secret_rule::SecretRule; use serde_sarif::sarif::{ self, Artifact, ArtifactBuilder, ArtifactChangeBuilder, ArtifactLocationBuilder, FixBuilder, - LocationBuilder, MessageBuilder, PhysicalLocationBuilder, PropertyBagBuilder, RegionBuilder, - Replacement, ReportingDescriptor, Result as SarifResult, ResultBuilder, RunBuilder, Sarif, - SarifBuilder, SuppressionBuilder, Tool, ToolBuilder, ToolComponent, ToolComponentBuilder, + LocationBuilder, LogicalLocationBuilder, MessageBuilder, PhysicalLocationBuilder, + PropertyBagBuilder, RegionBuilder, Replacement, ReportingDescriptor, Result as SarifResult, + ResultBuilder, RunBuilder, Sarif, SarifBuilder, SuppressionBuilder, Tool, ToolBuilder, + ToolComponent, ToolComponentBuilder, }; use crate::file_utils::get_fingerprint_for_violation; @@ -196,6 +197,7 @@ impl SarifRuleResult { fixes: vec![], taint_flow: None, is_suppressed: r.is_suppressed, + enclosing_function: None, }, r.validation_status.clone(), ) @@ -638,21 +640,28 @@ fn generate_results( .map(move |sarif_violation| { let violation = sarif_violation.get_violation(); // if we find the rule for this violation, get the id, level and category - let location = LocationBuilder::default() - .physical_location( - PhysicalLocationBuilder::default() - .artifact_location(artifact_loc.clone()) - .region( - RegionBuilder::default() - .start_line(violation.start.line) - .start_column(violation.start.col) - .end_line(violation.end.line) - .end_column(violation.end.col) - .build()?, - ) - .build()?, - ) - .build()?; + let mut location_builder = LocationBuilder::default(); + location_builder.physical_location( + PhysicalLocationBuilder::default() + .artifact_location(artifact_loc.clone()) + .region( + RegionBuilder::default() + .start_line(violation.start.line) + .start_column(violation.start.col) + .end_line(violation.end.line) + .end_column(violation.end.col) + .build()?, + ) + .build()?, + ); + if let Some(ref ef) = violation.enclosing_function { + location_builder.logical_locations(vec![LogicalLocationBuilder::default() + .name(ef.name.clone()) + .fully_qualified_name(ef.fully_qualified_name.clone()) + .kind("function".to_string()) + .build()?]); + } + let location = location_builder.build()?; let fixes: Vec = violation .fixes @@ -965,7 +974,7 @@ mod tests { use super::*; use assert_json_diff::{assert_json_eq, assert_json_include}; use common::model::position::{Position, PositionBuilder, Region}; - use kernel::model::violation::{Fix, Violation}; + use kernel::model::violation::{EnclosingFunction, Fix, Violation}; use kernel::model::{ common::Language, rule::{RuleBuilder, RuleCategory, RuleResultBuilder, RuleSeverity, RuleType}, @@ -996,6 +1005,7 @@ mod tests { fixes: vec![], taint_flow: None, is_suppressed: false, + enclosing_function: None, })); // good location in the violation location and no fixes @@ -1008,6 +1018,7 @@ mod tests { fixes: vec![], taint_flow: None, is_suppressed: false, + enclosing_function: None, })); // bad location in the fixes location @@ -1028,6 +1039,7 @@ mod tests { }], taint_flow: None, is_suppressed: false, + enclosing_function: None, })); // good location everywhere @@ -1048,6 +1060,7 @@ mod tests { }], taint_flow: None, is_suppressed: false, + enclosing_function: None, })); } @@ -1116,6 +1129,7 @@ mod tests { fixes: vec![], taint_flow: Some(vec![region0, region1, region2]), is_suppressed: false, + enclosing_function: None, }; let rule_result_single_region = RuleResultBuilder::default() @@ -1298,6 +1312,108 @@ mod tests { assert!(validate_data(&sarif_report_to_string)); } + #[test] + fn test_generate_sarif_report_logical_location() { + let rule = RuleBuilder::default() + .name("my-rule".to_string()) + .description_base64(None) + .language(Language::Python) + .checksum("abc".to_string()) + .pattern(None) + .tree_sitter_query_base64(None) + .category(RuleCategory::BestPractices) + .code_base64("Zm9v".to_string()) + .short_description_base64(None) + .entity_checked(None) + .rule_type(RuleType::TreeSitterQuery) + .severity(RuleSeverity::Error) + .cwe(None) + .arguments(vec![]) + .tests(vec![]) + .is_testing(false) + .documentation_url(None) + .build() + .unwrap(); + + let violation_with_method = Violation { + start: Position { line: 10, col: 1 }, + end: Position { line: 10, col: 20 }, + message: "some violation".to_string(), + severity: RuleSeverity::Error, + category: RuleCategory::BestPractices, + fixes: vec![], + taint_flow: None, + is_suppressed: false, + enclosing_function: Some(EnclosingFunction { + name: "my_method".to_string(), + fully_qualified_name: "def my_method(self)".to_string(), + }), + }; + let violation_without_method = Violation { + start: Position { line: 20, col: 1 }, + end: Position { line: 20, col: 5 }, + message: "another violation".to_string(), + severity: RuleSeverity::Error, + category: RuleCategory::BestPractices, + fixes: vec![], + taint_flow: None, + is_suppressed: false, + enclosing_function: None, + }; + + let rule_result = RuleResult { + rule_name: "my-rule".to_string(), + filename: "myfile.py".to_string(), + violations: vec![violation_with_method, violation_without_method], + errors: vec![], + execution_error: None, + output: None, + execution_time_ms: 0, + parsing_time_ms: 0, + query_node_time_ms: 0, + }; + + let sarif_report = generate_sarif_report( + &[rule.into()], + &[rule_result.try_into().unwrap()], + &"mydir".to_string(), + SarifReportMetadata { + add_git_info: false, + debug: false, + config_digest: "abc".to_string(), + diff_aware_parameters: None, + execution_time_secs: 0, + }, + &Default::default(), + ) + .expect("generate sarif report"); + + let sarif_json = serde_json::to_value(sarif_report).unwrap(); + + // Violation with enclosing_function: logicalLocations must carry name, fullyQualifiedName, and kind. + let logical_locations = sarif_json + .pointer("/runs/0/results/0/locations/0/logicalLocations") + .expect("logicalLocations should be present when enclosing_function is set"); + assert_json_include!( + actual: logical_locations, + expected: serde_json::json!([{ + "kind": "function", + "name": "my_method", + "fullyQualifiedName": "def my_method(self)" + }]) + ); + + // Violation without enclosing_function: no logicalLocations key at all. + let no_logical_locations = + sarif_json.pointer("/runs/0/results/1/locations/0/logicalLocations"); + assert!( + no_logical_locations.is_none(), + "logicalLocations should be absent when enclosing_function is None" + ); + + assert!(validate_data(&sarif_json)); + } + // Ensure that diff-aware scanning information are correctly surfaced #[test] fn test_generate_sarif_diff_aware_scanning() { @@ -1956,6 +2072,7 @@ mod tests { fixes: vec![], taint_flow: None, is_suppressed: false, + enclosing_function: None, }; let rr = RuleResult { rule_name: format!("rule-{idx}"), @@ -2049,6 +2166,7 @@ mod tests { fixes: vec![], taint_flow: None, is_suppressed: false, + enclosing_function: None, }; let rule_results = [TEST_FILE_PATH, NON_TEST_FILE_PATH] .into_iter() From 1403ef54e76f97a4fdd5ab37c7a3be6aadf3d503 Mon Sep 17 00:00:00 2001 From: Xabier Aldama Date: Mon, 20 Apr 2026 15:03:48 +0200 Subject: [PATCH 04/19] Add integration tests for method name detection --- misc/integration-test-method-name.sh | 201 +++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100755 misc/integration-test-method-name.sh diff --git a/misc/integration-test-method-name.sh b/misc/integration-test-method-name.sh new file mode 100755 index 000000000..15560f9b0 --- /dev/null +++ b/misc/integration-test-method-name.sh @@ -0,0 +1,201 @@ +#!/bin/bash + +# Integration test: verify that method names are populated in SARIF logicalLocations +# for languages that implement enclosing-function detection. +# +# Each language block: +# 1. Clones a representative repo +# 2. Runs the analyzer with relevant rulesets +# 3. Asserts that at least one violation has a logicalLocations entry (i.e. method_name was resolved) +# +# If this test fails for a previously-passing language, it means enclosing-function +# detection broke for that language. If a newly-supported language never appears here, +# add it so that coverage is enforced going forward. + +set -euo pipefail + +ANALYZER="./target/release-dev/datadog-static-analyzer" + +cargo fetch +cargo build --locked --profile release-dev --bin datadog-static-analyzer + +# --------------------------------------------------------------------------- +# Helper: count results that carry at least one logicalLocations entry. +# --------------------------------------------------------------------------- +count_with_method() { + local results_file="$1" + jq '[.runs[0].results[] | + select(any(.locations[]?; (.logicalLocations // []) | length > 0)) + ] | length' "${results_file}" +} + +# --------------------------------------------------------------------------- +# Python – django-realworld-example-app +# --------------------------------------------------------------------------- +echo "=== Python: method-name detection ===" +PY_DIR=$(mktemp -d) +git clone --depth=1 https://github.com/gothinkster/django-realworld-example-app.git "${PY_DIR}" + +cat > "${PY_DIR}/code-security.datadog.yaml" <<'YAML' +schema-version: v1.0 +sast: + use-default-rulesets: false + use-rulesets: + - python-security + - python-best-practices + - python-django +YAML + +"${ANALYZER}" --directory "${PY_DIR}" -o "${PY_DIR}/results.json" -f sarif + +TOTAL=$(jq '.runs[0].results | length' "${PY_DIR}/results.json") +WITH_METHOD=$(count_with_method "${PY_DIR}/results.json") + +echo "Python: ${WITH_METHOD}/${TOTAL} violations have a method name" + +if [ "${TOTAL}" -lt 1 ]; then + echo "FAIL: no Python violations found – ruleset may be empty or repo changed" + exit 1 +fi +if [ "${WITH_METHOD}" -lt 1 ]; then + echo "FAIL: no Python violations carry a logicalLocations/method name" + exit 1 +fi + +# --------------------------------------------------------------------------- +# JavaScript / TypeScript – juice-shop +# --------------------------------------------------------------------------- +echo "=== JavaScript/TypeScript: method-name detection ===" +JS_DIR=$(mktemp -d) +git clone --depth=1 https://github.com/juice-shop/juice-shop.git "${JS_DIR}" + +cat > "${JS_DIR}/code-security.datadog.yaml" <<'YAML' +schema-version: v1.0 +sast: + use-default-rulesets: false + use-rulesets: + - javascript-best-practices + - typescript-best-practices + - javascript-common-security + - typescript-common-security + - javascript-node-security + - typescript-node-security +YAML + +"${ANALYZER}" --directory "${JS_DIR}" -o "${JS_DIR}/results.json" -f sarif + +TOTAL=$(jq '.runs[0].results | length' "${JS_DIR}/results.json") +WITH_METHOD=$(count_with_method "${JS_DIR}/results.json") + +echo "JS/TS: ${WITH_METHOD}/${TOTAL} violations have a method name" + +if [ "${TOTAL}" -lt 1 ]; then + echo "FAIL: no JS/TS violations found – ruleset may be empty or repo changed" + exit 1 +fi +if [ "${WITH_METHOD}" -lt 1 ]; then + echo "FAIL: no JS/TS violations carry a logicalLocations/method name" + exit 1 +fi + +# --------------------------------------------------------------------------- +# Java – OWASP Benchmark +# --------------------------------------------------------------------------- +echo "=== Java: method-name detection ===" +JAVA_DIR=$(mktemp -d) +git clone --depth=1 https://github.com/OWASP-Benchmark/BenchmarkJava.git "${JAVA_DIR}" + +cat > "${JAVA_DIR}/code-security.datadog.yaml" <<'YAML' +schema-version: v1.0 +sast: + use-default-rulesets: false + use-rulesets: + - java-security + - java-best-practices +YAML + +"${ANALYZER}" --directory "${JAVA_DIR}" -o "${JAVA_DIR}/results.json" -f sarif + +TOTAL=$(jq '.runs[0].results | length' "${JAVA_DIR}/results.json") +WITH_METHOD=$(count_with_method "${JAVA_DIR}/results.json") + +echo "Java: ${WITH_METHOD}/${TOTAL} violations have a method name" + +if [ "${TOTAL}" -lt 1 ]; then + echo "FAIL: no Java violations found – ruleset may be empty or repo changed" + exit 1 +fi +if [ "${WITH_METHOD}" -lt 1 ]; then + echo "FAIL: no Java violations carry a logicalLocations/method name" + exit 1 +fi + +# --------------------------------------------------------------------------- +# Go – github-mcp-server +# --------------------------------------------------------------------------- +echo "=== Go: method-name detection ===" +GO_DIR=$(mktemp -d) +git clone --depth=1 https://github.com/github/github-mcp-server.git "${GO_DIR}" + +cat > "${GO_DIR}/code-security.datadog.yaml" <<'YAML' +schema-version: v1.0 +sast: + use-default-rulesets: false + use-rulesets: + - go-security + - go-best-practices +YAML + +"${ANALYZER}" --directory "${GO_DIR}" -o "${GO_DIR}/results.json" -f sarif + +TOTAL=$(jq '.runs[0].results | length' "${GO_DIR}/results.json") +WITH_METHOD=$(count_with_method "${GO_DIR}/results.json") + +echo "Go: ${WITH_METHOD}/${TOTAL} violations have a method name" + +if [ "${TOTAL}" -lt 1 ]; then + echo "FAIL: no Go violations found – ruleset may be empty or repo changed" + exit 1 +fi +if [ "${WITH_METHOD}" -lt 1 ]; then + echo "FAIL: no Go violations carry a logicalLocations/method name" + exit 1 +fi + +# --------------------------------------------------------------------------- +# C# – unity-mcp +# --------------------------------------------------------------------------- +echo "=== C#: method-name detection ===" +CS_DIR=$(mktemp -d) +git clone --depth=1 https://github.com/CoplayDev/unity-mcp.git "${CS_DIR}" + +cat > "${CS_DIR}/code-security.datadog.yaml" <<'YAML' +schema-version: v1.0 +sast: + use-default-rulesets: false + use-rulesets: + - csharp-security + - csharp-best-practices +YAML + +"${ANALYZER}" --directory "${CS_DIR}" -o "${CS_DIR}/results.json" -f sarif + +TOTAL=$(jq '.runs[0].results | length' "${CS_DIR}/results.json") +WITH_METHOD=$(count_with_method "${CS_DIR}/results.json") + +echo "C#: ${WITH_METHOD}/${TOTAL} violations have a method name" + +if [ "${TOTAL}" -lt 1 ]; then + echo "FAIL: no C# violations found – ruleset may be empty or repo changed" + exit 1 +fi +if [ "${WITH_METHOD}" -lt 1 ]; then + echo "FAIL: no C# violations carry a logicalLocations/method name" + exit 1 +fi + +# --------------------------------------------------------------------------- +# Done +# --------------------------------------------------------------------------- +echo "All method-name integration tests passed" +exit 0 From 5ae9806fee151913433e262d7eee52a526f0d953 Mon Sep 17 00:00:00 2001 From: Xabier Aldama Date: Mon, 20 Apr 2026 15:48:31 +0200 Subject: [PATCH 05/19] Improve doc comment for fully_qualified_name --- crates/static-analysis-kernel/src/model/violation.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/static-analysis-kernel/src/model/violation.rs b/crates/static-analysis-kernel/src/model/violation.rs index 2e2c4b915..8bcaf6e43 100644 --- a/crates/static-analysis-kernel/src/model/violation.rs +++ b/crates/static-analysis-kernel/src/model/violation.rs @@ -9,8 +9,12 @@ use serde::{Deserialize, Serialize}; pub struct EnclosingFunction { /// Simple identifier (e.g. `handle`, `doSomething`). pub name: String, - /// Service-definition qualified name: `ClassName.methodName` when the function belongs to a - /// class or struct, or just `methodName` for top-level functions. + /// Language-specific fully-qualified function or method signature. The format varies by language: + /// + /// - **Java**: `package.ClassName#methodName(ParamType1, ParamType2):ReturnType` + /// - **Go**: `package.FunctionName` / `package.TypeName.MethodName` + /// - **Python / JS / TS**: `ClassName.methodName` for methods, `functionName` for top-level functions + /// - **C#**: `Namespace.ClassName.MethodName(ParamType1, ParamType2)` pub fully_qualified_name: String, } From d8cee537ddb7427973243bcc51c420d119387369 Mon Sep 17 00:00:00 2001 From: Xabier Aldama Date: Mon, 20 Apr 2026 15:54:52 +0200 Subject: [PATCH 06/19] Precompute Java file context once per file across violations --- .../src/analysis/ddsa_lib/runtime.rs | 8 +++- .../src/analysis/languages.rs | 36 ++++++++++++++ .../src/analysis/languages/java/methods.rs | 48 +++++++++++++++---- 3 files changed, 81 insertions(+), 11 deletions(-) diff --git a/crates/static-analysis-kernel/src/analysis/ddsa_lib/runtime.rs b/crates/static-analysis-kernel/src/analysis/ddsa_lib/runtime.rs index c0994630e..d4dd6d83c 100644 --- a/crates/static-analysis-kernel/src/analysis/ddsa_lib/runtime.rs +++ b/crates/static-analysis-kernel/src/analysis/ddsa_lib/runtime.rs @@ -197,16 +197,20 @@ impl JsRuntime { let execution_time = now.elapsed(); + let file_ctx = analysis::languages::LanguageFileContext::new( + source_text.as_ref(), + source_tree.as_ref(), + &rule.language, + ); let violations = js_violations .into_iter() .map(|v| v.into_violation(rule.severity, rule.category)) .map(|mut v| { - v.enclosing_function = analysis::languages::find_enclosing_function_with_tree( + v.enclosing_function = file_ctx.find_enclosing_function( source_text.as_ref(), source_tree.as_ref(), v.start.line, v.start.col, - &rule.language, ); v }) diff --git a/crates/static-analysis-kernel/src/analysis/languages.rs b/crates/static-analysis-kernel/src/analysis/languages.rs index 9a0ccd420..7a9d9982a 100644 --- a/crates/static-analysis-kernel/src/analysis/languages.rs +++ b/crates/static-analysis-kernel/src/analysis/languages.rs @@ -12,6 +12,42 @@ pub mod typescript; use crate::model::common::Language; use crate::model::violation::EnclosingFunction; +/// Per-file context precomputed once and reused across all violations in the same source file. +/// +/// Construct with [`LanguageFileContext::new`] before iterating over violations, then call +/// [`LanguageFileContext::find_enclosing_function`] for each one. +pub enum LanguageFileContext { + Java(java::methods::JavaFileContext), + /// Language has no precomputed context; falls back to the regular per-call dispatch. + Other(Language), +} + +impl LanguageFileContext { + pub fn new(source_code: &str, tree: &tree_sitter::Tree, language: &Language) -> Self { + match language { + Language::Java => { + Self::Java(java::methods::JavaFileContext::new(source_code, tree)) + } + lang => Self::Other(*lang), + } + } + + pub fn find_enclosing_function( + &self, + source_code: &str, + tree: &tree_sitter::Tree, + line: u32, + col: u32, + ) -> Option { + match self { + Self::Java(ctx) => { + java::methods::find_enclosing_function_with_context(source_code, tree, line, col, ctx) + } + Self::Other(lang) => find_enclosing_function_with_tree(source_code, tree, line, col, lang), + } + } +} + /// Returns the enclosing function for the given source position, or `None` if the position /// is not inside any named function or the language has no implementation. /// diff --git a/crates/static-analysis-kernel/src/analysis/languages/java/methods.rs b/crates/static-analysis-kernel/src/analysis/languages/java/methods.rs index b950b5a55..01476132a 100644 --- a/crates/static-analysis-kernel/src/analysis/languages/java/methods.rs +++ b/crates/static-analysis-kernel/src/analysis/languages/java/methods.rs @@ -9,6 +9,22 @@ use crate::analysis::tree_sitter::get_tree; use crate::model::common::Language; use crate::model::violation::EnclosingFunction; +/// Per-file data that is expensive to compute and reused across violations in the same source. +pub struct JavaFileContext { + package: Option, + import_map: HashMap, +} + +impl JavaFileContext { + pub fn new(source_code: &str, tree: &tree_sitter::Tree) -> Self { + let root = tree.root_node(); + Self { + package: find_package(source_code, root), + import_map: build_import_map(source_code, root), + } + } +} + /// Returns the enclosing method or constructor for the given source position, or `None` if the /// position is not inside any method. /// @@ -32,11 +48,28 @@ pub fn find_enclosing_function( /// Types are resolved to fully qualified names using the file's import declarations. /// Types from `java.lang` (String, Integer, etc.) are always resolved. Types only /// reachable via wildcard imports are returned as simple names. +/// +/// When called repeatedly for violations in the same file, prefer +/// [`find_enclosing_function_with_context`] to avoid re-scanning the file for +/// package and import declarations on every call. pub fn find_enclosing_function_with_tree( source_code: &str, tree: &tree_sitter::Tree, line: u32, col: u32, +) -> Option { + let ctx = JavaFileContext::new(source_code, tree); + find_enclosing_function_with_context(source_code, tree, line, col, &ctx) +} + +/// Like [`find_enclosing_function_with_tree`] but reuses a [`JavaFileContext`] that was +/// precomputed once for the file, avoiding repeated scans for the package and import map. +pub fn find_enclosing_function_with_context( + source_code: &str, + tree: &tree_sitter::Tree, + line: u32, + col: u32, + ctx: &JavaFileContext, ) -> Option { let point = tree_sitter::Point { row: line.saturating_sub(1) as usize, @@ -51,7 +84,7 @@ pub fn find_enclosing_function_with_tree( let name = node .child_by_field_name("name") .map(|n| ts_node_text(source_code, n).to_owned())?; - let fully_qualified_name = build_fqn(source_code, tree, node, &name); + let fully_qualified_name = build_fqn(source_code, ctx, node, &name); return Some(EnclosingFunction { name, fully_qualified_name, @@ -69,12 +102,10 @@ pub fn find_enclosing_function_with_tree( /// Constructors have no return type and omit the `:ReturnType` suffix. fn build_fqn( source_code: &str, - tree: &tree_sitter::Tree, + ctx: &JavaFileContext, method_node: tree_sitter::Node, method_name: &str, ) -> String { - let root = tree.root_node(); - let package = find_package(source_code, root); let class_kinds = &[ "class_declaration", "interface_declaration", @@ -82,24 +113,23 @@ fn build_fqn( ]; let class_name = enclosing_class_name(source_code, method_node, class_kinds); - let fqn_class = match (package.as_deref(), class_name) { + let fqn_class = match (ctx.package.as_deref(), class_name) { (Some(pkg), Some(cls)) => format!("{pkg}.{cls}"), (None, Some(cls)) => cls.to_string(), (Some(pkg), None) => pkg.to_string(), (None, None) => String::new(), }; - let import_map = build_import_map(source_code, root); - let pkg = package.as_deref(); + let pkg = ctx.package.as_deref(); // method_declaration has a `type` field; constructor_declaration does not. let return_type = method_node .child_by_field_name("type") - .map(|t| resolve_type(source_code, t, &import_map, pkg)); + .map(|t| resolve_type(source_code, t, &ctx.import_map, pkg)); let param_types = method_node .child_by_field_name("parameters") - .map(|p| extract_param_types(source_code, p, &import_map, pkg)) + .map(|p| extract_param_types(source_code, p, &ctx.import_map, pkg)) .unwrap_or_default(); let params_str = param_types.join(", "); From 1a6d99859779e66cbb2895bd6fb294cca93c9367 Mon Sep 17 00:00:00 2001 From: Xabier Aldama Date: Mon, 20 Apr 2026 15:58:15 +0200 Subject: [PATCH 07/19] Fix formatting --- .../src/analysis/languages.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/crates/static-analysis-kernel/src/analysis/languages.rs b/crates/static-analysis-kernel/src/analysis/languages.rs index 7a9d9982a..359649bdd 100644 --- a/crates/static-analysis-kernel/src/analysis/languages.rs +++ b/crates/static-analysis-kernel/src/analysis/languages.rs @@ -25,9 +25,7 @@ pub enum LanguageFileContext { impl LanguageFileContext { pub fn new(source_code: &str, tree: &tree_sitter::Tree, language: &Language) -> Self { match language { - Language::Java => { - Self::Java(java::methods::JavaFileContext::new(source_code, tree)) - } + Language::Java => Self::Java(java::methods::JavaFileContext::new(source_code, tree)), lang => Self::Other(*lang), } } @@ -40,10 +38,16 @@ impl LanguageFileContext { col: u32, ) -> Option { match self { - Self::Java(ctx) => { - java::methods::find_enclosing_function_with_context(source_code, tree, line, col, ctx) + Self::Java(ctx) => java::methods::find_enclosing_function_with_context( + source_code, + tree, + line, + col, + ctx, + ), + Self::Other(lang) => { + find_enclosing_function_with_tree(source_code, tree, line, col, lang) } - Self::Other(lang) => find_enclosing_function_with_tree(source_code, tree, line, col, lang), } } } From dd3aea22dc827c451cc5e5a14dd8a08d5990aa85 Mon Sep 17 00:00:00 2001 From: Xabier Aldama Date: Mon, 20 Apr 2026 16:00:57 +0200 Subject: [PATCH 08/19] Scope integration test to Java only --- misc/integration-test-method-name.sh | 144 +-------------------------- 1 file changed, 2 insertions(+), 142 deletions(-) diff --git a/misc/integration-test-method-name.sh b/misc/integration-test-method-name.sh index 15560f9b0..2521822e4 100755 --- a/misc/integration-test-method-name.sh +++ b/misc/integration-test-method-name.sh @@ -1,16 +1,9 @@ #!/bin/bash # Integration test: verify that method names are populated in SARIF logicalLocations -# for languages that implement enclosing-function detection. +# for Java, which is the first language with enclosing-function detection. # -# Each language block: -# 1. Clones a representative repo -# 2. Runs the analyzer with relevant rulesets -# 3. Asserts that at least one violation has a logicalLocations entry (i.e. method_name was resolved) -# -# If this test fails for a previously-passing language, it means enclosing-function -# detection broke for that language. If a newly-supported language never appears here, -# add it so that coverage is enforced going forward. +# Each language block added here as support is implemented. set -euo pipefail @@ -29,75 +22,6 @@ count_with_method() { ] | length' "${results_file}" } -# --------------------------------------------------------------------------- -# Python – django-realworld-example-app -# --------------------------------------------------------------------------- -echo "=== Python: method-name detection ===" -PY_DIR=$(mktemp -d) -git clone --depth=1 https://github.com/gothinkster/django-realworld-example-app.git "${PY_DIR}" - -cat > "${PY_DIR}/code-security.datadog.yaml" <<'YAML' -schema-version: v1.0 -sast: - use-default-rulesets: false - use-rulesets: - - python-security - - python-best-practices - - python-django -YAML - -"${ANALYZER}" --directory "${PY_DIR}" -o "${PY_DIR}/results.json" -f sarif - -TOTAL=$(jq '.runs[0].results | length' "${PY_DIR}/results.json") -WITH_METHOD=$(count_with_method "${PY_DIR}/results.json") - -echo "Python: ${WITH_METHOD}/${TOTAL} violations have a method name" - -if [ "${TOTAL}" -lt 1 ]; then - echo "FAIL: no Python violations found – ruleset may be empty or repo changed" - exit 1 -fi -if [ "${WITH_METHOD}" -lt 1 ]; then - echo "FAIL: no Python violations carry a logicalLocations/method name" - exit 1 -fi - -# --------------------------------------------------------------------------- -# JavaScript / TypeScript – juice-shop -# --------------------------------------------------------------------------- -echo "=== JavaScript/TypeScript: method-name detection ===" -JS_DIR=$(mktemp -d) -git clone --depth=1 https://github.com/juice-shop/juice-shop.git "${JS_DIR}" - -cat > "${JS_DIR}/code-security.datadog.yaml" <<'YAML' -schema-version: v1.0 -sast: - use-default-rulesets: false - use-rulesets: - - javascript-best-practices - - typescript-best-practices - - javascript-common-security - - typescript-common-security - - javascript-node-security - - typescript-node-security -YAML - -"${ANALYZER}" --directory "${JS_DIR}" -o "${JS_DIR}/results.json" -f sarif - -TOTAL=$(jq '.runs[0].results | length' "${JS_DIR}/results.json") -WITH_METHOD=$(count_with_method "${JS_DIR}/results.json") - -echo "JS/TS: ${WITH_METHOD}/${TOTAL} violations have a method name" - -if [ "${TOTAL}" -lt 1 ]; then - echo "FAIL: no JS/TS violations found – ruleset may be empty or repo changed" - exit 1 -fi -if [ "${WITH_METHOD}" -lt 1 ]; then - echo "FAIL: no JS/TS violations carry a logicalLocations/method name" - exit 1 -fi - # --------------------------------------------------------------------------- # Java – OWASP Benchmark # --------------------------------------------------------------------------- @@ -130,70 +54,6 @@ if [ "${WITH_METHOD}" -lt 1 ]; then exit 1 fi -# --------------------------------------------------------------------------- -# Go – github-mcp-server -# --------------------------------------------------------------------------- -echo "=== Go: method-name detection ===" -GO_DIR=$(mktemp -d) -git clone --depth=1 https://github.com/github/github-mcp-server.git "${GO_DIR}" - -cat > "${GO_DIR}/code-security.datadog.yaml" <<'YAML' -schema-version: v1.0 -sast: - use-default-rulesets: false - use-rulesets: - - go-security - - go-best-practices -YAML - -"${ANALYZER}" --directory "${GO_DIR}" -o "${GO_DIR}/results.json" -f sarif - -TOTAL=$(jq '.runs[0].results | length' "${GO_DIR}/results.json") -WITH_METHOD=$(count_with_method "${GO_DIR}/results.json") - -echo "Go: ${WITH_METHOD}/${TOTAL} violations have a method name" - -if [ "${TOTAL}" -lt 1 ]; then - echo "FAIL: no Go violations found – ruleset may be empty or repo changed" - exit 1 -fi -if [ "${WITH_METHOD}" -lt 1 ]; then - echo "FAIL: no Go violations carry a logicalLocations/method name" - exit 1 -fi - -# --------------------------------------------------------------------------- -# C# – unity-mcp -# --------------------------------------------------------------------------- -echo "=== C#: method-name detection ===" -CS_DIR=$(mktemp -d) -git clone --depth=1 https://github.com/CoplayDev/unity-mcp.git "${CS_DIR}" - -cat > "${CS_DIR}/code-security.datadog.yaml" <<'YAML' -schema-version: v1.0 -sast: - use-default-rulesets: false - use-rulesets: - - csharp-security - - csharp-best-practices -YAML - -"${ANALYZER}" --directory "${CS_DIR}" -o "${CS_DIR}/results.json" -f sarif - -TOTAL=$(jq '.runs[0].results | length' "${CS_DIR}/results.json") -WITH_METHOD=$(count_with_method "${CS_DIR}/results.json") - -echo "C#: ${WITH_METHOD}/${TOTAL} violations have a method name" - -if [ "${TOTAL}" -lt 1 ]; then - echo "FAIL: no C# violations found – ruleset may be empty or repo changed" - exit 1 -fi -if [ "${WITH_METHOD}" -lt 1 ]; then - echo "FAIL: no C# violations carry a logicalLocations/method name" - exit 1 -fi - # --------------------------------------------------------------------------- # Done # --------------------------------------------------------------------------- From 90c1484de863e6c19ed531d3fc1dd1d1debc3a42 Mon Sep 17 00:00:00 2001 From: Xabier Aldama Date: Tue, 21 Apr 2026 09:25:41 +0200 Subject: [PATCH 09/19] List all languages explicitly in enclosing-function dispatch --- .../src/analysis/languages.rs | 90 +++++++++---------- 1 file changed, 42 insertions(+), 48 deletions(-) diff --git a/crates/static-analysis-kernel/src/analysis/languages.rs b/crates/static-analysis-kernel/src/analysis/languages.rs index 359649bdd..b6ff77442 100644 --- a/crates/static-analysis-kernel/src/analysis/languages.rs +++ b/crates/static-analysis-kernel/src/analysis/languages.rs @@ -65,7 +65,27 @@ pub fn find_enclosing_function( ) -> Option { match language { Language::Java => java::methods::find_enclosing_function(source_code, line, col), - _ => None, + Language::Python + | Language::Go + | Language::JavaScript + | Language::TypeScript + | Language::Csharp + | Language::Dockerfile + | Language::Elixir + | Language::Json + | Language::Kotlin + | Language::Ruby + | Language::Rust + | Language::Swift + | Language::Terraform + | Language::Yaml + | Language::Starlark + | Language::Bash + | Language::PHP + | Language::Markdown + | Language::Apex + | Language::R + | Language::SQL => None, } } @@ -82,7 +102,27 @@ pub fn find_enclosing_function_with_tree( Language::Java => { java::methods::find_enclosing_function_with_tree(source_code, tree, line, col) } - _ => None, + Language::Python + | Language::Go + | Language::JavaScript + | Language::TypeScript + | Language::Csharp + | Language::Dockerfile + | Language::Elixir + | Language::Json + | Language::Kotlin + | Language::Ruby + | Language::Rust + | Language::Swift + | Language::Terraform + | Language::Yaml + | Language::Starlark + | Language::Bash + | Language::PHP + | Language::Markdown + | Language::Apex + | Language::R + | Language::SQL => None, } } @@ -115,49 +155,3 @@ pub(crate) fn ts_node_text<'text>(parsed_text: &'text str, node: tree_sitter::No node.utf8_text(parsed_text.as_bytes()) .expect("node should be from `parsed_text`'s tree") } - -#[cfg(test)] -mod tests { - use crate::model::common::{Language, ALL_LANGUAGES}; - - // Languages with an enclosing-function implementation in this module. - const SUPPORTED: &[Language] = &[Language::Java]; - - // Languages that intentionally have no implementation yet. - // When adding a new language to the analyzer, add it here (no detection) or to - // SUPPORTED (detection implemented) — leaving it out causes this test to fail. - const NOT_IMPLEMENTED: &[Language] = &[ - Language::Python, - Language::Go, - Language::JavaScript, - Language::TypeScript, - Language::Csharp, - Language::Dockerfile, - Language::Elixir, - Language::Json, - Language::Kotlin, - Language::Ruby, - Language::Rust, - Language::Swift, - Language::Terraform, - Language::Yaml, - Language::Starlark, - Language::Bash, - Language::PHP, - Language::Markdown, - Language::Apex, - Language::R, - Language::SQL, - ]; - - #[test] - fn all_languages_accounted_for() { - for lang in ALL_LANGUAGES { - assert!( - SUPPORTED.contains(lang) || NOT_IMPLEMENTED.contains(lang), - "{lang:?} is not listed in SUPPORTED or NOT_IMPLEMENTED — \ - either add enclosing-function detection for it or add it to NOT_IMPLEMENTED" - ); - } - } -} From 3249aa68d241c057ea00e9a3d9704e1a577ef8ec Mon Sep 17 00:00:00 2001 From: Xabier Aldama Date: Tue, 21 Apr 2026 10:12:41 +0200 Subject: [PATCH 10/19] Use FQMN format for enclosing function (params, no return type) --- .../src/analysis/languages/java/methods.rs | 38 ++++++++----------- .../src/model/violation.rs | 4 +- 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/crates/static-analysis-kernel/src/analysis/languages/java/methods.rs b/crates/static-analysis-kernel/src/analysis/languages/java/methods.rs index 01476132a..ac76d926e 100644 --- a/crates/static-analysis-kernel/src/analysis/languages/java/methods.rs +++ b/crates/static-analysis-kernel/src/analysis/languages/java/methods.rs @@ -42,8 +42,8 @@ pub fn find_enclosing_function( /// Returns the enclosing method or constructor for the given source position. /// See [`find_enclosing_function`] for documentation. /// -/// The `fullyQualifiedName` follows the standard Java FQN format: -/// `package.ClassName#methodName(ParamType1, ParamType2):ReturnType` +/// The `fully_qualified_name` follows the FQMN format: +/// `package.ClassName.methodName(ParamType1, ParamType2)` /// /// Types are resolved to fully qualified names using the file's import declarations. /// Types from `java.lang` (String, Integer, etc.) are always resolved. Types only @@ -96,10 +96,8 @@ pub fn find_enclosing_function_with_context( } } -/// Builds the fully qualified method name in the format: -/// `package.ClassName#methodName(ParamType1, ParamType2):ReturnType` -/// -/// Constructors have no return type and omit the `:ReturnType` suffix. +/// Builds the fully qualified method name (FQMN) in the format: +/// `package.ClassName.methodName(ParamType1, ParamType2)` fn build_fqn( source_code: &str, ctx: &JavaFileContext, @@ -122,11 +120,6 @@ fn build_fqn( let pkg = ctx.package.as_deref(); - // method_declaration has a `type` field; constructor_declaration does not. - let return_type = method_node - .child_by_field_name("type") - .map(|t| resolve_type(source_code, t, &ctx.import_map, pkg)); - let param_types = method_node .child_by_field_name("parameters") .map(|p| extract_param_types(source_code, p, &ctx.import_map, pkg)) @@ -134,9 +127,10 @@ fn build_fqn( let params_str = param_types.join(", "); - match return_type { - Some(rt) => format!("{fqn_class}#{method_name}({params_str}):{rt}"), - None => format!("{fqn_class}#{method_name}({params_str})"), + if fqn_class.is_empty() { + format!("{method_name}({params_str})") + } else { + format!("{fqn_class}.{method_name}({params_str})") } } @@ -375,7 +369,7 @@ class Foo { } } "; - assert_eq!(find(src, 3, 9), ef("doSomething", "Foo#doSomething():void")); + assert_eq!(find(src, 3, 9), ef("doSomething", "Foo.doSomething()")); } #[test] @@ -387,7 +381,7 @@ class Foo { } } "; - assert_eq!(find(src, 3, 9), ef("Foo", "Foo#Foo()")); + assert_eq!(find(src, 3, 9), ef("Foo", "Foo.Foo()")); } #[test] @@ -402,7 +396,7 @@ class Foo { "; assert_eq!( find(src, 4, 9), - ef("doSomething", "com.example.Foo#doSomething():void") + ef("doSomething", "com.example.Foo.doSomething()") ); } @@ -418,7 +412,7 @@ class Foo { "; assert_eq!( find(src, 3, 9), - ef("handle", "Foo#handle(java.lang.String):void") + ef("handle", "Foo.handle(java.lang.String)") ); } @@ -437,7 +431,7 @@ class Foo { find(src, 4, 9), ef( "process", - "Foo#process(org.springframework.web.multipart.MultipartFile, org.springframework.ui.Model):java.lang.String" + "Foo.process(org.springframework.web.multipart.MultipartFile, org.springframework.ui.Model)" ) ); } @@ -458,7 +452,7 @@ class DashboardController { find(src, 5, 9), ef( "processSimple", - "org.hdivsamples.controllers.DashboardController#processSimple(org.springframework.web.multipart.MultipartFile, org.springframework.ui.Model):java.lang.String" + "org.hdivsamples.controllers.DashboardController.processSimple(org.springframework.web.multipart.MultipartFile, org.springframework.ui.Model)" ) ); } @@ -474,7 +468,7 @@ class Foo { } } "; - assert_eq!(find(src, 4, 9), ef("doSomething", "Foo#doSomething():void")); + assert_eq!(find(src, 4, 9), ef("doSomething", "Foo.doSomething()")); } #[test] @@ -487,7 +481,7 @@ class Foo { } } "; - assert_eq!(find(src, 3, 9), ef("parse", "Foo#parse():void")); + assert_eq!(find(src, 3, 9), ef("parse", "Foo.parse()")); } #[test] diff --git a/crates/static-analysis-kernel/src/model/violation.rs b/crates/static-analysis-kernel/src/model/violation.rs index 8bcaf6e43..606311ba6 100644 --- a/crates/static-analysis-kernel/src/model/violation.rs +++ b/crates/static-analysis-kernel/src/model/violation.rs @@ -9,9 +9,9 @@ use serde::{Deserialize, Serialize}; pub struct EnclosingFunction { /// Simple identifier (e.g. `handle`, `doSomething`). pub name: String, - /// Language-specific fully-qualified function or method signature. The format varies by language: + /// Fully qualified method name (FQMN). The format varies by language: /// - /// - **Java**: `package.ClassName#methodName(ParamType1, ParamType2):ReturnType` + /// - **Java**: `package.ClassName.methodName(ParamType1, ParamType2)` /// - **Go**: `package.FunctionName` / `package.TypeName.MethodName` /// - **Python / JS / TS**: `ClassName.methodName` for methods, `functionName` for top-level functions /// - **C#**: `Namespace.ClassName.MethodName(ParamType1, ParamType2)` From ed06c28906be0a731c3723ef54bdf26f5e4e25b8 Mon Sep 17 00:00:00 2001 From: Xabier Aldama Date: Tue, 21 Apr 2026 11:02:59 +0200 Subject: [PATCH 11/19] Remove LanguageFileContext optimization (negligible perf gain) --- .../src/analysis/ddsa_lib/runtime.rs | 8 +--- .../src/analysis/languages.rs | 40 ------------------- .../src/analysis/languages/java/methods.rs | 22 ++-------- 3 files changed, 5 insertions(+), 65 deletions(-) diff --git a/crates/static-analysis-kernel/src/analysis/ddsa_lib/runtime.rs b/crates/static-analysis-kernel/src/analysis/ddsa_lib/runtime.rs index d4dd6d83c..c0994630e 100644 --- a/crates/static-analysis-kernel/src/analysis/ddsa_lib/runtime.rs +++ b/crates/static-analysis-kernel/src/analysis/ddsa_lib/runtime.rs @@ -197,20 +197,16 @@ impl JsRuntime { let execution_time = now.elapsed(); - let file_ctx = analysis::languages::LanguageFileContext::new( - source_text.as_ref(), - source_tree.as_ref(), - &rule.language, - ); let violations = js_violations .into_iter() .map(|v| v.into_violation(rule.severity, rule.category)) .map(|mut v| { - v.enclosing_function = file_ctx.find_enclosing_function( + v.enclosing_function = analysis::languages::find_enclosing_function_with_tree( source_text.as_ref(), source_tree.as_ref(), v.start.line, v.start.col, + &rule.language, ); v }) diff --git a/crates/static-analysis-kernel/src/analysis/languages.rs b/crates/static-analysis-kernel/src/analysis/languages.rs index b6ff77442..1bd21446e 100644 --- a/crates/static-analysis-kernel/src/analysis/languages.rs +++ b/crates/static-analysis-kernel/src/analysis/languages.rs @@ -12,46 +12,6 @@ pub mod typescript; use crate::model::common::Language; use crate::model::violation::EnclosingFunction; -/// Per-file context precomputed once and reused across all violations in the same source file. -/// -/// Construct with [`LanguageFileContext::new`] before iterating over violations, then call -/// [`LanguageFileContext::find_enclosing_function`] for each one. -pub enum LanguageFileContext { - Java(java::methods::JavaFileContext), - /// Language has no precomputed context; falls back to the regular per-call dispatch. - Other(Language), -} - -impl LanguageFileContext { - pub fn new(source_code: &str, tree: &tree_sitter::Tree, language: &Language) -> Self { - match language { - Language::Java => Self::Java(java::methods::JavaFileContext::new(source_code, tree)), - lang => Self::Other(*lang), - } - } - - pub fn find_enclosing_function( - &self, - source_code: &str, - tree: &tree_sitter::Tree, - line: u32, - col: u32, - ) -> Option { - match self { - Self::Java(ctx) => java::methods::find_enclosing_function_with_context( - source_code, - tree, - line, - col, - ctx, - ), - Self::Other(lang) => { - find_enclosing_function_with_tree(source_code, tree, line, col, lang) - } - } - } -} - /// Returns the enclosing function for the given source position, or `None` if the position /// is not inside any named function or the language has no implementation. /// diff --git a/crates/static-analysis-kernel/src/analysis/languages/java/methods.rs b/crates/static-analysis-kernel/src/analysis/languages/java/methods.rs index ac76d926e..446362b6c 100644 --- a/crates/static-analysis-kernel/src/analysis/languages/java/methods.rs +++ b/crates/static-analysis-kernel/src/analysis/languages/java/methods.rs @@ -9,14 +9,13 @@ use crate::analysis::tree_sitter::get_tree; use crate::model::common::Language; use crate::model::violation::EnclosingFunction; -/// Per-file data that is expensive to compute and reused across violations in the same source. -pub struct JavaFileContext { +struct JavaFileContext { package: Option, import_map: HashMap, } impl JavaFileContext { - pub fn new(source_code: &str, tree: &tree_sitter::Tree) -> Self { + fn new(source_code: &str, tree: &tree_sitter::Tree) -> Self { let root = tree.root_node(); Self { package: find_package(source_code, root), @@ -49,9 +48,6 @@ pub fn find_enclosing_function( /// Types from `java.lang` (String, Integer, etc.) are always resolved. Types only /// reachable via wildcard imports are returned as simple names. /// -/// When called repeatedly for violations in the same file, prefer -/// [`find_enclosing_function_with_context`] to avoid re-scanning the file for -/// package and import declarations on every call. pub fn find_enclosing_function_with_tree( source_code: &str, tree: &tree_sitter::Tree, @@ -59,18 +55,6 @@ pub fn find_enclosing_function_with_tree( col: u32, ) -> Option { let ctx = JavaFileContext::new(source_code, tree); - find_enclosing_function_with_context(source_code, tree, line, col, &ctx) -} - -/// Like [`find_enclosing_function_with_tree`] but reuses a [`JavaFileContext`] that was -/// precomputed once for the file, avoiding repeated scans for the package and import map. -pub fn find_enclosing_function_with_context( - source_code: &str, - tree: &tree_sitter::Tree, - line: u32, - col: u32, - ctx: &JavaFileContext, -) -> Option { let point = tree_sitter::Point { row: line.saturating_sub(1) as usize, column: col.saturating_sub(1) as usize, @@ -84,7 +68,7 @@ pub fn find_enclosing_function_with_context( let name = node .child_by_field_name("name") .map(|n| ts_node_text(source_code, n).to_owned())?; - let fully_qualified_name = build_fqn(source_code, ctx, node, &name); + let fully_qualified_name = build_fqn(source_code, &ctx, node, &name); return Some(EnclosingFunction { name, fully_qualified_name, From 6bb14e821dc7c9d825c3099dec2e185742b87a29 Mon Sep 17 00:00:00 2001 From: Xabier Aldama Date: Tue, 21 Apr 2026 11:12:40 +0200 Subject: [PATCH 12/19] Pass violation start and end range to find_enclosing_function --- .../src/analysis/ddsa_lib/runtime.rs | 2 ++ .../src/analysis/languages.rs | 16 ++++++---- .../src/analysis/languages/java/methods.rs | 31 ++++++++++++------- 3 files changed, 32 insertions(+), 17 deletions(-) diff --git a/crates/static-analysis-kernel/src/analysis/ddsa_lib/runtime.rs b/crates/static-analysis-kernel/src/analysis/ddsa_lib/runtime.rs index c0994630e..36030d2b1 100644 --- a/crates/static-analysis-kernel/src/analysis/ddsa_lib/runtime.rs +++ b/crates/static-analysis-kernel/src/analysis/ddsa_lib/runtime.rs @@ -206,6 +206,8 @@ impl JsRuntime { source_tree.as_ref(), v.start.line, v.start.col, + v.end.line, + v.end.col, &rule.language, ); v diff --git a/crates/static-analysis-kernel/src/analysis/languages.rs b/crates/static-analysis-kernel/src/analysis/languages.rs index 1bd21446e..c5cdf68f1 100644 --- a/crates/static-analysis-kernel/src/analysis/languages.rs +++ b/crates/static-analysis-kernel/src/analysis/languages.rs @@ -19,12 +19,14 @@ use crate::model::violation::EnclosingFunction; /// If you already have a parsed tree, use [`find_enclosing_function_with_tree`]. pub fn find_enclosing_function( source_code: &str, - line: u32, - col: u32, + start_line: u32, + start_col: u32, + end_line: u32, + end_col: u32, language: &Language, ) -> Option { match language { - Language::Java => java::methods::find_enclosing_function(source_code, line, col), + Language::Java => java::methods::find_enclosing_function(source_code, start_line, start_col, end_line, end_col), Language::Python | Language::Go | Language::JavaScript @@ -54,13 +56,15 @@ pub fn find_enclosing_function( pub fn find_enclosing_function_with_tree( source_code: &str, tree: &tree_sitter::Tree, - line: u32, - col: u32, + start_line: u32, + start_col: u32, + end_line: u32, + end_col: u32, language: &Language, ) -> Option { match language { Language::Java => { - java::methods::find_enclosing_function_with_tree(source_code, tree, line, col) + java::methods::find_enclosing_function_with_tree(source_code, tree, start_line, start_col, end_line, end_col) } Language::Python | Language::Go diff --git a/crates/static-analysis-kernel/src/analysis/languages/java/methods.rs b/crates/static-analysis-kernel/src/analysis/languages/java/methods.rs index 446362b6c..1928ae4f0 100644 --- a/crates/static-analysis-kernel/src/analysis/languages/java/methods.rs +++ b/crates/static-analysis-kernel/src/analysis/languages/java/methods.rs @@ -31,11 +31,14 @@ impl JavaFileContext { /// If you already have a parsed tree, use [`find_enclosing_function_with_tree`]. pub fn find_enclosing_function( source_code: &str, - line: u32, - col: u32, + start_line: u32, + start_col: u32, + end_line: u32, + end_col: u32, ) -> Option { - get_tree(source_code, &Language::Java) - .and_then(|tree| find_enclosing_function_with_tree(source_code, &tree, line, col)) + get_tree(source_code, &Language::Java).and_then(|tree| { + find_enclosing_function_with_tree(source_code, &tree, start_line, start_col, end_line, end_col) + }) } /// Returns the enclosing method or constructor for the given source position. @@ -51,17 +54,23 @@ pub fn find_enclosing_function( pub fn find_enclosing_function_with_tree( source_code: &str, tree: &tree_sitter::Tree, - line: u32, - col: u32, + start_line: u32, + start_col: u32, + end_line: u32, + end_col: u32, ) -> Option { let ctx = JavaFileContext::new(source_code, tree); - let point = tree_sitter::Point { - row: line.saturating_sub(1) as usize, - column: col.saturating_sub(1) as usize, + let start = tree_sitter::Point { + row: start_line.saturating_sub(1) as usize, + column: start_col.saturating_sub(1) as usize, + }; + let end = tree_sitter::Point { + row: end_line.saturating_sub(1) as usize, + column: end_col.saturating_sub(1) as usize, }; let mut node = tree .root_node() - .named_descendant_for_point_range(point, point)?; + .named_descendant_for_point_range(start, end)?; loop { match node.kind() { "method_declaration" | "constructor_declaration" => { @@ -334,7 +343,7 @@ mod tests { fn find(source: &str, line: u32, col: u32) -> Option { let tree = get_tree(source, &Language::Java).unwrap(); - find_enclosing_function_with_tree(source, &tree, line, col) + find_enclosing_function_with_tree(source, &tree, line, col, line, col) } fn ef(name: &str, sig: &str) -> Option { From 9fe010255331549d41c13de44fb604b592eb8b94 Mon Sep 17 00:00:00 2001 From: Xabier Aldama Date: Tue, 21 Apr 2026 11:14:25 +0200 Subject: [PATCH 13/19] cargo fmt --- .../src/analysis/languages.rs | 19 +++++++++++++++---- .../src/analysis/languages/java/methods.rs | 9 ++++++++- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/crates/static-analysis-kernel/src/analysis/languages.rs b/crates/static-analysis-kernel/src/analysis/languages.rs index c5cdf68f1..f07f20117 100644 --- a/crates/static-analysis-kernel/src/analysis/languages.rs +++ b/crates/static-analysis-kernel/src/analysis/languages.rs @@ -26,7 +26,13 @@ pub fn find_enclosing_function( language: &Language, ) -> Option { match language { - Language::Java => java::methods::find_enclosing_function(source_code, start_line, start_col, end_line, end_col), + Language::Java => java::methods::find_enclosing_function( + source_code, + start_line, + start_col, + end_line, + end_col, + ), Language::Python | Language::Go | Language::JavaScript @@ -63,9 +69,14 @@ pub fn find_enclosing_function_with_tree( language: &Language, ) -> Option { match language { - Language::Java => { - java::methods::find_enclosing_function_with_tree(source_code, tree, start_line, start_col, end_line, end_col) - } + Language::Java => java::methods::find_enclosing_function_with_tree( + source_code, + tree, + start_line, + start_col, + end_line, + end_col, + ), Language::Python | Language::Go | Language::JavaScript diff --git a/crates/static-analysis-kernel/src/analysis/languages/java/methods.rs b/crates/static-analysis-kernel/src/analysis/languages/java/methods.rs index 1928ae4f0..fe9c5280d 100644 --- a/crates/static-analysis-kernel/src/analysis/languages/java/methods.rs +++ b/crates/static-analysis-kernel/src/analysis/languages/java/methods.rs @@ -37,7 +37,14 @@ pub fn find_enclosing_function( end_col: u32, ) -> Option { get_tree(source_code, &Language::Java).and_then(|tree| { - find_enclosing_function_with_tree(source_code, &tree, start_line, start_col, end_line, end_col) + find_enclosing_function_with_tree( + source_code, + &tree, + start_line, + start_col, + end_line, + end_col, + ) }) } From 02c943554543f709526b63e3dd493f85d454a675 Mon Sep 17 00:00:00 2001 From: Xabier Aldama Date: Mon, 27 Apr 2026 15:52:00 +0200 Subject: [PATCH 14/19] Delete the logic to get the FQMN --- crates/cli/src/sarif/sarif_utils.rs | 7 +- .../src/analysis/languages/java/methods.rs | 361 +----------------- .../src/model/violation.rs | 7 - 3 files changed, 13 insertions(+), 362 deletions(-) diff --git a/crates/cli/src/sarif/sarif_utils.rs b/crates/cli/src/sarif/sarif_utils.rs index 822bf3c0b..1dbc40974 100644 --- a/crates/cli/src/sarif/sarif_utils.rs +++ b/crates/cli/src/sarif/sarif_utils.rs @@ -657,7 +657,6 @@ fn generate_results( if let Some(ref ef) = violation.enclosing_function { location_builder.logical_locations(vec![LogicalLocationBuilder::default() .name(ef.name.clone()) - .fully_qualified_name(ef.fully_qualified_name.clone()) .kind("function".to_string()) .build()?]); } @@ -1346,7 +1345,6 @@ mod tests { is_suppressed: false, enclosing_function: Some(EnclosingFunction { name: "my_method".to_string(), - fully_qualified_name: "def my_method(self)".to_string(), }), }; let violation_without_method = Violation { @@ -1390,7 +1388,7 @@ mod tests { let sarif_json = serde_json::to_value(sarif_report).unwrap(); - // Violation with enclosing_function: logicalLocations must carry name, fullyQualifiedName, and kind. + // Violation with enclosing_function: logicalLocations must carry name and kind. let logical_locations = sarif_json .pointer("/runs/0/results/0/locations/0/logicalLocations") .expect("logicalLocations should be present when enclosing_function is set"); @@ -1398,8 +1396,7 @@ mod tests { actual: logical_locations, expected: serde_json::json!([{ "kind": "function", - "name": "my_method", - "fullyQualifiedName": "def my_method(self)" + "name": "my_method" }]) ); diff --git a/crates/static-analysis-kernel/src/analysis/languages/java/methods.rs b/crates/static-analysis-kernel/src/analysis/languages/java/methods.rs index fe9c5280d..ae2e2a91b 100644 --- a/crates/static-analysis-kernel/src/analysis/languages/java/methods.rs +++ b/crates/static-analysis-kernel/src/analysis/languages/java/methods.rs @@ -2,28 +2,11 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2024 Datadog, Inc. -use std::collections::HashMap; - -use crate::analysis::languages::{enclosing_class_name, ts_node_text}; +use crate::analysis::languages::ts_node_text; use crate::analysis::tree_sitter::get_tree; use crate::model::common::Language; use crate::model::violation::EnclosingFunction; -struct JavaFileContext { - package: Option, - import_map: HashMap, -} - -impl JavaFileContext { - fn new(source_code: &str, tree: &tree_sitter::Tree) -> Self { - let root = tree.root_node(); - Self { - package: find_package(source_code, root), - import_map: build_import_map(source_code, root), - } - } -} - /// Returns the enclosing method or constructor for the given source position, or `None` if the /// position is not inside any method. /// @@ -49,15 +32,6 @@ pub fn find_enclosing_function( } /// Returns the enclosing method or constructor for the given source position. -/// See [`find_enclosing_function`] for documentation. -/// -/// The `fully_qualified_name` follows the FQMN format: -/// `package.ClassName.methodName(ParamType1, ParamType2)` -/// -/// Types are resolved to fully qualified names using the file's import declarations. -/// Types from `java.lang` (String, Integer, etc.) are always resolved. Types only -/// reachable via wildcard imports are returned as simple names. -/// pub fn find_enclosing_function_with_tree( source_code: &str, tree: &tree_sitter::Tree, @@ -66,7 +40,6 @@ pub fn find_enclosing_function_with_tree( end_line: u32, end_col: u32, ) -> Option { - let ctx = JavaFileContext::new(source_code, tree); let start = tree_sitter::Point { row: start_line.saturating_sub(1) as usize, column: start_col.saturating_sub(1) as usize, @@ -84,11 +57,7 @@ pub fn find_enclosing_function_with_tree( let name = node .child_by_field_name("name") .map(|n| ts_node_text(source_code, n).to_owned())?; - let fully_qualified_name = build_fqn(source_code, &ctx, node, &name); - return Some(EnclosingFunction { - name, - fully_qualified_name, - }); + return Some(EnclosingFunction { name }); } _ => {} } @@ -96,251 +65,6 @@ pub fn find_enclosing_function_with_tree( } } -/// Builds the fully qualified method name (FQMN) in the format: -/// `package.ClassName.methodName(ParamType1, ParamType2)` -fn build_fqn( - source_code: &str, - ctx: &JavaFileContext, - method_node: tree_sitter::Node, - method_name: &str, -) -> String { - let class_kinds = &[ - "class_declaration", - "interface_declaration", - "enum_declaration", - ]; - let class_name = enclosing_class_name(source_code, method_node, class_kinds); - - let fqn_class = match (ctx.package.as_deref(), class_name) { - (Some(pkg), Some(cls)) => format!("{pkg}.{cls}"), - (None, Some(cls)) => cls.to_string(), - (Some(pkg), None) => pkg.to_string(), - (None, None) => String::new(), - }; - - let pkg = ctx.package.as_deref(); - - let param_types = method_node - .child_by_field_name("parameters") - .map(|p| extract_param_types(source_code, p, &ctx.import_map, pkg)) - .unwrap_or_default(); - - let params_str = param_types.join(", "); - - if fqn_class.is_empty() { - format!("{method_name}({params_str})") - } else { - format!("{fqn_class}.{method_name}({params_str})") - } -} - -/// Returns the package name declared at the top of the compilation unit, if any. -fn find_package(source_code: &str, root: tree_sitter::Node) -> Option { - for i in 0..root.named_child_count() { - let Some(child) = root.named_child(i) else { - continue; - }; - if child.kind() == "package_declaration" { - for j in 0..child.named_child_count() { - let Some(pkg_child) = child.named_child(j) else { - continue; - }; - if matches!(pkg_child.kind(), "scoped_identifier" | "identifier") { - return Some(ts_node_text(source_code, pkg_child).to_owned()); - } - } - } - } - None -} - -/// Builds a map of simple class name → fully qualified name from explicit (non-wildcard, -/// non-static) import declarations in the file. -fn build_import_map(source_code: &str, root: tree_sitter::Node) -> HashMap { - let mut map = HashMap::new(); - for i in 0..root.named_child_count() { - let Some(child) = root.named_child(i) else { - continue; - }; - if child.kind() != "import_declaration" { - continue; - } - let text = ts_node_text(source_code, child); - // Strip "import " prefix and ";" suffix, then trim whitespace - let body = text - .trim_start_matches("import") - .trim() - .trim_end_matches(';') - .trim(); - // Skip static and wildcard imports — they can't be resolved without a classpath - if body.starts_with("static ") || body.ends_with('*') { - continue; - } - let simple_name = body.split('.').next_back().unwrap_or("").to_string(); - if !simple_name.is_empty() { - map.insert(simple_name, body.to_string()); - } - } - map -} - -/// Resolves a tree-sitter type node to a fully qualified type name where possible. -/// -/// - Explicit imports: `MultipartFile` → `org.springframework.web.multipart.MultipartFile` -/// - java.lang types: `String` → `java.lang.String` -/// - Everything else: returned as the simple name from source -fn resolve_type( - source_code: &str, - type_node: tree_sitter::Node, - import_map: &HashMap, - package: Option<&str>, -) -> String { - match type_node.kind() { - // Primitives and void — always use as-is - "void_type" | "integral_type" | "floating_point_type" | "boolean_type" => { - ts_node_text(source_code, type_node).to_owned() - } - // Simple class name reference - "type_identifier" => { - resolve_class_name(ts_node_text(source_code, type_node), import_map, package) - } - // Already fully qualified in source (rare) - "scoped_type_identifier" => ts_node_text(source_code, type_node).to_owned(), - // Generic type: List → java.util.List - "generic_type" => { - let base = type_node - .named_child(0) - .map(|n| resolve_type(source_code, n, import_map, package)) - .unwrap_or_default(); - let type_args = type_node - .named_child(1) - .filter(|n| n.kind() == "type_arguments"); - match type_args { - Some(args_node) => { - let args: Vec = (0..args_node.named_child_count()) - .filter_map(|i| args_node.named_child(i)) - .map(|n| resolve_type(source_code, n, import_map, package)) - .collect(); - if args.is_empty() { - base - } else { - format!("{base}<{}>", args.join(", ")) - } - } - None => base, - } - } - // Array type: String[] — resolve element type and keep dimensions - "array_type" => { - let element = type_node - .child_by_field_name("element") - .map(|n| resolve_type(source_code, n, import_map, package)) - .unwrap_or_default(); - let dims = type_node - .child_by_field_name("dimensions") - .map(|n| ts_node_text(source_code, n)) - .unwrap_or("[]"); - format!("{element}{dims}") - } - // Annotated type: @NotNull String — strip annotation and resolve the underlying type - "annotated_type" => { - // The last named child is the actual type - let count = type_node.named_child_count(); - type_node - .named_child(count.saturating_sub(1)) - .map(|n| resolve_type(source_code, n, import_map, package)) - .unwrap_or_else(|| ts_node_text(source_code, type_node).to_owned()) - } - // Wildcard (? extends Foo, ? super Bar) — keep as raw text - "wildcard" => ts_node_text(source_code, type_node).to_owned(), - _ => ts_node_text(source_code, type_node).to_owned(), - } -} - -/// Resolves a simple class name to its fully qualified form using explicit imports and -/// the well-known `java.lang` package that is always implicitly available. -fn resolve_class_name( - name: &str, - import_map: &HashMap, - _package: Option<&str>, -) -> String { - if let Some(fqn) = import_map.get(name) { - return fqn.clone(); - } - if JAVA_LANG_TYPES.contains(&name) { - return format!("java.lang.{name}"); - } - name.to_owned() -} - -/// Extracts the ordered list of parameter types from a `formal_parameters` node. -/// Annotations and variable names are excluded; only the type is kept. -fn extract_param_types( - source_code: &str, - params_node: tree_sitter::Node, - import_map: &HashMap, - package: Option<&str>, -) -> Vec { - let mut types = vec![]; - for i in 0..params_node.named_child_count() { - let Some(child) = params_node.named_child(i) else { - continue; - }; - match child.kind() { - "formal_parameter" => { - if let Some(type_node) = child.child_by_field_name("type") { - types.push(resolve_type(source_code, type_node, import_map, package)); - } - } - "spread_parameter" => { - // Varargs: `Type... name` — type field exists on spread_parameter too - if let Some(type_node) = child.child_by_field_name("type") { - let t = resolve_type(source_code, type_node, import_map, package); - types.push(format!("{t}...")); - } - } - _ => {} - } - } - types -} - -// Types always in scope from java.lang.* (implicit import in every Java file) -const JAVA_LANG_TYPES: &[&str] = &[ - "AutoCloseable", - "Boolean", - "Byte", - "CharSequence", - "Character", - "Class", - "ClassLoader", - "Cloneable", - "Comparable", - "Double", - "Enum", - "Error", - "Exception", - "Float", - "Integer", - "Iterable", - "Long", - "Math", - "Number", - "Object", - "Process", - "Runnable", - "Runtime", - "RuntimeException", - "Short", - "String", - "StringBuffer", - "StringBuilder", - "System", - "Thread", - "Throwable", - "Void", -]; - #[cfg(test)] mod tests { use super::find_enclosing_function_with_tree; @@ -353,10 +77,9 @@ mod tests { find_enclosing_function_with_tree(source, &tree, line, col, line, col) } - fn ef(name: &str, sig: &str) -> Option { + fn ef(name: &str) -> Option { Some(EnclosingFunction { name: name.to_string(), - fully_qualified_name: sig.to_string(), }) } @@ -369,7 +92,7 @@ class Foo { } } "; - assert_eq!(find(src, 3, 9), ef("doSomething", "Foo.doSomething()")); + assert_eq!(find(src, 3, 9), ef("doSomething")); } #[test] @@ -381,7 +104,7 @@ class Foo { } } "; - assert_eq!(find(src, 3, 9), ef("Foo", "Foo.Foo()")); + assert_eq!(find(src, 3, 9), ef("Foo")); } #[test] @@ -394,72 +117,23 @@ class Foo { } } "; - assert_eq!( - find(src, 4, 9), - ef("doSomething", "com.example.Foo.doSomething()") - ); + assert_eq!(find(src, 4, 9), ef("doSomething")); } #[test] - fn java_lang_type_resolved() { - // String is in java.lang and always resolved without an explicit import + fn with_params() { let src = "\ class Foo { - public void handle(String s) { + public void handle(String s, int n) { int x = 1; } } "; - assert_eq!( - find(src, 3, 9), - ef("handle", "Foo.handle(java.lang.String)") - ); + assert_eq!(find(src, 3, 9), ef("handle")); } #[test] - fn explicit_import_resolved() { - let src = "\ -import org.springframework.web.multipart.MultipartFile; -import org.springframework.ui.Model; -class Foo { - public String process(MultipartFile file, Model model) { - return \"ok\"; - } -} -"; - assert_eq!( - find(src, 4, 9), - ef( - "process", - "Foo.process(org.springframework.web.multipart.MultipartFile, org.springframework.ui.Model)" - ) - ); - } - - #[test] - fn full_fqn_with_package_and_imports() { - let src = "\ -package org.hdivsamples.controllers; -import org.springframework.web.multipart.MultipartFile; -import org.springframework.ui.Model; -class DashboardController { - public String processSimple(MultipartFile file, Model model) { - return \"ok\"; - } -} -"; - assert_eq!( - find(src, 5, 9), - ef( - "processSimple", - "org.hdivsamples.controllers.DashboardController.processSimple(org.springframework.web.multipart.MultipartFile, org.springframework.ui.Model)" - ) - ); - } - - #[test] - fn annotations_not_in_fqn() { - // Method and parameter annotations are excluded from the FQN + fn annotations_ignored() { let src = "\ class Foo { @Override @@ -468,20 +142,7 @@ class Foo { } } "; - assert_eq!(find(src, 4, 9), ef("doSomething", "Foo.doSomething()")); - } - - #[test] - fn throws_not_in_fqn() { - // throws clause is not part of the standard Java FQN - let src = "\ -class Foo { - public void parse() throws IOException { - int x = 1; - } -} -"; - assert_eq!(find(src, 3, 9), ef("parse", "Foo.parse()")); + assert_eq!(find(src, 4, 9), ef("doSomething")); } #[test] diff --git a/crates/static-analysis-kernel/src/model/violation.rs b/crates/static-analysis-kernel/src/model/violation.rs index 606311ba6..b251d85bc 100644 --- a/crates/static-analysis-kernel/src/model/violation.rs +++ b/crates/static-analysis-kernel/src/model/violation.rs @@ -9,13 +9,6 @@ use serde::{Deserialize, Serialize}; pub struct EnclosingFunction { /// Simple identifier (e.g. `handle`, `doSomething`). pub name: String, - /// Fully qualified method name (FQMN). The format varies by language: - /// - /// - **Java**: `package.ClassName.methodName(ParamType1, ParamType2)` - /// - **Go**: `package.FunctionName` / `package.TypeName.MethodName` - /// - **Python / JS / TS**: `ClassName.methodName` for methods, `functionName` for top-level functions - /// - **C#**: `Namespace.ClassName.MethodName(ParamType1, ParamType2)` - pub fully_qualified_name: String, } #[derive(Copy, Clone, Deserialize, Debug, Serialize, Eq, PartialEq)] From 3423c461d606bc5cf3a6c4f39c6eb0287d588a2d Mon Sep 17 00:00:00 2001 From: Xabier Aldama Date: Mon, 27 Apr 2026 16:26:23 +0200 Subject: [PATCH 15/19] Delete unused code --- .../src/analysis/languages.rs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/crates/static-analysis-kernel/src/analysis/languages.rs b/crates/static-analysis-kernel/src/analysis/languages.rs index f07f20117..5af39c93c 100644 --- a/crates/static-analysis-kernel/src/analysis/languages.rs +++ b/crates/static-analysis-kernel/src/analysis/languages.rs @@ -101,22 +101,6 @@ pub fn find_enclosing_function_with_tree( } } -/// Walks up from `node` looking for an ancestor whose `kind()` is one of `class_kinds`. -/// Returns the text of that ancestor's `name` field, or `None` if not found. -pub(crate) fn enclosing_class_name<'s>( - source_code: &'s str, - mut node: tree_sitter::Node<'_>, - class_kinds: &[&str], -) -> Option<&'s str> { - loop { - node = node.parent()?; - if class_kinds.contains(&node.kind()) { - return node - .child_by_field_name("name") - .map(|n| ts_node_text(source_code, n)); - } - } -} /// Returns the text that `node` spans. /// From 75581b604d60c523abf9f394999c6f7b9b53d728 Mon Sep 17 00:00:00 2001 From: Xabier Aldama Date: Mon, 27 Apr 2026 16:28:21 +0200 Subject: [PATCH 16/19] Fix format --- crates/static-analysis-kernel/src/analysis/languages.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/static-analysis-kernel/src/analysis/languages.rs b/crates/static-analysis-kernel/src/analysis/languages.rs index 5af39c93c..e22bde6dd 100644 --- a/crates/static-analysis-kernel/src/analysis/languages.rs +++ b/crates/static-analysis-kernel/src/analysis/languages.rs @@ -101,7 +101,6 @@ pub fn find_enclosing_function_with_tree( } } - /// Returns the text that `node` spans. /// /// This is simply a wrapper around [`tree_sitter::Node::utf8_text`] From a0c6673798e6130f264db26b711f756bf6ddeb47 Mon Sep 17 00:00:00 2001 From: Xabier Aldama Date: Tue, 28 Apr 2026 14:59:07 +0200 Subject: [PATCH 17/19] Make integration test more deterministic --- misc/integration-test-method-name.sh | 103 ++++++++++++++++++--------- 1 file changed, 69 insertions(+), 34 deletions(-) diff --git a/misc/integration-test-method-name.sh b/misc/integration-test-method-name.sh index 2521822e4..ed6282b9d 100755 --- a/misc/integration-test-method-name.sh +++ b/misc/integration-test-method-name.sh @@ -1,9 +1,10 @@ #!/bin/bash -# Integration test: verify that method names are populated in SARIF logicalLocations -# for Java, which is the first language with enclosing-function detection. +# Integration test: verify that enclosing method names are populated in SARIF +# logicalLocations when a violation falls inside a named method. # -# Each language block added here as support is implemented. +# Uses a self-contained dummy rule so the test does not depend on external +# rulesets or repositories. set -euo pipefail @@ -12,50 +13,84 @@ ANALYZER="./target/release-dev/datadog-static-analyzer" cargo fetch cargo build --locked --profile release-dev --bin datadog-static-analyzer +WORK_DIR=$(mktemp -d) +trap 'rm -rf "${WORK_DIR}"' EXIT + # --------------------------------------------------------------------------- -# Helper: count results that carry at least one logicalLocations entry. +# Source file: one class with one method containing one local variable. # --------------------------------------------------------------------------- -count_with_method() { - local results_file="$1" - jq '[.runs[0].results[] | - select(any(.locations[]?; (.logicalLocations // []) | length > 0)) - ] | length' "${results_file}" +cat > "${WORK_DIR}/Sample.java" << 'EOF' +class Sample { + public void doWork() { + int x = 1; + } } +EOF # --------------------------------------------------------------------------- -# Java – OWASP Benchmark +# Rule: flag every local_variable_declaration. +# +# code (base64 of): +# function visit(node, filename, code) { +# const n = node.captures["decl"]; +# addError(buildError(n.start.line, n.start.col, n.end.line, n.end.col, +# "test violation", "WARNING", "BEST_PRACTICES")); +# } +# +# tree_sitter_query (base64 of): +# (local_variable_declaration) @decl # --------------------------------------------------------------------------- -echo "=== Java: method-name detection ===" -JAVA_DIR=$(mktemp -d) -git clone --depth=1 https://github.com/OWASP-Benchmark/BenchmarkJava.git "${JAVA_DIR}" +cat > "${WORK_DIR}/rules.json" << 'EOF' +[{ + "name": "test-ruleset", + "description": "dGVzdA==", + "rules": [{ + "name": "test-ruleset/flag-local-var", + "short_description": "dGVzdA==", + "description": "dGVzdA==", + "category": "BEST_PRACTICES", + "severity": "WARNING", + "language": "JAVA", + "rule_type": "TREE_SITTER_QUERY", + "entity_checked": null, + "code": "ZnVuY3Rpb24gdmlzaXQobm9kZSwgZmlsZW5hbWUsIGNvZGUpIHsKICBjb25zdCBuID0gbm9kZS5jYXB0dXJlc1siZGVjbCJdOwogIGFkZEVycm9yKGJ1aWxkRXJyb3Iobi5zdGFydC5saW5lLCBuLnN0YXJ0LmNvbCwgbi5lbmQubGluZSwgbi5lbmQuY29sLCAidGVzdCB2aW9sYXRpb24iLCAiV0FSTklORyIsICJCRVNUX1BSQUNUSUNFUyIpKTsKfQo=", + "checksum": "ed0928bb71c63712480323e22437d5e955e998e7658d3bb24ba9ed89eebc9723", + "pattern": null, + "tree_sitter_query": "KGxvY2FsX3ZhcmlhYmxlX2RlY2xhcmF0aW9uKSBAZGVjbAo=", + "tests": [], + "is_testing": false + }] +}] +EOF -cat > "${JAVA_DIR}/code-security.datadog.yaml" <<'YAML' -schema-version: v1.0 -sast: - use-default-rulesets: false - use-rulesets: - - java-security - - java-best-practices -YAML +"${ANALYZER}" \ + --directory "${WORK_DIR}" \ + -r "${WORK_DIR}/rules.json" \ + -o "${WORK_DIR}/results.json" \ + -f sarif \ + -b -"${ANALYZER}" --directory "${JAVA_DIR}" -o "${JAVA_DIR}/results.json" -f sarif - -TOTAL=$(jq '.runs[0].results | length' "${JAVA_DIR}/results.json") -WITH_METHOD=$(count_with_method "${JAVA_DIR}/results.json") +# --------------------------------------------------------------------------- +# Assertions +# --------------------------------------------------------------------------- +TOTAL=$(jq '.runs[0].results | length' "${WORK_DIR}/results.json") +if [ "${TOTAL}" -ne 1 ]; then + echo "FAIL: expected 1 violation, got ${TOTAL}" + exit 1 +fi -echo "Java: ${WITH_METHOD}/${TOTAL} violations have a method name" +METHOD_NAME=$(jq -r '.runs[0].results[0].locations[0].logicalLocations[0].name // empty' "${WORK_DIR}/results.json") +KIND=$(jq -r '.runs[0].results[0].locations[0].logicalLocations[0].kind // empty' "${WORK_DIR}/results.json") -if [ "${TOTAL}" -lt 1 ]; then - echo "FAIL: no Java violations found – ruleset may be empty or repo changed" +if [ "${METHOD_NAME}" != "doWork" ]; then + echo "FAIL: expected logicalLocations name 'doWork', got '${METHOD_NAME}'" exit 1 fi -if [ "${WITH_METHOD}" -lt 1 ]; then - echo "FAIL: no Java violations carry a logicalLocations/method name" + +if [ "${KIND}" != "function" ]; then + echo "FAIL: expected logicalLocations kind 'function', got '${KIND}'" exit 1 fi -# --------------------------------------------------------------------------- -# Done -# --------------------------------------------------------------------------- -echo "All method-name integration tests passed" +echo "PASS: logicalLocations name=${METHOD_NAME}, kind=${KIND}" exit 0 From 6a1009da77dba5d65e49d36f24178214418e4b5f Mon Sep 17 00:00:00 2001 From: Xabier Aldama Date: Tue, 28 Apr 2026 15:59:02 +0200 Subject: [PATCH 18/19] Add a new test for lambdas --- .../src/analysis/languages/java/methods.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/crates/static-analysis-kernel/src/analysis/languages/java/methods.rs b/crates/static-analysis-kernel/src/analysis/languages/java/methods.rs index ae2e2a91b..1ddbdb174 100644 --- a/crates/static-analysis-kernel/src/analysis/languages/java/methods.rs +++ b/crates/static-analysis-kernel/src/analysis/languages/java/methods.rs @@ -154,4 +154,20 @@ class Foo { "; assert_eq!(find(src, 2, 9), None); } + + // Lambdas are not named, so we report the nearest enclosing named method instead. + // Naming individual lambdas is not implemented. + #[test] + fn inside_lambda_reports_enclosing_method() { + let src = "\ +class Foo { + public void doWork() { + Runnable r = () -> { + int x = 1; + }; + } +} +"; + assert_eq!(find(src, 4, 13), ef("doWork")); + } } From c004e32b47252a45baa40924d30c48ee76d2df23 Mon Sep 17 00:00:00 2001 From: Xabier Aldama Date: Tue, 28 Apr 2026 16:01:57 +0200 Subject: [PATCH 19/19] Use the value instead of a pointer --- .../static-analysis-kernel/src/analysis/ddsa_lib/runtime.rs | 2 +- crates/static-analysis-kernel/src/analysis/languages.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/static-analysis-kernel/src/analysis/ddsa_lib/runtime.rs b/crates/static-analysis-kernel/src/analysis/ddsa_lib/runtime.rs index 36030d2b1..9cb506ba8 100644 --- a/crates/static-analysis-kernel/src/analysis/ddsa_lib/runtime.rs +++ b/crates/static-analysis-kernel/src/analysis/ddsa_lib/runtime.rs @@ -208,7 +208,7 @@ impl JsRuntime { v.start.col, v.end.line, v.end.col, - &rule.language, + rule.language, ); v }) diff --git a/crates/static-analysis-kernel/src/analysis/languages.rs b/crates/static-analysis-kernel/src/analysis/languages.rs index e22bde6dd..c6a2fee13 100644 --- a/crates/static-analysis-kernel/src/analysis/languages.rs +++ b/crates/static-analysis-kernel/src/analysis/languages.rs @@ -23,7 +23,7 @@ pub fn find_enclosing_function( start_col: u32, end_line: u32, end_col: u32, - language: &Language, + language: Language, ) -> Option { match language { Language::Java => java::methods::find_enclosing_function( @@ -66,7 +66,7 @@ pub fn find_enclosing_function_with_tree( start_col: u32, end_line: u32, end_col: u32, - language: &Language, + language: Language, ) -> Option { match language { Language::Java => java::methods::find_enclosing_function_with_tree(