Files
atomctl/pkg/ast/provider/import_resolver.go
Rogee e1f83ae469 feat: 重构 pkg/ast/provider 模块,优化代码组织逻辑和功能实现
## 主要改进

### 架构重构
- 将单体 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.
2025-09-19 18:58:30 +08:00

391 lines
11 KiB
Go

package provider
import (
"fmt"
"go/ast"
"math/rand"
"path/filepath"
"strings"
"go.ipao.vip/atomctl/v2/pkg/utils/gomod"
)
// ImportResolver handles resolution of Go imports and package aliases
type ImportResolver struct {
resolverConfig *ResolverConfig
cache map[string]*ImportResolution
}
// ResolverConfig configures the import resolver behavior
type ResolverConfig struct {
EnableCache bool
StrictMode bool
DefaultAliasStrategy AliasStrategy
AnonymousImportHandling AnonymousImportPolicy
}
// AliasStrategy defines how to generate default aliases
type AliasStrategy int
const (
AliasStrategyModuleName AliasStrategy = iota
AliasStrategyLastPath
AliasStrategyCustom
)
// AnonymousImportPolicy defines how to handle anonymous imports
type AnonymousImportPolicy int
const (
AnonymousImportSkip AnonymousImportPolicy = iota
AnonymousImportUseModuleName
AnonymousImportGenerateUnique
)
// ImportResolution represents a resolved import
type ImportResolution struct {
Path string // Import path
Alias string // Package alias
IsAnonymous bool // Is this an anonymous import (_)
IsValid bool // Is the import valid
Error string // Error message if invalid
PackageName string // Actual package name
Dependencies map[string]string // Dependencies of this import
}
// ImportContext maintains context for import resolution
type ImportContext struct {
FileImports map[string]*ImportResolution // Alias -> Resolution
ImportPaths map[string]string // Path -> Alias
ModuleInfo map[string]string // Module path -> module name
WorkingDir string // Current working directory
ModuleName string // Current module name
ProcessedFiles map[string]bool // Track processed files
}
// NewImportResolver creates a new ImportResolver
func NewImportResolver() *ImportResolver {
return &ImportResolver{
resolverConfig: &ResolverConfig{
EnableCache: true,
StrictMode: false,
DefaultAliasStrategy: AliasStrategyModuleName,
AnonymousImportHandling: AnonymousImportUseModuleName,
},
cache: make(map[string]*ImportResolution),
}
}
// NewImportResolverWithConfig creates a new ImportResolver with custom configuration
func NewImportResolverWithConfig(config *ResolverConfig) *ImportResolver {
if config == nil {
return NewImportResolver()
}
return &ImportResolver{
resolverConfig: config,
cache: make(map[string]*ImportResolution),
}
}
// ResolveFileImports resolves all imports for a given AST file
func (ir *ImportResolver) ResolveFileImports(file *ast.File, filePath string) (*ImportContext, error) {
context := &ImportContext{
FileImports: make(map[string]*ImportResolution),
ImportPaths: make(map[string]string),
ModuleInfo: make(map[string]string),
WorkingDir: filepath.Dir(filePath),
ProcessedFiles: make(map[string]bool),
}
// Resolve current module name
moduleName := gomod.GetModuleName()
context.ModuleName = moduleName
// Process imports
for _, imp := range file.Imports {
resolution, err := ir.resolveImportSpec(imp, context)
if err != nil {
if ir.resolverConfig.StrictMode {
return nil, err
}
// In non-strict mode, continue with other imports
continue
}
if resolution != nil {
context.FileImports[resolution.Alias] = resolution
context.ImportPaths[resolution.Path] = resolution.Alias
}
}
return context, nil
}
// resolveImportSpec resolves a single import specification
func (ir *ImportResolver) resolveImportSpec(imp *ast.ImportSpec, context *ImportContext) (*ImportResolution, error) {
// Extract import path
path := strings.Trim(imp.Path.Value, "\"")
if path == "" {
return nil, fmt.Errorf("empty import path")
}
// Check cache first
if ir.resolverConfig.EnableCache {
if cached, found := ir.cache[path]; found {
return cached, nil
}
}
// Determine alias
alias := ir.determineAlias(imp, path, context)
// Resolve package name
packageName, err := ir.resolvePackageName(path, context)
if err != nil {
resolution := &ImportResolution{
Path: path,
Alias: alias,
IsAnonymous: imp.Name != nil && imp.Name.Name == "_",
IsValid: false,
Error: err.Error(),
PackageName: "",
}
if ir.resolverConfig.EnableCache {
ir.cache[path] = resolution
}
return resolution, err
}
// Create resolution
resolution := &ImportResolution{
Path: path,
Alias: alias,
IsAnonymous: imp.Name != nil && imp.Name.Name == "_",
IsValid: true,
PackageName: packageName,
Dependencies: make(map[string]string),
}
// Resolve dependencies if needed
if err := ir.resolveDependencies(resolution, context); err != nil {
resolution.IsValid = false
resolution.Error = err.Error()
}
// Cache the result
if ir.resolverConfig.EnableCache {
ir.cache[path] = resolution
}
return resolution, nil
}
// determineAlias determines the appropriate alias for an import
func (ir *ImportResolver) determineAlias(imp *ast.ImportSpec, path string, context *ImportContext) string {
// If explicit alias is provided, use it
if imp.Name != nil {
if imp.Name.Name == "_" {
// Handle anonymous import based on policy
return ir.handleAnonymousImport(path, context)
}
return imp.Name.Name
}
// Generate default alias based on strategy
switch ir.resolverConfig.DefaultAliasStrategy {
case AliasStrategyModuleName:
return gomod.GetPackageModuleName(path)
case AliasStrategyLastPath:
return ir.getLastPathComponent(path)
case AliasStrategyCustom:
return ir.generateCustomAlias(path, context)
default:
return gomod.GetPackageModuleName(path)
}
}
// handleAnonymousImport handles anonymous imports based on policy
func (ir *ImportResolver) handleAnonymousImport(path string, context *ImportContext) string {
switch ir.resolverConfig.AnonymousImportHandling {
case AnonymousImportSkip:
return "_"
case AnonymousImportUseModuleName:
alias := gomod.GetPackageModuleName(path)
// Check for conflicts
if _, exists := context.FileImports[alias]; exists {
return ir.generateUniqueAlias(alias, context)
}
return alias
case AnonymousImportGenerateUnique:
baseAlias := gomod.GetPackageModuleName(path)
return ir.generateUniqueAlias(baseAlias, context)
default:
return "_"
}
}
// resolvePackageName resolves the actual package name for an import path
func (ir *ImportResolver) resolvePackageName(path string, context *ImportContext) (string, error) {
// Handle standard library packages
if !strings.Contains(path, ".") {
// For standard library, the package name is typically the last component
return ir.getLastPathComponent(path), nil
}
// Handle third-party packages
packageName := gomod.GetPackageModuleName(path)
if packageName == "" {
return "", fmt.Errorf("could not resolve package name for %s", path)
}
return packageName, nil
}
// resolveDependencies resolves dependencies for an import
func (ir *ImportResolver) resolveDependencies(resolution *ImportResolution, context *ImportContext) error {
// This is a placeholder for dependency resolution
// In a more sophisticated implementation, this could:
// - Parse the imported package to find its dependencies
// - Check for version conflicts
// - Validate import compatibility
// For now, we'll just note that third-party packages might have dependencies
if strings.Contains(resolution.Path, ".") {
// Add some common dependencies as examples
// This could be made configurable
}
return nil
}
// GetAlias returns the alias for a given import path
func (ir *ImportResolver) GetAlias(path string, context *ImportContext) (string, bool) {
alias, exists := context.ImportPaths[path]
return alias, exists
}
// GetPath returns the import path for a given alias
func (ir *ImportResolver) GetPath(alias string, context *ImportContext) (string, bool) {
if resolution, exists := context.FileImports[alias]; exists {
return resolution.Path, true
}
return "", false
}
// GetPackageName returns the package name for a given alias or path
func (ir *ImportResolver) GetPackageName(identifier string, context *ImportContext) (string, bool) {
// First try as alias
if resolution, exists := context.FileImports[identifier]; exists {
return resolution.PackageName, true
}
// Then try as path
if alias, exists := context.ImportPaths[identifier]; exists {
if resolution, resExists := context.FileImports[alias]; resExists {
return resolution.PackageName, true
}
}
return "", false
}
// IsValidImport checks if an import path is valid
func (ir *ImportResolver) IsValidImport(path string) bool {
// Basic validation
if path == "" {
return false
}
// Check for invalid characters
if strings.ContainsAny(path, " \t\n\r\"'") {
return false
}
// TODO: Add more sophisticated validation
return true
}
// GetImportPathFromType extracts the import path from a qualified type name
func (ir *ImportResolver) GetImportPathFromType(typeName string, context *ImportContext) (string, bool) {
if !strings.Contains(typeName, ".") {
return "", false
}
alias := strings.Split(typeName, ".")[0]
path, exists := ir.GetPath(alias, context)
return path, exists
}
// Helper methods
func (ir *ImportResolver) getLastPathComponent(path string) string {
parts := strings.Split(path, "/")
if len(parts) == 0 {
return ""
}
return parts[len(parts)-1]
}
func (ir *ImportResolver) generateCustomAlias(path string, context *ImportContext) string {
// Generate a meaningful alias based on the path
parts := strings.Split(path, "/")
if len(parts) == 0 {
return "unknown"
}
// Use the last few parts to create a meaningful alias
start := 0
if len(parts) > 2 {
start = len(parts) - 2
}
aliasParts := parts[start:]
for i, part := range aliasParts {
aliasParts[i] = strings.ToLower(part)
}
return strings.Join(aliasParts, "")
}
func (ir *ImportResolver) generateUniqueAlias(baseAlias string, context *ImportContext) string {
// Check if base alias is available
if _, exists := context.FileImports[baseAlias]; !exists {
return baseAlias
}
// Generate unique alias by adding suffix
for i := 1; i < 1000; i++ {
candidate := fmt.Sprintf("%s%d", baseAlias, i)
if _, exists := context.FileImports[candidate]; !exists {
return candidate
}
}
// Fallback to random suffix
return fmt.Sprintf("%s%d", baseAlias, rand.Intn(10000))
}
// ClearCache clears the import resolution cache
func (ir *ImportResolver) ClearCache() {
ir.cache = make(map[string]*ImportResolution)
}
// GetCacheSize returns the number of cached resolutions
func (ir *ImportResolver) GetCacheSize() int {
return len(ir.cache)
}
// GetConfig returns the resolver configuration
func (ir *ImportResolver) GetConfig() *ResolverConfig {
return ir.resolverConfig
}
// SetConfig updates the resolver configuration
func (ir *ImportResolver) SetConfig(config *ResolverConfig) {
ir.resolverConfig = config
// Clear cache when config changes
ir.ClearCache()
}