## 主要改进 ### 架构重构 - 将单体 provider.go 拆分为多个专门的模块文件 - 实现了清晰的职责分离和模块化设计 - 遵循 SOLID 原则,提高代码可维护性 ### 新增功能 - **验证规则系统**: 实现了完整的 provider 验证框架 - **报告生成器**: 支持多种格式的验证报告 (JSON/HTML/Markdown/Text) - **解析器优化**: 重新设计了解析流程,提高性能和可扩展性 - **错误处理**: 增强了错误处理和诊断能力 ### 修复关键 Bug - 修复 @provider(job) 注解缺失 __job 注入参数的问题 - 统一了 job 和 cronjob 模式的处理逻辑 - 确保了 provider 生成的正确性和一致性 ### 代码质量提升 - 添加了完整的测试套件 - 引入了 golangci-lint 代码质量检查 - 优化了代码格式和结构 - 增加了详细的文档和规范 ### 文件结构优化 ``` pkg/ast/provider/ ├── types.go # 类型定义 ├── parser.go # 解析器实现 ├── validator.go # 验证规则 ├── report_generator.go # 报告生成 ├── renderer.go # 渲染器 ├── comment_parser.go # 注解解析 ├── modes.go # 模式定义 ├── errors.go # 错误处理 └── validator_test.go # 测试文件 ``` ### 兼容性 - 保持向后兼容性 - 支持现有的所有 provider 模式 - 优化了 API 设计和用户体验 This completes the implementation of T025-T029 tasks following TDD principles, including validation rules implementation and critical bug fixes.
1207 lines
35 KiB
Go
1207 lines
35 KiB
Go
package provider
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
)
|
|
|
|
// Validator defines the interface for validating provider configurations
|
|
type Validator interface {
|
|
// Validate validates a provider configuration
|
|
Validate(provider *Provider) error
|
|
|
|
// ValidateComment validates a provider comment annotation
|
|
ValidateComment(comment string) error
|
|
|
|
// AddRule adds a validation rule to the validator
|
|
AddRule(rule ValidationRule)
|
|
|
|
// RemoveRule removes a validation rule from the validator
|
|
RemoveRule(name string)
|
|
|
|
// GetRules returns all validation rules
|
|
GetRules() []ValidationRule
|
|
|
|
// ValidateAll validates multiple providers and returns a comprehensive report
|
|
ValidateAll(providers []*Provider) *ValidationReport
|
|
}
|
|
|
|
// ValidationRule defines the interface for validation rules
|
|
type ValidationRule interface {
|
|
// Name returns the name of the validation rule
|
|
Name() string
|
|
|
|
// Validate validates a provider against this rule
|
|
Validate(provider *Provider) *ValidationError
|
|
|
|
// Description returns a human-readable description of the rule
|
|
Description() string
|
|
}
|
|
|
|
// ValidationError represents a validation error
|
|
type ValidationError struct {
|
|
RuleName string `json:"rule_name"`
|
|
Message string `json:"message"`
|
|
Field string `json:"field,omitempty"`
|
|
Value string `json:"value,omitempty"`
|
|
Severity string `json:"severity"` // "error", "warning", "info"
|
|
Suggestion string `json:"suggestion,omitempty"`
|
|
ProviderRef string `json:"provider_ref,omitempty"`
|
|
Cause error `json:"cause,omitempty"`
|
|
}
|
|
|
|
// Error implements the error interface
|
|
func (e *ValidationError) Error() string {
|
|
if e.ProviderRef != "" {
|
|
return fmt.Sprintf("[%s] %s: %s", e.ProviderRef, e.RuleName, e.Message)
|
|
}
|
|
return fmt.Sprintf("%s: %s", e.RuleName, e.Message)
|
|
}
|
|
|
|
// Unwrap implements the error unwrapping interface
|
|
func (e *ValidationError) Unwrap() error {
|
|
return e.Cause
|
|
}
|
|
|
|
// ValidationReport represents the result of validating multiple providers
|
|
type ValidationReport struct {
|
|
Timestamp time.Time `json:"timestamp"`
|
|
TotalProviders int `json:"total_providers"`
|
|
ValidCount int `json:"valid_count"`
|
|
InvalidCount int `json:"invalid_count"`
|
|
Errors []ValidationError `json:"errors"`
|
|
Warnings []ValidationError `json:"warnings"`
|
|
Infos []ValidationError `json:"infos"`
|
|
IsValid bool `json:"is_valid"`
|
|
Statistics *ValidationStatistics `json:"statistics,omitempty"`
|
|
}
|
|
|
|
// ValidationStatistics contains detailed statistics about validation results
|
|
type ValidationStatistics struct {
|
|
ProvidersByMode map[string]int `json:"providers_by_mode"`
|
|
ProvidersByStatus map[string]int `json:"providers_by_status"`
|
|
RuleViolations map[string]int `json:"rule_violations"`
|
|
ErrorByField map[string]int `json:"error_by_field"`
|
|
CommonErrors []ValidationErrorDetail `json:"common_errors"`
|
|
}
|
|
|
|
// ValidationErrorDetail represents a detailed validation error with count
|
|
type ValidationErrorDetail struct {
|
|
ErrorKey string `json:"error_key"`
|
|
Count int `json:"count"`
|
|
}
|
|
|
|
// GoValidator implements the Validator interface
|
|
type GoValidator struct {
|
|
rules []ValidationRule
|
|
}
|
|
|
|
// NewGoValidator creates a new GoValidator with default rules
|
|
func NewGoValidator() *GoValidator {
|
|
validator := &GoValidator{
|
|
rules: make([]ValidationRule, 0),
|
|
}
|
|
|
|
// Add default validation rules
|
|
validator.AddDefaultRules()
|
|
|
|
return validator
|
|
}
|
|
|
|
// Validate implements Validator.Validate
|
|
func (v *GoValidator) Validate(provider *Provider) error {
|
|
var errors []ValidationError
|
|
|
|
for _, rule := range v.rules {
|
|
if err := rule.Validate(provider); err != nil {
|
|
errors = append(errors, *err)
|
|
}
|
|
}
|
|
|
|
if len(errors) > 0 {
|
|
return &ValidationErrors{Errors: errors}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateComment implements Validator.ValidateComment
|
|
func (v *GoValidator) ValidateComment(comment string) error {
|
|
if !strings.HasPrefix(comment, "@provider") {
|
|
return &ValidationError{
|
|
RuleName: "CommentFormat",
|
|
Message: "provider comment must start with @provider",
|
|
Severity: "error",
|
|
}
|
|
}
|
|
|
|
// Parse the comment to check its structure
|
|
providerDoc := parseProvider(comment)
|
|
|
|
// Validate the parsed structure
|
|
if providerDoc.Mode != "" && !IsValidProviderMode(providerDoc.Mode) {
|
|
return &ValidationError{
|
|
RuleName: "ProviderMode",
|
|
Message: fmt.Sprintf("invalid provider mode: %s", providerDoc.Mode),
|
|
Severity: "error",
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AddRule implements Validator.AddRule
|
|
func (v *GoValidator) AddRule(rule ValidationRule) {
|
|
v.rules = append(v.rules, rule)
|
|
}
|
|
|
|
// RemoveRule implements Validator.RemoveRule
|
|
func (v *GoValidator) RemoveRule(name string) {
|
|
for i, rule := range v.rules {
|
|
if rule.Name() == name {
|
|
v.rules = append(v.rules[:i], v.rules[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// GetRules implements Validator.GetRules
|
|
func (v *GoValidator) GetRules() []ValidationRule {
|
|
return v.rules
|
|
}
|
|
|
|
// ValidateAll implements Validator.ValidateAll
|
|
func (v *GoValidator) ValidateAll(providers []*Provider) *ValidationReport {
|
|
report := &ValidationReport{
|
|
Timestamp: time.Now(),
|
|
TotalProviders: len(providers),
|
|
ValidCount: 0,
|
|
InvalidCount: 0,
|
|
Errors: make([]ValidationError, 0),
|
|
Warnings: make([]ValidationError, 0),
|
|
Infos: make([]ValidationError, 0),
|
|
}
|
|
|
|
for _, provider := range providers {
|
|
providerRef := fmt.Sprintf("%s:%s", provider.PkgName, provider.StructName)
|
|
|
|
providerValid := true
|
|
for _, rule := range v.rules {
|
|
if err := rule.Validate(provider); err != nil {
|
|
err.ProviderRef = providerRef
|
|
|
|
switch err.Severity {
|
|
case "error":
|
|
report.Errors = append(report.Errors, *err)
|
|
providerValid = false
|
|
case "warning":
|
|
report.Warnings = append(report.Warnings, *err)
|
|
case "info":
|
|
report.Infos = append(report.Infos, *err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if providerValid {
|
|
report.ValidCount++
|
|
} else {
|
|
report.InvalidCount++
|
|
}
|
|
}
|
|
|
|
report.IsValid = len(report.Errors) == 0
|
|
|
|
return report
|
|
}
|
|
|
|
// ValidateWithDetails validates all providers and returns a detailed report with statistics
|
|
func (v *GoValidator) ValidateWithDetails(providers []*Provider) *ValidationReport {
|
|
report := v.ValidateAll(providers)
|
|
|
|
// Add detailed statistics
|
|
stats := calculateValidationStatistics(report, providers)
|
|
report.Statistics = &stats
|
|
|
|
return report
|
|
}
|
|
|
|
// calculateValidationStatistics calculates detailed validation statistics
|
|
func calculateValidationStatistics(report *ValidationReport, providers []*Provider) ValidationStatistics {
|
|
stats := ValidationStatistics{
|
|
ProvidersByMode: make(map[string]int),
|
|
ProvidersByStatus: make(map[string]int),
|
|
RuleViolations: make(map[string]int),
|
|
ErrorByField: make(map[string]int),
|
|
CommonErrors: make([]ValidationErrorDetail, 0),
|
|
}
|
|
|
|
// Count providers by mode
|
|
for _, provider := range providers {
|
|
mode := string(provider.Mode)
|
|
stats.ProvidersByMode[mode]++
|
|
}
|
|
|
|
// Count violations by rule
|
|
allIssues := append(append(report.Errors, report.Warnings...), report.Infos...)
|
|
for _, issue := range allIssues {
|
|
stats.RuleViolations[issue.RuleName]++
|
|
stats.ErrorByField[issue.Field]++
|
|
}
|
|
|
|
// Find common errors
|
|
errorCounts := make(map[string]int)
|
|
for _, err := range report.Errors {
|
|
key := fmt.Sprintf("%s:%s", err.RuleName, err.Message)
|
|
errorCounts[key]++
|
|
}
|
|
|
|
// Get top 5 common errors
|
|
for key, count := range errorCounts {
|
|
stats.CommonErrors = append(stats.CommonErrors, ValidationErrorDetail{
|
|
ErrorKey: key,
|
|
Count: count,
|
|
})
|
|
}
|
|
|
|
// Sort by count (descending)
|
|
for i := 0; i < len(stats.CommonErrors)-1; i++ {
|
|
for j := i + 1; j < len(stats.CommonErrors); j++ {
|
|
if stats.CommonErrors[i].Count < stats.CommonErrors[j].Count {
|
|
stats.CommonErrors[i], stats.CommonErrors[j] = stats.CommonErrors[j], stats.CommonErrors[i]
|
|
}
|
|
}
|
|
}
|
|
|
|
// Keep only top 5
|
|
if len(stats.CommonErrors) > 5 {
|
|
stats.CommonErrors = stats.CommonErrors[:5]
|
|
}
|
|
|
|
return stats
|
|
}
|
|
|
|
// GenerateReport generates a formatted validation report
|
|
func (v *GoValidator) GenerateReport(providers []*Provider, format ReportFormat) (string, error) {
|
|
report := v.ValidateWithDetails(providers)
|
|
generator := NewReportGenerator(report)
|
|
return generator.GenerateReport(format)
|
|
}
|
|
|
|
// SaveReport saves a validation report to a file
|
|
func (v *GoValidator) SaveReport(providers []*Provider, filename string, format ReportFormat) error {
|
|
report := v.ValidateWithDetails(providers)
|
|
writer := NewReportWriter(report)
|
|
return writer.WriteToFile(filename, format)
|
|
}
|
|
|
|
// PrintReport prints a validation report to the console
|
|
func (v *GoValidator) PrintReport(providers []*Provider, format ReportFormat) error {
|
|
report := v.ValidateWithDetails(providers)
|
|
writer := NewReportWriter(report)
|
|
return writer.WriteToConsole(format)
|
|
}
|
|
|
|
// AddDefaultRules adds the default validation rules
|
|
func (v *GoValidator) AddDefaultRules() {
|
|
v.AddRule(&StructNameRule{})
|
|
v.AddRule(&ReturnTypeRule{})
|
|
v.AddRule(&ProviderModeRule{})
|
|
v.AddRule(&InjectionParamsRule{})
|
|
v.AddRule(&PackageAliasRule{})
|
|
}
|
|
|
|
// ValidationErrors represents multiple validation errors
|
|
type ValidationErrors struct {
|
|
Errors []ValidationError
|
|
}
|
|
|
|
// Error implements the error interface
|
|
func (e *ValidationErrors) Error() string {
|
|
if len(e.Errors) == 0 {
|
|
return "no validation errors"
|
|
}
|
|
|
|
if len(e.Errors) == 1 {
|
|
return e.Errors[0].Message
|
|
}
|
|
|
|
return fmt.Sprintf("%d validation errors occurred, first: %s", len(e.Errors), e.Errors[0].Message)
|
|
}
|
|
|
|
// StructNameRule validates struct names
|
|
type StructNameRule struct{}
|
|
|
|
func (r *StructNameRule) Name() string { return "StructName" }
|
|
|
|
func (r *StructNameRule) Description() string {
|
|
return "Validates that struct names follow Go naming conventions"
|
|
}
|
|
|
|
func (r *StructNameRule) Validate(provider *Provider) *ValidationError {
|
|
if provider.StructName == "" {
|
|
return &ValidationError{
|
|
RuleName: r.Name(),
|
|
Message: "struct name cannot be empty",
|
|
Field: "StructName",
|
|
Severity: "error",
|
|
}
|
|
}
|
|
|
|
if !isExportedName(provider.StructName) {
|
|
return &ValidationError{
|
|
RuleName: r.Name(),
|
|
Message: "struct name must be exported (start with uppercase letter)",
|
|
Field: "StructName",
|
|
Value: provider.StructName,
|
|
Severity: "error",
|
|
}
|
|
}
|
|
|
|
// Check for Go naming conventions
|
|
if !isValidGoIdentifier(provider.StructName) {
|
|
return &ValidationError{
|
|
RuleName: r.Name(),
|
|
Message: "struct name must be a valid Go identifier",
|
|
Field: "StructName",
|
|
Value: provider.StructName,
|
|
Severity: "error",
|
|
}
|
|
}
|
|
|
|
// Check for common naming conventions (CamelCase for structs)
|
|
if !isCamelCase(provider.StructName) {
|
|
return &ValidationError{
|
|
RuleName: r.Name(),
|
|
Message: "struct name should use CamelCase convention",
|
|
Field: "StructName",
|
|
Value: provider.StructName,
|
|
Severity: "warning",
|
|
Suggestion: "consider using CamelCase (e.g., UserService instead of userService or USER_SERVICE)",
|
|
}
|
|
}
|
|
|
|
// Check for reserved words or problematic names
|
|
if isReservedWord(provider.StructName) {
|
|
return &ValidationError{
|
|
RuleName: r.Name(),
|
|
Message: "struct name should not use reserved words or common Go types",
|
|
Field: "StructName",
|
|
Value: provider.StructName,
|
|
Severity: "warning",
|
|
Suggestion: "choose a more descriptive name that doesn't conflict with Go built-ins",
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ReturnTypeRule validates return types
|
|
type ReturnTypeRule struct{}
|
|
|
|
func (r *ReturnTypeRule) Name() string { return "ReturnType" }
|
|
|
|
func (r *ReturnTypeRule) Description() string {
|
|
return "Validates that return types are properly formatted and consistent"
|
|
}
|
|
|
|
func (r *ReturnTypeRule) Validate(provider *Provider) *ValidationError {
|
|
if provider.ReturnType == "" {
|
|
return &ValidationError{
|
|
RuleName: r.Name(),
|
|
Message: "return type cannot be empty",
|
|
Field: "ReturnType",
|
|
Severity: "error",
|
|
}
|
|
}
|
|
|
|
// Check if return type is a valid Go type identifier
|
|
if !isValidGoType(provider.ReturnType) {
|
|
return &ValidationError{
|
|
RuleName: r.Name(),
|
|
Message: fmt.Sprintf("invalid return type format: %s", provider.ReturnType),
|
|
Field: "ReturnType",
|
|
Value: provider.ReturnType,
|
|
Severity: "error",
|
|
}
|
|
}
|
|
|
|
// Check for mode-specific return type requirements
|
|
if err := validateModeSpecificReturnType(provider); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check for interface types (should generally be avoided for providers)
|
|
if strings.HasPrefix(provider.ReturnType, "*") &&
|
|
(strings.HasSuffix(provider.ReturnType, "Interface") ||
|
|
strings.HasSuffix(provider.ReturnType, "IFace")) {
|
|
return &ValidationError{
|
|
RuleName: r.Name(),
|
|
Message: "pointer to interface types should generally be avoided",
|
|
Field: "ReturnType",
|
|
Value: provider.ReturnType,
|
|
Severity: "warning",
|
|
Suggestion: "consider using the interface type directly or a concrete type",
|
|
}
|
|
}
|
|
|
|
// Check for overly complex generic types
|
|
if strings.Count(provider.ReturnType, "[") > 2 || strings.Count(provider.ReturnType, "]") > 2 {
|
|
return &ValidationError{
|
|
RuleName: r.Name(),
|
|
Message: "return type appears overly complex with too many generic parameters",
|
|
Field: "ReturnType",
|
|
Value: provider.ReturnType,
|
|
Severity: "warning",
|
|
Suggestion: "consider simplifying the type or using type aliases",
|
|
}
|
|
}
|
|
|
|
// Check for pointer vs value type consistency
|
|
if strings.HasPrefix(provider.ReturnType, "*") {
|
|
// Pointer type validations
|
|
pointedType := strings.TrimPrefix(provider.ReturnType, "*")
|
|
if !isExportedName(pointedType) {
|
|
return &ValidationError{
|
|
RuleName: r.Name(),
|
|
Message: "pointed type in pointer return type must be exported",
|
|
Field: "ReturnType",
|
|
Value: provider.ReturnType,
|
|
Severity: "error",
|
|
}
|
|
}
|
|
} else {
|
|
// Value type validations
|
|
if !isExportedName(provider.ReturnType) && !isBuiltInType(provider.ReturnType) {
|
|
return &ValidationError{
|
|
RuleName: r.Name(),
|
|
Message: "non-pointer return type must be either exported or a built-in type",
|
|
Field: "ReturnType",
|
|
Value: provider.ReturnType,
|
|
Severity: "error",
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateModeSpecificReturnType checks return type requirements for specific provider modes
|
|
func validateModeSpecificReturnType(provider *Provider) *ValidationError {
|
|
switch provider.Mode {
|
|
case ProviderModeGrpc:
|
|
// gRPC providers should typically return interface types
|
|
if !strings.HasSuffix(provider.ReturnType, "Client") &&
|
|
!strings.HasSuffix(provider.ReturnType, "Service") {
|
|
return &ValidationError{
|
|
RuleName: "ReturnType",
|
|
Message: "gRPC providers should typically return client or service interface types",
|
|
Field: "ReturnType",
|
|
Value: provider.ReturnType,
|
|
Severity: "warning",
|
|
Suggestion: "consider naming the return type ending with Client or Service",
|
|
}
|
|
}
|
|
|
|
case ProviderModeModel:
|
|
// Model providers should return struct types
|
|
if strings.Contains(provider.ReturnType, "interface") ||
|
|
strings.Contains(provider.ReturnType, "Interface") {
|
|
return &ValidationError{
|
|
RuleName: "ReturnType",
|
|
Message: "model providers should return concrete struct types, not interfaces",
|
|
Field: "ReturnType",
|
|
Value: provider.ReturnType,
|
|
Severity: "error",
|
|
}
|
|
}
|
|
|
|
case ProviderModeJob, ProviderModeCronJob:
|
|
// Job providers should return types that implement job interfaces
|
|
if !strings.Contains(provider.ReturnType, "Job") {
|
|
return &ValidationError{
|
|
RuleName: "ReturnType",
|
|
Message: "job providers should return types that implement job interfaces",
|
|
Field: "ReturnType",
|
|
Value: provider.ReturnType,
|
|
Severity: "warning",
|
|
Suggestion: "consider naming the return type to indicate it's a job handler",
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// isBuiltInType checks if a type is a Go built-in type
|
|
func isBuiltInType(typeStr string) bool {
|
|
builtInTypes := map[string]bool{
|
|
"string": true, "int": true, "int8": true, "int16": true, "int32": true, "int64": true,
|
|
"uint": true, "uint8": true, "uint16": true, "uint32": true, "uint64": true,
|
|
"float32": true, "float64": true, "complex64": true, "complex128": true,
|
|
"bool": true, "byte": true, "rune": true, "error": true,
|
|
}
|
|
|
|
return builtInTypes[typeStr]
|
|
}
|
|
|
|
// ProviderModeRule validates provider modes
|
|
type ProviderModeRule struct{}
|
|
|
|
func (r *ProviderModeRule) Name() string { return "ProviderMode" }
|
|
|
|
func (r *ProviderModeRule) Description() string {
|
|
return "Validates that provider modes are valid and appropriate for the provider configuration"
|
|
}
|
|
|
|
func (r *ProviderModeRule) Validate(provider *Provider) *ValidationError {
|
|
if !IsValidProviderMode(string(provider.Mode)) {
|
|
return &ValidationError{
|
|
RuleName: r.Name(),
|
|
Message: fmt.Sprintf("invalid provider mode: %s", provider.Mode),
|
|
Field: "Mode",
|
|
Value: string(provider.Mode),
|
|
Severity: "error",
|
|
}
|
|
}
|
|
|
|
// Validate mode-specific configurations
|
|
if err := validateModeSpecificConfiguration(provider); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check for deprecated or discouraged mode combinations
|
|
if err := validateModeCombinations(provider); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateModeSpecificConfiguration checks mode-specific configuration requirements
|
|
func validateModeSpecificConfiguration(provider *Provider) *ValidationError {
|
|
switch provider.Mode {
|
|
case ProviderModeGrpc:
|
|
// gRPC providers need gRPC register function
|
|
if provider.GrpcRegisterFunc == "" {
|
|
return &ValidationError{
|
|
RuleName: "ProviderMode",
|
|
Message: "gRPC providers must specify a gRPC register function",
|
|
Field: "GrpcRegisterFunc",
|
|
Value: provider.GrpcRegisterFunc,
|
|
Severity: "error",
|
|
Suggestion: "add gRPC register function to the provider annotation",
|
|
}
|
|
}
|
|
|
|
case ProviderModeJob, ProviderModeCronJob:
|
|
// Job providers should have prepare function
|
|
if !provider.NeedPrepareFunc {
|
|
return &ValidationError{
|
|
RuleName: "ProviderMode",
|
|
Message: "job providers should typically need a prepare function",
|
|
Field: "NeedPrepareFunc",
|
|
Value: fmt.Sprintf("%t", provider.NeedPrepareFunc),
|
|
Severity: "warning",
|
|
Suggestion: "consider setting prepare function for job initialization",
|
|
}
|
|
}
|
|
|
|
// Check if job provider has proper injection parameters
|
|
hasJobParam := false
|
|
for paramName := range provider.InjectParams {
|
|
if paramName == "__job" {
|
|
hasJobParam = true
|
|
break
|
|
}
|
|
}
|
|
if !hasJobParam {
|
|
return &ValidationError{
|
|
RuleName: "ProviderMode",
|
|
Message: "job providers should inject __job parameter",
|
|
Field: "InjectParams",
|
|
Severity: "warning",
|
|
Suggestion: "add __job *job.Job parameter to injection parameters",
|
|
}
|
|
}
|
|
|
|
case ProviderModeEvent:
|
|
// Event providers should have __event parameter
|
|
hasEventParam := false
|
|
for paramName := range provider.InjectParams {
|
|
if paramName == "__event" {
|
|
hasEventParam = true
|
|
break
|
|
}
|
|
}
|
|
if !hasEventParam {
|
|
return &ValidationError{
|
|
RuleName: "ProviderMode",
|
|
Message: "event providers should inject __event parameter",
|
|
Field: "InjectParams",
|
|
Severity: "warning",
|
|
Suggestion: "add __event parameter to injection parameters",
|
|
}
|
|
}
|
|
|
|
case ProviderModeBasic:
|
|
// Basic providers should not have special parameters
|
|
for paramName := range provider.InjectParams {
|
|
if paramName == "__job" || paramName == "__event" || paramName == "__grpc" {
|
|
return &ValidationError{
|
|
RuleName: "ProviderMode",
|
|
Message: "basic providers should not inject special parameters (__job, __event, __grpc)",
|
|
Field: "InjectParams",
|
|
Value: paramName,
|
|
Severity: "error",
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateModeCombinations checks for problematic mode combinations
|
|
func validateModeCombinations(provider *Provider) *ValidationError {
|
|
// Check for incompatible mode combinations
|
|
if provider.ProviderGroup != "" {
|
|
groupLower := strings.ToLower(provider.ProviderGroup)
|
|
modeStr := string(provider.Mode)
|
|
|
|
// Basic checks for common issues
|
|
if (modeStr == "grpc" && groupLower == "job") ||
|
|
(modeStr == "job" && groupLower == "grpc") {
|
|
return &ValidationError{
|
|
RuleName: "ProviderMode",
|
|
Message: "incompatible combination of provider mode and group",
|
|
Field: "ProviderGroup",
|
|
Value: provider.ProviderGroup,
|
|
Severity: "warning",
|
|
Suggestion: "ensure provider mode and group are compatible",
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for mode-specific naming suggestions
|
|
if err := validateModeNamingConventions(provider); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateModeNamingConventions provides naming suggestions for different modes
|
|
func validateModeNamingConventions(provider *Provider) *ValidationError {
|
|
structName := provider.StructName
|
|
|
|
switch provider.Mode {
|
|
case ProviderModeGrpc:
|
|
if !strings.HasSuffix(structName, "Client") &&
|
|
!strings.HasSuffix(structName, "Service") &&
|
|
!strings.HasSuffix(structName, "Provider") {
|
|
return &ValidationError{
|
|
RuleName: "ProviderMode",
|
|
Message: "gRPC provider struct names should typically end with Client, Service, or Provider",
|
|
Field: "StructName",
|
|
Value: structName,
|
|
Severity: "warning",
|
|
Suggestion: "consider naming the struct to indicate it's a gRPC provider",
|
|
}
|
|
}
|
|
|
|
case ProviderModeJob, ProviderModeCronJob:
|
|
if !strings.HasSuffix(structName, "Job") &&
|
|
!strings.HasSuffix(structName, "Worker") &&
|
|
!strings.HasSuffix(structName, "Handler") {
|
|
return &ValidationError{
|
|
RuleName: "ProviderMode",
|
|
Message: "job provider struct names should typically end with Job, Worker, or Handler",
|
|
Field: "StructName",
|
|
Value: structName,
|
|
Severity: "warning",
|
|
Suggestion: "consider naming the struct to indicate it's a job provider",
|
|
}
|
|
}
|
|
|
|
case ProviderModeEvent:
|
|
if !strings.HasSuffix(structName, "Handler") &&
|
|
!strings.HasSuffix(structName, "Listener") &&
|
|
!strings.HasSuffix(structName, "Subscriber") {
|
|
return &ValidationError{
|
|
RuleName: "ProviderMode",
|
|
Message: "event provider struct names should typically end with Handler, Listener, or Subscriber",
|
|
Field: "StructName",
|
|
Value: structName,
|
|
Severity: "warning",
|
|
Suggestion: "consider naming the struct to indicate it's an event provider",
|
|
}
|
|
}
|
|
|
|
case ProviderModeModel:
|
|
if !strings.HasSuffix(structName, "Model") &&
|
|
!strings.HasSuffix(structName, "Entity") &&
|
|
!strings.HasSuffix(structName, "Repo") &&
|
|
!strings.HasSuffix(structName, "Repository") {
|
|
return &ValidationError{
|
|
RuleName: "ProviderMode",
|
|
Message: "model provider struct names should typically end with Model, Entity, Repo, or Repository",
|
|
Field: "StructName",
|
|
Value: structName,
|
|
Severity: "warning",
|
|
Suggestion: "consider naming the struct to indicate it's a model provider",
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// InjectionParamsRule validates injection parameters
|
|
type InjectionParamsRule struct{}
|
|
|
|
func (r *InjectionParamsRule) Name() string { return "InjectionParams" }
|
|
|
|
func (r *InjectionParamsRule) Description() string {
|
|
return "Validates injection parameters for consistency and correctness"
|
|
}
|
|
|
|
func (r *InjectionParamsRule) Validate(provider *Provider) *ValidationError {
|
|
if len(provider.InjectParams) == 0 {
|
|
// Some providers might not need injection parameters
|
|
if provider.Mode == ProviderModeBasic {
|
|
return nil // Basic providers can have no injection params
|
|
}
|
|
|
|
return &ValidationError{
|
|
RuleName: r.Name(),
|
|
Message: "providers should have at least one injection parameter",
|
|
Field: "InjectParams",
|
|
Severity: "warning",
|
|
Suggestion: "consider adding injection parameters for dependency injection",
|
|
}
|
|
}
|
|
|
|
// Check for duplicate parameter names
|
|
paramNames := make(map[string]bool)
|
|
for paramName := range provider.InjectParams {
|
|
if paramNames[paramName] {
|
|
return &ValidationError{
|
|
RuleName: r.Name(),
|
|
Message: "duplicate injection parameter name",
|
|
Field: "InjectParams",
|
|
Value: paramName,
|
|
Severity: "error",
|
|
}
|
|
}
|
|
paramNames[paramName] = true
|
|
}
|
|
|
|
// Validate each injection parameter
|
|
for paramName, param := range provider.InjectParams {
|
|
if err := validateInjectionParameter(paramName, param, provider.Mode); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Check for parameter consistency
|
|
if err := validateParameterConsistency(provider); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateInjectionParameter validates a single injection parameter
|
|
func validateInjectionParameter(paramName string, param InjectParam, mode ProviderMode) *ValidationError {
|
|
// Validate parameter name
|
|
if paramName == "" {
|
|
return &ValidationError{
|
|
RuleName: "InjectionParams",
|
|
Message: "injection parameter name cannot be empty",
|
|
Field: "InjectParams",
|
|
Severity: "error",
|
|
}
|
|
}
|
|
|
|
// Special parameters are allowed to be unexported
|
|
if !isSpecialParameter(paramName) && !isExportedName(paramName) {
|
|
return &ValidationError{
|
|
RuleName: "InjectionParams",
|
|
Message: "injection parameter names must be exported",
|
|
Field: "InjectParams",
|
|
Value: paramName,
|
|
Severity: "warning",
|
|
Suggestion: "use exported names for injection parameters",
|
|
}
|
|
}
|
|
|
|
// Validate parameter type
|
|
if param.Type == "" {
|
|
return &ValidationError{
|
|
RuleName: "InjectionParams",
|
|
Message: "injection parameter type cannot be empty",
|
|
Field: "InjectParams",
|
|
Value: paramName,
|
|
Severity: "error",
|
|
}
|
|
}
|
|
|
|
// Validate type format
|
|
if !isValidGoType(param.Type) {
|
|
return &ValidationError{
|
|
RuleName: "InjectionParams",
|
|
Message: fmt.Sprintf("invalid injection parameter type format: %s", param.Type),
|
|
Field: "InjectParams",
|
|
Value: paramName,
|
|
Severity: "error",
|
|
}
|
|
}
|
|
|
|
// Validate package and alias consistency
|
|
if param.Package != "" && param.PackageAlias == "" {
|
|
return &ValidationError{
|
|
RuleName: "InjectionParams",
|
|
Message: "package alias is required when package is specified",
|
|
Field: "InjectParams",
|
|
Value: paramName,
|
|
Severity: "error",
|
|
}
|
|
}
|
|
|
|
if param.Package == "" && param.PackageAlias != "" {
|
|
return &ValidationError{
|
|
RuleName: "InjectionParams",
|
|
Message: "package cannot be empty when package alias is specified",
|
|
Field: "InjectParams",
|
|
Value: paramName,
|
|
Severity: "error",
|
|
}
|
|
}
|
|
|
|
// Validate special parameters
|
|
if isSpecialParameter(paramName) {
|
|
if err := validateSpecialParameter(paramName, param, mode); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateParameterConsistency checks for consistency across all parameters
|
|
func validateParameterConsistency(provider *Provider) *ValidationError {
|
|
// Check for conflicting parameter types
|
|
paramTypes := make(map[string]string)
|
|
for paramName, param := range provider.InjectParams {
|
|
// Check if same type is used with different aliases
|
|
if existingAlias, exists := paramTypes[param.Type]; exists {
|
|
// Extract package alias from existing parameter
|
|
existingParam := provider.InjectParams[existingAlias]
|
|
if existingParam.PackageAlias != param.PackageAlias {
|
|
return &ValidationError{
|
|
RuleName: "InjectionParams",
|
|
Message: "same type used with different package aliases",
|
|
Field: "InjectParams",
|
|
Value: fmt.Sprintf("%s (%s vs %s)", param.Type, existingParam.PackageAlias, param.PackageAlias),
|
|
Severity: "warning",
|
|
Suggestion: "use consistent package aliases for the same type",
|
|
}
|
|
}
|
|
}
|
|
paramTypes[param.Type] = paramName
|
|
}
|
|
|
|
// Check for circular dependencies (simplified check)
|
|
for paramName, param := range provider.InjectParams {
|
|
if param.Type == provider.StructName {
|
|
return &ValidationError{
|
|
RuleName: "InjectionParams",
|
|
Message: "provider cannot inject itself",
|
|
Field: "InjectParams",
|
|
Value: paramName,
|
|
Severity: "error",
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for mode-specific parameter requirements
|
|
if err := validateModeSpecificParameters(provider); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateModeSpecificParameters checks mode-specific parameter requirements
|
|
func validateModeSpecificParameters(provider *Provider) *ValidationError {
|
|
switch provider.Mode {
|
|
case ProviderModeJob, ProviderModeCronJob:
|
|
// Job providers should have __job parameter
|
|
if _, hasJobParam := provider.InjectParams["__job"]; !hasJobParam {
|
|
return &ValidationError{
|
|
RuleName: "InjectionParams",
|
|
Message: "job providers should inject __job parameter",
|
|
Field: "InjectParams",
|
|
Severity: "warning",
|
|
Suggestion: "add __job *job.Job parameter for job context",
|
|
}
|
|
}
|
|
|
|
case ProviderModeEvent:
|
|
// Event providers should have __event parameter
|
|
if _, hasEventParam := provider.InjectParams["__event"]; !hasEventParam {
|
|
return &ValidationError{
|
|
RuleName: "InjectionParams",
|
|
Message: "event providers should inject __event parameter",
|
|
Field: "InjectParams",
|
|
Severity: "warning",
|
|
Suggestion: "add __event parameter for event context",
|
|
}
|
|
}
|
|
|
|
case ProviderModeGrpc:
|
|
// gRPC providers should have __grpc parameter
|
|
if _, hasGrpcParam := provider.InjectParams["__grpc"]; !hasGrpcParam {
|
|
return &ValidationError{
|
|
RuleName: "InjectionParams",
|
|
Message: "gRPC providers should inject __grpc parameter",
|
|
Field: "InjectParams",
|
|
Severity: "warning",
|
|
Suggestion: "add __grpc parameter for gRPC context",
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateSpecialParameter validates special parameters like __job, __event, etc.
|
|
func validateSpecialParameter(paramName string, param InjectParam, mode ProviderMode) *ValidationError {
|
|
switch paramName {
|
|
case "__job":
|
|
if mode != ProviderModeJob && mode != ProviderModeCronJob {
|
|
return &ValidationError{
|
|
RuleName: "InjectionParams",
|
|
Message: "__job parameter should only be used in job providers",
|
|
Field: "InjectParams",
|
|
Value: paramName,
|
|
Severity: "error",
|
|
}
|
|
}
|
|
if param.Type != "*job.Job" {
|
|
return &ValidationError{
|
|
RuleName: "InjectionParams",
|
|
Message: "__job parameter should have type *job.Job",
|
|
Field: "InjectParams",
|
|
Value: param.Type,
|
|
Severity: "error",
|
|
}
|
|
}
|
|
|
|
case "__event":
|
|
if mode != ProviderModeEvent {
|
|
return &ValidationError{
|
|
RuleName: "InjectionParams",
|
|
Message: "__event parameter should only be used in event providers",
|
|
Field: "InjectParams",
|
|
Value: paramName,
|
|
Severity: "error",
|
|
}
|
|
}
|
|
if !strings.Contains(param.Type, "Event") {
|
|
return &ValidationError{
|
|
RuleName: "InjectionParams",
|
|
Message: "__event parameter should have an event type",
|
|
Field: "InjectParams",
|
|
Value: param.Type,
|
|
Severity: "warning",
|
|
Suggestion: "use a type that indicates it's an event",
|
|
}
|
|
}
|
|
|
|
case "__grpc":
|
|
if mode != ProviderModeGrpc {
|
|
return &ValidationError{
|
|
RuleName: "InjectionParams",
|
|
Message: "__grpc parameter should only be used in gRPC providers",
|
|
Field: "InjectParams",
|
|
Value: paramName,
|
|
Severity: "error",
|
|
}
|
|
}
|
|
if !strings.Contains(param.Type, "grpc") {
|
|
return &ValidationError{
|
|
RuleName: "InjectionParams",
|
|
Message: "__grpc parameter should have a gRPC-related type",
|
|
Field: "InjectParams",
|
|
Value: param.Type,
|
|
Severity: "warning",
|
|
Suggestion: "use a type that indicates it's gRPC-related",
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// isSpecialParameter checks if a parameter name is a special parameter
|
|
func isSpecialParameter(paramName string) bool {
|
|
specialParams := map[string]bool{
|
|
"__job": true,
|
|
"__event": true,
|
|
"__grpc": true,
|
|
}
|
|
return specialParams[paramName]
|
|
}
|
|
|
|
// PackageAliasRule validates package aliases
|
|
type PackageAliasRule struct{}
|
|
|
|
func (r *PackageAliasRule) Name() string { return "PackageAlias" }
|
|
|
|
func (r *PackageAliasRule) Description() string {
|
|
return "Validates package aliases for consistency"
|
|
}
|
|
|
|
func (r *PackageAliasRule) Validate(provider *Provider) *ValidationError {
|
|
for alias, path := range provider.Imports {
|
|
if alias == "" {
|
|
return &ValidationError{
|
|
RuleName: r.Name(),
|
|
Message: "package alias cannot be empty",
|
|
Field: "Imports",
|
|
Value: path,
|
|
Severity: "error",
|
|
}
|
|
}
|
|
|
|
if path == "" {
|
|
return &ValidationError{
|
|
RuleName: r.Name(),
|
|
Message: "package path cannot be empty",
|
|
Field: "Imports",
|
|
Value: alias,
|
|
Severity: "error",
|
|
}
|
|
}
|
|
|
|
if !isValidGoIdentifier(alias) {
|
|
return &ValidationError{
|
|
RuleName: r.Name(),
|
|
Message: "package alias must be a valid Go identifier",
|
|
Field: "Imports",
|
|
Value: alias,
|
|
Severity: "error",
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
func isExportedName(name string) bool {
|
|
if name == "" {
|
|
return false
|
|
}
|
|
return unicode.IsUpper(rune(name[0]))
|
|
}
|
|
|
|
func isValidGoType(typeStr string) bool {
|
|
// Enhanced validation for Go type identifiers including pointers
|
|
return regexp.MustCompile(`^(\*[a-zA-Z_][a-zA-Z0-9_.]*|[a-zA-Z_][a-zA-Z0-9_.]*(\[\])?|[a-zA-Z_][a-zA-Z0-9_.]*(\[\])?\*?)$`).MatchString(typeStr)
|
|
}
|
|
|
|
func isValidGoIdentifier(name string) bool {
|
|
if name == "" {
|
|
return false
|
|
}
|
|
|
|
// Check if it's a valid Go identifier
|
|
for i, r := range name {
|
|
if i == 0 && !unicode.IsLetter(r) && r != '_' {
|
|
return false
|
|
}
|
|
if !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '_' {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func isCamelCase(name string) bool {
|
|
if !isValidGoIdentifier(name) {
|
|
return false
|
|
}
|
|
|
|
// Check if first character is uppercase (exported)
|
|
if !unicode.IsUpper(rune(name[0])) {
|
|
return false
|
|
}
|
|
|
|
// Check for snake_case or kebab-case patterns
|
|
if strings.Contains(name, "_") || strings.Contains(name, "-") {
|
|
return false
|
|
}
|
|
|
|
// Check for ALL_CAPS (usually used for constants)
|
|
if strings.ToUpper(name) == name && len(name) > 1 {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func isReservedWord(name string) bool {
|
|
reservedWords := map[string]bool{
|
|
// Go keywords
|
|
"break": true, "case": true, "chan": true, "const": true, "continue": true,
|
|
"default": true, "defer": true, "else": true, "fallthrough": true, "for": true,
|
|
"func": true, "go": true, "goto": true, "if": true, "import": true,
|
|
"interface": true, "map": true, "package": true, "range": true, "return": true,
|
|
"select": true, "struct": true, "switch": true, "type": true, "var": true,
|
|
|
|
// Predeclared identifiers
|
|
"bool": true, "byte": true, "complex64": true, "complex128": true,
|
|
"error": true, "float32": true, "float64": true, "int": true, "int8": true,
|
|
"int16": true, "int32": true, "int64": true, "rune": true, "string": true,
|
|
"uint": true, "uint8": true, "uint16": true, "uint32": true, "uint64": true,
|
|
"uintptr": true, "true": true, "false": true, "nil": true, "iota": true,
|
|
|
|
// Common problematic names for providers
|
|
"Provider": true, "Service": true, "Handler": true, "Manager": true,
|
|
"Controller": true, "Repository": true, "Config": true, "Client": true,
|
|
}
|
|
|
|
return reservedWords[name]
|
|
}
|
|
|
|
func isValidPackageName(name string) bool {
|
|
if name == "" {
|
|
return false
|
|
}
|
|
|
|
// Package names should be lowercase, short, and descriptive
|
|
if !isValidGoIdentifier(name) {
|
|
return false
|
|
}
|
|
|
|
// Package names should typically be lowercase
|
|
if name != strings.ToLower(name) {
|
|
return false
|
|
}
|
|
|
|
// Avoid common problematic package names
|
|
problematicNames := map[string]bool{
|
|
"main": true, "testing": true, "fmt": true, "strings": true,
|
|
"container": true, "service": true, "utils": true, "common": true,
|
|
}
|
|
|
|
return !problematicNames[name]
|
|
}
|