Control Flow Analysis

bashrs generates Control Flow Graphs (CFGs) and calculates complexity metrics to identify hard-to-maintain shell scripts.

Complexity Metrics

Three complexity measures following software engineering best practices:

Cyclomatic Complexity (McCabe, 1976)

Measures the number of linearly independent paths through code.

V(G) = E - N + 2P

Where:
  E = number of edges
  N = number of nodes
  P = number of connected components (usually 1)

Toyota Standard: Maximum cyclomatic complexity of 10.

Essential Complexity

Measures how much complexity remains after structured programming constructs are removed. High essential complexity indicates spaghetti code.

Cognitive Complexity

Measures how difficult code is for humans to understand, penalizing:

  • Deeply nested structures
  • Breaking linear flow (break, continue, goto-like patterns)
  • Recursion

Usage

use bashrs::quality::{CfgBuilder, ComplexityMetrics};

fn main() {
    // Build CFG from shell script
    let script = r#"
        if [ -f "$1" ]; then
            while read -r line; do
                if [ -n "$line" ]; then
                    echo "$line"
                fi
            done < "$1"
        else
            echo "File not found"
            exit 1
        fi
    "#;

    let cfg = CfgBuilder::new().build_from_source(script);
    let metrics = ComplexityMetrics::from_cfg(&cfg);

    println!("Cyclomatic: {}", metrics.cyclomatic);
    println!("Essential: {}", metrics.essential);
    println!("Cognitive: {}", metrics.cognitive);
    println!("Max depth: {}", metrics.max_depth);
    println!("Decision points: {}", metrics.decision_points);
    println!("Loop count: {}", metrics.loop_count);

    // Check against Toyota threshold
    if metrics.exceeds_threshold() {
        println!("WARNING: Complexity {} exceeds threshold 10", metrics.cyclomatic);
    }

    // Get grade
    println!("Grade: {:?}", metrics.grade());
}

Complexity Grades

GradeCyclomaticRisk Level
Simple1-5Low risk, easy to test
Moderate6-10Acceptable, Toyota standard
Complex11-20High risk, needs attention
VeryComplex21-50Very high risk, should refactor
Untestable50+Must refactor immediately

CFG Nodes

The CFG represents code structure with these node types:

use bashrs::quality::CfgNode;

// Node types in the control flow graph
enum CfgNode {
    Entry,           // Function/script entry point
    Exit,            // Function/script exit point
    BasicBlock {     // Sequential statements
        statements: Vec<String>,
    },
    Conditional {    // if/elif/case branches
        condition: String,
    },
    LoopHeader {     // while/for/until loops
        condition: String,
    },
    FunctionEntry {  // Function definition
        name: String,
    },
    SubshellEntry,   // Subshell $(...) or (...)
}

ASCII Visualization

Generate ASCII art representation of the CFG:

use bashrs::quality::{render_cfg_ascii, CfgBuilder, ComplexityMetrics};

fn main() {
    let script = r#"
        if [ "$1" = "start" ]; then
            start_service
        elif [ "$1" = "stop" ]; then
            stop_service
        else
            echo "Usage: $0 {start|stop}"
        fi
    "#;

    let cfg = CfgBuilder::new().build_from_source(script);
    let metrics = ComplexityMetrics::from_cfg(&cfg);
    let ascii = render_cfg_ascii(&cfg, &metrics, 60);

    println!("{}", ascii);
}

Output:

╔══════════════════════════════════════════════════════════════╗
║                 Control Flow Graph Analysis                   ║
╠══════════════════════════════════════════════════════════════╣
║  Cyclomatic: 4   Essential: 2   Cognitive: 6                 ║
║  Grade: Simple   Max Depth: 2   Decisions: 3                 ║
╠══════════════════════════════════════════════════════════════╣
║                                                               ║
║    [ENTRY]                                                    ║
║       │                                                       ║
║       ▼                                                       ║
║    <$1 = "start"?>──────┐                                    ║
║       │ yes             │ no                                  ║
║       ▼                 ▼                                     ║
║  [start_service]   <$1 = "stop"?>────┐                       ║
║       │                 │ yes        │ no                     ║
║       │                 ▼            ▼                        ║
║       │           [stop_service] [echo Usage]                 ║
║       │                 │            │                        ║
║       └────────────────┴────────────┘                        ║
║                        │                                      ║
║                        ▼                                      ║
║                     [EXIT]                                    ║
║                                                               ║
╚══════════════════════════════════════════════════════════════╝

Best Practices

Keep Complexity Low

 BAD: High cyclomatic complexity (11)
process_args() {
    if [ "$1" = "a" ]; then
        if [ "$2" = "1" ]; then
            if [ "$3" = "x" ]; then
                 deeply nested...
            fi
        fi
    fi
}

 GOOD: Low complexity (3)
process_args() {
    case "$1:$2:$3" in
        a:1:x) handle_a1x ;;
        a:1:*) handle_a1 ;;
        a:*:*) handle_a ;;
        *)     handle_default ;;
    esac
}

Extract Functions

 BAD: Everything in main
main() {
     50 lines of validation
     30 lines of processing
     20 lines of output
}

 GOOD: Separate concerns
validate_input() { ... }   # Complexity: 3
process_data() { ... }     # Complexity: 4
generate_output() { ... }  # Complexity: 2

main() {
    validate_input "$@"
    process_data
    generate_output
}

Reduce Nesting

 BAD: Deep nesting
if [ -f "$file" ]; then
    if [ -r "$file" ]; then
        if [ -s "$file" ]; then
            process "$file"
        fi
    fi
fi

 GOOD: Early returns
[ -f "$file" ] || { echo "Not a file"; exit 1; }
[ -r "$file" ] || { echo "Not readable"; exit 1; }
[ -s "$file" ] || { echo "Empty file"; exit 1; }
process "$file"

Integration with CI/CD

Add complexity checks to your pipeline:

# .github/workflows/quality.yml
- name: Check complexity
  run: |
    cargo run -- complexity src/*.sh --max 10
    if [ $? -ne 0 ]; then
      echo "Complexity exceeds Toyota threshold (10)"
      exit 1
    fi

References

  • McCabe, T.J. (1976). "A Complexity Measure"
  • Watson, A.H. & McCabe, T.J. (1996). "Structured Testing: A Testing Methodology Using the Cyclomatic Complexity Metric"
  • Campbell, G.A. (2018). "Cognitive Complexity: A new way of measuring understandability"