Skip to content

Complex Analysis

This example demonstrates advanced parsing and analysis scenarios using the Erlang Rebar Config Parser for real-world applications.

Overview

This section covers complex use cases such as:

  • Multi-file configuration analysis
  • Dependency tree analysis
  • Configuration validation and linting
  • Migration tools
  • Performance analysis

Multi-File Configuration Analysis

Project Structure Analysis

go
package main

import (
    "fmt"
    "log"
    "os"
    "path/filepath"
    "strings"
    
    "github.com/scagogogo/erlang-rebar-config-parser/pkg/parser"
)

type ProjectAnalysis struct {
    MainConfig    *parser.RebarConfig
    ProfileConfigs map[string]*parser.RebarConfig
    Dependencies  []Dependency
    Profiles      []string
    Warnings      []string
}

type Dependency struct {
    Name     string
    Version  string
    Source   string
    Profiles []string
}

func analyzeProject(projectPath string) (*ProjectAnalysis, error) {
    analysis := &ProjectAnalysis{
        ProfileConfigs: make(map[string]*parser.RebarConfig),
        Dependencies:   []Dependency{},
        Profiles:       []string{},
        Warnings:       []string{},
    }
    
    // Parse main rebar.config
    mainConfigPath := filepath.Join(projectPath, "rebar.config")
    if _, err := os.Stat(mainConfigPath); err == nil {
        config, err := parser.ParseFile(mainConfigPath)
        if err != nil {
            return nil, fmt.Errorf("failed to parse main config: %w", err)
        }
        analysis.MainConfig = config
        
        // Extract dependencies from main config
        analysis.extractDependencies("main")
        
        // Extract profiles
        analysis.extractProfiles()
    } else {
        analysis.Warnings = append(analysis.Warnings, "No main rebar.config found")
    }
    
    // Look for profile-specific configs
    configDir := filepath.Join(projectPath, "config")
    if _, err := os.Stat(configDir); err == nil {
        analysis.analyzeProfileConfigs(configDir)
    }
    
    return analysis, nil
}

func (a *ProjectAnalysis) extractDependencies(profile string) {
    if a.MainConfig == nil {
        return
    }
    
    if deps, ok := a.MainConfig.GetDeps(); ok && len(deps) > 0 {
        if depsList, ok := deps[0].(parser.List); ok {
            for _, dep := range depsList.Elements {
                if tuple, ok := dep.(parser.Tuple); ok && len(tuple.Elements) >= 2 {
                    if name, ok := tuple.Elements[0].(parser.Atom); ok {
                        dependency := Dependency{
                            Name:     name.Value,
                            Profiles: []string{profile},
                        }
                        
                        // Extract version
                        switch version := tuple.Elements[1].(type) {
                        case parser.String:
                            dependency.Version = version.Value
                        case parser.Tuple:
                            dependency.Version = version.String()
                            // Could be a git dependency or version spec
                            if len(version.Elements) > 0 {
                                if atom, ok := version.Elements[0].(parser.Atom); ok {
                                    dependency.Source = atom.Value
                                }
                            }
                        }
                        
                        a.Dependencies = append(a.Dependencies, dependency)
                    }
                }
            }
        }
    }
}

func (a *ProjectAnalysis) extractProfiles() {
    if a.MainConfig == nil {
        return
    }
    
    if profiles, ok := a.MainConfig.GetProfilesConfig(); ok && len(profiles) > 0 {
        if profilesList, ok := profiles[0].(parser.List); ok {
            for _, profile := range profilesList.Elements {
                if tuple, ok := profile.(parser.Tuple); ok && len(tuple.Elements) >= 1 {
                    if name, ok := tuple.Elements[0].(parser.Atom); ok {
                        a.Profiles = append(a.Profiles, name.Value)
                    }
                }
            }
        }
    }
}

func (a *ProjectAnalysis) analyzeProfileConfigs(configDir string) {
    filepath.Walk(configDir, func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }
        
        if strings.HasSuffix(path, ".config") && !info.IsDir() {
            profileName := strings.TrimSuffix(filepath.Base(path), ".config")
            
            config, err := parser.ParseFile(path)
            if err != nil {
                a.Warnings = append(a.Warnings, 
                    fmt.Sprintf("Failed to parse %s: %v", path, err))
                return nil
            }
            
            a.ProfileConfigs[profileName] = config
        }
        
        return nil
    })
}

func (a *ProjectAnalysis) GenerateReport() string {
    var report strings.Builder
    
    report.WriteString("=== Project Analysis Report ===\n\n")
    
    // Basic information
    if a.MainConfig != nil {
        report.WriteString(fmt.Sprintf("Main configuration: %d terms\n", len(a.MainConfig.Terms)))
        
        if appName, ok := a.MainConfig.GetAppName(); ok {
            report.WriteString(fmt.Sprintf("Application: %s\n", appName))
        }
    }
    
    report.WriteString(fmt.Sprintf("Profiles: %d (%v)\n", len(a.Profiles), a.Profiles))
    report.WriteString(fmt.Sprintf("Profile configs: %d\n", len(a.ProfileConfigs)))
    report.WriteString(fmt.Sprintf("Dependencies: %d\n", len(a.Dependencies)))
    
    // Dependencies analysis
    if len(a.Dependencies) > 0 {
        report.WriteString("\n--- Dependencies ---\n")
        for _, dep := range a.Dependencies {
            report.WriteString(fmt.Sprintf("- %s: %s", dep.Name, dep.Version))
            if dep.Source != "" {
                report.WriteString(fmt.Sprintf(" (source: %s)", dep.Source))
            }
            report.WriteString(fmt.Sprintf(" [profiles: %v]\n", dep.Profiles))
        }
    }
    
    // Warnings
    if len(a.Warnings) > 0 {
        report.WriteString("\n--- Warnings ---\n")
        for _, warning := range a.Warnings {
            report.WriteString(fmt.Sprintf("⚠ %s\n", warning))
        }
    }
    
    return report.String()
}

Dependency Tree Analysis

Dependency Graph Builder

go
type DependencyGraph struct {
    Nodes map[string]*DependencyNode
    Edges map[string][]string
}

type DependencyNode struct {
    Name         string
    Version      string
    Dependencies []string
    Dependents   []string
}

func buildDependencyGraph(configs []*parser.RebarConfig) *DependencyGraph {
    graph := &DependencyGraph{
        Nodes: make(map[string]*DependencyNode),
        Edges: make(map[string][]string),
    }
    
    // First pass: collect all dependencies
    for _, config := range configs {
        if deps, ok := config.GetDeps(); ok && len(deps) > 0 {
            if depsList, ok := deps[0].(parser.List); ok {
                for _, dep := range depsList.Elements {
                    if tuple, ok := dep.(parser.Tuple); ok && len(tuple.Elements) >= 2 {
                        if name, ok := tuple.Elements[0].(parser.Atom); ok {
                            node := &DependencyNode{
                                Name:         name.Value,
                                Dependencies: []string{},
                                Dependents:   []string{},
                            }
                            
                            if version, ok := tuple.Elements[1].(parser.String); ok {
                                node.Version = version.Value
                            }
                            
                            graph.Nodes[name.Value] = node
                        }
                    }
                }
            }
        }
    }
    
    return graph
}

func (g *DependencyGraph) FindCircularDependencies() [][]string {
    var cycles [][]string
    visited := make(map[string]bool)
    recStack := make(map[string]bool)
    
    for node := range g.Nodes {
        if !visited[node] {
            if cycle := g.dfsForCycle(node, visited, recStack, []string{}); len(cycle) > 0 {
                cycles = append(cycles, cycle)
            }
        }
    }
    
    return cycles
}

func (g *DependencyGraph) dfsForCycle(node string, visited, recStack map[string]bool, path []string) []string {
    visited[node] = true
    recStack[node] = true
    path = append(path, node)
    
    for _, neighbor := range g.Edges[node] {
        if !visited[neighbor] {
            if cycle := g.dfsForCycle(neighbor, visited, recStack, path); len(cycle) > 0 {
                return cycle
            }
        } else if recStack[neighbor] {
            // Found cycle
            cycleStart := -1
            for i, n := range path {
                if n == neighbor {
                    cycleStart = i
                    break
                }
            }
            if cycleStart >= 0 {
                return append(path[cycleStart:], neighbor)
            }
        }
    }
    
    recStack[node] = false
    return nil
}

func (g *DependencyGraph) GenerateReport() string {
    var report strings.Builder
    
    report.WriteString("=== Dependency Graph Analysis ===\n\n")
    report.WriteString(fmt.Sprintf("Total dependencies: %d\n", len(g.Nodes)))
    
    // Find root dependencies (no dependents)
    var roots []string
    for name, node := range g.Nodes {
        if len(node.Dependents) == 0 {
            roots = append(roots, name)
        }
    }
    
    if len(roots) > 0 {
        report.WriteString(fmt.Sprintf("Root dependencies: %v\n", roots))
    }
    
    // Check for circular dependencies
    cycles := g.FindCircularDependencies()
    if len(cycles) > 0 {
        report.WriteString("\n⚠ Circular dependencies found:\n")
        for i, cycle := range cycles {
            report.WriteString(fmt.Sprintf("  %d. %s\n", i+1, strings.Join(cycle, " -> ")))
        }
    } else {
        report.WriteString("\n✓ No circular dependencies found\n")
    }
    
    return report.String()
}

Configuration Validation and Linting

Advanced Validator

go
type ConfigValidator struct {
    Rules []ValidationRule
}

type ValidationRule interface {
    Validate(config *parser.RebarConfig) []ValidationIssue
    Name() string
}

type ValidationIssue struct {
    Level   string // "error", "warning", "info"
    Rule    string
    Message string
    Term    parser.Term
}

// Rule: Check for required sections
type RequiredSectionsRule struct{}

func (r RequiredSectionsRule) Name() string {
    return "required-sections"
}

func (r RequiredSectionsRule) Validate(config *parser.RebarConfig) []ValidationIssue {
    var issues []ValidationIssue
    
    requiredSections := []string{"erl_opts", "deps"}
    
    for _, section := range requiredSections {
        if _, ok := config.GetTerm(section); !ok {
            issues = append(issues, ValidationIssue{
                Level:   "warning",
                Rule:    r.Name(),
                Message: fmt.Sprintf("Missing recommended section: %s", section),
            })
        }
    }
    
    return issues
}

// Rule: Check for deprecated options
type DeprecatedOptionsRule struct{}

func (r DeprecatedOptionsRule) Name() string {
    return "deprecated-options"
}

func (r DeprecatedOptionsRule) Validate(config *parser.RebarConfig) []ValidationIssue {
    var issues []ValidationIssue
    
    deprecatedOptions := map[string]string{
        "no_debug_info": "Use {debug_info, false} instead",
        "debug_info":    "Consider using {debug_info, true} for clarity",
    }
    
    if erlOpts, ok := config.GetErlOpts(); ok && len(erlOpts) > 0 {
        if optsList, ok := erlOpts[0].(parser.List); ok {
            for _, opt := range optsList.Elements {
                if atom, ok := opt.(parser.Atom); ok {
                    if suggestion, deprecated := deprecatedOptions[atom.Value]; deprecated {
                        issues = append(issues, ValidationIssue{
                            Level:   "info",
                            Rule:    r.Name(),
                            Message: fmt.Sprintf("Deprecated option '%s': %s", atom.Value, suggestion),
                            Term:    opt,
                        })
                    }
                }
            }
        }
    }
    
    return issues
}

// Rule: Check dependency versions
type DependencyVersionRule struct{}

func (r DependencyVersionRule) Name() string {
    return "dependency-versions"
}

func (r DependencyVersionRule) Validate(config *parser.RebarConfig) []ValidationIssue {
    var issues []ValidationIssue
    
    if deps, ok := config.GetDeps(); ok && len(deps) > 0 {
        if depsList, ok := deps[0].(parser.List); ok {
            for _, dep := range depsList.Elements {
                if tuple, ok := dep.(parser.Tuple); ok && len(tuple.Elements) >= 2 {
                    if name, ok := tuple.Elements[0].(parser.Atom); ok {
                        if version, ok := tuple.Elements[1].(parser.String); ok {
                            // Check for loose version constraints
                            if strings.Contains(version.Value, "*") {
                                issues = append(issues, ValidationIssue{
                                    Level:   "warning",
                                    Rule:    r.Name(),
                                    Message: fmt.Sprintf("Dependency '%s' uses loose version constraint: %s", name.Value, version.Value),
                                    Term:    dep,
                                })
                            }
                            
                            // Check for very old versions (example heuristic)
                            if strings.HasPrefix(version.Value, "0.") || strings.HasPrefix(version.Value, "1.") {
                                issues = append(issues, ValidationIssue{
                                    Level:   "info",
                                    Rule:    r.Name(),
                                    Message: fmt.Sprintf("Dependency '%s' may be using an old version: %s", name.Value, version.Value),
                                    Term:    dep,
                                })
                            }
                        }
                    }
                }
            }
        }
    }
    
    return issues
}

func NewConfigValidator() *ConfigValidator {
    return &ConfigValidator{
        Rules: []ValidationRule{
            RequiredSectionsRule{},
            DeprecatedOptionsRule{},
            DependencyVersionRule{},
        },
    }
}

func (v *ConfigValidator) ValidateConfig(config *parser.RebarConfig) []ValidationIssue {
    var allIssues []ValidationIssue
    
    for _, rule := range v.Rules {
        issues := rule.Validate(config)
        allIssues = append(allIssues, issues...)
    }
    
    return allIssues
}

func (v *ConfigValidator) GenerateReport(issues []ValidationIssue) string {
    var report strings.Builder
    
    report.WriteString("=== Configuration Validation Report ===\n\n")
    
    if len(issues) == 0 {
        report.WriteString("✓ No issues found\n")
        return report.String()
    }
    
    // Group by level
    errorCount := 0
    warningCount := 0
    infoCount := 0
    
    for _, issue := range issues {
        switch issue.Level {
        case "error":
            errorCount++
        case "warning":
            warningCount++
        case "info":
            infoCount++
        }
    }
    
    report.WriteString(fmt.Sprintf("Found %d issues: %d errors, %d warnings, %d info\n\n", 
        len(issues), errorCount, warningCount, infoCount))
    
    // List issues by level
    levels := []string{"error", "warning", "info"}
    symbols := map[string]string{"error": "✗", "warning": "⚠", "info": "ℹ"}
    
    for _, level := range levels {
        levelIssues := filterIssuesByLevel(issues, level)
        if len(levelIssues) > 0 {
            report.WriteString(fmt.Sprintf("--- %s (%d) ---\n", strings.ToUpper(level), len(levelIssues)))
            for _, issue := range levelIssues {
                report.WriteString(fmt.Sprintf("%s [%s] %s\n", symbols[level], issue.Rule, issue.Message))
                if issue.Term != nil {
                    report.WriteString(fmt.Sprintf("    %s\n", issue.Term.String()))
                }
            }
            report.WriteString("\n")
        }
    }
    
    return report.String()
}

func filterIssuesByLevel(issues []ValidationIssue, level string) []ValidationIssue {
    var filtered []ValidationIssue
    for _, issue := range issues {
        if issue.Level == level {
            filtered = append(filtered, issue)
        }
    }
    return filtered
}

Complete Analysis Example

go
package main

import (
    "fmt"
    "log"
    
    "github.com/scagogogo/erlang-rebar-config-parser/pkg/parser"
)

func main() {
    // Sample complex configuration
    complexConfig := `
    {app_name, complex_app}.
    
    {erl_opts, [
        debug_info,
        warnings_as_errors,
        {parse_transform, lager_transform},
        no_debug_info  % deprecated option
    ]}.
    
    {deps, [
        {cowboy, "2.9.0"},
        {jsx, "3.*"},  % loose version constraint
        {lager, "1.2.0"},  % potentially old version
        {custom_dep, {git, "https://github.com/user/custom_dep.git", {branch, "master"}}}
    ]}.
    
    {profiles, [
        {dev, [
            {deps, [{sync, "0.1.3"}]},
            {erl_opts, [debug_info]}
        ]},
        {test, [
            {deps, [{proper, "1.3.0"}, {meck, "0.9.0"}]},
            {erl_opts, [debug_info, export_all]}
        ]},
        {prod, [
            {erl_opts, [warnings_as_errors, no_debug_info]}
        ]}
    ]}.
    
    {relx, [
        {release, {complex_app, "0.1.0"}, [complex_app, sasl]},
        {dev_mode, true},
        {include_erts, false}
    ]}.
    `
    
    config, err := parser.Parse(complexConfig)
    if err != nil {
        log.Fatalf("Failed to parse config: %v", err)
    }
    
    fmt.Println("=== Complex Configuration Analysis ===")
    
    // 1. Basic analysis
    performBasicAnalysis(config)
    
    // 2. Validation
    performValidation(config)
    
    // 3. Dependency analysis
    performDependencyAnalysis(config)
    
    // 4. Profile analysis
    performProfileAnalysis(config)
}

func performBasicAnalysis(config *parser.RebarConfig) {
    fmt.Println("\n--- Basic Analysis ---")
    fmt.Printf("Total terms: %d\n", len(config.Terms))
    
    if appName, ok := config.GetAppName(); ok {
        fmt.Printf("Application: %s\n", appName)
    }
    
    // Count different section types
    sectionCounts := make(map[string]int)
    for _, term := range config.Terms {
        if tuple, ok := term.(parser.Tuple); ok && len(tuple.Elements) > 0 {
            if atom, ok := tuple.Elements[0].(parser.Atom); ok {
                sectionCounts[atom.Value]++
            }
        }
    }
    
    fmt.Println("Sections:")
    for section, count := range sectionCounts {
        fmt.Printf("  %s: %d\n", section, count)
    }
}

func performValidation(config *parser.RebarConfig) {
    fmt.Println("\n--- Validation ---")
    
    validator := NewConfigValidator()
    issues := validator.ValidateConfig(config)
    
    report := validator.GenerateReport(issues)
    fmt.Print(report)
}

func performDependencyAnalysis(config *parser.RebarConfig) {
    fmt.Println("\n--- Dependency Analysis ---")
    
    if deps, ok := config.GetDeps(); ok && len(deps) > 0 {
        if depsList, ok := deps[0].(parser.List); ok {
            fmt.Printf("Main dependencies: %d\n", len(depsList.Elements))
            
            for _, dep := range depsList.Elements {
                analyzeDependency(dep)
            }
        }
    }
    
    // Analyze profile dependencies
    if profiles, ok := config.GetProfilesConfig(); ok && len(profiles) > 0 {
        if profilesList, ok := profiles[0].(parser.List); ok {
            fmt.Printf("\nProfile dependencies:\n")
            for _, profile := range profilesList.Elements {
                analyzeProfileDependencies(profile)
            }
        }
    }
}

func analyzeDependency(dep parser.Term) {
    if tuple, ok := dep.(parser.Tuple); ok && len(tuple.Elements) >= 2 {
        if name, ok := tuple.Elements[0].(parser.Atom); ok {
            fmt.Printf("  %s: ", name.Value)
            
            switch version := tuple.Elements[1].(type) {
            case parser.String:
                fmt.Printf("version %s", version.Value)
            case parser.Tuple:
                if len(version.Elements) > 0 {
                    if source, ok := version.Elements[0].(parser.Atom); ok {
                        fmt.Printf("source %s", source.Value)
                    }
                }
            }
            fmt.Println()
        }
    }
}

func analyzeProfileDependencies(profile parser.Term) {
    if tuple, ok := profile.(parser.Tuple); ok && len(tuple.Elements) >= 2 {
        if name, ok := tuple.Elements[0].(parser.Atom); ok {
            fmt.Printf("  Profile %s: ", name.Value)
            
            if configList, ok := tuple.Elements[1].(parser.List); ok {
                depCount := 0
                for _, configItem := range configList.Elements {
                    if configTuple, ok := configItem.(parser.Tuple); ok && len(configTuple.Elements) >= 2 {
                        if key, ok := configTuple.Elements[0].(parser.Atom); ok && key.Value == "deps" {
                            if depsList, ok := configTuple.Elements[1].(parser.List); ok {
                                depCount = len(depsList.Elements)
                            }
                        }
                    }
                }
                fmt.Printf("%d dependencies\n", depCount)
            }
        }
    }
}

func performProfileAnalysis(config *parser.RebarConfig) {
    fmt.Println("\n--- Profile Analysis ---")
    
    if profiles, ok := config.GetProfilesConfig(); ok && len(profiles) > 0 {
        if profilesList, ok := profiles[0].(parser.List); ok {
            fmt.Printf("Total profiles: %d\n", len(profilesList.Elements))
            
            for _, profile := range profilesList.Elements {
                if tuple, ok := profile.(parser.Tuple); ok && len(tuple.Elements) >= 2 {
                    if name, ok := tuple.Elements[0].(parser.Atom); ok {
                        fmt.Printf("\nProfile: %s\n", name.Value)
                        
                        if configList, ok := tuple.Elements[1].(parser.List); ok {
                            for _, configItem := range configList.Elements {
                                if configTuple, ok := configItem.(parser.Tuple); ok && len(configTuple.Elements) >= 2 {
                                    if key, ok := configTuple.Elements[0].(parser.Atom); ok {
                                        fmt.Printf("  %s: %s\n", key.Value, configTuple.Elements[1].String())
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

This comprehensive example demonstrates advanced analysis capabilities including project structure analysis, dependency graph building, configuration validation, and detailed reporting.

Released under the MIT License.