github.com/vmware/govmomi@v0.51.0/hack/header/main.go (about)

     1  // © Broadcom. All Rights Reserved.
     2  // The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries.
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package main
     6  
     7  import (
     8  	"bufio"
     9  	"encoding/json"
    10  	"flag"
    11  	"fmt"
    12  	"os"
    13  	"path/filepath"
    14  	"strings"
    15  )
    16  
    17  // Config defines the structure of the JSON configuration file.
    18  type Config struct {
    19  	HeaderLines         []string          `json:"headerLines"`         // Expected file header.
    20  	FileCommentPrefixes map[string]string `json:"fileCommentPrefixes"` // Comment styles for file types.
    21  	IgnoredPaths        []string          `json:"ignoredPaths"`        // Paths to ignore.
    22  	MaxScanLines        int               `json:"maxScanLines"`        // Number of lines to check for the header.
    23  }
    24  
    25  // Global variables to hold the configuration and results.
    26  var config Config
    27  var filesWithIssues []string
    28  var filesWithError []string
    29  var processedCount = 0
    30  
    31  var providedConfigPath = flag.String("config", "", "Path to the configuration file")
    32  
    33  // main is the entry point for the header check.
    34  func main() {
    35  	flag.Parse()
    36  
    37  	if *providedConfigPath == "" {
    38  		fmt.Println("Provide a JSON configuration using the --config flag.")
    39  		os.Exit(1)
    40  	}
    41  
    42  	fmt.Printf("Configuration file: %s\n", *providedConfigPath)
    43  	configPathToLoad := *providedConfigPath
    44  
    45  	// Load the configuration file.
    46  	if err := loadConfig(configPathToLoad); err != nil {
    47  		fmt.Printf("Error loading configuration: %v\n", err)
    48  		os.Exit(1) // Configuration error is critical.
    49  	}
    50  
    51  	// Start directory traversal.
    52  	err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
    53  		if err != nil {
    54  			return err
    55  		}
    56  
    57  		// Skip directories.
    58  		if info.IsDir() {
    59  			return nil
    60  		}
    61  
    62  		// Skip ignored paths.
    63  		if isIgnored(path) {
    64  			return nil
    65  		}
    66  
    67  		// Check for file types supported by `fileCommentPrefixes`.
    68  		ext := filepath.Ext(path)
    69  		if commentPrefix, ok := config.FileCommentPrefixes[ext]; ok {
    70  			processedCount++
    71  			currentHeader := transformHeader(config.HeaderLines, commentPrefix)
    72  
    73  			// Check the file for the required header.
    74  			if missing, err := checkHeader(path, currentHeader); err != nil {
    75  				filesWithError = append(filesWithError, fmt.Sprintf("%s (error: %v)", path, err))
    76  			} else if missing {
    77  				filesWithIssues = append(filesWithIssues, path)
    78  			}
    79  		}
    80  
    81  		return nil
    82  	})
    83  
    84  	if err != nil {
    85  		fmt.Printf("❌ Error during directory traversal: %v\n", err)
    86  		os.Exit(2) // Critical error during file traversal.
    87  	}
    88  
    89  	// Print the summary and get the exit code.
    90  	exitCode := printSummary()
    91  	os.Exit(exitCode)
    92  }
    93  
    94  // loadConfig loads the JSON configuration into the global config variable.
    95  func loadConfig(filepath string) error {
    96  	file, err := os.Open(filepath)
    97  	if err != nil {
    98  		return fmt.Errorf("failed to open configuration file: %w", err)
    99  	}
   100  	defer file.Close()
   101  
   102  	decoder := json.NewDecoder(file)
   103  	if err := decoder.Decode(&config); err != nil {
   104  		return fmt.Errorf("failed to decode configuration: %w", err)
   105  	}
   106  
   107  	return nil
   108  }
   109  
   110  // isIgnored determines if a file or directory should be skipped based on ignoredPaths.
   111  func isIgnored(path string) bool {
   112  	normalizedPath := strings.ReplaceAll(path, string(filepath.Separator), "/")
   113  	for _, pattern := range config.IgnoredPaths {
   114  		if matched, _ := filepath.Match(pattern, normalizedPath); matched {
   115  			return true
   116  		}
   117  		if strings.Contains(pattern, "**") {
   118  			prefix := strings.Split(pattern, "**")[0]
   119  			if strings.HasPrefix(normalizedPath, prefix) {
   120  				return true
   121  			}
   122  		}
   123  	}
   124  	return false
   125  }
   126  
   127  // transformHeader adjusts the header format for the target file's comment style.
   128  func transformHeader(header []string, prefix string) []string {
   129  	transformed := make([]string, len(header))
   130  	for i, line := range header {
   131  		transformed[i] = prefix + " " + strings.TrimPrefix(line, "// ")
   132  	}
   133  	return transformed
   134  }
   135  
   136  // checkHeader verifies if the file contains the required header.
   137  func checkHeader(path string, headerLines []string) (bool, error) {
   138  	file, err := os.Open(path)
   139  	if err != nil {
   140  		return false, err
   141  	}
   142  	defer file.Close()
   143  
   144  	scanner := bufio.NewScanner(file)
   145  	lines := []string{}
   146  	skipShebang := filepath.Ext(path) == ".sh" // Handle shebang for shell scripts.
   147  
   148  	// Only scan the first `MaxScanLines` lines.
   149  	for i := 0; i < config.MaxScanLines && scanner.Scan(); i++ {
   150  		line := strings.TrimSpace(scanner.Text())
   151  
   152  		// Skip shebang if present.
   153  		if skipShebang && strings.HasPrefix(line, "#!") {
   154  			skipShebang = false
   155  			continue
   156  		}
   157  
   158  		lines = append(lines, line)
   159  	}
   160  
   161  	if err := scanner.Err(); err != nil {
   162  		return false, err
   163  	}
   164  
   165  	return !containsHeader(lines, headerLines), nil
   166  }
   167  
   168  // containsHeader checks if the required header lines exist in a file.
   169  func containsHeader(fileLines, headerLines []string) bool {
   170  	i := 0
   171  	for _, line := range fileLines {
   172  		if line == headerLines[i] {
   173  			i++
   174  			if i == len(headerLines) {
   175  				return true
   176  			}
   177  		}
   178  	}
   179  	return false
   180  }
   181  
   182  // printSummary displays the results after processing all files and returns the exit code.
   183  func printSummary() int {
   184  	fmt.Println("Processing complete.")
   185  	fmt.Printf("Total files processed: %d\n", processedCount)
   186  
   187  	exitCode := 0
   188  
   189  	if len(filesWithIssues) > 0 {
   190  		fmt.Printf("Missing headers: %d\n", len(filesWithIssues))
   191  		for _, file := range filesWithIssues {
   192  			fmt.Printf("   - %s\n", file)
   193  		}
   194  
   195  		exitCode = 1 // Missing headers found.
   196  	}
   197  
   198  	if len(filesWithError) > 0 {
   199  		fmt.Printf("Errors encountered in files: %d\n", len(filesWithError))
   200  		for _, file := range filesWithError {
   201  			fmt.Printf("   - %s\n", file)
   202  		}
   203  
   204  		if exitCode == 1 {
   205  			exitCode = 3 // Both missing headers and errors.
   206  		} else {
   207  			exitCode = 2 // Only errors occurred.
   208  		}
   209  	}
   210  
   211  	if len(filesWithIssues) == 0 && len(filesWithError) == 0 {
   212  		fmt.Println("All processed files have the expected header.")
   213  	}
   214  
   215  	return exitCode
   216  }