Documentation
¶
Overview ¶
Package linter provides a comprehensive SQL linting engine for GoSQLX with configurable rules, auto-fix capabilities, and detailed violation reporting.
The linter engine analyzes SQL code at multiple levels (text, tokens, AST) to enforce coding standards, style guidelines, and best practices. It includes 10 built-in rules covering whitespace, formatting, keywords, and style consistency.
Architecture ¶
The linter follows a pipeline architecture:
- Input: SQL content (string or file)
- Context Creation: Builds linting context with line splitting
- Tokenization: Best-effort tokenization for token-based rules
- Parsing: Best-effort AST generation for AST-based rules
- Rule Execution: All rules check the context independently
- Result Collection: Violations aggregated with severity levels
The pipeline is designed to be fault-tolerant - tokenization and parsing failures don't prevent text-based rules from executing. This allows linting of partially valid or syntactically incorrect SQL.
Built-in Rules ¶
The linter includes 10 production-ready rules (v1.6.0):
Whitespace Rules:
- L001: Trailing Whitespace - removes trailing spaces/tabs (auto-fix)
- L002: Mixed Indentation - enforces consistent tabs/spaces (auto-fix)
- L003: Consecutive Blank Lines - limits consecutive blank lines (auto-fix)
- L004: Indentation Depth - warns about excessive nesting (no auto-fix)
- L005: Line Length - enforces maximum line length (no auto-fix)
- L010: Redundant Whitespace - removes multiple consecutive spaces (auto-fix)
Style Rules:
- L006: Column Alignment - checks SELECT column alignment (no auto-fix)
- L008: Comma Placement - enforces trailing/leading comma style (no auto-fix)
- L009: Aliasing Consistency - checks consistent table alias usage (no auto-fix)
Keyword Rules:
- L007: Keyword Case - enforces uppercase/lowercase keywords (auto-fix)
Basic Usage ¶
Create a linter with desired rules and lint SQL content:
import (
"fmt"
"github.com/ajitpratap0/GoSQLX/pkg/linter"
"github.com/ajitpratap0/GoSQLX/pkg/linter/rules/whitespace"
"github.com/ajitpratap0/GoSQLX/pkg/linter/rules/keywords"
)
func main() {
// Create linter with selected rules
l := linter.New(
whitespace.NewTrailingWhitespaceRule(),
whitespace.NewMixedIndentationRule(),
keywords.NewKeywordCaseRule(keywords.CaseUpper),
)
// Lint SQL string
sql := "SELECT * FROM users WHERE active = true "
result := l.LintString(sql, "query.sql")
// Check for violations
if len(result.Violations) > 0 {
fmt.Println(linter.FormatResult(linter.Result{
Files: []linter.FileResult{result},
TotalFiles: 1,
TotalViolations: len(result.Violations),
}))
}
}
Linting Files and Directories ¶
The linter supports single files, multiple files, and directory recursion:
// Lint single file
fileResult := l.LintFile("path/to/query.sql")
// Lint multiple files
files := []string{"query1.sql", "query2.sql", "schema.sql"}
result := l.LintFiles(files)
// Lint directory recursively with pattern matching
result := l.LintDirectory("/path/to/sql/files", "*.sql")
fmt.Printf("Found %d violations in %d files\n",
result.TotalViolations, result.TotalFiles)
Auto-Fix Support ¶
Five rules support automatic fixing (L001, L002, L003, L007, L010):
sql := "select * from users" // Multiple spaces, lowercase keywords
// Lint to find violations
result := l.LintString(sql, "query.sql")
// Apply auto-fixes for rules that support it
fixedSQL := sql
for _, rule := range l.Rules() {
if rule.CanAutoFix() {
violations := filterViolationsByRule(result.Violations, rule.ID())
if len(violations) > 0 {
fixedSQL, _ = rule.Fix(fixedSQL, violations)
}
}
}
// Result: "SELECT * FROM users" (uppercase keywords, single spaces)
Custom Rules ¶
Implement the Rule interface to create custom linting rules:
type CustomRule struct {
linter.BaseRule
}
func NewCustomRule() *CustomRule {
return &CustomRule{
BaseRule: linter.NewBaseRule(
"C001", // Unique rule ID
"Custom Rule Name", // Human-readable name
"Description of what it checks", // Rule description
linter.SeverityWarning, // Default severity
false, // Auto-fix support
),
}
}
func (r *CustomRule) Check(ctx *linter.Context) ([]linter.Violation, error) {
violations := []linter.Violation{}
// Access SQL content
for lineNum, line := range ctx.Lines {
// Your custom logic here
if hasViolation(line) {
violations = append(violations, linter.Violation{
Rule: r.ID(),
RuleName: r.Name(),
Severity: r.Severity(),
Message: "Violation description",
Location: models.Location{Line: lineNum + 1, Column: 1},
Line: line,
Suggestion: "How to fix this",
CanAutoFix: false,
})
}
}
return violations, nil
}
func (r *CustomRule) Fix(content string, violations []linter.Violation) (string, error) {
// Return unchanged if no auto-fix support
return content, nil
}
Accessing Context Data ¶
Rules receive a Context with multi-level access to SQL:
func (r *CustomRule) Check(ctx *linter.Context) ([]linter.Violation, error) {
// Text level: Raw SQL and lines
sql := ctx.SQL // Complete SQL string
lines := ctx.Lines // Split into lines
line5 := ctx.GetLine(5) // Get specific line (1-indexed)
count := ctx.GetLineCount()
// Token level: Tokenization results (if available)
if ctx.Tokens != nil {
for _, tok := range ctx.Tokens {
// Check token type, value, position
fmt.Printf("Token: %s at %d:%d\n",
tok.Token.Type, tok.Span.Start.Line, tok.Span.Start.Column)
}
}
// AST level: Parsed structure (if available)
if ctx.AST != nil && ctx.ParseErr == nil {
for _, stmt := range ctx.AST.Statements {
// Analyze statement structure
if selectStmt, ok := stmt.(*ast.SelectStatement); ok {
// Check SELECT statement properties
}
}
}
// Metadata
filename := ctx.Filename
return violations, nil
}
Severity Levels ¶
Violations are categorized by severity:
- SeverityError: Critical issues that should block deployment
- SeverityWarning: Important issues that should be addressed
- SeverityInfo: Style preferences and suggestions
Severity affects violation reporting priority and can be used for CI/CD failure thresholds (e.g., fail on errors, warn on warnings).
Violation Reporting ¶
Each violation includes detailed context:
violation := linter.Violation{
Rule: "L001", // Rule ID
RuleName: "Trailing Whitespace", // Rule name
Severity: linter.SeverityWarning, // Severity level
Message: "Line has trailing whitespace", // What's wrong
Location: models.Location{Line: 42, Column: 80}, // Where (1-indexed)
Line: "SELECT * FROM users ", // Actual line
Suggestion: "Remove trailing spaces", // How to fix
CanAutoFix: true, // Auto-fix available
}
Use FormatViolation() and FormatResult() for human-readable output:
fmt.Println(linter.FormatViolation(violation)) // Output: // [L001] Trailing Whitespace at line 42, column 80 // Severity: warning // Line has trailing whitespace // // 42 | SELECT * FROM users // | ^ // // Suggestion: Remove trailing spaces
Configuration Example ¶
Typical production configuration with commonly used rules:
import (
"github.com/ajitpratap0/GoSQLX/pkg/linter"
"github.com/ajitpratap0/GoSQLX/pkg/linter/rules/whitespace"
"github.com/ajitpratap0/GoSQLX/pkg/linter/rules/keywords"
"github.com/ajitpratap0/GoSQLX/pkg/linter/rules/style"
)
func NewProductionLinter() *linter.Linter {
return linter.New(
// Whitespace rules (all with auto-fix)
whitespace.NewTrailingWhitespaceRule(),
whitespace.NewMixedIndentationRule(),
whitespace.NewConsecutiveBlankLinesRule(1), // Max 1 blank line
whitespace.NewIndentationDepthRule(4, 4), // Max 4 levels, 4 spaces
whitespace.NewLongLinesRule(100), // Max 100 chars
whitespace.NewRedundantWhitespaceRule(),
// Keyword rules
keywords.NewKeywordCaseRule(keywords.CaseUpper), // Uppercase keywords
// Style rules
style.NewColumnAlignmentRule(),
style.NewCommaPlacementRule(style.CommaTrailing), // Trailing commas
style.NewAliasingConsistencyRule(true), // Explicit AS keyword
)
}
Integration with CLI ¶
The linter is integrated into the gosqlx CLI tool:
# Lint with default rules
gosqlx lint query.sql
# Lint with auto-fix
gosqlx lint --fix query.sql
# Lint entire directory
gosqlx lint --recursive /path/to/sql/files
# Configure via .gosqlx.yml
linter:
rules:
- id: L001
enabled: true
- id: L007
enabled: true
config:
case_style: upper
- id: L005
enabled: true
config:
max_length: 120
Performance Characteristics ¶
The linter is designed for production use with efficient resource usage:
- Text-based rules: O(n) where n is line count, fastest
- Token-based rules: O(t) where t is token count, uses object pooling
- AST-based rules: O(n) where n is AST node count, uses object pooling
- Auto-fix operations: O(n) line processing, preserves string literals
- Memory: Minimal allocations, reuses tokenizer/parser pools
Typical performance: 10,000+ lines/second per rule on modern hardware.
Thread Safety ¶
The Linter type is thread-safe and can be reused across goroutines:
linter := linter.New(rules...)
// Safe to call concurrently
var wg sync.WaitGroup
for _, file := range files {
wg.Add(1)
go func(f string) {
defer wg.Done()
result := linter.LintFile(f)
processResult(result)
}(file)
}
wg.Wait()
The Context and Rule implementations are designed for concurrent execution, using read-only access patterns and avoiding shared mutable state.
Error Handling ¶
The linter uses graceful error handling:
- File read errors: Returned in FileResult.Error, don't stop batch processing
- Tokenization errors: Logged but don't prevent text-based rules from running
- Parse errors: Stored in Context.ParseErr, AST-based rules can fall back to text
- Rule errors: Returned in FileResult.Error, indicate rule implementation issues
Example error handling:
result := linter.LintFile("query.sql")
if result.Error != nil {
log.Printf("Linting error: %v", result.Error)
// Continue processing other files
}
// Check violations even if errors occurred
for _, v := range result.Violations {
handleViolation(v)
}
See Also ¶
- docs/LINTING_RULES.md - Complete reference for all 10 rules
- docs/CONFIGURATION.md - Configuration file (.gosqlx.yml) reference
- pkg/linter/rules/ - Rule implementations by category
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ValidRuleIDs = map[string]string{
"L001": "Trailing Whitespace",
"L002": "Mixed Indentation",
"L003": "Consecutive Blank Lines",
"L004": "Indentation Depth",
"L005": "Long Lines",
"L006": "Column Alignment",
"L007": "Keyword Case Consistency",
"L008": "Comma Placement",
"L009": "Aliasing Consistency",
"L010": "Redundant Whitespace",
}
ValidRuleIDs returns the set of all implemented rule IDs. Use this to validate that user-specified rule names in configuration files (e.g., .gosqlx.yml) reference actual rules.
Functions ¶
func FormatResult ¶
FormatResult returns a formatted string representation of linting results.
Produces a comprehensive report including:
- Per-file violation details with formatted violations
- File-level error messages for files that couldn't be linted
- Summary statistics (total files, total violations)
Files with no violations are omitted from the output for clarity.
Example output:
queries/search.sql: 3 violation(s) ================================================================================ [L001] Trailing Whitespace at line 5, column 42 Severity: warning ... ================================================================================ Total files: 10 Total violations: 15
func FormatViolation ¶
FormatViolation returns a formatted string representation of a violation.
The output includes:
- Rule ID and name
- Location (line and column)
- Severity level
- Message describing the violation
- The actual line content with column indicator
- Suggestion for fixing (if available)
Example output:
[L001] Trailing Whitespace at line 42, column 80
Severity: warning
Line has trailing whitespace
42 | SELECT * FROM users
| ^
Suggestion: Remove trailing spaces or tabs from the end of the line
func IsValidRuleID ¶ added in v1.9.3
IsValidRuleID checks whether a rule ID corresponds to an implemented rule.
Types ¶
type BaseRule ¶
type BaseRule struct {
// contains filtered or unexported fields
}
BaseRule provides common functionality for implementing rules.
Embedding BaseRule in custom rule types eliminates the need to implement ID(), Name(), Description(), Severity(), and CanAutoFix() methods manually. Only Check() and Fix() need to be implemented.
Example:
type MyRule struct {
linter.BaseRule
}
func NewMyRule() *MyRule {
return &MyRule{
BaseRule: linter.NewBaseRule(
"C001",
"My Custom Rule",
"Checks for custom patterns",
linter.SeverityWarning,
false,
),
}
}
func NewBaseRule ¶
NewBaseRule creates a new base rule with the specified properties.
Parameters:
- id: Unique rule identifier (e.g., "L001", "C001")
- name: Human-readable rule name
- description: Detailed description of what the rule checks
- severity: Default severity level (Error, Warning, or Info)
- canAutoFix: Whether the rule supports automatic fixing
Returns a BaseRule that can be embedded in custom rule implementations.
func (BaseRule) CanAutoFix ¶
CanAutoFix returns whether auto-fix is supported
func (BaseRule) Description ¶
Description returns the rule description
type Context ¶
type Context struct {
// Source SQL content (complete, unmodified)
SQL string
// SQL split into lines for line-by-line analysis (preserves original content)
Lines []string
// Tokenization results (nil if tokenization failed)
Tokens []models.TokenWithSpan
// Parsing results (nil if parsing failed)
AST *ast.AST
// Parse error (non-nil if parsing failed, nil if successful or not attempted)
ParseErr error
// File metadata for violation reporting
Filename string
}
Context provides all information needed for linting at multiple levels.
Context is passed to every rule's Check method and contains:
- Text level: Raw SQL and line-by-line access
- Token level: Tokenization results (if successful)
- AST level: Parsed structure (if successful)
- Metadata: Filename for reporting
Rules should check if Tokens and AST are nil before using them, as tokenization and parsing are best-effort. Text-based rules can run even if tokenization fails; token-based rules can run if parsing fails.
Example usage in a rule:
func (r *MyRule) Check(ctx *linter.Context) ([]linter.Violation, error) {
// Text level (always available)
for lineNum, line := range ctx.Lines {
// Check line content
}
// Token level (check availability)
if ctx.Tokens != nil {
for _, tok := range ctx.Tokens {
// Analyze tokens
}
}
// AST level (check availability and parse success)
if ctx.AST != nil && ctx.ParseErr == nil {
for _, stmt := range ctx.AST.Statements {
// Analyze AST structure
}
}
return violations, nil
}
func NewContext ¶
NewContext creates a new linting context from SQL content and filename.
The SQL is split into lines for convenient line-by-line analysis. Tokens and AST are initially nil and should be added via WithTokens and WithAST if tokenization and parsing succeed.
Parameters:
- sql: The SQL content to lint
- filename: File path for violation reporting (can be a logical name like "<stdin>")
Returns a new Context ready for rule checking.
func (*Context) GetLine ¶
GetLine returns a specific line by number (1-indexed).
This is a convenience method for rules that need to access individual lines by line number from violation locations.
Returns the line content, or empty string if line number is out of bounds.
Example:
line := ctx.GetLine(42) // Get line 42
if strings.TrimSpace(line) == "" {
// Line 42 is blank or whitespace-only
}
func (*Context) GetLineCount ¶
GetLineCount returns the total number of lines in the SQL content.
This is useful for rules that need to check file-level properties (e.g., overall structure, ending newlines).
func (*Context) WithAST ¶
WithAST adds parsing results to the context.
This method is called by the linter after attempting to parse tokens. Both successful and failed parses are recorded. Rules should check ctx.AST != nil && ctx.ParseErr == nil to ensure usable AST.
Parameters:
- astObj: The parsed AST (may be nil or incomplete if parsing failed)
- err: Parse error (nil if successful)
Returns the context for method chaining.
func (*Context) WithTokens ¶
func (c *Context) WithTokens(tokens []models.TokenWithSpan) *Context
WithTokens adds tokenization results to the context.
This method is called by the linter after successful tokenization. Rules can check ctx.Tokens != nil to determine if tokenization succeeded.
Returns the context for method chaining.
type FileResult ¶
FileResult represents linting results for a single file.
Fields:
- Filename: Path to the file that was linted
- Violations: All rule violations found in this file
- Error: Any error encountered during linting (file read, rule execution)
A FileResult with non-nil Error may still contain partial violations from rules that executed successfully before the error occurred.
type Linter ¶
type Linter struct {
// contains filtered or unexported fields
}
Linter performs SQL linting with configurable rules. A Linter instance is thread-safe and can be reused across goroutines.
The linter executes all configured rules independently, collecting violations from each. Rules have access to SQL text, tokens (if tokenization succeeds), and AST (if parsing succeeds), allowing multi-level analysis.
Example:
linter := linter.New(
whitespace.NewTrailingWhitespaceRule(),
keywords.NewKeywordCaseRule(keywords.CaseUpper),
)
result := linter.LintFile("query.sql")
func New ¶
New creates a new linter with the given rules.
Rules are executed in the order provided, though results are order-independent. The same linter instance can be safely reused for multiple files.
Example:
linter := linter.New(
whitespace.NewTrailingWhitespaceRule(),
whitespace.NewMixedIndentationRule(),
keywords.NewKeywordCaseRule(keywords.CaseUpper),
)
func (*Linter) LintDirectory ¶
LintDirectory recursively lints all SQL files in a directory.
The directory is walked recursively, and all files matching the pattern are linted. The pattern uses filepath.Match syntax (e.g., "*.sql", "test_*.sql").
Directory walk errors are returned in a single FileResult with Error set. Individual file linting errors are handled per-file.
Returns a Result with all matching files processed.
Example:
// Lint all .sql files in directory tree
result := linter.LintDirectory("./database", "*.sql")
// Lint only test files
result := linter.LintDirectory("./database", "test_*.sql")
// Process results
for _, fileResult := range result.Files {
if fileResult.Error != nil {
log.Printf("Error: %s: %v", fileResult.Filename, fileResult.Error)
}
for _, violation := range fileResult.Violations {
fmt.Println(linter.FormatViolation(violation))
}
}
func (*Linter) LintFile ¶
func (l *Linter) LintFile(filename string) FileResult
LintFile lints a single SQL file.
The file is read from disk and processed through all configured rules. If the file cannot be read, a FileResult with a non-nil Error is returned.
Returns a FileResult containing any violations found and potential errors.
Example:
result := linter.LintFile("queries/user_search.sql")
if result.Error != nil {
log.Printf("Error linting file: %v", result.Error)
}
for _, v := range result.Violations {
fmt.Println(linter.FormatViolation(v))
}
func (*Linter) LintFiles ¶
LintFiles lints multiple files in batch.
Each file is linted independently. Errors reading or linting one file don't prevent processing of other files. Individual file errors are captured in each FileResult.Error field.
Returns a Result with aggregated statistics and individual FileResults.
Example:
files := []string{
"queries/search.sql",
"queries/reports.sql",
"schema/tables.sql",
}
result := linter.LintFiles(files)
fmt.Printf("Processed %d files, found %d violations\n",
result.TotalFiles, result.TotalViolations)
func (*Linter) LintString ¶
func (l *Linter) LintString(sql string, filename string) FileResult
LintString lints SQL content provided as a string.
This method is useful for linting SQL from sources other than files (e.g., in-memory queries, database dumps, or editor buffers). The filename parameter is used only for violation reporting and can be a logical name.
The method performs best-effort tokenization and parsing. If tokenization fails, only text-based rules execute. If parsing fails, token-based rules still run. This allows partial linting of syntactically invalid SQL.
Returns a FileResult containing violations. The Error field is only set if a rule execution fails, not for tokenization/parsing failures.
Example:
sql := "SELECT * FROM users WHERE status = 'active'"
result := linter.LintString(sql, "<stdin>")
fmt.Printf("Found %d violations\n", len(result.Violations))
type Result ¶
type Result struct {
Files []FileResult
TotalFiles int
TotalViolations int
}
Result represents the linting result for one or more files. It aggregates individual file results and provides summary statistics for batch linting operations.
Fields:
- Files: Results for each file that was linted
- TotalFiles: Total number of files processed
- TotalViolations: Sum of violations across all files
Use FormatResult to generate human-readable output.
type Rule ¶
type Rule interface {
// ID returns the unique rule identifier (e.g., "L001", "L002").
// IDs should be unique across all rules in a linter instance.
// Built-in rules use L001-L010, custom rules should use a different prefix.
ID() string
// Name returns the human-readable rule name displayed in violation reports.
// Example: "Trailing Whitespace", "Keyword Case Consistency"
Name() string
// Description returns a detailed description of what the rule checks.
// This should explain the rule's purpose and what patterns it enforces.
Description() string
// Severity returns the default severity level for this rule.
// Returns one of: SeverityError, SeverityWarning, or SeverityInfo.
Severity() Severity
// Check performs the rule check and returns any violations found.
//
// The context provides access to SQL text, tokens (if available), and
// AST (if available). Rules should handle missing tokenization/parsing
// gracefully by checking ctx.Tokens and ctx.AST for nil.
//
// Returns a slice of violations (empty if none found) and any error
// encountered during checking. Errors should indicate rule implementation
// issues, not SQL syntax problems.
Check(ctx *Context) ([]Violation, error)
// CanAutoFix returns whether this rule supports automatic fixing.
// If true, the Fix method should be implemented to apply corrections.
CanAutoFix() bool
// Fix applies automatic fixes for the given violations.
//
// Takes the original SQL content and violations from this rule, returns
// the fixed content. If the rule doesn't support auto-fixing, this should
// return the content unchanged.
//
// The Fix implementation should:
// - Preserve SQL semantics (don't change query meaning)
// - Handle edge cases (string literals, comments)
// - Be idempotent (applying twice produces same result)
//
// Returns the fixed content and any error encountered during fixing.
Fix(content string, violations []Violation) (string, error)
}
Rule defines the interface that all linting rules must implement.
Rules check SQL content at various levels (text, tokens, AST) and report violations. Rules can optionally support automatic fixing of violations.
Implementing a custom rule:
type MyRule struct {
linter.BaseRule
}
func NewMyRule() *MyRule {
return &MyRule{
BaseRule: linter.NewBaseRule(
"C001", // Unique ID
"My Custom Rule", // Name
"Description of rule", // Description
linter.SeverityWarning, // Severity
false, // Auto-fix support
),
}
}
func (r *MyRule) Check(ctx *linter.Context) ([]linter.Violation, error) {
// Implement rule logic
return violations, nil
}
func (r *MyRule) Fix(content string, violations []linter.Violation) (string, error) {
// Implement fix logic (if CanAutoFix is true)
return content, nil
}
Rules should be stateless and thread-safe for concurrent use.
type Severity ¶
type Severity string
Severity represents the severity level of a lint violation.
Severity levels can be used to categorize violations and determine CI/CD failure thresholds (e.g., fail builds on errors, warn on warnings).
const ( // SeverityError indicates critical issues that should block deployment. // Examples: mixed indentation, syntax errors, security vulnerabilities. SeverityError Severity = "error" // SeverityWarning indicates important issues that should be addressed. // Examples: trailing whitespace, inconsistent keyword case, missing aliases. SeverityWarning Severity = "warning" // SeverityInfo indicates style preferences and suggestions. // Examples: line length, column alignment, comma placement. SeverityInfo Severity = "info" )
type Violation ¶
type Violation struct {
Rule string // Rule ID (e.g., "L001")
RuleName string // Human-readable rule name
Severity Severity // Severity level
Message string // Violation description
Location models.Location // Position in source (1-based line and column)
Line string // The actual line content
Suggestion string // How to fix the violation
CanAutoFix bool // Whether this violation can be auto-fixed
}
Violation represents a single linting rule violation with full context.
Violations include precise location information, the actual problematic code, and suggestions for fixing. Violations may support automatic fixing depending on the rule.
Example:
violation := linter.Violation{
Rule: "L001",
RuleName: "Trailing Whitespace",
Severity: linter.SeverityWarning,
Message: "Line has trailing whitespace",
Location: models.Location{Line: 42, Column: 80},
Line: "SELECT * FROM users ",
Suggestion: "Remove trailing spaces or tabs",
CanAutoFix: true,
}
Directories
¶
| Path | Synopsis |
|---|---|
|
rules
|
|
|
keywords
Package keywords provides linting rules for SQL keyword formatting and consistency.
|
Package keywords provides linting rules for SQL keyword formatting and consistency. |
|
style
Package style provides linting rules for SQL style and formatting conventions.
|
Package style provides linting rules for SQL style and formatting conventions. |
|
whitespace
Package whitespace provides linting rules for whitespace and formatting issues.
|
Package whitespace provides linting rules for whitespace and formatting issues. |