Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ unqueryvet is a Go static analysis tool (linter) that detects `SELECT *` usage i
## Features

- **Detects `SELECT *` in string literals** - Finds problematic queries in your Go code
- **Constants and variables support** - Detects `SELECT *` in const and var declarations
- **SQL Builder support** - Works with popular SQL builders like Squirrel, GORM, etc.
- **Highly configurable** - Extensive configuration options for different use cases
- **Supports `//nolint:unqueryvet`** - Standard Go linting suppression
Expand Down Expand Up @@ -78,6 +79,12 @@ linters:
### Problematic code (will trigger warnings)

```go
// Constants with SELECT *
const QueryUsers = "SELECT * FROM users"

// Variables with SELECT *
var QueryOrders = "SELECT * FROM orders"

// String literals with SELECT *
query := "SELECT * FROM users"
rows, err := db.Query("SELECT * FROM orders WHERE status = ?", "active")
Expand All @@ -90,7 +97,13 @@ query := builder.Select().Columns("*").From("inventory")
### Good code (recommended)

```go
// Explicit column selection
// Constants with explicit columns
const QueryUsers = "SELECT id, name, email FROM users"

// Variables with explicit columns
var QueryOrders = "SELECT id, status, total FROM orders"

// String literals with explicit column selection
query := "SELECT id, name, email FROM users"
rows, err := db.Query("SELECT id, total FROM orders WHERE status = ?", "active")

Expand Down
150 changes: 124 additions & 26 deletions _examples/testdata/example.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,38 +10,131 @@ import (
// ExampleBadCode shows patterns that Unqueryvet will warn about
func ExampleBadCode() {
var db *sql.DB
_ = db // db would be used in real code

// BAD: Package-level style constants with SELECT *
const (
queryUsers = "SELECT * FROM users"
queryOrders = "SELECT * FROM orders WHERE status = 'pending'"
queryComplex = `
SELECT *
FROM products
WHERE price > 100
ORDER BY created_at DESC
`
)
_, _, _ = queryUsers, queryOrders, queryComplex

// BAD: Single constant with SELECT *
const querySingle = "SELECT * FROM inventory"
_ = querySingle

// BAD: Variables with SELECT *
var (
dynamicQuery = "SELECT * FROM logs WHERE date > NOW()"
configQuery = "SELECT * FROM settings"
)
_, _ = dynamicQuery, configQuery

// BAD: Config-style constants
const (
getAllUsers = "SELECT * FROM users"
getAllProducts = "SELECT * FROM products"
getAllOrders = "SELECT * FROM orders"
)
_, _, _ = getAllUsers, getAllProducts, getAllOrders

// BAD: Local constant with SELECT *
const localBadQuery = "SELECT * FROM categories"
_ = localBadQuery

// BAD: Multiple local constants with SELECT *
const (
localQuery1 = "SELECT * FROM tags"
localQuery2 = "SELECT * FROM comments WHERE active = true"
)
_, _ = localQuery1, localQuery2

// BAD: Mixed declaration styles - all use SELECT *
const constQuery = "SELECT * FROM users"
var varQuery = "SELECT * FROM products"
shortQuery := "SELECT * FROM orders"
_, _, _ = constQuery, varQuery, shortQuery

// BAD: Direct SELECT * in string literal
// unqueryvet will warn about this
query1 := "SELECT * FROM users WHERE active = true"
_ = query1
_ = db // db would be used in real code

// BAD: SELECT * in function call
// unqueryvet will warn about this
// BAD: SELECT * in function call argument
// In real code: rows, err := db.Query("SELECT * FROM products")
_ = "SELECT * FROM products"

// BAD: Multiline SELECT *
// unqueryvet will warn about this
multilineQuery := `
SELECT *
FROM orders
WHERE status = 'pending'
`
_ = multilineQuery

// BAD: Real-world example - query constants for single use
const (
getUsersQuery = "SELECT * FROM users WHERE role = 'admin'"
getProductsQuery = "SELECT * FROM products WHERE in_stock = true"
getOrdersQuery = "SELECT * FROM orders WHERE created_at > NOW() - INTERVAL '7 days'"
)
_, _, _ = getUsersQuery, getProductsQuery, getOrdersQuery

// BAD: SQL Builder patterns (pseudo-code)
// query := squirrel.Select("*").From("users")
// query := squirrel.Select().From("users") // Empty Select defaults to SELECT *
}

// ExampleGoodCode shows patterns that Unqueryvet approves
func ExampleGoodCode() {
var db *sql.DB
_ = db // db would be used in real code

// GOOD: Explicit column selection
// GOOD: Package-level style constants with explicit columns
const (
goodQueryPackage = "SELECT id, name, email FROM users"
countQueryPackage = "SELECT COUNT(*) FROM users"
schemaQueryPackage = "SELECT * FROM information_schema.tables"
)
_, _, _ = goodQueryPackage, countQueryPackage, schemaQueryPackage

// GOOD: Not a SQL query
const messagePackage = "Use * for multiplication"
_ = messagePackage

// GOOD: Config-style constants with explicit columns
const (
getUserBasics = "SELECT id, username, email FROM users"
getProductInfo = "SELECT id, name, price, stock FROM products"
getOrderDetails = "SELECT id, user_id, total, status FROM orders"
)
_, _, _ = getUserBasics, getProductInfo, getOrderDetails

// GOOD: Local constant with explicit columns
const localGoodQuery = "SELECT id, name FROM categories"
_ = localGoodQuery

// GOOD: Multiple local constants with explicit columns
const (
goodLocalQuery1 = "SELECT id, name FROM tags"
goodLocalQuery2 = "SELECT id, text, author_id FROM comments WHERE active = true"
)
_, _ = goodLocalQuery1, goodLocalQuery2

// GOOD: Local constant with COUNT(*)
const localCountQuery = "SELECT COUNT(*) FROM sessions"
_ = localCountQuery

// GOOD: Explicit column selection in variable
query1 := "SELECT id, name, email FROM users WHERE active = true"
_ = query1

// GOOD: Specific columns in function call
// GOOD: Specific columns in function call argument
// In real code: rows, err := db.Query("SELECT id, name, price FROM products")
_ = "SELECT id, name, price FROM products"

Expand All @@ -60,36 +153,41 @@ func ExampleGoodCode() {
// GOOD: Information schema queries are allowed
schemaQuery := "SELECT * FROM information_schema.tables WHERE table_name = 'users'"
_ = schemaQuery

// GOOD: Real-world example - properly defined queries
const (
getUsersQueryGood = "SELECT id, username, email, role FROM users WHERE role = 'admin'"
getProductsQueryGood = "SELECT id, name, price, stock FROM products WHERE in_stock = true"
getOrdersQueryGood = "SELECT id, user_id, total, created_at FROM orders WHERE created_at > NOW() - INTERVAL '7 days'"
)
_, _, _ = getUsersQueryGood, getProductsQueryGood, getOrdersQueryGood

// GOOD: SQL Builder patterns (pseudo-code)
// query := squirrel.Select("id", "name", "email").From("users")
// query := squirrel.Select().Columns("id", "name").From("users")
}

// ExampleSuppression shows how to suppress Unqueryvet warnings
func ExampleSuppression() {
var db *sql.DB
_ = db // db would be used in real code

// Using nolint directive to suppress warning
// Using nolint directive to suppress warning for variable
debugQuery := "SELECT * FROM debug_logs" //nolint:unqueryvet
_ = debugQuery

// Alternative: use for temporary debugging
// Remove before committing
tempQuery := "SELECT * FROM temp_table" //nolint:unqueryvet // temporary for debugging
// Using nolint directive for constant
const tempQuery = "SELECT * FROM temp_table" //nolint:unqueryvet
_ = tempQuery
}

// ExampleSQLBuilders shows SQL builder patterns
func ExampleSQLBuilders() {
// Pseudo-code examples (requires actual SQL builder libraries)

// BAD: SELECT * in SQL builder
// query := squirrel.Select("*").From("users")

// BAD: Empty Select defaults to SELECT *
// query := squirrel.Select().From("users")

// GOOD: Explicit columns in SQL builder
// query := squirrel.Select("id", "name", "email").From("users")
// Using nolint directive for multiple constants
const (
debugConst = "SELECT * FROM backup" //nolint:unqueryvet
tempConst = "SELECT * FROM temp_data" //nolint:unqueryvet // temporary for debugging
)
_, _ = debugConst, tempConst

// GOOD: Using Columns method
// query := squirrel.Select().Columns("id", "name").From("users")
// Using nolint for package-level style variable
var tempVar = "SELECT * FROM staging" //nolint:unqueryvet
_ = tempVar
}
39 changes: 39 additions & 0 deletions internal/analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ func RunWithConfig(pass *analysis.Pass, cfg *config.UnqueryvetSettings) (any, er
(*ast.CallExpr)(nil), // Function/method calls
(*ast.File)(nil), // Files (for SQL builder analysis)
(*ast.AssignStmt)(nil), // Assignment statements for standalone literals
(*ast.GenDecl)(nil), // General declarations (const, var, type)
}

// Walk through all AST nodes and analyze them
Expand All @@ -77,6 +78,9 @@ func RunWithConfig(pass *analysis.Pass, cfg *config.UnqueryvetSettings) (any, er
case *ast.AssignStmt:
// Check assignment statements for standalone SQL literals
checkAssignStmt(pass, node, cfg)
case *ast.GenDecl:
// Check constant and variable declarations
checkGenDecl(pass, node, cfg)
case *ast.CallExpr:
// Analyze function calls for SQL with SELECT * usage
checkCallExpr(pass, node, cfg)
Expand All @@ -95,6 +99,7 @@ func run(pass *analysis.Pass) (any, error) {
(*ast.CallExpr)(nil), // Function/method calls
(*ast.File)(nil), // Files (for SQL builder analysis)
(*ast.AssignStmt)(nil), // Assignment statements for standalone literals
(*ast.GenDecl)(nil), // General declarations (const, var)
}

// Always use default settings since passing settings through ResultOf doesn't work reliably
Expand All @@ -112,6 +117,9 @@ func run(pass *analysis.Pass) (any, error) {
case *ast.AssignStmt:
// Check assignment statements for standalone SQL literals
checkAssignStmt(pass, node, cfg)
case *ast.GenDecl:
// Check constant and variable declarations
checkGenDecl(pass, node, cfg)
case *ast.CallExpr:
// Analyze function calls for SQL with SELECT * usage
checkCallExpr(pass, node, cfg)
Expand All @@ -138,6 +146,37 @@ func checkAssignStmt(pass *analysis.Pass, stmt *ast.AssignStmt, cfg *config.Unqu
}
}

// checkGenDecl checks general declarations (const, var) for SELECT * in SQL queries
func checkGenDecl(pass *analysis.Pass, decl *ast.GenDecl, cfg *config.UnqueryvetSettings) {
// Only check const and var declarations
if decl.Tok != token.CONST && decl.Tok != token.VAR {
return
}

// Iterate through all specifications in the declaration
for _, spec := range decl.Specs {
// Type assert to ValueSpec (const/var specifications)
valueSpec, ok := spec.(*ast.ValueSpec)
if !ok {
continue
}

// Check all values in the specification
for _, value := range valueSpec.Values {
// Only check direct string literals
if lit, ok := value.(*ast.BasicLit); ok && lit.Kind == token.STRING {
content := normalizeSQLQuery(lit.Value)
if isSelectStarQuery(content, cfg) {
pass.Report(analysis.Diagnostic{
Pos: lit.Pos(),
Message: getWarningMessage(),
})
}
}
}
}
}

// checkCallExpr analyzes function calls for SQL with SELECT * usage
// Includes checking arguments and SQL builders
func checkCallExpr(pass *analysis.Pass, call *ast.CallExpr, cfg *config.UnqueryvetSettings) {
Expand Down
44 changes: 43 additions & 1 deletion internal/analyzer/testdata/src/a/a.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,48 @@ import (
"os"
)

// Package-level constants with SELECT * (should trigger warnings)
const (
// Should trigger warning - SELECT * in const
BadQuery1 = "SELECT * FROM users" // want "avoid SELECT \\* - explicitly specify needed columns for better performance, maintainability and stability"

// Should trigger warning - SELECT * with WHERE
BadQuery2 = "SELECT * FROM orders WHERE status = 'active'" // want "avoid SELECT \\* - explicitly specify needed columns for better performance, maintainability and stability"

// Good query - explicit columns
GoodQuery1 = "SELECT id, name, email FROM users"

// Good query - COUNT(*)
GoodQuery2 = "SELECT COUNT(*) FROM users"

// Good query - system tables
GoodQuery3 = "SELECT * FROM information_schema.tables"
)

// Single package-level const declaration
const SingleBadQuery = "SELECT * FROM products" // want "avoid SELECT \\* - explicitly specify needed columns for better performance, maintainability and stability"

const SingleGoodQuery = "SELECT id, name FROM products"

// Multiline query constant
const MultilineQuery = `SELECT * FROM inventory WHERE quantity > 0` // want "avoid SELECT \\* - explicitly specify needed columns for better performance, maintainability and stability"

// Package-level variables (should also be checked)
var (
VarBadQuery = "SELECT * FROM categories" // want "avoid SELECT \\* - explicitly specify needed columns for better performance, maintainability and stability"
VarGoodQuery = "SELECT id, name FROM categories"
VarCount = "SELECT COUNT(*) FROM categories"
)

// Non-SQL constants (should not trigger warnings)
const (
NotSQL = "This is not * a SQL query"
AlsoNotSQL = "Use asterisk * in documentation"
JustText = "Some random * text"
NumberValue = 42
BoolValue = true
)

// Basic SELECT * detection in string literals
func basicSelectStar() {
query := "SELECT * FROM users" // want "avoid SELECT \\* - explicitly specify needed columns for better performance, maintainability and stability"
Expand Down Expand Up @@ -61,7 +103,7 @@ func defaultBehavior() {
_, _ = os.Open("file.sql")
}

// Test data for removed nolint functionality
// Test data for removed nolint functionality
func removedNolintDirectives() {
// This now triggers - nolint support removed
query := "SELECT * FROM temp_table" // want "avoid SELECT \\* - explicitly specify needed columns for better performance, maintainability and stability"
Expand Down