Add DAST, graph modules, toast notifications, and dashboard enhancements
Add DAST scanning and code knowledge graph features across the stack: - compliance-dast and compliance-graph workspace crates - Agent API handlers and routes for DAST targets/scans and graph builds - Core models and traits for DAST and graph domains - Dashboard pages for DAST targets/findings/overview and graph explorer/impact - Toast notification system with auto-dismiss for async action feedback - Button click animations and disabled states for better UX Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
426
compliance-graph/src/parsers/rust_parser.rs
Normal file
426
compliance-graph/src/parsers/rust_parser.rs
Normal file
@@ -0,0 +1,426 @@
|
||||
use std::path::Path;
|
||||
|
||||
use compliance_core::error::CoreError;
|
||||
use compliance_core::models::graph::{CodeEdge, CodeEdgeKind, CodeNode, CodeNodeKind};
|
||||
use compliance_core::traits::graph_builder::{LanguageParser, ParseOutput};
|
||||
use tree_sitter::{Node, Parser};
|
||||
|
||||
pub struct RustParser;
|
||||
|
||||
impl RustParser {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn walk_tree(
|
||||
&self,
|
||||
node: Node<'_>,
|
||||
source: &str,
|
||||
file_path: &str,
|
||||
repo_id: &str,
|
||||
graph_build_id: &str,
|
||||
parent_qualified: Option<&str>,
|
||||
output: &mut ParseOutput,
|
||||
) {
|
||||
match node.kind() {
|
||||
"function_item" | "function_signature_item" => {
|
||||
if let Some(name_node) = node.child_by_field_name("name") {
|
||||
let name = &source[name_node.byte_range()];
|
||||
let qualified = match parent_qualified {
|
||||
Some(p) => format!("{p}::{name}"),
|
||||
None => format!("{file_path}::{name}"),
|
||||
};
|
||||
|
||||
let is_entry = name == "main"
|
||||
|| self.has_attribute(&node, source, "test")
|
||||
|| self.has_attribute(&node, source, "tokio::main")
|
||||
|| self.has_pub_visibility(&node, source);
|
||||
|
||||
output.nodes.push(CodeNode {
|
||||
id: None,
|
||||
repo_id: repo_id.to_string(),
|
||||
graph_build_id: graph_build_id.to_string(),
|
||||
qualified_name: qualified.clone(),
|
||||
name: name.to_string(),
|
||||
kind: CodeNodeKind::Function,
|
||||
file_path: file_path.to_string(),
|
||||
start_line: node.start_position().row as u32 + 1,
|
||||
end_line: node.end_position().row as u32 + 1,
|
||||
language: "rust".to_string(),
|
||||
community_id: None,
|
||||
is_entry_point: is_entry,
|
||||
graph_index: None,
|
||||
});
|
||||
|
||||
// Extract function calls within the body
|
||||
if let Some(body) = node.child_by_field_name("body") {
|
||||
self.extract_calls(
|
||||
body,
|
||||
source,
|
||||
file_path,
|
||||
repo_id,
|
||||
graph_build_id,
|
||||
&qualified,
|
||||
output,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
"struct_item" => {
|
||||
if let Some(name_node) = node.child_by_field_name("name") {
|
||||
let name = &source[name_node.byte_range()];
|
||||
let qualified = match parent_qualified {
|
||||
Some(p) => format!("{p}::{name}"),
|
||||
None => format!("{file_path}::{name}"),
|
||||
};
|
||||
|
||||
output.nodes.push(CodeNode {
|
||||
id: None,
|
||||
repo_id: repo_id.to_string(),
|
||||
graph_build_id: graph_build_id.to_string(),
|
||||
qualified_name: qualified,
|
||||
name: name.to_string(),
|
||||
kind: CodeNodeKind::Struct,
|
||||
file_path: file_path.to_string(),
|
||||
start_line: node.start_position().row as u32 + 1,
|
||||
end_line: node.end_position().row as u32 + 1,
|
||||
language: "rust".to_string(),
|
||||
community_id: None,
|
||||
is_entry_point: false,
|
||||
graph_index: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
"enum_item" => {
|
||||
if let Some(name_node) = node.child_by_field_name("name") {
|
||||
let name = &source[name_node.byte_range()];
|
||||
let qualified = match parent_qualified {
|
||||
Some(p) => format!("{p}::{name}"),
|
||||
None => format!("{file_path}::{name}"),
|
||||
};
|
||||
|
||||
output.nodes.push(CodeNode {
|
||||
id: None,
|
||||
repo_id: repo_id.to_string(),
|
||||
graph_build_id: graph_build_id.to_string(),
|
||||
qualified_name: qualified,
|
||||
name: name.to_string(),
|
||||
kind: CodeNodeKind::Enum,
|
||||
file_path: file_path.to_string(),
|
||||
start_line: node.start_position().row as u32 + 1,
|
||||
end_line: node.end_position().row as u32 + 1,
|
||||
language: "rust".to_string(),
|
||||
community_id: None,
|
||||
is_entry_point: false,
|
||||
graph_index: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
"trait_item" => {
|
||||
if let Some(name_node) = node.child_by_field_name("name") {
|
||||
let name = &source[name_node.byte_range()];
|
||||
let qualified = match parent_qualified {
|
||||
Some(p) => format!("{p}::{name}"),
|
||||
None => format!("{file_path}::{name}"),
|
||||
};
|
||||
|
||||
output.nodes.push(CodeNode {
|
||||
id: None,
|
||||
repo_id: repo_id.to_string(),
|
||||
graph_build_id: graph_build_id.to_string(),
|
||||
qualified_name: qualified.clone(),
|
||||
name: name.to_string(),
|
||||
kind: CodeNodeKind::Trait,
|
||||
file_path: file_path.to_string(),
|
||||
start_line: node.start_position().row as u32 + 1,
|
||||
end_line: node.end_position().row as u32 + 1,
|
||||
language: "rust".to_string(),
|
||||
community_id: None,
|
||||
is_entry_point: false,
|
||||
graph_index: None,
|
||||
});
|
||||
|
||||
// Parse methods inside the trait
|
||||
self.walk_children(
|
||||
node,
|
||||
source,
|
||||
file_path,
|
||||
repo_id,
|
||||
graph_build_id,
|
||||
Some(&qualified),
|
||||
output,
|
||||
);
|
||||
return; // Don't walk children again
|
||||
}
|
||||
}
|
||||
"impl_item" => {
|
||||
// Extract impl target type for qualified naming
|
||||
let impl_name = self.extract_impl_type(&node, source);
|
||||
let qualified = match parent_qualified {
|
||||
Some(p) => format!("{p}::{impl_name}"),
|
||||
None => format!("{file_path}::{impl_name}"),
|
||||
};
|
||||
|
||||
// Check for trait impl (impl Trait for Type)
|
||||
if let Some(trait_node) = node.child_by_field_name("trait") {
|
||||
let trait_name = &source[trait_node.byte_range()];
|
||||
output.edges.push(CodeEdge {
|
||||
id: None,
|
||||
repo_id: repo_id.to_string(),
|
||||
graph_build_id: graph_build_id.to_string(),
|
||||
source: qualified.clone(),
|
||||
target: trait_name.to_string(),
|
||||
kind: CodeEdgeKind::Implements,
|
||||
file_path: file_path.to_string(),
|
||||
line_number: Some(node.start_position().row as u32 + 1),
|
||||
});
|
||||
}
|
||||
|
||||
// Walk methods inside impl block
|
||||
self.walk_children(
|
||||
node,
|
||||
source,
|
||||
file_path,
|
||||
repo_id,
|
||||
graph_build_id,
|
||||
Some(&qualified),
|
||||
output,
|
||||
);
|
||||
return;
|
||||
}
|
||||
"use_declaration" => {
|
||||
let use_text = &source[node.byte_range()];
|
||||
// Extract the imported path
|
||||
if let Some(path) = self.extract_use_path(use_text) {
|
||||
output.edges.push(CodeEdge {
|
||||
id: None,
|
||||
repo_id: repo_id.to_string(),
|
||||
graph_build_id: graph_build_id.to_string(),
|
||||
source: parent_qualified
|
||||
.unwrap_or(file_path)
|
||||
.to_string(),
|
||||
target: path,
|
||||
kind: CodeEdgeKind::Imports,
|
||||
file_path: file_path.to_string(),
|
||||
line_number: Some(node.start_position().row as u32 + 1),
|
||||
});
|
||||
}
|
||||
}
|
||||
"mod_item" => {
|
||||
if let Some(name_node) = node.child_by_field_name("name") {
|
||||
let name = &source[name_node.byte_range()];
|
||||
let qualified = match parent_qualified {
|
||||
Some(p) => format!("{p}::{name}"),
|
||||
None => format!("{file_path}::{name}"),
|
||||
};
|
||||
|
||||
output.nodes.push(CodeNode {
|
||||
id: None,
|
||||
repo_id: repo_id.to_string(),
|
||||
graph_build_id: graph_build_id.to_string(),
|
||||
qualified_name: qualified.clone(),
|
||||
name: name.to_string(),
|
||||
kind: CodeNodeKind::Module,
|
||||
file_path: file_path.to_string(),
|
||||
start_line: node.start_position().row as u32 + 1,
|
||||
end_line: node.end_position().row as u32 + 1,
|
||||
language: "rust".to_string(),
|
||||
community_id: None,
|
||||
is_entry_point: false,
|
||||
graph_index: None,
|
||||
});
|
||||
|
||||
// If it has a body (inline module), walk it
|
||||
if let Some(body) = node.child_by_field_name("body") {
|
||||
self.walk_children(
|
||||
body,
|
||||
source,
|
||||
file_path,
|
||||
repo_id,
|
||||
graph_build_id,
|
||||
Some(&qualified),
|
||||
output,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Default: walk children
|
||||
self.walk_children(
|
||||
node,
|
||||
source,
|
||||
file_path,
|
||||
repo_id,
|
||||
graph_build_id,
|
||||
parent_qualified,
|
||||
output,
|
||||
);
|
||||
}
|
||||
|
||||
fn walk_children(
|
||||
&self,
|
||||
node: Node<'_>,
|
||||
source: &str,
|
||||
file_path: &str,
|
||||
repo_id: &str,
|
||||
graph_build_id: &str,
|
||||
parent_qualified: Option<&str>,
|
||||
output: &mut ParseOutput,
|
||||
) {
|
||||
let mut cursor = node.walk();
|
||||
for child in node.children(&mut cursor) {
|
||||
self.walk_tree(
|
||||
child,
|
||||
source,
|
||||
file_path,
|
||||
repo_id,
|
||||
graph_build_id,
|
||||
parent_qualified,
|
||||
output,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_calls(
|
||||
&self,
|
||||
node: Node<'_>,
|
||||
source: &str,
|
||||
file_path: &str,
|
||||
repo_id: &str,
|
||||
graph_build_id: &str,
|
||||
caller_qualified: &str,
|
||||
output: &mut ParseOutput,
|
||||
) {
|
||||
if node.kind() == "call_expression" {
|
||||
if let Some(func_node) = node.child_by_field_name("function") {
|
||||
let callee = &source[func_node.byte_range()];
|
||||
output.edges.push(CodeEdge {
|
||||
id: None,
|
||||
repo_id: repo_id.to_string(),
|
||||
graph_build_id: graph_build_id.to_string(),
|
||||
source: caller_qualified.to_string(),
|
||||
target: callee.to_string(),
|
||||
kind: CodeEdgeKind::Calls,
|
||||
file_path: file_path.to_string(),
|
||||
line_number: Some(node.start_position().row as u32 + 1),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let mut cursor = node.walk();
|
||||
for child in node.children(&mut cursor) {
|
||||
self.extract_calls(
|
||||
child,
|
||||
source,
|
||||
file_path,
|
||||
repo_id,
|
||||
graph_build_id,
|
||||
caller_qualified,
|
||||
output,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn has_attribute(&self, node: &Node<'_>, source: &str, attr_name: &str) -> bool {
|
||||
if let Some(prev) = node.prev_sibling() {
|
||||
if prev.kind() == "attribute_item" || prev.kind() == "attribute" {
|
||||
let text = &source[prev.byte_range()];
|
||||
return text.contains(attr_name);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn has_pub_visibility(&self, node: &Node<'_>, source: &str) -> bool {
|
||||
let mut cursor = node.walk();
|
||||
for child in node.children(&mut cursor) {
|
||||
if child.kind() == "visibility_modifier" {
|
||||
let text = &source[child.byte_range()];
|
||||
return text == "pub";
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn extract_impl_type(&self, node: &Node<'_>, source: &str) -> String {
|
||||
if let Some(type_node) = node.child_by_field_name("type") {
|
||||
return source[type_node.byte_range()].to_string();
|
||||
}
|
||||
"unknown".to_string()
|
||||
}
|
||||
|
||||
fn extract_use_path(&self, use_text: &str) -> Option<String> {
|
||||
// "use foo::bar::baz;" -> "foo::bar::baz"
|
||||
let trimmed = use_text
|
||||
.strip_prefix("use ")?
|
||||
.trim_end_matches(';')
|
||||
.trim();
|
||||
Some(trimmed.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageParser for RustParser {
|
||||
fn language(&self) -> &str {
|
||||
"rust"
|
||||
}
|
||||
|
||||
fn extensions(&self) -> &[&str] {
|
||||
&["rs"]
|
||||
}
|
||||
|
||||
fn parse_file(
|
||||
&self,
|
||||
file_path: &Path,
|
||||
source: &str,
|
||||
repo_id: &str,
|
||||
graph_build_id: &str,
|
||||
) -> Result<ParseOutput, CoreError> {
|
||||
let mut parser = Parser::new();
|
||||
let language = tree_sitter_rust::LANGUAGE;
|
||||
parser
|
||||
.set_language(&language.into())
|
||||
.map_err(|e| CoreError::Graph(format!("Failed to set Rust language: {e}")))?;
|
||||
|
||||
let tree = parser
|
||||
.parse(source, None)
|
||||
.ok_or_else(|| CoreError::Graph("Failed to parse Rust file".to_string()))?;
|
||||
|
||||
let file_path_str = file_path.to_string_lossy().to_string();
|
||||
let mut output = ParseOutput::default();
|
||||
|
||||
// Add file node
|
||||
output.nodes.push(CodeNode {
|
||||
id: None,
|
||||
repo_id: repo_id.to_string(),
|
||||
graph_build_id: graph_build_id.to_string(),
|
||||
qualified_name: file_path_str.clone(),
|
||||
name: file_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_default(),
|
||||
kind: CodeNodeKind::File,
|
||||
file_path: file_path_str.clone(),
|
||||
start_line: 1,
|
||||
end_line: source.lines().count() as u32,
|
||||
language: "rust".to_string(),
|
||||
community_id: None,
|
||||
is_entry_point: false,
|
||||
graph_index: None,
|
||||
});
|
||||
|
||||
self.walk_tree(
|
||||
tree.root_node(),
|
||||
source,
|
||||
&file_path_str,
|
||||
repo_id,
|
||||
graph_build_id,
|
||||
None,
|
||||
&mut output,
|
||||
);
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user