diff --git a/Makefile b/Makefile index 8a335c3..260d936 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ -.PHONY: code_gen format go_lint go_lint_fix go_mod_tidy lint sqlc sql_lint sql_lint_fix vendor sql_whitespace_dry_run sql_whitespace_fix +# Sort targets alphabetically. +.PHONY: code_gen format go_lint go_lint_fix go_mod_tidy lint multiline_sql_strings_lint_fix sqlc sql_lint sql_lint_fix vendor code_gen: go_mod_tidy sqlc @@ -11,9 +12,12 @@ go_lint_fix: go_mod_tidy: go mod tidy -lint: go_lint sql_lint +lint: go_lint multiline_sql_strings_lint_fix sql_lint -lint_fix: go_lint_fix sql_lint_fix +lint_fix: go_lint_fix multiline_sql_strings_lint_fix sql_lint_fix + +multiline_sql_strings_lint_fix: + go run ./scripts/lint/multiline_sql_strings_lint.go --fix sqlc: cd internal/queries && sqlc generate @@ -25,11 +29,4 @@ sql_lint_fix: sqlfluff fix vendor: - go mod vendor - -sql_whitespace_dry_run: - go run ./scripts/acceptance_test_sql_linter/main.go - -sql_whitespace_fix: - go run ./scripts/acceptance_test_sql_linter/main.go --fix - + go mod vendor \ No newline at end of file diff --git a/scripts/acceptance_test_sql_linter/main.go b/scripts/acceptance_test_sql_linter/main.go deleted file mode 100644 index bd1e8b7..0000000 --- a/scripts/acceptance_test_sql_linter/main.go +++ /dev/null @@ -1,167 +0,0 @@ -package main - -import ( - "bufio" - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/spf13/cobra" -) - -var ( - dirPath string - fixMode bool -) - -func main() { - var rootCmd = &cobra.Command{ - Use: "replace-spaces", - Short: "Replace tabs with spaces in SQL strings within files", - Long: `This tool replaces tab characters with four spaces in SQL strings across files in a specified directory.`, - Run: func(cmd *cobra.Command, args []string) { - replaceSpaces() - }, - } - - rootCmd.Flags().StringVarP(&dirPath, "dir", "d", "./internal/migration_acceptance_tests", "Directory path containing files to process") - rootCmd.Flags().BoolVar(&fixMode, "fix", false, "Apply changes (without this flag, only shows what would change)") - - if err := rootCmd.Execute(); err != nil { - fmt.Println(err) - os.Exit(1) - } -} - -func replaceSpaces() { - files, err := os.ReadDir(dirPath) - if err != nil { - fmt.Printf("Error reading directory: %v\n", err) - return - } - - if !fixMode { - fmt.Println("Running in dry-run mode. Use --fix to apply changes.") - } - - for _, file := range files { - if file.IsDir() { - continue - } - - filePath := filepath.Join(dirPath, file.Name()) - needsChanges, err := checkFileNeedsChanges(filePath) - - if err != nil { - fmt.Printf("Error checking file %s: %v\n", filePath, err) - continue - } - - if needsChanges { - if fixMode { - if err := processFile(filePath); err != nil { - fmt.Printf("Error processing file %s: %v\n", filePath, err) - } else { - fmt.Printf("Updated file: %s\n", filePath) - } - } else { - fmt.Printf("Would update file: %s\n", filePath) - } - } - } -} - -// Check if the file would need changes without modifying it -func checkFileNeedsChanges(filePath string) (bool, error) { - srcFile, err := os.Open(filePath) - if err != nil { - return false, fmt.Errorf("failed to open source file: %w", err) - } - defer srcFile.Close() - - scanner := bufio.NewScanner(srcFile) - inSQLString := false - needsChanges := false - - for scanner.Scan() { - line := scanner.Text() - - if inSQLString && !strings.Contains(line, "`") && strings.Contains(line, "\t") { - // Found a line that needs changes - needsChanges = true - break - } - - // Toggle SQL string flag if line contains backtick - if strings.Contains(line, "`") { - inSQLString = !inSQLString - } - } - - if err := scanner.Err(); err != nil { - return false, fmt.Errorf("error reading source file: %w", err) - } - - return needsChanges, nil -} - -func processFile(filePath string) error { - // Create a temporary file - tempFile, err := os.CreateTemp(filepath.Dir(filePath), "temp_*") - if err != nil { - return fmt.Errorf("failed to create temp file: %w", err) - } - tempFilePath := tempFile.Name() - defer os.Remove(tempFilePath) // Clean up in case of failure - - // Open source file for reading - srcFile, err := os.Open(filePath) - if err != nil { - tempFile.Close() - return fmt.Errorf("failed to open source file: %w", err) - } - defer srcFile.Close() - - // Process the file - inSQLString := false - scanner := bufio.NewScanner(srcFile) - writer := bufio.NewWriter(tempFile) - - for scanner.Scan() { - line := scanner.Text() - - if inSQLString && !strings.Contains(line, "`") { - // Replace tabs with spaces in SQL strings - newLine := strings.ReplaceAll(line, "\t", " ") - _, err = writer.WriteString(newLine + "\n") - } else { - _, err = writer.WriteString(line + "\n") - } - - if err != nil { - tempFile.Close() - return fmt.Errorf("failed to write to temp file: %w", err) - } - - // Toggle SQL string flag if line contains backtick - if strings.Contains(line, "`") { - inSQLString = !inSQLString - } - } - - if err := scanner.Err(); err != nil { - tempFile.Close() - return fmt.Errorf("error reading source file: %w", err) - } - - // Ensure all buffered data is written - if err := writer.Flush(); err != nil { - tempFile.Close() - return fmt.Errorf("failed to flush buffer: %w", err) - } - tempFile.Close() - - // Replace the original file with the temporary file - return os.Rename(tempFilePath, filePath) -} diff --git a/scripts/lint/multiline_sql_strings_lint.go b/scripts/lint/multiline_sql_strings_lint.go new file mode 100644 index 0000000..169c02c --- /dev/null +++ b/scripts/lint/multiline_sql_strings_lint.go @@ -0,0 +1,126 @@ +package main + +import ( + "bufio" + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + + "github.com/spf13/cobra" +) + +func main() { + var rootCmd = &cobra.Command{ + Use: "multiline-sql-strings-lint", + Short: "Lints multiline sql strings, replacing tabs with spaces", + } + dirPath := rootCmd.Flags().String("dir", "./internal/migration_acceptance_tests", "Directory path containing files to process") + fix := rootCmd.Flags().Bool("fix", false, "Apply changes (without this flag, only shows what would change)") + rootCmd.RunE = func(cmd *cobra.Command, args []string) error { + if !*fix { + fmt.Println("Running in dry-run mode. Use --fix to apply changes.") + } + + var filesRequiringChanges []string + if err := filepath.Walk(*dirPath, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + + fi, err := os.Stat(path) + if err != nil { + return fmt.Errorf("os.Stat: %w", err) + } + + if fi.IsDir() { + return nil + } + + cr, err := processFile(path, *fix) + if err != nil { + return fmt.Errorf("processFile: %w", err) + } + if cr { + filesRequiringChanges = append(filesRequiringChanges, path) + } + return nil + }); err != nil { + return fmt.Errorf("filepath.Walk: %w", err) + } + + if len(filesRequiringChanges) == 0 { + fmt.Println("No changes required!") + return nil + } + + verb := "require changes" + if *fix { + verb = "fixed" + } + fmt.Printf("The following files %s:\n", verb) + for _, f := range filesRequiringChanges { + fmt.Printf(" - %s\n", f) + } + if !*fix { + // Return a non-zero exit code, since the linter failed, and fixes could not be applied. + os.Exit(1) + } + return nil + } + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +// processFile replaces tabs with spaces in multi-line comments (sql strings). It will return if changes need to be made +// and an error if any occurred. If fix is false, the files won't be updated. +func processFile(filePath string, fix bool) (bool, error) { + // Create a temporary file + tempFile, err := os.CreateTemp(filepath.Dir(filePath), "temp_*") + if err != nil { + return false, fmt.Errorf("failed to create temp file: %w", err) + } + defer os.Remove(tempFile.Name()) // Clean up in case of failure + defer tempFile.Close() + + // Open source file for reading + srcFile, err := os.Open(filePath) + if err != nil { + return false, fmt.Errorf("failed to open source file: %w", err) + } + defer srcFile.Close() + + scanner := bufio.NewScanner(srcFile) + writer := bufio.NewWriter(tempFile) + + inMultilineString := false + changesRequired := false + for scanner.Scan() { + line := scanner.Text() + newLine := line + if inMultilineString && !strings.Contains(line, "`") && strings.Contains(line, "\t") { + changesRequired = true + newLine = strings.ReplaceAll(line, "\t", " ") + } + if _, err = writer.WriteString(newLine + "\n"); err != nil { + return false, fmt.Errorf("writing new line: %w", err) + } + + if strings.Contains(line, "`") { + inMultilineString = !inMultilineString + } + } + + if err := scanner.Err(); err != nil { + return false, fmt.Errorf("reading source file: %w", err) + } + + if err := writer.Flush(); err != nil { + return false, fmt.Errorf("lush buffer: %w", err) + } + + return changesRequired, os.Rename(tempFile.Name(), filePath) +}