linter

package
v1.6.0 Latest Latest
Warning

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

Go to latest
Published: Dec 11, 2025 License: AGPL-3.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

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func FormatResult

func FormatResult(result Result) string

FormatResult returns a formatted string representation of linting results

func FormatViolation

func FormatViolation(v Violation) string

FormatViolation returns a formatted string representation of a violation

Types

type BaseRule

type BaseRule struct {
	// contains filtered or unexported fields
}

BaseRule provides common functionality for rules

func NewBaseRule

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

NewBaseRule creates a new base rule

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
	SQL string

	// SQL split into lines for convenience
	Lines []string

	// Tokenization results (if available)
	Tokens []models.TokenWithSpan

	// Parsing results (if available)
	AST      *ast.AST
	ParseErr error

	// File metadata
	Filename string
}

Context provides all information needed for linting

func NewContext

func NewContext(sql string, filename string) *Context

NewContext creates a new linting context

func (*Context) GetLine

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

GetLine returns a specific line (1-indexed) Returns empty string if line number is out of bounds

func (*Context) GetLineCount

func (c *Context) GetLineCount() int

GetLineCount returns the total number of lines

func (*Context) WithAST

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

WithAST adds parsing results to the context

func (*Context) WithTokens

func (c *Context) WithTokens(tokens []models.TokenWithSpan) *Context

WithTokens adds tokenization results to the context

type FileResult

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

FileResult represents linting results for a single file

type Linter

type Linter struct {
	// contains filtered or unexported fields
}

Linter performs SQL linting with configurable rules

func New

func New(rules ...Rule) *Linter

New creates a new linter with the given rules

func (*Linter) LintDirectory

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

LintDirectory recursively lints all SQL files in a directory

func (*Linter) LintFile

func (l *Linter) LintFile(filename string) FileResult

LintFile lints a single SQL file

func (*Linter) LintFiles

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

LintFiles lints multiple files

func (*Linter) LintString

func (l *Linter) LintString(sql string, filename string) FileResult

LintString lints SQL content provided as a string

func (*Linter) Rules

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

Rules returns the list of rules configured for this linter

type Result

type Result struct {
	Files           []FileResult
	TotalFiles      int
	TotalViolations int
}

Result represents the linting result for one or more files

type Rule

type Rule interface {
	// ID returns the unique rule identifier (e.g., "L001")
	ID() string

	// Name returns the human-readable rule name
	Name() string

	// Description returns a description of what the rule checks
	Description() string

	// Severity returns the default severity level for this rule
	Severity() Severity

	// Check performs the rule check and returns violations
	Check(ctx *Context) ([]Violation, error)

	// CanAutoFix returns whether this rule supports auto-fixing
	CanAutoFix() bool

	// Fix applies automatic fixes if supported
	// Returns the fixed content or an error
	Fix(content string, violations []Violation) (string, error)
}

Rule defines the interface for all linting rules

type Severity

type Severity string

Severity represents the severity level of a lint violation

const (
	SeverityError   Severity = "error"
	SeverityWarning Severity = "warning"
	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       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

Directories

Path Synopsis
rules

Jump to

Keyboard shortcuts

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