docs: update rustdoc comments
This commit is contained in:
		
					parent
					
						
							
								03fbeb2bf2
							
						
					
				
			
			
				commit
				
					
						1740121517
					
				
			
		
					 3 changed files with 410 additions and 60 deletions
				
			
		|  | @ -150,7 +150,7 @@ pub enum SyntaxKind { | |||
|     // Error recovery
 | ||||
|     Error = 106, | ||||
| 
 | ||||
|     // Additional protocol keywords
 | ||||
|     // Protocol keywords for nftables
 | ||||
|     VmapKw = 107, | ||||
|     NdRouterAdvertKw = 108, | ||||
|     NdNeighborSolicitKw = 109, | ||||
|  | @ -237,7 +237,7 @@ impl From<TokenKind> for SyntaxKind { | |||
|             New => SyntaxKind::NewKw, | ||||
|             Invalid => SyntaxKind::InvalidKw, | ||||
| 
 | ||||
|             // Additional protocol keywords
 | ||||
|             // Protocol keywords for ICMP/ICMPv6
 | ||||
|             Vmap => SyntaxKind::VmapKw, | ||||
|             NdRouterAdvert => SyntaxKind::NdRouterAdvertKw, | ||||
|             NdNeighborSolicit => SyntaxKind::NdNeighborSolicitKw, | ||||
|  | @ -358,7 +358,7 @@ pub enum CstError { | |||
| /// Result type for CST operations
 | ||||
| pub type CstResult<T> = Result<T, CstError>; | ||||
| 
 | ||||
| /// Basic CST builder
 | ||||
| /// CST builder for nftables syntax
 | ||||
| pub struct CstBuilder; | ||||
| 
 | ||||
| impl CstBuilder { | ||||
|  |  | |||
|  | @ -10,9 +10,9 @@ | |||
| use crate::lexer::LexError; | ||||
| use crate::parser::{ParseError, Parser}; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use serde_json; | ||||
| use std::collections::{HashMap, HashSet}; | ||||
| use std::fmt; | ||||
| use std::net::IpAddr; | ||||
| use text_size::TextSize; | ||||
| 
 | ||||
| /// Diagnostic severity levels following LSP specification
 | ||||
|  | @ -171,10 +171,23 @@ pub struct Position { | |||
| } | ||||
| 
 | ||||
| impl Position { | ||||
|     /// Creates a new position with line and character coordinates
 | ||||
|     ///
 | ||||
|     /// # Parameters
 | ||||
|     /// * `line` - Zero-based line number
 | ||||
|     /// * `character` - Zero-based character offset in the line
 | ||||
|     pub fn new(line: u32, character: u32) -> Self { | ||||
|         Self { line, character } | ||||
|     } | ||||
| 
 | ||||
|     /// Converts a text offset to line and character coordinates
 | ||||
|     ///
 | ||||
|     /// # Parameters
 | ||||
|     /// * `text_size` - Byte offset in the source code
 | ||||
|     /// * `source` - Complete source text to analyze for line breaks
 | ||||
|     ///
 | ||||
|     /// # Returns
 | ||||
|     /// A position with line and character coordinates corresponding to the text offset
 | ||||
|     pub fn from_text_size(text_size: TextSize, source: &str) -> Self { | ||||
|         let mut line = 0; | ||||
|         let mut character = 0; | ||||
|  | @ -204,10 +217,21 @@ pub struct Range { | |||
| } | ||||
| 
 | ||||
| impl Range { | ||||
|     /// Creates a new range with start and end positions
 | ||||
|     ///
 | ||||
|     /// # Parameters
 | ||||
|     /// * `start` - Starting position (line and character)
 | ||||
|     /// * `end` - Ending position (line and character)
 | ||||
|     pub fn new(start: Position, end: Position) -> Self { | ||||
|         Self { start, end } | ||||
|     } | ||||
| 
 | ||||
|     /// Creates a range that covers only a single position
 | ||||
|     ///
 | ||||
|     /// Useful for diagnostics that point to a specific location rather than a range
 | ||||
|     ///
 | ||||
|     /// # Parameters
 | ||||
|     /// * `position` - The position to create a single-point range for
 | ||||
|     pub fn single_position(position: Position) -> Self { | ||||
|         Self { | ||||
|             start: position.clone(), | ||||
|  | @ -266,6 +290,16 @@ pub struct Diagnostic { | |||
| } | ||||
| 
 | ||||
| impl Diagnostic { | ||||
|     /// Creates a new diagnostic with essential information
 | ||||
|     ///
 | ||||
|     /// # Parameters
 | ||||
|     /// * `range` - The source code range the diagnostic applies to
 | ||||
|     /// * `severity` - The severity level of the diagnostic
 | ||||
|     /// * `code` - The diagnostic code indicating the type of issue
 | ||||
|     /// * `message` - A human-readable description of the diagnostic
 | ||||
|     ///
 | ||||
|     /// # Returns
 | ||||
|     /// A new diagnostic with default values for other fields
 | ||||
|     pub fn new( | ||||
|         range: Range, | ||||
|         severity: DiagnosticSeverity, | ||||
|  | @ -294,6 +328,11 @@ pub struct DiagnosticCollection { | |||
| } | ||||
| 
 | ||||
| impl DiagnosticCollection { | ||||
|     /// Creates a new diagnostic collection for a file
 | ||||
|     ///
 | ||||
|     /// # Parameters
 | ||||
|     /// * `file_path` - Path to the file being analyzed
 | ||||
|     /// * `source_text` - The content of the file
 | ||||
|     pub fn new(file_path: String, source_text: String) -> Self { | ||||
|         Self { | ||||
|             diagnostics: Vec::new(), | ||||
|  | @ -302,26 +341,44 @@ impl DiagnosticCollection { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// Adds multiple diagnostics to the collection
 | ||||
|     ///
 | ||||
|     /// # Parameters
 | ||||
|     /// * `diagnostics` - Vector of diagnostics to add
 | ||||
|     pub fn extend(&mut self, diagnostics: Vec<Diagnostic>) { | ||||
|         self.diagnostics.extend(diagnostics); | ||||
|     } | ||||
| 
 | ||||
|     /// Returns an iterator over all error-level diagnostics in the collection
 | ||||
|     pub fn errors(&self) -> impl Iterator<Item = &Diagnostic> { | ||||
|         self.diagnostics | ||||
|             .iter() | ||||
|             .filter(|d| d.severity == DiagnosticSeverity::Error) | ||||
|     } | ||||
| 
 | ||||
|     /// Checks if the collection contains any error-level diagnostics
 | ||||
|     pub fn has_errors(&self) -> bool { | ||||
|         self.errors().count() > 0 | ||||
|     } | ||||
| 
 | ||||
|     /// Convert to JSON for LSP or tooling integration
 | ||||
|     /// Converts the diagnostic collection to JSON format
 | ||||
|     ///
 | ||||
|     /// Useful for integrating with Language Server Protocol (LSP) clients
 | ||||
|     /// or other tools that consume structured diagnostic data.
 | ||||
|     ///
 | ||||
|     /// # Returns
 | ||||
|     /// A JSON string representation of the diagnostic collection, or an error if serialization fails
 | ||||
|     pub fn to_json(&self) -> serde_json::Result<String> { | ||||
|         serde_json::to_string_pretty(self) | ||||
|     } | ||||
| 
 | ||||
|     /// Convert to a human-readable format
 | ||||
|     /// Converts the diagnostic collection to a human-readable text format
 | ||||
|     ///
 | ||||
|     /// Produces a formatted report suitable for display in a terminal,
 | ||||
|     /// with file locations, error codes, and code snippets for context.
 | ||||
|     ///
 | ||||
|     /// # Returns
 | ||||
|     /// A formatted string containing all diagnostics with relevant context
 | ||||
|     pub fn to_human_readable(&self) -> String { | ||||
|         let mut output = String::new(); | ||||
| 
 | ||||
|  | @ -359,6 +416,18 @@ impl DiagnosticCollection { | |||
|         output | ||||
|     } | ||||
| 
 | ||||
|     /// Extracts source code lines around a diagnostic location
 | ||||
|     ///
 | ||||
|     /// Provides context for a diagnostic by showing the lines of code surrounding
 | ||||
|     /// the issue, with line numbers and a marker pointing to the problematic line.
 | ||||
|     ///
 | ||||
|     /// # Parameters
 | ||||
|     /// * `range` - The range in the source code to provide context for
 | ||||
|     /// * `context_lines` - Number of lines to include before and after the range
 | ||||
|     ///
 | ||||
|     /// # Returns
 | ||||
|     /// A vector of formatted strings containing the context lines with line numbers,
 | ||||
|     /// or None if the range is invalid
 | ||||
|     fn get_context_lines(&self, range: &Range, context_lines: usize) -> Option<Vec<String>> { | ||||
|         let lines: Vec<&str> = self.source_text.lines().collect(); | ||||
|         let start_line = range.start.line as usize; | ||||
|  | @ -435,9 +504,7 @@ impl AnalyzerModule for LexicalAnalyzer { | |||
|         let mut lexer = NftablesLexer::new(source); | ||||
| 
 | ||||
|         match lexer.tokenize() { | ||||
|             Ok(_) => { | ||||
|                 // No lexical errors
 | ||||
|             } | ||||
|             Ok(_) => {} | ||||
|             Err(lex_error) => { | ||||
|                 let diagnostic = Self::lex_error_to_diagnostic(&lex_error, source); | ||||
|                 diagnostics.push(diagnostic); | ||||
|  | @ -453,6 +520,17 @@ impl AnalyzerModule for LexicalAnalyzer { | |||
| } | ||||
| 
 | ||||
| impl LexicalAnalyzer { | ||||
|     /// Converts a lexical error to a diagnostic
 | ||||
|     ///
 | ||||
|     /// Translates lexer-specific errors into the generic diagnostic format
 | ||||
|     /// with appropriate severity, code, and location information.
 | ||||
|     ///
 | ||||
|     /// # Parameters
 | ||||
|     /// * `error` - The lexical error to convert
 | ||||
|     /// * `source` - Source code for position calculation
 | ||||
|     ///
 | ||||
|     /// # Returns
 | ||||
|     /// A diagnostic describing the lexical error
 | ||||
|     pub fn lex_error_to_diagnostic(error: &LexError, source: &str) -> Diagnostic { | ||||
|         match error { | ||||
|             LexError::InvalidToken { position, text } => { | ||||
|  | @ -508,18 +586,14 @@ impl AnalyzerModule for SyntaxAnalyzer { | |||
|             Ok(tokens) => { | ||||
|                 let mut parser = Parser::new(tokens); | ||||
|                 match parser.parse() { | ||||
|                     Ok(_) => { | ||||
|                         // No parse errors
 | ||||
|                     } | ||||
|                     Ok(_) => {} | ||||
|                     Err(parse_error) => { | ||||
|                         let diagnostic = Self::parse_error_to_diagnostic(&parse_error, source); | ||||
|                         diagnostics.push(diagnostic); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             Err(_) => { | ||||
|                 // Already handled in lexical analysis
 | ||||
|             } | ||||
|             Err(_) => {} | ||||
|         } | ||||
| 
 | ||||
|         diagnostics | ||||
|  | @ -531,6 +605,17 @@ impl AnalyzerModule for SyntaxAnalyzer { | |||
| } | ||||
| 
 | ||||
| impl SyntaxAnalyzer { | ||||
|     /// Converts a parse error to a diagnostic
 | ||||
|     ///
 | ||||
|     /// Translates parser-specific errors into the generic diagnostic format
 | ||||
|     /// with appropriate severity, code, and meaningful error messages.
 | ||||
|     ///
 | ||||
|     /// # Parameters
 | ||||
|     /// * `error` - The parse error to convert
 | ||||
|     /// * `_source` - Source code for position calculation
 | ||||
|     ///
 | ||||
|     /// # Returns
 | ||||
|     /// A diagnostic describing the syntax error
 | ||||
|     fn parse_error_to_diagnostic(error: &ParseError, _source: &str) -> Diagnostic { | ||||
|         match error { | ||||
|             ParseError::UnexpectedToken { | ||||
|  | @ -607,12 +692,9 @@ impl AnalyzerModule for StyleAnalyzer { | |||
|     fn analyze(&self, source: &str, config: &DiagnosticConfig) -> Vec<Diagnostic> { | ||||
|         let mut diagnostics = Vec::new(); | ||||
| 
 | ||||
|         // Only perform style analysis if enabled
 | ||||
|         if !config.enable_style_warnings { | ||||
|             return diagnostics; | ||||
|         } | ||||
| 
 | ||||
|         // Check for missing shebang
 | ||||
|         if !source.starts_with("#!") { | ||||
|             let range = Range::new(Position::new(0, 0), Position::new(0, 0)); | ||||
|             let diagnostic = Diagnostic::new( | ||||
|  | @ -637,6 +719,11 @@ impl AnalyzerModule for StyleAnalyzer { | |||
| } | ||||
| 
 | ||||
| impl StyleAnalyzer { | ||||
|     /// Analyzes line issues in nftables configuration
 | ||||
|     /// 
 | ||||
|     /// Detects:
 | ||||
|     /// - Lines exceeding maximum length
 | ||||
|     /// - Trailing whitespace at end of lines
 | ||||
|     fn analyze_line_issues(&self, source: &str, config: &DiagnosticConfig) -> Vec<Diagnostic> { | ||||
|         let mut diagnostics = Vec::new(); | ||||
| 
 | ||||
|  | @ -680,6 +767,11 @@ impl StyleAnalyzer { | |||
|         diagnostics | ||||
|     } | ||||
| 
 | ||||
|     /// Analyzes whitespace issues in nftables configuration
 | ||||
|     /// 
 | ||||
|     /// Detects:
 | ||||
|     /// - Too many consecutive empty lines
 | ||||
|     /// - Trailing empty lines at end of file
 | ||||
|     fn analyze_whitespace_issues( | ||||
|         &self, | ||||
|         source: &str, | ||||
|  | @ -736,6 +828,12 @@ impl StyleAnalyzer { | |||
|         diagnostics | ||||
|     } | ||||
| 
 | ||||
|     /// Analyzes indentation consistency in nftables configuration
 | ||||
|     ///
 | ||||
|     /// Checks for:
 | ||||
|     /// - Mixed tabs and spaces in a single line
 | ||||
|     /// - Inconsistent indentation styles across the file
 | ||||
|     /// - Adherence to preferred indentation style if specified
 | ||||
|     fn analyze_indentation(&self, source: &str, config: &DiagnosticConfig) -> Vec<Diagnostic> { | ||||
|         let mut diagnostics = Vec::new(); | ||||
|         let mut has_tabs = false; | ||||
|  | @ -810,23 +908,19 @@ impl AnalyzerModule for SemanticAnalyzer { | |||
|     fn analyze(&self, source: &str, config: &DiagnosticConfig) -> Vec<Diagnostic> { | ||||
|         let mut diagnostics = Vec::new(); | ||||
| 
 | ||||
|         // Always run semantic validation (syntax/semantic errors)
 | ||||
|         diagnostics.extend(self.validate_table_declarations(source)); | ||||
|         diagnostics.extend(self.validate_chain_declarations_semantic(source)); | ||||
|         diagnostics.extend(self.validate_cidr_notation(source)); | ||||
| 
 | ||||
|         // Best practices checks (only if enabled)
 | ||||
|         if config.enable_best_practices { | ||||
|             diagnostics.extend(self.validate_chain_best_practices(source)); | ||||
|             diagnostics.extend(self.check_for_redundant_rules(source)); | ||||
|         } | ||||
| 
 | ||||
|         // Performance hints (only if enabled)
 | ||||
|         if config.enable_performance_hints { | ||||
|             diagnostics.extend(self.check_performance_hints(source)); | ||||
|         } | ||||
| 
 | ||||
|         // Security warnings (only if enabled)
 | ||||
|         if config.enable_security_warnings { | ||||
|             diagnostics.extend(self.check_security_warnings(source)); | ||||
|         } | ||||
|  | @ -840,6 +934,11 @@ impl AnalyzerModule for SemanticAnalyzer { | |||
| } | ||||
| 
 | ||||
| impl SemanticAnalyzer { | ||||
|     /// Validates table declarations in nftables configuration
 | ||||
|     /// 
 | ||||
|     /// Checks for:
 | ||||
|     /// - Valid table family (ip, ip6, inet, arp, bridge, netdev)
 | ||||
|     /// - Duplicate table names
 | ||||
|     fn validate_table_declarations(&self, source: &str) -> Vec<Diagnostic> { | ||||
|         let mut diagnostics = Vec::new(); | ||||
|         let mut seen_tables = HashSet::new(); | ||||
|  | @ -854,11 +953,8 @@ impl SemanticAnalyzer { | |||
|                     let family = parts[1]; | ||||
|                     let name = parts[2]; | ||||
| 
 | ||||
|                     // Validate family
 | ||||
|                     match family { | ||||
|                         "ip" | "ip6" | "inet" | "arp" | "bridge" | "netdev" => { | ||||
|                             // Valid family
 | ||||
|                         } | ||||
|                         "ip" | "ip6" | "inet" | "arp" | "bridge" | "netdev" => {} | ||||
|                         _ => { | ||||
|                             let start_col = line.find(family).unwrap_or(0); | ||||
|                             let range = Range::new( | ||||
|  | @ -900,6 +996,11 @@ impl SemanticAnalyzer { | |||
|         diagnostics | ||||
|     } | ||||
| 
 | ||||
|     /// Validates chain declarations for correct hook types
 | ||||
|     ///
 | ||||
|     /// Checks for valid hooks in chain declarations:
 | ||||
|     /// - input, output, forward, prerouting, postrouting
 | ||||
|     /// - Reports unknown hooks as errors
 | ||||
|     fn validate_chain_declarations_semantic(&self, source: &str) -> Vec<Diagnostic> { | ||||
|         let mut diagnostics = Vec::new(); | ||||
| 
 | ||||
|  | @ -908,7 +1009,6 @@ impl SemanticAnalyzer { | |||
|             let trimmed = line.trim(); | ||||
| 
 | ||||
|             if trimmed.starts_with("type ") && trimmed.contains("hook") { | ||||
|                 // Validate chain type and hook (semantic validation)
 | ||||
|                 if let Some(hook_pos) = trimmed.find("hook") { | ||||
|                     let hook_part = &trimmed[hook_pos..]; | ||||
|                     let hook_words: Vec<&str> = hook_part.split_whitespace().collect(); | ||||
|  | @ -916,9 +1016,7 @@ impl SemanticAnalyzer { | |||
|                     if hook_words.len() >= 2 { | ||||
|                         let hook = hook_words[1]; | ||||
|                         match hook { | ||||
|                             "input" | "output" | "forward" | "prerouting" | "postrouting" => { | ||||
|                                 // Valid hook
 | ||||
|                             } | ||||
|                             "input" | "output" | "forward" | "prerouting" | "postrouting" => {} | ||||
|                             _ => { | ||||
|                                 let start_col = line.find(hook).unwrap_or(0); | ||||
|                                 let range = Range::new( | ||||
|  | @ -942,6 +1040,10 @@ impl SemanticAnalyzer { | |||
|         diagnostics | ||||
|     } | ||||
| 
 | ||||
|     /// Checks for best practices in nftables chain definitions
 | ||||
|     ///
 | ||||
|     /// Verifies that:
 | ||||
|     /// - Filter chains have explicit policies defined
 | ||||
|     fn validate_chain_best_practices(&self, source: &str) -> Vec<Diagnostic> { | ||||
|         let mut diagnostics = Vec::new(); | ||||
| 
 | ||||
|  | @ -949,7 +1051,6 @@ impl SemanticAnalyzer { | |||
|             let line_num = line_idx as u32; | ||||
|             let trimmed = line.trim(); | ||||
| 
 | ||||
|             // Check for missing policy in filter chains (best practice)
 | ||||
|             if trimmed.contains("type filter") && !trimmed.contains("policy") { | ||||
|                 let range = Range::new( | ||||
|                     Position::new(line_num, 0), | ||||
|  | @ -968,6 +1069,11 @@ impl SemanticAnalyzer { | |||
|         diagnostics | ||||
|     } | ||||
| 
 | ||||
|     /// Analyzes nftables configuration for performance optimizations
 | ||||
|     ///
 | ||||
|     /// Checks for:
 | ||||
|     /// - Large sets without timeouts
 | ||||
|     /// - Rules without counters for monitoring
 | ||||
|     fn check_performance_hints(&self, source: &str) -> Vec<Diagnostic> { | ||||
|         let mut diagnostics = Vec::new(); | ||||
| 
 | ||||
|  | @ -975,9 +1081,7 @@ impl SemanticAnalyzer { | |||
|             let line_num = line_idx as u32; | ||||
|             let trimmed = line.trim(); | ||||
| 
 | ||||
|             // Check for large sets without timeout (performance hint)
 | ||||
|             if trimmed.contains("set ") && trimmed.contains("{") && !trimmed.contains("timeout") { | ||||
|                 // Simple heuristic: if set definition is long, suggest timeout
 | ||||
|                 if trimmed.len() > 100 { | ||||
|                     let range = Range::new( | ||||
|                         Position::new(line_num, 0), | ||||
|  | @ -1015,6 +1119,10 @@ impl SemanticAnalyzer { | |||
|         diagnostics | ||||
|     } | ||||
| 
 | ||||
|     /// Checks for security risks in nftables configuration
 | ||||
|     ///
 | ||||
|     /// Identifies:
 | ||||
|     /// - Overly permissive rules that accept traffic from anywhere (0.0.0.0/0 or ::/0)
 | ||||
|     fn check_security_warnings(&self, source: &str) -> Vec<Diagnostic> { | ||||
|         let mut diagnostics = Vec::new(); | ||||
| 
 | ||||
|  | @ -1044,35 +1152,62 @@ impl SemanticAnalyzer { | |||
|         diagnostics | ||||
|     } | ||||
| 
 | ||||
|     /// Validates CIDR notation in nftables configuration
 | ||||
|     ///
 | ||||
|     /// Checks for:
 | ||||
|     /// - Valid IP address format (both IPv4 and IPv6)
 | ||||
|     /// - Valid prefix length (0-32 for IPv4, 0-128 for IPv6)
 | ||||
|     /// - Valid numeric prefix format
 | ||||
|     fn validate_cidr_notation(&self, source: &str) -> Vec<Diagnostic> { | ||||
|         let mut diagnostics = Vec::new(); | ||||
| 
 | ||||
|         for (line_idx, line) in source.lines().enumerate() { | ||||
|             let line_num = line_idx as u32; | ||||
| 
 | ||||
|             // Simple CIDR validation without regex dependency
 | ||||
|             // Look for potential CIDR notation patterns
 | ||||
|             let words: Vec<&str> = line.split_whitespace().collect(); | ||||
|             for word in words { | ||||
|                 if word.contains('/') && word.chars().any(|c| c.is_ascii_digit()) { | ||||
|                     if let Some(slash_pos) = word.find('/') { | ||||
|                         let (ip_part, prefix_part) = word.split_at(slash_pos); | ||||
|                         let prefix_part = &prefix_part[1..]; // Remove the '/'
 | ||||
|                         let prefix_part = &prefix_part[1..]; | ||||
| 
 | ||||
|                         // Basic IPv4 validation
 | ||||
|                         let ip_parts: Vec<&str> = ip_part.split('.').collect(); | ||||
|                         if ip_parts.len() == 4 { | ||||
|                             let mut valid_ip = true; | ||||
|                             for part in ip_parts { | ||||
|                                 if part.parse::<u8>().is_err() { | ||||
|                                     valid_ip = false; | ||||
|                                     break; | ||||
|                         match ip_part.parse::<IpAddr>() { | ||||
|                             Ok(ip_addr) => match prefix_part.parse::<u8>() { | ||||
|                                 Ok(prefix) => { | ||||
|                                     let max_prefix = match ip_addr { | ||||
|                                         IpAddr::V4(_) => 32, | ||||
|                                         IpAddr::V6(_) => 128, | ||||
|                                     }; | ||||
| 
 | ||||
|                                     if prefix > max_prefix { | ||||
|                                         if let Some(start_col) = line.find(word) { | ||||
|                                             let range = Range::new( | ||||
|                                                 Position::new(line_num, start_col as u32), | ||||
|                                                 Position::new( | ||||
|                                                     line_num, | ||||
|                                                     (start_col + word.len()) as u32, | ||||
|                                                 ), | ||||
|                                             ); | ||||
|                                             let ip_type = match ip_addr { | ||||
|                                                 IpAddr::V4(_) => "IPv4", | ||||
|                                                 IpAddr::V6(_) => "IPv6", | ||||
|                                             }; | ||||
|                                             let diagnostic = Diagnostic::new( | ||||
|                                                 range, | ||||
|                                                 DiagnosticSeverity::Error, | ||||
|                                                 DiagnosticCode::InvalidCidrNotation, | ||||
|                                                 format!( | ||||
|                                                     "Invalid CIDR prefix length: '{}' (max {} for {})", | ||||
|                                                     prefix, max_prefix, ip_type | ||||
|                                                 ), | ||||
|                                             ); | ||||
|                                             diagnostics.push(diagnostic); | ||||
|                                         } | ||||
|                                     } | ||||
|                                 } | ||||
|                             } | ||||
| 
 | ||||
|                             // Validate prefix length
 | ||||
|                             if valid_ip { | ||||
|                                 if let Ok(prefix) = prefix_part.parse::<u8>() { | ||||
|                                     if prefix > 32 { | ||||
|                                 Err(_) => { | ||||
|                                     if !prefix_part.is_empty() { | ||||
|                                         if let Some(start_col) = line.find(word) { | ||||
|                                             let range = Range::new( | ||||
|                                                 Position::new(line_num, start_col as u32), | ||||
|  | @ -1086,14 +1221,23 @@ impl SemanticAnalyzer { | |||
|                                                 DiagnosticSeverity::Error, | ||||
|                                                 DiagnosticCode::InvalidCidrNotation, | ||||
|                                                 format!( | ||||
|                                                     "Invalid CIDR prefix length: '{}' (max 32 for IPv4)", | ||||
|                                                     prefix | ||||
|                                                     "Invalid CIDR prefix: '{}' must be a number", | ||||
|                                                     prefix_part | ||||
|                                                 ), | ||||
|                                             ); | ||||
|                                             diagnostics.push(diagnostic); | ||||
|                                         } | ||||
|                                     } | ||||
|                                 } else if !prefix_part.is_empty() { | ||||
|                                 } | ||||
|                             }, | ||||
|                             Err(_) => { | ||||
|                                 if (ip_part.contains('.') | ||||
|                                     && ip_part.chars().any(|c| c.is_ascii_digit())) | ||||
|                                     || (ip_part.contains(':') | ||||
|                                         && ip_part | ||||
|                                             .chars() | ||||
|                                             .any(|c| c.is_ascii_digit() || c.is_ascii_hexdigit())) | ||||
|                                 { | ||||
|                                     if let Some(start_col) = line.find(word) { | ||||
|                                         let range = Range::new( | ||||
|                                             Position::new(line_num, start_col as u32), | ||||
|  | @ -1106,7 +1250,10 @@ impl SemanticAnalyzer { | |||
|                                             range, | ||||
|                                             DiagnosticSeverity::Error, | ||||
|                                             DiagnosticCode::InvalidCidrNotation, | ||||
|                                             format!("Invalid CIDR notation: '{}'", word), | ||||
|                                             format!( | ||||
|                                                 "Invalid IP address in CIDR notation: '{}'", | ||||
|                                                 ip_part | ||||
|                                             ), | ||||
|                                         ); | ||||
|                                         diagnostics.push(diagnostic); | ||||
|                                     } | ||||
|  | @ -1121,6 +1268,10 @@ impl SemanticAnalyzer { | |||
|         diagnostics | ||||
|     } | ||||
| 
 | ||||
|     /// Identifies redundant rules in nftables configuration
 | ||||
|     ///
 | ||||
|     /// Detects:
 | ||||
|     /// - Duplicate accept/drop/reject rules
 | ||||
|     fn check_for_redundant_rules(&self, source: &str) -> Vec<Diagnostic> { | ||||
|         let mut diagnostics = Vec::new(); | ||||
|         let mut seen_rules = HashSet::new(); | ||||
|  | @ -1129,12 +1280,10 @@ impl SemanticAnalyzer { | |||
|             let line_num = line_idx as u32; | ||||
|             let trimmed = line.trim(); | ||||
| 
 | ||||
|             // Simple rule detection (lines that contain actions)
 | ||||
|             if trimmed.contains(" accept") | ||||
|                 || trimmed.contains(" drop") | ||||
|                 || trimmed.contains(" reject") | ||||
|             { | ||||
|                 // Check if rule already exists in the HashSet
 | ||||
|                 if seen_rules.contains(trimmed) { | ||||
|                     let range = Range::new( | ||||
|                         Position::new(line_num, 0), | ||||
|  | @ -1148,7 +1297,6 @@ impl SemanticAnalyzer { | |||
|                     ); | ||||
|                     diagnostics.push(diagnostic); | ||||
|                 } else { | ||||
|                     // Add the rule to the HashSet if it's not a duplicate
 | ||||
|                     seen_rules.insert(trimmed.to_string()); | ||||
|                 } | ||||
|             } | ||||
|  | @ -1164,11 +1312,25 @@ pub struct DiagnosticAnalyzer { | |||
| } | ||||
| 
 | ||||
| impl DiagnosticAnalyzer { | ||||
|     /// Creates a new diagnostic analyzer with the specified configuration
 | ||||
|     ///
 | ||||
|     /// # Parameters
 | ||||
|     /// * `config` - Configuration settings for the analyzer
 | ||||
|     pub fn new(config: DiagnosticConfig) -> Self { | ||||
|         Self { config } | ||||
|     } | ||||
| 
 | ||||
|     /// Analyze source code with all available modules
 | ||||
|     /// Analyzes source code with all standard analysis modules
 | ||||
|     ///
 | ||||
|     /// This is the main entry point for a complete analysis of nftables configurations.
 | ||||
|     /// It runs lexical, syntax, style, and semantic analysis on the provided source.
 | ||||
|     ///
 | ||||
|     /// # Parameters
 | ||||
|     /// * `source` - Source code to analyze
 | ||||
|     /// * `file_path` - Path to the file being analyzed
 | ||||
|     ///
 | ||||
|     /// # Returns
 | ||||
|     /// A collection of all diagnostics found in the source
 | ||||
|     pub fn analyze(&self, source: &str, file_path: &str) -> DiagnosticCollection { | ||||
|         self.analyze_with_modules( | ||||
|             source, | ||||
|  | @ -1177,7 +1339,17 @@ impl DiagnosticAnalyzer { | |||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /// Analyze source code with specific modules
 | ||||
|     /// Analyzes source code with specific analysis modules
 | ||||
|     ///
 | ||||
|     /// Allows running only selected analysis modules for more targeted diagnostics.
 | ||||
|     ///
 | ||||
|     /// # Parameters
 | ||||
|     /// * `source` - Source code to analyze
 | ||||
|     /// * `file_path` - Path to the file being analyzed
 | ||||
|     /// * `module_names` - Names of modules to run ("lexical", "syntax", "style", "semantic")
 | ||||
|     ///
 | ||||
|     /// # Returns
 | ||||
|     /// A collection of diagnostics from the selected modules
 | ||||
|     pub fn analyze_with_modules( | ||||
|         &self, | ||||
|         source: &str, | ||||
|  | @ -1258,4 +1430,184 @@ mod tests { | |||
|                 .any(|d| d.code == DiagnosticCode::TrailingWhitespace) | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_cidr_validation_ipv4_valid() { | ||||
|         let analyzer = SemanticAnalyzer; | ||||
|         let source = "ip saddr 192.168.1.0/24 accept"; | ||||
|         let diagnostics = analyzer.validate_cidr_notation(source); | ||||
| 
 | ||||
|         // Verify valid IPv4 CIDR notation doesn't produce errors
 | ||||
|         assert!( | ||||
|             !diagnostics | ||||
|                 .iter() | ||||
|                 .any(|d| d.code == DiagnosticCode::InvalidCidrNotation) | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_cidr_validation_ipv4_invalid_prefix() { | ||||
|         let analyzer = SemanticAnalyzer; | ||||
|         let source = "ip saddr 192.168.1.0/33 accept"; | ||||
|         let diagnostics = analyzer.validate_cidr_notation(source); | ||||
| 
 | ||||
|         // Verify detection of IPv4 prefix exceeding max (32)
 | ||||
|         assert!(diagnostics.iter().any(|d| { | ||||
|             d.code == DiagnosticCode::InvalidCidrNotation | ||||
|                 && d.message | ||||
|                     .contains("Invalid CIDR prefix length: '33' (max 32 for IPv4)") | ||||
|         })); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_cidr_validation_ipv6_valid() { | ||||
|         let analyzer = SemanticAnalyzer; | ||||
|         let source = "ip6 saddr 2001:db8::/32 accept"; | ||||
|         let diagnostics = analyzer.validate_cidr_notation(source); | ||||
| 
 | ||||
|         // Verify valid IPv6 CIDR notation doesn't produce errors
 | ||||
|         assert!( | ||||
|             !diagnostics | ||||
|                 .iter() | ||||
|                 .any(|d| d.code == DiagnosticCode::InvalidCidrNotation) | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_cidr_validation_ipv6_invalid_prefix() { | ||||
|         let analyzer = SemanticAnalyzer; | ||||
|         let source = "ip6 saddr 2001:db8::/129 accept"; | ||||
|         let diagnostics = analyzer.validate_cidr_notation(source); | ||||
| 
 | ||||
|         // Verify detection of IPv6 prefix exceeding max (128)
 | ||||
|         assert!(diagnostics.iter().any(|d| { | ||||
|             d.code == DiagnosticCode::InvalidCidrNotation | ||||
|                 && d.message | ||||
|                     .contains("Invalid CIDR prefix length: '129' (max 128 for IPv6)") | ||||
|         })); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_cidr_validation_invalid_ip_address() { | ||||
|         let analyzer = SemanticAnalyzer; | ||||
|         let source = "ip saddr 192.168.1.256/24 accept"; | ||||
|         let diagnostics = analyzer.validate_cidr_notation(source); | ||||
| 
 | ||||
|         // Verify detection of invalid IP address format
 | ||||
|         assert!(diagnostics.iter().any(|d| { | ||||
|             d.code == DiagnosticCode::InvalidCidrNotation | ||||
|                 && d.message | ||||
|                     .contains("Invalid IP address in CIDR notation: '192.168.1.256'") | ||||
|         })); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_cidr_validation_invalid_prefix_format() { | ||||
|         let analyzer = SemanticAnalyzer; | ||||
|         let source = "ip saddr 192.168.1.0/abc accept"; | ||||
|         let diagnostics = analyzer.validate_cidr_notation(source); | ||||
| 
 | ||||
|         // Verify detection of non-numeric CIDR prefix
 | ||||
|         assert!(diagnostics.iter().any(|d| { | ||||
|             d.code == DiagnosticCode::InvalidCidrNotation | ||||
|                 && d.message | ||||
|                     .contains("Invalid CIDR prefix: 'abc' must be a number") | ||||
|         })); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_cidr_validation_ipv6_compressed_notation() { | ||||
|         let analyzer = SemanticAnalyzer; | ||||
|         let source = "ip6 saddr ::1/128 accept"; | ||||
|         let diagnostics = analyzer.validate_cidr_notation(source); | ||||
| 
 | ||||
|         // Verify compressed IPv6 notation is properly handled
 | ||||
|         assert!( | ||||
|             !diagnostics | ||||
|                 .iter() | ||||
|                 .any(|d| d.code == DiagnosticCode::InvalidCidrNotation) | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_cidr_validation_multiple_cidrs() { | ||||
|         let analyzer = SemanticAnalyzer; | ||||
|         let source = r#" | ||||
|             ip saddr 192.168.1.0/24 accept | ||||
|             ip6 saddr 2001:db8::/32 accept | ||||
|             ip saddr 10.0.0.0/8 drop | ||||
|             ip6 saddr fe80::/64 accept | ||||
|         "#;
 | ||||
|         let diagnostics = analyzer.validate_cidr_notation(source); | ||||
| 
 | ||||
|         // Verify multiple valid CIDRs are properly parsed
 | ||||
|         assert!( | ||||
|             !diagnostics | ||||
|                 .iter() | ||||
|                 .any(|d| d.code == DiagnosticCode::InvalidCidrNotation) | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_cidr_validation_mixed_valid_invalid() { | ||||
|         let analyzer = SemanticAnalyzer; | ||||
|         let source = r#" | ||||
|             ip saddr 192.168.1.0/24 accept | ||||
|             ip saddr 192.168.1.0/33 drop | ||||
|             ip6 saddr 2001:db8::/32 accept | ||||
|             ip6 saddr 2001:db8::/129 drop | ||||
|         "#;
 | ||||
|         let diagnostics = analyzer.validate_cidr_notation(source); | ||||
| 
 | ||||
|         // Verify detection of specific invalid prefixes in a mixed content
 | ||||
|         let cidr_errors: Vec<_> = diagnostics | ||||
|             .iter() | ||||
|             .filter(|d| d.code == DiagnosticCode::InvalidCidrNotation) | ||||
|             .collect(); | ||||
|         assert_eq!(cidr_errors.len(), 2); | ||||
| 
 | ||||
|         // Check for IPv4 error
 | ||||
|         assert!( | ||||
|             cidr_errors | ||||
|                 .iter() | ||||
|                 .any(|d| d.message.contains("33") && d.message.contains("IPv4")) | ||||
|         ); | ||||
|         // Check for IPv6 error
 | ||||
|         assert!( | ||||
|             cidr_errors | ||||
|                 .iter() | ||||
|                 .any(|d| d.message.contains("129") && d.message.contains("IPv6")) | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_cidr_validation_edge_cases() { | ||||
|         let analyzer = SemanticAnalyzer; | ||||
| 
 | ||||
|         // Test edge case: maximum allowed prefix lengths
 | ||||
|         let source_ipv4_max = "ip saddr 192.168.1.0/32 accept"; | ||||
|         let diagnostics_ipv4 = analyzer.validate_cidr_notation(source_ipv4_max); | ||||
|         assert!( | ||||
|             !diagnostics_ipv4 | ||||
|                 .iter() | ||||
|                 .any(|d| d.code == DiagnosticCode::InvalidCidrNotation) | ||||
|         ); | ||||
| 
 | ||||
|         let source_ipv6_max = "ip6 saddr 2001:db8::/128 accept"; | ||||
|         let diagnostics_ipv6 = analyzer.validate_cidr_notation(source_ipv6_max); | ||||
|         assert!( | ||||
|             !diagnostics_ipv6 | ||||
|                 .iter() | ||||
|                 .any(|d| d.code == DiagnosticCode::InvalidCidrNotation) | ||||
|         ); | ||||
| 
 | ||||
|         // Test edge case: minimum prefix (catch-all address)
 | ||||
|         let source_zero = "ip saddr 0.0.0.0/0 accept"; | ||||
|         let diagnostics_zero = analyzer.validate_cidr_notation(source_zero); | ||||
|         assert!( | ||||
|             !diagnostics_zero | ||||
|                 .iter() | ||||
|                 .any(|d| d.code == DiagnosticCode::InvalidCidrNotation) | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -506,7 +506,6 @@ fn convert_parse_error_to_formatter_error( | |||
|             suggestion: None, | ||||
|         }, | ||||
|         ParseError::MissingToken { expected } => { | ||||
|             // Try to find current position from last token
 | ||||
|             let (line, column) = if let Some(last_token) = tokens.last() { | ||||
|                 position_from_range(&last_token.range, source) | ||||
|             } else { | ||||
|  | @ -520,7 +519,6 @@ fn convert_parse_error_to_formatter_error( | |||
|             } | ||||
|         } | ||||
|         ParseError::InvalidExpression { message } => { | ||||
|             // Try to find the current token position
 | ||||
|             let (line, column) = find_current_parse_position(tokens, source); | ||||
|             FormatterError::SyntaxError { | ||||
|                 line, | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue