Welcome to Chapter 5 of our journey to build a production-grade Mermaid analyzer and fixer. In the previous chapters, we successfully developed a robust lexer to tokenize Mermaid input and a sophisticated parser to transform those tokens into a strongly typed Abstract Syntax Tree (AST). With the raw structure of the Mermaid diagram now represented in a programmatic form, it’s time to introduce the critical next phase: the Strict Validation Layer.
This chapter will guide you through designing and implementing a comprehensive validation system that scrutinizes the generated AST for both syntax and semantic correctness. While our parser handled basic grammatical rules, the validator goes deeper, ensuring that the diagram makes logical sense and adheres to the strict rules of Mermaid. We will detect issues like undefined nodes in edges, duplicate node IDs, illegal nesting, and other structural inconsistencies that a simple grammar-based parser might miss. The ultimate goal is to produce actionable diagnostics, similar to what the Rust compiler provides, empowering developers to quickly identify and rectify issues in their Mermaid code.
By the end of this chapter, you will have a Validator component capable of taking any Mermaid AST and returning a structured list of diagnostics. This output will be crucial for the subsequent rule engine and formatter, laying a solid foundation for a truly reliable and production-ready Mermaid tool. Let’s dive in and elevate our tool’s intelligence to pinpoint and explain common Mermaid pitfalls.
Planning & Design
The validation layer acts as a gatekeeper, ensuring that the AST, despite being syntactically correct according to the grammar, also adheres to the semantic rules and best practices of Mermaid diagrams. This involves traversing the AST and applying a series of checks.
Component Architecture
Our validation process will integrate directly after the parsing phase. The Validator component will receive the AST and produce a list of Diagnostic objects. Each Diagnostic will encapsulate details about an error or warning, including its level, a unique error code, a descriptive message, the precise location (span) in the source code, and actionable help messages.
Here’s a high-level overview of the data flow and component interaction:
File Structure
We’ll introduce new modules for diagnostics and the validator, alongside modifications to existing ones.
.
├── src/
│ ├── main.rs
│ ├── lib.rs
│ ├── lexer/
│ │ ├── mod.rs
│ │ └── token.rs
│ ├── parser/
│ │ ├── mod.rs
│ │ └── ast.rs <-- AST definitions
│ ├── diagnostics/ <-- NEW: Defines Diagnostic types and error codes
│ │ ├── mod.rs
│ │ ├── diagnostic.rs
│ │ └── error_codes.rs
│ ├── validator/ <-- NEW: Contains the core validation logic
│ │ ├── mod.rs
│ │ └── validator.rs
│ └── utils/
│ └── span.rs <-- REFACTOR: Move Span here for wider use
└── tests/
├── lexer_tests.rs
├── parser_tests.rs
├── validator_tests.rs <-- NEW: Tests for the validator
Diagnostic Design
Our Diagnostic struct will be comprehensive, allowing for rich error reporting.
// src/diagnostics/diagnostic.rs
pub enum DiagnosticLevel {
Error,
Warning,
Note,
Help,
}
pub struct Span {
pub start: usize,
pub end: usize,
pub line: usize,
pub column: usize,
}
pub struct Diagnostic {
pub level: DiagnosticLevel,
pub code: String, // e.g., "E0001", "W0001"
pub message: String,
pub span: Option<Span>,
pub help: Option<String>,
pub notes: Vec<String>,
pub suggested_fix: Option<String>, // For future auto-fixing
}
Error Codes
We’ll define a set of unique error codes. This allows for programmatic identification of issues and provides a consistent reference for documentation and user support.
// src/diagnostics/error_codes.rs
pub enum ErrorCode {
// Syntax-related errors (E0xxx)
E0001, // Missing graph declaration
E0002, // Invalid character in label
E0003, // Unexpected token (should be caught by parser, but context-sensitive checks)
E0004, // Malformed label syntax
E0005, // Invalid arrow type for diagram
// Semantic-related errors (E1xxx)
E1001, // Undefined node in edge
E1002, // Duplicate node ID
E1003, // Illegal subgraph nesting
E1004, // Invalid direction for diagram type
E1005, // Circular dependency detected (future work)
E1006, // Self-referencing node in edge without explicit loop syntax
// Warnings (W0xxx)
W0001, // Unused node declaration
W0002, // Ambiguous label (e.g., node ID and label are identical)
W0003, // Inconsistent arrow style
W0004, // Missing quotes around label with special characters
}
impl ErrorCode {
pub fn as_str(&self) -> &'static str {
match self {
ErrorCode::E0001 => "E0001",
ErrorCode::E0002 => "E0002",
ErrorCode::E0003 => "E0003",
ErrorCode::E0004 => "E0004",
ErrorCode::E0005 => "E0005",
ErrorCode::E1001 => "E1001",
ErrorCode::E1002 => "E1002",
ErrorCode::E1003 => "E1003",
ErrorCode::E1004 => "E1004",
ErrorCode::E1005 => "E1005",
ErrorCode::E1006 => "E1006",
ErrorCode::W0001 => "W0001",
ErrorCode::W0002 => "W0002",
ErrorCode::W0003 => "W0003",
ErrorCode::W0004 => "W0004",
}
}
}
Step-by-Step Implementation
a) Setup/Configuration: src/diagnostics and src/utils/span.rs
First, let’s create the necessary files and define our core diagnostic types. We’ll also centralize the Span definition.
1. Create src/utils directory and span.rs
mkdir -p src/utils
touch src/utils/span.rs
File: src/utils/span.rs
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Span {
pub start: usize, // Start byte index in the source string
pub end: usize, // End byte index in the source string (exclusive)
pub line: usize, // Start line number (1-indexed)
pub column: usize, // Start column number (1-indexed, byte-offset)
}
impl Span {
pub fn new(start: usize, end: usize, line: usize, column: usize) -> Self {
Span { start, end, line, column }
}
pub fn len(&self) -> usize {
self.end - self.start
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
- Explanation: The
Spanstruct precisely identifies a region in the source code.startandendare byte indices, which are robust for UTF-8.lineandcolumnprovide human-readable location. We deriveDebug,Clone,PartialEq,Eqfor convenience in testing and debugging.
2. Create src/diagnostics directory and files
mkdir -p src/diagnostics
touch src/diagnostics/mod.rs
touch src/diagnostics/diagnostic.rs
touch src/diagnostics/error_codes.rs
File: src/diagnostics/mod.rs
pub mod diagnostic;
pub mod error_codes;
pub use diagnostic::{Diagnostic, DiagnosticLevel, Span};
pub use error_codes::ErrorCode;
- Explanation: This
mod.rsmakes the diagnostic types publicly accessible fromsrc/diagnostics.
File: src/diagnostics/diagnostic.rs
use crate::diagnostics::error_codes::ErrorCode;
use crate::utils::span::Span; // Use the centralized Span
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DiagnosticLevel {
Error,
Warning,
Note,
Help,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Diagnostic {
pub level: DiagnosticLevel,
pub code: ErrorCode, // Use the enum
pub message: String,
pub span: Option<Span>,
pub help: Option<String>,
pub notes: Vec<String>,
pub suggested_fix: Option<String>, // For future auto-fixing
}
impl Diagnostic {
pub fn new(level: DiagnosticLevel, code: ErrorCode, message: String) -> Self {
Diagnostic {
level,
code,
message,
span: None,
help: None,
notes: Vec::new(),
suggested_fix: None,
}
}
pub fn with_span(mut self, span: Span) -> Self {
self.span = Some(span);
self
}
pub fn with_help(mut self, help: String) -> Self {
self.help = Some(help);
self
}
pub fn with_note(mut self, note: String) -> Self {
self.notes.push(note);
self
}
pub fn with_suggested_fix(mut self, fix: String) -> Self {
self.suggested_fix = Some(fix);
self
}
}
// Implement Display for Diagnostic for pretty printing (Rust-compiler style)
impl std::fmt::Display for Diagnostic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let level_str = match self.level {
DiagnosticLevel::Error => "error",
DiagnosticLevel::Warning => "warning",
DiagnosticLevel::Note => "note",
DiagnosticLevel::Help => "help",
};
write!(f, "{}[{}]: {}", level_str, self.code.as_str(), self.message)?;
if let Some(span) = &self.span {
// In a real CLI, we'd read the source file to highlight the span.
// For now, we'll just print the location.
write!(f, "\n --> {}:{}:{}", "input.mmd", span.line, span.column)?;
// TODO: Add source snippet highlighting here in a later chapter
}
if let Some(help) = &self.help {
write!(f, "\n = help: {}", help)?;
}
for note in &self.notes {
write!(f, "\n = note: {}", note)?;
}
if let Some(fix) = &self.suggested_fix {
write!(f, "\n = suggested fix: {}", fix)?;
}
Ok(())
}
}
- Explanation: This file defines the
Diagnosticstruct and its associated methods for construction. It also provides aDisplayimplementation that formats diagnostics in a compiler-like style, including error level, code, message, and location. We’re usinginput.mmdas a placeholder filename for now.
File: src/diagnostics/error_codes.rs
// This file was defined in the planning section, just ensure it's here.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ErrorCode {
// Syntax-related errors (E0xxx)
E0001, // Missing graph declaration
E0002, // Invalid character in label
E0003, // Unexpected token (should be caught by parser, but context-sensitive checks)
E0004, // Malformed label syntax
E0005, // Invalid arrow type for diagram
// Semantic-related errors (E1xxx)
E1001, // Undefined node in edge
E1002, // Duplicate node ID
E1003, // Illegal subgraph nesting
E1004, // Invalid direction for diagram type
E1005, // Circular dependency detected (future work)
E1006, // Self-referencing node in edge without explicit loop syntax
// Warnings (W0xxx)
W0001, // Unused node declaration
W0002, // Ambiguous label (e.g., node ID and label are identical)
W0003, // Inconsistent arrow style
W0004, // Missing quotes around label with special characters
}
impl ErrorCode {
pub fn as_str(&self) -> &'static str {
match self {
ErrorCode::E0001 => "E0001",
ErrorCode::E0002 => "E0002",
ErrorCode::E0003 => "E0003",
ErrorCode::E0004 => "E0004",
ErrorCode::E0005 => "E0005",
ErrorCode::E1001 => "E1001",
ErrorCode::E1002 => "E1002",
ErrorCode::E1003 => "E1003",
ErrorCode::E1004 => "E1004",
ErrorCode::E1005 => "E1005",
ErrorCode::E1006 => "E1006",
ErrorCode::W0001 => "W0001",
ErrorCode::W0002 => "W0002",
ErrorCode::W0003 => "W0003",
ErrorCode::W0004 => "W0004",
}
}
}
- Explanation: This enum provides strongly typed error codes, making our diagnostics more robust and manageable. The
as_strmethod provides the string representation for display.
3. Update src/lib.rs and src/parser/ast.rs
We need to ensure Span is correctly integrated into our AST nodes.
File: src/lib.rs
// Add these modules
pub mod diagnostics;
pub mod utils; // Make utils public
// Re-export Span from utils
pub use utils::span::Span;
// ... other existing pub mods
File: src/parser/ast.rs (Partial, only showing changes)
use crate::utils::span::Span; // Import Span
// --- Diagram Types ---
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Diagram {
Flowchart(Flowchart),
Sequence(SequenceDiagram),
Class(ClassDiagram),
// Add a default span for the entire diagram, or None if not directly tied
// For now, let's assume Diagram itself doesn't have a span directly,
// its components do.
}
// --- Flowchart Specifics ---
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Flowchart {
pub direction: Option<FlowchartDirection>,
pub statements: Vec<FlowchartStatement>,
pub span: Span, // Add span to Flowchart
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FlowchartStatement {
Node(Node),
Edge(Edge),
Subgraph(Subgraph),
// Add span to the statement enum if needed for generic statement errors
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Node {
pub id: String,
pub label: Option<String>,
pub kind: NodeKind,
pub span: Span, // Crucial for diagnostics
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Edge {
pub source: EdgeEndpoint,
pub target: EdgeEndpoint,
pub label: Option<String>,
pub arrow: ArrowType,
pub span: Span, // Crucial for diagnostics
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EdgeEndpoint {
Node(String, Span), // Store span for node ID reference
Subgraph(String, Span), // Store span for subgraph ID reference
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Subgraph {
pub id: String,
pub label: Option<String>,
pub statements: Vec<FlowchartStatement>,
pub span: Span, // Crucial for diagnostics
}
// ... similar additions for SequenceDiagram, ClassDiagram, etc.
// Ensure all relevant AST nodes (Node, Edge, Subgraph, Participant, Class, etc.)
// have a `pub span: Span` field. If you already did this in Chapter 4, great!
// Otherwise, go back and add it now.
- Explanation: We’ve updated the
parser::astmodule to use theSpanstruct fromsrc/utils/span.rs. Critically, every AST node that can be a source of an error (e.g.,Node,Edge,Subgraph,EdgeEndpoint) must carry itsSpaninformation. This allows the validator to report errors with precise locations.
b) Core Implementation: src/validator/validator.rs
Now, let’s build the Validator itself.
1. Create src/validator directory and validator.rs
mkdir -p src/validator
touch src/validator/mod.rs
touch src/validator/validator.rs
File: src/validator/mod.rs
pub mod validator;
pub use validator::Validator;
- Explanation: Makes the
Validatorpublicly accessible.
File: src/validator/validator.rs
use std::collections::{HashMap, HashSet};
use crate::diagnostics::{Diagnostic, DiagnosticLevel, ErrorCode, Span};
use crate::parser::ast::{
ArrowType, ClassDiagram, Diagram, Edge, EdgeEndpoint, Flowchart, FlowchartStatement, Node,
NodeKind, SequenceDiagram, Subgraph,
};
#[derive(Debug)]
pub struct Validator {
diagnostics: Vec<Diagnostic>,
// Store defined node IDs and their spans for semantic checks
defined_nodes: HashMap<String, Span>,
// Keep track of visited node IDs to detect duplicates within the same scope
current_scope_nodes: HashSet<String>,
// Reference to the original source text (optional, but useful for rich diagnostics)
// For this chapter, we'll assume we don't have it here directly, but could add it.
}
impl Validator {
pub fn new() -> Self {
Validator {
diagnostics: Vec::new(),
defined_nodes: HashMap::new(),
current_scope_nodes: HashSet::new(),
}
}
/// Validates the given Mermaid AST and returns a list of diagnostics.
pub fn validate(mut self, ast: &Diagram) -> Vec<Diagnostic> {
self.diagnostics.clear(); // Clear any previous diagnostics
match ast {
Diagram::Flowchart(flowchart) => self.validate_flowchart(flowchart),
Diagram::Sequence(sequence) => self.validate_sequence_diagram(sequence),
Diagram::Class(class) => self.validate_class_diagram(class),
// Handle other diagram types as they are implemented
}
self.diagnostics
}
/// Validates a Flowchart diagram.
fn validate_flowchart(&mut self, flowchart: &Flowchart) {
// Clear scope for top-level diagram
self.defined_nodes.clear();
self.current_scope_nodes.clear();
// Rule E0001: Missing graph declaration (This should largely be caught by parser,
// but if parser is lenient, we can check if flowchart is "empty" or malformed)
// For our strict parser, if we have a Flowchart enum, the 'graph' keyword was present.
// So this check might be more about an empty flowchart or one missing direction.
if flowchart.statements.is_empty() && flowchart.direction.is_none() {
self.diagnostics.push(
Diagnostic::new(
DiagnosticLevel::Warning, // Could be a warning if empty but valid.
ErrorCode::W0001,
"Flowchart is empty or has no statements.".to_string(),
)
.with_span(flowchart.span.clone())
.with_help("Consider adding nodes and edges to your flowchart.".to_string()),
);
}
self.validate_flowchart_statements(&flowchart.statements);
// After all statements are processed, clear defined_nodes for next diagram
self.defined_nodes.clear();
}
/// Validates a list of flowchart statements (nodes, edges, subgraphs).
fn validate_flowchart_statements(&mut self, statements: &[FlowchartStatement]) {
// First pass: Collect all defined nodes in this scope and check for duplicates
let mut local_nodes: HashSet<String> = HashSet::new();
for statement in statements {
match statement {
FlowchartStatement::Node(node) => {
if !local_nodes.insert(node.id.clone()) {
self.diagnostics.push(
Diagnostic::new(
DiagnosticLevel::Error,
ErrorCode::E1002,
format!("Duplicate node ID '{}' found in the same scope.", node.id),
)
.with_span(node.span.clone())
.with_help("Node IDs must be unique within their immediate scope. Rename or remove the duplicate node.".to_string()),
);
}
// Add to global defined nodes
self.defined_nodes.insert(node.id.clone(), node.span.clone());
// Rule E0002: Invalid character in label (basic check)
// Mermaid allows almost anything in quoted labels, but unquoted labels
// have restrictions. We'll simplify for now.
if let Some(label) = &node.label {
if !label.is_empty() && label.contains(|c: char| !c.is_alphanumeric() && !c.is_whitespace() && !"-_.@#$".contains(c)) {
self.diagnostics.push(
Diagnostic::new(
DiagnosticLevel::Warning, // Warning, as Mermaid might render it anyway
ErrorCode::W0004, // Suggest quotes
format!("Label '{}' contains special characters. Consider enclosing it in quotes.", label),
)
.with_span(node.span.clone())
.with_help(format!("Labels with special characters (e.g., `{}`) should be enclosed in double quotes: `node_id[\"{}\"]`", label, label))
.with_suggested_fix(format!("\"{}\"", label)),
);
}
}
}
FlowchartStatement::Subgraph(subgraph) => {
if !local_nodes.insert(subgraph.id.clone()) {
self.diagnostics.push(
Diagnostic::new(
DiagnosticLevel::Error,
ErrorCode::E1002,
format!("Duplicate subgraph ID '{}' found in the same scope.", subgraph.id),
)
.with_span(subgraph.span.clone())
.with_help("Subgraph IDs must be unique within their immediate scope. Rename or remove the duplicate subgraph.".to_string()),
);
}
// Add to global defined nodes (subgraphs can be referenced like nodes)
self.defined_nodes.insert(subgraph.id.clone(), subgraph.span.clone());
}
_ => {} // Edges don't define nodes
}
}
// Second pass: Validate edges and recursively validate subgraphs
for statement in statements {
match statement {
FlowchartStatement::Edge(edge) => self.validate_edge(edge),
FlowchartStatement::Subgraph(subgraph) => self.validate_subgraph(subgraph),
_ => {}
}
}
}
/// Validates an Edge statement.
fn validate_edge(&mut self, edge: &Edge) {
// Rule E1001: Undefined node in edge
self.check_endpoint_definition(&edge.source, "source node or subgraph");
self.check_endpoint_definition(&edge.target, "target node or subgraph");
// Rule E1006: Self-referencing node in edge without explicit loop syntax
// This is a common pattern for loops, but if it's not explicitly drawn as a loop,
// it can be confusing. For strictness, we'll flag it.
// TODO: This check might need more context about actual loop syntax.
// For now, a simple direct self-reference check.
if let (EdgeEndpoint::Node(src_id, _), EdgeEndpoint::Node(tgt_id, _)) = (&edge.source, &edge.target) {
if src_id == tgt_id {
self.diagnostics.push(
Diagnostic::new(
DiagnosticLevel::Warning,
ErrorCode::W0002, // Re-using W0002 for now, or create W0005 for self-reference
format!("Node '{}' is self-referencing in an edge. Consider using explicit loop syntax if supported by diagram type.", src_id),
)
.with_span(edge.span.clone())
.with_help("A node connecting to itself might be better represented with a specific loop style if the diagram type supports it, or ensure this is intentional.".to_string()),
);
}
}
// Rule E0005: Invalid arrow type for diagram
// Flowcharts allow various arrow types, but specific ones might be restricted in future.
// For now, we assume all parsed arrow types are valid for flowcharts.
// This rule would be more relevant for Sequence or Class diagrams.
}
/// Helper to check if an endpoint (node/subgraph) is defined.
fn check_endpoint_definition(&mut self, endpoint: &EdgeEndpoint, endpoint_type: &str) {
let (id, span) = match endpoint {
EdgeEndpoint::Node(id, span) => (id, span),
EdgeEndpoint::Subgraph(id, span) => (id, span),
};
if !self.defined_nodes.contains_key(id) {
self.diagnostics.push(
Diagnostic::new(
DiagnosticLevel::Error,
ErrorCode::E1001,
format!("Undefined {} '{}' referenced in an edge.", endpoint_type, id),
)
.with_span(span.clone())
.with_help("Ensure all nodes and subgraphs used in edges are explicitly defined with a unique ID.".to_string()),
);
}
}
/// Validates a Subgraph statement.
fn validate_subgraph(&mut self, subgraph: &Subgraph) {
// Recursively validate statements within the subgraph.
// Note: For strict scope, we'd push/pop `current_scope_nodes` here.
// For simplicity, `defined_nodes` is global for all flowcharts, allowing cross-subgraph references.
// If strict local scoping for node IDs is required, `defined_nodes` would be a stack.
// For Mermaid, cross-subgraph references are common, so a global `defined_nodes` is acceptable.
self.validate_flowchart_statements(&subgraph.statements);
// Rule E1003: Illegal subgraph nesting (e.g., in a diagram type that doesn't support it)
// This is primarily for other diagram types. For Flowcharts, arbitrary nesting is generally allowed.
}
/// Validates a Sequence Diagram.
fn validate_sequence_diagram(&mut self, sequence: &SequenceDiagram) {
// TODO: Implement specific validation rules for Sequence Diagrams
// e.g., participant definitions, valid message types, activation blocks.
self.diagnostics.push(
Diagnostic::new(
DiagnosticLevel::Note,
ErrorCode::E0000, // Placeholder
"Sequence diagram validation is not yet fully implemented.".to_string(),
)
.with_span(sequence.span.clone())
.with_help("This is a placeholder. Full validation for sequence diagrams will be added in future iterations.".to_string()),
);
}
/// Validates a Class Diagram.
fn validate_class_diagram(&mut self, class: &ClassDiagram) {
// TODO: Implement specific validation rules for Class Diagrams
// e.g., class definitions, relationships, members.
self.diagnostics.push(
Diagnostic::new(
DiagnosticLevel::Note,
ErrorCode::E0000, // Placeholder
"Class diagram validation is not yet fully implemented.".to_string(),
)
.with_span(class.span.clone())
.with_help("This is a placeholder. Full validation for class diagrams will be added in future iterations.".to_string()),
);
}
}
- Explanation:
- The
Validatorstruct holds a list ofDiagnosticobjects and aHashMap(defined_nodes) to track all node and subgraph IDs encountered, along with theirSpans. ThisHashMapis crucial for semantic checks like “undefined node in edge.” validateis the entry point, dispatching to diagram-specific validation methods.validate_flowchartorchestrates the validation for flowcharts. It first clears the scope, then performs two passes over the statements:- First pass: Collects all
NodeandSubgraphIDs, adding them todefined_nodesandlocal_nodes. It also checks forE1002: Duplicate node IDwithin the current scope. - Second pass: Validates
Edgestatements (checking forE1001: Undefined node in edgeandW0002: Self-referencing node) and recursively callsvalidate_subgraphfor nested subgraphs.
- First pass: Collects all
check_endpoint_definitionis a helper forE1001.- Basic
W0004: Missing quotes around label with special charactersis included for nodes. - Placeholders for
SequenceDiagramandClassDiagramvalidation are added, indicating future extensibility. - Important Note on Scoping: Mermaid’s scoping rules for node IDs are somewhat flexible. A node defined in a subgraph can often be referenced from outside it. Our current
defined_nodes: HashMap<String, Span>tracks all defined IDs globally within a single diagram, which aligns well with this flexibility. If strict lexical scoping were required (e.g., node IDs only visible within their subgraph),defined_nodeswould need to be a stack of HashMaps. For now, the global approach is simpler and often sufficient for Mermaid.
- The
c) Integrate Validator into src/lib.rs
Now, let’s integrate our new Validator into the main library flow.
File: src/lib.rs (Partial, only showing changes)
pub mod lexer;
pub mod parser;
pub mod diagnostics;
pub mod utils;
pub mod validator; // Add validator module
use lexer::Lexer;
use parser::Parser;
use diagnostics::Diagnostic;
use validator::Validator;
use parser::ast::Diagram; // Assuming Diagram is visible here
/// Processes Mermaid code through lexing, parsing, and validation.
/// Returns the AST and any collected diagnostics.
pub fn process_mermaid_code(input: &str) -> (Option<Diagram>, Vec<Diagnostic>) {
let mut diagnostics: Vec<Diagnostic> = Vec::new();
// 1. Lexing
let (tokens, lexer_diagnostics) = Lexer::new(input).tokenize();
diagnostics.extend(lexer_diagnostics);
if tokens.is_empty() {
// If lexer produced no tokens, parsing won't work.
return (None, diagnostics);
}
// 2. Parsing
let mut parser = Parser::new(tokens);
let ast_result = parser.parse();
diagnostics.extend(parser.diagnostics()); // Collect parser-specific diagnostics
let ast = match ast_result {
Ok(ast) => ast,
Err(_) => {
// Parser already added its errors to diagnostics.
// If parsing failed fundamentally, we might not have a full AST for validation.
return (None, diagnostics);
}
};
// 3. Validation
let validator = Validator::new();
let validation_diagnostics = validator.validate(&ast);
diagnostics.extend(validation_diagnostics);
(Some(ast), diagnostics)
}
- Explanation: The
process_mermaid_codefunction now includes a third step: validation. After parsing, theValidator::new().validate(&ast)method is called, and any diagnostics it produces are added to the overall list. This function now returns both the (optional) AST and the complete list of diagnostics from all stages.
d) Testing This Component
It’s crucial to test our Validator thoroughly to ensure it catches the intended errors and doesn’t produce false positives.
1. Create tests/validator_tests.rs
touch tests/validator_tests.rs
File: tests/validator_tests.rs
use mermaid_analyzer::{
diagnostics::{DiagnosticLevel, ErrorCode},
process_mermaid_code,
};
#[test]
fn test_flowchart_undefined_node_in_edge() {
let input = r#"
graph TD
A --> B
C --> D
"#;
let (_, diagnostics) = process_mermaid_code(input);
assert!(!diagnostics.is_empty(), "Expected diagnostics for undefined nodes");
assert_eq!(diagnostics.len(), 2, "Expected 2 diagnostics");
let diag1 = &diagnostics[0];
assert_eq!(diag1.level, DiagnosticLevel::Error);
assert_eq!(diag1.code, ErrorCode::E1001);
assert!(diag1.message.contains("Undefined source node or subgraph 'A'"));
assert!(diag1.span.is_some());
let diag2 = &diagnostics[1];
assert_eq!(diag2.level, DiagnosticLevel::Error);
assert_eq!(diag2.code, ErrorCode::E1001);
assert!(diag2.message.contains("Undefined source node or subgraph 'C'"));
assert!(diag2.span.is_some());
}
#[test]
fn test_flowchart_duplicate_node_id() {
let input = r#"
graph TD
A[Node A]
B[Node B]
A[Another Node A]
"#;
let (_, diagnostics) = process_mermaid_code(input);
assert!(!diagnostics.is_empty(), "Expected diagnostics for duplicate node ID");
assert_eq!(diagnostics.len(), 1, "Expected 1 diagnostic");
let diag = &diagnostics[0];
assert_eq!(diag.level, DiagnosticLevel::Error);
assert_eq!(diag.code, ErrorCode::E1002);
assert!(diag.message.contains("Duplicate node ID 'A' found"));
assert!(diag.span.is_some());
}
#[test]
fn test_flowchart_duplicate_subgraph_id() {
let input = r#"
graph TD
subgraph MySub["My Subgraph"]
A[Node A]
end
subgraph MySub["Another Subgraph"]
B[Node B]
end
"#;
let (_, diagnostics) = process_mermaid_code(input);
assert!(!diagnostics.is_empty(), "Expected diagnostics for duplicate subgraph ID");
assert_eq!(diagnostics.len(), 1, "Expected 1 diagnostic");
let diag = &diagnostics[0];
assert_eq!(diag.level, DiagnosticLevel::Error);
assert_eq!(diag.code, ErrorCode::E1002);
assert!(diag.message.contains("Duplicate subgraph ID 'MySub' found"));
assert!(diag.span.is_some());
}
#[test]
fn test_flowchart_valid_with_subgraphs() {
let input = r#"
graph TD
A[Start] --> B(Process)
subgraph Sub1["First Sub"]
B --> C{Decision}
C -- Yes --> D[Task 1]
C -- No --> E[Task 2]
end
D --> F[End]
E --> F
F --> G[Finish]
Sub1 --> G // Reference subgraph as a node
"#;
let (_, diagnostics) = process_mermaid_code(input);
assert!(diagnostics.is_empty(), "Expected no diagnostics for valid flowchart, got: {:?}", diagnostics);
}
#[test]
fn test_flowchart_label_with_special_chars_warning() {
let input = r#"
graph TD
A[Node with spaces and !@#$]
"#;
let (_, diagnostics) = process_mermaid_code(input);
assert!(!diagnostics.is_empty(), "Expected warning for special characters in label");
assert_eq!(diagnostics.len(), 1, "Expected 1 diagnostic");
let diag = &diagnostics[0];
assert_eq!(diag.level, DiagnosticLevel::Warning);
assert_eq!(diag.code, ErrorCode::W0004);
assert!(diag.message.contains("Label 'Node with spaces and !@#$' contains special characters. Consider enclosing it in quotes."));
assert!(diag.span.is_some());
assert!(diag.suggested_fix.is_some());
assert_eq!(diag.suggested_fix.as_ref().unwrap(), "\"Node with spaces and !@#$\"");
}
#[test]
fn test_flowchart_self_referencing_node_warning() {
let input = r#"
graph TD
A[Node A] --> A
"#;
let (_, diagnostics) = process_mermaid_code(input);
assert!(!diagnostics.is_empty(), "Expected warning for self-referencing node");
assert_eq!(diagnostics.len(), 1, "Expected 1 diagnostic");
let diag = &diagnostics[0];
assert_eq!(diag.level, DiagnosticLevel::Warning);
assert_eq!(diag.code, ErrorCode::W0002);
assert!(diag.message.contains("Node 'A' is self-referencing in an edge."));
assert!(diag.span.is_some());
}
#[test]
fn test_flowchart_empty_diagram_warning() {
let input = r#"
graph TD
"#;
let (_, diagnostics) = process_mermaid_code(input);
assert!(!diagnostics.is_empty(), "Expected warning for empty flowchart");
assert_eq!(diagnostics.len(), 1, "Expected 1 diagnostic");
let diag = &diagnostics[0];
assert_eq!(diag.level, DiagnosticLevel::Warning);
assert_eq!(diag.code, ErrorCode::W0001);
assert!(diag.message.contains("Flowchart is empty or has no statements."));
}
#[test]
fn test_flowchart_multiple_errors() {
let input = r#"
graph TD
A --> B
C --> D
A[Duplicate A]
E[Node E !]
"#;
let (_, diagnostics) = process_mermaid_code(input);
assert!(!diagnostics.is_empty(), "Expected multiple diagnostics");
assert_eq!(diagnostics.len(), 4, "Expected 4 diagnostics (2 undefined, 1 duplicate, 1 bad label)");
// Sort diagnostics by code and then by span start to ensure consistent order
let mut sorted_diagnostics = diagnostics.clone();
sorted_diagnostics.sort_by(|a, b| {
a.code.as_str().cmp(b.code.as_str())
.then_with(|| a.span.as_ref().map_or(0, |s| s.start).cmp(&b.span.as_ref().map_or(0, |s| s.start)))
});
// Check E1001 (Undefined node)
assert_eq!(sorted_diagnostics[0].code, ErrorCode::E1001);
assert_eq!(sorted_diagnostics[1].code, ErrorCode::E1001);
// Check E1002 (Duplicate node ID)
assert_eq!(sorted_diagnostics[2].code, ErrorCode::E1002);
// Check W0004 (Special chars in label)
assert_eq!(sorted_diagnostics[3].code, ErrorCode::W0004);
}
- Explanation: These tests cover various error scenarios for flowcharts: undefined nodes in edges, duplicate node IDs (for both nodes and subgraphs), valid diagrams, labels with special characters (warning), self-referencing nodes (warning), and empty diagrams (warning). The
process_mermaid_codefunction is used, which now includes the full lexer -> parser -> validator pipeline. We assert on the number of diagnostics, their level, code, and a part of their message, along with the presence of a span. Sorting diagnostics intest_flowchart_multiple_errorshelps ensure consistent test results when multiple errors are present.
Production Considerations
- Error Handling & Reporting: The
Diagnosticstruct is designed for robust error reporting. In a production CLI, we would implement aDiagnosticReporterthat takes theVec<Diagnostic>and pretty-prints them to the console, potentially highlighting the relevant source code lines using libraries liketermcolororcodespan-reporting. This will be covered in a later chapter on CLI implementation. - Performance Optimization:
- AST Traversal: For very large diagrams, recursive AST traversal can be a performance bottleneck or lead to stack overflows. Rust’s call stack is generally deep, but for extremely deep nesting, an iterative approach or explicit stack management might be considered.
HashMapLookups: UsingHashMapfordefined_nodesprovides O(1) average-case lookup, which is efficient.- String Clones: Minimize string cloning where possible. For
defined_nodes, we clone the node ID strings once. For diagnostics, messages are ownedStrings, which is appropriate.
- Security Considerations: While a Mermaid validator doesn’t typically face direct security threats, strict validation helps prevent malformed or malicious input from causing unexpected behavior or crashes in downstream rendering engines or other tools that consume the validated AST. By enforcing correctness, we enhance the overall robustness of the system.
- Logging and Monitoring: The diagnostics themselves serve as the primary output for monitoring the correctness of Mermaid code. For the tool itself, internal logging (e.g., using
logcrate) could track performance metrics or unexpected internal states, though for a simple CLI tool, this might be overkill.
Code Review Checkpoint
At this point, you should have implemented the core validation logic for Flowcharts.
Files Created/Modified:
src/utils/span.rs: New file, centralizingSpandefinition.src/diagnostics/mod.rs: New module, exports diagnostic types.src/diagnostics/diagnostic.rs: New file, definesDiagnosticstruct and itsDisplayimplementation.src/diagnostics/error_codes.rs: New file, definesErrorCodeenum.src/validator/mod.rs: New module, exportsValidator.src/validator/validator.rs: New file, contains theValidatorstruct and its validation methods.src/lib.rs: Modified to import new modules and integrate theValidatorintoprocess_mermaid_code.src/parser/ast.rs: Modified to includeSpanin relevant AST nodes (if not already done).tests/validator_tests.rs: New file, containing unit tests for the validation layer.
Integration with Existing Code:
The process_mermaid_code function in src/lib.rs now orchestrates the entire pipeline: lexing -> parsing -> validation. All diagnostics from these stages are collected into a single Vec<Diagnostic>, which is returned along with the AST. This unified diagnostic approach is critical for providing a consistent user experience.
Common Issues & Solutions
- False Positives/Negatives:
- Issue: The validator reports an error for valid Mermaid code (false positive) or misses an actual error (false negative).
- Debugging: This often points to a misinterpretation of Mermaid’s official syntax rules. Double-check the Mermaid documentation for the specific diagram type and construct. Use
mermaid.liveto verify how a snippet renders. - Solution: Refine the validation logic in
src/validator/validator.rs. Ensure checks are precise and cover all edge cases allowed by Mermaid. Add more specific unit tests to reproduce the false positive/negative.
- Performance on Large Diagrams:
- Issue: Validation takes too long for very large Mermaid diagrams (thousands of nodes/edges).
- Debugging: Use Rust’s built-in
cargo bench(after adding benchmarks) or simpleInstant::now()measurements to profile thevalidatefunction. Identify which parts of the traversal or data structures are slow. - Solution: Ensure efficient data structures are used (e.g.,
HashMapfor node lookups). Avoid excessive cloning. If recursion depth becomes an issue, consider converting recursive traversals to iterative ones using an explicit stack.
- Inaccurate
SpanInformation:- Issue: Diagnostics point to the wrong line/column or highlight the wrong token.
- Debugging: Trace the
Spanpropagation from the lexer, through the parser, and into the AST. Ensure that each AST node correctly captures the span of its corresponding source text. - Solution: Review the
Spangeneration insrc/lexer/mod.rsandsrc/parser/mod.rs. Confirm thatSpan::newparameters (start,end,line,column) are always correct relative to the original source string.
Testing & Verification
To verify the work in this chapter, run your tests:
cargo test
You should see all your existing lexer and parser tests pass, along with the new validator_tests.rs tests.
What should work now:
- The
process_mermaid_codefunction can now take Mermaid input, tokenize it, parse it into an AST, and then validate that AST. - It should correctly identify and report:
- Undefined nodes or subgraphs referenced in edges (
E1001). - Duplicate node/subgraph IDs within the same scope (
E1002). - Warnings for special characters in unquoted labels (
W0004). - Warnings for self-referencing nodes (
W0002). - Warnings for empty flowcharts (
W0001).
- Undefined nodes or subgraphs referenced in edges (
- The diagnostics are structured according to our
Diagnostictype, including level, code, message, and span. - The
Displayimplementation forDiagnosticshould print messages in a clear, compiler-like format.
How to verify everything is correct:
Run
cargo test: All tests, especiallyvalidator_tests.rs, should pass.Manual Testing: Create
main.rs(if you haven’t already, or temporarily modify it) to callprocess_mermaid_codewith various Mermaid snippets, both valid and intentionally invalid, and print the resulting diagnostics.File:
src/main.rs(Temporary for testing)use mermaid_analyzer::process_mermaid_code; fn main() { let test_cases = vec![ ( "Valid Flowchart", r#" graph TD A[Start] --> B(Process) B --> C{Decision} C -- Yes --> D[Task 1] C -- No --> E[Task 2] D --> F[End] E --> F "#, ), ( "Undefined Node Error", r#" graph TD A --> B C --> D "#, ), ( "Duplicate Node ID Error", r#" graph TD A[Node A] B[Node B] A[Another Node A] "#, ), ( "Label with Special Chars Warning", r#" graph TD A[Node with !@#$ and spaces] "#, ), ( "Self-Referencing Node Warning", r#" graph TD A[Node A] --> A "#, ), ( "Empty Flowchart Warning", r#" graph TD "#, ), ]; for (name, code) in test_cases { println!("--- Testing: {} ---", name); println!("Mermaid Code:\n{}", code); let (ast_opt, diagnostics) = process_mermaid_code(code); if let Some(ast) = ast_opt { println!("AST Generated: {:?}", ast); } else { println!("No AST generated due to critical errors."); } if diagnostics.is_empty() { println!("No diagnostics reported. Code is valid."); } else { println!("Diagnostics:"); for diag in diagnostics { println!("{}", diag); } } println!("\n"); } }Run this with
cargo runand observe the output. This manual verification helps confirm that the diagnostics are user-friendly and accurate.
Summary & Next Steps
In this chapter, we successfully implemented a crucial component of our Mermaid analyzer: the Strict Validation Layer. We designed a comprehensive Diagnostic system, complete with error levels, unique codes, and rich contextual information, mirroring the high standards of the Rust compiler. We then built the Validator itself, which traverses the AST to detect both post-parsing syntax issues and complex semantic errors such as undefined node references, duplicate IDs, and potentially ambiguous constructs. The integration into our process_mermaid_code function ensures that every Mermaid input now undergoes a thorough correctness check.
This validation layer is fundamental. It provides the necessary feedback to developers and ensures that subsequent processing steps, like our upcoming rule engine and formatter, operate on a semantically sound representation of the Mermaid diagram.
In Chapter 6: Deterministic Rule Engine and AST Transformations, we will build upon this foundation. We will design and implement a flexible rule engine using a Rule trait system, allowing us to define and apply various checks and safe, reversible fixes directly to the AST. This will empower our tool to not only report issues but also to automatically correct common Mermaid pitfalls, moving us closer to a fully functional linter and formatter.