Skip to main content
Glama
grafana

Grafana

Official
by grafana
jsonschema_lint.go6.37 kB
package linter import ( "fmt" "go/ast" "go/parser" "go/token" "os" "path/filepath" "regexp" "sort" "strings" ) // JSONSchemaLinter checks for unescaped commas in jsonschema struct tags type JSONSchemaLinter struct { FilePaths []string Errors []JSONSchemaError FixMode bool Fixed map[string]bool } // JSONSchemaError represents a linting error with file position details type JSONSchemaError struct { FilePath string Line int Column int Offset int // Byte offset in the file Struct string Field string Tag string FixedTag string } // tagPattern matches jsonschema tags with description containing unescaped commas // It captures: // 1. The jsonschema tag // 2. Parts of the description containing unescaped commas // The pattern correctly handles: // - Simple unescaped comma: "description=Something, with comma" // - Escaped quote followed by unescaped comma: "description=With \"quote, and comma" // - But not match escaped comma: "description=With escaped\, comma" var tagPattern = regexp.MustCompile(`jsonschema:"([^"]*)description=(.*?[^\\],)([^"]*)"`) // FindUnescapedCommas scans Go files for jsonschema struct tags with unescaped commas in descriptions func (l *JSONSchemaLinter) FindUnescapedCommas(baseDir string) error { // Reset errors l.Errors = nil if l.FixMode { l.Fixed = make(map[string]bool) } // Walk through the directory and find Go files err := filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } // Skip non-Go files if !info.IsDir() && strings.HasSuffix(path, ".go") { l.FilePaths = append(l.FilePaths, path) } return nil }) if err != nil { return fmt.Errorf("error walking directory: %v", err) } // Parse all Go files and check for the unescaped commas for _, path := range l.FilePaths { fset := token.NewFileSet() f, err := parser.ParseFile(fset, path, nil, parser.ParseComments) if err != nil { return fmt.Errorf("error parsing file %s: %v", path, err) } fileErrors := []JSONSchemaError{} // Visit all struct types ast.Inspect(f, func(n ast.Node) bool { ts, ok := n.(*ast.TypeSpec) if !ok || ts.Type == nil { return true } st, ok := ts.Type.(*ast.StructType) if !ok { return true } structName := ts.Name.Name // Check each field of the struct for _, field := range st.Fields.List { if field.Tag == nil { continue } tag := field.Tag.Value // Check if the tag has a jsonschema description with unescaped comma matches := tagPattern.FindStringSubmatch(tag) if len(matches) > 0 { fieldName := "" if len(field.Names) > 0 { fieldName = field.Names[0].Name } // Generate the fixed tag by escaping the commas in the description fixedTag := tag if len(matches) > 2 { descWithUnescapedCommas := matches[2] // Escape all unescaped commas fixedDesc := escapeUnescapedCommas(descWithUnescapedCommas) // Replace the original description with the fixed one fixedTag = strings.Replace(tag, descWithUnescapedCommas, fixedDesc, 1) } pos := fset.Position(field.Tag.Pos()) errorInfo := JSONSchemaError{ FilePath: path, Line: pos.Line, Column: pos.Column, Offset: pos.Offset, Struct: structName, Field: fieldName, Tag: tag, FixedTag: fixedTag, } fileErrors = append(fileErrors, errorInfo) } } return true }) // Add all errors for this file l.Errors = append(l.Errors, fileErrors...) // If in fix mode and we found errors, fix the file if l.FixMode && len(fileErrors) > 0 { err := l.fixFile(path, fileErrors) if err != nil { return fmt.Errorf("error fixing file %s: %v", path, err) } l.Fixed[path] = true } } return nil } // escapeUnescapedCommas escapes any unescaped commas in the description func escapeUnescapedCommas(desc string) string { // Use regex to find all commas that are not preceded by a backslash r := regexp.MustCompile(`([^\\]),`) // Replace them with the same text but with an escaped comma return r.ReplaceAllString(desc, `$1\\,`) } // fixFile applies the fixes to a file func (l *JSONSchemaLinter) fixFile(path string, errors []JSONSchemaError) error { // Read the file content content, err := os.ReadFile(path) if err != nil { return fmt.Errorf("error reading file %s: %v", path, err) } // Convert to string for easier manipulation fileContent := string(content) // Sort errors by offset in reverse order to avoid offset changes sort.Slice(errors, func(i, j int) bool { return errors[i].Offset > errors[j].Offset }) // Apply fixes for _, e := range errors { // Find the tag in the file content tagStart := strings.Index(fileContent[e.Offset:], e.Tag) if tagStart == -1 { continue } absOffset := e.Offset + tagStart // Replace the tag with the fixed version fixedContent := fileContent[:absOffset] + e.FixedTag + fileContent[absOffset+len(e.Tag):] fileContent = fixedContent } // Write back to the file err = os.WriteFile(path, []byte(fileContent), 0644) if err != nil { return fmt.Errorf("error writing file %s: %v", path, err) } return nil } // PrintErrors outputs all the found errors func (l *JSONSchemaLinter) PrintErrors() { if len(l.Errors) == 0 { fmt.Println("No unescaped commas found in jsonschema descriptions.") return } if l.FixMode { fmt.Printf("Found and fixed %d unescaped commas in jsonschema descriptions:\n\n", len(l.Errors)) } else { fmt.Printf("Found %d unescaped commas in jsonschema descriptions:\n\n", len(l.Errors)) } for i, err := range l.Errors { relPath, _ := filepath.Rel(".", err.FilePath) fmt.Printf("%d. %s:%d:%d - Struct: %s, Field: %s\n", i+1, relPath, err.Line, err.Column, err.Struct, err.Field) fmt.Printf(" - %s\n", err.Tag) if l.FixMode { fmt.Printf(" - Fixed to: %s\n\n", err.FixedTag) } else { fmt.Printf(" - Commas in description must be escaped with \\\\,\n\n") } } if !l.FixMode { fmt.Println("Please escape all commas in jsonschema descriptions with \\\\, to prevent truncation.") fmt.Println("You can run with --fix to automatically fix these issues.") } else { fixedFileCount := len(l.Fixed) fmt.Printf("Fixed %d file(s).\n", fixedFileCount) } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/grafana/mcp-grafana'

If you have feedback or need assistance with the MCP directory API, please join our Discord server