Skip to content
Merged
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
45 changes: 29 additions & 16 deletions cmd/mtlog-analyzer/analyzer/property_checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,29 +197,42 @@ func checkDollarPrefix(pass *analysis.Pass, call *ast.CallExpr, arg ast.Expr, ar
func checkNoPrefix(pass *analysis.Pass, call *ast.CallExpr, arg ast.Expr, argType types.Type, propName string, config *Config) {
// No prefix - suggest @ for complex types
if !isBasicType(argType) && !isTimeType(argType) && !isStringer(argType) && !isErrorType(argType) {
// Create suggested fix to add @ prefix
// Check if diagnostic is suppressed
if config.SuppressedDiagnostics[DiagIDCapturingHints] {
return
}

var suggestedFixes []analysis.SuggestedFix

// First fix: Add @ prefix
if lit, ok := call.Args[0].(*ast.BasicLit); ok {
oldTemplate := lit.Value
newProp := "@" + propName
// Replace in the template, preserving quotes
newTemplate := strings.Replace(oldTemplate, "{"+propName, "{"+newProp, -1)

// Check if diagnostic is suppressed
if !config.SuppressedDiagnostics[DiagIDCapturingHints] {
diag := analysis.Diagnostic{
Pos: arg.Pos(),
Message: fmt.Sprintf("[%s] %s: consider using @ prefix for complex type %s to enable capturing", DiagIDCapturingHints, SeveritySuggestion, argType),
SuggestedFixes: []analysis.SuggestedFix{{
Message: fmt.Sprintf("Add @ prefix to '%s' for capturing", propName),
TextEdits: []analysis.TextEdit{{
Pos: lit.Pos(),
End: lit.End(),
NewText: []byte(newTemplate),
}},
}},
}
pass.Report(diag)
suggestedFixes = append(suggestedFixes, analysis.SuggestedFix{
Message: fmt.Sprintf("Add @ prefix to '%s' for capturing", propName),
TextEdits: []analysis.TextEdit{{
Pos: lit.Pos(),
End: lit.End(),
NewText: []byte(newTemplate),
}},
})
}

// Second fix: Generate LogValue() method stub
if logValueFix := createLogValueStub(pass, argType); logValueFix != nil {
suggestedFixes = append(suggestedFixes, *logValueFix)
}

if len(suggestedFixes) > 0 {
diag := analysis.Diagnostic{
Pos: arg.Pos(),
Message: fmt.Sprintf("[%s] %s: consider using @ prefix for complex type %s to enable capturing", DiagIDCapturingHints, SeveritySuggestion, argType),
SuggestedFixes: suggestedFixes,
}
pass.Report(diag)
} else {
reportDiagnosticWithID(pass, arg.Pos(), SeveritySuggestion, config, DiagIDCapturingHints,
"consider using @ prefix for complex type %s to enable capturing", argType)
Expand Down
209 changes: 209 additions & 0 deletions cmd/mtlog-analyzer/analyzer/quickfix.go
Original file line number Diff line number Diff line change
Expand Up @@ -527,4 +527,213 @@ func parseInt(s string, def int) int {
return val
}
return def
}

// createLogValueStub generates a LogValue() method stub for a complex type
func createLogValueStub(pass *analysis.Pass, argType types.Type) *analysis.SuggestedFix {
// Get the underlying named type
named, ok := argType.(*types.Named)
if !ok {
// Check if it's a pointer to a named type
if ptr, ok := argType.(*types.Pointer); ok {
named, ok = ptr.Elem().(*types.Named)
if !ok {
return nil
}
} else {
return nil
}
}

// Don't generate for types from other packages
if named.Obj().Pkg() == nil {
return nil
}
// Check if the type is in the current package being analyzed
if named.Obj().Pkg() != pass.Pkg {
// For test purposes, also check if it's in the same file
typePos := named.Obj().Pos()
typeInCurrentFile := false
for _, file := range pass.Files {
if file.Pos() <= typePos && typePos < file.End() {
typeInCurrentFile = true
break
}
}
if !typeInCurrentFile {
return nil
}
}

// Check if LogValue method already exists
if hasLogValueMethod(named) {
return nil
}

// Get the struct type to inspect fields
structType, ok := named.Underlying().(*types.Struct)
if !ok {
// Not a struct, can't generate meaningful stub
return nil
}

// Find the type declaration in the AST
typePos := named.Obj().Pos()
var targetFile *ast.File
for _, file := range pass.Files {
if file.Pos() <= typePos && typePos < file.End() {
targetFile = file
break
}
}

if targetFile == nil {
return nil
}

// Find the end of the type declaration to insert the method after
var typeDecl *ast.TypeSpec
ast.Inspect(targetFile, func(n ast.Node) bool {
if ts, ok := n.(*ast.TypeSpec); ok && ts.Name.Name == named.Obj().Name() {
typeDecl = ts
return false
}
return true
})

if typeDecl == nil {
return nil
}

// Generate the LogValue method stub
methodStub := generateLogValueMethodStub(named, structType)

// Find the insertion point (after the type declaration)
insertPos := findMethodInsertPosition(targetFile, typeDecl)

return &analysis.SuggestedFix{
Message: fmt.Sprintf("Generate LogValue() method for %s", named.Obj().Name()),
TextEdits: []analysis.TextEdit{{
Pos: insertPos,
End: insertPos,
NewText: []byte(methodStub),
}},
}
}

// hasLogValueMethod checks if a type already has a LogValue method
func hasLogValueMethod(named *types.Named) bool {
for i := 0; i < named.NumMethods(); i++ {
method := named.Method(i)
if method.Name() == "LogValue" {
return true
}
}
return false
}

// generateLogValueMethodStub generates the LogValue method code
func generateLogValueMethodStub(named *types.Named, structType *types.Struct) string {
typeName := named.Obj().Name()
receiverName := strings.ToLower(typeName[:1])

var sb strings.Builder
sb.WriteString(fmt.Sprintf("\n\n// LogValue provides a custom log representation for %s\n", typeName))
sb.WriteString(fmt.Sprintf("func (%s %s) LogValue() any {\n", receiverName, typeName))
sb.WriteString("\treturn map[string]any{\n")

// List of common sensitive field names to warn about
sensitiveNames := map[string]bool{
"password": true, "pass": true, "passwd": true, "pwd": true,
"secret": true, "apikey": true, "api_key": true, "apiKey": true,
"token": true, "accesstoken": true, "access_token": true, "accessToken": true,
"refreshtoken": true, "refresh_token": true, "refreshToken": true,
"privatekey": true, "private_key": true, "privateKey": true,
"key": true, "authtoken": true, "auth_token": true, "authToken": true,
"credential": true, "credentials": true, "cred": true, "creds": true,
"ssn": true, "socialsecurity": true, "social_security": true,
"creditcard": true, "credit_card": true, "creditCard": true,
"cardnumber": true, "card_number": true, "cardNumber": true,
"cvv": true, "cvc": true, "securitycode": true, "security_code": true,
}

// Generate field entries
for i := 0; i < structType.NumFields(); i++ {
field := structType.Field(i)
if !field.Exported() {
continue
}

fieldName := field.Name()
fieldNameLower := strings.ToLower(fieldName)

// Check if this might be a sensitive field
isSensitive := false
for sensitive := range sensitiveNames {
if strings.Contains(fieldNameLower, sensitive) {
isSensitive = true
break
}
}

if isSensitive {
sb.WriteString(fmt.Sprintf("\t\t// \"%s\": %s.%s, // TODO: Review - potentially sensitive field\n",
fieldName, receiverName, fieldName))
} else {
sb.WriteString(fmt.Sprintf("\t\t\"%s\": %s.%s,\n", fieldName, receiverName, fieldName))
}
}

sb.WriteString("\t}\n")
sb.WriteString("}")

return sb.String()
}

// findMethodInsertPosition finds where to insert a method for a type
func findMethodInsertPosition(file *ast.File, typeDecl *ast.TypeSpec) token.Pos {
// First, try to find other methods for this type and insert after them
typeName := typeDecl.Name.Name
var lastMethodEnd token.Pos

// Look for existing methods
for _, decl := range file.Decls {
if funcDecl, ok := decl.(*ast.FuncDecl); ok && funcDecl.Recv != nil {
if len(funcDecl.Recv.List) > 0 {
recvType := funcDecl.Recv.List[0].Type

// Check if this is a method for our type
if ident, ok := recvType.(*ast.Ident); ok && ident.Name == typeName {
if funcDecl.End() > lastMethodEnd {
lastMethodEnd = funcDecl.End()
}
} else if star, ok := recvType.(*ast.StarExpr); ok {
if ident, ok := star.X.(*ast.Ident); ok && ident.Name == typeName {
if funcDecl.End() > lastMethodEnd {
lastMethodEnd = funcDecl.End()
}
}
}
}
}
}

if lastMethodEnd != 0 {
return lastMethodEnd
}

// No existing methods, insert after the type declaration
// Find the GenDecl containing the TypeSpec
for _, decl := range file.Decls {
if genDecl, ok := decl.(*ast.GenDecl); ok {
for _, spec := range genDecl.Specs {
if spec == typeDecl {
return genDecl.End()
}
}
}
}

// Fallback: insert at the end of the file
return file.End()
}
5 changes: 4 additions & 1 deletion cmd/mtlog-analyzer/analyzer/suggestedfix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ func TestSuggestedFixes(t *testing.T) {
name: "MTLOG001 - Template argument mismatch fixes",
dir: "suggestedfix/mtlog001",
},
{
name: "MTLOG005 - LogValue() stub generation",
dir: "suggestedfix/mtlog005",
},
{
name: "MTLOG006 - Missing error parameter fixes",
dir: "suggestedfix/mtlog006",
Expand All @@ -28,7 +32,6 @@ func TestSuggestedFixes(t *testing.T) {
},
// TODO: Add test data for other diagnostics with suggested fixes:
// - MTLOG004 (PascalCase properties)
// - MTLOG005 (Capturing hints)
}

for _, tt := range tests {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package mtlog005

import (
"time"

"github.com/willibrandon/mtlog"
)

// User demonstrates a struct with sensitive fields
type User struct {
ID int
Username string
Email string
Password string // Sensitive field
APIKey string // Sensitive field
LastLogin time.Time
}

// Order demonstrates a struct without sensitive fields
type Order struct {
ID int
CustomerID int
Total float64
Status string
CreatedAt time.Time
}

// Account demonstrates a struct with mixed fields
type Account struct {
AccountNumber string
Balance float64
Token string // Sensitive field
CreditCard string // Sensitive field
Owner string
}

func TestLogValueStubGeneration() {
log := mtlog.New()

// These should trigger MTLOG005 with LogValue() stub suggestion
user := User{
ID: 123,
Username: "alice",
Email: "alice@example.com",
Password: "secret123",
APIKey: "sk_live_abc123",
LastLogin: time.Now(),
}
log.Information("User logged in: {User}", user) // want `\[MTLOG005\] suggestion: consider using @ prefix for complex type.*User to enable capturing`

order := Order{
ID: 456,
CustomerID: 123,
Total: 99.99,
Status: "pending",
CreatedAt: time.Now(),
}
log.Information("Order created: {Order}", order) // want `\[MTLOG005\] suggestion: consider using @ prefix for complex type.*Order to enable capturing`

account := Account{
AccountNumber: "ACC-123456",
Balance: 1000.00,
Token: "bearer_token_xyz",
CreditCard: "4111111111111111",
Owner: "Alice Smith",
}
log.Information("Account details: {Account}", account) // want `\[MTLOG005\] suggestion: consider using @ prefix for complex type.*Account to enable capturing`

// Pointer types should also work
log.Information("User pointer: {UserPtr}", &user) // want `\[MTLOG005\] suggestion: consider using @ prefix for complex type.*User to enable capturing`
}
Loading
Loading