linter

package
v1.12.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 15, 2026 License: Apache-2.0 Imports: 8 Imported by: 0

README

SQL Linter Package

Overview

The linter package provides a comprehensive SQL linting rules engine similar to SQLFluff. It offers code style checking, auto-fix capabilities, and extensible rule system for SQL quality enforcement.

Status: Phase 1a Complete (3/10 rules implemented) Test Coverage: 98.1% (exceeded 70% target by +28%)

Key Features

  • Extensible Rule System: Plugin-based architecture for custom rules
  • Auto-Fix Capability: Automatic correction for applicable violations
  • Multi-Input Support: Files, directories (recursive), stdin
  • Severity Levels: Error, Warning, Info
  • CLI Integration: gosqlx lint command
  • Context-Aware: Access to SQL text, tokens, and AST
  • Thread-Safe: Safe for concurrent linting operations

Implemented Rules (Phase 1a)

Rule Name Severity Auto-Fix Status
L001 Trailing Whitespace Warning ✅ Yes ✅ Complete
L002 Mixed Indentation Error ✅ Yes ✅ Complete
L005 Long Lines Info ❌ No ✅ Complete

Planned Rules (Phase 1)

Rule Name Status
L003 Consecutive Blank Lines 📋 Planned
L004 Indentation Depth 📋 Planned
L006 SELECT Column Alignment 📋 Planned
L007 Keyword Case Consistency 📋 Planned
L008 Comma Placement 📋 Planned
L009 Aliasing Consistency 📋 Planned
L010 Redundant Whitespace 📋 Planned

Usage

CLI Usage
# Lint a single file
gosqlx lint query.sql

# Auto-fix violations
gosqlx lint --auto-fix query.sql

# Lint directory recursively
gosqlx lint -r ./sql-queries/

# Custom max line length
gosqlx lint --max-length 120 query.sql

# Lint from stdin
cat query.sql | gosqlx lint
echo "SELECT * FROM users" | gosqlx lint
Programmatic Usage
package main

import (
    "github.com/ajitpratap0/GoSQLX/pkg/linter"
    "github.com/ajitpratap0/GoSQLX/pkg/linter/rules/whitespace"
)

func main() {
    // Create linter with rules
    l := linter.New(
        whitespace.NewTrailingWhitespaceRule(),
        whitespace.NewMixedIndentationRule(),
        whitespace.NewLongLinesRule(100), // Max 100 chars
    )

    // Lint SQL string
    sql := `SELECT * FROM users WHERE active = true  `  // Trailing space
    result := l.LintString(sql, "query.sql")
    if result.Error != nil {
        // Handle error
    }

    // Check violations
    for _, violation := range result.Violations {
        fmt.Printf("[%s] Line %d: %s\n",
            violation.Rule,
            violation.Location.Line,
            violation.Message)
    }
}
Auto-Fix Example
l := linter.New(
    whitespace.NewTrailingWhitespaceRule(),
    whitespace.NewMixedIndentationRule(),
)

sql := `SELECT *
FROM users	WHERE active = true`  // Mixed tabs/spaces, trailing space

// Lint and get violations
result := l.LintString(sql, "query.sql")

// Auto-fix violations by rule
for _, rule := range l.Rules() {
    if rule.CanAutoFix() {
        // Get violations for this rule
        ruleViolations := []Violation{}
        for _, v := range result.Violations {
            if v.Rule == rule.ID() {
                ruleViolations = append(ruleViolations, v)
            }
        }

        if len(ruleViolations) > 0 {
            fixedSQL, err := rule.Fix(sql, ruleViolations)
            if err == nil {
                sql = fixedSQL
            }
        }
    }
}

fmt.Println(sql)  // Cleaned SQL

Architecture

Core Components
Rule Interface
type Rule interface {
    ID() string           // L001, L002, etc.
    Name() string         // Human-readable name
    Description() string  // Detailed description
    Severity() Severity   // Error, Warning, Info
    Check(ctx *Context) ([]Violation, error)
    CanAutoFix() bool
    Fix(content string, violations []Violation) (string, error)
}
Context

Provides access to SQL analysis results:

type Context struct {
    SQL      string                     // Raw SQL
    Filename string                     // Source file name
    Lines    []string                   // Split by line
    Tokens   []models.TokenWithSpan     // Tokenization result
    AST      *ast.AST                   // Parsed AST (if available)
    ParseErr error                      // Parse error (if any)
}
Violation

Represents a rule 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       string          // The actual line content
    Suggestion string          // How to fix the violation
    CanAutoFix bool            // Whether this violation can be auto-fixed
}
Package Structure
pkg/linter/
├── rule.go           # Rule interface, BaseRule, Violation
├── context.go        # Linting context
├── linter.go         # Main linter engine
└── rules/
    └── whitespace/
        ├── trailing_whitespace.go
        ├── mixed_indentation.go
        └── long_lines.go

Creating Custom Rules

Simple Rule Example
package myrules

import "github.com/ajitpratap0/GoSQLX/pkg/linter"

type MyCustomRule struct {
    linter.BaseRule
}

func NewMyCustomRule() *MyCustomRule {
    return &MyCustomRule{
        BaseRule: linter.NewBaseRule(
            "C001",                  // Rule ID
            "My Custom Rule",        // Name
            "Checks custom pattern", // Description
            linter.SeverityWarning,  // Severity
            false,                   // CanAutoFix
        ),
    }
}

func (r *MyCustomRule) Check(ctx *linter.Context) ([]linter.Violation, error) {
    violations := []linter.Violation{}

    // Iterate through lines
    for lineNum, line := range ctx.Lines {
        // Check for your pattern
        if /* violation found */ {
            violations = append(violations, linter.Violation{
                Rule:       r.ID(),
                RuleName:   r.Name(),
                Message:    "Custom violation message",
                Location:   models.Location{Line: lineNum + 1, Column: 1},  // 1-based
                Line:       line,
                Severity:   r.Severity(),
                CanAutoFix: false,
            })
        }
    }

    return violations, nil
}
Rule with Auto-Fix
func (r *MyCustomRule) CanAutoFix() bool {
    return true
}

func (r *MyCustomRule) Fix(content string, violations []linter.Violation) (string, error) {
    // Apply fixes to content
    fixed := content

    for _, violation := range violations {
        // Apply fix for this violation
        // ...
    }

    return fixed, nil
}

Testing

Run linter tests:

# All linter tests (98.1% coverage)
go test -v ./pkg/linter/...

# With race detection
go test -race ./pkg/linter/...

# Specific rules
go test -v ./pkg/linter/rules/whitespace/

# Coverage report
go test -cover -coverprofile=coverage.out ./pkg/linter/...
go tool cover -html=coverage.out

Performance

Benchmarks
go test -bench=. -benchmem ./pkg/linter/...
Characteristics
  • Speed: Designed for batch processing of large SQL codebases
  • Memory: Leverages existing tokenizer/parser infrastructure
  • Graceful Degradation: Works even if parsing fails (text-only rules)
  • Concurrent-Safe: Thread-safe for parallel file processing

Best Practices

1. Use Appropriate Severity
// Critical violations (prevents execution)
linter.SeverityError

// Style violations (should fix)
linter.SeverityWarning

// Informational (nice to have)
linter.SeverityInfo
2. Provide Clear Messages
// GOOD: Specific, actionable message
"Line exceeds maximum length of 100 characters (current: 125 chars)"

// BAD: Vague message
"Line too long"
3. Implement Auto-Fix When Possible
// Auto-fix for deterministic corrections
rule.CanAutoFix() == true

// Manual review for complex/ambiguous cases
rule.CanAutoFix() == false

CLI Exit Codes

Exit Code Meaning
0 No violations found
1 Violations found (errors or warnings)
2 Linter execution error

Configuration (Future)

Configuration file support planned:

# .gosqlx.yml
linter:
  rules:
    L001: enabled   # Trailing whitespace
    L002: enabled   # Mixed indentation
    L005:
      enabled: true
      max-length: 120  # Custom max line length

Examples

Example 1: Trailing Whitespace (L001)
-- VIOLATION
SELECT * FROM users
-- Trailing spaces ^^

-- FIXED
SELECT * FROM users
Example 2: Mixed Indentation (L002)
-- VIOLATION
SELECT *
    FROM users  -- 4 spaces
	WHERE id = 1  -- Tab character

-- FIXED (converted to spaces)
SELECT *
    FROM users
    WHERE id = 1
Example 3: Long Lines (L005)
-- VIOLATION (assuming max-length=80)
SELECT very_long_column_name, another_long_column, yet_another_column, and_more FROM users;

-- SUGGESTION: Break into multiple lines
SELECT
    very_long_column_name,
    another_long_column,
    yet_another_column,
    and_more
FROM users;
  • tokenizer: Provides tokens for token-based rules
  • parser: Provides AST for semantic rules
  • ast: AST node types for tree traversal

Documentation

Roadmap

Phase 1 (10 basic rules)
  • L001: Trailing Whitespace
  • L002: Mixed Indentation
  • L005: Long Lines
  • L003: Consecutive Blank Lines
  • L004: Indentation Depth
  • L006: SELECT Column Alignment
  • L007: Keyword Case Consistency
  • L008: Comma Placement
  • L009: Aliasing Consistency
  • L010: Redundant Whitespace
Phase 2 (10 more rules)
  • Naming conventions
  • Style consistency
  • Custom rule API
Phase 3 (20 advanced rules)
  • Complexity analysis
  • Performance anti-patterns
  • Rule packs (postgres, mysql, style)

Version History

  • v1.5.0: Phase 1b - 98.1% test coverage, bug fixes
  • v1.5.0: Phase 1a - Initial release with 3 whitespace rules

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:

  1. Input: SQL content (string or file)
  2. Context Creation: Builds linting context with line splitting
  3. Tokenization: Best-effort tokenization for token-based rules
  4. Parsing: Best-effort AST generation for AST-based rules
  5. Rule Execution: All rules check the context independently
  6. 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

View Source
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

func FormatResult(result Result) string

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

func FormatViolation(v Violation) string

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

func IsValidRuleID(id string) bool

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

func NewBaseRule(id, name, description string, severity Severity, canAutoFix bool) BaseRule

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

func (r BaseRule) CanAutoFix() bool

CanAutoFix returns whether auto-fix is supported

func (BaseRule) Description

func (r BaseRule) Description() string

Description returns the rule description

func (BaseRule) ID

func (r BaseRule) ID() string

ID returns the rule ID

func (BaseRule) Name

func (r BaseRule) Name() string

Name returns the rule name

func (BaseRule) Severity

func (r BaseRule) Severity() Severity

Severity returns the rule severity

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

func NewContext(sql string, filename string) *Context

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

func (c *Context) GetLine(lineNum int) string

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

func (c *Context) GetLineCount() int

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

func (c *Context) WithAST(astObj *ast.AST, err error) *Context

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

type FileResult struct {
	Filename   string
	Violations []Violation
	Error      error
}

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

func New(rules ...Rule) *Linter

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

func (l *Linter) LintDirectory(dir string, pattern string) Result

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

func (l *Linter) LintFiles(filenames []string) Result

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))

func (*Linter) Rules

func (l *Linter) Rules() []Rule

Rules returns the list of rules configured for this linter. The returned slice should not be modified.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL