diff --git a/CHANGELOG.md b/CHANGELOG.md
deleted file mode 100644
index c2d093b..0000000
--- a/CHANGELOG.md
+++ /dev/null
@@ -1,50 +0,0 @@
-# Gleece Changelog
-
-## Gleece v2.0.0
-
-### Summary
-
-*Gleece* 2 is a major milestone that includes a complete overhaul of the internal code analysis and validation facilities
-as well as a multitude of small bug fixes.
-
-These changes aim drastically improve performance and allow us to better expand and maintain the project and provide the groundwork for powerful and unique features down the road like live OAS preview, LSP support and more.
-
-For more information, please see the [architecture](https://docs.gleece.dev/docs/about/architecture) section of our documentation.
-
-### Features
-
-* Added a rich, LSP oriented diagnostics system. Issues will be reporter with far greater detail and clarity
-
-* Added many validation previously available only via the IDE extension
-
-* Added facilities necessary to generate full project dependency graphs (`SymbolGraph.ToDot`)
-
-* Created a `GleecePipeline` to orchestrate execution and lifecycle.
- This allows re-using caches and previous analysis results to expedite subsequent operations.
-
-* Added support for `byte` and `time.Time` fields in returned structs
-
-### Enhancements
-
-* Improved analysis speed by up to 50% via code optimization and introduction of package, file and node caches
-
-* Adjusted most processes to yield sorted results for more consistent builds results
-
-* Reduced import clutter in generated route files
-
-* Re-structured the project to provide a much clearer separation of concerns and allow for easier maintenance
-
-* Improved test coverage
-
-### Bugfixes
-* Fixed several cases of panic due to mis-configuration or invalid commands
-
-* Fixed cases where documentation was not properly siphoned from some types of entities
-
-* Fixed several issues with complex, nested type layers (*e.g*.map[string][][]int) resolution
-
-* Fixed several issues with complex type resolution
-
-* Fixed several issues with import detection resulting in resolution failures
-
-* Fixed an issue that could cause type information to be emitted with incorrect `PkgPath`
diff --git a/changelog/CHANGELOG.md b/changelog/CHANGELOG.md
new file mode 100644
index 0000000..7c87f6a
--- /dev/null
+++ b/changelog/CHANGELOG.md
@@ -0,0 +1,159 @@
+# Gleece Changelog
+
+## Gleece v2.1.0
+
+### Summary
+
+*Gleece* 2.1 brings in concrete support for type aliases, map input/outputs and an overhauled type resolution system.
+
+### Features
+
+* Support for type aliasing.
+Previously, alias support was limited and resulted in the aliased type effectively wiping the alias itself.
+
+ Both declared and assigned aliases are valid:
+
+ ```go
+ // A unique ID
+ type Id string `validate:"regex=^[0-9A-Fa-f]{8}(?:-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}$"`
+
+ // A unique ID
+ type Id = string `validate:"regex=^[0-9A-Fa-f]{8}(?:-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}$"`
+ ```
+
+ These aliases will be translated into an equivalent OpenAPI model:
+
+ ```yaml
+ Id:
+ type: "string"
+ format: "^[0-9A-Fa-f]{8}(?:-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}$"
+ description: "A unique ID"
+ ```
+
+
+* Full support for `map`.
+ Previously, non-primitive maps like `map[string]something.Something` would not resolve correctly, resulting errors.
+
+
+* Infrastructure-level support for generics
+ Generics support represent *Gleece*'s next major milestone.
+
+ This change includes the necessary infrastructure.
+ These include expansion of the HIR to encode the one-to-many relationships between composites (e.g. `Something[string, bool, int]`) and their instantiated dependencies (e.g. `string`, `bool` and `int`)
+
+ Below you can see a graphical dump of a controller with generics in use:
+
+ 
+
+
+* Added an configuration option to disable failure after encountering issues during package loads.
+
+ To use this option, create a `failOnAnyPackageLoadError` (`boolean`) field under the `commonConfig` section of your `Gleece Config` file, e.g.:
+
+ ```json
+ {
+ "commonConfig": {
+ "failOnAnyPackageLoadError": false,
+ "controllerGlobs": [
+ "./*.go",
+ "./**/*.go"
+ ]
+ },
+ ...
+ ...
+ ...
+ }
+ ```
+
+ For some complex environments, specifically those using generated code, there sometimes is a need to ignore package load errors.
+
+ A typical use case is when the project has controllers and generated code in the same directory hierarchy with no discernable naming patterns.
+
+ *Gleece* may attempt to load the yet-to-be-generated code and fail.
+
+ This configuration option is an 'escape hatch' for these cases.
+
+### Enhancements
+
+* Overhauled type resolution flows.
+
+ *Gleece* now has a set of composable visitors, each tailored for a specific part of the AST.
+
+* Added a global `ApiValidator` as the entry point to the HIR validation subsystem.
+ This currently includes a URL conflict detection algorithm to emit diagnostics upon route conflicts such as between `POST /a/b/c` and `POST /a/{foo}/c`
+
+* Added an explicit visitor error when using interfaces in general and `interface{}` in particular
+
+* Added a validation error when passing arrays via inputs that do not accept them (i.e., a URL parameter)
+
+
+### Bugfixes
+
+* Fixed array/slice support. Previously resulted in broken generated code
+
+* Fixed cases where model fields were named after the type rather than the field name or JSON tag
+
+* Fixed an issue where `any` yielded an `object` schema entity rather than a 'map-like' one, i.e.,
+```json
+ "someField": {
+ "additionalProperties": {
+ "type": "object"
+ },
+ "type": "object"
+}
+```
+
+------------------
+
+## Gleece v2.0.0
+
+### Summary
+
+*Gleece* 2 is a major milestone that includes a complete overhaul of the internal code analysis and validation facilities
+as well as a multitude of small bug fixes.
+
+These changes aim drastically improve performance and allow us to better expand and maintain the project and provide the groundwork for powerful and unique features down the road like live OAS preview, LSP support and more.
+
+For more information, please see the [architecture](https://docs.gleece.dev/docs/about/architecture) section of our documentation.
+
+### Features
+
+* Added a rich, LSP oriented diagnostics system. Issues will be reporter with far greater detail and clarity
+
+* Added many validation previously available only via the IDE extension
+
+* Added facilities necessary to generate full project dependency graphs (`SymbolGraph.ToDot`)
+
+* Created a `GleecePipeline` to orchestrate execution and lifecycle.
+ This allows re-using caches and previous analysis results to expedite subsequent operations.
+
+* Added support for `byte` and `time.Time` fields in returned structs
+
+### Enhancements
+
+* Improved analysis speed by up to 50% via code optimization and introduction of package, file and node caches
+
+* Adjusted most processes to yield sorted results for more consistent builds results
+
+* Reduced import clutter in generated route files
+
+* Re-structured the project to provide a much clearer separation of concerns and allow for easier maintenance
+
+* Improved test coverage
+
+### Bugfixes
+* Fixed several cases of panic due to mis-configuration or invalid commands
+
+* Fixed cases where documentation was not properly siphoned from some types of entities
+
+* Fixed several issues with complex, nested type layers (*e.g*.map[string][][]int) resolution
+
+* Fixed several issues with complex type resolution
+
+* Fixed several issues with import detection resulting in resolution failures
+
+* Fixed an issue that could cause type information to be emitted with incorrect `PkgPath`
+
+* Fix custom error causing OpenAPI 3.1 generation to fail
+
+* Fix Query parameter with array of primitive types not being generated correctly
diff --git a/changelog/images/2.1_generics_graph.svg b/changelog/images/2.1_generics_graph.svg
new file mode 100644
index 0000000..a0d0375
--- /dev/null
+++ b/changelog/images/2.1_generics_graph.svg
@@ -0,0 +1,284 @@
+
+
\ No newline at end of file
diff --git a/common/enumerations.go b/common/enumerations.go
index c2e16bc..9c8b4f6 100644
--- a/common/enumerations.go
+++ b/common/enumerations.go
@@ -9,6 +9,8 @@ const (
SymKindController SymKind = "Controller"
SymKindInterface SymKind = "Interface"
SymKindAlias SymKind = "Alias"
+ SymKindComposite SymKind = "Composite"
+ SymKindTypeParam SymKind = "TypeParam"
SymKindEnum SymKind = "Enum"
SymKindEnumValue SymKind = "EnumValue"
SymKindFunction SymKind = "Function"
@@ -40,3 +42,111 @@ const (
ImportTypeAlias ImportType = "Alias"
ImportTypeDot ImportType = "Dot"
)
+
+type PrimitiveType string
+
+const (
+ PrimitiveTypeBool PrimitiveType = "bool"
+ PrimitiveTypeString PrimitiveType = "string"
+
+ // Signed integers
+ PrimitiveTypeInt PrimitiveType = "int"
+ PrimitiveTypeInt8 PrimitiveType = "int8"
+ PrimitiveTypeInt16 PrimitiveType = "int16"
+ PrimitiveTypeInt32 PrimitiveType = "int32"
+ PrimitiveTypeInt64 PrimitiveType = "int64"
+
+ // Unsigned integers
+ PrimitiveTypeUint PrimitiveType = "uint"
+ PrimitiveTypeUint8 PrimitiveType = "uint8"
+ PrimitiveTypeUint16 PrimitiveType = "uint16"
+ PrimitiveTypeUint32 PrimitiveType = "uint32"
+ PrimitiveTypeUint64 PrimitiveType = "uint64"
+ PrimitiveTypeUintptr PrimitiveType = "uintptr"
+
+ // Aliases
+ PrimitiveTypeByte PrimitiveType = "byte" // alias for uint8
+ PrimitiveTypeRune PrimitiveType = "rune" // alias for int32
+
+ // Floating point numbers
+ PrimitiveTypeFloat32 PrimitiveType = "float32"
+ PrimitiveTypeFloat64 PrimitiveType = "float64"
+
+ // Complex numbers
+ PrimitiveTypeComplex64 PrimitiveType = "complex64"
+ PrimitiveTypeComplex128 PrimitiveType = "complex128"
+)
+
+// ToPrimitiveType checks if the given string represents a valid PrimitiveType.
+// If it does, it returns (corresponding PrimitiveType, true).
+func ToPrimitiveType(typeName string) (PrimitiveType, bool) {
+ switch typeName {
+ case
+ string(PrimitiveTypeBool),
+ string(PrimitiveTypeString),
+
+ string(PrimitiveTypeInt),
+ string(PrimitiveTypeInt8),
+ string(PrimitiveTypeInt16),
+ string(PrimitiveTypeInt32),
+ string(PrimitiveTypeInt64),
+
+ string(PrimitiveTypeUint),
+ string(PrimitiveTypeUint8),
+ string(PrimitiveTypeUint16),
+ string(PrimitiveTypeUint32),
+ string(PrimitiveTypeUint64),
+ string(PrimitiveTypeUintptr),
+
+ string(PrimitiveTypeByte),
+ string(PrimitiveTypeRune),
+
+ string(PrimitiveTypeFloat32),
+ string(PrimitiveTypeFloat64),
+
+ string(PrimitiveTypeComplex64),
+ string(PrimitiveTypeComplex128):
+ return PrimitiveType(typeName), true
+ default:
+ return "", false
+ }
+}
+
+type SpecialType string
+
+const (
+ SpecialTypeError SpecialType = "error"
+ SpecialTypeEmptyInterface SpecialType = "interface{}"
+ SpecialTypeContext SpecialType = "context.Context"
+ SpecialTypeTime SpecialType = "time.Time"
+ SpecialTypeAny SpecialType = "any" // alias of interface{}
+ SpecialTypeUnsafePointer SpecialType = "unsafe.Pointer"
+)
+
+func (s SpecialType) IsUniverse() bool {
+ switch s {
+ case SpecialTypeError, SpecialTypeEmptyInterface, SpecialTypeAny:
+ return true
+ }
+
+ return false
+}
+
+func ToSpecialType(s string) (SpecialType, bool) {
+ switch s {
+ case "error":
+ return SpecialTypeError, true
+ case "interface{}":
+ return SpecialTypeEmptyInterface, true
+ case "any":
+ return SpecialTypeAny, true
+ case "context.Context":
+ return SpecialTypeContext, true
+ case "time.Time":
+ return SpecialTypeTime, true
+ case "unsafe.Pointer":
+ return SpecialTypeUnsafePointer, true
+ default:
+ return "", false
+ }
+}
diff --git a/common/utils.go b/common/utils.go
index bc9a8c0..f68b626 100644
--- a/common/utils.go
+++ b/common/utils.go
@@ -6,3 +6,9 @@ func Ptr[T any](v T) *T {
return &v
}
+func Ternary[T any](condition bool, left, right T) T {
+ if condition {
+ return left
+ }
+ return right
+}
diff --git a/core/annotations/functional.go b/core/annotations/functional.go
index 9b778dd..78ce5c6 100644
--- a/core/annotations/functional.go
+++ b/core/annotations/functional.go
@@ -6,3 +6,11 @@ func GetDescription(holder *AnnotationHolder) string {
}
return holder.GetDescription()
}
+
+func GetTag(holder *AnnotationHolder) string {
+ if holder == nil {
+ return ""
+ }
+
+ return holder.GetFirstValueOrEmpty(GleeceAnnotationTag)
+}
diff --git a/core/arbitrators/ast.arbitrator.go b/core/arbitrators/ast.arbitrator.go
index 737998b..dbee56c 100644
--- a/core/arbitrators/ast.arbitrator.go
+++ b/core/arbitrators/ast.arbitrator.go
@@ -25,7 +25,7 @@ func NewAstArbitrator(pkgFacade *PackagesFacade) AstArbitrator {
}
func (arb *AstArbitrator) GetFuncParametersMeta(
- typeVisitor TypeVisitor,
+ typeVisitor FieldVisitor,
pkg *packages.Package,
file *ast.File,
funcDecl *ast.FuncDecl,
@@ -38,7 +38,7 @@ func (arb *AstArbitrator) GetFuncParametersMeta(
}
for paramOrdinal, field := range funcDecl.Type.Params.List {
- fields, err := typeVisitor.VisitField(pkg, file, field)
+ fields, err := typeVisitor.VisitField(pkg, file, field, common.SymKindParameter)
if err != nil {
return nil, err
}
@@ -67,7 +67,7 @@ func (arb *AstArbitrator) GetFuncParametersMeta(
}
func (arb *AstArbitrator) GetFuncRetValMeta(
- typeVisitor TypeVisitor,
+ fieldVisitor FieldVisitor,
pkg *packages.Package,
file *ast.File,
funcDecl *ast.FuncDecl,
@@ -80,7 +80,7 @@ func (arb *AstArbitrator) GetFuncRetValMeta(
}
for index, field := range funcDecl.Type.Results.List {
- fields, err := typeVisitor.VisitField(pkg, file, field)
+ fields, err := fieldVisitor.VisitField(pkg, file, field, common.SymKindReturnType)
if err != nil {
return nil, err
}
@@ -108,7 +108,7 @@ func (arb *AstArbitrator) GetFuncRetValMeta(
SymNodeMeta: metadata.SymNodeMeta{
Name: retValField.Name,
Node: retValField.Node,
- SymbolKind: common.SymKindParameter,
+ SymbolKind: common.SymKindReturnType,
PkgPath: retValField.PkgPath,
Annotations: funcAnnotations,
FVersion: retValField.FVersion,
@@ -124,50 +124,53 @@ func (arb *AstArbitrator) GetFuncRetValMeta(
return params, nil
}
+// GetImportType returns the import classification for an expression.
+// Policy summary:
+// - For Base[T] and Base[T1,...] -> return import type of Base.
+// - For pointer/slice/array/paren/chan -> return import type of the element/base.
+// - For map[K]V -> ImportTypeNone (K and V may differ).
+// - For selector -> ImportTypeAlias.
+// - For ident -> dot-import detection or ImportTypeNone.
+// - For func / struct (and other multi-origin/no-single-base) -> ImportTypeNone.
func (arb *AstArbitrator) GetImportType(file *ast.File, expr ast.Expr) (common.ImportType, error) {
switch e := expr.(type) {
-
case *ast.Ident:
- // Check for universe type first
if gast.IsUniverseType(e.Name) {
return common.ImportTypeNone, nil
}
-
- // Try to detect dot-import
- relevantPkg, err := arb.GetPackageFromDotImportedIdent(file, e)
+ pkg, err := arb.GetPackageFromDotImportedIdent(file, e)
if err != nil {
return common.ImportTypeNone, err
}
- if relevantPkg != nil {
+ if pkg != nil {
return common.ImportTypeDot, nil
}
-
- // If not dot, and not selector, assume it's local
return common.ImportTypeNone, nil
case *ast.SelectorExpr:
- // If it's a selector, assume it's an aliased import (either default or custom alias)
return common.ImportTypeAlias, nil
+ // single-operand composites: defer to operand's import type
case *ast.StarExpr:
- // Dereference and recurse
return arb.GetImportType(file, e.X)
-
+ case *ast.ParenExpr:
+ return arb.GetImportType(file, e.X)
case *ast.ArrayType:
return arb.GetImportType(file, e.Elt)
-
- case *ast.MapType:
- // IMPORTANT NOTE: Maps CAN have two separate import types! (Key & Value)
- // Currently, we're making a pretty ugly assumption that the key is a universe type and not an imported one!
- //
- // Return the import type of the value type
- return arb.GetImportType(file, e.Value)
-
+ case *ast.IndexExpr:
+ // Base[Index] -> base determines import
+ return arb.GetImportType(file, e.X)
+ case *ast.IndexListExpr:
+ return arb.GetImportType(file, e.X)
case *ast.ChanType:
return arb.GetImportType(file, e.Value)
+ // Maps, funcs and structs are always 'not imported' - map is a universe type and Func/StructTypes are local declarations
+ case *ast.MapType, *ast.FuncType, *ast.StructType:
+ return common.ImportTypeNone, nil
+
default:
- return common.ImportTypeNone, fmt.Errorf("unsupported expression type: %T", expr)
+ return common.ImportTypeNone, fmt.Errorf("unsupported expression type for import detection: %T", expr)
}
}
diff --git a/core/arbitrators/caching/metadata.cache.go b/core/arbitrators/caching/metadata.cache.go
index e756577..b73b614 100644
--- a/core/arbitrators/caching/metadata.cache.go
+++ b/core/arbitrators/caching/metadata.cache.go
@@ -15,9 +15,11 @@ type MetadataCache struct {
receivers map[graphs.SymbolKey]*metadata.ReceiverMeta
structs map[graphs.SymbolKey]*metadata.StructMeta
enums map[graphs.SymbolKey]*metadata.EnumMeta
+ aliases map[graphs.SymbolKey]*metadata.AliasMeta
visited map[graphs.SymbolKey]struct{}
+ inProgress map[graphs.SymbolKey]struct{}
- FileVersions map[*ast.File]*gast.FileVersion
+ fileVersions map[*ast.File]*gast.FileVersion
}
func NewMetadataCache() *MetadataCache {
@@ -26,8 +28,10 @@ func NewMetadataCache() *MetadataCache {
receivers: make(map[graphs.SymbolKey]*metadata.ReceiverMeta),
structs: make(map[graphs.SymbolKey]*metadata.StructMeta),
enums: make(map[graphs.SymbolKey]*metadata.EnumMeta),
+ aliases: make(map[graphs.SymbolKey]*metadata.AliasMeta),
visited: make(map[graphs.SymbolKey]struct{}),
- FileVersions: map[*ast.File]*gast.FileVersion{},
+ inProgress: make(map[graphs.SymbolKey]struct{}),
+ fileVersions: map[*ast.File]*gast.FileVersion{},
}
}
@@ -43,6 +47,10 @@ func (c *MetadataCache) GetEnum(key graphs.SymbolKey) *metadata.EnumMeta {
return c.enums[key]
}
+func (c *MetadataCache) GetAlias(key graphs.SymbolKey) *metadata.AliasMeta {
+ return c.aliases[key]
+}
+
func (c *MetadataCache) HasController(meta *metadata.ControllerMeta) bool {
key := graphs.NewSymbolKey(meta.Struct.Node, meta.Struct.FVersion)
return c.controllers[key] != nil
@@ -60,13 +68,17 @@ func (c *MetadataCache) HasEnum(key graphs.SymbolKey) bool {
return c.enums[key] != nil
}
+func (c *MetadataCache) HasAlias(key graphs.SymbolKey) bool {
+ return c.aliases[key] != nil
+}
+
func (c *MetadataCache) HasVisited(key graphs.SymbolKey) bool {
_, ok := c.visited[key]
return ok
}
func (c *MetadataCache) GetFileVersion(file *ast.File, fileSet *token.FileSet) (*gast.FileVersion, error) {
- fVersion := c.FileVersions[file]
+ fVersion := c.fileVersions[file]
if fVersion != nil {
return fVersion, nil
}
@@ -76,7 +88,7 @@ func (c *MetadataCache) GetFileVersion(file *ast.File, fileSet *token.FileSet) (
return nil, err
}
- c.FileVersions[file] = &version
+ c.fileVersions[file] = &version
return &version, nil
}
@@ -101,6 +113,32 @@ func (c *MetadataCache) AddEnum(meta *metadata.EnumMeta) error {
return addEntity(key, c.enums, c.visited, meta)
}
+func (c *MetadataCache) AddAlias(meta *metadata.AliasMeta) error {
+ key := graphs.NewSymbolKey(meta.Node, meta.FVersion)
+ return addEntity(key, c.aliases, c.visited, meta)
+}
+
+// StartMaterializing claims the key for materialization.
+// Returns true if the caller should proceed (not visited, not already in-progress).
+func (c *MetadataCache) StartMaterializing(key graphs.SymbolKey) bool {
+ if _, seen := c.visited[key]; seen {
+ return false // already done
+ }
+ if _, doing := c.inProgress[key]; doing {
+ return false // someone else is already working on it
+ }
+ c.inProgress[key] = struct{}{}
+ return true
+}
+
+// FinishMaterializing clears in-progress. If success==true, also mark visited.
+func (c *MetadataCache) FinishMaterializing(key graphs.SymbolKey, success bool) {
+ delete(c.inProgress, key)
+ if success {
+ c.visited[key] = struct{}{}
+ }
+}
+
func addEntity[TMeta any](
key graphs.SymbolKey,
cache map[graphs.SymbolKey]*TMeta,
diff --git a/core/arbitrators/configs.go b/core/arbitrators/configs.go
new file mode 100644
index 0000000..cd12148
--- /dev/null
+++ b/core/arbitrators/configs.go
@@ -0,0 +1,6 @@
+package arbitrators
+
+type PackageFacadeConfig struct {
+ Globs []string `json:"globs"`
+ AllowPackageLoadFailures bool `json:"allowPackageLoadFailures"`
+}
diff --git a/core/arbitrators/packages.facade.go b/core/arbitrators/packages.facade.go
index 0181b6c..d51c93c 100644
--- a/core/arbitrators/packages.facade.go
+++ b/core/arbitrators/packages.facade.go
@@ -15,6 +15,8 @@ import (
)
type PackagesFacade struct {
+ config PackageFacadeConfig
+
fileSet *token.FileSet
files map[string]*ast.File // filename → *ast.File
fileToPackage map[string]*packages.Package // filename → owning *packages.Package
@@ -24,8 +26,9 @@ type PackagesFacade struct {
}
-func NewPackagesFacade(globs []string) (PackagesFacade, error) {
+func NewPackagesFacade(config PackageFacadeConfig) (PackagesFacade, error) {
facade := PackagesFacade{
+ config: config,
fileSet: token.NewFileSet(),
files: make(map[string]*ast.File),
@@ -35,7 +38,7 @@ func NewPackagesFacade(globs []string) (PackagesFacade, error) {
packageToFiles: map[string][]*ast.File{},
}
- err := facade.initWithGlobs(globs)
+ err := facade.initWithGlobs()
return facade, err
}
@@ -51,7 +54,7 @@ func (facade *PackagesFacade) GetAllSourceFiles() []*ast.File {
return result
}
-func (facade *PackagesFacade) initWithGlobs(globs []string) error {
+func (facade *PackagesFacade) initWithGlobs() error {
pkgPathsToLoad := MapSet.NewSet[string]()
@@ -65,7 +68,7 @@ func (facade *PackagesFacade) initWithGlobs(globs []string) error {
matchedAbsPaths := map[string]struct{}{}
// For each glob expression (provided via gleece.config), parse all matching files
- for _, globExpr := range globs {
+ for _, globExpr := range facade.config.Globs {
globbedSources, err := doublestar.FilepathGlob(globExpr)
if err != nil {
return err
@@ -172,21 +175,38 @@ func (facade *PackagesFacade) loadAndCacheExpressions(
}
var pkgErrs []packages.Error
+ var failedPackageNames []string
+
failedPkgCount := 0
for _, p := range matchingPackages {
if len(p.Errors) > 0 {
failedPkgCount++
pkgErrs = append(pkgErrs, p.Errors...)
+ failedPackageNames = append(failedPackageNames, p.Name)
}
}
if len(pkgErrs) > 0 {
- return nil, fmt.Errorf(
- "encountered %d errors over %d packages during load - %v",
- len(pkgErrs),
- failedPkgCount,
+ failMessage := fmt.Sprintf(
+ "Failed to fully load the following packages: %v. Reported errors: %v",
+ failedPackageNames,
pkgErrs,
)
+
+ if facade.config.AllowPackageLoadFailures {
+ logger.Warn(failMessage)
+ } else {
+ skipMessage := "To ignore package load failures, set 'commonConfig->allowPackageLoadFailures' to 'true' in the gleece config file."
+ logger.Fatal(failMessage + skipMessage)
+ return nil, fmt.Errorf(
+ "encountered %d errors over %d package/s (%v) during load - %v. %s",
+ len(pkgErrs),
+ failedPkgCount,
+ failedPackageNames,
+ pkgErrs,
+ skipMessage,
+ )
+ }
}
// Note that packages.Load does *not* guarantee order
diff --git a/core/arbitrators/visitor.go b/core/arbitrators/visitor.go
index a88b4f5..a299ee2 100644
--- a/core/arbitrators/visitor.go
+++ b/core/arbitrators/visitor.go
@@ -3,17 +3,16 @@ package arbitrators
import (
"go/ast"
+ "github.com/gopher-fleece/gleece/common"
"github.com/gopher-fleece/gleece/core/metadata"
- "github.com/gopher-fleece/gleece/graphs"
"golang.org/x/tools/go/packages"
)
-type TypeVisitor interface {
- Visit(node ast.Node) ast.Visitor
- VisitStructType(
+type FieldVisitor interface {
+ VisitField(
+ pkg *packages.Package,
file *ast.File,
- nodeGenDecl *ast.GenDecl,
- node *ast.TypeSpec,
- ) (metadata.StructMeta, graphs.SymbolKey, error)
- VisitField(pkg *packages.Package, file *ast.File, field *ast.Field) ([]metadata.FieldMeta, error)
+ field *ast.Field,
+ kind common.SymKind,
+ ) ([]metadata.FieldMeta, error)
}
diff --git a/core/metadata/alias.go b/core/metadata/alias.go
new file mode 100644
index 0000000..b67f70f
--- /dev/null
+++ b/core/metadata/alias.go
@@ -0,0 +1,31 @@
+package metadata
+
+import (
+ "github.com/gopher-fleece/gleece/core/annotations"
+ "github.com/gopher-fleece/gleece/definitions"
+)
+
+type AliasKind int
+
+const (
+ AliasKindTypedef AliasKind = iota
+ AliasKindAssigned
+)
+
+type AliasMeta struct {
+ SymNodeMeta
+ AliasType AliasKind
+ Type TypeUsageMeta
+}
+
+// Reduce converts the HIR representation of an Alias (type A = string or type A string)
+// into a StructMetadata that can be used by the spec emitters to create OAS alias models
+func (m AliasMeta) Reduce(_ ReductionContext) (definitions.NakedAliasMetadata, error) {
+ return definitions.NakedAliasMetadata{
+ Name: m.Name,
+ PkgPath: m.PkgPath,
+ Description: annotations.GetDescription(m.Annotations),
+ Type: m.Type.Name,
+ Deprecation: GetDeprecationOpts(m.Annotations),
+ }, nil
+}
diff --git a/core/metadata/common.go b/core/metadata/common.go
new file mode 100644
index 0000000..f30c27d
--- /dev/null
+++ b/core/metadata/common.go
@@ -0,0 +1,47 @@
+package metadata
+
+import (
+ "go/ast"
+ "go/token"
+
+ "github.com/gopher-fleece/gleece/definitions"
+ "github.com/gopher-fleece/gleece/gast"
+ "github.com/gopher-fleece/gleece/graphs"
+)
+
+type IdProvider interface {
+ GetIdForKey(key graphs.SymbolKey) uint64
+}
+
+// Go's insane package system forces us to get... creative.
+type MetaCache interface {
+ GetStruct(key graphs.SymbolKey) *StructMeta
+ GetReceiver(key graphs.SymbolKey) *ReceiverMeta
+ GetEnum(key graphs.SymbolKey) *EnumMeta
+ GetAlias(key graphs.SymbolKey) *AliasMeta
+ HasController(meta *ControllerMeta) bool
+ HasReceiver(key graphs.SymbolKey) bool
+ HasStruct(key graphs.SymbolKey) bool
+ HasEnum(key graphs.SymbolKey) bool
+ HasAlias(key graphs.SymbolKey) bool
+ HasVisited(key graphs.SymbolKey) bool
+ GetFileVersion(file *ast.File, fileSet *token.FileSet) (*gast.FileVersion, error)
+ AddController(meta *ControllerMeta) error
+ AddReceiver(meta *ReceiverMeta) error
+ AddStruct(meta *StructMeta) error
+ AddEnum(meta *EnumMeta) error
+ AddAlias(meta *AliasMeta) error
+
+ // StartMaterializing claims the key for materialization.
+ // Returns true if the caller should proceed (not visited, not already in-progress).
+ StartMaterializing(key graphs.SymbolKey) bool
+
+ // FinishMaterializing clears in-progress. If success==true, also mark visited.
+ FinishMaterializing(key graphs.SymbolKey, success bool)
+}
+
+type ReductionContext struct {
+ GleeceConfig *definitions.GleeceConfig
+ MetaCache MetaCache
+ SyncedProvider IdProvider
+}
diff --git a/core/metadata/composite.go b/core/metadata/composite.go
new file mode 100644
index 0000000..3952eb7
--- /dev/null
+++ b/core/metadata/composite.go
@@ -0,0 +1,10 @@
+package metadata
+
+import "github.com/gopher-fleece/gleece/graphs"
+
+// CompositeMeta holds minimal metadata for composite-type nodes.
+// Stored in SymbolNode.Data when creating composite nodes.
+type CompositeMeta struct {
+ Canonical string
+ Operands []graphs.SymbolKey
+}
diff --git a/core/metadata/const.go b/core/metadata/const.go
new file mode 100644
index 0000000..b916aa9
--- /dev/null
+++ b/core/metadata/const.go
@@ -0,0 +1,7 @@
+package metadata
+
+type ConstMeta struct {
+ SymNodeMeta
+ Value any
+ Type TypeUsageMeta
+}
diff --git a/core/metadata/controller.go b/core/metadata/controller.go
new file mode 100644
index 0000000..b820ee4
--- /dev/null
+++ b/core/metadata/controller.go
@@ -0,0 +1,50 @@
+package metadata
+
+import (
+ "github.com/gopher-fleece/gleece/core/annotations"
+ "github.com/gopher-fleece/gleece/definitions"
+ "github.com/gopher-fleece/gleece/infrastructure/logger"
+)
+
+type ControllerMeta struct {
+ Struct StructMeta
+ Receivers []ReceiverMeta
+}
+
+func (m ControllerMeta) Reduce(ctx ReductionContext) (definitions.ControllerMetadata, error) {
+ // Parse any explicit Security annotations
+ security, err := GetSecurityFromContext(m.Struct.Annotations)
+ if err != nil {
+ return definitions.ControllerMetadata{}, err
+ }
+
+ // If there are no explicitly defined securities, check for inherited ones
+ if len(security) <= 0 {
+ logger.Debug("Controller %s does not have explicit security; Using user-defined defaults", m.Struct.Name)
+ security = GetDefaultSecurity(ctx.GleeceConfig)
+ }
+
+ var reducedReceivers []definitions.RouteMetadata
+ for _, rec := range m.Receivers {
+ reduced, err := rec.Reduce(ctx, security)
+ if err != nil {
+ logger.Error("Failed to reduce receiver '%s' of controller '%s' - %w", rec.Name, m.Struct.Name, err)
+ return definitions.ControllerMetadata{}, err
+ }
+ reducedReceivers = append(reducedReceivers, reduced)
+ }
+
+ meta := definitions.ControllerMetadata{
+ Name: m.Struct.Name,
+ PkgPath: m.Struct.PkgPath,
+ Tag: annotations.GetTag(m.Struct.Annotations),
+ Description: m.Struct.Annotations.GetDescription(),
+ RestMetadata: definitions.RestMetadata{
+ Path: m.Struct.Annotations.GetFirstValueOrEmpty(annotations.GleeceAnnotationRoute),
+ },
+ Routes: reducedReceivers,
+ Security: security,
+ }
+
+ return meta, nil
+}
diff --git a/core/metadata/enum.go b/core/metadata/enum.go
new file mode 100644
index 0000000..bd5955f
--- /dev/null
+++ b/core/metadata/enum.go
@@ -0,0 +1,94 @@
+package metadata
+
+import (
+ "fmt"
+ "go/types"
+
+ "github.com/gopher-fleece/gleece/common/linq"
+ "github.com/gopher-fleece/gleece/core/annotations"
+ "github.com/gopher-fleece/gleece/definitions"
+)
+
+type EnumValueKind string
+
+const (
+ EnumValueKindString EnumValueKind = "string"
+ EnumValueKindInt EnumValueKind = "int"
+ EnumValueKindInt8 EnumValueKind = "int8"
+ EnumValueKindInt16 EnumValueKind = "int16"
+ EnumValueKindInt32 EnumValueKind = "int32"
+ EnumValueKindInt64 EnumValueKind = "int64"
+ EnumValueKindUInt EnumValueKind = "uint"
+ EnumValueKindUInt8 EnumValueKind = "uint8"
+ EnumValueKindUInt16 EnumValueKind = "uint16"
+ EnumValueKindUInt32 EnumValueKind = "uint32"
+ EnumValueKindUInt64 EnumValueKind = "uint64"
+ EnumValueKindFloat32 EnumValueKind = "float32"
+ EnumValueKindFloat64 EnumValueKind = "float64"
+ EnumValueKindBool EnumValueKind = "bool"
+)
+
+func NewEnumValueKind(kind types.BasicKind) (EnumValueKind, error) {
+ switch kind {
+ case types.String:
+ return EnumValueKindString, nil
+ case types.Int:
+ return EnumValueKindInt, nil
+ case types.Int8:
+ return EnumValueKindInt8, nil
+ case types.Int16:
+ return EnumValueKindInt16, nil
+ case types.Int32:
+ return EnumValueKindInt32, nil
+ case types.Int64:
+ return EnumValueKindInt64, nil
+ case types.Uint:
+ return EnumValueKindUInt, nil
+ case types.Uint8:
+ return EnumValueKindUInt8, nil
+ case types.Uint16:
+ return EnumValueKindUInt16, nil
+ case types.Uint32:
+ return EnumValueKindUInt32, nil
+ case types.Uint64:
+ return EnumValueKindUInt64, nil
+ case types.Float32:
+ return EnumValueKindFloat32, nil
+ case types.Float64:
+ return EnumValueKindFloat64, nil
+ case types.Bool:
+ return EnumValueKindBool, nil
+ default:
+ return "", fmt.Errorf("unsupported basic kind: %v", kind)
+ }
+}
+
+type EnumValueDefinition struct {
+ // The enum's value definition node meta, e.g. EnumValueA SomeEnumType = "Abc"
+ SymNodeMeta
+ Value any // e.g. ["Meter", "Kilometer"]
+ // TODO: An exact textual representation of the value. For example "1 << 2"
+ //RawLiteralValue string
+}
+
+type EnumMeta struct {
+ // The enum's type definition's node meta e.g. type SomeEnumType string
+ SymNodeMeta
+ ValueKind EnumValueKind // e.g. string, int, etc.
+ Values []EnumValueDefinition
+}
+
+func (e EnumMeta) Reduce(_ ReductionContext) (definitions.EnumMetadata, error) {
+ stringifiedValues := linq.Map(e.Values, func(value EnumValueDefinition) string {
+ return fmt.Sprintf("%v", value.Value)
+ })
+
+ return definitions.EnumMetadata{
+ Name: e.Name,
+ PkgPath: e.PkgPath,
+ Description: annotations.GetDescription(e.Annotations),
+ Values: stringifiedValues,
+ Type: string(e.ValueKind),
+ Deprecation: GetDeprecationOpts(e.Annotations),
+ }, nil
+}
diff --git a/core/metadata/field.go b/core/metadata/field.go
new file mode 100644
index 0000000..dfcef0f
--- /dev/null
+++ b/core/metadata/field.go
@@ -0,0 +1,48 @@
+package metadata
+
+import (
+ "fmt"
+ "go/ast"
+ "strings"
+
+ "github.com/gopher-fleece/gleece/common"
+ "github.com/gopher-fleece/gleece/core/annotations"
+ "github.com/gopher-fleece/gleece/definitions"
+ "github.com/gopher-fleece/gleece/gast"
+)
+
+type FieldMeta struct {
+ SymNodeMeta
+ Type TypeUsageMeta
+ IsEmbedded bool
+}
+
+func (f FieldMeta) Reduce(_ ReductionContext) (definitions.FieldMetadata, error) {
+ fieldNode, ok := f.Node.(*ast.Field)
+ if !ok {
+ return definitions.FieldMetadata{}, fmt.Errorf("field '%s' has a non-field node type", f.Name)
+ }
+
+ var tag string
+ if fieldNode != nil && fieldNode.Tag != nil {
+ tag = strings.Trim(fieldNode.Tag.Value, "`")
+ }
+
+ return definitions.FieldMetadata{
+ Name: f.Name,
+ Type: f.Type.Root.SimpleTypeString(),
+ Description: annotations.GetDescription(f.Annotations),
+ Tag: tag,
+ IsEmbedded: f.IsEmbedded,
+ Deprecation: common.Ptr(GetDeprecationOpts(f.Annotations)),
+ }, nil
+}
+
+func (m TypeUsageMeta) IsUniverseType() bool {
+ return gast.IsUniverseType(m.Name)
+}
+
+func (m TypeUsageMeta) IsByAddress() bool {
+ _, isStar := m.Node.(*ast.StarExpr)
+ return isStar
+}
diff --git a/core/metadata/generics.go b/core/metadata/generics.go
new file mode 100644
index 0000000..8b5c714
--- /dev/null
+++ b/core/metadata/generics.go
@@ -0,0 +1,47 @@
+package metadata
+
+import (
+ "github.com/gopher-fleece/gleece/gast"
+ "github.com/gopher-fleece/gleece/graphs"
+)
+
+type TypeRefKind string
+
+const (
+ TypeRefKindNamed TypeRefKind = "named"
+ TypeRefKindParam TypeRefKind = "param"
+ TypeRefKindPtr TypeRefKind = "ptr"
+ TypeRefKindSlice TypeRefKind = "slice"
+ TypeRefKindArray TypeRefKind = "array"
+ TypeRefKindMap TypeRefKind = "map"
+ TypeRefKindFunc TypeRefKind = "func"
+ TypeRefKindInlineStruct TypeRefKind = "inline_struct"
+)
+
+type TypeRef interface {
+ Kind() TypeRefKind
+ // Deterministic structural representation used for interning/canonicalization.
+ CanonicalString() string
+
+ // SimpleTypeString returns a simplified representation of the reference.
+ // An example could be as follows:
+ // A MapTypeRef has a string key and an imported value module.SomeStruct.
+ // Simple string will return a 'schema-compatible' type string map[string]SomeStruct
+ //
+ // This can be used to feed the spec generator as it does not require any package/language level information
+ SimpleTypeString() string
+
+ // A key to be used for metadata cache lookups.
+ // Usually the same as the SimpleTypeString
+ CacheLookupKey(fileVersion *gast.FileVersion) (graphs.SymbolKey, error)
+
+ ToSymKey(fileVersion *gast.FileVersion) (graphs.SymbolKey, error)
+ Flatten() []TypeRef
+}
+
+// TypeParamDecl is a minimal declaration-side record for a type parameter.
+type TypeParamDecl struct {
+ Name string // "T" - original name (optional, but helpful for debugging)
+ Index int // 0-based index in the declaration; prefer populating this
+ Constraint TypeRef // optional constraint (interface, union, etc.) - nil == any
+}
diff --git a/core/metadata/param.go b/core/metadata/param.go
new file mode 100644
index 0000000..ff2fe72
--- /dev/null
+++ b/core/metadata/param.go
@@ -0,0 +1,75 @@
+package metadata
+
+import (
+ "fmt"
+
+ "github.com/gopher-fleece/gleece/definitions"
+)
+
+type FuncParam struct {
+ SymNodeMeta
+ Ordinal int
+ Type TypeUsageMeta
+}
+
+func (v FuncParam) Reduce(ctx ReductionContext) (definitions.FuncParam, error) {
+ typeMeta, err := v.Type.Reduce(ctx)
+ if err != nil {
+ return definitions.FuncParam{}, err
+ }
+
+ var nameInSchema string
+ var passedIn definitions.ParamPassedIn
+ var validator string
+
+ isContext := v.Type.IsContext()
+
+ if !isContext {
+ nameInSchema, err = GetParameterSchemaName(v.Name, v.Annotations)
+ if err != nil {
+ return definitions.FuncParam{}, err
+ }
+
+ passedIn, err = GetParamPassedIn(v.Name, v.Annotations)
+ if err != nil {
+ return definitions.FuncParam{}, err
+ }
+
+ validator, err = GetParamValidator(v.Name, v.Annotations, passedIn, v.Type.IsByAddress())
+ if err != nil {
+ return definitions.FuncParam{}, err
+ }
+ }
+
+ // Find the parameter's attribute in the receiver's annotations
+ var paramDescription string
+ paramAttrib := v.Annotations.FindFirstByValue(v.Name)
+ if paramAttrib != nil {
+ // Note that nil here is not valid and should be rejected at the validation stage
+ paramDescription = paramAttrib.Description
+ }
+
+ symKey, err := v.Type.Root.CacheLookupKey(v.FVersion)
+ if err != nil {
+ return definitions.FuncParam{}, fmt.Errorf(
+ "failed to derive symbol key for function parameter '%s' - %v",
+ v.Name,
+ err,
+ )
+ }
+
+ return definitions.FuncParam{
+ ParamMeta: definitions.ParamMeta{
+ Name: v.Name,
+ Ordinal: v.Ordinal,
+ TypeMeta: typeMeta,
+ IsContext: isContext,
+ },
+ PassedIn: passedIn,
+ NameInSchema: nameInSchema,
+ Description: paramDescription,
+ UniqueImportSerial: ctx.SyncedProvider.GetIdForKey(symKey),
+ Validator: validator,
+ Deprecation: GetDeprecationOpts(v.Annotations),
+ }, nil
+}
diff --git a/core/metadata/receiver.go b/core/metadata/receiver.go
new file mode 100644
index 0000000..957db13
--- /dev/null
+++ b/core/metadata/receiver.go
@@ -0,0 +1,119 @@
+package metadata
+
+import (
+ "cmp"
+ "fmt"
+ "slices"
+
+ "github.com/gopher-fleece/gleece/common"
+ "github.com/gopher-fleece/gleece/core/annotations"
+ "github.com/gopher-fleece/gleece/definitions"
+)
+
+type ReceiverMeta struct {
+ SymNodeMeta
+ Params []FuncParam
+ RetVals []FuncReturnValue
+}
+
+func (m ReceiverMeta) Reduce(
+ ctx ReductionContext,
+ parentSecurity []definitions.RouteSecurity,
+) (definitions.RouteMetadata, error) {
+
+ verbAnnotation := m.Annotations.GetFirst(annotations.GleeceAnnotationMethod)
+ if verbAnnotation == nil || verbAnnotation.Value == "" {
+ // Not ideal- we'd like to separate visitation, reduction and validation but typing currently doesn't
+ // cleanly allow it so we have to embed a bit of validation in a few other places as well
+ return definitions.RouteMetadata{}, fmt.Errorf("receiver %s has not @Method annotation", m.Name)
+ }
+
+ security, err := GetRouteSecurityWithInheritance(m.Annotations, parentSecurity)
+ if err != nil {
+ return definitions.RouteMetadata{}, err
+ }
+
+ templateCtx, err := GetTemplateContextMetadata(m.Annotations)
+ if err != nil {
+ return definitions.RouteMetadata{}, err
+ }
+
+ hasReturnValue := len(m.RetVals) > 1
+
+ responses := []definitions.FuncReturnValue{}
+ for _, fRetVal := range m.RetVals {
+ response, err := fRetVal.Reduce(ctx)
+ if err != nil {
+ return definitions.RouteMetadata{}, err
+ }
+ responses = append(responses, response)
+ }
+
+ reducedParams := []definitions.FuncParam{}
+ for _, param := range m.Params {
+ reducedParam, err := param.Reduce(ctx)
+ if err != nil {
+ return definitions.RouteMetadata{}, err
+ }
+ reducedParams = append(reducedParams, reducedParam)
+ }
+
+ successResponseCode, successResponseDescription, err := GetResponseStatusCodeAndDescription(m.Annotations, hasReturnValue)
+ if err != nil {
+ return definitions.RouteMetadata{}, err
+ }
+
+ errorResponses, err := GetErrorResponses(m.Annotations)
+ if err != nil {
+ return definitions.RouteMetadata{}, err
+ }
+
+ return definitions.RouteMetadata{
+ OperationId: m.Name,
+ HttpVerb: definitions.HttpVerb(verbAnnotation.Value),
+ Hiding: GetMethodHideOpts(m.Annotations),
+ Deprecation: GetDeprecationOpts(m.Annotations),
+ Description: m.Annotations.GetDescription(),
+ RestMetadata: definitions.RestMetadata{
+ Path: m.Annotations.GetFirstValueOrEmpty(annotations.GleeceAnnotationRoute),
+ },
+ HasReturnValue: hasReturnValue,
+ RequestContentType: definitions.ContentTypeJSON, // Hardcoded for now, should be supported via annotations later on
+ ResponseContentType: definitions.ContentTypeJSON, // Hardcoded for now, should be supported via annotations later on
+ Security: security,
+ TemplateContext: templateCtx,
+ ResponseSuccessCode: successResponseCode,
+ ResponseDescription: successResponseDescription,
+ FuncParams: reducedParams,
+ Responses: responses,
+ ErrorResponses: errorResponses,
+ }, nil
+}
+
+func (v ReceiverMeta) RetValsRange() common.ResolvedRange {
+ switch len(v.RetVals) {
+ case 0:
+ return common.ResolvedRange{}
+ case 1:
+ return common.ResolvedRange{
+ StartLine: v.RetVals[0].Range.StartLine,
+ EndLine: v.RetVals[0].Range.EndLine,
+ StartCol: v.RetVals[0].Range.StartCol,
+ EndCol: v.RetVals[0].Range.EndCol,
+ }
+ default:
+ // Copy so as not to affect original order
+ retVals := append([]FuncReturnValue{}, v.RetVals...)
+ slices.SortFunc(retVals, func(valA, valB FuncReturnValue) int {
+ return cmp.Compare(valA.Ordinal, valB.Ordinal)
+ })
+
+ lastIndex := len(retVals) - 1
+ return common.ResolvedRange{
+ StartLine: retVals[0].Range.StartLine,
+ EndLine: retVals[lastIndex].Range.EndLine,
+ StartCol: retVals[0].Range.StartCol,
+ EndCol: retVals[lastIndex].Range.EndCol,
+ }
+ }
+}
diff --git a/core/metadata/retval.go b/core/metadata/retval.go
new file mode 100644
index 0000000..e431496
--- /dev/null
+++ b/core/metadata/retval.go
@@ -0,0 +1,33 @@
+package metadata
+
+import (
+ "fmt"
+
+ "github.com/gopher-fleece/gleece/definitions"
+)
+
+type FuncReturnValue struct {
+ SymNodeMeta
+ Ordinal int
+ Type TypeUsageMeta
+}
+
+func (v FuncReturnValue) Reduce(ctx ReductionContext) (definitions.FuncReturnValue, error) {
+ typeMeta, err := v.Type.Reduce(ctx)
+ if err != nil {
+ return definitions.FuncReturnValue{}, err
+ }
+
+ // This cannot actually error, for now - v.Type.Resolve(ctx) uses the same CacheLookupKey
+ // so if the above call succeeds, this one will too
+ symKey, err := v.Type.Root.CacheLookupKey(v.FVersion)
+ if err != nil {
+ return definitions.FuncReturnValue{}, fmt.Errorf("failed to derive a symbol key for field '%s' - %v", v.Name, err)
+ }
+
+ return definitions.FuncReturnValue{
+ Ordinal: v.Ordinal,
+ UniqueImportSerial: ctx.SyncedProvider.GetIdForKey(symKey),
+ TypeMetadata: typeMeta,
+ }, nil
+}
diff --git a/core/metadata/struct.go b/core/metadata/struct.go
new file mode 100644
index 0000000..346560b
--- /dev/null
+++ b/core/metadata/struct.go
@@ -0,0 +1,33 @@
+package metadata
+
+import (
+ "fmt"
+
+ "github.com/gopher-fleece/gleece/core/annotations"
+ "github.com/gopher-fleece/gleece/definitions"
+)
+
+type StructMeta struct {
+ SymNodeMeta
+ TypeParams []TypeParamDecl
+ Fields []FieldMeta
+}
+
+func (s StructMeta) Reduce(ctx ReductionContext) (definitions.StructMetadata, error) {
+ reducedFields := make([]definitions.FieldMetadata, len(s.Fields))
+ for idx, field := range s.Fields {
+ reduced, err := field.Reduce(ctx)
+ if err != nil {
+ return definitions.StructMetadata{}, fmt.Errorf("failed to reduce field '%s' - %v", field.Name, err)
+ }
+ reducedFields[idx] = reduced
+ }
+
+ return definitions.StructMetadata{
+ Name: s.Name,
+ PkgPath: s.PkgPath,
+ Description: annotations.GetDescription(s.Annotations),
+ Fields: reducedFields,
+ Deprecation: GetDeprecationOpts(s.Annotations),
+ }, nil
+}
diff --git a/core/metadata/sym.node.go b/core/metadata/sym.node.go
new file mode 100644
index 0000000..20ae47d
--- /dev/null
+++ b/core/metadata/sym.node.go
@@ -0,0 +1,33 @@
+package metadata
+
+import (
+ "go/ast"
+
+ "github.com/gopher-fleece/gleece/common"
+ "github.com/gopher-fleece/gleece/core/annotations"
+ "github.com/gopher-fleece/gleece/gast"
+)
+
+// A symbol's general properties
+//
+// This struct serves as the HIR's backbone and the basis for nearly all symbols; They all have a name, an associated AST node/s, a kind and so forth.
+type SymNodeMeta struct {
+ // The symbol's name.
+ // May be empty for anonymous fields like return types.
+ Name string
+ // The AST node associated with this symbol. Common examples are Fields, Structs, Funcs and so forth.
+ Node ast.Node
+ SymbolKind common.SymKind
+ // The package path in which this symbol resides
+ PkgPath string
+ // Any Gleece annotations decorating this symbol
+ Annotations *annotations.AnnotationHolder
+ // The node's resolved range in the file.
+ Range common.ResolvedRange
+
+ // The FileVersion for the file in which this symbol is located.
+ //
+ // Currently, this field is only partially used (though fully populated).
+ // Its purpose is mostly to allow tracking file changes to perform graph prunes and incremental rebuilds.
+ FVersion *gast.FileVersion
+}
diff --git a/core/metadata/type.layers.go b/core/metadata/type.layers.go
deleted file mode 100644
index 3c73230..0000000
--- a/core/metadata/type.layers.go
+++ /dev/null
@@ -1,55 +0,0 @@
-package metadata
-
-import (
- "github.com/gopher-fleece/gleece/graphs"
-)
-
-// TypeLayerKind represents the kind of syntactic type layer.
-type TypeLayerKind string
-
-const (
- TypeLayerKindPointer TypeLayerKind = "pointer"
- TypeLayerKindArray TypeLayerKind = "array"
- TypeLayerKindMap TypeLayerKind = "map"
- TypeLayerKindBase TypeLayerKind = "base"
-)
-
-// TypeLayer represents one syntactic layer of a type.
-// Types are modeled from outermost to innermost.
-type TypeLayer struct {
- Kind TypeLayerKind
-
- // For map types:
- KeyType *graphs.SymbolKey
- ValueType *graphs.SymbolKey
-
- // For base types:
- BaseTypeRef *graphs.SymbolKey // Yo, what if this is a map?
-}
-
-// NewPointerLayer creates a pointer type layer (*T).
-func NewPointerLayer() TypeLayer {
- return TypeLayer{Kind: TypeLayerKindPointer}
-}
-
-// NewArrayLayer creates an array/slice type layer ([]T).
-func NewArrayLayer() TypeLayer {
- return TypeLayer{Kind: TypeLayerKindArray}
-}
-
-// NewMapLayer creates a map type layer (map[K]V).
-func NewMapLayer(key, value *graphs.SymbolKey) TypeLayer {
- return TypeLayer{
- Kind: TypeLayerKindMap,
- KeyType: key,
- ValueType: value,
- }
-}
-
-// NewBaseLayer creates the base type layer (e.g., a struct, interface, primitive, etc).
-func NewBaseLayer(base *graphs.SymbolKey) TypeLayer {
- return TypeLayer{
- Kind: TypeLayerKindBase,
- BaseTypeRef: base,
- }
-}
diff --git a/core/metadata/type.param.go b/core/metadata/type.param.go
new file mode 100644
index 0000000..9cc01da
--- /dev/null
+++ b/core/metadata/type.param.go
@@ -0,0 +1,23 @@
+package metadata
+
+// Represents a generic parameter declaration
+// e.g.,
+//
+// TValue
+//
+// in
+//
+// SomeStruct[TValue]
+type TypeParamDeclMeta struct {
+ // The parameter's name, as determined by the symbol key, e.g.,
+ // "typeparam:TValue#0"
+ Name string
+
+ // The parameter's index at the declaration,
+ // Example - In
+ //
+ // SomeStruct[TA, TB]
+ //
+ // TA has index 0 and TB has index 1
+ Index int
+}
diff --git a/core/metadata/type.usage.go b/core/metadata/type.usage.go
new file mode 100644
index 0000000..84b97c0
--- /dev/null
+++ b/core/metadata/type.usage.go
@@ -0,0 +1,121 @@
+package metadata
+
+import (
+ "fmt"
+
+ "github.com/gopher-fleece/gleece/common"
+ "github.com/gopher-fleece/gleece/core/annotations"
+ "github.com/gopher-fleece/gleece/definitions"
+ "github.com/gopher-fleece/gleece/gast"
+ "github.com/gopher-fleece/gleece/graphs"
+)
+
+// Represents a type usage site's metadata.
+//
+// Example:
+// The type usage for:
+//
+// m := map[string]int
+//
+// is
+//
+// map[string]int
+//
+// Metadata includes the usage's location in code, how it's imported and any modifiers like pointers/slices.
+//
+// This structure is part of the HIR in that it's linked to the source AST but also also serves as a high level abstraction that can be reduced
+// to a flatter, simpler IR that's used by the downstream generators to emit routing and schema code.
+type TypeUsageMeta struct {
+ SymNodeMeta
+ Import common.ImportType
+ // Describes the actual type reference.
+ // Recursively encodes information such as 'is this a pointer type?' or 'what generic parameters does this usage have?'
+ Root TypeRef
+}
+
+// Reduce returns the IR for the type usage.
+// This IR is a simpler and flatter TypeMetadata that can be used by downstream generators to emit the final code.
+func (t TypeUsageMeta) Reduce(ctx ReductionContext) (definitions.TypeMetadata, error) {
+ // First, we need to get the cache key for this usage. This allows us to better understand
+ // what we're looking at and pull off related info from the graph.
+ // Not an ideal situation but good enough for now.
+ symKey, err := t.Root.CacheLookupKey(t.FVersion)
+ if err != nil {
+ return definitions.TypeMetadata{}, fmt.Errorf(
+ "failed to derive cache lookup symbol key for type usage '%s' - %v",
+ t.Name,
+ err,
+ )
+ }
+
+ // The Symbol Key is the easiest way to tell whether something is a 'universe' or 'builtin' type.
+ // Universe are a special case as they never have PkgPath, imports, annotations etc.
+ if symKey.IsUniverse {
+ return definitions.TypeMetadata{
+ Name: t.Root.SimpleTypeString(),
+ Import: common.ImportTypeNone,
+ IsUniverseType: true,
+ IsByAddress: t.IsByAddress(),
+ SymbolKind: t.SymbolKind,
+ AliasMetadata: nil,
+ }, nil
+ }
+
+ return definitions.TypeMetadata{
+ Name: t.Root.SimpleTypeString(),
+ PkgPath: t.PkgPath,
+ DefaultPackageAlias: gast.GetDefaultPkgAliasByName(t.PkgPath),
+ Description: annotations.GetDescription(t.Annotations),
+ Import: t.Import,
+ IsUniverseType: t.PkgPath == "" && gast.IsUniverseType(t.Name),
+ IsByAddress: t.IsByAddress(),
+ SymbolKind: t.SymbolKind,
+ AliasMetadata: common.Ptr(getAliasMeta(ctx, symKey)),
+ }, nil
+}
+
+// IsContext returns a boolean indicating whether this usage is of Go's context.Context.
+// Context is a special object and has specific treatment at both visitation and validation layers.
+func (t TypeUsageMeta) IsContext() bool {
+ return t.Name == "Context" && t.PkgPath == "context"
+}
+
+// IsIterable returns a boolean indicating whether this usage is of a slice or an array
+func (t TypeUsageMeta) IsIterable() bool {
+ return t.Root.Kind() == TypeRefKindSlice || t.Root.Kind() == TypeRefKindArray
+}
+
+// getAliasMeta attempts to retrieve alias metadata for the given type.
+// Returns a populated AliasMetadata, if the type is an enum or alias, otherwise returns an empty AliasMetadata
+func getAliasMeta(
+ ctx ReductionContext,
+ typeSymKey graphs.SymbolKey,
+) definitions.AliasMetadata {
+ // Take the actual data from the cache. Quite hacky but good enough for now.
+ alias := definitions.AliasMetadata{}
+
+ // Check if this usage is for an enum, if so, flatten it to an AliasMetadata
+ underlyingEnum := ctx.MetaCache.GetEnum(typeSymKey)
+
+ if underlyingEnum != nil {
+ // This is an enum
+ alias.Name = underlyingEnum.Name
+ alias.AliasType = string(underlyingEnum.ValueKind)
+
+ values := []string{}
+ for _, v := range underlyingEnum.Values {
+ values = append(values, fmt.Sprintf("%v", v.Value))
+ }
+ alias.Values = values
+ } else {
+ // If the type is not an enum, check if it's an alias
+ underlyingAlias := ctx.MetaCache.GetAlias(typeSymKey)
+ if underlyingAlias != nil {
+ // This is an alias
+ alias.Name = underlyingAlias.Name
+ alias.AliasType = underlyingAlias.Type.Root.SimpleTypeString()
+ }
+ }
+
+ return alias
+}
diff --git a/core/metadata/typeref/array.go b/core/metadata/typeref/array.go
new file mode 100644
index 0000000..6c2948c
--- /dev/null
+++ b/core/metadata/typeref/array.go
@@ -0,0 +1,44 @@
+package typeref
+
+import (
+ "fmt"
+
+ "github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/gast"
+ "github.com/gopher-fleece/gleece/graphs"
+)
+
+// ArrayTypeRef (Len == nil treated like a slice in some contexts; we preserve Len for fixed-arrays)
+type ArrayTypeRef struct {
+ Len *int
+ Elem metadata.TypeRef
+}
+
+func (a *ArrayTypeRef) Kind() metadata.TypeRefKind { return metadata.TypeRefKindArray }
+
+func (a *ArrayTypeRef) CanonicalString() string {
+ if a.Len == nil {
+ return "[]" + a.Elem.CanonicalString()
+ }
+ return fmt.Sprintf("[%d]%s", *a.Len, a.Elem.CanonicalString())
+}
+
+func (a *ArrayTypeRef) SimpleTypeString() string {
+ return "[]" + a.Elem.SimpleTypeString()
+}
+
+func (a *ArrayTypeRef) CacheLookupKey(fileVersion *gast.FileVersion) (graphs.SymbolKey, error) {
+ return a.Elem.CacheLookupKey(fileVersion)
+}
+
+func (a *ArrayTypeRef) ToSymKey(fileVersion *gast.FileVersion) (graphs.SymbolKey, error) {
+ elemKey, err := a.Elem.ToSymKey(fileVersion)
+ if err != nil {
+ return graphs.SymbolKey{}, err
+ }
+ return graphs.NewCompositeTypeKey(graphs.CompositeKindArray, fileVersion, []graphs.SymbolKey{elemKey}), nil
+}
+
+func (f *ArrayTypeRef) Flatten() []metadata.TypeRef {
+ return flatten(f)
+}
diff --git a/core/metadata/typeref/common.go b/core/metadata/typeref/common.go
new file mode 100644
index 0000000..82acce9
--- /dev/null
+++ b/core/metadata/typeref/common.go
@@ -0,0 +1,55 @@
+package typeref
+
+import (
+ "fmt"
+
+ "github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/graphs"
+)
+
+func flatten(root metadata.TypeRef) []metadata.TypeRef {
+ switch t := root.(type) {
+ case *PtrTypeRef:
+ return []metadata.TypeRef{t.Elem}
+ case *SliceTypeRef:
+ return []metadata.TypeRef{t.Elem}
+ case *ArrayTypeRef:
+ return []metadata.TypeRef{t.Elem}
+ case *MapTypeRef:
+ return []metadata.TypeRef{t.Key, t.Value}
+ case *FuncTypeRef:
+ parts := make([]metadata.TypeRef, 0, len(t.Params)+len(t.Results))
+ parts = append(parts, t.Params...)
+ parts = append(parts, t.Results...)
+ return parts
+ case *NamedTypeRef:
+ return t.TypeArgs
+ case *InlineStructTypeRef:
+ out := make([]metadata.TypeRef, 0, len(t.Fields))
+ for _, f := range t.Fields {
+ out = append(out, f.Type.Root)
+ }
+ return out
+ default:
+ return nil
+ }
+}
+
+// ------------------------- canonicalSymKey helper ----------------------------
+// Produce a stable textual identity for a graphs.SymbolKey suitable for canonical strings.
+// Use FileId (if present) or FilePath to disambiguate same-name types in different files/packages.
+// Builtins/universe use only the name.
+func canonicalSymKey(k graphs.SymbolKey) string {
+ if k.IsUniverse || k.IsBuiltIn {
+ return k.Name
+ }
+ // Prefer FileId when available; fallback to FilePath
+ if k.FileId != "" {
+ return fmt.Sprintf("%s|%s", k.Name, k.FileId)
+ }
+ if k.FilePath != "" {
+ return fmt.Sprintf("%s|%s", k.Name, k.FilePath)
+ }
+ // final fallback
+ return k.Name
+}
diff --git a/core/metadata/typeref/func.go b/core/metadata/typeref/func.go
new file mode 100644
index 0000000..37bbf9b
--- /dev/null
+++ b/core/metadata/typeref/func.go
@@ -0,0 +1,77 @@
+package typeref
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/gast"
+ "github.com/gopher-fleece/gleece/graphs"
+)
+
+// FuncTypeRef
+type FuncTypeRef struct {
+ Params []metadata.TypeRef
+ Results []metadata.TypeRef
+ Variadic bool
+}
+
+func (f *FuncTypeRef) Kind() metadata.TypeRefKind { return metadata.TypeRefKindFunc }
+
+func (f *FuncTypeRef) CanonicalString() string {
+ return f.stringRepresentation(true)
+}
+
+func (f *FuncTypeRef) SimpleTypeString() string {
+ return f.stringRepresentation(false)
+}
+
+func (f *FuncTypeRef) CacheLookupKey(fileVersion *gast.FileVersion) (graphs.SymbolKey, error) {
+ return f.ToSymKey(fileVersion)
+}
+
+func (f *FuncTypeRef) stringRepresentation(canonical bool) string {
+ params := make([]string, 0, len(f.Params))
+
+ for _, p := range f.Params {
+ if canonical {
+ params = append(params, p.CanonicalString())
+ } else {
+ params = append(params, p.SimpleTypeString())
+ }
+ }
+
+ rs := make([]string, 0, len(f.Results))
+ for _, r := range f.Results {
+ if canonical {
+ rs = append(rs, r.CanonicalString())
+ } else {
+ rs = append(rs, r.SimpleTypeString())
+ }
+ }
+
+ return fmt.Sprintf("func(%s)(%s)", strings.Join(params, ","), strings.Join(rs, ","))
+}
+
+func (f *FuncTypeRef) ToSymKey(fileVersion *gast.FileVersion) (graphs.SymbolKey, error) {
+ parts := make([]graphs.SymbolKey, 0, len(f.Params)+len(f.Results))
+ for _, p := range f.Params {
+ k, err := p.ToSymKey(fileVersion)
+ if err != nil {
+ return graphs.SymbolKey{}, err
+ }
+ parts = append(parts, k)
+ }
+ for _, r := range f.Results {
+ k, err := r.ToSymKey(fileVersion)
+ if err != nil {
+ return graphs.SymbolKey{}, err
+ }
+ parts = append(parts, k)
+ }
+ return graphs.NewCompositeTypeKey(graphs.CompositeKindFunc, fileVersion, parts), nil
+}
+
+func (f *FuncTypeRef) Flatten() []metadata.TypeRef {
+ return flatten(f)
+}
diff --git a/core/metadata/typeref/inline.struct.go b/core/metadata/typeref/inline.struct.go
new file mode 100644
index 0000000..bbd1f48
--- /dev/null
+++ b/core/metadata/typeref/inline.struct.go
@@ -0,0 +1,73 @@
+package typeref
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/gast"
+ "github.com/gopher-fleece/gleece/graphs"
+)
+
+// InlineStructTypeRef represents an inline anonymous struct used directly in expressions.
+type InlineStructTypeRef struct {
+ // Fields of the anonymous struct. They contain FieldMeta (with TypeUsage inside).
+ Fields []metadata.FieldMeta
+
+ // Representative key derived from the struct node position + file version.
+ // May be zero if not set.
+ RepKey graphs.SymbolKey
+}
+
+func (i *InlineStructTypeRef) Kind() metadata.TypeRefKind { return metadata.TypeRefKindInlineStruct }
+
+func (i *InlineStructTypeRef) CanonicalString() string {
+ return i.stringRepresentation(true)
+}
+
+func (i *InlineStructTypeRef) SimpleTypeString() string {
+ return i.stringRepresentation(false)
+}
+
+func (i *InlineStructTypeRef) CacheLookupKey(fileVersion *gast.FileVersion) (graphs.SymbolKey, error) {
+ return i.ToSymKey(fileVersion)
+}
+
+func (i *InlineStructTypeRef) stringRepresentation(canonical bool) string {
+ // Build canonical from fields (short, deterministic).
+ parts := make([]string, 0, len(i.Fields))
+ for _, f := range i.Fields {
+ // include name if present, otherwise just type
+ var typeName string
+ if canonical {
+ typeName = f.Type.Root.CanonicalString()
+ } else {
+ typeName = f.Type.Root.SimpleTypeString()
+ }
+ if f.Name != "" {
+ parts = append(parts, fmt.Sprintf("%s:%s", f.Name, typeName))
+ } else {
+ parts = append(parts, typeName)
+ }
+ }
+ base := fmt.Sprintf("inline{%s}", strings.Join(parts, ","))
+ // if we have a representative key, append short key so canonical differs by location
+ if !i.RepKey.Equals(graphs.SymbolKey{}) {
+ return base + "|" + canonicalSymKey(i.RepKey)
+ }
+ return base
+}
+
+func (in *InlineStructTypeRef) ToSymKey(_ *gast.FileVersion) (graphs.SymbolKey, error) {
+ if in == nil {
+ return graphs.SymbolKey{}, fmt.Errorf("nil InlineStructTypeRef")
+ }
+ if in.RepKey.Equals(graphs.SymbolKey{}) {
+ return graphs.SymbolKey{}, fmt.Errorf("inline struct missing RepKey")
+ }
+ return in.RepKey, nil
+}
+
+func (f *InlineStructTypeRef) Flatten() []metadata.TypeRef {
+ return flatten(f)
+}
diff --git a/core/metadata/typeref/map.go b/core/metadata/typeref/map.go
new file mode 100644
index 0000000..1f5439c
--- /dev/null
+++ b/core/metadata/typeref/map.go
@@ -0,0 +1,44 @@
+package typeref
+
+import (
+ "fmt"
+
+ "github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/gast"
+ "github.com/gopher-fleece/gleece/graphs"
+)
+
+// MapTypeRef
+type MapTypeRef struct {
+ Key, Value metadata.TypeRef
+}
+
+func (m *MapTypeRef) Kind() metadata.TypeRefKind { return metadata.TypeRefKindMap }
+
+func (m *MapTypeRef) CanonicalString() string {
+ return fmt.Sprintf("map[%s]%s", m.Key.CanonicalString(), m.Value.CanonicalString())
+}
+
+func (m *MapTypeRef) SimpleTypeString() string {
+ return fmt.Sprintf("map[%s]%s", m.Key.SimpleTypeString(), m.Value.SimpleTypeString())
+}
+
+func (m *MapTypeRef) CacheLookupKey(fileVersion *gast.FileVersion) (graphs.SymbolKey, error) {
+ return m.ToSymKey(fileVersion)
+}
+
+func (m *MapTypeRef) ToSymKey(fileVersion *gast.FileVersion) (graphs.SymbolKey, error) {
+ keyK, err := m.Key.ToSymKey(fileVersion)
+ if err != nil {
+ return graphs.SymbolKey{}, err
+ }
+ valK, err := m.Value.ToSymKey(fileVersion)
+ if err != nil {
+ return graphs.SymbolKey{}, err
+ }
+ return graphs.NewCompositeTypeKey(graphs.CompositeKindMap, fileVersion, []graphs.SymbolKey{keyK, valK}), nil
+}
+
+func (f *MapTypeRef) Flatten() []metadata.TypeRef {
+ return flatten(f)
+}
diff --git a/core/metadata/typeref/named.go b/core/metadata/typeref/named.go
new file mode 100644
index 0000000..0704cc1
--- /dev/null
+++ b/core/metadata/typeref/named.go
@@ -0,0 +1,93 @@
+package typeref
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/gast"
+ "github.com/gopher-fleece/gleece/graphs"
+)
+
+// NamedTypeRef: reference to an existing declared type via graphs.SymbolKey.
+// TypeArgs are concrete type arguments if this usage is an instantiation (e.g. MyType[int]).
+type NamedTypeRef struct {
+ Key graphs.SymbolKey
+ TypeArgs []metadata.TypeRef
+}
+
+// NewNamedTypeRef creates a new named type reference.
+// Serves mostly as a single point of reference for easier code lookups
+func NewNamedTypeRef(key *graphs.SymbolKey, typeArgs []metadata.TypeRef) NamedTypeRef {
+ ref := NamedTypeRef{TypeArgs: typeArgs}
+ if key != nil {
+ ref.Key = *key
+ }
+ return ref
+}
+
+func (n *NamedTypeRef) Kind() metadata.TypeRefKind { return metadata.TypeRefKindNamed }
+
+func (n *NamedTypeRef) CanonicalString() string {
+ base := canonicalSymKey(n.Key)
+ if len(n.TypeArgs) == 0 {
+ return base
+ }
+
+ argStrings := make([]string, 0, len(n.TypeArgs))
+ for _, a := range n.TypeArgs {
+ argStrings = append(argStrings, a.CanonicalString())
+ }
+
+ return fmt.Sprintf("%s[%s]", base, strings.Join(argStrings, ","))
+}
+
+func (n *NamedTypeRef) SimpleTypeString() string {
+ if len(n.TypeArgs) == 0 {
+ return n.Key.Name
+ }
+
+ argStrings := make([]string, 0, len(n.TypeArgs))
+ for _, a := range n.TypeArgs {
+ argStrings = append(argStrings, a.SimpleTypeString())
+ }
+
+ return fmt.Sprintf("%s[%s]", n.Key.Name, strings.Join(argStrings, ","))
+}
+
+func (n *NamedTypeRef) CacheLookupKey(fileVersion *gast.FileVersion) (graphs.SymbolKey, error) {
+ return n.ToSymKey(fileVersion)
+}
+
+func (n *NamedTypeRef) ToSymKey(fileVersion *gast.FileVersion) (graphs.SymbolKey, error) {
+ // if Key present (declared/universe), use it
+ if !n.Key.Empty() {
+ // Instantiation case: combine base key with type args (if any).
+ if len(n.TypeArgs) == 0 {
+ return n.Key, nil
+ }
+
+ // Build arg keys first.
+ argKeys := make([]graphs.SymbolKey, 0, len(n.TypeArgs))
+ for _, arg := range n.TypeArgs {
+ key, err := arg.ToSymKey(fileVersion)
+ if err != nil {
+ return graphs.SymbolKey{}, err
+ }
+ argKeys = append(argKeys, key)
+ }
+ return graphs.NewInstSymbolKey(n.Key, argKeys), nil
+ }
+
+ // If no base Key present but type args exist, we cannot produce a stable instantiation
+ if len(n.TypeArgs) > 0 {
+ return graphs.SymbolKey{}, fmt.Errorf("cannot instantiate named type without base Key")
+ }
+
+ // No Key and no TypeArgs - this is unexpected.
+ return graphs.SymbolKey{}, fmt.Errorf("named type ref missing Key")
+}
+
+func (f *NamedTypeRef) Flatten() []metadata.TypeRef {
+ return flatten(f)
+}
diff --git a/core/metadata/typeref/param.go b/core/metadata/typeref/param.go
new file mode 100644
index 0000000..939ab63
--- /dev/null
+++ b/core/metadata/typeref/param.go
@@ -0,0 +1,45 @@
+package typeref
+
+import (
+ "fmt"
+
+ "github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/gast"
+ "github.com/gopher-fleece/gleece/graphs"
+)
+
+// ParamTypeRef: placeholder inside declaration bodies (e.g. "T").
+// Index should be set to declaration position (0-based) whenever possible for deterministic canonicalization.
+type ParamTypeRef struct {
+ Name string // original name if available ("T"); used for debugging
+ Index int // -1 if unknown; prefer setting this during parsing of declarations
+}
+
+func (p *ParamTypeRef) Kind() metadata.TypeRefKind { return metadata.TypeRefKindParam }
+
+func (p *ParamTypeRef) CanonicalString() string {
+ if p.Index >= 0 {
+ return fmt.Sprintf("P#%d", p.Index)
+ }
+ return fmt.Sprintf("P{%s}", p.Name)
+}
+
+func (p *ParamTypeRef) SimpleTypeString() string {
+ // This should not actually be used - we should filter out type param nodes from the graph during reduction
+ return p.CanonicalString()
+}
+
+func (p *ParamTypeRef) CacheLookupKey(fileVersion *gast.FileVersion) (graphs.SymbolKey, error) {
+ return p.ToSymKey(fileVersion)
+}
+
+func (p *ParamTypeRef) ToSymKey(fileVersion *gast.FileVersion) (graphs.SymbolKey, error) {
+ if fileVersion == nil {
+ return graphs.SymbolKey{}, fmt.Errorf("fileVersion required for ParamTypeRef key")
+ }
+ return graphs.NewParamSymbolKey(fileVersion, p.Name, p.Index), nil
+}
+
+func (f *ParamTypeRef) Flatten() []metadata.TypeRef {
+ return flatten(f)
+}
diff --git a/core/metadata/typeref/ptr.go b/core/metadata/typeref/ptr.go
new file mode 100644
index 0000000..eef9d1a
--- /dev/null
+++ b/core/metadata/typeref/ptr.go
@@ -0,0 +1,38 @@
+package typeref
+
+import (
+ "github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/gast"
+ "github.com/gopher-fleece/gleece/graphs"
+)
+
+// PtrTypeRef
+type PtrTypeRef struct{ Elem metadata.TypeRef }
+
+func (p *PtrTypeRef) Kind() metadata.TypeRefKind { return metadata.TypeRefKindPtr }
+
+func (p *PtrTypeRef) CanonicalString() string {
+ return "*" + p.Elem.CanonicalString()
+}
+
+func (p *PtrTypeRef) SimpleTypeString() string {
+ // Currently, this is a bit of a mess. The models list expects arrays to appear with '[]'
+ // but pointers are omitted, i.e., the lack of '*' in the output here is intentional.
+ return p.Elem.SimpleTypeString()
+}
+
+func (p *PtrTypeRef) CacheLookupKey(fileVersion *gast.FileVersion) (graphs.SymbolKey, error) {
+ return p.Elem.CacheLookupKey(fileVersion)
+}
+
+func (p *PtrTypeRef) ToSymKey(fileVersion *gast.FileVersion) (graphs.SymbolKey, error) {
+ elemKey, err := p.Elem.ToSymKey(fileVersion)
+ if err != nil {
+ return graphs.SymbolKey{}, err
+ }
+ return graphs.NewCompositeTypeKey(graphs.CompositeKindPtr, fileVersion, []graphs.SymbolKey{elemKey}), nil
+}
+
+func (f *PtrTypeRef) Flatten() []metadata.TypeRef {
+ return flatten(f)
+}
diff --git a/core/metadata/typeref/slice.go b/core/metadata/typeref/slice.go
new file mode 100644
index 0000000..ddff957
--- /dev/null
+++ b/core/metadata/typeref/slice.go
@@ -0,0 +1,36 @@
+package typeref
+
+import (
+ "github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/gast"
+ "github.com/gopher-fleece/gleece/graphs"
+)
+
+// SliceTypeRef
+type SliceTypeRef struct{ Elem metadata.TypeRef }
+
+func (s *SliceTypeRef) Kind() metadata.TypeRefKind { return metadata.TypeRefKindSlice }
+
+func (s *SliceTypeRef) CanonicalString() string {
+ return "[]" + s.Elem.CanonicalString()
+}
+
+func (s *SliceTypeRef) SimpleTypeString() string {
+ return "[]" + s.Elem.SimpleTypeString()
+}
+
+func (s *SliceTypeRef) CacheLookupKey(fileVersion *gast.FileVersion) (graphs.SymbolKey, error) {
+ return s.Elem.CacheLookupKey(fileVersion)
+}
+
+func (s *SliceTypeRef) ToSymKey(fileVersion *gast.FileVersion) (graphs.SymbolKey, error) {
+ elemKey, err := s.Elem.ToSymKey(fileVersion)
+ if err != nil {
+ return graphs.SymbolKey{}, err
+ }
+ return graphs.NewCompositeTypeKey(graphs.CompositeKindSlice, fileVersion, []graphs.SymbolKey{elemKey}), nil
+}
+
+func (f *SliceTypeRef) Flatten() []metadata.TypeRef {
+ return flatten(f)
+}
diff --git a/core/metadata/types.go b/core/metadata/types.go
deleted file mode 100644
index 5640c5e..0000000
--- a/core/metadata/types.go
+++ /dev/null
@@ -1,507 +0,0 @@
-package metadata
-
-import (
- "cmp"
- "fmt"
- "go/ast"
- "go/types"
- "slices"
- "strings"
-
- "github.com/gopher-fleece/gleece/common"
- "github.com/gopher-fleece/gleece/common/linq"
- "github.com/gopher-fleece/gleece/core/annotations"
- "github.com/gopher-fleece/gleece/definitions"
- "github.com/gopher-fleece/gleece/gast"
- "github.com/gopher-fleece/gleece/graphs"
- "github.com/gopher-fleece/gleece/infrastructure/logger"
-)
-
-type IdProvider interface {
- GetIdForKey(key graphs.SymbolKey) uint64
-}
-
-// Go's insane package system forces us to get... creative.
-type MetaCache interface {
- GetStruct(key graphs.SymbolKey) *StructMeta
- GetEnum(key graphs.SymbolKey) *EnumMeta
-}
-
-type EnumValueKind string
-
-const (
- EnumValueKindString EnumValueKind = "string"
- EnumValueKindInt EnumValueKind = "int"
- EnumValueKindInt8 EnumValueKind = "int8"
- EnumValueKindInt16 EnumValueKind = "int16"
- EnumValueKindInt32 EnumValueKind = "int32"
- EnumValueKindInt64 EnumValueKind = "int64"
- EnumValueKindUInt EnumValueKind = "uint"
- EnumValueKindUInt8 EnumValueKind = "uint8"
- EnumValueKindUInt16 EnumValueKind = "uint16"
- EnumValueKindUInt32 EnumValueKind = "uint32"
- EnumValueKindUInt64 EnumValueKind = "uint64"
- EnumValueKindFloat32 EnumValueKind = "float32"
- EnumValueKindFloat64 EnumValueKind = "float64"
- EnumValueKindBool EnumValueKind = "bool"
-)
-
-func NewEnumValueKind(kind types.BasicKind) (EnumValueKind, error) {
- switch kind {
- case types.String:
- return EnumValueKindString, nil
- case types.Int:
- return EnumValueKindInt, nil
- case types.Int8:
- return EnumValueKindInt8, nil
- case types.Int16:
- return EnumValueKindInt16, nil
- case types.Int32:
- return EnumValueKindInt32, nil
- case types.Int64:
- return EnumValueKindInt64, nil
- case types.Uint:
- return EnumValueKindUInt, nil
- case types.Uint8:
- return EnumValueKindUInt8, nil
- case types.Uint16:
- return EnumValueKindUInt16, nil
- case types.Uint32:
- return EnumValueKindUInt32, nil
- case types.Uint64:
- return EnumValueKindUInt64, nil
- case types.Float32:
- return EnumValueKindFloat32, nil
- case types.Float64:
- return EnumValueKindFloat64, nil
- case types.Bool:
- return EnumValueKindBool, nil
- default:
- return "", fmt.Errorf("unsupported basic kind: %v", kind)
- }
-}
-
-type SymNodeMeta struct {
- Name string
- Node ast.Node
- SymbolKind common.SymKind
- PkgPath string
- Annotations *annotations.AnnotationHolder
- // The node's resolved range in the file.
- // Note this information may or may not be available
- Range common.ResolvedRange
- FVersion *gast.FileVersion
-}
-
-type StructMeta struct {
- SymNodeMeta
- Fields []FieldMeta
-}
-
-func (s StructMeta) Reduce() definitions.StructMetadata {
- reducedFields := make([]definitions.FieldMetadata, len(s.Fields))
- for idx, field := range s.Fields {
- reducedFields[idx] = field.Reduce()
- }
-
- return definitions.StructMetadata{
- Name: s.Name,
- PkgPath: s.PkgPath,
- Description: annotations.GetDescription(s.Annotations),
- Fields: reducedFields,
- Deprecation: GetDeprecationOpts(s.Annotations),
- }
-}
-
-type ConstMeta struct {
- SymNodeMeta
- Value any
- Type TypeUsageMeta
-}
-
-type ControllerMeta struct {
- Struct StructMeta
- Receivers []ReceiverMeta
-}
-
-func (m ControllerMeta) Reduce(
- gleeceConfig *definitions.GleeceConfig,
- metaCache MetaCache,
- syncedProvider IdProvider,
-) (definitions.ControllerMetadata, error) {
- // Parse any explicit Security annotations
- security, err := GetSecurityFromContext(m.Struct.Annotations)
- if err != nil {
- return definitions.ControllerMetadata{}, err
- }
-
- // If there are no explicitly defined securities, check for inherited ones
- if len(security) <= 0 {
- logger.Debug("Controller %s does not have explicit security; Using user-defined defaults", m.Struct.Name)
- security = GetDefaultSecurity(gleeceConfig)
- }
-
- var reducedReceivers []definitions.RouteMetadata
- for _, rec := range m.Receivers {
- reduced, err := rec.Reduce(metaCache, syncedProvider, security)
- if err != nil {
- logger.Error("Failed to reduce receiver '%s' of controller '%s' - %w", rec.Name, m.Struct.Name, err)
- return definitions.ControllerMetadata{}, err
- }
- reducedReceivers = append(reducedReceivers, reduced)
- }
-
- meta := definitions.ControllerMetadata{
- Name: m.Struct.Name,
- PkgPath: m.Struct.PkgPath,
- Tag: m.Struct.Annotations.GetFirstValueOrEmpty(annotations.GleeceAnnotationTag),
- Description: m.Struct.Annotations.GetDescription(),
- RestMetadata: definitions.RestMetadata{
- Path: m.Struct.Annotations.GetFirstValueOrEmpty(annotations.GleeceAnnotationRoute),
- },
- Routes: reducedReceivers,
- Security: security,
- }
-
- return meta, nil
-}
-
-type ReceiverMeta struct {
- SymNodeMeta
- Params []FuncParam
- RetVals []FuncReturnValue
-}
-
-func (v ReceiverMeta) RetValsRange() common.ResolvedRange {
- switch len(v.RetVals) {
- case 0:
- return common.ResolvedRange{}
- case 1:
- return common.ResolvedRange{
- StartLine: v.RetVals[0].Range.StartLine,
- EndLine: v.RetVals[0].Range.EndLine,
- StartCol: v.RetVals[0].Range.StartCol,
- EndCol: v.RetVals[0].Range.EndCol,
- }
- default:
- // Copy so as not to affect original order
- retVals := append([]FuncReturnValue{}, v.RetVals...)
- slices.SortFunc(retVals, func(valA, valB FuncReturnValue) int {
- return cmp.Compare(valA.Ordinal, valB.Ordinal)
- })
-
- lastIndex := len(retVals) - 1
- return common.ResolvedRange{
- StartLine: retVals[0].Range.StartLine,
- EndLine: retVals[lastIndex].Range.EndLine,
- StartCol: retVals[0].Range.StartCol,
- EndCol: retVals[lastIndex].Range.EndCol,
- }
- }
-}
-
-func (m ReceiverMeta) Reduce(
- metaCache MetaCache,
- syncedProvider IdProvider,
- parentSecurity []definitions.RouteSecurity,
-) (definitions.RouteMetadata, error) {
-
- verbAnnotation := m.Annotations.GetFirst(annotations.GleeceAnnotationMethod)
- if verbAnnotation == nil || verbAnnotation.Value == "" {
- // Not ideal- we'd like to separate visitation, reduction and validation but typing currently doesn't
- // cleanly allow it so we have to embed a bit of validation in a few other places as well
- return definitions.RouteMetadata{}, fmt.Errorf("receiver %s has not @Method annotation", m.Name)
- }
-
- security, err := GetRouteSecurityWithInheritance(m.Annotations, parentSecurity)
- if err != nil {
- return definitions.RouteMetadata{}, err
- }
-
- templateCtx, err := GetTemplateContextMetadata(m.Annotations)
- if err != nil {
- return definitions.RouteMetadata{}, err
- }
-
- hasReturnValue := len(m.RetVals) > 1
-
- responses := []definitions.FuncReturnValue{}
- for _, fRetVal := range m.RetVals {
- response, err := fRetVal.Reduce(metaCache, syncedProvider)
- if err != nil {
- return definitions.RouteMetadata{}, err
- }
- responses = append(responses, response)
- }
-
- reducedParams := []definitions.FuncParam{}
- for _, param := range m.Params {
- reducedParam, err := param.Reduce(metaCache, syncedProvider)
- if err != nil {
- return definitions.RouteMetadata{}, err
- }
- reducedParams = append(reducedParams, reducedParam)
- }
-
- successResponseCode, successResponseDescription, err := GetResponseStatusCodeAndDescription(m.Annotations, hasReturnValue)
- if err != nil {
- return definitions.RouteMetadata{}, err
- }
-
- errorResponses, err := GetErrorResponses(m.Annotations)
- if err != nil {
- return definitions.RouteMetadata{}, err
- }
-
- return definitions.RouteMetadata{
- OperationId: m.Name,
- HttpVerb: definitions.HttpVerb(verbAnnotation.Value),
- Hiding: GetMethodHideOpts(m.Annotations),
- Deprecation: GetDeprecationOpts(m.Annotations),
- Description: m.Annotations.GetDescription(),
- RestMetadata: definitions.RestMetadata{
- Path: m.Annotations.GetFirstValueOrEmpty(annotations.GleeceAnnotationRoute),
- },
- HasReturnValue: hasReturnValue,
- RequestContentType: definitions.ContentTypeJSON, // Hardcoded for now, should be supported via annotations later on
- ResponseContentType: definitions.ContentTypeJSON, // Hardcoded for now, should be supported via annotations later on
- Security: security,
- TemplateContext: templateCtx,
- ResponseSuccessCode: successResponseCode,
- ResponseDescription: successResponseDescription,
- FuncParams: reducedParams,
- Responses: responses,
- ErrorResponses: errorResponses,
- }, nil
-}
-
-type FuncParam struct {
- SymNodeMeta
- Ordinal int
- Type TypeUsageMeta
-}
-
-func (v FuncParam) Reduce(metaCache MetaCache, syncedProvider IdProvider) (definitions.FuncParam, error) {
- typeMeta, err := v.Type.Resolve(metaCache)
- if err != nil {
- return definitions.FuncParam{}, err
- }
-
- var nameInSchema string
- var passedIn definitions.ParamPassedIn
- var validator string
-
- isContext := v.Type.IsContext()
-
- if !isContext {
- nameInSchema, err = GetParameterSchemaName(v.Name, v.Annotations)
- if err != nil {
- return definitions.FuncParam{}, err
- }
-
- passedIn, err = GetParamPassedIn(v.Name, v.Annotations)
- if err != nil {
- return definitions.FuncParam{}, err
- }
-
- validator, err = GetParamValidator(v.Name, v.Annotations, passedIn, v.Type.IsByAddress())
- if err != nil {
- return definitions.FuncParam{}, err
- }
- }
-
- typeRef, err := v.Type.GetBaseTypeRefKey()
- if err != nil {
- return definitions.FuncParam{}, err
- }
-
- // Find the parameter's attribute in the receiver's annotations
- var paramDescription string
- paramAttrib := v.Annotations.FindFirstByValue(v.Name)
- if paramAttrib != nil {
- // Note that nil here is not valid and should be rejected at the validation stage
- paramDescription = paramAttrib.Description
- }
-
- return definitions.FuncParam{
- ParamMeta: definitions.ParamMeta{
- Name: v.Name,
- Ordinal: v.Ordinal,
- TypeMeta: typeMeta,
- IsContext: isContext,
- },
- PassedIn: passedIn,
- NameInSchema: nameInSchema,
- Description: paramDescription,
- UniqueImportSerial: syncedProvider.GetIdForKey(typeRef),
- Validator: validator,
- Deprecation: GetDeprecationOpts(v.Annotations),
- }, nil
-}
-
-type FuncReturnValue struct {
- SymNodeMeta
- Ordinal int
- Type TypeUsageMeta
-}
-
-func (v FuncReturnValue) Reduce(metaCache MetaCache, syncedProvider IdProvider) (definitions.FuncReturnValue, error) {
- typeMeta, err := v.Type.Resolve(metaCache)
- if err != nil {
- return definitions.FuncReturnValue{}, err
- }
-
- typeRef, err := v.Type.GetBaseTypeRefKey()
- if err != nil {
- return definitions.FuncReturnValue{}, err
- }
-
- return definitions.FuncReturnValue{
- Ordinal: v.Ordinal,
- UniqueImportSerial: syncedProvider.GetIdForKey(typeRef),
- TypeMetadata: typeMeta,
- }, nil
-}
-
-type EnumValueDefinition struct {
- // The enum's value definition node meta, e.g. EnumValueA SomeEnumType = "Abc"
- SymNodeMeta
- Value any // e.g. ["Meter", "Kilometer"]
- // TODO: An exact textual representation of the value. For example "1 << 2"
- //RawLiteralValue string
-}
-
-type EnumMeta struct {
- // The enum's type definition's node meta e.g. type SomeEnumType string
- SymNodeMeta
- ValueKind EnumValueKind // e.g. string, int, etc.
- Values []EnumValueDefinition
-}
-
-func (e EnumMeta) Reduce() definitions.EnumMetadata {
- stringifiedValues := linq.Map(e.Values, func(value EnumValueDefinition) string {
- return fmt.Sprintf("%v", value.Value)
- })
-
- return definitions.EnumMetadata{
- Name: e.Name,
- PkgPath: e.PkgPath,
- Description: annotations.GetDescription(e.Annotations),
- Values: stringifiedValues,
- Type: string(e.ValueKind),
- Deprecation: GetDeprecationOpts(e.Annotations),
- }
-}
-
-type TypeUsageMeta struct {
- SymNodeMeta
- Import common.ImportType
- Layers []TypeLayer
-}
-
-func (t TypeUsageMeta) GetBaseTypeRefKey() (graphs.SymbolKey, error) {
- if len(t.Layers) == 0 {
- return graphs.SymbolKey{}, fmt.Errorf("TypeUsageMeta has no layers")
- }
- baseRef := t.Layers[len(t.Layers)-1].BaseTypeRef
- if baseRef == nil {
- return graphs.SymbolKey{}, fmt.Errorf("BaseTypeRef is nil on last TypeLayer")
- }
- return *baseRef, nil
-}
-
-func (t TypeUsageMeta) GetArrayLayersString() string {
- // Currently we only use arrays for spec generation
- arrayCount := 0
- for _, layer := range t.Layers {
- if layer.Kind == TypeLayerKindArray {
- arrayCount++
- }
- }
-
- return strings.Repeat("[]", arrayCount)
-}
-
-func (t TypeUsageMeta) Resolve(metaCache MetaCache) (definitions.TypeMetadata, error) {
- typeRef, err := t.GetBaseTypeRefKey()
- if err != nil {
- return definitions.TypeMetadata{}, err
- }
-
- underlyingEnum := metaCache.GetEnum(typeRef)
-
- alias := definitions.AliasMetadata{}
- if underlyingEnum != nil {
- alias.Name = underlyingEnum.Name
- alias.AliasType = string(underlyingEnum.ValueKind)
-
- values := []string{}
- for _, v := range underlyingEnum.Values {
- values = append(values, fmt.Sprintf("%v", v.Value))
- }
- alias.Values = values
- }
-
- description := ""
- if t.Annotations != nil {
- description = t.Annotations.GetDescription()
- }
-
- // Join the actual type name with its "[]" prefixes, as necessary.
- // Ugly, but the spec generator uses that - for now.
- name := t.GetArrayLayersString() + t.Name
-
- return definitions.TypeMetadata{
- Name: name,
- PkgPath: t.PkgPath,
- DefaultPackageAlias: gast.GetDefaultPkgAliasByName(t.PkgPath),
- Description: description,
- Import: t.Import,
- IsUniverseType: t.PkgPath == "" && gast.IsUniverseType(t.Name),
- IsByAddress: t.IsByAddress(),
- SymbolKind: t.SymbolKind,
- AliasMetadata: &alias,
- }, nil
-}
-
-func (t TypeUsageMeta) IsContext() bool {
- return t.Name == "Context" && t.PkgPath == "context"
-}
-
-type FieldMeta struct {
- SymNodeMeta
- Type TypeUsageMeta
- IsEmbedded bool
-}
-
-func (f FieldMeta) Reduce() definitions.FieldMetadata {
- fieldNode, ok := f.Node.(*ast.Field)
- if !ok {
- // Reduce has a pretty nice signature so pretty reluctant to hole it with an added error
- panic("field %s has a non-field node type")
- }
-
- var tag string
- if fieldNode != nil && fieldNode.Tag != nil {
- tag = strings.Trim(fieldNode.Tag.Value, "`")
- }
-
- decoratedType := f.Type.GetArrayLayersString() + f.Type.Name
- return definitions.FieldMetadata{
- Name: f.Name,
- Type: decoratedType,
- Description: annotations.GetDescription(f.Annotations),
- Tag: tag,
- IsEmbedded: f.IsEmbedded,
- Deprecation: common.Ptr(GetDeprecationOpts(f.Annotations)),
- }
-}
-
-func (m TypeUsageMeta) IsUniverseType() bool {
- return gast.IsUniverseType(m.Name)
-}
-
-func (m TypeUsageMeta) IsByAddress() bool {
- _, isStar := m.Node.(*ast.StarExpr)
- return isStar
-}
diff --git a/core/pipeline/pipeline.go b/core/pipeline/pipeline.go
index 87d85df..00e6bd1 100644
--- a/core/pipeline/pipeline.go
+++ b/core/pipeline/pipeline.go
@@ -33,43 +33,40 @@ type GleecePipeline struct {
arbitrationProvider providers.ArbitrationProvider
syncedProvider providers.SyncedProvider
- symGraph symboldg.SymbolGraphBuilder
- rootVisitor *visitors.ControllerVisitor
+ symGraph symboldg.SymbolGraphBuilder
+ visitorOrchestrator *visitors.VisitorOrchestrator
}
func NewGleecePipeline(gleeceConfig *definitions.GleeceConfig) (GleecePipeline, error) {
- var globs []string
- if len(gleeceConfig.CommonConfig.ControllerGlobs) > 0 {
- globs = gleeceConfig.CommonConfig.ControllerGlobs
- } else {
- globs = []string{"./*.go", "./**/*.go"}
- }
-
- arbProvider, err := providers.NewArbitrationProvider(globs)
+ arbProvider, err := providers.NewArbitrationProviderFromGleeceConfig(gleeceConfig)
if err != nil {
return GleecePipeline{}, err
}
metaCache := caching.NewMetadataCache()
symGraph := symboldg.NewSymbolGraph()
+ syncedProvider := providers.NewSyncedProvider()
- visitor, err := visitors.NewControllerVisitor(&visitors.VisitContext{
+ visitCtx := &visitors.VisitContext{
GleeceConfig: gleeceConfig,
ArbitrationProvider: arbProvider,
MetadataCache: metaCache,
- GraphBuilder: &symGraph,
- })
+ Graph: &symGraph,
+ SyncedProvider: &syncedProvider,
+ }
+
+ visitorOrchestrator, err := visitors.NewVisitorOrchestrator(visitCtx)
if err != nil {
- return GleecePipeline{}, err
+ return GleecePipeline{}, fmt.Errorf("the GleecePipeline could not construct an instance of VisitorOrchestrator - %v", err)
}
return GleecePipeline{
- rootVisitor: visitor,
+ visitorOrchestrator: visitorOrchestrator,
symGraph: &symGraph,
gleeceConfig: gleeceConfig,
metadataCache: metaCache,
arbitrationProvider: *arbProvider,
- syncedProvider: providers.NewSyncedProvider(),
+ syncedProvider: syncedProvider,
}, nil
}
@@ -107,13 +104,17 @@ func (p *GleecePipeline) Run() (GleeceFlattenedMetadata, error) {
}
func (p *GleecePipeline) GenerateGraph() error {
- for _, file := range p.rootVisitor.GetAllSourceFiles() {
- ast.Walk(p.rootVisitor, file)
+ for _, file := range p.arbitrationProvider.GetAllSourceFiles() {
+ ast.Walk(p.visitorOrchestrator, file)
}
- lastErr := p.rootVisitor.GetLastError()
+ lastErr := p.visitorOrchestrator.GetLastError()
if lastErr != nil {
- logger.Error("Visitor encountered at-least one error. Last error:\n%v\n\t%s", lastErr, p.rootVisitor.GetFormattedDiagnosticStack())
+ logger.Error(
+ "Visitor encountered at-least one error. Last error:\n%v\n\t%s",
+ lastErr,
+ p.visitorOrchestrator.GetFormattedDiagnosticStack(),
+ )
return lastErr
}
@@ -127,16 +128,22 @@ func (p *GleecePipeline) GenerateIntermediate() (GleeceFlattenedMetadata, error)
return GleeceFlattenedMetadata{}, err
}
+ models, err := p.getModels()
+ if err != nil {
+ logger.Error("Pipeline failed to obtain models list - %w", err)
+ return GleeceFlattenedMetadata{}, err
+ }
+
return GleeceFlattenedMetadata{
Imports: p.getImports(controllers),
Flat: controllers,
- Models: p.getModels(),
- PlainErrorPresent: p.symGraph.IsSpecialPresent(symboldg.SpecialTypeError),
+ Models: models,
+ PlainErrorPresent: p.symGraph.IsSpecialPresent(common.SpecialTypeError),
}, nil
}
func (p *GleecePipeline) getReducedControllers() ([]definitions.ControllerMetadata, error) {
- controllers, err := p.reduceControllers(p.rootVisitor.GetControllers())
+ controllers, err := p.reduceControllers(p.getControllers())
if err != nil {
logger.Error("Failed to reduce controller tree to flat form: %w", err)
return []definitions.ControllerMetadata{}, err
@@ -213,28 +220,27 @@ func (p *GleecePipeline) appendRouteImports(imports map[string]MapSet.Set[string
// Validate validates the metadata created by the graph generation phase
func (p *GleecePipeline) Validate() ([]diagnostics.EntityDiagnostic, error) {
- allDiags := []diagnostics.EntityDiagnostic{}
-
- for _, ctrl := range p.rootVisitor.GetControllers() {
- validator := validators.NewControllerValidator(p.gleeceConfig, p.arbitrationProvider.Pkg(), &ctrl)
- ctrlDiag, err := validator.Validate()
- if err != nil {
- return allDiags, fmt.Errorf("failed to validate controller '%s' due to an error - %w", ctrl.Struct.Name, err)
- }
+ validator := validators.NewApiValidator(
+ p.gleeceConfig,
+ p.arbitrationProvider.Pkg(),
+ p.getControllers(),
+ )
- if !ctrlDiag.Empty() {
- allDiags = append(allDiags, ctrlDiag)
- }
- }
+ return validator.Validate()
+}
- return allDiags, nil
+func (p *GleecePipeline) getControllers() []metadata.ControllerMeta {
+ controllerNodes := p.symGraph.FindByKind(common.SymKindController)
+ return linq.Map(controllerNodes, func(node *symboldg.SymbolNode) metadata.ControllerMeta {
+ return node.Data.(metadata.ControllerMeta)
+ })
}
func (p *GleecePipeline) reduceControllers(controllers []metadata.ControllerMeta) ([]definitions.ControllerMetadata, error) {
var reducedControllers []definitions.ControllerMetadata
for _, controller := range controllers {
- reduced, err := controller.Reduce(p.gleeceConfig, p.metadataCache, &p.syncedProvider)
+ reduced, err := controller.Reduce(p.getReductionContext())
if err != nil {
return []definitions.ControllerMetadata{}, err
}
@@ -244,16 +250,27 @@ func (p *GleecePipeline) reduceControllers(controllers []metadata.ControllerMeta
return reducedControllers, nil
}
-func (p *GleecePipeline) getModels() definitions.Models {
- structs := p.symGraph.Structs()
- reducedStructs := linq.Map(structs, func(s metadata.StructMeta) definitions.StructMetadata {
- return s.Reduce()
- })
+func (p *GleecePipeline) getModels() (definitions.Models, error) {
+ ctx := p.getReductionContext()
- enums := p.symGraph.Enums()
- reducedEnums := linq.Map(enums, func(e metadata.EnumMeta) definitions.EnumMetadata {
- return e.Reduce()
- })
+ reducedStructs, err := symboldg.ComposeStructs(p.getReductionContext(), p.Graph(), nil)
+ if err != nil {
+ return definitions.Models{}, err
+ }
+
+ reducedAliases, err := symboldg.ComposeAliases(p.getReductionContext(), p.Graph())
+ if err != nil {
+ return definitions.Models{}, err
+ }
+
+ reducedEnums := []definitions.EnumMetadata{}
+ for _, enumEntity := range p.symGraph.Enums() {
+ reduced, err := enumEntity.Reduce(ctx)
+ if err != nil {
+ return definitions.Models{}, fmt.Errorf("failed during reduction of enum '%s' - %v", enumEntity.Name, err)
+ }
+ reducedEnums = append(reducedEnums, reduced)
+ }
slices.SortFunc(reducedStructs, func(a, b definitions.StructMetadata) int {
return strings.Compare(a.Name, b.Name)
@@ -266,5 +283,14 @@ func (p *GleecePipeline) getModels() definitions.Models {
return definitions.Models{
Structs: reducedStructs,
Enums: reducedEnums,
+ Aliases: reducedAliases,
+ }, nil
+}
+
+func (p *GleecePipeline) getReductionContext() metadata.ReductionContext {
+ return metadata.ReductionContext{
+ GleeceConfig: p.gleeceConfig,
+ MetaCache: p.metadataCache,
+ SyncedProvider: &p.syncedProvider,
}
}
diff --git a/core/validators/api.validator.go b/core/validators/api.validator.go
new file mode 100644
index 0000000..187fc7c
--- /dev/null
+++ b/core/validators/api.validator.go
@@ -0,0 +1,165 @@
+package validators
+
+import (
+ "fmt"
+ "slices"
+
+ "github.com/gopher-fleece/gleece/core/annotations"
+ "github.com/gopher-fleece/gleece/core/arbitrators"
+ "github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/core/validators/diagnostics"
+ "github.com/gopher-fleece/gleece/core/validators/paths"
+ "github.com/gopher-fleece/gleece/definitions"
+)
+
+type ApiValidator struct {
+ gleeceConfig *definitions.GleeceConfig
+ packagesFacade *arbitrators.PackagesFacade
+ controllers []metadata.ControllerMeta
+}
+
+func NewApiValidator(
+ gleeceConfig *definitions.GleeceConfig,
+ packagesFacade *arbitrators.PackagesFacade,
+ controllers []metadata.ControllerMeta,
+) ApiValidator {
+ return ApiValidator{
+ gleeceConfig: gleeceConfig,
+ packagesFacade: packagesFacade,
+ controllers: controllers,
+ }
+}
+
+func (v *ApiValidator) Validate() ([]diagnostics.EntityDiagnostic, error) {
+ controllerDiags, routeEntries, err := v.validateControllers()
+ if err != nil {
+ return controllerDiags, fmt.Errorf("failed to validate one or more controllers - %w", err)
+ }
+
+ conflicts, err := v.inPlaceAppendPathConflictDiagnostics(controllerDiags, routeEntries)
+ if err != nil {
+ return controllerDiags, err
+ }
+
+ return conflicts, nil
+}
+
+func (v *ApiValidator) validateControllers() ([]diagnostics.EntityDiagnostic, []paths.RouteEntry, error) {
+ controllerDiags := []diagnostics.EntityDiagnostic{}
+
+ routeEntries := []paths.RouteEntry{}
+
+ for _, ctrl := range v.controllers {
+ validator := NewControllerValidator(v.gleeceConfig, v.packagesFacade, &ctrl)
+ ctrlDiag, err := validator.Validate()
+ if err != nil {
+ return controllerDiags, routeEntries, fmt.Errorf(
+ "failed to validate controller '%s' due to an error - %w",
+ ctrl.Struct.Name,
+ err,
+ )
+ }
+
+ if !ctrlDiag.Empty() {
+ controllerDiags = append(controllerDiags, ctrlDiag)
+ }
+
+ routeEntries = slices.Concat(routeEntries, v.getRouteEntries(&ctrl))
+ }
+
+ return controllerDiags, routeEntries, nil
+}
+
+func (v *ApiValidator) getRouteEntries(controller *metadata.ControllerMeta) []paths.RouteEntry {
+ entries := make([]paths.RouteEntry, 0, len(controller.Receivers))
+
+ for _, route := range controller.Receivers {
+ entries = append(
+ entries,
+ paths.RouteEntry{
+ Path: route.Annotations.GetFirstValueOrEmpty(annotations.GleeceAnnotationRoute),
+ Method: route.Annotations.GetFirstValueOrEmpty(annotations.GleeceAnnotationMethod),
+ Meta: paths.RouteEntryMeta{
+ Controller: controller,
+ Receiver: &route,
+ },
+ },
+ )
+ }
+
+ return entries
+}
+
+func (v *ApiValidator) inPlaceAppendPathConflictDiagnostics(
+ controllerDiags []diagnostics.EntityDiagnostic,
+ routeEntries []paths.RouteEntry,
+) ([]diagnostics.EntityDiagnostic, error) {
+
+ conflicts := paths.FindConflicts(routeEntries)
+
+ if len(conflicts) <= 0 {
+ return controllerDiags, nil
+ }
+
+ for _, conflict := range conflicts {
+ controllerDiags = v.adjustDiagsForConflictingEntry(controllerDiags, conflict.A, conflict.Reason)
+ controllerDiags = v.adjustDiagsForConflictingEntry(controllerDiags, conflict.B, conflict.Reason)
+ }
+
+ return controllerDiags, nil
+}
+
+// adjustDiagsForConflictingEntry merges the given controller-level diagnostics with the given
+// global path conflict entry
+func (v *ApiValidator) adjustDiagsForConflictingEntry(
+ controllerDiags []diagnostics.EntityDiagnostic,
+ entry paths.RouteEntry,
+ conflictReason string,
+) []diagnostics.EntityDiagnostic {
+ // First, see if there are existing diagnostics for the controller referenced
+ // by the route entry
+ relevantDiagIdx := slices.IndexFunc(controllerDiags, func(diag diagnostics.EntityDiagnostic) bool {
+ return diag.BaseKey() == diagnostics.CreateEntityDiagKey(controllerDiagKind, entry.Meta.Controller.Struct.Name)
+ })
+
+ var relevantDiag diagnostics.EntityDiagnostic
+
+ if relevantDiagIdx < 0 {
+ // This is the first diagnostic for this controller - need to create a new diagnostic entity
+ relevantDiag = diagnostics.NewEntityDiagnostic(
+ controllerDiagKind,
+ entry.Meta.Controller.Struct.Name,
+ )
+ } else {
+ // This controller already has diagnostics - we just need to append to them
+ relevantDiag = controllerDiags[relevantDiagIdx]
+ }
+
+ // Get the route for the receiver (API endpoint) mentioned by the conflict entry
+ routeAnnotation := entry.Meta.Receiver.Annotations.GetFirst(annotations.GleeceAnnotationRoute)
+ receiverResolvedDiag := diagnostics.NewWarningDiagnostic(
+ entry.Meta.Receiver.Annotations.FileName(),
+ fmt.Sprintf("Path conflict - %s", conflictReason),
+ diagnostics.DiagRouteConflict,
+ routeAnnotation.GetValueRange(),
+ )
+
+ // Same logic as before - if the controller already has a diagnostic for the referenced receiver (route)
+ // use that, otherwise, create a new one and append
+ receiverDiag := relevantDiag.GetChild(receiverDiagKind, entry.Meta.Receiver.Name)
+ if receiverDiag == nil {
+ diag := diagnostics.NewEntityDiagnostic(receiverDiagKind, entry.Meta.Receiver.Name)
+ diag.AddDiagnostic(receiverResolvedDiag)
+ relevantDiag.AddChild(&diag)
+ } else {
+ receiverDiag.AddDiagnostic(receiverResolvedDiag)
+ }
+
+ if relevantDiagIdx >= 0 {
+ controllerDiags[relevantDiagIdx] = relevantDiag
+ } else {
+ controllerDiags = append(controllerDiags, relevantDiag)
+ }
+
+ return controllerDiags
+}
diff --git a/core/validators/commos.go b/core/validators/commos.go
new file mode 100644
index 0000000..8f98aec
--- /dev/null
+++ b/core/validators/commos.go
@@ -0,0 +1,4 @@
+package validators
+
+const controllerDiagKind = "Controller"
+const receiverDiagKind = "Receiver"
diff --git a/core/validators/controller.go b/core/validators/controller.validator.go
similarity index 100%
rename from core/validators/controller.go
rename to core/validators/controller.validator.go
diff --git a/core/validators/diagnostics/classified.go b/core/validators/diagnostics/classified.go
new file mode 100644
index 0000000..14d0420
--- /dev/null
+++ b/core/validators/diagnostics/classified.go
@@ -0,0 +1,71 @@
+package diagnostics
+
+import (
+ "fmt"
+ "strings"
+)
+
+type ClassifiedEntityDiags struct {
+ Errors []ResolvedDiagnostic
+ Warnings []ResolvedDiagnostic
+ Info []ResolvedDiagnostic
+ Hints []ResolvedDiagnostic
+}
+
+func (d ClassifiedEntityDiags) formatSeverityClass(severity string, diags []ResolvedDiagnostic) string {
+ builder := strings.Builder{}
+ builder.WriteString(fmt.Sprintf("%s: (Total %d)\n", severity, len(diags)))
+ for _, diag := range diags {
+ builder.WriteString(fmt.Sprintf(
+ "\t %s at %s:%d:%d - %s\n",
+ diag.Code,
+ diag.FilePath,
+ diag.Range.StartLine+1, // These are 0 based but IDE is generally 1 based.
+ diag.Range.StartCol+1, // Might have to re-think this and move back to 1-base
+ diag.Message,
+ ))
+ }
+
+ return builder.String()
+}
+
+func (d ClassifiedEntityDiags) String() string {
+ builder := strings.Builder{}
+ builder.WriteString(fmt.Sprintf("%s\n", d.formatSeverityClass("Errors", d.Errors)))
+ builder.WriteString(fmt.Sprintf("%s\n", d.formatSeverityClass("Warnings", d.Warnings)))
+ builder.WriteString(fmt.Sprintf("%s\n", d.formatSeverityClass("Info", d.Info)))
+ builder.WriteString(fmt.Sprintf("%s\n", d.formatSeverityClass("Hints", d.Hints)))
+ return builder.String()
+}
+
+func ClassifyEntityDiags(entityDiag EntityDiagnostic) ClassifiedEntityDiags {
+ classified := ClassifiedEntityDiags{
+ Errors: []ResolvedDiagnostic{},
+ Warnings: []ResolvedDiagnostic{},
+ Info: []ResolvedDiagnostic{},
+ Hints: []ResolvedDiagnostic{},
+ }
+
+ for _, diag := range entityDiag.Diagnostics {
+ switch diag.Severity {
+ case DiagnosticError:
+ classified.Errors = append(classified.Errors, diag)
+ case DiagnosticWarning:
+ classified.Warnings = append(classified.Warnings, diag)
+ case DiagnosticInformation:
+ classified.Info = append(classified.Info, diag)
+ case DiagnosticHint:
+ classified.Hints = append(classified.Hints, diag)
+ }
+ }
+
+ for _, childDiag := range entityDiag.Children {
+ classifiedChild := ClassifyEntityDiags(*childDiag)
+ classified.Errors = append(classified.Errors, classifiedChild.Errors...)
+ classified.Warnings = append(classified.Warnings, classifiedChild.Warnings...)
+ classified.Info = append(classified.Info, classifiedChild.Info...)
+ classified.Hints = append(classified.Hints, classifiedChild.Hints...)
+ }
+
+ return classified
+}
diff --git a/core/validators/diagnostics/entity.go b/core/validators/diagnostics/entity.go
new file mode 100644
index 0000000..875089a
--- /dev/null
+++ b/core/validators/diagnostics/entity.go
@@ -0,0 +1,130 @@
+package diagnostics
+
+import (
+ "errors"
+ "fmt"
+ "slices"
+ "strings"
+
+ "github.com/gopher-fleece/gleece/common/linq"
+)
+
+type EntityDiagnostic struct {
+ EntityName string
+ EntityKind string
+ Diagnostics []ResolvedDiagnostic // Diagnostics directly attached to this entity
+ Children []*EntityDiagnostic // Nested entity diagnostics
+}
+
+func NewEntityDiagnostic(context, name string) EntityDiagnostic {
+ return EntityDiagnostic{
+ EntityKind: context,
+ EntityName: name,
+ Diagnostics: []ResolvedDiagnostic{},
+ }
+}
+
+// BaseKey is the diagnostic entity's identifier - a combination of entity kind and name.
+// Note this does not consider 'contents' such as Diagnostics or Children
+func (d EntityDiagnostic) BaseKey() string {
+ return CreateEntityDiagKey(d.EntityKind, d.EntityName)
+}
+
+func (d EntityDiagnostic) Empty() bool {
+ return len(d.Diagnostics) <= 0 && len(d.Children) <= 0
+}
+
+func (d *EntityDiagnostic) AddDiagnostic(diag ResolvedDiagnostic) {
+ if d.Diagnostics == nil {
+ d.Diagnostics = []ResolvedDiagnostic{diag}
+ } else {
+ d.Diagnostics = append(d.Diagnostics, diag)
+ }
+}
+
+func (d *EntityDiagnostic) AddDiagnostics(diags []ResolvedDiagnostic) {
+ if len(diags) <= 0 {
+ return
+ }
+
+ if d.Diagnostics == nil {
+ d.Diagnostics = diags
+ } else {
+ d.Diagnostics = append(d.Diagnostics, diags...)
+ }
+}
+
+func (d *EntityDiagnostic) AddDiagnosticIfNotNil(diag *ResolvedDiagnostic) {
+ if diag == nil {
+ return
+ }
+
+ d.AddDiagnostic(*diag)
+}
+
+// AddChild appends a child EntityDiagnostic to this entity.
+// 'Child' entity diagnostics are meant to represent a nested but distinct entity with an issue
+func (d *EntityDiagnostic) AddChild(child *EntityDiagnostic) {
+ if child == nil {
+ return
+ }
+
+ if d.Children == nil {
+ d.Children = []*EntityDiagnostic{child}
+ } else {
+ d.Children = append(d.Children, child)
+ }
+}
+
+// GetChild returns a reference to the first diagnostic entity that matches the given name/kind
+// or nil if no match was found.
+//
+// This method is suboptimal as children may be duplicated.
+// Need to revise this to a map perhaps.
+func (d *EntityDiagnostic) GetChild(name, kind string) *EntityDiagnostic {
+ match := linq.First(d.Children, func(child *EntityDiagnostic) bool {
+ return child != nil && child.EntityName == name && child.EntityKind == kind
+ })
+
+ if match != nil {
+ return *match
+ }
+
+ return nil
+}
+
+func GetDiagnosticsWithSeverity(diags []EntityDiagnostic, severities []DiagnosticSeverity) []EntityDiagnostic {
+ matching := []EntityDiagnostic{}
+
+ for _, diagEntity := range diags {
+ for _, diag := range diagEntity.Diagnostics {
+ if slices.Contains(severities, diag.Severity) {
+ matching = append(matching, diagEntity)
+ }
+ }
+
+ if len(diagEntity.Children) > 0 {
+ // Got a bit of a ptr-value mess over here. Need to improve
+ dereferencedChildren := linq.DereferenceSliceElements(diagEntity.Children)
+ matching = append(matching, GetDiagnosticsWithSeverity(dereferencedChildren, severities)...)
+ }
+ }
+
+ return matching
+}
+
+func CreateEntityDiagKey(kind, name string) string {
+ return fmt.Sprintf("%s-%s", kind, name)
+}
+
+func DiagnosticsToError(diags []EntityDiagnostic) error {
+ builder := strings.Builder{}
+ builder.WriteString(fmt.Sprintf("Entities with diagnostics: %d\n", len(diags)))
+ for _, diag := range diags {
+ builder.WriteString(fmt.Sprintf("%s %s:\n\t", diag.EntityKind, diag.EntityName))
+ classified := ClassifyEntityDiags(diag)
+ builder.WriteString(classified.String())
+ }
+
+ return errors.New(builder.String())
+}
diff --git a/core/validators/diagnostics/enums.go b/core/validators/diagnostics/enums.go
index 739e2d8..3e2ee6d 100644
--- a/core/validators/diagnostics/enums.go
+++ b/core/validators/diagnostics/enums.go
@@ -62,4 +62,5 @@ const (
DiagReceiverRetValsIsNotError DiagnosticCode = "receiver-return-value-is-not-an-error"
DiagReceiverMissingSecurity DiagnosticCode = "receiver-missing-security"
DiagFeatureUnsupported DiagnosticCode = "unsupported-feature"
+ DiagRouteConflict DiagnosticCode = "route-conflict"
)
diff --git a/core/validators/diagnostics/resolved.go b/core/validators/diagnostics/resolved.go
new file mode 100644
index 0000000..1fd30cf
--- /dev/null
+++ b/core/validators/diagnostics/resolved.go
@@ -0,0 +1,92 @@
+package diagnostics
+
+import (
+ "fmt"
+ "slices"
+
+ "github.com/gopher-fleece/gleece/common"
+)
+
+type TextEdit struct {
+ NewText string
+ FilePath string
+ Range common.ResolvedRange
+}
+
+func (t TextEdit) Key() string {
+ return fmt.Sprintf("%s|%d:%d-%d:%d|%s",
+ t.FilePath,
+ t.Range.StartLine, t.Range.StartCol, t.Range.EndLine, t.Range.EndCol,
+ t.NewText,
+ )
+}
+
+type ResolvedDiagnostic struct {
+ Message string
+ Severity DiagnosticSeverity
+ FilePath string
+ Range common.ResolvedRange
+ Code string // optional rule id
+ Source string // "gleece"
+ Fixes []TextEdit // optional
+}
+
+func (d *ResolvedDiagnostic) Equal(other ResolvedDiagnostic) bool {
+ if d.Message != other.Message {
+ return false
+ }
+
+ if d.Severity != other.Severity {
+ return false
+ }
+
+ if d.Code != other.Code {
+ return false
+ }
+
+ if d.FilePath != other.FilePath {
+ return false
+ }
+
+ if d.Range != other.Range {
+ return false
+ }
+
+ if d.Source != other.Source {
+ return false
+ }
+
+ return slices.Equal(d.Fixes, other.Fixes)
+}
+
+func NewDiagnostic(
+ filePath, message string,
+ code DiagnosticCode,
+ severity DiagnosticSeverity,
+ rng common.ResolvedRange,
+) ResolvedDiagnostic {
+ return ResolvedDiagnostic{
+ Message: message,
+ Severity: severity,
+ FilePath: filePath,
+ Range: rng,
+ Code: string(code),
+ Source: "gleece",
+ }
+}
+
+func NewErrorDiagnostic(filePath, message string, code DiagnosticCode, rng common.ResolvedRange) ResolvedDiagnostic {
+ return NewDiagnostic(filePath, message, code, DiagnosticError, rng)
+}
+
+func NewWarningDiagnostic(filePath, message string, code DiagnosticCode, rng common.ResolvedRange) ResolvedDiagnostic {
+ return NewDiagnostic(filePath, message, code, DiagnosticWarning, rng)
+}
+
+func NewInfoDiagnostic(filePath, message string, code DiagnosticCode, rng common.ResolvedRange) ResolvedDiagnostic {
+ return NewDiagnostic(filePath, message, code, DiagnosticInformation, rng)
+}
+
+func NewHintDiagnostic(filePath, message string, code DiagnosticCode, rng common.ResolvedRange) ResolvedDiagnostic {
+ return NewDiagnostic(filePath, message, code, DiagnosticHint, rng)
+}
diff --git a/core/validators/diagnostics/structs.go b/core/validators/diagnostics/structs.go
deleted file mode 100644
index c591c89..0000000
--- a/core/validators/diagnostics/structs.go
+++ /dev/null
@@ -1,237 +0,0 @@
-package diagnostics
-
-import (
- "errors"
- "fmt"
- "slices"
- "strings"
-
- "github.com/gopher-fleece/gleece/common"
- "github.com/gopher-fleece/gleece/common/linq"
-)
-
-type TextEdit struct {
- NewText string
- FilePath string
- Range common.ResolvedRange
-}
-
-type ResolvedDiagnostic struct {
- Message string
- Severity DiagnosticSeverity
- FilePath string
- Range common.ResolvedRange
- Code string // optional rule id
- Source string // "gleece"
- Fixes []TextEdit // optional
-}
-
-func (d *ResolvedDiagnostic) Equal(other ResolvedDiagnostic) bool {
- if d.Message != other.Message {
- return false
- }
-
- if d.Severity != other.Severity {
- return false
- }
-
- if d.Code != other.Code {
- return false
- }
-
- if d.FilePath != other.FilePath {
- return false
- }
-
- if d.Range != other.Range {
- return false
- }
-
- if d.Source != other.Source {
- return false
- }
-
- return slices.Equal(d.Fixes, other.Fixes)
-}
-
-func NewDiagnostic(
- filePath, message string,
- code DiagnosticCode,
- severity DiagnosticSeverity,
- rng common.ResolvedRange,
-) ResolvedDiagnostic {
- return ResolvedDiagnostic{
- Message: message,
- Severity: severity,
- FilePath: filePath,
- Range: rng,
- Code: string(code),
- Source: "gleece",
- }
-}
-
-func NewErrorDiagnostic(filePath, message string, code DiagnosticCode, rng common.ResolvedRange) ResolvedDiagnostic {
- return NewDiagnostic(filePath, message, code, DiagnosticError, rng)
-}
-
-func NewWarningDiagnostic(filePath, message string, code DiagnosticCode, rng common.ResolvedRange) ResolvedDiagnostic {
- return NewDiagnostic(filePath, message, code, DiagnosticWarning, rng)
-}
-
-func NewInfoDiagnostic(filePath, message string, code DiagnosticCode, rng common.ResolvedRange) ResolvedDiagnostic {
- return NewDiagnostic(filePath, message, code, DiagnosticInformation, rng)
-}
-
-func NewHintDiagnostic(filePath, message string, code DiagnosticCode, rng common.ResolvedRange) ResolvedDiagnostic {
- return NewDiagnostic(filePath, message, code, DiagnosticHint, rng)
-}
-
-type EntityDiagnostic struct {
- EntityName string
- EntityKind string
- Diagnostics []ResolvedDiagnostic // Diagnostics directly attached to this entity
- Children []*EntityDiagnostic // Nested entity diagnostics
-}
-
-func NewEntityDiagnostic(context, name string) EntityDiagnostic {
- return EntityDiagnostic{
- EntityKind: context,
- EntityName: name,
- Diagnostics: []ResolvedDiagnostic{},
- }
-}
-
-func (d EntityDiagnostic) Empty() bool {
- return len(d.Diagnostics) <= 0 && len(d.Children) <= 0
-}
-
-func (d *EntityDiagnostic) AddDiagnostic(diag ResolvedDiagnostic) {
- if d.Diagnostics == nil {
- d.Diagnostics = []ResolvedDiagnostic{diag}
- } else {
- d.Diagnostics = append(d.Diagnostics, diag)
- }
-}
-
-func (d *EntityDiagnostic) AddDiagnostics(diags []ResolvedDiagnostic) {
- if len(diags) <= 0 {
- return
- }
-
- if d.Diagnostics == nil {
- d.Diagnostics = diags
- } else {
- d.Diagnostics = append(d.Diagnostics, diags...)
- }
-}
-
-func (d *EntityDiagnostic) AddDiagnosticIfNotNil(diag *ResolvedDiagnostic) {
- if diag == nil {
- return
- }
-
- d.AddDiagnostic(*diag)
-}
-
-// add child (short & safe)
-func (d *EntityDiagnostic) AddChild(child *EntityDiagnostic) {
- if child == nil {
- return
- }
-
- if d.Children == nil {
- d.Children = []*EntityDiagnostic{child}
- } else {
- d.Children = append(d.Children, child)
- }
-}
-
-func GetDiagnosticsWithSeverity(diags []EntityDiagnostic, severities []DiagnosticSeverity) []EntityDiagnostic {
- matching := []EntityDiagnostic{}
-
- for _, diagEntity := range diags {
- for _, diag := range diagEntity.Diagnostics {
- if slices.Contains(severities, diag.Severity) {
- matching = append(matching, diagEntity)
- }
- }
-
- if len(diagEntity.Children) > 0 {
- // Got a bit of a ptr-value mess over here. Need to improve
- dereferencedChildren := linq.DereferenceSliceElements(diagEntity.Children)
- matching = append(matching, GetDiagnosticsWithSeverity(dereferencedChildren, severities)...)
- }
- }
-
- return matching
-}
-
-type ClassifiedEntityDiags struct {
- Errors []ResolvedDiagnostic
- Warnings []ResolvedDiagnostic
- Info []ResolvedDiagnostic
- Hints []ResolvedDiagnostic
-}
-
-func (d ClassifiedEntityDiags) formatSeverityClass(severity string, diags []ResolvedDiagnostic) string {
- builder := strings.Builder{}
- builder.WriteString(fmt.Sprintf("%s: (Total %d)\n", severity, len(diags)))
- for _, diag := range diags {
- builder.WriteString(fmt.Sprintf("\t %s at %s - %s ", diag.Code, diag.FilePath, diag.Message))
- }
-
- return builder.String()
-}
-
-func (d ClassifiedEntityDiags) String() string {
- builder := strings.Builder{}
- builder.WriteString(fmt.Sprintf("%s\n", d.formatSeverityClass("Errors", d.Errors)))
- builder.WriteString(fmt.Sprintf("%s\n", d.formatSeverityClass("Warnings", d.Warnings)))
- builder.WriteString(fmt.Sprintf("%s\n", d.formatSeverityClass("Info", d.Info)))
- builder.WriteString(fmt.Sprintf("%s\n", d.formatSeverityClass("Hints", d.Hints)))
- return builder.String()
-}
-
-func ClassifyEntityDiags(entityDiag EntityDiagnostic) ClassifiedEntityDiags {
- classified := ClassifiedEntityDiags{
- Errors: []ResolvedDiagnostic{},
- Warnings: []ResolvedDiagnostic{},
- Info: []ResolvedDiagnostic{},
- Hints: []ResolvedDiagnostic{},
- }
-
- for _, diag := range entityDiag.Diagnostics {
- switch diag.Severity {
- case DiagnosticError:
- classified.Errors = append(classified.Errors, diag)
- case DiagnosticWarning:
- classified.Warnings = append(classified.Warnings, diag)
- case DiagnosticInformation:
- classified.Info = append(classified.Info, diag)
- case DiagnosticHint:
- classified.Hints = append(classified.Hints, diag)
- }
- }
-
- for _, childDiag := range entityDiag.Children {
- classifiedChild := ClassifyEntityDiags(*childDiag)
- classified.Errors = append(classified.Errors, classifiedChild.Errors...)
- classified.Warnings = append(classified.Warnings, classifiedChild.Warnings...)
- classified.Info = append(classified.Info, classifiedChild.Info...)
- classified.Hints = append(classified.Hints, classifiedChild.Hints...)
- }
-
- return classified
-}
-
-func DiagnosticsToError(diags []EntityDiagnostic) error {
- builder := strings.Builder{}
- builder.WriteString(fmt.Sprintf("Entities with diagnostics: %d\n", len(diags)))
- for _, diag := range diags {
- builder.WriteString(fmt.Sprintf("%s %s:\n\t", diag.EntityKind, diag.EntityName))
- classified := ClassifyEntityDiags(diag)
- builder.WriteString(classified.String())
- }
-
- return errors.New(builder.String())
-}
diff --git a/core/validators/paths/paths.go b/core/validators/paths/paths.go
new file mode 100644
index 0000000..7a88cfc
--- /dev/null
+++ b/core/validators/paths/paths.go
@@ -0,0 +1,309 @@
+package paths
+
+import (
+ "fmt"
+ "slices"
+ "strings"
+
+ "github.com/gopher-fleece/gleece/core/metadata"
+)
+
+type trieNode struct {
+ literalChildren map[string]*trieNode
+ paramChild *trieNode
+ // endpoints keyed by HTTP method (e.g. "GET", "POST")
+ endpoint map[string]*RouteEntry
+}
+
+func newTrieNode() *trieNode {
+ return &trieNode{
+ literalChildren: map[string]*trieNode{},
+ endpoint: map[string]*RouteEntry{},
+ }
+}
+
+type RouteEntryMeta struct {
+ Controller *metadata.ControllerMeta
+ Receiver *metadata.ReceiverMeta
+}
+
+// RouteEntry represents a discovered route to validate.
+type RouteEntry struct {
+ Path string
+ Method string // GET, POST etc.
+ Meta RouteEntryMeta
+}
+
+type Conflict struct {
+ A RouteEntry
+ B RouteEntry
+ Reason string
+}
+
+// FindConflicts returns deterministic list of route conflicts.
+// Methods are respected: only endpoints with the same method can conflict.
+func FindConflicts(entries []RouteEntry) []Conflict {
+ root := newTrieNode()
+ var conflicts []Conflict
+ seen := map[string]bool{}
+
+ for i := range entries {
+ entry := entries[i]
+ normPath := normalizePath(entry.Path)
+ newSegments := splitSegments(normPath)
+
+ curr := root
+ for idx, seg := range newSegments {
+ if isParamSegment(seg) {
+ reportParamVsLiterals(&conflicts, seen, curr, entry, newSegments, seg)
+ reportParamVsParam(&conflicts, seen, curr, entry, newSegments, idx, seg)
+
+ if curr.paramChild == nil {
+ curr.paramChild = newTrieNode()
+ }
+ curr = curr.paramChild
+ continue
+ }
+
+ // literal segment
+ reportLiteralVsParam(&conflicts, seen, curr, entry, newSegments, idx, seg)
+ next := curr.literalChildren[seg]
+ if next == nil {
+ next = newTrieNode()
+ curr.literalChildren[seg] = next
+ }
+ curr = next
+ }
+
+ // endpoint (method-aware)
+ existing := curr.endpoint[entry.Method]
+ if existing != nil {
+ addConflict(&conflicts, seen, entry, *existing, "duplicate method/path combination")
+ } else {
+ // register endpoint for this method
+ curr.endpoint[entry.Method] = &entries[i]
+ }
+ }
+
+ inPlaceSortConflicts(conflicts)
+ return conflicts
+}
+
+func normalizePath(p string) string {
+ if p == "" {
+ return "/"
+ }
+ if !strings.HasPrefix(p, "/") {
+ p = "/" + p
+ }
+ for strings.Contains(p, "//") {
+ p = strings.ReplaceAll(p, "//", "/")
+ }
+ if len(p) > 1 && strings.HasSuffix(p, "/") {
+ p = strings.TrimRight(p, "/")
+ }
+ return p
+}
+
+func splitSegments(p string) []string {
+ if p == "/" {
+ return []string{}
+ }
+ p = strings.TrimPrefix(p, "/")
+ if p == "" {
+ return []string{}
+ }
+ return strings.Split(p, "/")
+}
+
+func isParamSegment(seg string) bool {
+ return strings.HasPrefix(seg, "{") && strings.HasSuffix(seg, "}")
+}
+
+func trimParamName(seg string) string {
+ return strings.Trim(seg, "{}")
+}
+
+// patternsConflict returns true iff two templates can match the same concrete path.
+// Requires same number of segments; at each position either equal literals or at
+// least one parameter segment.
+func patternsConflict(a, b []string) bool {
+ if len(a) != len(b) {
+ return false
+ }
+ for i := range a {
+ aSeg, bSeg := a[i], b[i]
+ if aSeg == bSeg {
+ continue
+ }
+ if isParamSegment(aSeg) || isParamSegment(bSeg) {
+ continue
+ }
+ // different literals -> cannot match same concrete path
+ return false
+ }
+ return true
+}
+
+// collectEndpointsByMethod traverses the subtree rooted at n and returns
+// registered endpoints that have the same HTTP method as requested.
+func collectEndpointsByMethod(n *trieNode, method string) []*RouteEntry {
+ var out []*RouteEntry
+ var dfs func(*trieNode)
+ dfs = func(cur *trieNode) {
+ if cur == nil {
+ return
+ }
+ if cur.endpoint != nil {
+ if ep := cur.endpoint[method]; ep != nil {
+ out = append(out, ep)
+ }
+ }
+ for _, child := range cur.literalChildren {
+ dfs(child)
+ }
+ if cur.paramChild != nil {
+ dfs(cur.paramChild)
+ }
+ }
+ dfs(n)
+ return out
+}
+
+func reportParamVsLiterals(
+ conflicts *[]Conflict,
+ seen map[string]bool,
+ curr *trieNode,
+ entry RouteEntry,
+ newSegments []string,
+ seg string,
+) {
+
+ if curr == nil {
+ return
+ }
+
+ for lit, litNode := range curr.literalChildren {
+ for _, ep := range collectEndpointsByMethod(litNode, entry.Method) {
+ epSegments := splitSegments(normalizePath(ep.Path))
+ if !patternsConflict(newSegments, epSegments) {
+ continue
+ }
+ reason := fmt.Sprintf(
+ "parameter %q in path '%s %s' conflicts with literal %q in path '%s %s'",
+ seg,
+ entry.Method,
+ entry.Path,
+ lit,
+ ep.Method,
+ ep.Path,
+ )
+ addConflict(conflicts, seen, entry, *ep, reason)
+ }
+ }
+}
+
+func reportParamVsParam(
+ conflicts *[]Conflict,
+ seen map[string]bool,
+ curr *trieNode,
+ entry RouteEntry,
+ newSegments []string,
+ idx int,
+ seg string,
+) {
+
+ if curr == nil || curr.paramChild == nil {
+ return
+ }
+ for _, ep := range collectEndpointsByMethod(curr.paramChild, entry.Method) {
+ epSegments := splitSegments(normalizePath(ep.Path))
+ if !patternsConflict(newSegments, epSegments) {
+ continue
+ }
+ reason := fmt.Sprintf(
+ "parameter %q in path '%s %s' conflicts with parameter %q in path '%s %s'",
+ seg,
+ entry.Method,
+ entry.Path,
+ epSegments[idx],
+ ep.Method,
+ ep.Path,
+ )
+ addConflict(conflicts, seen, entry, *ep, reason)
+ }
+}
+
+func reportLiteralVsParam(
+ conflicts *[]Conflict,
+ seen map[string]bool,
+ curr *trieNode,
+ entry RouteEntry,
+ newSegments []string,
+ idx int,
+ seg string,
+) {
+
+ if curr == nil || curr.paramChild == nil {
+ return
+ }
+ for _, ep := range collectEndpointsByMethod(curr.paramChild, entry.Method) {
+ epSegments := splitSegments(normalizePath(ep.Path))
+ if !patternsConflict(newSegments, epSegments) {
+ continue
+ }
+ reason := fmt.Sprintf(
+ "literal %q in path \"%s %s\" conflicts with parameter %q of in path '%s %s'",
+ seg,
+ entry.Method,
+ entry.Path,
+ epSegments[idx],
+ ep.Method,
+ ep.Path,
+ )
+ addConflict(conflicts, seen, entry, *ep, reason)
+ }
+}
+
+func addConflict(out *[]Conflict, seen map[string]bool, a RouteEntry, b RouteEntry, reason string) {
+ aPath, bPath := a.Path, b.Path
+ // canonical order
+ if aPath > bPath {
+ aPath, bPath = bPath, aPath
+ a, b = b, a
+ }
+ key := aPath + "||" + bPath + "||" + reason
+ if seen[key] {
+ return
+ }
+ seen[key] = true
+ *out = append(*out, Conflict{A: a, B: b, Reason: reason})
+}
+
+func inPlaceSortConflicts(conflicts []Conflict) []Conflict {
+ slices.SortStableFunc(conflicts, func(a, b Conflict) int {
+ if a.A.Path == b.A.Path {
+ if a.B.Path == b.B.Path {
+ if a.Reason == b.Reason {
+ return 0
+ }
+ if a.Reason < b.Reason {
+ return -1
+ }
+ return 1
+ }
+ if a.B.Path < b.B.Path {
+ return -1
+ }
+ return 1
+ }
+
+ if a.A.Path < b.A.Path {
+ return -1
+ }
+
+ return 1
+ })
+
+ return conflicts
+}
diff --git a/core/validators/route.go b/core/validators/receiver.validator.go
similarity index 80%
rename from core/validators/route.go
rename to core/validators/receiver.validator.go
index 9e0ebe7..9ce6f5f 100644
--- a/core/validators/route.go
+++ b/core/validators/receiver.validator.go
@@ -86,26 +86,26 @@ func (v ReceiverValidator) validateParams(receiver *metadata.ReceiverMeta) ([]di
continue
}
- passedIn, diag, err := v.getPassedInValue(param)
+ passedIn, err := v.getPassedInValue(param)
if err != nil {
return diags, err
}
- common.AppendIfNotNil(diags, diag)
if passedIn == nil {
// If we couldn't process the passedIn portion, no reason in continuing.
- // An error or error diagnostic will have been added, at this point.
+ // An error or error diagnostic will have been added, at this point
+ // or later on by the annotation link validator.
continue
}
switch *passedIn {
case definitions.PassedInBody:
- common.AppendIfNotNil(diags, v.validateBodyParam(receiver, param))
+ diags = common.AppendIfNotNil(diags, v.validateBodyParam(receiver, param))
default:
- common.AppendIfNotNil(diags, v.validatePrimitiveParam(receiver, param, *passedIn))
+ diags = common.AppendIfNotNil(diags, v.validateNonBodyParam(receiver, param, *passedIn))
}
- common.AppendIfNotNil(diags, v.validateParamsCombinations(processedParams, param, *passedIn))
+ diags = common.AppendIfNotNil(diags, v.validateParamsCombinations(processedParams, param, *passedIn))
processedParams = append(processedParams, funcParamEx{FuncParam: param, PassedIn: *passedIn})
}
@@ -113,37 +113,26 @@ func (v ReceiverValidator) validateParams(receiver *metadata.ReceiverMeta) ([]di
return diags, nil
}
-func (v ReceiverValidator) getPassedInValue(param metadata.FuncParam) (
- *definitions.ParamPassedIn,
- *diagnostics.ResolvedDiagnostic,
- error,
-) {
+func (v ReceiverValidator) getPassedInValue(param metadata.FuncParam) (*definitions.ParamPassedIn, error) {
// This function gets the parameter's passed-in value (e.g. passed-in-body or passed-in-header)
// If it fails, it may return a standard error or an InvalidAnnotation error.
passedIn, err := metadata.GetParamPassedIn(param.Name, param.Annotations)
if err == nil {
- return &passedIn, nil, nil
+ return &passedIn, nil
}
// If we didn't get a value, it generally means a missing annotation which is a 'diagnostic'
// or an outright malformed one which we consider an 'error'.
// InvalidAnnotation error is the former.
+ // In such a case, we return a nil here to halt further checks.
+ //
+ // The relevant diagnostic will be emitted by a subsequent call to validators.AnnotationLinkValidator
if _, isInvalidAnnotationErr := err.(metadata.InvalidAnnotationError); isInvalidAnnotationErr {
- diag := diagnostics.NewErrorDiagnostic(
- v.receiver.Annotations.FileName(),
- fmt.Sprintf(
- "Parameter '%s' in receiver '%s' is not referenced by any annotation",
- param.Name,
- v.receiver.Name,
- ),
- diagnostics.DiagLinkerUnreferencedParameter,
- v.receiver.RetValsRange(),
- )
- return nil, &diag, nil
+ return nil, nil
}
// A true error or a grossly malformed annotation. Regardless, this is a flow-terminating error.
- return nil, nil, fmt.Errorf(
+ return nil, fmt.Errorf(
"failed to determine 'passed-in' type for parameter '%s' in receiver '%s' - %w",
param.Name,
v.receiver.Name,
@@ -155,21 +144,15 @@ func (v ReceiverValidator) validateBodyParam(
receiver *metadata.ReceiverMeta,
param metadata.FuncParam,
) *diagnostics.ResolvedDiagnostic {
- // Verify the body is a struct
- if param.SymbolKind != common.SymKindStruct {
- nameInSchema, err := metadata.GetParameterSchemaName(param.Name, param.Annotations)
- if err != nil {
- nameInSchema = "unknown"
- }
-
+ // Currently, body parameters cannot be a non-array/slice primitive/built-in special
+ if param.Type.SymbolKind.IsBuiltin() && !param.Type.IsIterable() {
diag := diagnostics.NewErrorDiagnostic(
receiver.Annotations.FileName(),
fmt.Sprintf(
- "body parameters must be structs but '%s' (schema name '%s', type '%s') is of kind '%s'",
+ "body parameter '%s' (schema name '%s', type '%s') is a built-in primitive or special (e.g. time.Time) which is not allowed",
param.Name,
- nameInSchema,
+ getParamSchemaNameOrFallback(param, "unknown"),
param.Type.Name,
- param.Type.SymbolKind,
),
diagnostics.DiagReceiverInvalidBody,
param.Range,
@@ -180,32 +163,54 @@ func (v ReceiverValidator) validateBodyParam(
return nil
}
-func (v ReceiverValidator) validatePrimitiveParam(
+func (v ReceiverValidator) validateNonBodyParam(
receiver *metadata.ReceiverMeta,
param metadata.FuncParam,
passedIn definitions.ParamPassedIn,
) *diagnostics.ResolvedDiagnostic {
+ // First - any arrays in anything other than query
+ // (remember that this validates non-body parameters)
+ // is automatically invalid - Neither URL parameters
+ if param.Type.IsIterable() && passedIn != definitions.PassedInQuery && passedIn != definitions.PassedInBody {
+ diag := diagnostics.NewErrorDiagnostic(
+ receiver.Annotations.FileName(),
+ fmt.Sprintf(
+ "parameter '%s' (schema name '%s', type '%s') is an array/slice and can only be passed in a query or a body",
+ param.Name,
+ getParamSchemaNameOrFallback(param, "unknown"),
+ param.Type.Name,
+ ),
+ diagnostics.DiagReceiverParamNotPrimitive,
+ param.Range,
+ )
+ return &diag
+ }
+
isErrType := param.Type.PkgPath == "" && param.Type.Name == "error"
isMapType := param.Type.PkgPath == "" && strings.HasPrefix(param.Type.Name, "map[")
isAnEnum := param.Type.SymbolKind == common.SymKindEnum
- if (param.Type.IsUniverseType() || isAnEnum) && !isErrType && !isMapType {
+ isAnAlias, isAPrimitiveAlias := isPrimitiveAlias(param)
+
+ if (param.Type.IsUniverseType() || isAnEnum || (isAnAlias && isAPrimitiveAlias)) && !isErrType && !isMapType {
return nil
}
- nameInSchema, err := metadata.GetParameterSchemaName(param.Name, param.Annotations)
- if err != nil {
- nameInSchema = "unknown"
+ isIterableMsg := ""
+ if param.Type.IsIterable() {
+ isIterableMsg = "an iterable "
}
+
diag := diagnostics.NewErrorDiagnostic(
receiver.Annotations.FileName(),
fmt.Sprintf(
- "header, path and query parameters are currently limited to primitives only but "+
- "%s parameter '%s' (schema name '%s', type '%s') is of kind '%s'",
+ "header/path/query parameters may only be primitives but "+
+ "%s parameter '%s' (schema name '%s', type '%s') is %sof kind '%s'",
passedIn,
param.Name,
- nameInSchema,
+ getParamSchemaNameOrFallback(param, "unknown"),
param.Type.Name,
+ isIterableMsg,
param.Type.SymbolKind,
),
diagnostics.DiagReceiverParamNotPrimitive,
@@ -403,3 +408,28 @@ func getDiagForRetSig(receiver *metadata.ReceiverMeta) (int, *diagnostics.Resolv
return errorRetTypeIndex, nil
}
+
+func getParamSchemaNameOrFallback(param metadata.FuncParam, fallback string) string {
+ nameInSchema, err := metadata.GetParameterSchemaName(param.Name, param.Annotations)
+ if err != nil {
+ return fallback
+ }
+ return nameInSchema
+}
+
+func isPrimitiveAlias(param metadata.FuncParam) (bool, bool) {
+ if param.Type.SymbolKind != common.SymKindAlias {
+ return false, false
+ }
+
+ flattenedTypeRef := param.Type.Root.Flatten()
+
+ switch len(flattenedTypeRef) {
+ case 0:
+ return true, true
+ case 1:
+ return true, flattenedTypeRef[0].Kind() == metadata.TypeRefKindNamed
+ default:
+ return true, flattenedTypeRef[len(flattenedTypeRef)-1].Kind() == metadata.TypeRefKindNamed
+ }
+}
diff --git a/core/visitors/alias.visitor.go b/core/visitors/alias.visitor.go
new file mode 100644
index 0000000..eefa1df
--- /dev/null
+++ b/core/visitors/alias.visitor.go
@@ -0,0 +1,315 @@
+package visitors
+
+import (
+ "fmt"
+ "go/ast"
+
+ "golang.org/x/tools/go/packages"
+
+ "github.com/gopher-fleece/gleece/common"
+ "github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/core/metadata/typeref"
+ "github.com/gopher-fleece/gleece/gast"
+ "github.com/gopher-fleece/gleece/graphs"
+ "github.com/gopher-fleece/gleece/graphs/symboldg"
+)
+
+// AliasVisitor specializes in 'Alias'-type AST structures.
+//
+// It parses declarations like
+//
+// type Foo = Bar
+//
+// and stores the HIR in the graph for further use.
+type AliasVisitor struct {
+ BaseVisitor
+
+ declVisitor *TypeDeclVisitor
+ typeUsageVisitor *TypeUsageVisitor
+}
+
+// NewAliasVisitor constructs an AliasVisitor.
+func NewAliasVisitor(ctx *VisitContext) (*AliasVisitor, error) {
+ v := &AliasVisitor{}
+ if err := v.initialize(ctx); err != nil {
+ return nil, err
+ }
+ return v, nil
+}
+
+func (v *AliasVisitor) setTypeDeclVisitor(visitor *TypeDeclVisitor) {
+ v.declVisitor = visitor
+}
+
+func (v *AliasVisitor) setTypeUsageVisitor(visitor *TypeUsageVisitor) {
+ v.typeUsageVisitor = visitor
+}
+
+// VisitAlias attempts to parse the given GenDecl & TypeSpec as a type alias
+//
+// For our purposes, both forms of aliasing are valid and equivalent:
+//
+// type A = string
+// type A string
+//
+// both result in the same HIR branch structure and are eventually reduced to the same output.
+func (v *AliasVisitor) VisitAlias(
+ pkg *packages.Package,
+ file *ast.File,
+ genDecl *ast.GenDecl,
+ spec *ast.TypeSpec,
+) (graphs.SymbolKey, error) {
+ specName := gast.GetIdentNameOrFallback(spec.Name, "N/A")
+ v.enterFmt("VisitAlias %s in %s", specName, pkg.PkgPath)
+ defer v.exit()
+
+ // Parse the spec into metadata structures
+ aliasMeta, err := v.parseAliasSpec(pkg, file, genDecl, spec)
+ if err != nil {
+ return graphs.SymbolKey{}, v.getFrozenError("failed to visit alias '%s' - %w", specName, err)
+ }
+
+ // Add the alias to the graph
+ created, err := v.context.Graph.AddAlias(symboldg.CreateAliasNode{
+ Data: aliasMeta,
+ Annotations: aliasMeta.Annotations,
+ })
+
+ if err != nil {
+ return graphs.SymbolKey{}, v.getFrozenError(
+ "failed to add alias '%s' to the symbol graph - %w",
+ aliasMeta.Name,
+ err,
+ )
+ }
+
+ if v.context.MetadataCache.AddAlias(&aliasMeta) != nil {
+ return graphs.SymbolKey{}, v.getFrozenError(
+ "failed to add alias '%s' to the metadata cache - %w",
+ aliasMeta.Name,
+ err,
+ )
+ }
+
+ // Derive a canonical symbol key for the alias's underlying type
+ targetKey, err := aliasMeta.Type.Root.ToSymKey(aliasMeta.Type.FVersion)
+ if err != nil {
+ return graphs.SymbolKey{}, v.getFrozenError(
+ "failed to obtain SymKey for alias metadata '%s' - %v",
+ aliasMeta.Name,
+ err,
+ )
+ }
+
+ // Add an edge from the newly added Alias node to the underlying type
+ v.context.Graph.AddEdge(created.Id, targetKey, symboldg.EdgeKindType, nil)
+
+ // Return the new alias node's ID so the caller can add their own edges to it
+ return created.Id, nil
+}
+
+func (v *AliasVisitor) parseAliasSpec(
+ pkg *packages.Package,
+ file *ast.File,
+ genDecl *ast.GenDecl,
+ spec *ast.TypeSpec,
+) (metadata.AliasMeta, error) {
+ specName := gast.GetIdentNameOrFallback(spec.Name, "N/A")
+
+ fileVersion, fvErr := v.context.MetadataCache.GetFileVersion(file, pkg.Fset)
+ if fvErr != nil || fileVersion == nil {
+ return metadata.AliasMeta{}, v.getFrozenError(
+ "could not obtain file version for alias '%s' - %w",
+ specName,
+ fvErr,
+ )
+ }
+
+ holder, err := v.getAnnotations(spec.Doc, genDecl)
+ if err != nil {
+ return metadata.AliasMeta{}, fmt.Errorf(
+ "failed to obtain annotations for alias '%s' - %v",
+ specName,
+ err,
+ )
+ }
+
+ // Resolve the right-hand-side (RHS) of the expression
+ typeUsage, err := v.resolveTypeDeclRhs(pkg, file, spec)
+ if err != nil {
+ return metadata.AliasMeta{}, v.getFrozenError(
+ "failed to resolve RHS expression for TypeSpec '%s' - %w",
+ specName,
+ err,
+ )
+ }
+
+ aliasKind := common.Ternary(spec.Assign != 0, metadata.AliasKindAssigned, metadata.AliasKindTypedef)
+
+ return metadata.AliasMeta{
+ SymNodeMeta: metadata.SymNodeMeta{
+ Name: spec.Name.Name,
+ Node: spec,
+ SymbolKind: common.SymKindAlias,
+ PkgPath: pkg.PkgPath,
+ FVersion: fileVersion,
+ Annotations: holder,
+ Range: common.ResolveNodeRange(pkg.Fset, spec),
+ },
+ AliasType: aliasKind,
+ Type: typeUsage,
+ }, nil
+}
+
+// resolveTypeDeclRhs attempts to resolve the right-hand-side of an alias type declaration,
+//
+// i.e., 'string' in
+//
+// type A string
+func (v *AliasVisitor) resolveTypeDeclRhs(
+ pkg *packages.Package,
+ file *ast.File,
+ spec *ast.TypeSpec,
+) (metadata.TypeUsageMeta, error) {
+ switch spec.Type.(type) {
+ case *ast.Ident, *ast.SelectorExpr:
+ // An Ident or Selector imply a a primitive or an imported type
+ return v.resolveAliasRhsIdentOrSelector(pkg, file, spec.Type)
+ default:
+ // For composite RHS (map[], []T, func(...), inline struct...), use the generic TypeUsageVisitor
+ // to get a complete TypeUsageMeta and materialize any pre-requisite declared nodes
+ return v.typeUsageVisitor.VisitExpr(pkg, file, spec.Type, nil)
+ }
+}
+
+// resolveAliasRhsIdentOrSelector resolves a RHS that is an Ident or Selector.
+// Returns a fully-populated TypeUsageMeta representing the aliased type (and materializes declared targets).
+func (v *AliasVisitor) resolveAliasRhsIdentOrSelector(
+ pkg *packages.Package,
+ file *ast.File,
+ expr ast.Expr,
+) (metadata.TypeUsageMeta, error) {
+ resolution, ok, resolveErr := gast.ResolveNamedType(
+ pkg,
+ file,
+ expr,
+ v.context.ArbitrationProvider.Pkg().GetPackage,
+ )
+ if resolveErr != nil {
+ return metadata.TypeUsageMeta{}, v.getFrozenError(
+ "failed to resolve named type for expression '%v' - %w",
+ expr,
+ resolveErr,
+ )
+ }
+ if !ok {
+ return metadata.TypeUsageMeta{}, fmt.Errorf("could not resolve aliased type expression")
+ }
+
+ // Built-ins have an easier materialization flow.
+ if resolution.IsBuiltin() {
+ return v.processBuiltinTypeResolution(resolution)
+ }
+
+ // If the resolved type isn't a built-in and doesn't have an associated file/package/spec,
+ // something has gone wrong during resolution and we must halt.
+ if resolution.DeclaringAstFile == nil || resolution.DeclaringPackage == nil || resolution.TypeSpec == nil {
+ return metadata.TypeUsageMeta{}, v.getFrozenError(
+ "incomplete declaration context for aliased type %s",
+ resolution.TypeName,
+ )
+ }
+
+ targetFileVersion, fvErr := v.context.MetadataCache.GetFileVersion(
+ resolution.DeclaringAstFile,
+ resolution.DeclaringPackage.Fset,
+ )
+ if fvErr != nil {
+ return metadata.TypeUsageMeta{}, v.getFrozenError(
+ "could not obtain file version for '%s' - %w",
+ gast.GetAstFileName(resolution.DeclaringPackage.Fset, resolution.DeclaringAstFile),
+ fvErr,
+ )
+ }
+ if targetFileVersion == nil {
+ return metadata.TypeUsageMeta{}, v.getFrozenError(
+ "missing file version for declaring file of aliased type %s",
+ resolution.TypeName,
+ )
+ }
+
+ targetKey := graphs.NewSymbolKey(resolution.TypeSpec, targetFileVersion)
+
+ // Ask declVisitor to materialize if needed
+ if !v.context.MetadataCache.HasVisited(targetKey) {
+ if v.declVisitor == nil {
+ return metadata.TypeUsageMeta{}, v.getFrozenError(
+ "declaration for %s not materialized and no materializer provided",
+ resolution.TypeName,
+ )
+ }
+ if _, err := v.declVisitor.EnsureDeclMaterialized(
+ resolution.DeclaringPackage,
+ resolution.DeclaringAstFile,
+ resolution.GenDecl,
+ resolution.TypeSpec,
+ ); err != nil {
+ return metadata.TypeUsageMeta{}, err
+ }
+ }
+
+ // sanity: target node must now exist
+ if !v.context.Graph.Exists(targetKey) {
+ return metadata.TypeUsageMeta{}, fmt.Errorf("aliased declarative target %s materialized but not present in graph", targetKey.Id())
+ }
+
+ // Build TypeUsageMeta whose Root is a NamedTypeRef pointing to the materialized targetKey.
+ named := typeref.NewNamedTypeRef(&targetKey, nil)
+ tu := metadata.TypeUsageMeta{
+ SymNodeMeta: metadata.SymNodeMeta{
+ Name: resolution.TypeName,
+ Node: nil,
+ FVersion: targetFileVersion,
+ },
+ Root: &named,
+ }
+ return tu, nil
+}
+
+// processBuiltinTypeResolution inserts the given built-in type's resolution and inserts it into
+// the symbol graph (as either a 'primitive' or a 'special') and returns a new TypeUsageMeta
+// fitting the resolution
+func (v *AliasVisitor) processBuiltinTypeResolution(resolution gast.TypeSpecResolution) (metadata.TypeUsageMeta, error) {
+ qualifiedTypeName := resolution.GetQualifiedName()
+
+ var symKind common.SymKind
+ var symKey graphs.SymbolKey
+
+ if prim, isPrim := common.ToPrimitiveType(qualifiedTypeName); isPrim {
+ node := v.context.Graph.AddPrimitive(prim)
+ symKind = common.SymKindBuiltin
+ symKey = node.Id
+ } else if sp, isSp := common.ToSpecialType(qualifiedTypeName); isSp {
+ node := v.context.Graph.AddSpecial(sp)
+
+ symKind = common.SymKindSpecialBuiltin
+ symKey = node.Id
+ } else {
+ return metadata.TypeUsageMeta{}, fmt.Errorf(
+ "type resolution named '%s' is a universe type but is neither a primitive nor a special",
+ qualifiedTypeName,
+ )
+ }
+
+ named := typeref.NewNamedTypeRef(&symKey, nil)
+ usage := metadata.TypeUsageMeta{
+ SymNodeMeta: metadata.SymNodeMeta{
+ Name: resolution.TypeName,
+ SymbolKind: symKind,
+ },
+ Import: common.ImportTypeNone,
+ Root: &named,
+ }
+
+ return usage, nil
+}
diff --git a/core/visitors/base.go b/core/visitors/base.go
index eabaa12..952fb64 100644
--- a/core/visitors/base.go
+++ b/core/visitors/base.go
@@ -8,9 +8,13 @@ import (
"strings"
"github.com/gopher-fleece/gleece/common"
+ "github.com/gopher-fleece/gleece/core/annotations"
"github.com/gopher-fleece/gleece/core/arbitrators/caching"
"github.com/gopher-fleece/gleece/core/visitors/providers"
+ "github.com/gopher-fleece/gleece/gast"
+ "github.com/gopher-fleece/gleece/graphs"
"github.com/gopher-fleece/gleece/infrastructure/logger"
+ "golang.org/x/tools/go/packages"
)
type Visitor interface {
@@ -72,16 +76,9 @@ func (v *BaseVisitor) initializeWithGlobs(context *VisitContext) error {
return err
}
- var globs []string
- if len(context.GleeceConfig.CommonConfig.ControllerGlobs) > 0 {
- globs = context.GleeceConfig.CommonConfig.ControllerGlobs
- } else {
- globs = []string{"./*.go", "./**/*.go"}
- }
-
v.context = context
- arbProvider, err := providers.NewArbitrationProvider(globs)
+ arbProvider, err := providers.NewArbitrationProviderFromGleeceConfig(context.GleeceConfig)
if err != nil {
return err
}
@@ -191,6 +188,60 @@ func (v *BaseVisitor) setLastError(err error) {
v.stackFrozen = true
}
+func (v *BaseVisitor) getAnnotations(
+ onNodeDoc *ast.CommentGroup,
+ nodeGenDecl *ast.GenDecl,
+) (*annotations.AnnotationHolder, error) {
+ v.enter("Obtaining annotations for comment group")
+ defer v.exit()
+
+ var commentSource *ast.CommentGroup
+ if onNodeDoc != nil {
+ commentSource = onNodeDoc
+ } else {
+ if nodeGenDecl != nil {
+ commentSource = nodeGenDecl.Doc
+ }
+ }
+
+ if commentSource != nil {
+ comments := gast.MapDocListToCommentBlock(commentSource.List, v.context.ArbitrationProvider.Pkg().FSet())
+ holder, err := annotations.NewAnnotationHolder(comments, annotations.CommentSourceController)
+ return &holder, err
+ }
+
+ return nil, nil
+}
+
+// tryGetSymKeyForSpecial gets a SymbolKey for the given spec, if it's a 'special', otherwise returns nil
+func (v *TypeUsageVisitor) tryGetSymKeyForSpecial(
+ pkg *packages.Package,
+ spec *ast.TypeSpec,
+ typeName string,
+) *graphs.SymbolKey {
+ var pkgPath string
+ if pkg != nil {
+ pkgPath = pkg.PkgPath
+ }
+
+ switch pkgPath {
+ case "context":
+ if spec != nil && spec.Name.Name == typeName && typeName == "Context" {
+ return common.Ptr(graphs.NewNonUniverseBuiltInSymbolKey("context.Context"))
+ }
+ case "time":
+ if spec != nil && spec.Name.Name == typeName && typeName == "Time" {
+ return common.Ptr(graphs.NewNonUniverseBuiltInSymbolKey("time.Time"))
+ }
+ case "":
+ if spec == nil && (typeName == "any" || typeName == "interface{}") {
+ return common.Ptr(graphs.NewUniverseSymbolKey(typeName))
+ }
+ }
+
+ return nil
+}
+
func contextInitGuard(context *VisitContext) error {
if context == nil {
return fmt.Errorf("nil context was given to contextInitGuard")
diff --git a/core/visitors/context.go b/core/visitors/context.go
index c1b5cd7..04e29fe 100644
--- a/core/visitors/context.go
+++ b/core/visitors/context.go
@@ -17,7 +17,7 @@ type VisitContext struct {
SyncedProvider *providers.SyncedProvider
// An interface used to build a symbol graph for the processed code
- GraphBuilder symboldg.SymbolGraphBuilder
+ Graph symboldg.SymbolGraphBuilder
// The project's configuration, as specified in the user's gleece.config.json
GleeceConfig *definitions.GleeceConfig
diff --git a/core/visitors/controller.go b/core/visitors/controller.visitor.go
similarity index 95%
rename from core/visitors/controller.go
rename to core/visitors/controller.visitor.go
index 78c4130..7030b98 100644
--- a/core/visitors/controller.go
+++ b/core/visitors/controller.visitor.go
@@ -33,6 +33,8 @@ type ControllerVisitor struct {
// A list of fully processed controller metadata, ready to be passed to the routes/spec generators
controllers []metadata.ControllerMeta
+
+ fieldVisitor *FieldVisitor
}
// NewControllerVisitor Instantiates a new Gleece Controller visitor.
@@ -42,6 +44,10 @@ func NewControllerVisitor(context *VisitContext) (*ControllerVisitor, error) {
return &visitor, err
}
+func (v *ControllerVisitor) setFieldVisitor(visitor *FieldVisitor) {
+ v.fieldVisitor = visitor
+}
+
// GetControllers returns all controllers known by this visitor.
// Note that the returned values are mutable.
// When used, care must be taken to not corrupt the internal state
@@ -88,9 +94,9 @@ func (v *ControllerVisitor) addSelfToGraph(meta metadata.ControllerMeta) error {
v.enter(fmt.Sprintf("Graph insertion - Controller %s", meta.Struct.Name))
defer v.exit()
- _, err := v.context.GraphBuilder.AddController(
+ _, err := v.context.Graph.AddController(
symboldg.CreateControllerNode{
- Data: meta.Struct,
+ Data: meta,
Annotations: meta.Struct.Annotations,
},
)
@@ -125,8 +131,10 @@ func (v *ControllerVisitor) visitController(controllerNode *ast.TypeSpec) (metad
}
routeVisitor, err := NewRouteVisitor(
- v.context, RouteParentContext{Controller: &controllerMeta},
+ v.context,
+ RouteParentContext{Controller: &controllerMeta},
)
+ routeVisitor.setFieldVisitor(v.fieldVisitor)
if err != nil {
logger.Error("Could not initialize a new route visitor - %v", err)
diff --git a/core/visitors/enum.visitor.go b/core/visitors/enum.visitor.go
new file mode 100644
index 0000000..7eeea75
--- /dev/null
+++ b/core/visitors/enum.visitor.go
@@ -0,0 +1,229 @@
+package visitors
+
+import (
+ "go/ast"
+ "go/types"
+
+ "golang.org/x/tools/go/packages"
+
+ "github.com/gopher-fleece/gleece/common"
+ "github.com/gopher-fleece/gleece/core/annotations"
+ "github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/gast"
+ "github.com/gopher-fleece/gleece/graphs"
+ "github.com/gopher-fleece/gleece/graphs/symboldg"
+)
+
+// EnumVisitor parses enum-like TypeSpecs and consts and materializes Enum nodes.
+type EnumVisitor struct {
+ BaseVisitor
+}
+
+// NewEnumVisitor constructs an EnumVisitor.
+func NewEnumVisitor(ctx *VisitContext) (*EnumVisitor, error) {
+ v := &EnumVisitor{}
+ if err := v.initialize(ctx); err != nil {
+ return nil, err
+ }
+ return v, nil
+}
+
+// VisitEnumType analyzes a type spec that is expected to be an enum-like alias (e.g. type X string).
+// It returns the constructed EnumMeta and the graph SymbolKey for the created node.
+func (v *EnumVisitor) VisitEnumType(
+ pkg *packages.Package,
+ file *ast.File,
+ fileVersion *gast.FileVersion,
+ specGenDecl *ast.GenDecl,
+ spec *ast.TypeSpec,
+) (metadata.EnumMeta, graphs.SymbolKey, error) {
+ specName := gast.GetIdentNameOrFallback(spec.Name, "N/A")
+
+ v.enterFmt("Visiting enum %s.%s", pkg.PkgPath, specName)
+ defer v.exit()
+
+ // Cache check
+ symKey := graphs.NewSymbolKey(spec, fileVersion)
+ if cached := v.context.MetadataCache.GetEnum(symKey); cached != nil {
+ return *cached, symKey, nil
+ }
+
+ // Resolve the named type object (types.TypeName) for this declaration
+ typeName, err := gast.GetTypeNameOrError(pkg, spec.Name.Name)
+ if err != nil {
+ return metadata.EnumMeta{}, graphs.SymbolKey{}, err
+ }
+
+ // Extract enum metadata (value kind + values)
+ enumMeta, err := v.extractEnumAliasType(pkg, fileVersion, specGenDecl, spec, typeName)
+ if err != nil {
+ return metadata.EnumMeta{}, graphs.SymbolKey{}, err
+ }
+
+ // Cache it
+ v.context.MetadataCache.AddEnum(&enumMeta)
+
+ // Insert into graph
+ create := symboldg.CreateEnumNode{
+ Data: enumMeta,
+ Annotations: enumMeta.Annotations,
+ }
+ enumNode, err := v.context.Graph.AddEnum(create)
+ if err != nil {
+ return metadata.EnumMeta{}, graphs.SymbolKey{}, err
+ }
+
+ return enumMeta, enumNode.Id, nil
+}
+
+// extractEnumAliasType builds metadata.EnumMeta from a types.TypeName (must be alias to a basic type).
+func (v *EnumVisitor) extractEnumAliasType(
+ enumPkg *packages.Package,
+ enumFVersion *gast.FileVersion,
+ specGenDecl *ast.GenDecl,
+ spec *ast.TypeSpec,
+ typeName *types.TypeName,
+) (metadata.EnumMeta, error) {
+ v.enterFmt("Extracting metadata for enum %s.%s", enumPkg.PkgPath, typeName.Name())
+ defer v.exit()
+
+ // Underlying must be a basic type (string/int/...)
+ basic, ok := typeName.Type().Underlying().(*types.Basic)
+ if !ok {
+ return metadata.EnumMeta{}, NewUnexpectedEntityError(
+ common.SymKindEnum,
+ common.SymKindUnknown,
+ "type '%s' is not a basic type and therefore not an enum",
+ typeName.Name(),
+ )
+ }
+
+ kind, err := metadata.NewEnumValueKind(basic.Kind())
+ if err != nil {
+ return metadata.EnumMeta{}, NewUnexpectedEntityError(
+ common.SymKindEnum,
+ common.SymKindUnknown,
+ "type '%s' has an underlying basic kind '%v' that is not enum-compatible and is therefore not an enum",
+ typeName.Name(),
+ basic.Kind(),
+ )
+ }
+
+ // Annotations (from TypeSpec doc or parent GenDecl)
+ enumAnnotations, err := v.getAnnotations(spec.Doc, specGenDecl)
+ if err != nil {
+ return metadata.EnumMeta{}, err
+ }
+
+ // Gather values from package scope
+ enumValues, err := v.getEnumValueDefinitions(enumFVersion, enumPkg, typeName, basic)
+ if err != nil {
+ return metadata.EnumMeta{}, err
+ }
+
+ if len(enumValues) <= 0 {
+ return metadata.EnumMeta{}, NewUnexpectedEntityError(
+ common.SymKindEnum,
+ common.SymKindUnknown,
+ "type '%s' does not appear to have 'values' in the same file and is therefore not an enum",
+ typeName.Name(),
+ )
+ }
+
+ enum := metadata.EnumMeta{
+ SymNodeMeta: metadata.SymNodeMeta{
+ Name: typeName.Name(),
+ Node: spec,
+ SymbolKind: common.SymKindEnum,
+ PkgPath: enumPkg.PkgPath,
+ FVersion: enumFVersion,
+ Annotations: enumAnnotations,
+ Range: common.ResolveNodeRange(enumPkg.Fset, spec),
+ },
+ ValueKind: kind,
+ Values: enumValues,
+ }
+
+ return enum, nil
+}
+
+// getEnumValueDefinitions scans the package scope and returns all consts of the given named enum type.
+func (v *EnumVisitor) getEnumValueDefinitions(
+ enumFVersion *gast.FileVersion,
+ enumPkg *packages.Package,
+ enumTypeName *types.TypeName,
+ enumBasic *types.Basic,
+) ([]metadata.EnumValueDefinition, error) {
+
+ v.enterFmt("Obtaining enum value definitions for %s.%s", enumPkg.PkgPath, enumTypeName.Name())
+ defer v.exit()
+
+ out := []metadata.EnumValueDefinition{}
+
+ scope := enumPkg.Types.Scope()
+ if scope == nil {
+ return out, nil
+ }
+
+ for _, name := range scope.Names() {
+ obj := scope.Lookup(name)
+ constObj, ok := obj.(*types.Const)
+ if !ok {
+ continue
+ }
+
+ // To avoid clobbering consts that happen to have the same type under aliases/enums,
+ // we check the actual underlying type name as well.
+ if curObjAlias, isCurObjAlias := constObj.Type().(*types.Alias); isCurObjAlias {
+ if enumTypeName.Name() != curObjAlias.Obj().Name() {
+ continue
+ }
+ }
+
+ // ensure the const's type exactly matches the enum type
+ if !types.Identical(enumTypeName.Type(), constObj.Type()) {
+ continue
+ }
+
+ // extract a stable value using your helper (returns a stringable info)
+ val := gast.ExtractConstValue(enumBasic.Kind(), constObj)
+ if val == nil {
+ // skip consts we cannot represent
+ continue
+ }
+
+ // try to find the AST node (ValueSpec) for this const if possible
+ var constNode ast.Node
+ if v.context != nil && v.context.ArbitrationProvider != nil {
+ constNode = gast.FindConstSpecNode(enumPkg, constObj.Name())
+ }
+
+ // attempt to extract annotations for individual enum value
+ var holder *annotations.AnnotationHolder
+ if constNode != nil {
+ comments := gast.GetCommentsFromNode(constNode, v.context.ArbitrationProvider.Pkg().FSet())
+ if h, err := annotations.NewAnnotationHolder(comments, annotations.CommentSourceProperty); err == nil {
+ holder = &h
+ } else {
+ return nil, err
+ }
+ }
+
+ ev := metadata.EnumValueDefinition{
+ SymNodeMeta: metadata.SymNodeMeta{
+ Name: constObj.Name(),
+ Node: constNode,
+ SymbolKind: common.SymKindEnumValue,
+ PkgPath: enumPkg.PkgPath,
+ Annotations: holder,
+ FVersion: enumFVersion,
+ Range: common.ResolveNodeRange(enumPkg.Fset, constNode),
+ },
+ Value: val,
+ }
+
+ out = append(out, ev)
+ }
+
+ return out, nil
+}
diff --git a/core/visitors/errors.go b/core/visitors/errors.go
new file mode 100644
index 0000000..9879135
--- /dev/null
+++ b/core/visitors/errors.go
@@ -0,0 +1,24 @@
+package visitors
+
+import (
+ "fmt"
+
+ "github.com/gopher-fleece/gleece/common"
+)
+
+// An error that indicates the given entity was not expected in this context.
+//
+// This error may be used to re-direct code flow between different heuristics, like with aliases and enums
+type UnexpectedEntityError struct {
+ error
+ Expected common.SymKind
+ Received common.SymKind
+}
+
+func NewUnexpectedEntityError(expected, received common.SymKind, messageFormat string, args ...any) UnexpectedEntityError {
+ return UnexpectedEntityError{
+ error: fmt.Errorf(messageFormat, args...),
+ Expected: expected,
+ Received: received,
+ }
+}
diff --git a/core/visitors/field.visitor.go b/core/visitors/field.visitor.go
new file mode 100644
index 0000000..5db881c
--- /dev/null
+++ b/core/visitors/field.visitor.go
@@ -0,0 +1,135 @@
+package visitors
+
+import (
+ "errors"
+ "fmt"
+ "go/ast"
+ "strings"
+
+ "github.com/gopher-fleece/gleece/common"
+ "github.com/gopher-fleece/gleece/core/annotations"
+ "github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/gast"
+ "github.com/gopher-fleece/gleece/graphs/symboldg"
+ "golang.org/x/tools/go/packages"
+)
+
+type FieldVisitor struct {
+ BaseVisitor
+
+ typeUsageVisitor *TypeUsageVisitor
+}
+
+// NewFieldVisitor Instantiates a new type visitor.
+func NewFieldVisitor(context *VisitContext) (*FieldVisitor, error) {
+ visitor := FieldVisitor{}
+ err := visitor.initialize(context)
+ if err != nil {
+ return nil, fmt.Errorf("failed to initialize a new FieldVisitor")
+ }
+
+ return &visitor, err
+}
+
+func (v *FieldVisitor) setTypeUsageVisitor(visitor *TypeUsageVisitor) {
+ v.typeUsageVisitor = visitor
+}
+
+// VisitField returns one metadata.FieldMeta per declared "name" in the AST field node.
+// The caller **must** pass the desired symbol kind (Parameter, RetType, Field).
+// For anonymous/embedded fields we always return a single FieldMeta with Name == "".
+// isEmbedded is set only when the AST declares an embedded field (len(field.Names)==0).
+func (v *FieldVisitor) VisitField(
+ pkg *packages.Package,
+ file *ast.File,
+ field *ast.Field,
+ kind common.SymKind,
+) ([]metadata.FieldMeta, error) {
+ if field == nil {
+ return nil, errors.New("nil field was provided to FieldVisitor.VisitField")
+ }
+
+ fileName := gast.GetAstFileNameOrFallback(file, nil)
+ v.enterFmt("VisitField in %s — kind=%s expr=%T", fileName, kind, field.Type)
+ defer v.exit()
+
+ annotationsHolder, err := v.getAnnotations(field.Doc, nil)
+ if err != nil {
+ return nil, v.frozenError(err)
+ }
+
+ // canonical name list: ensure at least one entry for anonymous/embedded fields and unnamed returns
+ names := gast.GetFieldNames(field)
+ if len(names) == 0 {
+ names = []string{""}
+ }
+
+ // delegate to type-usage visitor
+ typeUsage, err := v.typeUsageVisitor.VisitExpr(pkg, file, field.Type, nil)
+ if err != nil {
+ namesStr := "an anonymous field"
+ if len(names) > 0 && names[0] != "" {
+ namesStr = fmt.Sprintf("field/s [%s]", strings.Join(names, ", "))
+ }
+
+ return nil, v.frozenError(fmt.Errorf("cannot visit field type expression for %v - %w", namesStr, err))
+ }
+
+ fileVersion, err := v.context.MetadataCache.GetFileVersion(file, pkg.Fset)
+ if err != nil {
+ return nil, fmt.Errorf("failed to obtain FileVersion for '%s' - %v", fileName, err)
+ }
+
+ return v.buildFieldsMeta(
+ pkg,
+ field,
+ names,
+ kind,
+ annotationsHolder,
+ typeUsage,
+ fileVersion,
+ )
+}
+
+func (v *FieldVisitor) buildFieldsMeta(
+ pkg *packages.Package,
+ field *ast.Field,
+ names []string,
+ kind common.SymKind,
+ annotationsHolder *annotations.AnnotationHolder,
+ typeUsage metadata.TypeUsageMeta,
+ fileVersion *gast.FileVersion,
+) ([]metadata.FieldMeta, error) {
+ // *ast.Field here can be a parameter, return value, a named field or an embedded field.
+ isEmbedded := kind == common.SymKindField && gast.IsEmbeddedOrAnonymousField(field)
+
+ out := make([]metadata.FieldMeta, 0, len(names))
+ for _, name := range names {
+ fieldMeta := metadata.FieldMeta{
+ SymNodeMeta: metadata.SymNodeMeta{
+ Name: name,
+ Node: field,
+ SymbolKind: kind,
+ PkgPath: pkg.PkgPath,
+ Annotations: annotationsHolder,
+ FVersion: fileVersion,
+ Range: common.ResolveNodeRange(pkg.Fset, field),
+ },
+ Type: typeUsage,
+ IsEmbedded: isEmbedded,
+ }
+
+ // Graph insertion (GraphBuilder expected to de-dupe)
+ req := symboldg.CreateFieldNode{
+ Data: fieldMeta,
+ Annotations: annotationsHolder,
+ }
+ if _, err := v.context.Graph.AddField(req); err != nil {
+ return nil, v.frozenError(err)
+ }
+
+ out = append(out, fieldMeta)
+ }
+
+ return out, nil
+}
diff --git a/core/visitors/orchestrator.go b/core/visitors/orchestrator.go
new file mode 100644
index 0000000..e033369
--- /dev/null
+++ b/core/visitors/orchestrator.go
@@ -0,0 +1,153 @@
+package visitors
+
+import (
+ "errors"
+ "fmt"
+ "go/ast"
+)
+
+type VisitorOrchestrator struct {
+ ctx *VisitContext
+
+ typeDeclVisitor *TypeDeclVisitor
+ typeUsageVisitor *TypeUsageVisitor
+ structVisitor *StructVisitor
+ enumVisitor *EnumVisitor
+ fieldVisitor *FieldVisitor
+
+ controllerVisitor *ControllerVisitor
+}
+
+func NewVisitorOrchestrator(ctx *VisitContext) (*VisitorOrchestrator, error) {
+ err := validateContext(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ aliasVisitor, err := NewAliasVisitor(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to construct an AliasVisitor instance - %v", err)
+ }
+
+ typeDeclVisitor, err := NewTypeDeclVisitor(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to construct a TypeDeclVisitor instance - %v", err)
+ }
+
+ typeUsageVisitor, err := NewTypeUsageVisitor(ctx, true)
+ if err != nil {
+ return nil, fmt.Errorf("failed to construct a TypeUsageVisitor instance - %v", err)
+ }
+
+ nonMaterializingTypeUsageVisitor, err := NewTypeUsageVisitor(ctx, false)
+ if err != nil {
+ return nil, fmt.Errorf("failed to construct a non-materializing TypeUsageVisitor instance - %v", err)
+ }
+
+ structVisitor, err := NewStructVisitor(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to construct a StructVisitor instance - %v", err)
+ }
+
+ enumVisitor, err := NewEnumVisitor(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to construct an EnumVisitor instance - %v", err)
+ }
+
+ fieldVisitor, err := NewFieldVisitor(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to construct an FieldVisitor instance - %v", err)
+ }
+
+ controllerVisitor, err := NewControllerVisitor(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to construct a ControllerVisitor instance - %v", err)
+ }
+
+ // Need to re-think this whole thing. Maybe use the orchestrator itself for DI?
+
+ aliasVisitor.setTypeDeclVisitor(typeDeclVisitor)
+ aliasVisitor.setTypeUsageVisitor(typeUsageVisitor)
+
+ typeDeclVisitor.setStructVisitor(structVisitor)
+ typeDeclVisitor.setEnumVisitor(enumVisitor)
+ typeDeclVisitor.setAliasVisitor(aliasVisitor)
+
+ typeUsageVisitor.setDeclVisitor(typeDeclVisitor)
+
+ structVisitor.setTypeUsageVisitor(typeUsageVisitor)
+ structVisitor.setNonMaterializingTypeUsageVisitor(nonMaterializingTypeUsageVisitor)
+
+ fieldVisitor.setTypeUsageVisitor(typeUsageVisitor)
+
+ controllerVisitor.setFieldVisitor(fieldVisitor)
+
+ orchestrator := VisitorOrchestrator{
+ ctx: ctx,
+ controllerVisitor: controllerVisitor,
+ structVisitor: structVisitor,
+ fieldVisitor: fieldVisitor,
+ enumVisitor: enumVisitor,
+ typeUsageVisitor: typeUsageVisitor,
+ typeDeclVisitor: typeDeclVisitor,
+ }
+
+ return &orchestrator, nil
+}
+
+// Visit implements the AST Visitor interface via the internal ControllerVisitor
+func (o *VisitorOrchestrator) Visit(node ast.Node) ast.Visitor {
+ return o.controllerVisitor.Visit(node)
+}
+
+func (o *VisitorOrchestrator) GetAllSourceFiles() []*ast.File {
+ return o.ctx.ArbitrationProvider.GetAllSourceFiles()
+}
+
+// GetLastError retrieves the last visitor error via the the internal ControllerVisitor.
+// Note that errors are not diagnostics - they are breakages in the processing pipeline.
+func (o *VisitorOrchestrator) GetLastError() error {
+ return o.controllerVisitor.GetLastError()
+}
+
+// GetFormattedDiagnosticStack retrieves the current diagnostic call stack via the the internal ControllerVisitor.
+// This is used for outputting human-readable error information originating from deep within the visitor hierarchy
+func (o *VisitorOrchestrator) GetFormattedDiagnosticStack() string {
+ return o.controllerVisitor.GetFormattedDiagnosticStack()
+}
+
+func (o *VisitorOrchestrator) GetFieldVisitor() *FieldVisitor {
+ return o.fieldVisitor
+}
+
+func validateContext(ctx *VisitContext) error {
+ if ctx == nil {
+ return errors.New("nil context was provided to VisitorOrchestrator")
+ }
+
+ errs := []error{}
+ if ctx.ArbitrationProvider == nil {
+ errs = append(errs, errors.New("VisitContext does not have an arbitration provider"))
+ }
+
+ if ctx.GleeceConfig == nil {
+ errs = append(errs, errors.New("VisitContext does not have a Gleece Config"))
+ }
+
+ if ctx.Graph == nil {
+ errs = append(errs, errors.New("VisitContext does not have a graph builder"))
+ }
+
+ if ctx.MetadataCache == nil {
+ errs = append(errs, errors.New("VisitContext does not have a metadata cache"))
+ }
+
+ if ctx.SyncedProvider == nil {
+ errs = append(errs, errors.New("VisitContext does not have a synchronized provider"))
+ }
+
+ if len(errs) > 0 {
+ return errors.Join(errs...)
+ }
+ return nil
+}
diff --git a/core/visitors/providers/arbitration.go b/core/visitors/providers/arbitration.go
index 64a5254..c3d153e 100644
--- a/core/visitors/providers/arbitration.go
+++ b/core/visitors/providers/arbitration.go
@@ -4,6 +4,7 @@ import (
"go/ast"
"github.com/gopher-fleece/gleece/core/arbitrators"
+ "github.com/gopher-fleece/gleece/definitions"
)
type ArbitrationProvider struct {
@@ -21,8 +22,12 @@ func (p *ArbitrationProvider) Ast() *arbitrators.AstArbitrator {
return p.astArbitrator
}
-func NewArbitrationProvider(globs []string) (*ArbitrationProvider, error) {
- packagesFacade, err := arbitrators.NewPackagesFacade(globs)
+func NewArbitrationProviderFromGleeceConfig(gleeceConfig *definitions.GleeceConfig) (*ArbitrationProvider, error) {
+ return NewArbitrationProvider(NewArbitrationProviderConfig(gleeceConfig))
+}
+
+func NewArbitrationProvider(config ArbitrationProviderConfig) (*ArbitrationProvider, error) {
+ packagesFacade, err := arbitrators.NewPackagesFacade(config.PackageFacadeConfig)
if err != nil {
return nil, err
}
diff --git a/core/visitors/providers/configs.go b/core/visitors/providers/configs.go
new file mode 100644
index 0000000..7e28924
--- /dev/null
+++ b/core/visitors/providers/configs.go
@@ -0,0 +1,30 @@
+package providers
+
+import (
+ "github.com/gopher-fleece/gleece/core/arbitrators"
+ "github.com/gopher-fleece/gleece/definitions"
+)
+
+type ArbitrationProviderConfig struct {
+ arbitrators.PackageFacadeConfig
+}
+
+func NewArbitrationProviderConfig(gleeceConfig *definitions.GleeceConfig) ArbitrationProviderConfig {
+ globs := []string{"./*.go", "./**/*.go"}
+ allowPackageLoadFailures := false
+
+ if gleeceConfig != nil {
+ if len(gleeceConfig.CommonConfig.ControllerGlobs) > 0 {
+ globs = gleeceConfig.CommonConfig.ControllerGlobs
+ }
+
+ allowPackageLoadFailures = gleeceConfig.CommonConfig.AllowPackageLoadFailures
+ }
+
+ return ArbitrationProviderConfig{
+ PackageFacadeConfig: arbitrators.PackageFacadeConfig{
+ Globs: globs,
+ AllowPackageLoadFailures: allowPackageLoadFailures,
+ },
+ }
+}
diff --git a/core/visitors/route.go b/core/visitors/route.visitor.go
similarity index 88%
rename from core/visitors/route.go
rename to core/visitors/route.visitor.go
index 839d1a0..44349bf 100644
--- a/core/visitors/route.go
+++ b/core/visitors/route.visitor.go
@@ -44,7 +44,7 @@ type RouteVisitor struct {
parent RouteParentContext
- typeVisitor *RecursiveTypeVisitor
+ fieldVisitor *FieldVisitor
}
func NewRouteVisitor(
@@ -58,13 +58,28 @@ func NewRouteVisitor(
return &visitor, err
}
- typeVisitor, err := NewRecursiveTypeVisitor(visitor.context)
- if err != nil {
- return &visitor, err
+ return &visitor, err
+}
+
+func NewRouteVisitorFromVisitor(
+ context *VisitContext,
+ parent RouteParentContext,
+ fieldVisitor *FieldVisitor,
+) (*RouteVisitor, error) {
+ if fieldVisitor == nil {
+ return nil, fmt.Errorf("NewRouteVisitorFromVisitor constructor was given a nil FieldVisitor")
}
- visitor.typeVisitor = typeVisitor
- return &visitor, err
+ visitor, err := NewRouteVisitor(context, parent)
+ if err == nil {
+ visitor.setFieldVisitor(fieldVisitor)
+ }
+
+ return visitor, err
+}
+
+func (v *RouteVisitor) setFieldVisitor(visitor *FieldVisitor) {
+ v.fieldVisitor = visitor
}
// visitMethod Visits a controller route given as a FuncDecl and returns its metadata and whether it is an API endpoint
@@ -174,8 +189,8 @@ func (v *RouteVisitor) constructRouteMetadata(ctx executionContext) (*metadata.R
PkgPath: ctx.CurrentPkg.PkgPath,
Annotations: ctx.Annotations,
FVersion: ctx.FVersion,
- Range: common.ResolveNodeRange(ctx.CurrentPkg.Fset, ctx.FuncDecl),
// Range here encapsulates the entire function, from "func" to closing brace
+ Range: common.ResolveNodeRange(ctx.CurrentPkg.Fset, ctx.FuncDecl),
},
Params: params,
RetVals: retVals,
@@ -183,7 +198,7 @@ func (v *RouteVisitor) constructRouteMetadata(ctx executionContext) (*metadata.R
v.context.MetadataCache.AddReceiver(meta)
- _, err = v.context.GraphBuilder.AddRoute(
+ _, err = v.context.Graph.AddRoute(
symboldg.CreateRouteNode{
Data: meta,
ParentController: symboldg.KeyableNodeMeta{
@@ -198,7 +213,7 @@ func (v *RouteVisitor) constructRouteMetadata(ctx executionContext) (*metadata.R
}
for _, param := range params {
- v.context.GraphBuilder.AddRouteParam(symboldg.CreateParameterNode{
+ v.context.Graph.AddRouteParam(symboldg.CreateParameterNode{
Data: param,
ParentRoute: symboldg.KeyableNodeMeta{
Decl: meta.Node,
@@ -208,7 +223,7 @@ func (v *RouteVisitor) constructRouteMetadata(ctx executionContext) (*metadata.R
}
for _, retVal := range retVals {
- v.context.GraphBuilder.AddRouteRetVal(symboldg.CreateReturnValueNode{
+ v.context.Graph.AddRouteRetVal(symboldg.CreateReturnValueNode{
Data: retVal,
ParentRoute: symboldg.KeyableNodeMeta{
Decl: meta.Node,
@@ -225,7 +240,7 @@ func (v *RouteVisitor) getFuncParams(ctx executionContext) ([]metadata.FuncParam
defer v.exit()
paramTypes, err := v.context.ArbitrationProvider.Ast().GetFuncParametersMeta(
- v.typeVisitor,
+ v.fieldVisitor,
ctx.CurrentPkg,
ctx.SourceFile,
ctx.FuncDecl,
@@ -240,7 +255,7 @@ func (v *RouteVisitor) getFuncRetVals(ctx executionContext) ([]metadata.FuncRetu
defer v.exit()
retVals, err := v.context.ArbitrationProvider.Ast().GetFuncRetValMeta(
- v.typeVisitor,
+ v.fieldVisitor,
ctx.CurrentPkg,
ctx.SourceFile,
ctx.FuncDecl,
diff --git a/core/visitors/struct.visitor.go b/core/visitors/struct.visitor.go
new file mode 100644
index 0000000..a40a8e5
--- /dev/null
+++ b/core/visitors/struct.visitor.go
@@ -0,0 +1,267 @@
+package visitors
+
+import (
+ "go/ast"
+ "strings"
+
+ "github.com/gopher-fleece/gleece/common"
+ "github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/gast"
+ "github.com/gopher-fleece/gleece/graphs"
+ "github.com/gopher-fleece/gleece/graphs/symboldg"
+ "github.com/gopher-fleece/gleece/infrastructure/logger"
+ "golang.org/x/tools/go/packages"
+)
+
+// StructVisitor builds declaration-side StructMeta objects.
+// It does not mutate the graph; graph insertion is the caller's job.
+type StructVisitor struct {
+ BaseVisitor
+
+ // typeUsageVisitor must be wired so the StructVisitor can parse field types.
+ typeUsageVisitor *TypeUsageVisitor
+
+ nonMaterializingTypeUsageVisitor *TypeUsageVisitor
+}
+
+// NewStructVisitor constructs a StructVisitor.
+func NewStructVisitor(context *VisitContext) (*StructVisitor, error) {
+ visitor := &StructVisitor{}
+ if err := visitor.initialize(context); err != nil {
+ return nil, err
+ }
+
+ return visitor, nil
+}
+
+func (v *StructVisitor) setTypeUsageVisitor(visitor *TypeUsageVisitor) {
+ v.typeUsageVisitor = visitor
+}
+
+func (v *StructVisitor) setNonMaterializingTypeUsageVisitor(visitor *TypeUsageVisitor) {
+ v.nonMaterializingTypeUsageVisitor = visitor
+}
+
+// VisitStructType builds metadata.StructMeta for a declaration TypeSpec.
+// It does not mutate the graph; the caller is responsible for inserting the struct node.
+func (v *StructVisitor) VisitStructType(
+ pkg *packages.Package,
+ file *ast.File,
+ genDecl *ast.GenDecl,
+ typeSpec *ast.TypeSpec,
+) (metadata.StructMeta, graphs.SymbolKey, error) {
+ tsName := "unknown"
+ if typeSpec != nil && typeSpec.Name != nil {
+ tsName = typeSpec.Name.Name
+ }
+
+ v.enterFmt("Visiting '%s'", tsName)
+ defer v.exit()
+
+ // First, check whether this is a 'special' and short-circuit if it is.
+ var specName string
+ if typeSpec != nil && typeSpec.Name != nil {
+ specName = typeSpec.Name.Name
+ }
+
+ specialSymKey := v.typeUsageVisitor.tryGetSymKeyForSpecial(pkg, typeSpec, specName)
+ if specialSymKey != nil {
+ return metadata.StructMeta{}, *specialSymKey, nil
+ }
+
+ // Obtain file version for this declaration - fail on error to avoid inconsistent state.
+ fileVersion, fvErr := v.context.MetadataCache.GetFileVersion(file, pkg.Fset)
+ if fvErr != nil || fileVersion == nil {
+ return metadata.StructMeta{}, graphs.SymbolKey{},
+ v.getFrozenError(
+ "could not obtain file version for struct %s: %w",
+ typeSpec.Name.Name,
+ fvErr,
+ )
+ }
+
+ // Ensure the type is a struct
+ structType, ok := typeSpec.Type.(*ast.StructType)
+ if !ok {
+ return metadata.StructMeta{}, graphs.SymbolKey{}, v.getFrozenError("type spec %s is not a struct", typeSpec.Name.Name)
+ }
+
+ typeParamEnv, typeParamDecls, err := v.buildDeclTypeParamEnv(pkg, file, typeSpec.TypeParams)
+ if err != nil {
+ return metadata.StructMeta{}, graphs.SymbolKey{}, err
+ }
+
+ holder, err := v.getAnnotations(typeSpec.Doc, genDecl)
+ if err != nil {
+ return metadata.StructMeta{}, graphs.SymbolKey{}, v.getFrozenError("failed to obtain notifications for struct '%s' - %v", tsName, err)
+ }
+
+ // Build FieldMeta entries (no graph mutations)
+ allFieldsMeta := make([]metadata.FieldMeta, 0)
+ for _, field := range structType.Fields.List {
+ fieldMetadata, err := v.getFieldMeta(pkg, file, fileVersion, field, typeParamEnv)
+ if err != nil {
+ return metadata.StructMeta{}, graphs.SymbolKey{}, v.getFrozenError(
+ "failed to obtain metadata for field '%s' - %v",
+ strings.Join(gast.GetFieldNames(field), ", "),
+ err,
+ )
+ }
+ allFieldsMeta = append(allFieldsMeta, fieldMetadata...)
+
+ }
+
+ structMeta := metadata.StructMeta{
+ SymNodeMeta: metadata.SymNodeMeta{
+ Name: typeSpec.Name.Name,
+ Node: typeSpec,
+ SymbolKind: common.SymKindStruct,
+ PkgPath: pkg.PkgPath,
+ Annotations: holder,
+ FVersion: fileVersion,
+ Range: common.ResolveNodeRange(pkg.Fset, typeSpec),
+ },
+ TypeParams: typeParamDecls,
+ Fields: allFieldsMeta,
+ }
+
+ structSymKey, err := v.graphStructAndFields(structMeta)
+
+ return structMeta, structSymKey, err
+}
+
+func (v *StructVisitor) getFieldMeta(
+ pkg *packages.Package,
+ file *ast.File,
+ fileVersion *gast.FileVersion,
+ field *ast.Field,
+ typeParamEnv map[string]int,
+) ([]metadata.FieldMeta, error) {
+ names := gast.GetFieldNames(field)
+
+ // build a usage-side TypeUsageMeta for the field type (delegated)
+ typeUsage, err := v.typeUsageVisitor.VisitExpr(pkg, file, field.Type, typeParamEnv)
+ if err != nil {
+ return nil, v.getFrozenError("failed to visit expression for field/s [%v] - %v", names, err)
+ }
+
+ isEmbedded := gast.IsEmbeddedOrAnonymousField(field)
+
+ holder, err := v.getAnnotations(field.Doc, nil)
+ if err != nil {
+ return nil, v.getFrozenError("failed to obtain annotations for field/s [%v] - %v", names, err)
+ }
+
+ createMeta := func(name string) metadata.FieldMeta {
+ return metadata.FieldMeta{
+ SymNodeMeta: metadata.SymNodeMeta{
+ Name: name,
+ Node: field,
+ SymbolKind: common.SymKindField,
+ PkgPath: pkg.PkgPath,
+ Annotations: holder,
+ FVersion: fileVersion,
+ Range: common.ResolveNodeRange(pkg.Fset, field),
+ },
+ Type: typeUsage,
+ IsEmbedded: isEmbedded,
+ }
+ }
+
+ if len(names) <= 0 {
+ return []metadata.FieldMeta{createMeta(typeUsage.Name)}, nil
+ }
+
+ meta := []metadata.FieldMeta{}
+ for _, name := range names {
+ meta = append(meta, createMeta(name))
+ }
+
+ return meta, nil
+}
+
+func (v *StructVisitor) graphStructAndFields(structMeta metadata.StructMeta) (graphs.SymbolKey, error) {
+ // Insert struct node into graph (graph builder will de-dupe).
+ createStructReq := symboldg.CreateStructNode{
+ Data: structMeta,
+ Annotations: structMeta.Annotations,
+ }
+
+ structNode, err := v.context.Graph.AddStruct(createStructReq)
+ if err != nil {
+ return graphs.SymbolKey{}, err
+ }
+
+ if err := v.context.MetadataCache.AddStruct(&structMeta); err != nil {
+ logger.Warn("Struct visitor failed to cache struct '%s' - %v", structMeta.Name, err)
+ }
+
+ // Now insert each field node (so field nodes exist, and edges can be created).
+ // GraphBuilder.AddField will create the field node and add EdgeKindType -> typeRef.
+ for _, fieldMeta := range structMeta.Fields {
+ createFieldReq := symboldg.CreateFieldNode{
+ Data: fieldMeta,
+ Annotations: fieldMeta.Annotations,
+ }
+ if _, err := v.context.Graph.AddField(createFieldReq); err != nil {
+ return graphs.SymbolKey{}, err
+ }
+ }
+
+ return structNode.Id, nil
+
+}
+
+// buildDeclTypeParamEnv builds a name->index env and a []TypeParamDecl.
+// It assigns indexes first, then parses constraints (if present) using the
+// typeUsageVisitor and the env. Short and deterministic.
+func (v *StructVisitor) buildDeclTypeParamEnv(
+ pkg *packages.Package,
+ file *ast.File,
+ params *ast.FieldList,
+) (map[string]int, []metadata.TypeParamDecl, error) {
+
+ env := map[string]int{}
+ decls := []metadata.TypeParamDecl{}
+
+ if params == nil {
+ return env, decls, nil
+ }
+
+ // First pass: assign indexes deterministically
+ idx := 0
+ for _, fld := range params.List {
+ for _, name := range fld.Names {
+ env[name.Name] = idx
+ decls = append(decls, metadata.TypeParamDecl{
+ Name: name.Name,
+ Index: idx,
+ })
+ idx++
+ }
+ }
+
+ // Second pass: parse constraints (if any) and attach to decls
+ idx = 0
+ for _, field := range params.List {
+ var constraintRef metadata.TypeRef
+ if field.Type != nil {
+ // Parse constraint using same env (constraints can refer to earlier params)
+ // Note we're using a distinct, non-materializing usage visitor here to avoid polluting the HIR.
+ // The rationale for not having this as a mutable flag is to preserve forward compatibility with threading.
+ typeUsage, err := v.nonMaterializingTypeUsageVisitor.VisitExpr(pkg, file, field.Type, env)
+ if err != nil {
+ return nil, nil, err
+ }
+ constraintRef = typeUsage.Root
+ }
+ // assign constraint to every name in this field (usually single)
+ for range field.Names {
+ decls[idx].Constraint = constraintRef
+ idx++
+ }
+ }
+
+ return env, decls, nil
+
+}
diff --git a/core/visitors/type.usage.visitor.go b/core/visitors/type.usage.visitor.go
new file mode 100644
index 0000000..faa95cb
--- /dev/null
+++ b/core/visitors/type.usage.visitor.go
@@ -0,0 +1,884 @@
+package visitors
+
+import (
+ "errors"
+ "fmt"
+ "go/ast"
+ "go/token"
+
+ "golang.org/x/tools/go/packages"
+
+ "github.com/gopher-fleece/gleece/common"
+ "github.com/gopher-fleece/gleece/core/annotations"
+ "github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/core/metadata/typeref"
+ "github.com/gopher-fleece/gleece/gast"
+ "github.com/gopher-fleece/gleece/graphs"
+)
+
+type topLevelMeta struct {
+ SymName string
+ PackagePath string
+ NodeRange common.ResolvedRange
+ Annotations *annotations.AnnotationHolder
+ FileVersion *gast.FileVersion
+ SymbolKind common.SymKind
+}
+
+// TypeUsageVisitor builds TypeRef trees for usage sites.
+// VisitExpr accepts a typeParamEnv (map[name]index) or nil.
+// It may call a TypeDeclVisitor on-demand to materialize declarations.
+type TypeUsageVisitor struct {
+ BaseVisitor
+ declVisitor *TypeDeclVisitor // optional; set via setDeclVisitor after wiring
+
+ materializing bool
+}
+
+// NewTypeUsageVisitor constructs a new TypeUsageVisitor.
+func NewTypeUsageVisitor(context *VisitContext, materializing bool) (*TypeUsageVisitor, error) {
+ v := &TypeUsageVisitor{materializing: materializing}
+ if err := v.initialize(context); err != nil {
+ return nil, err
+ }
+ return v, nil
+}
+
+func (v *TypeUsageVisitor) setDeclVisitor(visitor *TypeDeclVisitor) {
+ v.declVisitor = visitor
+}
+
+// VisitExpr is the canonical entrypoint for usage-side type analysis.
+// It remains small: helpers do the work.
+func (v *TypeUsageVisitor) VisitExpr(
+ pkg *packages.Package,
+ file *ast.File,
+ expr ast.Expr,
+ typeParamEnv map[string]int,
+) (metadata.TypeUsageMeta, error) {
+ fileName := gast.GetAstFileNameOrFallback(file, nil)
+ v.enterFmt("TypeUsageVisitor.VisitExpr file=%s expr=%T", fileName, expr)
+ defer v.exit()
+
+ if expr == nil {
+ return metadata.TypeUsageMeta{}, nil
+ }
+
+ // 1) structural: always build the TypeRef tree (may include NamedTypeRef with TypeArgs)
+ root, err := v.buildTypeRef(pkg, file, expr, typeParamEnv)
+ if err != nil {
+ return metadata.TypeUsageMeta{}, err
+ }
+
+ // 2) derive top-level meta (name, pkg, annotations, FileVersion, SymKind) and import type
+ topMeta, importType, err := v.deriveTopMetaFromRoot(pkg, file, expr, root)
+ if err != nil {
+ return metadata.TypeUsageMeta{}, err
+ }
+
+ // 3) assemble final TypeUsageMeta
+ res := metadata.TypeUsageMeta{
+ SymNodeMeta: metadata.SymNodeMeta{
+ Name: topMeta.SymName,
+ Node: expr,
+ SymbolKind: topMeta.SymbolKind,
+ PkgPath: topMeta.PackagePath,
+ Annotations: topMeta.Annotations,
+ Range: topMeta.NodeRange,
+ FVersion: topMeta.FileVersion,
+ },
+ Import: importType,
+ Root: root,
+ }
+ return res, nil
+}
+
+// deriveTopMetaFromRoot inspects the structural root and returns a topLevelMeta and import type.
+// It collapses pointer/array/slice wrappers first so pointer-to-enum and slice-of-instantiated behave correctly.
+func (v *TypeUsageVisitor) deriveTopMetaFromRoot(
+ pkg *packages.Package,
+ file *ast.File,
+ expr ast.Expr,
+ root metadata.TypeRef,
+) (*topLevelMeta, common.ImportType, error) {
+ // Collapse pointer/array/slice to the inner element when possible
+ innerRef, collapsed := collapseContainerTypeRef(root)
+
+ // Unwrap common AST wrappers so resolution sees the inner node
+ unwrappedExpr := unwrapExpr(expr)
+
+ // Check named-ness against the inner (collapsed) ref
+ named, isNamed := isNamedRef(innerRef)
+ if isNamed && named != nil {
+ // plain named reference without type args -> resolve declaration or universe
+ if len(named.TypeArgs) == 0 {
+ return v.resolveNamedType(pkg, file, unwrappedExpr)
+ }
+
+ // Instantiated named usage T[A,...] -> pass the named inner ref for generic handling
+ return v.resolveGenericNamedType(pkg, file, unwrappedExpr, innerRef)
+ }
+
+ // Not a named inner type:
+ // - if we collapsed a container, prefer resolving the composite/inner using the unwrapped expr
+ // - otherwise resolve using the original shape (preserve pointer/composite for composite resolver)
+ if collapsed {
+ return v.resolveCompositeType(pkg, file, unwrappedExpr, innerRef)
+ }
+ return v.resolveCompositeType(pkg, file, expr, root)
+}
+
+func (v *TypeUsageVisitor) resolveNamedType(
+ pkg *packages.Package,
+ file *ast.File,
+ expr ast.Expr,
+) (*topLevelMeta, common.ImportType, error) {
+ var importType common.ImportType
+
+ resolution, ok, resolveErr := gast.ResolveNamedType(
+ pkg,
+ file,
+ expr,
+ v.context.ArbitrationProvider.Pkg().GetPackage,
+ )
+ if resolveErr != nil {
+ return nil, importType, v.getFrozenError(
+ "ResolveNamedType error for %T: %v", expr, resolveErr,
+ )
+ }
+ if !ok {
+ return nil, importType, v.getFrozenError(
+ "could not resolve named type for expression (%T)", expr,
+ )
+ }
+
+ // Universe/builtin types
+ if resolution.IsUniverse {
+ kind, err := v.ensureUniverseTypeInGraph(resolution.TypeName)
+ if err != nil {
+ return nil, importType, err
+ }
+
+ usageMeta, err := v.createUsageMeta(
+ pkg,
+ file,
+ resolution.TypeName,
+ kind,
+ expr,
+ )
+
+ return usageMeta, importType, err
+ }
+
+ // Declared type -> full derivation and import computation
+ derived, err := v.deriveTopLevelMeta(resolution, pkg, expr)
+ if err != nil {
+ return nil, importType, err
+ }
+ it, err := v.context.ArbitrationProvider.Ast().GetImportType(file, expr)
+ if err != nil {
+ return nil, importType, err
+ }
+ importType = it
+
+ // ensure materialized
+ if err := v.ensureDeclMaterialized(resolution, derived.FileVersion); err != nil {
+ return nil, importType, err
+ }
+
+ return derived, importType, nil
+}
+
+func (v *TypeUsageVisitor) resolveGenericNamedType(
+ pkg *packages.Package,
+ file *ast.File,
+ expr ast.Expr,
+ root metadata.TypeRef,
+) (*topLevelMeta, common.ImportType, error) {
+ resolution, ok, resolveErr := gast.ResolveNamedType(
+ pkg,
+ file,
+ expr,
+ v.context.ArbitrationProvider.Pkg().GetPackage,
+ )
+
+ if resolveErr != nil {
+ return nil, common.ImportTypeNone, v.getFrozenError(
+ "ResolveNamedType error for instantiated base %T: %v", expr, resolveErr,
+ )
+ }
+
+ // If resolver says it could not find a named base -> strict error.
+ if !ok {
+ return nil, common.ImportTypeNone, v.getFrozenError(
+ "could not resolve base identifier for instantiated type '%s' (%T)",
+ canonicalNameForUsage(root),
+ expr,
+ )
+ }
+
+ // If base resolved to universe -> ensure primitive and return alias-kind meta
+ if resolution.IsUniverse {
+ kind, err := v.ensureUniverseTypeInGraph(resolution.TypeName)
+ if err != nil {
+ return nil, common.ImportTypeNone, err
+ }
+
+ // usage-centric alias is acceptable here (base is builtin)
+ usageMeta, err := v.createUsageMeta(
+ pkg,
+ file,
+ resolution.TypeName,
+ kind,
+ expr,
+ )
+
+ return usageMeta, common.ImportTypeNone, err
+ }
+
+ return v.resolveNonUniverseGenericNamed(pkg, file, expr, root, resolution)
+}
+
+func (v *TypeUsageVisitor) resolveNonUniverseGenericNamed(
+ pkg *packages.Package,
+ file *ast.File,
+ expr ast.Expr,
+ root metadata.TypeRef,
+ resolution gast.TypeSpecResolution,
+) (*topLevelMeta, common.ImportType, error) {
+ usageName := canonicalNameForUsage(root)
+
+ if resolution.DeclaringPackage == nil || resolution.DeclaringAstFile == nil || resolution.TypeSpec == nil {
+ return nil, common.ImportTypeNone, v.getFrozenError(
+ "incomplete resolution for declared instantiated base %s (canonical name %s)",
+ resolution.TypeName,
+ usageName,
+ )
+ }
+
+ // Get the declaring file version (strict)
+ fv, fvErr := v.context.MetadataCache.GetFileVersion(
+ resolution.DeclaringAstFile,
+ resolution.DeclaringPackage.Fset,
+ )
+
+ if fvErr != nil {
+ return nil, common.ImportTypeNone, fvErr
+ }
+
+ if fv == nil {
+ return nil, common.ImportTypeNone, v.getFrozenError(
+ "file version missing for declaring file of instantiated base %s",
+ usageName,
+ )
+ }
+
+ // Ensure the declared type has been materialized (decl visitor will parse and insert the decl).
+ if err := v.ensureDeclMaterialized(resolution, fv); err != nil {
+ return nil, common.ImportTypeNone, err
+ }
+
+ // Build usage-centric top-level meta for the instantiated usage.
+ top := &topLevelMeta{
+ SymName: usageName,
+ PackagePath: resolution.DeclaringPackage.PkgPath, // origin package of the base
+ NodeRange: common.ResolveNodeRange(pkg.Fset, expr), // usage range (not decl range)
+ Annotations: nil, // keep annotations exclusive to decl visitor
+ FileVersion: fv, // declaring file version (useful for canonicalization)
+ SymbolKind: chooseSymKind(resolution, pkg), // conservative choice based on declaration
+ }
+
+ // Resolve whether the usage imports from another package (import type)
+ it, itErr := v.context.ArbitrationProvider.Ast().GetImportType(file, expr)
+ if itErr != nil {
+ return nil, common.ImportTypeNone, itErr
+ }
+
+ return top, it, nil
+}
+
+func (v *TypeUsageVisitor) resolveCompositeType(
+ pkg *packages.Package,
+ file *ast.File,
+ expr ast.Expr,
+ root metadata.TypeRef,
+) (*topLevelMeta, common.ImportType, error) {
+ var symKind common.SymKind
+ if _, isParam := root.(*typeref.ParamTypeRef); isParam {
+ symKind = common.SymKindUnknown
+ } else {
+ symKind = common.SymKindAlias
+ }
+
+ usageMeta, err := v.createUsageMeta(
+ pkg,
+ file,
+ root.CanonicalString(),
+ symKind,
+ expr,
+ )
+
+ return usageMeta, common.ImportTypeNone, err
+}
+
+// deriveTopLevelMeta returns name/pkg/annotations/FileVersion/SymKind for a declared resolution.
+func (v *TypeUsageVisitor) deriveTopLevelMeta(
+ resolution gast.TypeSpecResolution,
+ currentPkg *packages.Package,
+ expr ast.Expr,
+) (*topLevelMeta, error) {
+
+ // Universe handled by caller; here we expect declared resolution.
+ if resolution.IsUniverse {
+ return nil, v.getFrozenError("deriveTopLevelMeta called for universe type %s", resolution.TypeName)
+ }
+
+ if resolution.DeclaringPackage == nil ||
+ resolution.DeclaringAstFile == nil ||
+ resolution.TypeSpec == nil {
+ return nil, v.getFrozenError("incomplete resolution for declared type %s", resolution.TypeName)
+ }
+
+ fileVersion, err := v.context.MetadataCache.GetFileVersion(
+ resolution.DeclaringAstFile,
+ resolution.DeclaringPackage.Fset,
+ )
+ if err != nil {
+ return nil, err
+ }
+ if fileVersion == nil {
+ return nil, v.getFrozenError("file version missing for declaring file of type %s", resolution.TypeName)
+ }
+
+ holder, aErr := v.getAnnotations(resolution.TypeSpec.Doc, resolution.GenDecl)
+ if aErr != nil {
+ return nil, aErr
+ }
+
+ return &topLevelMeta{
+ SymName: resolution.TypeName,
+ PackagePath: resolution.DeclaringPackage.PkgPath,
+ NodeRange: common.ResolveNodeRange(resolution.DeclaringPackage.Fset, expr),
+ Annotations: holder,
+ FileVersion: fileVersion,
+ SymbolKind: chooseSymKind(resolution, currentPkg),
+ }, nil
+}
+
+// ensureDeclMaterialized makes sure declared type has been materialized (cached + graph).
+func (v *TypeUsageVisitor) ensureDeclMaterialized(
+ resolution gast.TypeSpecResolution,
+ fileVersion *gast.FileVersion,
+) error {
+ // Check if the visitor is meant to materialize nodes - this may be false for type-constraint analysis
+ if !v.materializing {
+ return nil
+ }
+
+ // No-op for universe types
+ if resolution.IsUniverse {
+ return nil
+ }
+ if resolution.TypeSpec == nil || resolution.DeclaringAstFile == nil || resolution.DeclaringPackage == nil {
+ return v.getFrozenError("cannot materialize incomplete declaration for %s", resolution.TypeName)
+ }
+ if fileVersion == nil {
+ return v.getFrozenError("missing fileVersion when trying to materialize %s", resolution.TypeName)
+ }
+
+ key := graphs.NewSymbolKey(resolution.TypeSpec, fileVersion)
+ if v.context.MetadataCache.HasVisited(key) {
+ return nil
+ }
+
+ _, err := v.declVisitor.EnsureDeclMaterialized(
+ resolution.DeclaringPackage,
+ resolution.DeclaringAstFile,
+ resolution.GenDecl,
+ resolution.TypeSpec,
+ )
+ return err
+}
+
+// ensureUniverseTypeInGraph inserts primitives/specials into the graph so later edges can reference them.
+func (v *TypeUsageVisitor) ensureUniverseTypeInGraph(typeName string) (common.SymKind, error) {
+
+ v.enterFmt("ensureUniverseTypeInGraph %s", typeName)
+ defer v.exit()
+
+ // primitives
+ if prim, ok := common.ToPrimitiveType(typeName); ok {
+ if v.materializing {
+ if !v.context.Graph.IsPrimitivePresent(prim) {
+ v.context.Graph.AddPrimitive(prim)
+ }
+ }
+ return common.SymKindBuiltin, nil
+ }
+
+ // special types
+ if sp, ok := common.ToSpecialType(typeName); ok {
+ if v.materializing {
+ if !v.context.Graph.IsSpecialPresent(sp) {
+ v.context.Graph.AddSpecial(sp)
+ }
+ }
+ return common.SymKindSpecialBuiltin, nil
+ }
+
+ return common.SymKindUnknown, v.getFrozenError("unknown universe type '%s'", typeName)
+}
+
+func (v *TypeUsageVisitor) buildTypeRef(
+ pkg *packages.Package,
+ file *ast.File,
+ expr ast.Expr,
+ typeParamEnv map[string]int,
+) (metadata.TypeRef, error) {
+ switch t := expr.(type) {
+ case *ast.Ident, *ast.SelectorExpr:
+ return v.buildIdentOrSelectorRef(pkg, file, expr, typeParamEnv)
+ case *ast.StarExpr:
+ return v.buildPtrRef(pkg, file, t, typeParamEnv)
+ case *ast.ArrayType:
+ return v.buildArrayOrSliceRef(pkg, file, t, typeParamEnv)
+ case *ast.MapType:
+ return v.buildMapRef(pkg, file, t, typeParamEnv)
+ case *ast.FuncType:
+ return v.buildFuncTypeRef(pkg, file, t, typeParamEnv)
+ case *ast.IndexExpr:
+ return v.buildIndexExprRef(pkg, file, t, typeParamEnv)
+ case *ast.IndexListExpr:
+ return v.buildIndexListExprRef(pkg, file, t, typeParamEnv)
+ case *ast.StructType:
+ return v.buildInlineStructRef(pkg, file, t, typeParamEnv)
+ case *ast.InterfaceType:
+ if t.Methods == nil || len(t.Methods.List) == 0 {
+ return nil, errors.New("'interface{}' is not supported. Use 'any' instead'")
+ }
+ return nil, fmt.Errorf("interfaces are not supported'%T'", expr)
+ default:
+ return nil, fmt.Errorf("unsupported type expression '%T'", expr)
+ }
+}
+
+func (v *TypeUsageVisitor) buildPtrRef(
+ pkg *packages.Package,
+ file *ast.File,
+ ptr *ast.StarExpr,
+ typeParamEnv map[string]int,
+) (metadata.TypeRef, error) {
+ elem, err := v.buildTypeRef(pkg, file, ptr.X, typeParamEnv)
+ if err != nil {
+ return nil, err
+ }
+ return &typeref.PtrTypeRef{Elem: elem}, nil
+}
+
+func (v *TypeUsageVisitor) buildArrayOrSliceRef(
+ pkg *packages.Package,
+ file *ast.File,
+ array *ast.ArrayType,
+ typeParamEnv map[string]int,
+) (metadata.TypeRef, error) {
+ elem, err := v.buildTypeRef(pkg, file, array.Elt, typeParamEnv)
+ if err != nil {
+ return nil, err
+ }
+ if array.Len == nil {
+ return &typeref.SliceTypeRef{Elem: elem}, nil
+ }
+ // preserving Len as nil for now
+ return &typeref.ArrayTypeRef{Len: nil, Elem: elem}, nil
+}
+
+func (v *TypeUsageVisitor) buildMapRef(
+ pkg *packages.Package,
+ file *ast.File,
+ m *ast.MapType,
+ typeParamEnv map[string]int,
+) (metadata.TypeRef, error) {
+ keyRef, err := v.buildTypeRef(pkg, file, m.Key, typeParamEnv)
+ if err != nil {
+ return nil, err
+ }
+ valueRef, err := v.buildTypeRef(pkg, file, m.Value, typeParamEnv)
+ if err != nil {
+ return nil, err
+ }
+ return &typeref.MapTypeRef{Key: keyRef, Value: valueRef}, nil
+}
+
+func (v *TypeUsageVisitor) buildFuncTypeRef(
+ pkg *packages.Package,
+ file *ast.File,
+ ft *ast.FuncType,
+ typeParamEnv map[string]int,
+) (metadata.TypeRef, error) {
+ params := make([]metadata.TypeRef, 0, 4)
+ results := make([]metadata.TypeRef, 0, 2)
+
+ if ft.Params != nil {
+ for _, f := range ft.Params.List {
+ tr, err := v.buildTypeRef(pkg, file, f.Type, typeParamEnv)
+ if err != nil {
+ return nil, err
+ }
+ params = append(params, tr)
+ }
+ }
+ if ft.Results != nil {
+ for _, f := range ft.Results.List {
+ tr, err := v.buildTypeRef(pkg, file, f.Type, typeParamEnv)
+ if err != nil {
+ return nil, err
+ }
+ results = append(results, tr)
+ }
+ }
+ return &typeref.FuncTypeRef{Params: params, Results: results, Variadic: false}, nil
+}
+
+func (v *TypeUsageVisitor) buildIndexExprRef(
+ pkg *packages.Package,
+ file *ast.File,
+ idx *ast.IndexExpr,
+ typeParamEnv map[string]int,
+) (metadata.TypeRef, error) {
+ baseRef, err := v.buildTypeRef(pkg, file, idx.X, typeParamEnv)
+ if err != nil {
+ return nil, err
+ }
+ argRef, err := v.buildTypeRef(pkg, file, idx.Index, typeParamEnv)
+ if err != nil {
+ return nil, err
+ }
+ if named, ok := baseRef.(*typeref.NamedTypeRef); ok {
+ named.TypeArgs = append(named.TypeArgs, argRef)
+ return named, nil
+ }
+ return common.Ptr(typeref.NewNamedTypeRef(nil, []metadata.TypeRef{argRef})), nil
+}
+
+func (v *TypeUsageVisitor) buildIndexListExprRef(
+ pkg *packages.Package,
+ file *ast.File,
+ ile *ast.IndexListExpr,
+ typeParamEnv map[string]int,
+) (metadata.TypeRef, error) {
+ baseRef, err := v.buildTypeRef(pkg, file, ile.X, typeParamEnv)
+ if err != nil {
+ return nil, err
+ }
+ args := make([]metadata.TypeRef, 0, len(ile.Indices))
+ for _, idx := range ile.Indices {
+ a, err := v.buildTypeRef(pkg, file, idx, typeParamEnv)
+ if err != nil {
+ return nil, err
+ }
+ args = append(args, a)
+ }
+ if named, ok := baseRef.(*typeref.NamedTypeRef); ok {
+ named.TypeArgs = append(named.TypeArgs, args...)
+ return named, nil
+ }
+ return common.Ptr(typeref.NewNamedTypeRef(nil, args)), nil
+}
+
+// buildInlineStructRef builds an InlineStructTypeRef for inline struct literals.
+// It returns a non-graphing inline representation keyed by a rep SymbolKey.
+func (v *TypeUsageVisitor) buildInlineStructRef(
+ pkg *packages.Package,
+ file *ast.File,
+ structType *ast.StructType,
+ typeParamEnv map[string]int,
+) (*typeref.InlineStructTypeRef, error) {
+ fv, err := v.context.MetadataCache.GetFileVersion(file, pkg.Fset)
+ if err != nil {
+ return nil, err
+ }
+ if fv == nil {
+ return nil, fmt.Errorf("file version missing for inline struct in %s", file.Name.Name)
+ }
+
+ out := &typeref.InlineStructTypeRef{
+ Fields: []metadata.FieldMeta{},
+ RepKey: graphs.NewSymbolKey(structType, fv),
+ }
+
+ for _, field := range structType.Fields.List {
+ typeUsage, err := v.VisitExpr(pkg, file, field.Type, typeParamEnv)
+ if err != nil {
+ return nil, err
+ }
+ names := gast.GetFieldNames(field)
+ if len(names) == 0 {
+ names = []string{""} // anonymous/embedded
+ }
+ isEmbedded := gast.IsEmbeddedOrAnonymousField(field)
+ ann, _ := v.getAnnotations(field.Doc, nil) // inline structs have no genDecl
+ for _, nm := range names {
+ fMeta := metadata.FieldMeta{
+ SymNodeMeta: metadata.SymNodeMeta{
+ Name: nm,
+ Node: field,
+ SymbolKind: common.SymKindField,
+ PkgPath: pkg.PkgPath,
+ Annotations: ann,
+ FVersion: fv,
+ Range: common.ResolveNodeRange(pkg.Fset, field),
+ },
+ Type: typeUsage,
+ IsEmbedded: isEmbedded,
+ }
+ out.Fields = append(out.Fields, fMeta)
+ }
+ }
+ return out, nil
+}
+
+// buildIdentOrSelectorRef resolves ident/selector to a TypeRef; handles type params and declared/universe types.
+func (v *TypeUsageVisitor) buildIdentOrSelectorRef(
+ pkg *packages.Package,
+ file *ast.File,
+ expr ast.Expr,
+ typeParamEnv map[string]int,
+) (metadata.TypeRef, error) {
+ // type parameter
+ if typeParamEnv != nil {
+ if name := gast.GetIdentNameFromExpr(expr); name != nil {
+ if idx, ok := typeParamEnv[*name]; ok {
+ return &typeref.ParamTypeRef{Name: *name, Index: idx}, nil
+ }
+ }
+ }
+
+ resolution, ok, resolveErr := gast.ResolveNamedType(
+ pkg,
+ file,
+ expr,
+ v.context.ArbitrationProvider.Pkg().GetPackage,
+ )
+ if resolveErr != nil {
+ return nil, resolveErr
+ }
+ if !ok {
+ return nil, fmt.Errorf("could not resolve named type for expression (%T)", expr)
+ }
+
+ // universe -> ensure primitive/special graph presence
+ if resolution.IsUniverse {
+ if _, err := v.ensureUniverseTypeInGraph(resolution.TypeName); err != nil {
+ return nil, err
+ }
+
+ key := graphs.NewUniverseSymbolKey(resolution.TypeName)
+ return common.Ptr(typeref.NewNamedTypeRef(&key, nil)), nil
+ }
+
+ // declared -> must have contextual information
+ if resolution.TypeSpec == nil || resolution.DeclaringPackage == nil || resolution.DeclaringAstFile == nil {
+ return nil, fmt.Errorf("incomplete declaration context for type %s", resolution.TypeName)
+ }
+
+ // Handles specials like 'time.Time' or 'context.Context'
+ specialKey := v.tryGetSymKeyForSpecial(
+ resolution.DeclaringPackage,
+ resolution.TypeSpec,
+ resolution.TypeName,
+ )
+
+ if specialKey != nil {
+ return common.Ptr(typeref.NewNamedTypeRef(specialKey, nil)), nil
+ }
+
+ fileVersion, err := v.context.MetadataCache.GetFileVersion(
+ resolution.DeclaringAstFile,
+ resolution.DeclaringPackage.Fset,
+ )
+ if err != nil {
+ return nil, err
+ }
+ if fileVersion == nil {
+ return nil, fmt.Errorf("missing file version for declaring file of type %s", resolution.TypeName)
+ }
+
+ key := graphs.NewSymbolKey(resolution.TypeSpec, fileVersion)
+ // on-demand materialize
+ if !v.context.MetadataCache.HasVisited(key) {
+ if v.declVisitor == nil {
+ return nil, fmt.Errorf("declaration for %s not materialized and no materializer provided", resolution.TypeName)
+ }
+
+ _, err := v.declVisitor.EnsureDeclMaterialized(
+ resolution.DeclaringPackage,
+ resolution.DeclaringAstFile,
+ resolution.GenDecl,
+ resolution.TypeSpec,
+ )
+
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return common.Ptr(typeref.NewNamedTypeRef(&key, nil)), nil
+}
+
+// createUsageMeta creates top level metadata for a usage site.
+//
+// Example: in
+//
+// type SomeStruct struct { SomeField string }
+//
+// the usage metadata refers to 'string' — it must include the usage's package path
+// (if known) and the usage node range so callers can report/anchor diagnostics.
+func (v *TypeUsageVisitor) createUsageMeta(
+ pkg *packages.Package,
+ file *ast.File,
+ name string,
+ kind common.SymKind,
+ expr ast.Expr,
+) (*topLevelMeta, error) {
+ var pkgPath string
+ if pkg != nil {
+ pkgPath = pkg.PkgPath
+ }
+
+ var nodeRange common.ResolvedRange
+ // Resolve range using package FileSet when available. If pkg is nil, keep zero-value.
+ if pkg != nil && expr != nil {
+ nodeRange = common.ResolveNodeRange(pkg.Fset, expr)
+ }
+
+ fileVersion, err := v.context.MetadataCache.GetFileVersion(file, pkg.Fset)
+ if err != nil {
+ return nil, fmt.Errorf(
+ "failed to obtain FileVersion for type usage '%s' in file '%s' - %v",
+ name,
+ gast.GetAstFileNameOrFallback(file, nil),
+ err,
+ )
+ }
+
+ return &topLevelMeta{
+ SymName: name,
+ PackagePath: pkgPath,
+ NodeRange: nodeRange,
+ Annotations: nil, // A type usage cannot have annotations - the field & declaration - yes. The usage? Nope.
+ FileVersion: fileVersion, // The FileVersion is the same as the Field's - *not* the declaration's
+ SymbolKind: kind,
+ }, nil
+}
+
+// chooseSymKind picks a SymKind based on the resolution (struct/interface/alias/enum/special/builtin).
+func chooseSymKind(resolution gast.TypeSpecResolution, pkg *packages.Package) common.SymKind {
+ if resolution.IsUniverse {
+ return common.SymKindBuiltin
+ }
+ if resolution.TypeSpec == nil {
+ return common.SymKindUnknown
+ }
+ switch t := resolution.TypeSpec.Type.(type) {
+ case *ast.StructType:
+ _ = t
+ return common.SymKindStruct
+ case *ast.InterfaceType:
+ if resolution.IsContext() {
+ return common.SymKindSpecialBuiltin
+ }
+ return common.SymKindInterface
+ case *ast.Ident:
+ if pkg != nil && gast.IsEnumLike(resolution.DeclaringPackage, resolution.TypeSpec) {
+ return common.SymKindEnum
+ }
+ return common.SymKindAlias
+ default:
+ return common.SymKindUnknown
+ }
+}
+
+// isNamedRef returns the NamedTypeRef when the root is a named reference.
+func isNamedRef(root metadata.TypeRef) (*typeref.NamedTypeRef, bool) {
+ if root == nil {
+ return nil, false
+ }
+ n, ok := root.(*typeref.NamedTypeRef)
+ return n, ok
+}
+
+// canonicalNameForUsage returns a deterministic usage-centric name for a TypeRef.
+func canonicalNameForUsage(root metadata.TypeRef) string {
+ if root == nil {
+ return ""
+ }
+ return root.CanonicalString()
+}
+
+// collapseContainerTypeRef unwraps Ptr / Slice / Array TypeRefs and returns the innermost element.
+// Returns the inner TypeRef and true if any container was collapsed.
+func collapseContainerTypeRef(root metadata.TypeRef) (metadata.TypeRef, bool) {
+ if root == nil {
+ return nil, false
+ }
+
+ inner := root
+ collapsed := false
+ for {
+ switch t := inner.(type) {
+ case *typeref.PtrTypeRef:
+ if t.Elem == nil {
+ return inner, collapsed
+ }
+ inner = t.Elem
+ collapsed = true
+ continue
+ case *typeref.SliceTypeRef:
+ if t.Elem == nil {
+ return inner, collapsed
+ }
+ inner = t.Elem
+ collapsed = true
+ continue
+ case *typeref.ArrayTypeRef:
+ if t.Elem == nil {
+ return inner, collapsed
+ }
+ inner = t.Elem
+ collapsed = true
+ continue
+ default:
+ return inner, collapsed
+ }
+ }
+}
+
+// unwrapExpr removes common wrappers from an AST expr so resolution sees the inner expression.
+// It strips &, * (unary/star) parentheses and array/slice types.
+func unwrapExpr(expr ast.Expr) ast.Expr {
+ for {
+ switch e := expr.(type) {
+ case *ast.UnaryExpr:
+ // address (&X) and indirection (*X)
+ if e.Op == token.AND || e.Op == token.MUL {
+ expr = e.X
+ continue
+ }
+ return expr
+ case *ast.StarExpr:
+ // pointer expr/type: *X
+ expr = e.X
+ continue
+ case *ast.ArrayType:
+ // slice/array: unwrap to element
+ expr = e.Elt
+ continue
+ case *ast.ParenExpr:
+ // (T) -> T
+ expr = e.X
+ continue
+ default:
+ return expr
+ }
+ }
+}
diff --git a/core/visitors/type.visitor.go b/core/visitors/type.visitor.go
deleted file mode 100644
index 3119f8f..0000000
--- a/core/visitors/type.visitor.go
+++ /dev/null
@@ -1,811 +0,0 @@
-package visitors
-
-import (
- "fmt"
- "go/ast"
- "go/types"
-
- "github.com/gopher-fleece/gleece/common"
- "github.com/gopher-fleece/gleece/core/annotations"
- "github.com/gopher-fleece/gleece/core/metadata"
- "github.com/gopher-fleece/gleece/gast"
- "github.com/gopher-fleece/gleece/graphs"
- "github.com/gopher-fleece/gleece/graphs/symboldg"
- "golang.org/x/tools/go/packages"
-)
-
-type RecursiveTypeVisitor struct {
- BaseVisitor
-
- // The file currently being worked on
- currentSourceFile *ast.File
-
- // The last-encountered GenDecl.
- //
- // Documentation may be placed on TypeDecl or their parent GenDecl so we track these,
- // in case we need to fetch the docs from the TypeDecl's parent.
- currentGenDecl *ast.GenDecl
-}
-
-// NewRecursiveTypeVisitor Instantiates a new type visitor.
-func NewRecursiveTypeVisitor(context *VisitContext) (*RecursiveTypeVisitor, error) {
- visitor := RecursiveTypeVisitor{}
- err := visitor.initialize(context)
- return &visitor, err
-}
-
-// Visit fulfils the ast.Visitor interface
-func (v *RecursiveTypeVisitor) Visit(node ast.Node) ast.Visitor {
- v.enterFmt("Visiting node interface at position %d", node.Pos())
- defer v.exit()
-
- switch currentNode := node.(type) {
- case *ast.File:
- // Update the current file when visiting an *ast.File node
- v.currentSourceFile = currentNode
- case *ast.GenDecl:
- v.currentGenDecl = currentNode
- case *ast.TypeSpec:
- // Check if it's a struct and if it embeds GleeceController
- if _, isStruct := currentNode.Type.(*ast.StructType); isStruct {
- _, _, err := v.VisitStructType(v.currentSourceFile, v.currentGenDecl, currentNode)
- if err != nil {
- v.setLastError(err)
- return v
- }
- }
- }
- return v
-}
-
-func (v *RecursiveTypeVisitor) VisitField(
- pkg *packages.Package,
- file *ast.File,
- field *ast.Field,
-) ([]metadata.FieldMeta, error) {
- v.enterFmt("Visiting field '%v' in file %s", field.Names, file.Name.Name)
- defer v.exit()
-
- var results []metadata.FieldMeta
-
- holder, err := v.getAnnotations(field.Doc, nil)
- if err != nil {
- return nil, v.frozenError(err)
- }
-
- isEmbedded := len(field.Names) == 0
- names := v.resolveFieldNames(field, isEmbedded)
-
- for _, nameIdent := range names {
- fieldMeta, err := v.buildFieldMeta(pkg, file, field, nameIdent, isEmbedded, holder)
- if err != nil {
- return nil, err
- }
-
- results = append(results, fieldMeta)
-
- if _, err := v.resolveFieldTypeRecursive(pkg, file, field); err != nil {
- return nil, v.frozenError(err)
- }
-
- if _, err := v.context.GraphBuilder.AddField(symboldg.CreateFieldNode{
- Data: fieldMeta,
- Annotations: holder,
- }); err != nil {
- return nil, v.frozenError(err)
- }
- }
-
- return results, nil
-}
-
-// VisitStructType dives into a Struct, given as a TypeSpec and returns its metadata.
-//
-// Note that this method is internally recursive and will dive into the struct's dependencies such
-// as nested fields and their types and build/store their entities in the symbol graph.
-func (v *RecursiveTypeVisitor) VisitStructType(
- file *ast.File,
- nodeGenDecl *ast.GenDecl,
- node *ast.TypeSpec,
-) (metadata.StructMeta, graphs.SymbolKey, error) {
- v.enterFmt("Visiting struct %s", node.Name.Name)
- defer v.exit()
-
- if node.Name.Name == "Time" && file.Name.Name == "time" {
- // Special case: time.Time is a struct but we treat it as a builtin
- // and in the specification & json marshalling it will be a string with 'date-time' format
- symKey := graphs.NewNonUniverseBuiltInSymbolKey("time.Time")
- return metadata.StructMeta{}, symKey, nil
- }
-
- // Check the cache first
- symKey, fVersion, cached, err := v.checkStructCache(file, node)
- if err != nil || cached != nil {
- return *cached, symKey, err
- }
-
- // If the type isn't cached, build it
- structMeta, err := v.constructStructMeta(file, fVersion, nodeGenDecl, node)
- if err != nil {
- return structMeta, symKey, err
- }
-
- // Insert it to the cache for faster lookup next time we encounter it
- v.context.MetadataCache.AddStruct(&structMeta)
-
- // And finally, add it to the symbol graph
- symNode, err := v.context.GraphBuilder.AddStruct(
- symboldg.CreateStructNode{
- Data: structMeta,
- Annotations: structMeta.Annotations,
- },
- )
-
- return structMeta, symNode.Id, err
-}
-
-func (v *RecursiveTypeVisitor) VisitEnumType(
- pkg *packages.Package,
- file *ast.File,
- fVersion *gast.FileVersion,
- nodeGenDecl *ast.GenDecl,
- node *ast.TypeSpec,
-) (metadata.EnumMeta, graphs.SymbolKey, error) {
- v.enterFmt("Visiting enum %s.%s", pkg.PkgPath, node.Name.Name)
- defer v.exit()
-
- // Check cache first
- symKey := graphs.NewSymbolKey(node, fVersion)
- cached := v.context.MetadataCache.GetEnum(symKey)
- if cached != nil {
- return *cached, symKey, nil
- }
-
- typeName, err := gast.GetTypeNameOrError(pkg, node.Name.Name)
- if err != nil {
- return metadata.EnumMeta{}, graphs.SymbolKey{}, err
- }
-
- meta, err := v.extractEnumAliasType(pkg, fVersion, nodeGenDecl, node, typeName)
- if err != nil {
- return meta, graphs.SymbolKey{}, err
- }
-
- v.context.MetadataCache.AddEnum(&meta)
-
- symNode, err := v.context.GraphBuilder.AddEnum(
- symboldg.CreateEnumNode{
- Data: meta,
- Annotations: meta.Annotations,
- },
- )
-
- if err != nil {
- return meta, graphs.SymbolKey{}, err
- }
-
- return meta, symNode.Id, nil
-}
-
-func (v *RecursiveTypeVisitor) constructStructMeta(
- file *ast.File,
- fVersion *gast.FileVersion,
- nodeGenDecl *ast.GenDecl,
- node *ast.TypeSpec,
-) (metadata.StructMeta, error) {
- v.enterFmt("Constructing struct metadata for %s", node.Name.Name)
- defer v.exit()
-
- pkg, err := v.context.ArbitrationProvider.Pkg().GetPackageForFile(file)
- if err != nil {
- return metadata.StructMeta{}, v.frozenError(err)
- }
-
- if pkg == nil {
- return metadata.StructMeta{}, v.getFrozenError(
- "could not determine package for file %s",
- gast.GetAstFileName(
- v.context.ArbitrationProvider.Pkg().FSet(),
- file,
- ),
- )
- }
-
- holder, err := v.getAnnotations(node.Doc, nodeGenDecl)
- if err != nil {
- return metadata.StructMeta{}, v.frozenError(err)
- }
-
- structType, isStruct := node.Type.(*ast.StructType)
- if !isStruct {
- return metadata.StructMeta{}, v.getFrozenError("non-struct node '%v' was provided to VisitStructType", node.Name.Name)
- }
-
- fields, err := v.buildStructFields(pkg, file, structType)
- if err != nil {
- return metadata.StructMeta{}, v.frozenError(err)
- }
-
- return metadata.StructMeta{
- SymNodeMeta: metadata.SymNodeMeta{
- Name: node.Name.Name,
- Node: node,
- SymbolKind: common.SymKindStruct,
- PkgPath: pkg.PkgPath,
- Annotations: holder,
- FVersion: fVersion,
- Range: common.ResolveNodeRange(pkg.Fset, node),
- },
- Fields: fields,
- }, nil
-}
-
-// checkStructCache retrieves cached metadata for the given file/node combination.
-// Returns the relevant SymbolKey, FileVersion and any cached information, if it exists
-func (v *RecursiveTypeVisitor) checkStructCache(
- file *ast.File,
- node *ast.TypeSpec,
-) (graphs.SymbolKey, *gast.FileVersion, *metadata.StructMeta, error) {
- v.enterFmt("Checking struct cache for file %s node %s", file.Name.Name, node.Name.Name)
- defer v.exit()
-
- fVersion, err := v.context.MetadataCache.GetFileVersion(file, v.context.ArbitrationProvider.Pkg().FSet())
- if err != nil {
- return graphs.SymbolKey{}, nil, nil, v.frozenError(err)
- }
-
- symKey := graphs.NewSymbolKey(node, fVersion)
- cached := v.context.MetadataCache.GetStruct(symKey)
- return symKey, fVersion, cached, nil
-}
-
-func (v *RecursiveTypeVisitor) extractEnumAliasType(
- pkg *packages.Package,
- fVersion *gast.FileVersion,
- specGenDecl *ast.GenDecl,
- spec *ast.TypeSpec,
- typeName *types.TypeName,
-) (metadata.EnumMeta, error) {
- v.enterFmt("Extracting metadata for enum %s.%s", pkg.PkgPath, typeName.Name())
- defer v.exit()
-
- basic, isBasicType := typeName.Type().Underlying().(*types.Basic)
- if !isBasicType {
- return metadata.EnumMeta{}, v.getFrozenError("type %s is not a basic type", typeName.Name())
- }
-
- kind, err := metadata.NewEnumValueKind(basic.Kind())
- if err != nil {
- return metadata.EnumMeta{}, err
- }
-
- enumAnnotations, err := v.getAnnotations(spec.Doc, specGenDecl)
- if err != nil {
- return metadata.EnumMeta{}, err
- }
-
- enumValues, err := v.getEnumValueDefinitions(
- fVersion,
- pkg,
- typeName,
- basic,
- )
-
- if err != nil {
- return metadata.EnumMeta{}, err
- }
-
- enum := metadata.EnumMeta{
- SymNodeMeta: metadata.SymNodeMeta{
- Name: typeName.Name(),
- Node: spec,
- SymbolKind: common.SymKindEnum,
- PkgPath: pkg.PkgPath,
- FVersion: fVersion,
- Annotations: enumAnnotations,
- Range: common.ResolveNodeRange(pkg.Fset, spec),
- },
- ValueKind: kind,
- Values: enumValues,
- }
-
- return enum, nil
-}
-
-func (v *RecursiveTypeVisitor) getEnumValueDefinitions(
- enumFVersion *gast.FileVersion,
- enumPkg *packages.Package,
- enumTypeName *types.TypeName,
- enumBasic *types.Basic,
-) ([]metadata.EnumValueDefinition, error) {
- v.enterFmt("Obtaining enum value definitions for %s.%s", enumPkg.PkgPath, enumTypeName.Name())
- defer v.exit()
-
- enumValues := []metadata.EnumValueDefinition{}
-
- scope := enumPkg.Types.Scope()
- for _, name := range scope.Names() {
- obj := scope.Lookup(name)
- constVal, isConst := obj.(*types.Const)
- if !isConst || !types.Identical(constVal.Type(), enumTypeName.Type()) {
- continue
- }
-
- val := gast.ExtractConstValue(enumBasic.Kind(), constVal)
- if val == nil {
- continue
- }
-
- // Attempt to find the AST node
- var constNode ast.Node
- if v.context != nil && v.context.ArbitrationProvider != nil {
- constNode = gast.FindConstSpecNode(enumPkg, constVal.Name())
- }
-
- // Attempt to extract annotations
- var holder *annotations.AnnotationHolder
- if constNode != nil {
- comments := gast.GetCommentsFromNode(constNode, v.context.ArbitrationProvider.Pkg().FSet())
- if h, err := annotations.NewAnnotationHolder(comments, annotations.CommentSourceProperty); err == nil {
- holder = &h
- } else {
- return []metadata.EnumValueDefinition{}, err
- }
- }
-
- enumValues = append(enumValues, metadata.EnumValueDefinition{
- SymNodeMeta: metadata.SymNodeMeta{
- Name: constVal.Name(),
- Node: constNode,
- SymbolKind: common.SymKindEnumValue,
- PkgPath: enumPkg.PkgPath,
- Annotations: holder,
- FVersion: enumFVersion,
- Range: common.ResolveNodeRange(enumPkg.Fset, constNode),
- },
- Value: val,
- })
- }
-
- return enumValues, nil
-}
-
-func (v *RecursiveTypeVisitor) resolveFieldNames(field *ast.Field, isEmbedded bool) []*ast.Ident {
- if !isEmbedded {
- return field.Names
- }
- if ident := gast.GetIdentFromExpr(field.Type); ident != nil {
- return []*ast.Ident{{Name: ident.Name}}
- }
- return nil
-}
-
-func (v *RecursiveTypeVisitor) buildFieldMeta(
- pkg *packages.Package,
- file *ast.File,
- field *ast.Field,
- fieldNameIdent *ast.Ident,
- isEmbedded bool,
- holder *annotations.AnnotationHolder,
-) (metadata.FieldMeta, error) {
- v.enterFmt("Building field metadata for file %s field %s", file.Name.Name, fieldNameIdent)
- defer v.exit()
-
- fieldFVersion, err := v.context.MetadataCache.GetFileVersion(file, pkg.Fset)
- if err != nil {
- return metadata.FieldMeta{}, v.getFrozenError(
- "could not obtain a FileVersion for file %v whilst processing field %v - %v",
- file.Name.Name,
- fieldNameIdent.Name,
- err,
- )
- }
-
- typeUsage, err := v.resolveTypeUsage(pkg, file, field.Type)
- if err != nil {
- return metadata.FieldMeta{}, v.getFrozenError(
- "could not create type usage metadata for field %v - %v",
- fieldNameIdent.Name,
- err,
- )
- }
-
- return metadata.FieldMeta{
- SymNodeMeta: metadata.SymNodeMeta{
- Name: fieldNameIdent.Name,
- Node: field,
- PkgPath: pkg.PkgPath,
- SymbolKind: common.SymKindField,
- Annotations: holder,
- FVersion: fieldFVersion,
- Range: common.ResolveNodeRange(pkg.Fset, field),
- },
- Type: typeUsage,
- IsEmbedded: isEmbedded,
- }, nil
-}
-
-func (v *RecursiveTypeVisitor) resolveTypeUsage(
- pkg *packages.Package,
- file *ast.File,
- typeExpr ast.Expr,
-) (metadata.TypeUsageMeta, error) {
- v.enterFmt("Type usage resolution for file %s expression %v", file.Name.Name, typeExpr)
- defer v.exit()
-
- // 1. Resolve the type
- resolvedType, err := gast.ResolveTypeSpecFromExpr(pkg, file, typeExpr, v.context.ArbitrationProvider.Pkg().GetPackage)
- if err != nil {
- return metadata.TypeUsageMeta{}, v.frozenError(err)
- }
-
- importType := common.ImportTypeNone
- var underlyingAnnotations *annotations.AnnotationHolder
- var pkgPath string
- var nodeRange common.ResolvedRange
-
- // 2. Gather any auxiliary metadata we need for non-universe types
- if !resolvedType.IsUniverse {
- nodeRange = common.ResolveNodeRange(resolvedType.DeclaringPackage.Fset, typeExpr)
- importType, err = v.context.ArbitrationProvider.Ast().GetImportType(file, typeExpr)
- if err != nil {
- return metadata.TypeUsageMeta{}, v.frozenError(err)
- }
-
- underlyingAnnotations, err = v.tryGetUnderlyingAnnotations(resolvedType)
- if err != nil {
- return metadata.TypeUsageMeta{}, err
- }
-
- pkgPath = resolvedType.DeclaringPackage.PkgPath
- }
-
- typeUsageKind := getTypeSymKind(resolvedType.DeclaringPackage, resolvedType)
- typeLayers, err := v.buildTypeLayers(
- resolvedType.DeclaringPackage,
- resolvedType.DeclaringAstFile,
- typeExpr,
- resolvedType.TypeName,
- )
-
- if err != nil {
- return metadata.TypeUsageMeta{}, v.getFrozenError(
- "failed to build type layers for expression with type name '%v' - %v",
- resolvedType.TypeName,
- err,
- )
- }
-
- // 3. Make sure that if the type is a built in like "error" or "string" or "any",
- // it's inserted into the symbol graph
- if err := v.ensureBuiltinTypeIsGraph(resolvedType); err != nil {
- return metadata.TypeUsageMeta{}, err
- }
-
- // Finally, cobble it all together
- return metadata.TypeUsageMeta{
- SymNodeMeta: metadata.SymNodeMeta{
- Name: resolvedType.TypeName,
- Node: typeExpr,
- PkgPath: pkgPath,
- SymbolKind: typeUsageKind,
- Annotations: underlyingAnnotations,
- Range: nodeRange, // May be empty for builtins
- },
- Layers: typeLayers,
- Import: importType,
- }, nil
-
-}
-
-// ensureBuiltinTypeIsGraph ensures that, if the given resolved type is a 'builtin' or 'special builtin' type, it's
-// inserted to the symbol graph
-func (v *RecursiveTypeVisitor) ensureBuiltinTypeIsGraph(resolved gast.TypeSpecResolution) error {
- v.enterFmt("Ensuring built-in resolution for %s", resolved.TypeName)
- defer v.exit()
-
- if resolved.DeclaringAstFile != nil {
- // This isn't a universe type — nothing to do here
- return nil
- }
-
- // Handle known universe types like string, int, etc
- if primitive, ok := symboldg.ToPrimitiveType(resolved.TypeName); ok {
- v.context.GraphBuilder.AddPrimitive(primitive)
- return nil
- }
-
- // Handle known special types like error
- if special, ok := symboldg.ToSpecialType(resolved.TypeName); ok {
- v.context.GraphBuilder.AddSpecial(special)
- return nil
- }
-
- return v.getFrozenError("encountered unknown universe type '%s'", resolved.TypeName)
-}
-
-func (v *RecursiveTypeVisitor) tryGetUnderlyingAnnotations(resolved gast.TypeSpecResolution) (*annotations.AnnotationHolder, error) {
- v.enterFmt("Retrieving annotations for resolved %s", resolved.String())
- defer v.exit()
-
- if resolved.IsUniverse ||
- resolved.DeclaringPackage == nil ||
- resolved.DeclaringAstFile == nil ||
- resolved.TypeSpec == nil {
- return nil, nil
- }
-
- return v.getAnnotations(resolved.TypeSpec.Doc, resolved.GenDecl)
-}
-
-func (v *RecursiveTypeVisitor) buildStructFields(
- pkg *packages.Package,
- file *ast.File,
- node *ast.StructType,
-) ([]metadata.FieldMeta, error) {
- v.enterFmt("Building struct fields for struct at position %d in file %s", node.Struct, file.Name.Name)
- defer v.exit()
-
- var results []metadata.FieldMeta
-
- for _, field := range node.Fields.List {
- // Note that fields may have fields, like:
- // a, b, c int
- fields, err := v.VisitField(pkg, file, field)
- if err != nil {
- return results, err
- }
- results = append(results, fields...)
- }
-
- return results, nil
-}
-
-func (v *RecursiveTypeVisitor) resolveFieldTypeRecursive(
- pkg *packages.Package,
- file *ast.File,
- field *ast.Field,
-) (gast.TypeSpecResolution, error) {
- v.enterFmt("Recursively resolving type for field [%v] in file %s", field.Names, file.Name.Name)
- defer v.exit()
-
- resolved, err := gast.ResolveTypeSpecFromField(pkg, file, field, v.context.ArbitrationProvider.Pkg().GetPackage)
- if err != nil {
- return gast.TypeSpecResolution{}, err
- }
-
- if err := v.recursivelyResolve(resolved); err != nil {
- return gast.TypeSpecResolution{}, err
- }
-
- return resolved, err
-}
-
-func (v *RecursiveTypeVisitor) recursivelyResolve(resolved gast.TypeSpecResolution) error {
- v.enterFmt("Headless recursive resolution for type %s", resolved.String())
- defer v.exit()
-
- if resolved.IsUniverse {
- return nil
- }
-
- if resolved.DeclaringPackage == nil || resolved.DeclaringAstFile == nil || resolved.TypeSpec == nil {
- return v.getFrozenError("resolved type '%s' is missing necessary declaration context", resolved.TypeName)
- }
-
- fVersion, err := v.context.MetadataCache.GetFileVersion(
- resolved.DeclaringAstFile,
- resolved.DeclaringPackage.Fset,
- )
- if err != nil {
- return v.frozenError(err)
- }
-
- key := graphs.NewSymbolKey(resolved.TypeSpec, fVersion)
- if v.context.MetadataCache.HasVisited(key) {
- return nil
- }
-
- _, err = v.visitTypeSpec(
- resolved.DeclaringPackage,
- resolved.DeclaringAstFile,
- fVersion,
- resolved.GenDecl,
- resolved.TypeSpec,
- )
-
- return v.frozenError(err)
-}
-
-func (v *RecursiveTypeVisitor) visitTypeSpec(
- pkg *packages.Package,
- file *ast.File,
- fVersion *gast.FileVersion,
- specGenDecl *ast.GenDecl,
- spec *ast.TypeSpec,
-) (graphs.SymbolKey, error) {
- v.enterFmt("Visiting type spec %s.%s", pkg.PkgPath, spec.Name.Name)
- defer v.exit()
-
- var err error
- switch t := spec.Type.(type) {
- case *ast.StructType:
- _, symKey, err := v.VisitStructType(file, specGenDecl, spec)
- if err != nil {
- return graphs.SymbolKey{}, v.frozenError(err)
- }
-
- return symKey, v.frozenIfError(err)
-
- case *ast.Ident:
- // Enum-like: alias of primitive (string, int, etc)
- // Optional: Check constants with the same name prefix to confirm it's "really" enum-like
- _, symKey, err := v.VisitEnumType(pkg, file, fVersion, specGenDecl, spec)
- return symKey, v.frozenIfError(err)
-
- case *ast.InterfaceType:
- // Check for special interface: Context
- if spec.Name.Name == "Context" && pkg.PkgPath == "context" {
- // Mark it as a special symbol
- key := v.context.GraphBuilder.AddSpecial(symboldg.SpecialTypeContext).Id
- return key, nil
- }
-
- // Otherwise, reject
- err = fmt.Errorf("interface type %q not supported (only Context is allowed)", spec.Name.Name)
-
- default:
- err = fmt.Errorf("unhandled TypeSpec type: %T", t)
- }
-
- return graphs.SymbolKey{}, err
-}
-
-func (v *RecursiveTypeVisitor) getAnnotations(
- onNodeDoc *ast.CommentGroup,
- nodeGenDecl *ast.GenDecl,
-) (*annotations.AnnotationHolder, error) {
- v.enter("Obtaining annotations for comment group")
- defer v.exit()
-
- var commentSource *ast.CommentGroup
- if onNodeDoc != nil {
- commentSource = onNodeDoc
- } else {
- if nodeGenDecl != nil {
- commentSource = nodeGenDecl.Doc
- }
- }
-
- if commentSource != nil {
- comments := gast.MapDocListToCommentBlock(commentSource.List, v.context.ArbitrationProvider.Pkg().FSet())
- holder, err := annotations.NewAnnotationHolder(comments, annotations.CommentSourceController)
- return &holder, err
- }
-
- return nil, nil
-}
-
-func (v *RecursiveTypeVisitor) buildTypeLayers(
- pkg *packages.Package,
- file *ast.File,
- expr ast.Expr,
- exprTypeName string,
-) ([]metadata.TypeLayer, error) {
- v.enterFmt("Building type layers for an expression named '%s'", exprTypeName)
- defer v.exit()
-
- switch t := expr.(type) {
- case *ast.StarExpr:
- inner, err := v.buildTypeLayers(pkg, file, t.X, exprTypeName)
- if err != nil {
- return nil, err
- }
- return append([]metadata.TypeLayer{metadata.NewPointerLayer()}, inner...), nil
-
- case *ast.ArrayType:
- inner, err := v.buildTypeLayers(pkg, file, t.Elt, exprTypeName)
- if err != nil {
- return nil, err
- }
- return append([]metadata.TypeLayer{metadata.NewArrayLayer()}, inner...), nil
-
- case *ast.MapType:
- keyLayers, err := v.buildTypeLayers(pkg, file, t.Key, exprTypeName)
- if err != nil {
- return nil, err
- }
- valueLayers, err := v.buildTypeLayers(pkg, file, t.Value, exprTypeName)
- if err != nil {
- return nil, err
- }
-
- if len(keyLayers) != 1 || keyLayers[0].Kind != metadata.TypeLayerKindBase {
- return nil, fmt.Errorf("map key must be a base type")
- }
- if len(valueLayers) != 1 || valueLayers[0].Kind != metadata.TypeLayerKindBase {
- return nil, fmt.Errorf("map value must be a base type")
- }
-
- return []metadata.TypeLayer{metadata.NewMapLayer(keyLayers[0].BaseTypeRef, valueLayers[0].BaseTypeRef)}, nil
-
- case *ast.Ident, *ast.SelectorExpr:
- if pkg == nil || file == nil {
- // E.g., this is a 'universe' type like 'string'
- typeKey := graphs.NewUniverseSymbolKey(exprTypeName)
- return []metadata.TypeLayer{metadata.NewBaseLayer(&typeKey)}, nil
- }
- baseKey, err := v.resolveBaseTypeKey(pkg, file, expr)
- if err != nil {
- return nil, err
- }
- return []metadata.TypeLayer{metadata.NewBaseLayer(baseKey)}, nil
-
- default:
- return nil, fmt.Errorf("unsupported type expression: %T", t)
- }
-}
-
-func (v *RecursiveTypeVisitor) resolveBaseTypeKey(
- pkg *packages.Package,
- file *ast.File,
- expr ast.Expr,
-) (*graphs.SymbolKey, error) {
- v.enterFmt("Resolving base type key for expression position %d in file %s", expr.Pos(), file.Name.Name)
- defer v.exit()
-
- resolved, err := gast.ResolveTypeSpecFromExpr(pkg, file, expr, v.context.ArbitrationProvider.Pkg().GetPackage)
- if err != nil {
- return nil, err
- }
-
- if resolved.DeclaringAstFile != nil && resolved.TypeSpec != nil {
- fVersion, err := v.context.MetadataCache.GetFileVersion(resolved.DeclaringAstFile, resolved.DeclaringPackage.Fset)
- if err != nil {
- return nil, err
- }
- key := graphs.NewSymbolKey(resolved.TypeSpec, fVersion)
- return &key, nil
- }
-
- key := graphs.NewUniverseSymbolKey(resolved.TypeName)
- return &key, nil
-}
-
-func getTypeSymKind(
- pkg *packages.Package,
- resolvedType gast.TypeSpecResolution,
-) common.SymKind {
- if _, isSpecial := symboldg.ToSpecialType(resolvedType.TypeName); isSpecial {
- return common.SymKindSpecialBuiltin
- }
-
- if resolvedType.IsUniverse {
- return common.SymKindBuiltin
- }
-
- if resolvedType.TypeSpec == nil {
- return common.SymKindUnknown
- }
-
- switch resolvedType.TypeSpec.Type.(type) {
- case *ast.StructType:
- return common.SymKindStruct
-
- case *ast.InterfaceType:
- if resolvedType.TypeSpec.Name.Name == "Context" && pkg.PkgPath == "context" {
- return common.SymKindSpecialBuiltin
- }
- return common.SymKindInterface
-
- case *ast.Ident:
- // Use your enum detection here — e.g., check constants or alias metadata
- if gast.IsEnumLike(pkg, resolvedType.TypeSpec) {
- return common.SymKindEnum
- }
- return common.SymKindAlias
-
- default:
- return common.SymKindUnknown
- }
-}
diff --git a/core/visitors/typedecl.visitor.go b/core/visitors/typedecl.visitor.go
new file mode 100644
index 0000000..e5c1740
--- /dev/null
+++ b/core/visitors/typedecl.visitor.go
@@ -0,0 +1,184 @@
+package visitors
+
+import (
+ "fmt"
+ "go/ast"
+
+ "golang.org/x/tools/go/packages"
+
+ "github.com/gopher-fleece/gleece/gast"
+ "github.com/gopher-fleece/gleece/graphs"
+)
+
+// TypeDeclVisitor dispatches type declarations to specialized visitors for processing
+type TypeDeclVisitor struct {
+ BaseVisitor
+
+ structVisitor *StructVisitor
+ enumVisitor *EnumVisitor
+ aliasVisitor *AliasVisitor
+}
+
+// NewTypeDeclVisitor constructs a TypeDeclVisitor and internal sub-visitors.
+func NewTypeDeclVisitor(ctx *VisitContext) (*TypeDeclVisitor, error) {
+ v := &TypeDeclVisitor{}
+ if err := v.initialize(ctx); err != nil {
+ return nil, err
+ }
+
+ return v, nil
+}
+
+func (v *TypeDeclVisitor) setStructVisitor(visitor *StructVisitor) {
+ v.structVisitor = visitor
+}
+
+func (v *TypeDeclVisitor) setEnumVisitor(visitor *EnumVisitor) {
+ v.enumVisitor = visitor
+}
+
+func (v *TypeDeclVisitor) setAliasVisitor(visitor *AliasVisitor) {
+ v.aliasVisitor = visitor
+}
+
+// VisitTypeDecl inspects the TypeSpec and routes to the appropriate decl visitor.
+// Returns the resulting symbol key for the type (graph node id) or an error.
+func (v *TypeDeclVisitor) VisitTypeDecl(
+ pkg *packages.Package,
+ file *ast.File,
+ genDecl *ast.GenDecl,
+ typeSpec *ast.TypeSpec,
+) (graphs.SymbolKey, error) {
+
+ v.enterFmt("TypeDeclVisitor.VisitTypeDecl %s in %s", typeSpec.Name.Name, pkg.PkgPath)
+ defer v.exit()
+
+ fileVersion, fvErr := v.context.MetadataCache.GetFileVersion(file, pkg.Fset)
+ if fvErr != nil || fileVersion == nil {
+ return graphs.SymbolKey{}, fmt.Errorf("could not obtain file version for type %s: %w", typeSpec.Name.Name, fvErr)
+ }
+
+ // An assigned type, e.g. `type Foo = Bar`
+ if typeSpec.Assign != 0 {
+ return v.visitAssignedType(pkg, file, fileVersion, genDecl, typeSpec)
+ }
+
+ // Dispatch the node to the relevant visitor
+ switch typeSpec.Type.(type) {
+ case *ast.StructType:
+ // StructVisitor is responsible for materializing the struct + fields in graph
+ _, symKey, err := v.structVisitor.VisitStructType(pkg, file, genDecl, typeSpec)
+ return symKey, err
+
+ case *ast.Ident:
+ // Idents here may be enums or assigned aliases.
+ // Since Go's AST does not actually have a concept of enum or even an alias, we have to
+ // do some heuristics here.
+ //
+ // We basically try extracting an enum for the declaration; If it works, we're looking at an enum.
+ // If it doesn't, we're looking at an alias (or a logic error)
+ if gast.IsEnumLike(pkg, typeSpec) {
+ _, enumSymKey, err := v.enumVisitor.VisitEnumType(pkg, file, fileVersion, genDecl, typeSpec)
+ return enumSymKey, err
+ }
+ return v.visitAssignedType(pkg, file, fileVersion, genDecl, typeSpec)
+
+ case *ast.SelectorExpr:
+ // An alias for a type import like
+ //
+ // type A = time.Time
+ return v.aliasVisitor.VisitAlias(pkg, file, genDecl, typeSpec)
+
+ case *ast.InterfaceType:
+ // Currently, the only interface we care about or support is context.Context
+ specName := gast.GetIdentNameOrFallback(typeSpec.Name, "")
+ if pkg.PkgPath == "context" && specName == "Context" {
+ return graphs.NewNonUniverseBuiltInSymbolKey("context.Context"), nil
+ }
+ }
+
+ return graphs.SymbolKey{}, fmt.Errorf(
+ "type-spec '%s' has an unexpected underlying type expression '%v'",
+ typeSpec.Name.Name,
+ typeSpec.Type,
+ )
+}
+
+// visitAssignedType attempts to parse an assigned type declaration like
+//
+// type Something = string
+//
+// into either an enum or an alias.
+//
+// This process is somewhat heuristic due to Go's AST not including a true 'enum' or 'alias' type nodes.
+func (v *TypeDeclVisitor) visitAssignedType(
+ pkg *packages.Package,
+ file *ast.File,
+ fileVersion *gast.FileVersion,
+ genDecl *ast.GenDecl,
+ typeSpec *ast.TypeSpec,
+) (graphs.SymbolKey, error) {
+ // Aliases are logically a subset of enums. As such, we have to first see if a declaration is an enum before deciding its an alias.
+ //
+ // As enum parsing is generally pretty expensive we actually try the full enum extraction here instead of having a dedicated check (which wouldn't be much cheaper)
+ // If it works, great, we're done. If it doesn't we try parsing as an alias.
+ _, enumKey, err := v.enumVisitor.VisitEnumType(pkg, file, fileVersion, genDecl, typeSpec)
+ if err == nil {
+ return enumKey, nil
+ }
+
+ if _, isUnexpectedEntity := err.(UnexpectedEntityError); isUnexpectedEntity {
+ return v.aliasVisitor.VisitAlias(pkg, file, genDecl, typeSpec)
+ }
+
+ return graphs.SymbolKey{}, fmt.Errorf(
+ "enum visitor for type %s yielded an error - %v",
+ gast.GetIdentNameOrFallback(typeSpec.Name, "Unknown"),
+ err,
+ )
+}
+
+// EnsureDeclMaterialized guarantees the given TypeSpec is materialized (cached + inserted in graph).
+// It is idempotent: repeated calls return quickly. It uses the cache's StartMaterializing/FinishMaterializing
+// to avoid recursion with recursive/mutually-recursive types.
+func (v *TypeDeclVisitor) EnsureDeclMaterialized(
+ pkg *packages.Package,
+ file *ast.File,
+ genDecl *ast.GenDecl,
+ typeSpec *ast.TypeSpec,
+) (graphs.SymbolKey, error) {
+ v.enterFmt("EnsureDeclMaterialized %s in %s", typeSpec.Name.Name, pkg.PkgPath)
+ defer v.exit()
+
+ // Strictly obtain file version for this declaring file.
+ fileVersion, fvErr := v.context.MetadataCache.GetFileVersion(file, pkg.Fset)
+ if fvErr != nil || fileVersion == nil {
+ return graphs.SymbolKey{}, fmt.Errorf("could not obtain file version for type %s: %w", typeSpec.Name.Name, fvErr)
+ }
+
+ declKey := graphs.NewSymbolKey(typeSpec, fileVersion)
+
+ // Fast path - already materialized/visited
+ if v.context.MetadataCache.HasVisited(declKey) {
+ return declKey, nil
+ }
+
+ // Try to claim materialization responsibility.
+ // If StartMaterializing returns false, the declaration is either already visited
+ // or another in-flight materializer is processing it — return the declKey to break recursion.
+ if !v.context.MetadataCache.StartMaterializing(declKey) {
+ return declKey, nil
+ }
+
+ // We are now the materializer for this declaration.
+ symKey, err := v.VisitTypeDecl(pkg, file, genDecl, typeSpec)
+ if err != nil {
+ // Mark the materialization as 'failed'
+ v.context.MetadataCache.FinishMaterializing(declKey, false)
+ return declKey, err
+ }
+
+ // Mark the materialization as 'successful'
+ v.context.MetadataCache.FinishMaterializing(declKey, true)
+ return symKey, nil
+}
diff --git a/definitions/structs.go b/definitions/structs.go
index 2ddd618..5a0e5e5 100644
--- a/definitions/structs.go
+++ b/definitions/structs.go
@@ -79,6 +79,14 @@ func (a AliasMetadata) Equals(other AliasMetadata) bool {
return true
}
+type NakedAliasMetadata struct {
+ Name string // e.g. MyStringAlias
+ PkgPath string
+ Type string // e.g. string, int, etc.
+ Description string
+ Deprecation DeprecationOptions
+}
+
type TypeMetadata struct {
Name string
PkgPath string
@@ -249,6 +257,36 @@ type StructMetadata struct {
Deprecation DeprecationOptions
}
+func (s StructMetadata) Clone() StructMetadata {
+ fields := make([]FieldMetadata, 0, len(s.Fields))
+ for _, field := range s.Fields {
+ var deprecationOpts *DeprecationOptions
+ if field.Deprecation != nil {
+ deprecationOpts = &DeprecationOptions{
+ Deprecated: field.Deprecation.Deprecated,
+ Description: field.Deprecation.Description,
+ }
+ }
+
+ fields = append(fields, FieldMetadata{
+ Name: field.Name,
+ Type: field.Type,
+ Description: field.Description,
+ Tag: field.Tag,
+ IsEmbedded: field.IsEmbedded,
+ Deprecation: deprecationOpts,
+ })
+ }
+
+ return StructMetadata{
+ Name: s.Name,
+ PkgPath: s.PkgPath,
+ Description: s.Description,
+ Fields: fields,
+ Deprecation: s.Deprecation,
+ }
+}
+
type EnumMetadata struct {
Name string
PkgPath string
@@ -275,6 +313,7 @@ type EnumValidator struct {
type Models struct {
Structs []StructMetadata
Enums []EnumMetadata
+ Aliases []NakedAliasMetadata
EnumValidators []EnumValidator
}
@@ -357,7 +396,8 @@ type AuthorizationConfig struct {
}
type CommonConfig struct {
- ControllerGlobs []string `json:"controllerGlobs" validate:"omitempty,min=1"`
+ ControllerGlobs []string `json:"controllerGlobs" validate:"omitempty,min=1"`
+ AllowPackageLoadFailures bool `json:"allowPackageLoadFailures"`
}
type ExperimentalConfig struct {
diff --git a/gast/ast.utils.go b/gast/ast.utils.go
index e27ec44..cc37fe5 100644
--- a/gast/ast.utils.go
+++ b/gast/ast.utils.go
@@ -14,6 +14,8 @@ import (
"golang.org/x/tools/go/packages"
)
+type GetPackageMethod func(string) (*packages.Package, error)
+
// IsFuncDeclReceiverForStruct determines if the given FuncDecl is a receiver for a struct with the given name
func IsFuncDeclReceiverForStruct(structName string, funcDecl *ast.FuncDecl) bool {
if funcDecl.Recv == nil || len(funcDecl.Recv.List) <= 0 {
@@ -372,6 +374,22 @@ func GetIdentFromExpr(expr ast.Expr) *ast.Ident {
return nil
}
+func GetIdentNameFromExpr(expr ast.Expr) *string {
+ ident := GetIdentFromExpr(expr)
+ if ident != nil {
+ return common.Ptr(ident.Name)
+ }
+ return nil
+}
+
+func GetIdentNameOrFallback(ident *ast.Ident, fallback string) string {
+ if ident != nil {
+ return ident.Name
+ }
+
+ return fallback
+}
+
type TypeSpecResolution struct {
IsUniverse bool
TypeName string
@@ -390,20 +408,33 @@ func (t TypeSpecResolution) String() string {
return fmt.Sprintf("Type %s.%s", t.DeclaringPackage.PkgPath, t.TypeName)
}
-// ResolveTypeSpecFromField Resolves type information from the given field.
-// Returns the declaring *packages.Package, *ast.File and the associated *ast.TypeSpec
-// Has 3 possible outcomes:
-//
-// * Returns all of the above (the field references a concrete type somewhere)
-// * Returns 4 nils - the field's type is a 'Universe' one
-// * Returns an error
-func ResolveTypeSpecFromField(
- declaringPkg *packages.Package, // <<<< Used only for locally defined type references
- declaringFile *ast.File,
- field *ast.Field,
- pkgResolver func(pkgPath string) (*packages.Package, error),
-) (TypeSpecResolution, error) {
- return ResolveTypeSpecFromExpr(declaringPkg, declaringFile, field.Type, pkgResolver)
+func (t TypeSpecResolution) IsPrimitive() bool {
+ _, isPrimitive := common.ToPrimitiveType(t.GetQualifiedName())
+ return isPrimitive
+}
+
+func (t TypeSpecResolution) IsSpecial() bool {
+ _, isSpecial := common.ToSpecialType(t.GetQualifiedName())
+ return isSpecial
+}
+
+func (t TypeSpecResolution) IsBuiltin() bool {
+ return t.IsUniverse || t.IsPrimitive() || t.IsSpecial()
+}
+
+func (t TypeSpecResolution) IsContext() bool {
+ if t.DeclaringPackage == nil || t.TypeSpec.Name == nil {
+ return false
+ }
+
+ return t.TypeSpec.Name.Name == "Context" && t.DeclaringPackage.PkgPath == "context"
+}
+
+func (t TypeSpecResolution) GetQualifiedName() string {
+ if t.DeclaringPackage != nil {
+ return fmt.Sprintf("%s.%s", t.DeclaringPackage.Name, t.TypeName)
+ }
+ return t.TypeName
}
// ResolveTypeSpecFromExpr resolves a type expression to its defining TypeSpec,
@@ -412,7 +443,7 @@ func ResolveTypeSpecFromExpr(
pkg *packages.Package,
file *ast.File,
expr ast.Expr,
- getPkg func(pkgPath string) (*packages.Package, error), // typically your package resolver
+ getPkg GetPackageMethod,
) (TypeSpecResolution, error) {
ident := GetIdentFromExpr(expr)
if ident == nil {
@@ -609,6 +640,15 @@ func IsEnumLike(pkg *packages.Package, spec *ast.TypeSpec) bool {
if !ok {
continue
}
+
+ // To avoid clobbering consts that happen to have the same type under aliases/enums,
+ // we check the actual underlying type name as well.
+ if curObjAlias, isCurObjAlias := constVal.Type().(*types.Alias); isCurObjAlias {
+ if typeName.Name() != curObjAlias.Obj().Name() {
+ continue
+ }
+ }
+
if types.Identical(constVal.Type(), typeName.Type()) {
return true
}
@@ -770,3 +810,182 @@ func GetCommentsFromTypeSpec(
Comments: []CommentNode{},
}
}
+
+// GetBaseIdent returns the base identifier for an expression.
+// Examples:
+//
+// Ident -> ident
+// IndexExpr(X[Arg]) -> base of X
+// SelectorExpr(Pkg.Type) -> sel (Type)
+//
+// Returns nil when there is no base ident (inline struct, func type, etc).
+func GetBaseIdent(expr ast.Expr) *ast.Ident {
+ switch t := expr.(type) {
+ case *ast.Ident:
+ return t
+ case *ast.SelectorExpr:
+ // we return the selector identifier (Type in pkg.Type)
+ return t.Sel
+ case *ast.IndexExpr:
+ return GetBaseIdent(t.X)
+ case *ast.IndexListExpr:
+ return GetBaseIdent(t.X)
+ case *ast.StarExpr:
+ return GetBaseIdent(t.X)
+ case *ast.ArrayType:
+ return GetBaseIdent(t.Elt)
+ default:
+ return nil
+ }
+}
+
+// ResolveNamedType resolves the named type behind expr (if any).
+// Returns (TypeSpecResolution, ok, err).
+// ok==false means "no named type found or not resolvable" (not an error).
+func ResolveNamedType(
+ pkg *packages.Package,
+ file *ast.File,
+ expr ast.Expr,
+ getPkg GetPackageMethod,
+) (TypeSpecResolution, bool, error) {
+
+ ident := GetBaseIdent(expr)
+ if ident == nil {
+ // Not a named type expression (inline, func, etc).
+ return TypeSpecResolution{}, false, nil
+ }
+
+ // Prefer Uses map.
+ if obj := pkg.TypesInfo.Uses[ident]; obj != nil {
+ return buildResolutionFromObject(obj, getPkg)
+ }
+
+ // Fallback to package scope lookup (dot-imports etc).
+ if obj := pkg.Types.Scope().Lookup(ident.Name); obj != nil {
+ return buildResolutionFromObject(obj, getPkg)
+ }
+
+ // Not resolved here; caller may try other packages or treat as unknown.
+ return TypeSpecResolution{}, false, nil
+}
+
+func buildResolutionFromObject(obj types.Object, getPkg GetPackageMethod) (TypeSpecResolution, bool, error) {
+ _, ok := obj.(*types.TypeName)
+ if !ok {
+ return TypeSpecResolution{}, false,
+ fmt.Errorf("resolved object is not a type: %T", obj)
+ }
+
+ // Universe / builtin
+ if obj.Pkg() == nil && types.Universe.Lookup(obj.Name()) == obj {
+ return TypeSpecResolution{
+ TypeName: obj.Name(),
+ IsUniverse: true,
+ }, true, nil
+ }
+
+ declPkg, err := getPkg(obj.Pkg().Path())
+ if err != nil || declPkg == nil {
+ return TypeSpecResolution{}, false,
+ fmt.Errorf("could not locate declaring package: %s", obj.Pkg().Path())
+ }
+
+ pos := obj.Pos()
+ if pos != token.NoPos && declPkg.Fset != nil {
+ // get filename that contains the object's position
+ targetName := declPkg.Fset.Position(pos).Filename
+
+ for _, file := range declPkg.Syntax {
+ // compare filenames derived from positions
+ filename := declPkg.Fset.Position(file.Pos()).Filename
+ if filename != targetName {
+ continue
+ }
+ if typeSpec, genDecl := findTypeSpecInFile(file, obj.Name()); typeSpec != nil {
+ return TypeSpecResolution{
+ DeclaringPackage: declPkg,
+ DeclaringAstFile: file,
+ TypeSpec: typeSpec,
+ GenDecl: genDecl,
+ TypeName: obj.Name(),
+ IsUniverse: false,
+ }, true, nil
+ }
+ // matched file but not found in it -> stop searching by filename
+ break
+ }
+ }
+
+ // fallback: scan whole package
+ if file, typeSpec, genDecl, ok := scanPackageForTypeSpec(declPkg, obj.Name()); ok {
+ return TypeSpecResolution{
+ DeclaringPackage: declPkg,
+ DeclaringAstFile: file,
+ TypeSpec: typeSpec,
+ GenDecl: genDecl,
+ TypeName: obj.Name(),
+ IsUniverse: false,
+ }, true, nil
+ }
+
+ return TypeSpecResolution{}, false, nil
+}
+
+// scanPackageForTypeSpec scans all files in the package to find the named type.
+// Returns (astFile, TypeSpec, GenDecl, ok).
+func scanPackageForTypeSpec(
+ p *packages.Package,
+ name string,
+) (*ast.File, *ast.TypeSpec, *ast.GenDecl, bool) {
+ for _, f := range p.Syntax {
+ if ts, gd := findTypeSpecInFile(f, name); ts != nil {
+ return f, ts, gd, true
+ }
+ }
+ return nil, nil, nil, false
+}
+
+// findTypeSpecInFile searches the given ast.File for type spec with the given name.
+func findTypeSpecInFile(f *ast.File, name string) (*ast.TypeSpec, *ast.GenDecl) {
+ for _, decl := range f.Decls {
+ gen, ok := decl.(*ast.GenDecl)
+ if !ok || gen.Tok != token.TYPE {
+ continue
+ }
+ for _, s := range gen.Specs {
+ ts, ok := s.(*ast.TypeSpec)
+ if !ok {
+ continue
+ }
+ if ts.Name != nil && ts.Name.Name == name {
+ return ts, gen
+ }
+ }
+ }
+ return nil, nil
+}
+
+func IsEmbeddedOrAnonymousField(field *ast.Field) bool {
+ return len(field.Names) == 0
+}
+
+// GetFieldNames returns the declared names for the field, or an empty slice for embedded/anonymous fields
+func GetFieldNames(field *ast.Field) []string {
+ if field == nil {
+ return nil
+ }
+
+ if IsEmbeddedOrAnonymousField(field) {
+ return []string{}
+ }
+
+ out := make([]string, 0, len(field.Names))
+ for _, id := range field.Names {
+ if id == nil {
+ out = append(out, "")
+ } else {
+ out = append(out, id.Name)
+ }
+ }
+ return out
+}
diff --git a/graphs/dot/builder.go b/graphs/dot/builder.go
index f6857c8..d9a01a1 100644
--- a/graphs/dot/builder.go
+++ b/graphs/dot/builder.go
@@ -2,8 +2,10 @@ package dot
import (
"fmt"
+ "slices"
"strings"
+ mapset "github.com/deckarep/golang-set/v2"
"github.com/gopher-fleece/gleece/common"
"github.com/gopher-fleece/gleece/graphs"
)
@@ -19,6 +21,8 @@ type DotBuilder struct {
theme DotTheme
idMap map[graphs.SymbolKey]string
counter int
+
+ usedNodeTypes mapset.Set[common.SymKind]
}
func NewDotBuilder(theme *DotTheme) *DotBuilder {
@@ -27,14 +31,22 @@ func NewDotBuilder(theme *DotTheme) *DotBuilder {
themeToUse = *theme
}
db := &DotBuilder{
- theme: themeToUse,
- idMap: make(map[graphs.SymbolKey]string),
+ theme: themeToUse,
+ idMap: make(map[graphs.SymbolKey]string),
+ usedNodeTypes: mapset.NewSet[common.SymKind](),
}
db.sb.WriteString("digraph SymbolGraph {\n")
db.sb.WriteString(fmt.Sprintf(" rankdir=%s;\n", themeToUse.Direction))
return db
}
+
+func (db *DotBuilder) LegendEnabled() bool {
+ return db.theme.LegendEnabled
+}
+
func (db *DotBuilder) AddNode(key graphs.SymbolKey, kind common.SymKind, label string) {
+ db.usedNodeTypes.Add(kind)
+
id := db.getId(key)
style, ok := db.theme.NodeStyles[kind]
if !ok {
@@ -51,16 +63,26 @@ func (db *DotBuilder) AddNode(key graphs.SymbolKey, kind common.SymKind, label s
}
db.sb.WriteString(fmt.Sprintf(
" %s [label=\"%s (%s)\", shape=%s, style=filled, fillcolor=\"%s\", fontcolor=\"%s\"];\n",
- id, label, kind, style.Shape, style.Color, style.FontColor))
+ id,
+ label,
+ kind,
+ style.Shape,
+ style.Color,
+ style.FontColor,
+ ))
}
-func (db *DotBuilder) AddEdge(from, to graphs.SymbolKey, kind string) {
- fromID := db.getId(from)
+func (db *DotBuilder) AddEdge(
+ from, to graphs.SymbolKey,
+ kind string,
+ suffix *string,
+) {
+ fromId := db.getId(from)
- toID, ok := db.idMap[to]
+ toId, ok := db.idMap[to]
if !ok {
db.addErrorNodeOnce()
- toID = errorNodeId
+ toId = errorNodeId
kind = "error"
}
@@ -69,6 +91,10 @@ func (db *DotBuilder) AddEdge(from, to graphs.SymbolKey, kind string) {
label = kind
}
+ if suffix != nil {
+ label = label + *suffix
+ }
+
style, ok := db.theme.EdgeStyles[kind]
if !ok {
style = DotStyle{}
@@ -85,32 +111,52 @@ func (db *DotBuilder) AddEdge(from, to graphs.SymbolKey, kind string) {
db.sb.WriteString(fmt.Sprintf(
" %s -> %s [label=\"%s\", color=\"%s\", style=\"%s\", arrowhead=\"%s\"];\n",
- fromID, toID, label, style.EdgeColor, style.EdgeStyle, style.ArrowHead))
+ fromId,
+ toId,
+ label,
+ style.EdgeColor,
+ style.EdgeStyle,
+ style.ArrowHead,
+ ))
}
func (db *DotBuilder) RenderLegend() {
+ if db.usedNodeTypes.IsEmpty() {
+ // Empty graph - no need to render legend
+ return
+ }
+
db.sb.WriteString(" subgraph cluster_legend {\n")
db.sb.WriteString(" label = \"Legend\";\n style = dashed;\n")
i := 0
- for _, nodeStyle := range db.theme.NodeStylesOrdered() {
- color := nodeStyle.Style.Color
+ usedNodeTypes := db.usedNodeTypes.ToSlice()
+ slices.Sort(usedNodeTypes)
+
+ for _, kind := range usedNodeTypes {
+ nodeStyle := db.theme.NodeStyles[kind]
+
+ color := nodeStyle.Color
if color == "" {
color = "gray90"
}
- shape := nodeStyle.Style.Shape
+ shape := nodeStyle.Shape
if shape == "" {
shape = "ellipse"
}
db.sb.WriteString(fmt.Sprintf(
" L%d [label=\"%s\", style=filled, shape=%s, fillcolor=\"%s\"];\n",
- i, nodeStyle.Kind, shape, color))
+ i, kind, shape, color))
i++
}
db.sb.WriteString(" }\n")
}
func (db *DotBuilder) Finish() string {
+ if db.LegendEnabled() {
+ db.RenderLegend()
+ }
+
db.sb.WriteString("}\n")
return db.sb.String()
}
diff --git a/graphs/dot/themes.go b/graphs/dot/themes.go
index c1cbbc8..db9725a 100644
--- a/graphs/dot/themes.go
+++ b/graphs/dot/themes.go
@@ -1,8 +1,6 @@
package dot
import (
- "sort"
-
"github.com/gopher-fleece/gleece/common"
)
@@ -31,6 +29,7 @@ type OrderedNodeStyle struct {
}
type DotTheme struct {
+ LegendEnabled bool
NodeStyles map[common.SymKind]DotStyle
EdgeStyles map[string]DotStyle
EdgeLabels map[string]string
@@ -38,56 +37,41 @@ type DotTheme struct {
Direction RankDir
}
-func (t DotTheme) NodeStylesOrdered() []OrderedNodeStyle {
- var kinds []common.SymKind
- for k := range t.NodeStyles {
- kinds = append(kinds, k)
- }
-
- // Sort the kinds alphabetically (or by some other stable order)
- sort.Slice(kinds, func(i, j int) bool {
- return kinds[i] < kinds[j]
- })
-
- // Build ordered slice
- ordered := make([]OrderedNodeStyle, len(kinds))
- for i, k := range kinds {
- ordered[i] = OrderedNodeStyle{
- Kind: k,
- Style: t.NodeStyles[k],
- }
- }
- return ordered
-}
-
var DefaultDotTheme = DotTheme{
+ LegendEnabled: true,
NodeStyles: map[common.SymKind]DotStyle{
- common.SymKindStruct: {Color: "lightblue", Shape: "box"},
- common.SymKindField: {Color: "gold", Shape: "ellipse"},
- common.SymKindEnum: {Color: "mediumpurple", Shape: "folder"},
- common.SymKindEnumValue: {Color: "plum", Shape: "note"},
- common.SymKindReceiver: {Color: "orange", Shape: "hexagon"},
- common.SymKindFunction: {Color: "darkseagreen", Shape: "oval"},
- common.SymKindParameter: {Color: "khaki", Shape: "parallelogram"},
- common.SymKindReturnType: {Color: "lightgrey", Shape: "diamond"},
- common.SymKindAlias: {Color: "palegreen", Shape: "note"},
- common.SymKindConstant: {Color: "plum", Shape: "egg"},
- common.SymKindBuiltin: {Color: "gray80", Shape: "box"},
- common.SymKindUnknown: {Color: "lightcoral", Shape: "triangle"},
- common.SymKindInterface: {Color: "lightskyblue", Shape: "component"},
- common.SymKindPackage: {Color: "lightyellow", Shape: "folder"},
- common.SymKindController: {Color: "lightcyan", Shape: "octagon"},
- common.SymKindVariable: {Color: "lightsteelblue", Shape: "circle"},
+ common.SymKindStruct: {Color: "lightblue", Shape: "box"},
+ common.SymKindField: {Color: "gold", Shape: "ellipse"},
+ common.SymKindEnum: {Color: "mediumpurple", Shape: "folder"},
+ common.SymKindEnumValue: {Color: "plum", Shape: "note"},
+ common.SymKindReceiver: {Color: "orange", Shape: "signature"},
+ common.SymKindFunction: {Color: "darkseagreen", Shape: "oval"},
+ common.SymKindParameter: {Color: "khaki", Shape: "parallelogram"},
+ common.SymKindTypeParam: {Color: "lightslateblue", Shape: "rect"},
+ common.SymKindReturnType: {Color: "lightgrey", Shape: "cds"},
+ common.SymKindAlias: {Color: "palegreen", Shape: "note"},
+ common.SymKindComposite: {Color: "lightcoral", Shape: "component"},
+ common.SymKindConstant: {Color: "plum", Shape: "egg"},
+ common.SymKindBuiltin: {Color: "green3", Shape: "box"},
+ common.SymKindSpecialBuiltin: {Color: "greenyellow", Shape: "box"},
+ common.SymKindUnknown: {Color: "lightcoral", Shape: "triangle"},
+ common.SymKindInterface: {Color: "lightskyblue", Shape: "component"},
+ common.SymKindPackage: {Color: "lightyellow", Shape: "folder"},
+ common.SymKindController: {Color: "lightcyan", Shape: "octagon"},
+ common.SymKindVariable: {Color: "lightsteelblue", Shape: "circle"},
},
EdgeLabels: map[string]string{
- "ty": "Type",
- "ret": "Return Value",
- "param": "Parameter",
- "fld": "Field",
- "recv": "Receiver",
- "val": "Value",
- "init": "Initialize",
- "ref": "Reference",
+ "ty": "Type",
+ "typaram": "Type Parameter",
+ "ret": "Return Value",
+ "param": "Parameter",
+ "fld": "Field",
+ "recv": "Receiver",
+ "val": "Value",
+ "init": "Initialize",
+ "ref": "Reference",
+ "inst": "Instantiates",
+ "alias": "Alias",
},
EdgeStyles: map[string]DotStyle{
"ty": {EdgeColor: "black", EdgeStyle: "solid", ArrowHead: "vee"},
diff --git a/graphs/symboldg/edges.go b/graphs/symboldg/edges.go
index 0d85973..4403612 100644
--- a/graphs/symboldg/edges.go
+++ b/graphs/symboldg/edges.go
@@ -12,9 +12,11 @@ const (
EdgeKindEmbed SymbolEdgeKind = "embed" // Struct → embedded type (anonymous field)
// Type and usage relationships
- EdgeKindReference SymbolEdgeKind = "ref" // General type reference (param → type)
- EdgeKindType SymbolEdgeKind = "ty" // Specific "has type" relationship
- EdgeKindAlias SymbolEdgeKind = "alias" // Symbol → aliased symbol
+ EdgeKindReference SymbolEdgeKind = "ref" // General type reference (param → type)
+ EdgeKindType SymbolEdgeKind = "ty" // Specific "has type" relationship
+ EdgeKindTypeParameter SymbolEdgeKind = "typaram" // An instantiation type parameter like 'string' and int in 'map[string]int'
+ EdgeKindInstantiates SymbolEdgeKind = "inst" // A generic type instantiation like "SomeStruct[string]"
+ EdgeKindAlias SymbolEdgeKind = "alias" // Symbol → aliased symbol
// Code behavior
EdgeKindCall SymbolEdgeKind = "call" // Func → called func
@@ -26,7 +28,7 @@ const (
EdgeKindDocument SymbolEdgeKind = "doc" // Symbol → doc/annotation node
// Misc
- EdgeKindInit SymbolEdgeKind = "init" // Struct → init function (e.g., for defaults)
+ EdgeKindInitialize SymbolEdgeKind = "init" // Struct → init function (e.g., for defaults)
)
type SymbolEdge struct {
diff --git a/graphs/symboldg/enumerations.go b/graphs/symboldg/enumerations.go
deleted file mode 100644
index c665c24..0000000
--- a/graphs/symboldg/enumerations.go
+++ /dev/null
@@ -1,109 +0,0 @@
-package symboldg
-
-type PrimitiveType string
-
-const (
- PrimitiveTypeBool PrimitiveType = "bool"
- PrimitiveTypeString PrimitiveType = "string"
-
- // Signed integers
- PrimitiveTypeInt PrimitiveType = "int"
- PrimitiveTypeInt8 PrimitiveType = "int8"
- PrimitiveTypeInt16 PrimitiveType = "int16"
- PrimitiveTypeInt32 PrimitiveType = "int32"
- PrimitiveTypeInt64 PrimitiveType = "int64"
-
- // Unsigned integers
- PrimitiveTypeUint PrimitiveType = "uint"
- PrimitiveTypeUint8 PrimitiveType = "uint8"
- PrimitiveTypeUint16 PrimitiveType = "uint16"
- PrimitiveTypeUint32 PrimitiveType = "uint32"
- PrimitiveTypeUint64 PrimitiveType = "uint64"
- PrimitiveTypeUintptr PrimitiveType = "uintptr"
-
- // Aliases
- PrimitiveTypeByte PrimitiveType = "byte" // alias for uint8
- PrimitiveTypeRune PrimitiveType = "rune" // alias for int32
-
- // Floating point numbers
- PrimitiveTypeFloat32 PrimitiveType = "float32"
- PrimitiveTypeFloat64 PrimitiveType = "float64"
-
- // Complex numbers
- PrimitiveTypeComplex64 PrimitiveType = "complex64"
- PrimitiveTypeComplex128 PrimitiveType = "complex128"
-)
-
-// ToPrimitiveType checks if the given string represents a valid PrimitiveType.
-// If it does, it returns (corresponding PrimitiveType, true).
-func ToPrimitiveType(typeName string) (PrimitiveType, bool) {
- switch typeName {
- case
- string(PrimitiveTypeBool),
- string(PrimitiveTypeString),
-
- string(PrimitiveTypeInt),
- string(PrimitiveTypeInt8),
- string(PrimitiveTypeInt16),
- string(PrimitiveTypeInt32),
- string(PrimitiveTypeInt64),
-
- string(PrimitiveTypeUint),
- string(PrimitiveTypeUint8),
- string(PrimitiveTypeUint16),
- string(PrimitiveTypeUint32),
- string(PrimitiveTypeUint64),
- string(PrimitiveTypeUintptr),
-
- string(PrimitiveTypeByte),
- string(PrimitiveTypeRune),
-
- string(PrimitiveTypeFloat32),
- string(PrimitiveTypeFloat64),
-
- string(PrimitiveTypeComplex64),
- string(PrimitiveTypeComplex128):
- return PrimitiveType(typeName), true
- default:
- return "", false
- }
-}
-
-type SpecialType string
-
-const (
- SpecialTypeError SpecialType = "error"
- SpecialTypeEmptyInterface SpecialType = "interface{}"
- SpecialTypeContext SpecialType = "context.Context"
- SpecialTypeTime SpecialType = "time.Time"
- SpecialTypeAny SpecialType = "any" // alias of interface{}
- SpecialTypeUnsafePointer SpecialType = "unsafe.Pointer"
-)
-
-func (s SpecialType) IsUniverse() bool {
- switch s {
- case SpecialTypeError, SpecialTypeEmptyInterface, SpecialTypeAny:
- return true
- }
-
- return false
-}
-
-func ToSpecialType(s string) (SpecialType, bool) {
- switch s {
- case "error":
- return SpecialTypeError, true
- case "interface{}":
- return SpecialTypeEmptyInterface, true
- case "any":
- return SpecialTypeAny, true
- case "context.Context":
- return SpecialTypeContext, true
- case "time.Time":
- return SpecialTypeTime, true
- case "unsafe.Pointer":
- return SpecialTypeUnsafePointer, true
- default:
- return "", false
- }
-}
diff --git a/graphs/symboldg/graph.go b/graphs/symboldg/graph.go
index b84e08a..55f8fdc 100644
--- a/graphs/symboldg/graph.go
+++ b/graphs/symboldg/graph.go
@@ -7,53 +7,17 @@ import (
"slices"
"strings"
+ mapset "github.com/deckarep/golang-set/v2"
"github.com/gopher-fleece/gleece/common"
"github.com/gopher-fleece/gleece/common/linq"
"github.com/gopher-fleece/gleece/core/annotations"
"github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/core/metadata/typeref"
"github.com/gopher-fleece/gleece/gast"
"github.com/gopher-fleece/gleece/graphs"
"github.com/gopher-fleece/gleece/graphs/dot"
)
-type SymbolGraphBuilder interface {
- AddController(request CreateControllerNode) (*SymbolNode, error)
- AddRoute(request CreateRouteNode) (*SymbolNode, error)
- AddRouteParam(request CreateParameterNode) (*SymbolNode, error)
- AddRouteRetVal(request CreateReturnValueNode) (*SymbolNode, error)
- AddStruct(request CreateStructNode) (*SymbolNode, error)
- AddEnum(request CreateEnumNode) (*SymbolNode, error)
- AddField(request CreateFieldNode) (*SymbolNode, error)
- AddConst(request CreateConstNode) (*SymbolNode, error)
- AddPrimitive(kind PrimitiveType) *SymbolNode
- AddSpecial(special SpecialType) *SymbolNode
- AddEdge(from, to graphs.SymbolKey, kind SymbolEdgeKind, meta map[string]string)
- RemoveEdge(from, to graphs.SymbolKey, kind *SymbolEdgeKind)
- RemoveNode(key graphs.SymbolKey)
-
- Structs() []metadata.StructMeta
- Enums() []metadata.EnumMeta
- FindByKind(kind common.SymKind) []*SymbolNode
-
- IsPrimitivePresent(primitive PrimitiveType) bool
- IsSpecialPresent(special SpecialType) bool
-
- // Children returns direct outward SymbolNode dependencies from the given node,
- // applying the given traversal behavior if non-nil.
- Children(node *SymbolNode, behavior *TraversalBehavior) []*SymbolNode
-
- // Parents returns nodes that have edges pointing to the given node,
- // applying the given traversal behavior if non-nil.
- Parents(node *SymbolNode, behavior *TraversalBehavior) []*SymbolNode
-
- // Descendants returns all transitive children reachable from root,
- // applying the behavior at each step to decide traversal and inclusion.
- Descendants(root *SymbolNode, behavior *TraversalBehavior) []*SymbolNode
-
- ToDot(theme *dot.DotTheme) string
- String() string
-}
-
type SymbolGraph struct {
// A map for retrieving full symbol keys from their comparable, non-versioned IDs
lookupKeys map[string]graphs.SymbolKey
@@ -165,9 +129,9 @@ func (g *SymbolGraph) RemoveEdge(from, to graphs.SymbolKey, kind *SymbolEdgeKind
func (g *SymbolGraph) AddController(request CreateControllerNode) (*SymbolNode, error) {
symNode, err := g.createAndAddSymNode(
- request.Data.Node,
+ request.Data.Struct.Node,
common.SymKindController,
- request.Data.FVersion,
+ request.Data.Struct.FVersion,
request.Annotations,
request.Data,
)
@@ -291,7 +255,7 @@ func (g *SymbolGraph) AddEnum(request CreateEnumNode) (*SymbolNode, error) {
// This is not strictly necessary due to how the visitor code is built but it serves as a bit of
// an extra layer of 'protection' against malformed graphs.
if g.nodes[typeRef.BaseId()] == nil {
- primitive, isPrimitive := ToPrimitiveType(string(request.Data.ValueKind))
+ primitive, isPrimitive := common.ToPrimitiveType(string(request.Data.ValueKind))
if !isPrimitive {
return nil, fmt.Errorf(
"value kind for enum '%s' is '%s' which is unexpected",
@@ -328,7 +292,7 @@ func (g *SymbolGraph) AddEnum(request CreateEnumNode) (*SymbolNode, error) {
func (g *SymbolGraph) AddField(request CreateFieldNode) (*SymbolNode, error) {
symNode, err := g.createAndAddSymNode(
request.Data.Node,
- common.SymKindField,
+ request.Data.SymbolKind,
request.Data.FVersion,
request.Annotations,
request.Data,
@@ -337,12 +301,17 @@ func (g *SymbolGraph) AddField(request CreateFieldNode) (*SymbolNode, error) {
return nil, err
}
- typeRef, err := getTypeRef(request.Data.Type)
+ typeRef, err := g.getKeyForUsage(request.Data.Type)
if err != nil {
return nil, err
}
- g.AddEdge(symNode.Id, typeRef, EdgeKindType, nil)
+ edgeKind := EdgeKindType
+ if request.Data.Type.Root.Kind() == metadata.TypeRefKindParam {
+ edgeKind = EdgeKindTypeParameter
+ }
+
+ g.AddEdge(symNode.Id, typeRef, edgeKind, nil)
return symNode, err
}
@@ -356,6 +325,67 @@ func (g *SymbolGraph) AddConst(request CreateConstNode) (*SymbolNode, error) {
)
}
+func (g *SymbolGraph) AddAlias(request CreateAliasNode) (*SymbolNode, error) {
+ return g.createAndAddSymNode(
+ request.Data.Node,
+ common.SymKindAlias,
+ request.Data.FVersion,
+ request.Annotations,
+ request.Data,
+ )
+}
+
+func (g *SymbolGraph) Get(key graphs.SymbolKey) *SymbolNode {
+ return g.nodes[key.BaseId()]
+}
+
+// GetEdges returns a map of edge descriptors involving 'key'.
+// It includes both outgoing edges (key -> X) and incoming edges (Y -> key).
+// If 'kinds' is non-empty, only edges whose kind matches one of the kinds are returned.
+// Returned map keys are stable and unique: "::".
+func (g *SymbolGraph) GetEdges(key graphs.SymbolKey, kinds []SymbolEdgeKind) map[string]SymbolEdgeDescriptor {
+ mapKey := key.BaseId()
+ out := make(map[string]SymbolEdgeDescriptor)
+
+ requestedKinds := mapset.NewSet(kinds...)
+
+ // 1) Outgoing edges from `key`
+ if outgoing, ok := g.edges[mapKey]; ok {
+ for innerKey, desc := range outgoing {
+ if !requestedKinds.IsEmpty() && !requestedKinds.ContainsOne(desc.Edge.Kind) {
+ continue
+ }
+ // Use fromBaseId prefix to guarantee uniqueness when aggregating from many sources
+ out[mapKey+"::"+innerKey] = desc
+ }
+ }
+
+ // 2) Incoming edges (parents -> key). Use revDeps to find parents.
+ if parents, ok := g.revDeps[mapKey]; ok {
+ for parentKey := range parents {
+ parentBase := parentKey.BaseId()
+ if parentEdges, ok := g.edges[parentBase]; ok {
+ for innerKey, desc := range parentEdges {
+ // we only want those edges whose 'To' is our key
+ if desc.Edge.To.BaseId() != mapKey {
+ continue
+ }
+ if !requestedKinds.IsEmpty() && !requestedKinds.ContainsOne(desc.Edge.Kind) {
+ continue
+ }
+ out[parentBase+"::"+innerKey] = desc
+ }
+ }
+ }
+ }
+
+ return out
+}
+
+func (g *SymbolGraph) Exists(key graphs.SymbolKey) bool {
+ return g.Get(key) != nil
+}
+
func (g *SymbolGraph) FindByKind(kind common.SymKind) []*SymbolNode {
var results []*SymbolNode
@@ -392,23 +422,61 @@ func (g *SymbolGraph) Structs() []metadata.StructMeta {
return results
}
-func (g *SymbolGraph) IsPrimitivePresent(primitive PrimitiveType) bool {
+func (g *SymbolGraph) IsPrimitivePresent(primitive common.PrimitiveType) bool {
return g.builtinSymbolExists(string(primitive), true)
}
-func (g *SymbolGraph) AddPrimitive(p PrimitiveType) *SymbolNode {
+func (g *SymbolGraph) AddPrimitive(p common.PrimitiveType) *SymbolNode {
// Primitives are always 'universe' types
return g.addBuiltinSymbol(string(p), common.SymKindBuiltin, true)
}
-func (g *SymbolGraph) IsSpecialPresent(special SpecialType) bool {
+func (g *SymbolGraph) IsSpecialPresent(special common.SpecialType) bool {
return g.builtinSymbolExists(string(special), special.IsUniverse())
}
-func (g *SymbolGraph) AddSpecial(special SpecialType) *SymbolNode {
+func (g *SymbolGraph) AddSpecial(special common.SpecialType) *SymbolNode {
return g.addBuiltinSymbol(string(special), common.SymKindSpecialBuiltin, special.IsUniverse())
}
+// addComposite creates (or returns existing) a composite-type SymbolNode and wires edges to operands.
+// It's idempotent.
+func (g *SymbolGraph) addComposite(req CreateCompositeNode) (*SymbolNode, error) {
+ // Return existing node if present
+ if node, exists := g.nodes[req.Key.BaseId()]; exists {
+ return node, nil
+ }
+
+ node := &SymbolNode{
+ Id: req.Key,
+ Kind: common.SymKindComposite,
+ Data: &metadata.CompositeMeta{
+ Canonical: req.Canonical,
+ Operands: req.Operands,
+ },
+ }
+
+ g.addNode(node)
+
+ // Add edges from composite -> operand nodes (type-parameter edges)
+ for _, op := range req.Operands {
+ g.AddEdge(node.Id, op, EdgeKindTypeParameter, nil)
+ }
+
+ // If this composite is an instantiation of a declared base, create the instantiation edge.
+ if !req.Base.Equals(graphs.SymbolKey{}) {
+ // we expect the base to be present (ensureInstantiatedBasePresent enforces this).
+ if g.Exists(req.Base) {
+ g.AddEdge(node.Id, req.Base, EdgeKindInstantiates, nil)
+ } else {
+ // defensive: this should not happen if ensureInstantiatedBasePresent ran.
+ return nil, fmt.Errorf("addComposite: base missing for instantiation: %s", req.Base.Id())
+ }
+ }
+
+ return node, nil
+}
+
func (g *SymbolGraph) Children(node *SymbolNode, behavior *TraversalBehavior) []*SymbolNode {
if behavior != nil && behavior.Sorting != TraversalSortingNone {
return g.childrenSorted(node, behavior)
@@ -537,6 +605,16 @@ func (g *SymbolGraph) Descendants(root *SymbolNode, behavior *TraversalBehavior)
return result
}
+func (g *SymbolGraph) Walk(node *SymbolNode, walker TreeWalker) {
+ edges := g.edges[node.Id.BaseId()]
+ for {
+ next := walker(node, edges)
+ if next == nil {
+ break
+ }
+ }
+}
+
func (g *SymbolGraph) builtinSymbolExists(name string, isUniverse bool) bool {
var key graphs.SymbolKey
if isUniverse {
@@ -749,27 +827,283 @@ func (g *SymbolGraph) ToDot(theme *dot.DotTheme) string {
// Add all edges
for fromKey, edges := range g.edges {
for _, edgeDescriptor := range edges {
- builder.AddEdge(g.lookupKeys[fromKey], edgeDescriptor.Edge.To, string(edgeDescriptor.Edge.Kind))
+ builder.AddEdge(
+ g.lookupKeys[fromKey],
+ edgeDescriptor.Edge.To,
+ string(edgeDescriptor.Edge.Kind),
+ g.getParamIndexesLabelSuffix(edgeDescriptor),
+ )
}
}
- // Add legend if theme requests it
- builder.RenderLegend()
-
return builder.Finish()
}
-func getTypeRef(typeUsage metadata.TypeUsageMeta) (graphs.SymbolKey, error) {
- if !typeUsage.SymbolKind.IsBuiltin() {
+func (g *SymbolGraph) getParamIndexesLabelSuffix(edgeDescriptor SymbolEdgeDescriptor) *string {
+ if edgeDescriptor.Edge.Kind != EdgeKindTypeParameter {
+ return nil
+ }
+
+ indices := g.getTypeParamIndex(edgeDescriptor.Edge.From, edgeDescriptor.Edge.To)
+ if len(indices) <= 0 {
+ return nil
+ }
+
+ indexStrings := linq.Map(indices, func(idx int) string {
+ return fmt.Sprint(idx)
+ })
+
+ formattedIndices := fmt.Sprintf(
+ " %s",
+ strings.Join(indexStrings, ", "),
+ )
+ return &formattedIndices
+}
+
+func (g *SymbolGraph) getTypeParamIndex(from, to graphs.SymbolKey) []int {
+ // Get the actual node that uses the 'to' key
+ fromNode := g.nodes[from.BaseId()]
+ if fromNode == nil {
+ return nil
+ }
- return typeUsage.GetBaseTypeRefKey()
+ composite, isComposite := fromNode.Data.(*metadata.CompositeMeta)
+ if !isComposite {
+ // Shouldn't happen - we should get here only if we're inspecting a composite node
+ return nil
+ }
+
+ paramIndices := []int{}
+
+ for idx, typeParam := range composite.Operands {
+ if to == typeParam {
+ paramIndices = append(paramIndices, idx)
+ }
}
- if typeUsage.IsUniverseType() {
- return graphs.NewUniverseSymbolKey(typeUsage.Name), nil
+ if len(paramIndices) > 0 {
+ return paramIndices
}
- return graphs.NewNonUniverseBuiltInSymbolKey(fmt.Sprintf("%s.%s", typeUsage.PkgPath, typeUsage.Name)), nil
+ return nil
+}
+
+// getKeyForUsage is a thin wrapper used by callers (AddField, AddRouteRetVal, ...).
+// It validates the input and delegates to ensureTypeNode.
+func (g *SymbolGraph) getKeyForUsage(typeUsage metadata.TypeUsageMeta) (graphs.SymbolKey, error) {
+ if typeUsage.Root == nil {
+ return graphs.SymbolKey{}, fmt.Errorf("getKeyForUsage: TypeUsage '%s' missing Root TypeRef", typeUsage.Name)
+ }
+ return g.ensureTypeNode(typeUsage.Root, typeUsage.FVersion)
+}
+
+// ensureTypeNode ensures a graph node exists for the given metadata.TypeRef and returns its SymbolKey.
+// - idempotent
+// - creates primitive / special universe nodes when needed
+// - creates composite nodes (ptr/slice/map/func/instantiation) and edges to operands
+// - does NOT attempt automatic materialization of declared base types; it errors if those are missing
+func (g *SymbolGraph) ensureTypeNode(root metadata.TypeRef, fileVersion *gast.FileVersion) (graphs.SymbolKey, error) {
+ if root == nil {
+ return graphs.SymbolKey{}, fmt.Errorf("ensureTypeNode: nil TypeRef")
+ }
+
+ // derive key for this usage
+ key, err := root.ToSymKey(fileVersion)
+ if err != nil {
+ return graphs.SymbolKey{}, fmt.Errorf(
+ "ensureTypeNode: ToSymKey failed for '%s': %w",
+ root.CanonicalString(),
+ err,
+ )
+ }
+
+ // 1) Universe / builtin -> ensure primitive/special node and return that key immediately.
+ if key.IsUniverse || key.IsBuiltIn {
+ if err := g.conditionalEnsureBuiltInNode(key); err != nil {
+ return graphs.SymbolKey{}, fmt.Errorf("ensureTypeNode: ensureUniverseNode failed '%s': %w",
+ key.Name, err)
+ }
+ return key, nil
+ }
+
+ // 2) Named (plain) that points to a declared base: prefer the declared base node.
+ // This prevents creating a zero-operand composite node for plain namedRef types.
+ namedRef, isNamedRef := root.(*typeref.NamedTypeRef)
+
+ if isNamedRef && len(namedRef.TypeArgs) == 0 && !namedRef.Key.Empty() {
+ // If declared base exists in graph - return it (preserve declared identity).
+ if g.Exists(namedRef.Key) {
+ return namedRef.Key, nil
+ }
+ // Declared base missing -> that's a policy error (we don't auto-materialize).
+ return graphs.SymbolKey{}, fmt.Errorf(
+ "ensureTypeNode: declared base not present for named type usage %s -> base %s",
+ root.CanonicalString(),
+ namedRef.Key.Id(),
+ )
+ }
+
+ // 3) If node already exists for this exact key, return it.
+ if g.Exists(key) {
+ return key, nil
+ }
+
+ // 4) Type parameter nodes: produce a TypeParam node (idempotent helper).
+ if root.Kind() == metadata.TypeRefKindParam {
+ castRef := root.(*typeref.ParamTypeRef)
+ tNode, err := g.conditionalEnsureTypeParamNode(key, castRef.Index)
+ if err != nil {
+ return graphs.SymbolKey{}, fmt.Errorf("ensureTypeNode: ensure type-param node failed: %w", err)
+ }
+ return tNode.Id, nil
+ }
+
+ // 5) For instantiated named types ensure the declared base exists (no auto-materialize).
+ if err := g.conditionalEnsureInstantiatedBase(root); err != nil {
+ return graphs.SymbolKey{}, err
+ }
+
+ // 6) Now ensure composite placement (recursively ensures operand nodes and creates composite).
+ if err := g.conditionalEnsureComposite(root, fileVersion, key); err != nil {
+ return graphs.SymbolKey{}, fmt.Errorf("ensureTypeNode: failed to ensure composite '%s' placement in graph - %w", key.Id(), err)
+ }
+
+ // final: key should now be present (addComposite is responsible for inserting node)
+ return key, nil
+}
+
+func (g *SymbolGraph) conditionalEnsureComposite(
+ root metadata.TypeRef,
+ fileVersion *gast.FileVersion,
+ rootKey graphs.SymbolKey,
+) error {
+
+ // Ensure operand nodes recursively
+ operands := gatherOperandTypeRefs(root)
+ operandKeys := make([]graphs.SymbolKey, 0, len(operands))
+ for _, op := range operands {
+ opKey, err := g.ensureTypeNode(op, fileVersion)
+ if err != nil {
+ return fmt.Errorf("ensureTypeNode: ensuring operand '%s' failed: %w",
+ op.CanonicalString(),
+ err,
+ )
+ }
+ operandKeys = append(operandKeys, opKey)
+ }
+
+ // Create composite node and wire operands
+ create := CreateCompositeNode{
+ Key: rootKey,
+ Canonical: root.CanonicalString(),
+ Operands: operandKeys,
+ }
+
+ // attach declared base if root is NamedTypeRef with base key
+ if named, ok := root.(*typeref.NamedTypeRef); ok {
+ if !named.Key.Equals(graphs.SymbolKey{}) && len(named.TypeArgs) > 0 {
+ create.Base = named.Key
+ }
+ }
+
+ if _, err := g.addComposite(create); err != nil {
+ return fmt.Errorf("ensureComposite: AddComposite failed for '%s': %w", rootKey.Id(), err)
+ }
+
+ return nil
+}
+
+// conditionalEnsureInstantiatedBase checks that for NamedTypeRef instantiations the base exists.
+// It does NOT attempt to auto-materialize declared bases; it returns an error when a declared base is absent.
+func (g *SymbolGraph) conditionalEnsureInstantiatedBase(root metadata.TypeRef) error {
+ // We only care about instantiated NamedTypeRef with a declared base key.
+ named, ok := root.(*typeref.NamedTypeRef)
+ if !ok {
+ return nil
+ }
+
+ // If there's no base key or no type args -> nothing to check.
+ if named.Key.Equals(graphs.SymbolKey{}) || len(named.TypeArgs) == 0 {
+ return nil
+ }
+
+ // Universe/builtin bases are always fine.
+ if named.Key.IsUniverse || named.Key.IsBuiltIn {
+ return nil
+ }
+
+ // Declared base must already exist in graph (no auto materialize policy).
+ if !g.Exists(named.Key) {
+ return fmt.Errorf("ensureInstantiatedBasePresent: declared base not present for instantiation: %s", named.Key.Id())
+ }
+
+ return nil
+}
+
+// conditionalEnsureBuiltInNode creates primitives/special nodes (idempotent).
+func (g *SymbolGraph) conditionalEnsureBuiltInNode(key graphs.SymbolKey) error {
+ if prim, ok := common.ToPrimitiveType(key.Name); ok {
+ g.AddPrimitive(prim)
+ return nil
+ }
+ if sp, ok := common.ToSpecialType(key.Name); ok {
+ g.AddSpecial(sp)
+ return nil
+ }
+ return fmt.Errorf("ensureUniverseNode: unknown universe type '%s'", key.Name)
+}
+
+func (g *SymbolGraph) conditionalEnsureTypeParamNode(
+ key graphs.SymbolKey,
+ index int,
+) (*SymbolNode, error) {
+ if existing := g.Get(key); existing != nil {
+ return existing, nil
+ }
+
+ node := &SymbolNode{
+ Id: key,
+ Kind: common.SymKindTypeParam,
+ Data: metadata.TypeParamDeclMeta{
+ Name: key.Name,
+ Index: index,
+ },
+ }
+
+ g.addNode(node)
+ return node, nil
+}
+
+// gatherOperandTypeRefs returns deterministic operand TypeRefs for a composite root.
+func gatherOperandTypeRefs(root metadata.TypeRef) []metadata.TypeRef {
+ switch t := root.(type) {
+ case *typeref.PtrTypeRef:
+ return []metadata.TypeRef{t.Elem}
+ case *typeref.SliceTypeRef:
+ return []metadata.TypeRef{t.Elem}
+ case *typeref.ArrayTypeRef:
+ return []metadata.TypeRef{t.Elem}
+ case *typeref.MapTypeRef:
+ return []metadata.TypeRef{t.Key, t.Value}
+ case *typeref.FuncTypeRef:
+ out := make([]metadata.TypeRef, 0, len(t.Params)+len(t.Results))
+ out = append(out, t.Params...)
+ out = append(out, t.Results...)
+ return out
+ case *typeref.NamedTypeRef:
+ // For instantiation, the explicit TypeArgs are operands (base handled separately)
+ return t.TypeArgs
+ case *typeref.InlineStructTypeRef:
+ out := make([]metadata.TypeRef, 0, len(t.Fields))
+ for _, f := range t.Fields {
+ if f.Type.Root != nil {
+ out = append(out, f.Type.Root)
+ }
+ }
+ return out
+ default:
+ return nil
+ }
}
func edgeMapKey(kind SymbolEdgeKind, toBaseId string) string {
diff --git a/graphs/symboldg/interfaces.go b/graphs/symboldg/interfaces.go
new file mode 100644
index 0000000..b2ba3d5
--- /dev/null
+++ b/graphs/symboldg/interfaces.go
@@ -0,0 +1,55 @@
+package symboldg
+
+import (
+ "github.com/gopher-fleece/gleece/common"
+ "github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/graphs"
+ "github.com/gopher-fleece/gleece/graphs/dot"
+)
+
+type TreeWalker func(node *SymbolNode, edges map[string]SymbolEdgeDescriptor) *SymbolNode
+
+type SymbolGraphBuilder interface {
+ AddController(request CreateControllerNode) (*SymbolNode, error)
+ AddRoute(request CreateRouteNode) (*SymbolNode, error)
+ AddRouteParam(request CreateParameterNode) (*SymbolNode, error)
+ AddRouteRetVal(request CreateReturnValueNode) (*SymbolNode, error)
+ AddStruct(request CreateStructNode) (*SymbolNode, error)
+ AddEnum(request CreateEnumNode) (*SymbolNode, error)
+ AddField(request CreateFieldNode) (*SymbolNode, error)
+ AddConst(request CreateConstNode) (*SymbolNode, error)
+ AddAlias(request CreateAliasNode) (*SymbolNode, error)
+ AddPrimitive(kind common.PrimitiveType) *SymbolNode
+ AddSpecial(special common.SpecialType) *SymbolNode
+ AddEdge(from, to graphs.SymbolKey, kind SymbolEdgeKind, meta map[string]string)
+ RemoveEdge(from, to graphs.SymbolKey, kind *SymbolEdgeKind)
+ RemoveNode(key graphs.SymbolKey)
+
+ Structs() []metadata.StructMeta
+ Enums() []metadata.EnumMeta
+
+ Exists(key graphs.SymbolKey) bool
+ Get(key graphs.SymbolKey) *SymbolNode
+ GetEdges(key graphs.SymbolKey, kinds []SymbolEdgeKind) map[string]SymbolEdgeDescriptor
+ FindByKind(kind common.SymKind) []*SymbolNode
+
+ IsPrimitivePresent(primitive common.PrimitiveType) bool
+ IsSpecialPresent(special common.SpecialType) bool
+
+ // Children returns direct outward SymbolNode dependencies from the given node,
+ // applying the given traversal behavior if non-nil.
+ Children(node *SymbolNode, behavior *TraversalBehavior) []*SymbolNode
+
+ // Parents returns nodes that have edges pointing to the given node,
+ // applying the given traversal behavior if non-nil.
+ Parents(node *SymbolNode, behavior *TraversalBehavior) []*SymbolNode
+
+ // Descendants returns all transitive children reachable from root,
+ // applying the behavior at each step to decide traversal and inclusion.
+ Descendants(root *SymbolNode, behavior *TraversalBehavior) []*SymbolNode
+
+ Walk(root *SymbolNode, callback TreeWalker)
+
+ ToDot(theme *dot.DotTheme) string
+ String() string
+}
diff --git a/graphs/symboldg/model.synthesis.go b/graphs/symboldg/model.synthesis.go
new file mode 100644
index 0000000..f8bcd14
--- /dev/null
+++ b/graphs/symboldg/model.synthesis.go
@@ -0,0 +1,326 @@
+package symboldg
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+ "unicode"
+
+ "github.com/gopher-fleece/gleece/common"
+ "github.com/gopher-fleece/gleece/common/linq"
+ "github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/definitions"
+ "github.com/gopher-fleece/gleece/graphs"
+)
+
+type ModelNameTransformer = func(modelBaseName string, typeParamNames []string) string
+
+type InitializationTarget struct {
+ TypeParams []*SymbolNode
+}
+
+type GenericInstantiationList struct {
+ Struct metadata.StructMeta
+ Targets []InitializationTarget
+}
+
+type MapComposite struct {
+ Composite *SymbolNode
+ TypeParams [2]*SymbolNode
+}
+
+type RawStructModelsList struct {
+ GenericStructs []GenericInstantiationList
+ Structs []metadata.StructMeta
+}
+
+func ComposeStructs(
+ reductionCtx metadata.ReductionContext,
+ graph SymbolGraphBuilder,
+ modelNameTransformer ModelNameTransformer,
+) ([]definitions.StructMetadata, error) {
+ // Collect struct and struct-like models (Structs, Aliases, generic struct instantiations)
+ modelsList, err := collectStructModelList(graph)
+ if err != nil {
+ return nil, fmt.Errorf("failed to collect models from graph - %v", err)
+ }
+
+ // Reduce said struct/struct-like HIR items to the flattened, emitter-ready structs (definitions)
+ allModels, err := reduceStructLists(reductionCtx, modelsList, modelNameTransformer)
+ if err != nil {
+ return nil, fmt.Errorf("failed to reduce struct models list - %v", err)
+ }
+
+ return allModels, nil
+}
+
+func ComposeAliases(
+ reductionCtx metadata.ReductionContext,
+ graph SymbolGraphBuilder,
+) ([]definitions.NakedAliasMetadata, error) {
+ return collectAliasModels(reductionCtx, graph)
+}
+
+func collectStructModelList(graph SymbolGraphBuilder) (RawStructModelsList, error) {
+ modelList := RawStructModelsList{
+ GenericStructs: []GenericInstantiationList{},
+ Structs: []metadata.StructMeta{},
+ }
+
+ for _, structNode := range graph.FindByKind(common.SymKindStruct) {
+ structMeta, isStructMeta := structNode.Data.(metadata.StructMeta)
+ if !isStructMeta {
+ return RawStructModelsList{}, fmt.Errorf(
+ "expected node with ID '%s' to be a StructMeta node but got '%v'",
+ structNode.Id.Name,
+ structNode.Kind,
+ )
+ }
+
+ instEdges := graph.GetEdges(
+ structNode.Id,
+ []SymbolEdgeKind{EdgeKindInstantiates},
+ )
+
+ if len(instEdges) <= 0 {
+ // Normal struct - append as-is
+ modelList.Structs = append(modelList.Structs, structMeta)
+ continue
+ }
+
+ // Generic struct - need to return create an instantiation list
+ instantiations := GenericInstantiationList{
+ Struct: structMeta,
+ Targets: []InitializationTarget{},
+ }
+
+ for _, instEdge := range instEdges {
+ governingComposite := graph.Get(instEdge.Edge.From)
+ if governingComposite == nil {
+ return modelList, fmt.Errorf(
+ "failed to determine governing composite for generic struct '%s'",
+ structMeta.Name,
+ )
+ }
+
+ compositeMeta, isCompositeMeta := governingComposite.Data.(*metadata.CompositeMeta)
+ if !isCompositeMeta {
+ return modelList, fmt.Errorf(
+ "expected node '%s' to be a composite meta but got '%v'",
+ governingComposite.Id.Name,
+ governingComposite.Kind,
+ )
+ }
+
+ typeParams := linq.Map(compositeMeta.Operands, func(key graphs.SymbolKey) *SymbolNode {
+ return graph.Get(key)
+ })
+
+ instantiations.Targets = append(
+ instantiations.Targets,
+ InitializationTarget{TypeParams: typeParams},
+ )
+ }
+
+ modelList.GenericStructs = append(modelList.GenericStructs, instantiations)
+ }
+
+ return modelList, nil
+}
+
+func reduceStructLists(
+ ctx metadata.ReductionContext,
+ structList RawStructModelsList,
+ modelNameTransformer ModelNameTransformer,
+) ([]definitions.StructMetadata, error) {
+ // Allocate enough for both standard structs and whatever materialized ephemeral/generic we need
+ reducedList := make(
+ []definitions.StructMetadata,
+ 0,
+ len(structList.Structs)+len(structList.GenericStructs),
+ )
+
+ // Reduce the normal structs
+ for _, stdStruct := range structList.Structs {
+ reduced, err := stdStruct.Reduce(ctx)
+ if err != nil {
+ return reducedList, fmt.Errorf("failed to reduce struct '%s' - %v", stdStruct.Name, err)
+ }
+ reducedList = append(reducedList, reduced)
+ }
+
+ // Synthesize ephemeral models (i.e., instantiations like SomeStruct[string, int])
+ for _, instComposite := range structList.GenericStructs {
+ instantiations, err := synthesizeModelsForGenericStruct(
+ ctx,
+ instComposite,
+ modelNameTransformer,
+ )
+
+ if err != nil {
+ return reducedList, fmt.Errorf(
+ "failed to synthesize instantiations for struct '%s' - %v",
+ instComposite.Struct.Name,
+ err,
+ )
+ }
+
+ reducedList = append(reducedList, instantiations...)
+ }
+
+ return reducedList, nil
+}
+
+func instantiateGenericModel(
+ rawStruct *metadata.StructMeta,
+ reducedStruct definitions.StructMetadata,
+ typeParamReplacementNodes []*SymbolNode,
+ modelNameTransformer func(modelBaseName string, typeParamNames []string) string,
+) (definitions.StructMetadata, error) {
+
+ // Clone the reduced struct - Fields is a slice and therefore the struct as a whole
+ // is not safe to modify.
+ clonedStruct := reducedStruct.Clone()
+
+ rawParamNames := linq.Map(typeParamReplacementNodes, func(tParamNode *SymbolNode) string {
+ if tParamNode.Kind.IsBuiltin() {
+ return tParamNode.Id.Name
+ }
+ return tParamNode.Data.(*metadata.TypeParamDeclMeta).Name
+ })
+
+ if modelNameTransformer != nil {
+ clonedStruct.Name = modelNameTransformer(clonedStruct.Name, rawParamNames)
+ } else {
+ clonedStruct.Name = StandardModelNameTransformer(clonedStruct.Name, rawParamNames)
+ }
+
+ for fieldIdx, field := range rawStruct.Fields {
+ // Check if this is a generic field
+ if field.Type.Root != nil && field.Type.Root.Kind() == metadata.TypeRefKindParam {
+ nameParts := strings.SplitN(field.Type.Name, "#", 2)
+ if len(nameParts) != 2 {
+ return clonedStruct, fmt.Errorf(
+ "failed to determine replacement to generic placeholder '%s' in field '%s'",
+ field.Type.Name,
+ field.Name,
+ )
+ }
+
+ replParamIdx, err := strconv.ParseInt(nameParts[1], 10, 16)
+ if err != nil {
+ return clonedStruct, fmt.Errorf(
+ "failed to parse replacement index to generic placeholder '%s' in field '%s'",
+ field.Type.Name,
+ field.Name,
+ )
+ }
+
+ // Re-write the type
+ clonedStruct.Fields[fieldIdx].Type = rawParamNames[replParamIdx]
+ }
+
+ }
+
+ return clonedStruct, nil
+}
+
+func materializeInstantiationTarget(
+ rawStruct *metadata.StructMeta,
+ reducedStruct definitions.StructMetadata,
+ target InitializationTarget,
+ modelNameTransformer func(modelBaseName string, typeParamNames []string) string,
+) (definitions.StructMetadata, error) {
+ instance, err := instantiateGenericModel(rawStruct, reducedStruct, target.TypeParams, modelNameTransformer)
+ if err != nil {
+ return instance, fmt.Errorf("failed to construct fields for generic struct '%s' - %v", rawStruct.Name, err)
+ }
+
+ return instance, nil
+}
+
+func synthesizeModelsForGenericStruct(
+ reductionCtx metadata.ReductionContext,
+ genInstantiation GenericInstantiationList,
+ modelNameTransformer func(modelBaseName string, typeParamNames []string) string,
+) ([]definitions.StructMetadata, error) {
+ // Reduce once and re-use
+ reduced, err := genInstantiation.Struct.Reduce(reductionCtx)
+ if err != nil {
+ return nil, fmt.Errorf(
+ "failed to reduce struct '%s' for materialization - %v",
+ genInstantiation.Struct.Name,
+ err,
+ )
+ }
+
+ materialized := []definitions.StructMetadata{}
+ for _, target := range genInstantiation.Targets {
+ instantiated, err := materializeInstantiationTarget(
+ &genInstantiation.Struct,
+ reduced,
+ target,
+ modelNameTransformer,
+ )
+ if err != nil {
+ return nil, fmt.Errorf(
+ "failed to materialize an instantiation of struct '%s' - %v",
+ genInstantiation.Struct.Name,
+ err,
+ )
+ }
+ materialized = append(materialized, instantiated)
+ }
+
+ return materialized, nil
+}
+
+func collectAliasModels(
+ ctx metadata.ReductionContext,
+ graph SymbolGraphBuilder,
+) ([]definitions.NakedAliasMetadata, error) {
+ reduced := []definitions.NakedAliasMetadata{}
+
+ for _, aliasNode := range graph.FindByKind(common.SymKindAlias) {
+ aliasMeta, isAliasMeta := aliasNode.Data.(metadata.AliasMeta)
+ if !isAliasMeta {
+ return reduced, fmt.Errorf(
+ "expected node with ID '%s' to be an AliasMeta node but got '%v'",
+ aliasNode.Id.Name,
+ aliasNode.Kind,
+ )
+ }
+
+ alias, err := aliasMeta.Reduce(ctx)
+ if err != nil {
+ return reduced, fmt.Errorf("failed to reduce AliasMeta '%s' - %v", aliasMeta.Name, err)
+ }
+
+ reduced = append(reduced, alias)
+ }
+
+ return reduced, nil
+}
+
+// StandardModelNameTransformer turns a model base name and type parameters
+// into a PascalCase model name. Example:
+//
+// StandardModelNameTransformer("user", []string{"id", "meta"}) == "UserIdMeta"
+func StandardModelNameTransformer(modelBaseName string, typeParamNames []string) string {
+ toPascal := func(s string) string {
+ if s == "" {
+ return ""
+ }
+ runes := []rune(s)
+ runes[0] = unicode.ToUpper(runes[0])
+ return string(runes)
+ }
+
+ var b strings.Builder
+ b.WriteString(toPascal(modelBaseName))
+ for _, tp := range typeParamNames {
+ b.WriteString(toPascal(tp))
+ }
+
+ return b.String()
+}
diff --git a/graphs/symboldg/requests.go b/graphs/symboldg/requests.go
index 5d1f097..dbf36b0 100644
--- a/graphs/symboldg/requests.go
+++ b/graphs/symboldg/requests.go
@@ -22,7 +22,7 @@ func (k KeyableNodeMeta) SymbolKey() graphs.SymbolKey {
}
type CreateControllerNode struct {
- Data metadata.StructMeta
+ Data metadata.ControllerMeta
Annotations *annotations.AnnotationHolder
}
@@ -61,3 +61,16 @@ type CreateConstNode struct {
Data metadata.ConstMeta
Annotations *annotations.AnnotationHolder
}
+
+type CreateAliasNode struct {
+ Data metadata.AliasMeta
+ Annotations *annotations.AnnotationHolder
+}
+
+// CreateCompositeNode is the request used to add a canonical composite node.
+type CreateCompositeNode struct {
+ Key graphs.SymbolKey
+ Base graphs.SymbolKey // optional: declared base for instantiations (empty == none)
+ Canonical string
+ Operands []graphs.SymbolKey
+}
diff --git a/graphs/symboldg/traversal.go b/graphs/symboldg/traversal.go
index 8a384f2..e6bbf7a 100644
--- a/graphs/symboldg/traversal.go
+++ b/graphs/symboldg/traversal.go
@@ -1,6 +1,10 @@
package symboldg
-import "github.com/gopher-fleece/gleece/common"
+import (
+ "slices"
+
+ "github.com/gopher-fleece/gleece/common"
+)
type TraversalResultSorting int
@@ -11,8 +15,8 @@ const (
)
type TraversalFilter struct {
- NodeKind *common.SymKind // Only include nodes of this kind (optional)
- EdgeKind *SymbolEdgeKind // Only follow edges of this kind (optional)
+ NodeKinds []common.SymKind // Only include nodes of this kind (optional)
+ EdgeKinds []SymbolEdgeKind // Only follow edges of this kind (optional)
FilterFunc func(*SymbolNode) bool // Optional user-defined predicate
}
@@ -22,7 +26,11 @@ type TraversalBehavior struct {
}
func shouldIncludeEdge(edge SymbolEdge, behavior *TraversalBehavior) bool {
- return behavior == nil || behavior.Filtering.EdgeKind == nil || edge.Kind == *behavior.Filtering.EdgeKind
+ if behavior == nil || behavior.Filtering.EdgeKinds == nil {
+ return true
+ }
+
+ return slices.Contains(behavior.Filtering.EdgeKinds, edge.Kind)
}
func shouldIncludeNode(node *SymbolNode, behavior *TraversalBehavior) bool {
@@ -30,11 +38,13 @@ func shouldIncludeNode(node *SymbolNode, behavior *TraversalBehavior) bool {
return true
}
- if behavior.Filtering.NodeKind != nil && node.Kind != *behavior.Filtering.NodeKind {
+ if behavior.Filtering.NodeKinds != nil && !slices.Contains(behavior.Filtering.NodeKinds, node.Kind) {
return false
}
+
if behavior.Filtering.FilterFunc != nil && !behavior.Filtering.FilterFunc(node) {
return false
}
+
return true
}
diff --git a/graphs/symkey.go b/graphs/symkey.go
index 9fe6cf9..11d9d95 100644
--- a/graphs/symkey.go
+++ b/graphs/symkey.go
@@ -5,8 +5,10 @@ import (
"go/ast"
"go/token"
"path/filepath"
+ "strconv"
"strings"
+ "github.com/gopher-fleece/gleece/common/linq"
"github.com/gopher-fleece/gleece/gast"
)
@@ -49,20 +51,54 @@ func (sk SymbolKey) formatId(fileIdPart string) string {
return fmt.Sprintf("@%d@%s", sk.Position, fileIdPart)
}
+// ShortLabel returns a compact, human-friendly label used in DOT dumps.
+// It delegates to small helpers for composite formatting and file info attachment.
func (sk SymbolKey) ShortLabel() string {
- file := filepath.Base(strings.Split(sk.FileId, "|")[0])
- hash := strings.Split(sk.FileId, "|")
- shortHash := ""
- if len(hash) == 3 {
- shortHash = hash[2]
- if len(shortHash) > 7 {
- shortHash = shortHash[:7]
+ fileBase, shortHash := extractFileInfo(sk)
+
+ attach := func(label string) string {
+ if fileBase == "" {
+ return label
}
+ if shortHash != "" {
+ return fmt.Sprintf("%s @%s|%s", label, fileBase, shortHash)
+ }
+ return fmt.Sprintf("%s @%s", label, fileBase)
+ }
+
+ // universe / builtin -> simple name
+ if sk.IsUniverse || sk.IsBuiltIn {
+ return attach(sk.Name)
+ }
+
+ // type param like "typeparam:TA#0"
+ if isTypeParamName(sk.Name) {
+ return attach(formatTypeParam(sk.Name))
+ }
+
+ // instantiated named type like "inst:Foo@...[...]"
+ if isInstName(sk.Name) {
+ return attach(instLabelFromName(sk.Name))
+ }
+
+ // composite nodes (slice/map/ptr/func/array)
+ if isCompositeName(sk.Name) {
+ return attach(compositeLabelFromName(sk.Name))
}
+
+ // default: show the raw name (trim to left of '@' to keep short)
if sk.Name != "" {
- return fmt.Sprintf("%s@%s|%s", sk.Name, file, shortHash)
+ return attach(trimAt(sk.Name))
+ }
+
+ // fallback: file info only
+ if fileBase != "" {
+ if shortHash != "" {
+ return fmt.Sprintf("%s|%s", fileBase, shortHash)
+ }
+ return fileBase
}
- return fmt.Sprintf("%s|%s", file, shortHash)
+ return "?"
}
func (sk SymbolKey) PrettyPrint() string {
@@ -97,6 +133,10 @@ func (sk SymbolKey) Equals(other SymbolKey) bool {
return sk.Name == other.Name && sk.Position == other.Position && sk.FileId == other.FileId
}
+func (sk SymbolKey) Empty() bool {
+ return sk == SymbolKey{}
+}
+
func NewSymbolKey(node ast.Node, version *gast.FileVersion) SymbolKey {
if node == nil || version == nil {
return SymbolKey{}
@@ -156,3 +196,232 @@ func NewNonUniverseBuiltInSymbolKey(typeName string) SymbolKey {
IsBuiltIn: true,
}
}
+
+// CompositeKind identifies a composite type family.
+type CompositeKind string
+
+const (
+ CompositeKindPtr CompositeKind = "ptr"
+ CompositeKindSlice CompositeKind = "slice"
+ CompositeKindArray CompositeKind = "array"
+ CompositeKindMap CompositeKind = "map"
+ CompositeKindFunc CompositeKind = "func"
+)
+
+// NewInstSymbolKey returns a canonical SymbolKey representing an instantiation:
+//
+// inst:[,...]
+//
+// The returned key uses the base's FileId so instantiated keys for the same base
+// are dedupable and stable across usage sites.
+func NewInstSymbolKey(base SymbolKey, argKeys []SymbolKey) SymbolKey {
+ argIds := linq.Map(argKeys, func(key SymbolKey) string {
+ return key.Id()
+ })
+
+ // Build canonical name using base.Id() and arg ids.
+ name := "inst:" + base.Id() + "[" + strings.Join(argIds, ",") + "]"
+
+ return SymbolKey{
+ Name: name,
+ Position: token.NoPos,
+ FileId: base.FileId, // scope instantiated type to base's declaring file id
+ FilePath: base.FilePath,
+ }
+}
+
+// NewCompositeTypeKey returns a canonical SymbolKey for composites like ptr/slice/map/func.
+// The canonical Name embeds operand Ids; FileId is derived from fileVersion (if provided).
+func NewCompositeTypeKey(kind CompositeKind, fileVersion *gast.FileVersion, operands []SymbolKey) SymbolKey {
+ operandIds := linq.Map(operands, func(op SymbolKey) string {
+ return op.Id()
+ })
+
+ name := "comp:" + string(kind) + "[" + strings.Join(operandIds, ",") + "]"
+ var fileId string
+ var filePath string
+ if fileVersion != nil {
+ fileId = fileVersion.String()
+ filePath = fileVersion.Path
+ }
+ return SymbolKey{
+ Name: name,
+ Position: token.NoPos,
+ FileId: fileId,
+ FilePath: filePath,
+ }
+}
+
+// NewParamSymbolKey returns a stable key for a type parameter occurrence scoped to the given fileVersion.
+func NewParamSymbolKey(fileVersion *gast.FileVersion, paramName string, index int) SymbolKey {
+ var fileId string
+ var filePath string
+ if fileVersion != nil {
+ fileId = fileVersion.String()
+ filePath = fileVersion.Path
+ }
+ name := "typeparam:" + paramName + "#" + strconv.Itoa(index)
+ return SymbolKey{
+ Name: name,
+ Position: token.NoPos,
+ FileId: fileId,
+ FilePath: filePath,
+ }
+}
+
+// extractFileInfo returns the file base and a short hash part from FileId (if present).
+func extractFileInfo(sk SymbolKey) (fileBase, shortHash string) {
+ if sk.FileId == "" {
+ return "", ""
+ }
+ parts := strings.Split(sk.FileId, "|")
+ if len(parts) > 0 {
+ fileBase = filepath.Base(parts[0])
+ }
+ if len(parts) > 2 && parts[2] != "" {
+ shortHash = parts[2]
+ if len(shortHash) > 7 {
+ shortHash = shortHash[:7]
+ }
+ }
+ return fileBase, shortHash
+}
+
+func isCompositeName(n string) bool {
+ return strings.HasPrefix(n, "comp:")
+}
+
+func isInstName(n string) bool {
+ return strings.HasPrefix(n, "inst:")
+}
+
+func isTypeParamName(n string) bool {
+ return strings.HasPrefix(n, "typeparam:")
+}
+
+func trimAt(s string) string {
+ if at := strings.Index(s, "@"); at >= 0 {
+ return s[:at]
+ }
+ return s
+}
+
+// formatTypeParam converts "typeparam:TA#0" -> "TA#0"
+func formatTypeParam(n string) string {
+ // accept either exact prefix or slight variants
+ n = strings.TrimPrefix(n, "typeparam:")
+ n = strings.TrimPrefix(n, "typeparam:")
+ return trimAt(n)
+}
+
+// instLabelFromName handles strings like:
+//
+// "inst:MultiGenericStruct@...|... [UniverseType:string,UniverseType:int]"
+//
+// and returns "MultiGenericStruct[string,int]".
+func instLabelFromName(inst string) string {
+ // remove prefix
+ body := strings.TrimPrefix(inst, "inst:")
+
+ // find args portion (inside square brackets)
+ brStart := strings.Index(body, "[")
+ brEnd := strings.LastIndex(body, "]")
+ basePart := body
+ argsPart := ""
+ if brStart >= 0 && brEnd > brStart {
+ basePart = strings.TrimSpace(body[:brStart])
+ argsPart = strings.TrimSpace(body[brStart+1 : brEnd])
+ }
+
+ base := trimAt(basePart) // leftmost token before '@'
+
+ // parse args (comma separated); remove common prefixes like "UniverseType:"
+ argParts := splitArgs(argsPart, 0)
+ for i := range argParts {
+ // remove known prefixes and trim at '@'
+ arg := strings.TrimPrefix(argParts[i], "UniverseType:")
+ arg = strings.TrimSpace(arg)
+ arg = trimAt(arg)
+ argParts[i] = arg
+ }
+
+ if len(argParts) == 0 || (len(argParts) == 1 && argParts[0] == "") {
+ return base
+ }
+ return fmt.Sprintf("%s[%s]", base, strings.Join(argParts, ","))
+}
+
+// compositeLabelFromName pretty-prints composite symbol names produced by canonicalizer.
+// Input examples:
+//
+// "comp:slice[SimpleStruct@...]" -> "[]SimpleStruct"
+// "comp:map[Key@... , Val@...]" -> "map[Key]Val"
+func compositeLabelFromName(comp string) string {
+ inner := strings.TrimPrefix(comp, "comp:")
+ kind, args := parseCompositeName(inner)
+
+ trim := func(s string) string {
+ // strip "UniverseType:" and anything after '@'
+ s = strings.TrimPrefix(s, "UniverseType:")
+ return trimAt(strings.TrimSpace(s))
+ }
+
+ switch kind {
+ case "slice":
+ return "[]" + trim(args)
+ case "array":
+ // args may encode length or inner type; show compactly
+ return "[" + trim(args) + "]"
+ case "ptr":
+ return "*" + trim(args)
+ case "map":
+ // args expected "Key,Val"
+ parts := splitArgs(args, 2)
+ if len(parts) == 2 {
+ return fmt.Sprintf("map[%s]%s", trim(parts[0]), trim(parts[1]))
+ }
+ return "map[" + trim(args) + "]"
+ case "func":
+ parts := splitArgs(args, 2)
+ if len(parts) == 2 {
+ return fmt.Sprintf("func(%s)(%s)", trim(parts[0]), trim(parts[1]))
+ }
+ return "func(" + trim(args) + ")"
+ default:
+ // unknown composite kind: return compact inner
+ return trim(args)
+ }
+}
+
+// parseCompositeName splits "kind[args]" into kind and args.
+func parseCompositeName(name string) (kind string, args string) {
+ br := strings.Index(name, "[")
+ if br < 0 {
+ return name, ""
+ }
+ kind = name[:br]
+ args = name[br+1:]
+ args = strings.TrimSuffix(args, "]")
+ return kind, args
+}
+
+// splitArgs splits a comma-separated argument list but optionally up to n parts.
+// n == 0 -> unlimited.
+func splitArgs(s string, n int) []string {
+ s = strings.TrimSpace(s)
+ if s == "" {
+ return nil
+ }
+ if n <= 0 {
+ parts := strings.Split(s, ",")
+ for i := range parts {
+ parts[i] = strings.TrimSpace(parts[i])
+ }
+ return parts
+ }
+ parts := strings.SplitN(s, ",", n)
+ for i := range parts {
+ parts[i] = strings.TrimSpace(parts[i])
+ }
+ return parts
+}
diff --git a/test/alias/alias.controller.go b/test/alias/alias.controller.go
new file mode 100644
index 0000000..96212c7
--- /dev/null
+++ b/test/alias/alias.controller.go
@@ -0,0 +1,154 @@
+package generics_test
+
+import (
+ "time"
+
+ "github.com/gopher-fleece/runtime"
+)
+
+type TypedefAlias string
+
+type AssignedAlias = string
+
+type NestedTypedefAlias TypedefAlias
+
+type NestedAssignedAlias = TypedefAlias
+
+type TypedefSpecialAlias time.Time
+
+type AssignedSpecialAlias = time.Time
+
+type BodyWithTypedefAlias struct {
+ Ally TypedefAlias
+}
+
+type BodyWithTypedefSpecialAlias struct {
+ Ally TypedefSpecialAlias
+}
+
+type BodyWithAssignedAlias struct {
+ Ally AssignedAlias
+}
+
+type BodyWithAssignedSpecialAlias struct {
+ Ally AssignedSpecialAlias
+}
+
+type BodyWithNestedTypedefAlias struct {
+ Ally NestedTypedefAlias
+}
+
+type BodyWithNestedAssignedAlias struct {
+ Ally NestedAssignedAlias
+}
+
+// @Tag(Alias Controller Tag)
+// @Route(/test/alias)
+// @Description Alias Controller
+type AliasController struct {
+ runtime.GleeceController
+}
+
+// Flat TypeDef
+
+// @Method(POST)
+// @Route(/td-alias-query)
+// @Query(alias)
+func (ec *AliasController) ReceivesTypedefAliasQuery(alias TypedefAlias) error {
+ return nil
+}
+
+// @Method(POST)
+// @Route(/td-alias-in-body)
+// @Body(body)
+func (ec *AliasController) ReceivesATypedefAliasInBody(body BodyWithTypedefAlias) error {
+ return nil
+}
+
+// @Method(POST)
+// @Route(/td-alias-return)
+func (ec *AliasController) ReturnsATypedefAlias() (TypedefAlias, error) {
+ return "", nil
+}
+
+// Flat Assigned
+
+// @Method(POST)
+// @Route(/as-alias-query)
+// @Query(alias)
+func (ec *AliasController) ReceivesAssignedAliasQuery(alias AssignedAlias) error {
+ return nil
+}
+
+// @Method(POST)
+// @Route(/as-alias-in-body)
+// @Body(body)
+func (ec *AliasController) ReceivesAnAssignedAliasInBody(body BodyWithAssignedAlias) error {
+ return nil
+}
+
+// @Method(POST)
+// @Route(/as-alias-return)
+func (ec *AliasController) ReturnsAnAssignedAlias() (AssignedAlias, error) {
+ return "", nil
+}
+
+// Nested TypeDef
+
+// @Method(POST)
+// @Route(/ntd-alias-query)
+// @Query(alias)
+func (ec *AliasController) ReceivesNestedTypedefAliasQuery(alias NestedTypedefAlias) error {
+ return nil
+}
+
+// @Method(POST)
+// @Route(/ntd-alias-in-body)
+// @Body(body)
+func (ec *AliasController) ReceivesAnNestedTypedefAliasInBody(body BodyWithNestedTypedefAlias) error {
+ return nil
+}
+
+// @Method(POST)
+// @Route(/ntd-alias-return)
+func (ec *AliasController) ReturnsAnNestedTypedefAlias() (NestedTypedefAlias, error) {
+ return "", nil
+}
+
+// Nested Assigned
+
+// @Method(POST)
+// @Route(/nas-alias-query)
+// @Query(alias)
+func (ec *AliasController) ReceivesNestedAssignedAliasQuery(alias NestedAssignedAlias) error {
+ return nil
+}
+
+// @Method(POST)
+// @Route(/nas-alias-in-body)
+// @Body(body)
+func (ec *AliasController) ReceivesAnNestedAssignedAliasInBody(body BodyWithNestedAssignedAlias) error {
+ return nil
+}
+
+// @Method(POST)
+// @Route(/nas-alias-return)
+func (ec *AliasController) ReturnsAnNestedAssignedAlias() (NestedAssignedAlias, error) {
+ return "", nil
+}
+
+// Special
+
+// @Method(POST)
+// @Route(/as-alias-query)
+// @Query(alias)
+func (ec *AliasController) ReceivesATypedefSpecialAliasQuery(alias TypedefSpecialAlias) error {
+ return nil
+}
+
+// @Method(POST)
+// @Route(/as-alias-query)
+// @Query(alias)
+func (ec *AliasController) ReceivesAnAssignedSpecialAliasQuery(alias AssignedSpecialAlias) error {
+ return nil
+}
diff --git a/test/alias/alias_test.go b/test/alias/alias_test.go
new file mode 100644
index 0000000..985330d
--- /dev/null
+++ b/test/alias/alias_test.go
@@ -0,0 +1,872 @@
+package generics_test
+
+import (
+ "testing"
+
+ "github.com/gopher-fleece/gleece/common"
+ "github.com/gopher-fleece/gleece/common/linq"
+ "github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/core/pipeline"
+ "github.com/gopher-fleece/gleece/definitions"
+ "github.com/gopher-fleece/gleece/graphs/symboldg"
+ "github.com/gopher-fleece/gleece/infrastructure/logger"
+ "github.com/gopher-fleece/gleece/test/utils"
+ "github.com/gopher-fleece/runtime"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+)
+
+const typedefAliasName = "TypedefAlias"
+const assignedAliasName = "AssignedAlias"
+
+var pipe pipeline.GleecePipeline
+
+var _ = BeforeSuite(func() {
+ pipe = utils.GetPipelineOrFail()
+ err := pipe.GenerateGraph()
+ Expect(err).To(BeNil())
+})
+
+var _ = Describe("Alias Controller", func() {
+
+ Context("ReceivesTypedefAliasQuery", func() {
+ It("Processes a TypeDef Alias parameter", func() {
+ info := utils.GetApiEndpointHierarchy(
+ pipe.Graph(),
+ "AliasController",
+ "ReceivesTypedefAliasQuery",
+ []string{"alias"},
+ )
+
+ Expect(info.Params).To(HaveLen(1))
+ Expect(info.RetVals).To(HaveLen(1))
+
+ aliasTypeParam := utils.GetSingularChildTypeNode(pipe.Graph(), info.Params[0].Node)
+
+ Expect(aliasTypeParam).ToNot(BeNil())
+ Expect(aliasTypeParam.Kind).To(Equal(common.SymKindAlias))
+
+ aliasMeta := utils.MustAliasMeta(aliasTypeParam)
+ Expect(aliasMeta.Name).To(Equal(typedefAliasName))
+
+ relevantEdges := common.MapValues(pipe.Graph().GetEdges(
+ aliasTypeParam.Id,
+ []symboldg.SymbolEdgeKind{symboldg.EdgeKindType}),
+ )
+
+ outgoingAliasEdge := linq.First(relevantEdges, func(edge symboldg.SymbolEdgeDescriptor) bool {
+ return pipe.Graph().Get(edge.Edge.From) == aliasTypeParam && edge.Edge.To.Name == "string"
+ })
+
+ aliasedType := pipe.Graph().Get(outgoingAliasEdge.Edge.To)
+
+ Expect(aliasedType).ToNot(BeNil())
+ Expect(aliasedType.Kind).To(Equal(common.SymKindBuiltin))
+ Expect(aliasedType.Id.Name).To(Equal("string"))
+ })
+ })
+
+ Context("ReceivesATypedefAliasInBody", func() {
+ It("Processes a Typedef Alias inside a body parameter", func() {
+ info := utils.GetApiEndpointHierarchy(
+ pipe.Graph(),
+ "AliasController",
+ "ReceivesATypedefAliasInBody",
+ []string{"body"},
+ )
+
+ Expect(info.Params).To(HaveLen(1))
+ Expect(info.RetVals).To(HaveLen(1))
+
+ bodyNode := utils.GetSingularChildTypeNode(pipe.Graph(), info.Params[0].Node)
+ Expect(bodyNode).ToNot(BeNil())
+ Expect(bodyNode.Kind).To(Equal(common.SymKindStruct))
+
+ // find the "Ally" field edge from the struct
+ fieldEdges := common.MapValues(pipe.Graph().GetEdges(
+ bodyNode.Id,
+ []symboldg.SymbolEdgeKind{symboldg.EdgeKindField},
+ ))
+
+ allyFieldEdge := linq.First(fieldEdges, func(edge symboldg.SymbolEdgeDescriptor) bool {
+ return edge.Edge.To.Name == "Ally"
+ })
+ allyField := pipe.Graph().Get(allyFieldEdge.Edge.To)
+ Expect(allyField).ToNot(BeNil())
+
+ // get the type of the "Ally" field
+ allyTypeEdges := common.MapValues(pipe.Graph().GetEdges(
+ allyField.Id,
+ []symboldg.SymbolEdgeKind{symboldg.EdgeKindType},
+ ))
+ allyTypeEdge := linq.First(allyTypeEdges, func(edge symboldg.SymbolEdgeDescriptor) bool {
+ return pipe.Graph().Get(edge.Edge.From) == allyField
+ })
+ allyType := pipe.Graph().Get(allyTypeEdge.Edge.To)
+
+ Expect(allyType).ToNot(BeNil())
+ Expect(allyType.Kind).To(Equal(common.SymKindAlias))
+
+ aliasMeta := utils.MustAliasMeta(allyType)
+ Expect(aliasMeta.Name).To(Equal(typedefAliasName))
+ Expect(aliasMeta.AliasType).To(Equal(metadata.AliasKindTypedef))
+
+ // ensure alias points to builtin string
+ relevantEdges := common.MapValues(pipe.Graph().GetEdges(
+ allyType.Id,
+ []symboldg.SymbolEdgeKind{symboldg.EdgeKindType},
+ ))
+ outgoing := linq.First(relevantEdges, func(edge symboldg.SymbolEdgeDescriptor) bool {
+ return pipe.Graph().Get(edge.Edge.From) == allyType && edge.Edge.To.Name == "string"
+ })
+ aliasedType := pipe.Graph().Get(outgoing.Edge.To)
+ Expect(aliasedType).ToNot(BeNil())
+ Expect(aliasedType.Kind).To(Equal(common.SymKindBuiltin))
+ Expect(aliasedType.Id.Name).To(Equal("string"))
+ })
+ })
+
+ Context("ReturnsATypedefAlias", func() {
+ It("Processes a Typedef Alias return value", func() {
+ info := utils.GetApiEndpointHierarchy(
+ pipe.Graph(),
+ "AliasController",
+ "ReturnsATypedefAlias",
+ []string{},
+ )
+
+ Expect(info.Params).To(HaveLen(0))
+ Expect(info.RetVals).To(HaveLen(2))
+
+ var retTypeNode *symboldg.SymbolNode
+ for _, retVal := range info.RetVals {
+ node := utils.GetSingularChildTypeNode(pipe.Graph(), retVal.Node)
+ if node != nil && node.Id.Name == typedefAliasName {
+ retTypeNode = node
+ break
+ }
+ }
+
+ Expect(retTypeNode).ToNot(BeNil())
+ Expect(retTypeNode.Kind).To(Equal(common.SymKindAlias))
+
+ aliasMeta := utils.MustAliasMeta(retTypeNode)
+ Expect(aliasMeta.Name).To(Equal(typedefAliasName))
+ Expect(aliasMeta.AliasType).To(Equal(metadata.AliasKindTypedef))
+
+ relevantEdges := common.MapValues(pipe.Graph().GetEdges(
+ retTypeNode.Id,
+ []symboldg.SymbolEdgeKind{symboldg.EdgeKindType},
+ ))
+
+ outgoing := linq.First(relevantEdges, func(edge symboldg.SymbolEdgeDescriptor) bool {
+ return pipe.Graph().Get(edge.Edge.From) == retTypeNode && edge.Edge.To.Name == "string"
+ })
+
+ aliasedType := pipe.Graph().Get(outgoing.Edge.To)
+ Expect(aliasedType).ToNot(BeNil())
+ Expect(aliasedType.Kind).To(Equal(common.SymKindBuiltin))
+ Expect(aliasedType.Id.Name).To(Equal("string"))
+ })
+ })
+
+ Context("ReceivesAssignedAliasQuery", func() {
+ It("Processes an Assigned Alias parameter", func() {
+ info := utils.GetApiEndpointHierarchy(
+ pipe.Graph(),
+ "AliasController",
+ "ReceivesAssignedAliasQuery",
+ []string{"alias"},
+ )
+
+ Expect(info.Params).To(HaveLen(1))
+ Expect(info.RetVals).To(HaveLen(1))
+
+ assignedTypeParam := utils.GetSingularChildTypeNode(pipe.Graph(), info.Params[0].Node)
+
+ Expect(assignedTypeParam).ToNot(BeNil())
+ Expect(assignedTypeParam.Kind).To(Equal(common.SymKindAlias))
+
+ assignedMeta := utils.MustAliasMeta(assignedTypeParam)
+ Expect(assignedMeta.Name).To(Equal(assignedAliasName))
+ Expect(assignedMeta.AliasType).To(Equal(metadata.AliasKindAssigned))
+
+ relevantEdges := common.MapValues(pipe.Graph().GetEdges(
+ assignedTypeParam.Id,
+ []symboldg.SymbolEdgeKind{symboldg.EdgeKindType},
+ ))
+
+ outgoingAssignedEdge := linq.First(relevantEdges, func(edge symboldg.SymbolEdgeDescriptor) bool {
+ return pipe.Graph().Get(edge.Edge.From) == assignedTypeParam && edge.Edge.To.Name == "string"
+ })
+
+ aliasedType := pipe.Graph().Get(outgoingAssignedEdge.Edge.To)
+
+ Expect(aliasedType).ToNot(BeNil())
+ Expect(aliasedType.Kind).To(Equal(common.SymKindBuiltin))
+ Expect(aliasedType.Id.Name).To(Equal("string"))
+ })
+ })
+
+ Context("ReceivesAnAssignedAliasInBody", func() {
+ It("Processes an Assigned Alias inside a body parameter", func() {
+ info := utils.GetApiEndpointHierarchy(
+ pipe.Graph(),
+ "AliasController",
+ "ReceivesAnAssignedAliasInBody",
+ []string{"body"},
+ )
+
+ Expect(info.Params).To(HaveLen(1))
+ Expect(info.RetVals).To(HaveLen(1))
+
+ bodyNode := utils.GetSingularChildTypeNode(pipe.Graph(), info.Params[0].Node)
+ Expect(bodyNode).ToNot(BeNil())
+ Expect(bodyNode.Kind).To(Equal(common.SymKindStruct))
+
+ fieldEdges := common.MapValues(pipe.Graph().GetEdges(
+ bodyNode.Id,
+ []symboldg.SymbolEdgeKind{symboldg.EdgeKindField},
+ ))
+ allyFieldEdge := linq.First(fieldEdges, func(edge symboldg.SymbolEdgeDescriptor) bool {
+ return edge.Edge.To.Name == "Ally"
+ })
+ allyField := pipe.Graph().Get(allyFieldEdge.Edge.To)
+ Expect(allyField).ToNot(BeNil())
+
+ allyTypeEdges := common.MapValues(pipe.Graph().GetEdges(
+ allyField.Id,
+ []symboldg.SymbolEdgeKind{symboldg.EdgeKindType},
+ ))
+ allyTypeEdge := linq.First(allyTypeEdges, func(edge symboldg.SymbolEdgeDescriptor) bool {
+ return pipe.Graph().Get(edge.Edge.From) == allyField
+ })
+ allyType := pipe.Graph().Get(allyTypeEdge.Edge.To)
+
+ Expect(allyType).ToNot(BeNil())
+ Expect(allyType.Kind).To(Equal(common.SymKindAlias))
+
+ aliasMeta := utils.MustAliasMeta(allyType)
+ Expect(aliasMeta.Name).To(Equal(assignedAliasName))
+ Expect(aliasMeta.AliasType).To(Equal(metadata.AliasKindAssigned))
+
+ relevantEdges := common.MapValues(pipe.Graph().GetEdges(
+ allyType.Id,
+ []symboldg.SymbolEdgeKind{symboldg.EdgeKindType},
+ ))
+ outgoing := linq.First(relevantEdges, func(edge symboldg.SymbolEdgeDescriptor) bool {
+ return pipe.Graph().Get(edge.Edge.From) == allyType && edge.Edge.To.Name == "string"
+ })
+ aliasedType := pipe.Graph().Get(outgoing.Edge.To)
+ Expect(aliasedType).ToNot(BeNil())
+ Expect(aliasedType.Kind).To(Equal(common.SymKindBuiltin))
+ Expect(aliasedType.Id.Name).To(Equal("string"))
+ })
+ })
+
+ Context("ReturnsAnAssignedAlias", func() {
+ It("Processes an Assigned Alias return value", func() {
+ info := utils.GetApiEndpointHierarchy(
+ pipe.Graph(),
+ "AliasController",
+ "ReturnsAnAssignedAlias",
+ []string{},
+ )
+
+ Expect(info.Params).To(HaveLen(0))
+ Expect(info.RetVals).To(HaveLen(2))
+
+ var retTypeNode *symboldg.SymbolNode
+ for _, retVal := range info.RetVals {
+ node := utils.GetSingularChildTypeNode(pipe.Graph(), retVal.Node)
+ if node != nil && node.Id.Name == assignedAliasName {
+ retTypeNode = node
+ break
+ }
+ }
+
+ Expect(retTypeNode).ToNot(BeNil())
+ Expect(retTypeNode.Kind).To(Equal(common.SymKindAlias))
+
+ aliasMeta := utils.MustAliasMeta(retTypeNode)
+ Expect(aliasMeta.Name).To(Equal(assignedAliasName))
+ Expect(aliasMeta.AliasType).To(Equal(metadata.AliasKindAssigned))
+
+ relevantEdges := common.MapValues(pipe.Graph().GetEdges(
+ retTypeNode.Id,
+ []symboldg.SymbolEdgeKind{symboldg.EdgeKindType},
+ ))
+ outgoing := linq.First(relevantEdges, func(edge symboldg.SymbolEdgeDescriptor) bool {
+ return pipe.Graph().Get(edge.Edge.From) == retTypeNode && edge.Edge.To.Name == "string"
+ })
+ aliasedType := pipe.Graph().Get(outgoing.Edge.To)
+ Expect(aliasedType).ToNot(BeNil())
+ Expect(aliasedType.Kind).To(Equal(common.SymKindBuiltin))
+ Expect(aliasedType.Id.Name).To(Equal("string"))
+ })
+ })
+
+ Context("ReceivesNestedTypedefAliasQuery", func() {
+ It("Processes a Nested Typedef Alias parameter", func() {
+ info := utils.GetApiEndpointHierarchy(
+ pipe.Graph(),
+ "AliasController",
+ "ReceivesNestedTypedefAliasQuery",
+ []string{"alias"},
+ )
+
+ Expect(info.Params).To(HaveLen(1))
+ Expect(info.RetVals).To(HaveLen(1))
+
+ nestedTypeParam := utils.GetSingularChildTypeNode(pipe.Graph(), info.Params[0].Node)
+ Expect(nestedTypeParam).ToNot(BeNil())
+ Expect(nestedTypeParam.Kind).To(Equal(common.SymKindAlias))
+
+ nestedMeta := utils.MustAliasMeta(nestedTypeParam)
+ Expect(nestedMeta.Name).To(Equal("NestedTypedefAlias"))
+ Expect(nestedMeta.AliasType).To(Equal(metadata.AliasKindTypedef))
+
+ // nested alias should point to TypedefAlias
+ relevantEdges := common.MapValues(pipe.Graph().GetEdges(
+ nestedTypeParam.Id,
+ []symboldg.SymbolEdgeKind{symboldg.EdgeKindType},
+ ))
+ outgoingToTypedef := linq.First(relevantEdges, func(edge symboldg.SymbolEdgeDescriptor) bool {
+ return pipe.Graph().Get(edge.Edge.From) == nestedTypeParam && edge.Edge.To.Name == typedefAliasName
+ })
+ aliasedType := pipe.Graph().Get(outgoingToTypedef.Edge.To)
+ Expect(aliasedType).ToNot(BeNil())
+ Expect(aliasedType.Kind).To(Equal(common.SymKindAlias))
+
+ // and that TypedefAlias resolves further to builtin string
+ typedefEdges := common.MapValues(pipe.Graph().GetEdges(
+ aliasedType.Id,
+ []symboldg.SymbolEdgeKind{symboldg.EdgeKindType},
+ ))
+ outgoingToBuiltin := linq.First(typedefEdges, func(edge symboldg.SymbolEdgeDescriptor) bool {
+ return pipe.Graph().Get(edge.Edge.From) == aliasedType && edge.Edge.To.Name == "string"
+ })
+ aliasedBuiltin := pipe.Graph().Get(outgoingToBuiltin.Edge.To)
+ Expect(aliasedBuiltin).ToNot(BeNil())
+ Expect(aliasedBuiltin.Kind).To(Equal(common.SymKindBuiltin))
+ Expect(aliasedBuiltin.Id.Name).To(Equal("string"))
+ })
+ })
+
+ Context("ReceivesAnNestedTypedefAliasInBody", func() {
+ It("Processes a Nested Typedef Alias inside a body parameter", func() {
+ info := utils.GetApiEndpointHierarchy(
+ pipe.Graph(),
+ "AliasController",
+ "ReceivesAnNestedTypedefAliasInBody",
+ []string{"body"},
+ )
+
+ Expect(info.Params).To(HaveLen(1))
+ Expect(info.RetVals).To(HaveLen(1))
+
+ bodyNode := utils.GetSingularChildTypeNode(pipe.Graph(), info.Params[0].Node)
+ Expect(bodyNode).ToNot(BeNil())
+ Expect(bodyNode.Kind).To(Equal(common.SymKindStruct))
+
+ fieldEdges := common.MapValues(pipe.Graph().GetEdges(
+ bodyNode.Id,
+ []symboldg.SymbolEdgeKind{symboldg.EdgeKindField},
+ ))
+ allyFieldEdge := linq.First(fieldEdges, func(edge symboldg.SymbolEdgeDescriptor) bool {
+ return edge.Edge.To.Name == "Ally"
+ })
+ allyField := pipe.Graph().Get(allyFieldEdge.Edge.To)
+ Expect(allyField).ToNot(BeNil())
+
+ allyTypeEdges := common.MapValues(pipe.Graph().GetEdges(
+ allyField.Id,
+ []symboldg.SymbolEdgeKind{symboldg.EdgeKindType},
+ ))
+ allyTypeEdge := linq.First(allyTypeEdges, func(edge symboldg.SymbolEdgeDescriptor) bool {
+ return pipe.Graph().Get(edge.Edge.From) == allyField
+ })
+ allyType := pipe.Graph().Get(allyTypeEdge.Edge.To)
+
+ Expect(allyType).ToNot(BeNil())
+ Expect(allyType.Kind).To(Equal(common.SymKindAlias))
+
+ aliasMeta := utils.MustAliasMeta(allyType)
+ Expect(aliasMeta.Name).To(Equal("NestedTypedefAlias"))
+ Expect(aliasMeta.AliasType).To(Equal(metadata.AliasKindTypedef))
+
+ // Nested alias -> TypedefAlias -> string
+ relevantEdges := common.MapValues(pipe.Graph().GetEdges(
+ allyType.Id,
+ []symboldg.SymbolEdgeKind{symboldg.EdgeKindType},
+ ))
+ toTypedef := linq.First(relevantEdges, func(edge symboldg.SymbolEdgeDescriptor) bool {
+ return pipe.Graph().Get(edge.Edge.From) == allyType && edge.Edge.To.Name == typedefAliasName
+ })
+ typedefNode := pipe.Graph().Get(toTypedef.Edge.To)
+ Expect(typedefNode).ToNot(BeNil())
+
+ typedefEdges := common.MapValues(pipe.Graph().GetEdges(
+ typedefNode.Id,
+ []symboldg.SymbolEdgeKind{symboldg.EdgeKindType},
+ ))
+ toBuiltin := linq.First(typedefEdges, func(edge symboldg.SymbolEdgeDescriptor) bool {
+ return pipe.Graph().Get(edge.Edge.From) == typedefNode && edge.Edge.To.Name == "string"
+ })
+ builtin := pipe.Graph().Get(toBuiltin.Edge.To)
+ Expect(builtin).ToNot(BeNil())
+ Expect(builtin.Kind).To(Equal(common.SymKindBuiltin))
+ Expect(builtin.Id.Name).To(Equal("string"))
+ })
+ })
+
+ Context("ReturnsAnNestedTypedefAlias", func() {
+ It("Processes a Nested Typedef Alias return value", func() {
+ info := utils.GetApiEndpointHierarchy(
+ pipe.Graph(),
+ "AliasController",
+ "ReturnsAnNestedTypedefAlias",
+ []string{},
+ )
+
+ Expect(info.Params).To(HaveLen(0))
+ Expect(info.RetVals).To(HaveLen(2))
+
+ var retTypeNode *symboldg.SymbolNode
+ for _, retVal := range info.RetVals {
+ node := utils.GetSingularChildTypeNode(pipe.Graph(), retVal.Node)
+ if node != nil && node.Id.Name == "NestedTypedefAlias" {
+ retTypeNode = node
+ break
+ }
+ }
+
+ Expect(retTypeNode).ToNot(BeNil())
+ Expect(retTypeNode.Kind).To(Equal(common.SymKindAlias))
+
+ aliasMeta := utils.MustAliasMeta(retTypeNode)
+ Expect(aliasMeta.Name).To(Equal("NestedTypedefAlias"))
+ Expect(aliasMeta.AliasType).To(Equal(metadata.AliasKindTypedef))
+
+ // Nested -> TypedefAlias -> string
+ nestedEdges := common.MapValues(pipe.Graph().GetEdges(
+ retTypeNode.Id,
+ []symboldg.SymbolEdgeKind{symboldg.EdgeKindType},
+ ))
+ toTypedef := linq.First(nestedEdges, func(edge symboldg.SymbolEdgeDescriptor) bool {
+ return pipe.Graph().Get(edge.Edge.From) == retTypeNode && edge.Edge.To.Name == typedefAliasName
+ })
+ typedefNode := pipe.Graph().Get(toTypedef.Edge.To)
+ Expect(typedefNode).ToNot(BeNil())
+
+ typedefEdges := common.MapValues(pipe.Graph().GetEdges(
+ typedefNode.Id,
+ []symboldg.SymbolEdgeKind{symboldg.EdgeKindType},
+ ))
+ toBuiltin := linq.First(typedefEdges, func(edge symboldg.SymbolEdgeDescriptor) bool {
+ return pipe.Graph().Get(edge.Edge.From) == typedefNode && edge.Edge.To.Name == "string"
+ })
+ builtin := pipe.Graph().Get(toBuiltin.Edge.To)
+ Expect(builtin).ToNot(BeNil())
+ Expect(builtin.Kind).To(Equal(common.SymKindBuiltin))
+ Expect(builtin.Id.Name).To(Equal("string"))
+ })
+ })
+
+ Context("ReceivesNestedAssignedAliasQuery", func() {
+ It("Processes a Nested Assigned Alias parameter", func() {
+ info := utils.GetApiEndpointHierarchy(
+ pipe.Graph(),
+ "AliasController",
+ "ReceivesNestedAssignedAliasQuery",
+ []string{"alias"},
+ )
+
+ Expect(info.Params).To(HaveLen(1))
+ Expect(info.RetVals).To(HaveLen(1))
+
+ nestedTypeParam := utils.GetSingularChildTypeNode(pipe.Graph(), info.Params[0].Node)
+ Expect(nestedTypeParam).ToNot(BeNil())
+ Expect(nestedTypeParam.Kind).To(Equal(common.SymKindAlias))
+
+ nestedMeta := utils.MustAliasMeta(nestedTypeParam)
+ Expect(nestedMeta.Name).To(Equal("NestedAssignedAlias"))
+ Expect(nestedMeta.AliasType).To(Equal(metadata.AliasKindAssigned))
+
+ // nested assigned alias should point to TypedefAlias
+ relevantEdges := common.MapValues(pipe.Graph().GetEdges(
+ nestedTypeParam.Id,
+ []symboldg.SymbolEdgeKind{symboldg.EdgeKindType},
+ ))
+ toTypedef := linq.First(relevantEdges, func(edge symboldg.SymbolEdgeDescriptor) bool {
+ return pipe.Graph().Get(edge.Edge.From) == nestedTypeParam && edge.Edge.To.Name == typedefAliasName
+ })
+ typedefNode := pipe.Graph().Get(toTypedef.Edge.To)
+ Expect(typedefNode).ToNot(BeNil())
+
+ // TypedefAlias -> string
+ typedefEdges := common.MapValues(pipe.Graph().GetEdges(
+ typedefNode.Id,
+ []symboldg.SymbolEdgeKind{symboldg.EdgeKindType},
+ ))
+ toBuiltin := linq.First(typedefEdges, func(edge symboldg.SymbolEdgeDescriptor) bool {
+ return pipe.Graph().Get(edge.Edge.From) == typedefNode && edge.Edge.To.Name == "string"
+ })
+ builtin := pipe.Graph().Get(toBuiltin.Edge.To)
+ Expect(builtin).ToNot(BeNil())
+ Expect(builtin.Kind).To(Equal(common.SymKindBuiltin))
+ Expect(builtin.Id.Name).To(Equal("string"))
+ })
+ })
+
+ Context("ReceivesAnNestedAssignedAliasInBody", func() {
+ It("Processes a Nested Assigned Alias inside a body parameter", func() {
+ info := utils.GetApiEndpointHierarchy(
+ pipe.Graph(),
+ "AliasController",
+ "ReceivesAnNestedAssignedAliasInBody",
+ []string{"body"},
+ )
+
+ Expect(info.Params).To(HaveLen(1))
+ Expect(info.RetVals).To(HaveLen(1))
+
+ bodyNode := utils.GetSingularChildTypeNode(pipe.Graph(), info.Params[0].Node)
+ Expect(bodyNode).ToNot(BeNil())
+ Expect(bodyNode.Kind).To(Equal(common.SymKindStruct))
+
+ fieldEdges := common.MapValues(pipe.Graph().GetEdges(
+ bodyNode.Id,
+ []symboldg.SymbolEdgeKind{symboldg.EdgeKindField},
+ ))
+ allyFieldEdge := linq.First(fieldEdges, func(edge symboldg.SymbolEdgeDescriptor) bool {
+ return edge.Edge.To.Name == "Ally"
+ })
+ allyField := pipe.Graph().Get(allyFieldEdge.Edge.To)
+ Expect(allyField).ToNot(BeNil())
+
+ allyTypeEdges := common.MapValues(pipe.Graph().GetEdges(
+ allyField.Id,
+ []symboldg.SymbolEdgeKind{symboldg.EdgeKindType},
+ ))
+ allyTypeEdge := linq.First(allyTypeEdges, func(edge symboldg.SymbolEdgeDescriptor) bool {
+ return pipe.Graph().Get(edge.Edge.From) == allyField
+ })
+ allyType := pipe.Graph().Get(allyTypeEdge.Edge.To)
+
+ Expect(allyType).ToNot(BeNil())
+ Expect(allyType.Kind).To(Equal(common.SymKindAlias))
+
+ aliasMeta := utils.MustAliasMeta(allyType)
+ Expect(aliasMeta.Name).To(Equal("NestedAssignedAlias"))
+ Expect(aliasMeta.AliasType).To(Equal(metadata.AliasKindAssigned))
+
+ // NestedAssignedAlias -> TypedefAlias -> string
+ relevantEdges := common.MapValues(pipe.Graph().GetEdges(
+ allyType.Id,
+ []symboldg.SymbolEdgeKind{symboldg.EdgeKindType},
+ ))
+ toTypedef := linq.First(relevantEdges, func(edge symboldg.SymbolEdgeDescriptor) bool {
+ return pipe.Graph().Get(edge.Edge.From) == allyType && edge.Edge.To.Name == typedefAliasName
+ })
+ typedefNode := pipe.Graph().Get(toTypedef.Edge.To)
+ Expect(typedefNode).ToNot(BeNil())
+
+ typedefEdges := common.MapValues(pipe.Graph().GetEdges(
+ typedefNode.Id,
+ []symboldg.SymbolEdgeKind{symboldg.EdgeKindType},
+ ))
+ toBuiltin := linq.First(typedefEdges, func(edge symboldg.SymbolEdgeDescriptor) bool {
+ return pipe.Graph().Get(edge.Edge.From) == typedefNode && edge.Edge.To.Name == "string"
+ })
+ builtin := pipe.Graph().Get(toBuiltin.Edge.To)
+ Expect(builtin).ToNot(BeNil())
+ Expect(builtin.Kind).To(Equal(common.SymKindBuiltin))
+ Expect(builtin.Id.Name).To(Equal("string"))
+ })
+ })
+
+ Context("ReturnsAnNestedAssignedAlias", func() {
+ It("Processes a Nested Assigned Alias return value", func() {
+ info := utils.GetApiEndpointHierarchy(
+ pipe.Graph(),
+ "AliasController",
+ "ReturnsAnNestedAssignedAlias",
+ []string{},
+ )
+
+ Expect(info.Params).To(HaveLen(0))
+ Expect(info.RetVals).To(HaveLen(2))
+
+ var retTypeNode *symboldg.SymbolNode
+ for _, retVal := range info.RetVals {
+ node := utils.GetSingularChildTypeNode(pipe.Graph(), retVal.Node)
+ if node != nil && node.Id.Name == "NestedAssignedAlias" {
+ retTypeNode = node
+ break
+ }
+ }
+
+ Expect(retTypeNode).ToNot(BeNil())
+ Expect(retTypeNode.Kind).To(Equal(common.SymKindAlias))
+
+ aliasMeta := utils.MustAliasMeta(retTypeNode)
+ Expect(aliasMeta.Name).To(Equal("NestedAssignedAlias"))
+ Expect(aliasMeta.AliasType).To(Equal(metadata.AliasKindAssigned))
+
+ // NestedAssignedAlias -> TypedefAlias -> string
+ nestedEdges := common.MapValues(pipe.Graph().GetEdges(
+ retTypeNode.Id,
+ []symboldg.SymbolEdgeKind{symboldg.EdgeKindType},
+ ))
+ toTypedef := linq.First(nestedEdges, func(edge symboldg.SymbolEdgeDescriptor) bool {
+ return pipe.Graph().Get(edge.Edge.From) == retTypeNode && edge.Edge.To.Name == typedefAliasName
+ })
+ typedefNode := pipe.Graph().Get(toTypedef.Edge.To)
+ Expect(typedefNode).ToNot(BeNil())
+
+ typedefEdges := common.MapValues(pipe.Graph().GetEdges(
+ typedefNode.Id,
+ []symboldg.SymbolEdgeKind{symboldg.EdgeKindType},
+ ))
+ toBuiltin := linq.First(typedefEdges, func(edge symboldg.SymbolEdgeDescriptor) bool {
+ return pipe.Graph().Get(edge.Edge.From) == typedefNode && edge.Edge.To.Name == "string"
+ })
+ builtin := pipe.Graph().Get(toBuiltin.Edge.To)
+ Expect(builtin).ToNot(BeNil())
+ Expect(builtin.Kind).To(Equal(common.SymKindBuiltin))
+ Expect(builtin.Id.Name).To(Equal("string"))
+ })
+ })
+
+ Context("ReceivesATypedefSpecialAliasQuery", func() {
+ It("Processes a Typedef-special alias parameter (TypedefSpecialAlias -> time.Time)", func() {
+ info := utils.GetApiEndpointHierarchy(
+ pipe.Graph(),
+ "AliasController",
+ "ReceivesATypedefSpecialAliasQuery",
+ []string{"alias"},
+ )
+
+ Expect(info.Params).To(HaveLen(1))
+ Expect(info.RetVals).To(HaveLen(1))
+
+ aliasParamNode := utils.GetSingularChildTypeNode(pipe.Graph(), info.Params[0].Node)
+ Expect(aliasParamNode).ToNot(BeNil())
+ Expect(aliasParamNode.Kind).To(Equal(common.SymKindAlias))
+
+ aliasMeta := utils.MustAliasMeta(aliasParamNode)
+ Expect(aliasMeta.Name).To(Equal("TypedefSpecialAlias"))
+ Expect(aliasMeta.AliasType).To(Equal(metadata.AliasKindTypedef))
+
+ // find outgoing type-edge from the alias that points to Time
+ relevantEdges := common.MapValues(pipe.Graph().GetEdges(
+ aliasParamNode.Id,
+ []symboldg.SymbolEdgeKind{symboldg.EdgeKindType},
+ ))
+
+ outgoingToTime := linq.First(relevantEdges, func(edge symboldg.SymbolEdgeDescriptor) bool {
+ return pipe.Graph().Get(edge.Edge.From) == aliasParamNode && edge.Edge.To.Name == "time.Time"
+ })
+ Expect(outgoingToTime).ToNot(BeNil())
+ aliasedType := pipe.Graph().Get(outgoingToTime.Edge.To)
+
+ Expect(aliasedType).ToNot(BeNil())
+ Expect(aliasedType.Id.Name).To(Equal("time.Time"))
+ Expect(aliasedType.Kind).To(Equal(common.SymKindSpecialBuiltin))
+ })
+ })
+
+ Context("ReceivesAnAssignedSpecialAliasQuery", func() {
+ It("Processes an Assigned-special alias parameter (AssignedSpecialAlias = time.Time)", func() {
+ info := utils.GetApiEndpointHierarchy(
+ pipe.Graph(),
+ "AliasController",
+ "ReceivesAnAssignedSpecialAliasQuery",
+ []string{"alias"},
+ )
+
+ Expect(info.Params).To(HaveLen(1))
+ Expect(info.RetVals).To(HaveLen(1))
+
+ aliasParamNode := utils.GetSingularChildTypeNode(pipe.Graph(), info.Params[0].Node)
+ Expect(aliasParamNode).ToNot(BeNil())
+ Expect(aliasParamNode.Kind).To(Equal(common.SymKindAlias))
+
+ aliasMeta := utils.MustAliasMeta(aliasParamNode)
+ Expect(aliasMeta.Name).To(Equal("AssignedSpecialAlias"))
+ Expect(aliasMeta.AliasType).To(Equal(metadata.AliasKindAssigned))
+
+ // find outgoing type-edge from the alias that points to Time
+ relevantEdges := common.MapValues(pipe.Graph().GetEdges(
+ aliasParamNode.Id,
+ []symboldg.SymbolEdgeKind{symboldg.EdgeKindType},
+ ))
+
+ outgoingToTime := linq.First(relevantEdges, func(edge symboldg.SymbolEdgeDescriptor) bool {
+ return pipe.Graph().Get(edge.Edge.From) == aliasParamNode && edge.Edge.To.Name == "time.Time"
+ })
+ Expect(outgoingToTime).ToNot(BeNil())
+
+ aliasedType := pipe.Graph().Get(outgoingToTime.Edge.To)
+
+ Expect(aliasedType).ToNot(BeNil())
+ Expect(aliasedType.Id.Name).To(Equal("time.Time"))
+ Expect(aliasedType.Kind).To(Equal(common.SymKindSpecialBuiltin))
+ })
+ })
+
+ Context("HIR Generation", func() {
+ It("Produces correct models list when an alias is present", func() {
+ intermediate, err := pipe.GenerateIntermediate()
+ Expect(err).To(BeNil())
+ Expect(intermediate).NotTo(BeNil())
+
+ // Basic top-level assertions
+ Expect(intermediate.PlainErrorPresent).To(BeTrue())
+
+ // Imports assertions (core: package present and contains expected names)
+ Expect(intermediate.Imports).To(HaveKey("github.com/gopher-fleece/gleece/test/alias"))
+ expectedImportNames := []string{
+ "Response1TypedefAlias",
+ "Param3alias",
+ "Param4body",
+ "Param6body",
+ "Param8body",
+ "AliasController",
+ "Param1alias",
+ "Param2body",
+ "Response3AssignedAlias",
+ "Param7alias",
+ "Param9alias",
+ "Param10alias",
+ "Param5alias",
+ "Response5NestedTypedefAlias",
+ "Response7NestedAssignedAlias",
+ }
+ Expect(intermediate.Imports["github.com/gopher-fleece/gleece/test/alias"]).To(
+ ConsistOf(expectedImportNames),
+ )
+
+ // Helper: find controller by name
+ findController := func(
+ flat []definitions.ControllerMetadata,
+ name string,
+ ) *definitions.ControllerMetadata {
+ for i := range flat {
+ if flat[i].Name == name {
+ return &flat[i]
+ }
+ }
+ return nil
+ }
+
+ ctrl := findController(intermediate.Flat, "AliasController")
+ Expect(ctrl).NotTo(BeNil())
+ Expect(ctrl.PkgPath).To(Equal("github.com/gopher-fleece/gleece/test/alias"))
+ Expect(ctrl.Tag).To(Equal("Alias Controller Tag"))
+ Expect(ctrl.Description).To(Equal("Alias Controller"))
+ Expect(ctrl.RestMetadata.Path).To(Equal("/test/alias"))
+
+ // Helper: find route by operation id
+ findRoute := func(routes []definitions.RouteMetadata, op string) *definitions.RouteMetadata {
+ for i := range routes {
+ if routes[i].OperationId == op {
+ return &routes[i]
+ }
+ }
+ return nil
+ }
+
+ // Check a few representative routes and their param/response core fields
+ r := findRoute(ctrl.Routes, "ReceivesTypedefAliasQuery")
+ Expect(r).NotTo(BeNil())
+ Expect(r.RestMetadata.Path).To(Equal("/td-alias-query"))
+ Expect(len(r.FuncParams)).To(BeNumerically(">", 0))
+ fp := r.FuncParams[0]
+ Expect(fp.Name).To(Equal("alias"))
+ Expect(fp.TypeMeta.Name).To(Equal("TypedefAlias"))
+ Expect(fp.TypeMeta.PkgPath).To(Equal("github.com/gopher-fleece/gleece/test/alias"))
+ Expect(fp.Validator).To(Equal("required"))
+ Expect(fp.UniqueImportSerial).To(Equal(uint64(1)))
+
+ // route that returns a value
+ rr := findRoute(ctrl.Routes, "ReturnsATypedefAlias")
+ Expect(rr).NotTo(BeNil())
+ Expect(rr.HasReturnValue).To(BeTrue())
+ Expect(len(rr.Responses)).To(BeNumerically(">=", 1))
+ Expect(rr.Responses[0].Name).To(Equal("TypedefAlias"))
+ Expect(rr.Responses[0].PkgPath).To(Equal("github.com/gopher-fleece/gleece/test/alias"))
+ Expect(rr.ResponseSuccessCode).To(Equal(runtime.StatusOK))
+
+ // Another representative: assigned alias query
+ r2 := findRoute(ctrl.Routes, "ReceivesAssignedAliasQuery")
+ Expect(r2).NotTo(BeNil())
+ Expect(r2.RestMetadata.Path).To(Equal("/as-alias-query"))
+ Expect(len(r2.FuncParams)).To(BeNumerically(">", 0))
+ fp2 := r2.FuncParams[0]
+ Expect(fp2.TypeMeta.Name).To(Equal("AssignedAlias"))
+ Expect(fp2.UniqueImportSerial).To(Equal(uint64(3)))
+
+ // Models assertions: build map for easier lookup and assert core fields
+
+ structMap := map[string]definitions.StructMetadata{}
+ for _, s := range intermediate.Models.Structs {
+ structMap[s.Name] = s
+ }
+
+ aliasMap := map[string]definitions.NakedAliasMetadata{}
+ for _, s := range intermediate.Models.Aliases {
+ aliasMap[s.Name] = s
+ }
+
+ // TypedefAlias
+ td, ok := aliasMap["TypedefAlias"]
+ Expect(ok).To(BeTrue())
+ Expect(td.PkgPath).To(Equal("github.com/gopher-fleece/gleece/test/alias"))
+ Expect(td.Type).To(Equal("string"))
+
+ // AssignedAlias
+ aa, ok := aliasMap["AssignedAlias"]
+ Expect(ok).To(BeTrue())
+ Expect(aa.PkgPath).To(Equal("github.com/gopher-fleece/gleece/test/alias"))
+ Expect(aa.Type).To(Equal("string"))
+
+ // NestedTypedefAlias (embedded typedef)
+ ntd, ok := aliasMap["NestedTypedefAlias"]
+ Expect(ok).To(BeTrue())
+ Expect(ntd.Type).To(Equal("TypedefAlias"))
+
+ // NestedAssignedAlias (embedded typedef alias)
+ nas, ok := aliasMap["NestedAssignedAlias"]
+ Expect(ok).To(BeTrue())
+ Expect(nas.Type).To(Equal("TypedefAlias"))
+
+ // Body structs containing alias-typed fields
+ btd, ok := structMap["BodyWithTypedefAlias"]
+ Expect(ok).To(BeTrue())
+ Expect(len(btd.Fields)).To(BeNumerically(">", 0))
+ Expect(btd.Fields[0].Type).To(Equal("TypedefAlias"))
+
+ bna, ok := structMap["BodyWithNestedAssignedAlias"]
+ Expect(ok).To(BeTrue())
+ Expect(len(bna.Fields)).To(BeNumerically(">", 0))
+ Expect(bna.Fields[0].Type).To(Equal("NestedAssignedAlias"))
+
+ // Special typedefs (time-based)
+ ts, ok := aliasMap["TypedefSpecialAlias"]
+ Expect(ok).To(BeTrue())
+ Expect(ts.Type).To(Equal("Time"))
+
+ aSpecial, ok := aliasMap["AssignedSpecialAlias"]
+ Expect(ok).To(BeTrue())
+ Expect(aSpecial.Type).To(Equal("Time"))
+ })
+ })
+})
+
+func TestAliasController(t *testing.T) {
+ logger.SetLogLevel(logger.LogLevelNone)
+ RegisterFailHandler(Fail)
+ RunSpecs(t, "Alias Controller")
+}
diff --git a/test/alias/gleece.test.config.json b/test/alias/gleece.test.config.json
new file mode 100644
index 0000000..ced1754
--- /dev/null
+++ b/test/alias/gleece.test.config.json
@@ -0,0 +1,54 @@
+{
+ "commonConfig": {
+ "controllerGlobs": [
+ "./*.controller.go"
+ ]
+ },
+ "routesConfig": {
+ "engine": "gin",
+ "outputPath": "./dist/gleece.go",
+ "outputFilePerms": "0644",
+ "authorizationConfig": {
+ "authFileFullPackageName": "github.com/gopher-fleece/gleece/test/fixtures",
+ "enforceSecurityOnAllRoutes": true
+ }
+ },
+ "openapiGeneratorConfig": {
+ "openapi": "3.0.0",
+ "info": {
+ "title": "Sample API",
+ "description": "This is a sample API",
+ "termsOfService": "http://example.com/terms/",
+ "contact": {
+ "name": "API Support",
+ "url": "http://www.example.com/support",
+ "email": "support@example.com"
+ },
+ "license": {
+ "name": "Apache 2.0",
+ "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
+ },
+ "version": "1.0.0"
+ },
+ "baseUrl": "https://api.example.com",
+ "securitySchemes": [
+ {
+ "description": "API Key for accessing the API",
+ "name": "securitySchemaName",
+ "fieldName": "x-header-name",
+ "type": "apiKey",
+ "in": "header"
+ }
+ ],
+ "defaultSecurity": {
+ "name": "sanitySchema",
+ "scopes": [
+ "read",
+ "write"
+ ]
+ },
+ "specGeneratorConfig": {
+ "outputPath": "./dist/swagger.json"
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/context/context.controller.go b/test/context/context.controller.go
index 7c6ffe3..804cd65 100644
--- a/test/context/context.controller.go
+++ b/test/context/context.controller.go
@@ -21,7 +21,7 @@ func (ec *ContextController) MethodWithContext(ctx context.Context, id string) e
}
// @Method(POST)
-// @Route(/{id}/context-as-last-param)
+// @Route(/context-as-last-param/{id})
// @Path(id)
func (ec *ContextController) MethodWithLastParamContext(id string, ctx context.Context) error {
return nil
diff --git a/test/diagnostics/diagnostics.controller.go b/test/diagnostics/diagnostics.controller.go
index c36553c..22df23b 100644
--- a/test/diagnostics/diagnostics.controller.go
+++ b/test/diagnostics/diagnostics.controller.go
@@ -27,7 +27,7 @@ func (c DiagnosticsController) MethodWithUnlinkedParam(id string) error {
return nil
}
-// @Route(/unlinked-param/{aliased})
+// @Route(/unlinked-alias/{aliased})
// @Method(POST)
// @Path(id)
func (c DiagnosticsController) MethodWithUnlinkedAlias(id string) error {
diff --git a/test/errorhandling/errorhandling_test.go b/test/errorhandling/errorhandling_test.go
index f404ab5..4db5065 100644
--- a/test/errorhandling/errorhandling_test.go
+++ b/test/errorhandling/errorhandling_test.go
@@ -68,19 +68,6 @@ var _ = Describe("Error-handling", func() {
Expect(err).To(MatchError(ContainSubstring("could not read given template ImportsExtension override at")))
})
-
- // Gleece now automatically crawls over package dependencies so this test is no longer valid.
- // Leaving it in for now for reference.
-
- /*
- It("Returns a clear error when type declared outside of global path", func() {
- configPath := utils.GetAbsPathByRelativeOrFail("gleece.unscanned.types.json")
- err := cmd.GenerateRoutes(arguments.CliArguments{ConfigPath: configPath})
- Expect(err).To(MatchError(ContainSubstring("encountered an error visiting controller UnScannedTypeController method EmptyMethod - type 'HoldsVeryNestedStructs' was not found in package 'errorhandling_test'")))
- // TODO: Test that case too
- // Expect(err).To(MatchError(ContainSubstring("encountered an error visiting controller UnScannedTypeController method EmptyMethod - could not find type 'HoldsVeryNestedStructs' in package 'github.com/gopher-fleece/gleece/test/errorhandling', are you sure it's included in the 'commonConfig->controllerGlobs' search paths?")))
- })
- */
})
func TestErrorHandling(t *testing.T) {
diff --git a/test/generics/generics.controller.go b/test/generics/generics.controller.go
new file mode 100644
index 0000000..83f9a2d
--- /dev/null
+++ b/test/generics/generics.controller.go
@@ -0,0 +1,100 @@
+package generics_test
+
+import (
+ "github.com/gopher-fleece/gleece/test/types"
+ "github.com/gopher-fleece/runtime"
+)
+
+type BodyWithPrimitiveMap struct {
+ Dict map[string]int
+}
+
+type MonoGenericStruct[T any] struct {
+ Value T
+}
+
+type MultiGenericStruct[TA, TB any] struct {
+ ValueA TA
+ ValueB TB
+}
+
+// @Tag(Generics Controller Tag)
+// @Route(/test/generics)
+// @Description Generics Controller
+type GenericsController struct {
+ runtime.GleeceController // Embedding the GleeceController to inherit its methods
+}
+
+// @Method(POST)
+// @Route(/primitive-map-in-body)
+// @Body(body)
+func (ec *GenericsController) RecvWithPrimitiveMapInBody(body BodyWithPrimitiveMap) error {
+ return nil
+}
+
+// @Method(POST)
+// @Route(/primitive-map-return)
+func (ec *GenericsController) RecvReturningAPrimitiveMap() (map[string]int, error) {
+ return nil, nil
+}
+
+// This checks for composite de-duplication/diffing.
+// Basically, all usages of an instantiated composite should have the same graph node
+// @Method(POST)
+// @Route(/other-primitive-map-return)
+func (ec *GenericsController) RecvReturningAnotherPrimitiveMap() (map[string]string, error) {
+ return nil, nil
+}
+
+// @Method(POST)
+// @Route(/primitive-map-body)
+// @Body(body)
+func (ec *GenericsController) RecvWithPrimitiveMapBody(body map[string]float32) error {
+ return nil
+}
+
+// @Method(POST)
+// @Route(/primitive-map-body)
+// @Body(body)
+func (ec *GenericsController) RecvWithNonPrimitiveMapBody(body map[string]types.HoldsVeryNestedStructs) error {
+ return nil
+}
+
+// @Method(POST)
+// @Route(/mono-generic-struct-body)
+// @Body(body)
+func (ec *GenericsController) RecvWithMonoGenericStructBody(body MonoGenericStruct[string]) error {
+ return nil
+}
+
+// @Method(POST)
+// @Route(/multi-generic-struct-body)
+// @Body(body)
+func (ec *GenericsController) RecvWithMultiGenericStructBody(body MultiGenericStruct[string, int]) error {
+ return nil
+}
+
+// @Method(POST)
+// @Route(/multi-generic-struct-response)
+func (ec *GenericsController) RecvWithMultiGenericStructResponse() (MultiGenericStruct[string, int], error) {
+ return MultiGenericStruct[string, int]{}, nil
+}
+
+// @Method(POST)
+// @Route(/multi-generic-struct-body-diff-params)
+func (ec *GenericsController) RecvWithMultiGenericStructWithDiffTParamsResponse() (MultiGenericStruct[bool, int], error) {
+ return MultiGenericStruct[bool, int]{}, nil
+}
+
+// @Method(POST)
+// @Route(/multi-generic-struct-ptr-body)
+// @Body(body)
+func (ec *GenericsController) RecvWithMultiGenericStructPtrBody(body *MultiGenericStruct[string, int]) error {
+ return nil
+}
+
+// @Method(POST)
+// @Route(/multi-generic-struct-ptr-response)
+func (ec *GenericsController) RecvWithMultiGenericStructPtrResponse() (*MultiGenericStruct[string, int], error) {
+ return nil, nil
+}
diff --git a/test/generics/generics_test.go b/test/generics/generics_test.go
new file mode 100644
index 0000000..8a65158
--- /dev/null
+++ b/test/generics/generics_test.go
@@ -0,0 +1,180 @@
+package generics_test
+
+import (
+ "testing"
+
+ "github.com/gopher-fleece/gleece/common"
+ "github.com/gopher-fleece/gleece/common/linq"
+ "github.com/gopher-fleece/gleece/core/pipeline"
+ "github.com/gopher-fleece/gleece/definitions"
+ "github.com/gopher-fleece/gleece/graphs"
+ "github.com/gopher-fleece/gleece/graphs/symboldg"
+ "github.com/gopher-fleece/gleece/infrastructure/logger"
+ "github.com/gopher-fleece/gleece/test/utils"
+ "github.com/gopher-fleece/gleece/test/utils/matchers"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+)
+
+var pipe pipeline.GleecePipeline
+
+var _ = Describe("Generics Controller", func() {
+ BeforeEach(func() {
+ pipe = utils.GetPipelineOrFail()
+ err := pipe.GenerateGraph()
+ Expect(err).To(BeNil())
+
+ pipe.Validate()
+ })
+
+ Context("RecvWithPrimitiveMapInBody", func() {
+ It("Generates correct graph for primitive maps in parameter structs", func() {
+ info := utils.GetApiEndpointHierarchy(
+ pipe.Graph(),
+ "GenericsController",
+ "RecvWithPrimitiveMapInBody",
+ []string{"body"},
+ )
+ // Parameter tree structure checks
+ Expect(info.Params).To(HaveLen(1))
+
+ bodyParamNode := utils.GetSingularChildNode(pipe.Graph(), info.Params[0].Node, symboldg.EdgeKindParam)
+ bodyStructNode := utils.GetSingularChildTypeNode(pipe.Graph(), bodyParamNode)
+ Expect(bodyStructNode.Id.Name).To(Equal("BodyWithPrimitiveMap"))
+ structMeta := utils.MustStructMeta(bodyStructNode)
+ Expect(structMeta.Fields).To(HaveLen(1))
+ utils.AssertFieldIsMap(structMeta, "Dict", "string", "int")
+
+ dictFieldNode, _ := utils.AssertGetField(pipe.Graph(), bodyStructNode, "Dict")
+ tParamChildren := utils.FollowThroughCompositeToTypeParams(pipe.Graph(), dictFieldNode)
+
+ Expect(tParamChildren).To(matchers.MatchNodeIdNames([]string{"string", "int"}))
+
+ // RetVals tree structure checks
+ Expect(info.RetVals).To(HaveLen(1))
+ retTypeNode := utils.GetSingularChildTypeNode(pipe.Graph(), info.RetVals[0].Node)
+ Expect(retTypeNode.Id).To(Equal(graphs.NewUniverseSymbolKey("error")))
+ })
+
+ It("Creates correct models", func() {
+ intermediate, err := pipe.GenerateIntermediate()
+ Expect(err).To(BeNil())
+ Expect(intermediate.Models.Structs).To(ContainElements(
+ []definitions.StructMetadata{
+ {
+
+ Name: "MonoGenericStructString",
+ PkgPath: "github.com/gopher-fleece/gleece/test/generics",
+ Fields: []definitions.FieldMetadata{{
+ Name: "Value",
+ Type: "string",
+ Deprecation: common.Ptr(definitions.DeprecationOptions{Deprecated: false, Description: ""}),
+ }},
+ },
+ {
+ Name: "MultiGenericStructBoolInt",
+ PkgPath: "github.com/gopher-fleece/gleece/test/generics",
+ Fields: []definitions.FieldMetadata{
+ {
+ Name: "ValueA",
+ Type: "bool",
+ Deprecation: common.Ptr(definitions.DeprecationOptions{Deprecated: false, Description: ""}),
+ },
+ {
+ Name: "ValueB",
+ Type: "int",
+ Deprecation: common.Ptr(definitions.DeprecationOptions{Deprecated: false, Description: ""}),
+ },
+ },
+ },
+ {
+ Name: "MultiGenericStructStringInt",
+ PkgPath: "github.com/gopher-fleece/gleece/test/generics",
+ Fields: []definitions.FieldMetadata{
+ {
+ Name: "ValueA",
+ Type: "string",
+ Deprecation: common.Ptr(definitions.DeprecationOptions{Deprecated: false, Description: ""}),
+ },
+ {
+ Name: "ValueB",
+ Type: "int",
+ Deprecation: common.Ptr(definitions.DeprecationOptions{Deprecated: false, Description: ""}),
+ },
+ },
+ },
+ }),
+ )
+ })
+ })
+
+ Context("RecvReturningAPrimitiveMap", func() {
+ It("Generates correct graph for primitive maps in parameter structs", func() {
+ info := utils.GetApiEndpointHierarchy(
+ pipe.Graph(),
+ "GenericsController",
+ "RecvReturningAPrimitiveMap",
+ nil,
+ )
+
+ Expect(info.Params).To(HaveLen(0))
+ Expect(info.RetVals).To(HaveLen(2))
+
+ mapRetValInfo := linq.First(
+ info.RetVals,
+ func(retVal utils.FuncRetValInfo) bool {
+ retTypeNode := utils.GetSingularChildTypeNode(pipe.Graph(), retVal.Node)
+ return retTypeNode.Id.Name != "error"
+ },
+ )
+
+ Expect(mapRetValInfo).ToNot(BeNil())
+ tParamChildren := utils.FollowThroughCompositeToTypeParams(pipe.Graph(), mapRetValInfo.Node)
+ Expect(tParamChildren).To(matchers.MatchNodeIdNames([]string{"string", "int"}))
+ })
+ })
+
+ Context("RecvWithNonPrimitiveMapBody", func() {
+ It("Generates correct graph for primitive maps in parameter structs", func() {
+ info := utils.GetApiEndpointHierarchy(
+ pipe.Graph(),
+ "GenericsController",
+ "RecvWithNonPrimitiveMapBody",
+ []string{"body"},
+ )
+
+ Expect(info.Params).To(HaveLen(1))
+ Expect(info.RetVals).To(HaveLen(1))
+
+ mapTypeParam := utils.GetSingularChildTypeNode(pipe.Graph(), info.Params[0].Node)
+
+ Expect(mapTypeParam).ToNot(BeNil())
+ tParamChildren := utils.FollowThroughCompositeToTypeParams(pipe.Graph(), info.Params[0].Node)
+
+ expectedStructName := "HoldsVeryNestedStructs"
+
+ Expect(tParamChildren).To(matchers.MatchNodeIdNames([]string{"string", expectedStructName}))
+
+ structNode := linq.First(tParamChildren, func(node *symboldg.SymbolNode) bool {
+ return node.Id.Name == expectedStructName
+ })
+
+ Expect(structNode).ToNot(BeNil())
+ Expect(*structNode).ToNot(BeNil())
+
+ structMeta := utils.MustStructMeta(*structNode)
+ Expect(structMeta.Name).To(Equal(expectedStructName))
+ Expect(structMeta).To(matchers.HaveStructFields([]matchers.FieldDesc{
+ {Name: "FieldA", TypeName: "float32"},
+ {Name: "FieldB", TypeName: "uint"},
+ {Name: "FieldC", TypeName: "SomeNestedStruct"},
+ }))
+ })
+ })
+})
+
+func TestGenericsController(t *testing.T) {
+ logger.SetLogLevel(logger.LogLevelNone)
+ RegisterFailHandler(Fail)
+ RunSpecs(t, "Generics Controller")
+}
diff --git a/test/generics/gleece.test.config.json b/test/generics/gleece.test.config.json
new file mode 100644
index 0000000..ced1754
--- /dev/null
+++ b/test/generics/gleece.test.config.json
@@ -0,0 +1,54 @@
+{
+ "commonConfig": {
+ "controllerGlobs": [
+ "./*.controller.go"
+ ]
+ },
+ "routesConfig": {
+ "engine": "gin",
+ "outputPath": "./dist/gleece.go",
+ "outputFilePerms": "0644",
+ "authorizationConfig": {
+ "authFileFullPackageName": "github.com/gopher-fleece/gleece/test/fixtures",
+ "enforceSecurityOnAllRoutes": true
+ }
+ },
+ "openapiGeneratorConfig": {
+ "openapi": "3.0.0",
+ "info": {
+ "title": "Sample API",
+ "description": "This is a sample API",
+ "termsOfService": "http://example.com/terms/",
+ "contact": {
+ "name": "API Support",
+ "url": "http://www.example.com/support",
+ "email": "support@example.com"
+ },
+ "license": {
+ "name": "Apache 2.0",
+ "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
+ },
+ "version": "1.0.0"
+ },
+ "baseUrl": "https://api.example.com",
+ "securitySchemes": [
+ {
+ "description": "API Key for accessing the API",
+ "name": "securitySchemaName",
+ "fieldName": "x-header-name",
+ "type": "apiKey",
+ "in": "header"
+ }
+ ],
+ "defaultSecurity": {
+ "name": "sanitySchema",
+ "scopes": [
+ "read",
+ "write"
+ ]
+ },
+ "specGeneratorConfig": {
+ "outputPath": "./dist/swagger.json"
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/graph/graph_test.go b/test/graph/graph_test.go
index f81a2ad..c0fb3a0 100644
--- a/test/graph/graph_test.go
+++ b/test/graph/graph_test.go
@@ -33,7 +33,7 @@ var _ = Describe("Graph Controller", func() {
Expect(receiver1Children[0].Id.Name).To(Equal("routeParam"))
Expect(receiver1Children[0].Id.IsUniverse).To(BeFalse())
Expect(receiver1Children[0].Id.IsBuiltIn).To(BeFalse())
- Expect(receiver1Children[0].Kind).To(Equal(common.SymKindField))
+ Expect(receiver1Children[0].Kind).To(Equal(common.SymKindParameter))
routeParamChildren := pipe.Graph().Children(receiver1Children[0], orderTraversalBehavior)
Expect(routeParamChildren).To(HaveLen(1))
@@ -46,7 +46,7 @@ var _ = Describe("Graph Controller", func() {
Expect(receiver1Children[1].Id.Name).To(Equal("queryParam"))
Expect(receiver1Children[1].Id.IsUniverse).To(BeFalse())
Expect(receiver1Children[1].Id.IsBuiltIn).To(BeFalse())
- Expect(receiver1Children[1].Kind).To(Equal(common.SymKindField))
+ Expect(receiver1Children[1].Kind).To(Equal(common.SymKindParameter))
queryParamChildren := pipe.Graph().Children(receiver1Children[1], orderTraversalBehavior)
Expect(queryParamChildren).To(HaveLen(1))
@@ -59,7 +59,7 @@ var _ = Describe("Graph Controller", func() {
Expect(receiver1Children[2].Id.Name).To(Equal("headerParam"))
Expect(receiver1Children[2].Id.IsUniverse).To(BeFalse())
Expect(receiver1Children[2].Id.IsBuiltIn).To(BeFalse())
- Expect(receiver1Children[2].Kind).To(Equal(common.SymKindField))
+ Expect(receiver1Children[2].Kind).To(Equal(common.SymKindParameter))
headerParamChildren := pipe.Graph().Children(receiver1Children[2], orderTraversalBehavior)
Expect(headerParamChildren).To(HaveLen(1))
@@ -72,7 +72,7 @@ var _ = Describe("Graph Controller", func() {
Expect(receiver1Children[3].Id.Name).To(Equal(""))
Expect(receiver1Children[3].Id.IsUniverse).To(BeFalse())
Expect(receiver1Children[3].Id.IsBuiltIn).To(BeFalse())
- Expect(receiver1Children[3].Kind).To(Equal(common.SymKindField))
+ Expect(receiver1Children[3].Kind).To(Equal(common.SymKindReturnType))
retValChildren := pipe.Graph().Children(receiver1Children[3], orderTraversalBehavior)
Expect(retValChildren).To(HaveLen(1))
diff --git a/test/sanity/sanity.controller.go b/test/sanity/sanity.controller.go
index e6f8426..584fb1f 100644
--- a/test/sanity/sanity.controller.go
+++ b/test/sanity/sanity.controller.go
@@ -21,7 +21,7 @@ type SanityController struct {
// A sanity test controller method
// @Method(POST)
-// @Route(/{routeParamAlias})
+// @Route(/valid-method-simple-route-query-and-header-params/{routeParamAlias})
// @Path(routeParam, {name: "routeParamAlias"})
// @Query(queryParam)
// @Header(headerParam)
diff --git a/test/sanity/sanity_test.go b/test/sanity/sanity_test.go
index 34f622a..9dc34f9 100644
--- a/test/sanity/sanity_test.go
+++ b/test/sanity/sanity_test.go
@@ -58,7 +58,7 @@ var _ = Describe("Sanity Controller", func() {
Expect(route.Deprecation.Deprecated).To(BeFalse())
Expect(route.Deprecation.Description).To(BeEmpty())
Expect(route.Description).To(Equal("A sanity test controller method"))
- Expect(route.RestMetadata.Path).To(Equal("/{routeParamAlias}"))
+ Expect(route.RestMetadata.Path).To(Equal("/valid-method-simple-route-query-and-header-params/{routeParamAlias}"))
Expect(route.FuncParams).To(HaveLen(3))
Expect(route.Responses).To(HaveLen(2))
Expect(route.HasReturnValue).To(BeTrue())
@@ -150,6 +150,8 @@ var _ = Describe("Sanity Controller", func() {
Expect(route.Responses[1].TypeMetadata.Import).To(Equal(common.ImportTypeNone))
Expect(route.Responses[1].TypeMetadata.IsUniverseType).To(BeTrue())
Expect(route.Responses[1].TypeMetadata.IsByAddress).To(BeFalse())
+ // Currently, 'error' is marked 'universe' since it exists in the universe scope and is a *types.TypeName
+ // This is somewhat conflicting with some other internal logic classifying this a
Expect(route.Responses[1].TypeMetadata.SymbolKind).To(Equal(common.SymKindSpecialBuiltin))
})
diff --git a/test/specials/gleece.test.config.json b/test/specials/gleece.test.config.json
new file mode 100644
index 0000000..ced1754
--- /dev/null
+++ b/test/specials/gleece.test.config.json
@@ -0,0 +1,54 @@
+{
+ "commonConfig": {
+ "controllerGlobs": [
+ "./*.controller.go"
+ ]
+ },
+ "routesConfig": {
+ "engine": "gin",
+ "outputPath": "./dist/gleece.go",
+ "outputFilePerms": "0644",
+ "authorizationConfig": {
+ "authFileFullPackageName": "github.com/gopher-fleece/gleece/test/fixtures",
+ "enforceSecurityOnAllRoutes": true
+ }
+ },
+ "openapiGeneratorConfig": {
+ "openapi": "3.0.0",
+ "info": {
+ "title": "Sample API",
+ "description": "This is a sample API",
+ "termsOfService": "http://example.com/terms/",
+ "contact": {
+ "name": "API Support",
+ "url": "http://www.example.com/support",
+ "email": "support@example.com"
+ },
+ "license": {
+ "name": "Apache 2.0",
+ "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
+ },
+ "version": "1.0.0"
+ },
+ "baseUrl": "https://api.example.com",
+ "securitySchemes": [
+ {
+ "description": "API Key for accessing the API",
+ "name": "securitySchemaName",
+ "fieldName": "x-header-name",
+ "type": "apiKey",
+ "in": "header"
+ }
+ ],
+ "defaultSecurity": {
+ "name": "sanitySchema",
+ "scopes": [
+ "read",
+ "write"
+ ]
+ },
+ "specGeneratorConfig": {
+ "outputPath": "./dist/swagger.json"
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/specials/specials.controller.go b/test/specials/specials.controller.go
new file mode 100644
index 0000000..bd3b25e
--- /dev/null
+++ b/test/specials/specials.controller.go
@@ -0,0 +1,54 @@
+package sanity_test
+
+import (
+ "context"
+ "time"
+
+ "github.com/gopher-fleece/runtime"
+)
+
+// @Tag(Specials Controller Tag)
+// @Route(/test/specials)
+// @Description Specials Controller
+type SpecialsController struct {
+ runtime.GleeceController // Embedding the GleeceController to inherit its methods
+}
+
+// @Method(POST)
+// @Route(/accepts-any)
+// @Body(body)
+func (ec *SpecialsController) ReceivesAny(body any) error {
+ return nil
+}
+
+// @Method(POST)
+// @Route(/returns-any)
+func (ec *SpecialsController) ReturnsAny() (any, error) {
+ return nil, nil
+}
+
+// @Method(POST)
+// @Route(/accepts-time)
+// @Body(body)
+func (ec *SpecialsController) ReceivesTime(body time.Time) error {
+ return nil
+}
+
+// @Method(POST)
+// @Route(/returns-time)
+func (ec *SpecialsController) ReturnsTime() (time.Time, error) {
+ return time.Now(), nil
+}
+
+// @Method(POST)
+// @Route(/accepts-time)
+// @Body(body)
+func (ec *SpecialsController) ReceivesContext(body context.Context) error {
+ return nil
+}
+
+// @Method(POST)
+// @Route(/returns-time)
+func (ec *SpecialsController) ReturnsContext() (context.Context, error) {
+ return nil, nil
+}
diff --git a/test/specials/specials_test.go b/test/specials/specials_test.go
new file mode 100644
index 0000000..3af24e0
--- /dev/null
+++ b/test/specials/specials_test.go
@@ -0,0 +1,32 @@
+package sanity_test
+
+import (
+ "testing"
+
+ "github.com/gopher-fleece/gleece/core/pipeline"
+ "github.com/gopher-fleece/gleece/infrastructure/logger"
+ "github.com/gopher-fleece/gleece/test/utils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+)
+
+var pipe pipeline.GleecePipeline
+
+var _ = Describe("Edge-Cases Controller", func() {
+ _ = BeforeEach(func() {
+ pipe = utils.GetPipelineOrFail()
+ })
+
+ Context("ReturnsAny", func() {
+ It("Generates correct graph", func() {
+ err := pipe.GenerateGraph()
+ Expect(err).To(BeNil())
+ })
+ })
+})
+
+func TestEdgeCasesController(t *testing.T) {
+ logger.SetLogLevel(logger.LogLevelNone)
+ RegisterFailHandler(Fail)
+ RunSpecs(t, "Edge-Cases Controller")
+}
diff --git a/test/units/arbitrators/ast.arbitrator_test.go b/test/units/arbitrators/ast.arbitrator_test.go
index d975ada..0187703 100644
--- a/test/units/arbitrators/ast.arbitrator_test.go
+++ b/test/units/arbitrators/ast.arbitrator_test.go
@@ -37,7 +37,12 @@ func (v *testVisitor) VisitStructType(file *ast.File, nodeGenDecl *ast.GenDecl,
return metadata.StructMeta{}, graphs.SymbolKey{}, nil
}
-func (v *testVisitor) VisitField(pkg *packages.Package, file *ast.File, field *ast.Field) ([]metadata.FieldMeta, error) {
+func (v *testVisitor) VisitField(
+ pkg *packages.Package,
+ file *ast.File,
+ field *ast.Field,
+ kind common.SymKind,
+) ([]metadata.FieldMeta, error) {
if v.VisitFieldFunc != nil {
return v.VisitFieldFunc(pkg, file, field)
}
@@ -62,11 +67,11 @@ func (v *testVisitor) VisitField(pkg *packages.Package, file *ast.File, field *a
var _ = Describe("Unit Tests - AST Arbitrator (external)", func() {
var arbProvider *providers.ArbitrationProvider
var astArb *arbitrators.AstArbitrator
- var pkgFacade interface{}
+ var pkgFacade any
BeforeEach(func() {
var err error
- arbProvider, err = providers.NewArbitrationProvider([]string{})
+ arbProvider, err = providers.NewArbitrationProviderFromGleeceConfig(nil)
Expect(err).To(BeNil())
// Use exported provider API to get the arbitrator.
@@ -352,7 +357,7 @@ var _ = Describe("Unit Tests - AST Arbitrator (external)", func() {
}
tp, err := astArb.GetImportType(nil, mp)
Expect(err).To(BeNil())
- Expect(tp).To(Equal(common.ImportTypeAlias))
+ Expect(tp).To(Equal(common.ImportTypeNone))
})
It("Inspects channel element types", func() {
@@ -362,13 +367,6 @@ var _ = Describe("Unit Tests - AST Arbitrator (external)", func() {
Expect(tp).To(Equal(common.ImportTypeNone))
})
- It("Returns an error for unsupported expression kinds", func() {
- ft := &ast.FuncType{}
- tp, err := astArb.GetImportType(nil, ft)
- Expect(tp).To(Equal(common.ImportTypeNone))
- Expect(err).To(MatchError(ContainSubstring("unsupported expression type")))
- })
-
It("Detects dot-imported idents via GetPackageFromDotImportedIdent and returns Dot", func() {
// Create an AST file with a dot import of "fmt"
file := &ast.File{
@@ -444,7 +442,9 @@ var _ = Describe("Unit Tests - AST Arbitrator (external)", func() {
}
ident := &ast.Ident{Name: "Sprintf"}
_, err := astArb.GetPackageFromDotImportedIdent(file, ident)
- Expect(err).To(MatchError(ContainSubstring("encountered 1 errors over 1 packages during load")))
+ Expect(err).To(MatchError(MatchRegexp(
+ `encountered \d+ errors over \d+ package/s \(.+?\) during load -`,
+ )))
})
It("Returns an error when PackagesFacade.GetPackage returns an error", func() {
diff --git a/test/units/cache/gleece.test.config.json b/test/units/cache/gleece.test.config.json
new file mode 100644
index 0000000..1aba1ba
--- /dev/null
+++ b/test/units/cache/gleece.test.config.json
@@ -0,0 +1,54 @@
+{
+ "commonConfig": {
+ "controllerGlobs": [
+ "./resources/micro.valid.controller.go"
+ ]
+ },
+ "routesConfig": {
+ "engine": "gin",
+ "outputPath": "./dist/gleece.go",
+ "outputFilePerms": "0644",
+ "authorizationConfig": {
+ "authFileFullPackageName": "github.com/gopher-fleece/gleece/test/fixtures",
+ "enforceSecurityOnAllRoutes": true
+ }
+ },
+ "openapiGeneratorConfig": {
+ "openapi": "3.0.0",
+ "info": {
+ "title": "Sample API",
+ "description": "This is a sample API",
+ "termsOfService": "http://example.com/terms/",
+ "contact": {
+ "name": "API Support",
+ "url": "http://www.example.com/support",
+ "email": "support@example.com"
+ },
+ "license": {
+ "name": "Apache 2.0",
+ "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
+ },
+ "version": "1.0.0"
+ },
+ "baseUrl": "https://api.example.com",
+ "securitySchemes": [
+ {
+ "description": "API Key for accessing the API",
+ "name": "securitySchemaName",
+ "fieldName": "x-header-name",
+ "type": "apiKey",
+ "in": "header"
+ }
+ ],
+ "defaultSecurity": {
+ "name": "sanitySchema",
+ "scopes": [
+ "read",
+ "write"
+ ]
+ },
+ "specGeneratorConfig": {
+ "outputPath": "./dist/swagger.json"
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/units/cache/metadata.cache_test.go b/test/units/cache/metadata.cache_test.go
index 37a0a30..2ce9430 100644
--- a/test/units/cache/metadata.cache_test.go
+++ b/test/units/cache/metadata.cache_test.go
@@ -7,10 +7,7 @@ import (
"github.com/gopher-fleece/gleece/common"
"github.com/gopher-fleece/gleece/core/arbitrators/caching"
"github.com/gopher-fleece/gleece/core/metadata"
- "github.com/gopher-fleece/gleece/core/visitors"
- "github.com/gopher-fleece/gleece/core/visitors/providers"
"github.com/gopher-fleece/gleece/graphs"
- "github.com/gopher-fleece/gleece/graphs/symboldg"
"github.com/gopher-fleece/gleece/infrastructure/logger"
"github.com/gopher-fleece/gleece/test/utils"
@@ -18,20 +15,10 @@ import (
. "github.com/onsi/gomega"
)
-const controllerFileRelPath = "./resources/micro.valid.controller.go"
-
-type TestCtx struct {
- arbProvider *providers.ArbitrationProvider
- metaCache *caching.MetadataCache
- symGraph symboldg.SymbolGraph
- visitCtx *visitors.VisitContext
- controllerVisitor *visitors.ControllerVisitor
-}
-
var _ = Describe("MetadataCache", func() {
- var ctx TestCtx
+ var ctx utils.StdTestCtx
BeforeEach(func() {
- ctx = createTestCtx([]string{controllerFileRelPath})
+ ctx = utils.CreateStdTestCtx("gleece.test.config.json")
})
Context("NewMetadataCache", func() {
@@ -57,14 +44,16 @@ var _ = Describe("MetadataCache", func() {
})
It("Returns controller when it exists in the cache", func() {
- for _, file := range ctx.controllerVisitor.GetAllSourceFiles() {
- ast.Walk(ctx.controllerVisitor, file)
+ for _, file := range ctx.Orc.GetAllSourceFiles() {
+ ast.Walk(ctx.Orc, file)
}
- controllers := ctx.controllerVisitor.GetControllers()
+ controllers := ctx.VisitCtx.Graph.FindByKind(common.SymKindController)
Expect(controllers).To(HaveLen(1))
- hasController := ctx.metaCache.HasController(&controllers[0])
+ meta := controllers[0].Data.(metadata.ControllerMeta)
+
+ hasController := ctx.VisitCtx.MetadataCache.HasController(&meta)
Expect(hasController).To(BeTrue())
})
})
@@ -80,15 +69,15 @@ var _ = Describe("MetadataCache", func() {
})
It("Returns receiver when it exists in the cache", func() {
- for _, file := range ctx.controllerVisitor.GetAllSourceFiles() {
- ast.Walk(ctx.controllerVisitor, file)
+ for _, file := range ctx.Orc.GetAllSourceFiles() {
+ ast.Walk(ctx.Orc, file)
}
- structType := ctx.visitCtx.GraphBuilder.FindByKind(common.SymKindStruct)
+ structType := ctx.VisitCtx.Graph.FindByKind(common.SymKindStruct)
Expect(structType).To(HaveLen(1))
- hasRoute := ctx.metaCache.HasStruct(structType[0].Id)
- Expect(hasRoute).To(BeTrue())
+ hasStruct := ctx.VisitCtx.MetadataCache.HasStruct(structType[0].Id)
+ Expect(hasStruct).To(BeTrue())
})
})
@@ -103,14 +92,14 @@ var _ = Describe("MetadataCache", func() {
})
It("Returns struct when it exists in the cache", func() {
- for _, file := range ctx.controllerVisitor.GetAllSourceFiles() {
- ast.Walk(ctx.controllerVisitor, file)
+ for _, file := range ctx.Orc.GetAllSourceFiles() {
+ ast.Walk(ctx.Orc, file)
}
- receivers := ctx.visitCtx.GraphBuilder.FindByKind(common.SymKindStruct)
+ receivers := ctx.VisitCtx.Graph.FindByKind(common.SymKindStruct)
Expect(receivers).To(HaveLen(1))
- hasRoute := ctx.metaCache.HasStruct(receivers[0].Id)
+ hasRoute := ctx.VisitCtx.MetadataCache.HasStruct(receivers[0].Id)
Expect(hasRoute).To(BeTrue())
})
})
@@ -126,21 +115,21 @@ var _ = Describe("MetadataCache", func() {
})
It("Returns enum when it exists in the cache", func() {
- for _, file := range ctx.controllerVisitor.GetAllSourceFiles() {
- ast.Walk(ctx.controllerVisitor, file)
+ for _, file := range ctx.Orc.GetAllSourceFiles() {
+ ast.Walk(ctx.Orc, file)
}
- enums := ctx.visitCtx.GraphBuilder.FindByKind(common.SymKindEnum)
+ enums := ctx.VisitCtx.Graph.FindByKind(common.SymKindEnum)
Expect(enums).To(HaveLen(1))
- hasEnum := ctx.metaCache.HasEnum(enums[0].Id)
+ hasEnum := ctx.VisitCtx.MetadataCache.HasEnum(enums[0].Id)
Expect(hasEnum).To(BeTrue())
})
})
Context("GetFileVersion", func() {
It("Returns an error when unable to construct a FileVersion", func() {
- _, err := ctx.metaCache.GetFileVersion(&ast.File{}, nil)
+ _, err := ctx.VisitCtx.MetadataCache.GetFileVersion(&ast.File{}, nil)
Expect(err).To(MatchError(ContainSubstring("GetFileFullPath was provided nil file or fileSet")))
})
})
@@ -176,41 +165,15 @@ var _ = Describe("MetadataCache", func() {
},
}
- err := ctx.metaCache.AddEnum(&enumMeta)
+ err := ctx.VisitCtx.MetadataCache.AddEnum(&enumMeta)
Expect(err).To(BeNil())
- err = ctx.metaCache.AddEnum(&enumMeta)
+ err = ctx.VisitCtx.MetadataCache.AddEnum(&enumMeta)
Expect(err).To(MatchError(ContainSubstring("already exists in cache")))
})
})
})
-func createTestCtx(fileGlobs []string) TestCtx {
- ctx := TestCtx{}
-
- // Pass the real controller file so the providers actually load it
- arbProvider, err := providers.NewArbitrationProvider(fileGlobs)
- Expect(err).To(BeNil())
- ctx.arbProvider = arbProvider
-
- // Verify files were properly loaded
- srcFiles := arbProvider.Pkg().GetAllSourceFiles()
- Expect(srcFiles).ToNot(BeEmpty(), "Arbitration provider parsed zero files; check glob and file contents")
-
- // Build the VisitContext and routeVisitor as before using arbProvider
- ctx.metaCache = caching.NewMetadataCache()
- ctx.symGraph = symboldg.NewSymbolGraph()
- ctx.visitCtx = &visitors.VisitContext{
- ArbitrationProvider: arbProvider,
- MetadataCache: ctx.metaCache,
- GraphBuilder: &ctx.symGraph,
- }
-
- ctx.controllerVisitor, err = visitors.NewControllerVisitor(ctx.visitCtx)
- Expect(err).To(BeNil(), "Failed to construct a new controller visitor")
- return ctx
-}
-
func TestMetadataCacheVisitor(t *testing.T) {
logger.SetLogLevel(logger.LogLevelNone)
RegisterFailHandler(Fail)
diff --git a/test/units/commons/commons_test.go b/test/units/commons/commons_test.go
index 81d73b3..f950507 100644
--- a/test/units/commons/commons_test.go
+++ b/test/units/commons/commons_test.go
@@ -218,7 +218,7 @@ var _ = Describe("Unit Tests - Commons", func() {
multiLine := "first line\nsecond line\nthird line"
err := &common.ContextualError{
Context: "multi",
- Errors: []error{fmt.Errorf(multiLine)},
+ Errors: []error{fmt.Errorf("%s", multiLine)},
}
msg := err.Error()
Expect(msg).To(ContainSubstring("- first line"))
diff --git a/test/units/graphs/dot/dot_test.go b/test/units/graphs/dot/dot_test.go
index 9b95f47..9314f64 100644
--- a/test/units/graphs/dot/dot_test.go
+++ b/test/units/graphs/dot/dot_test.go
@@ -7,6 +7,7 @@ import (
"github.com/gopher-fleece/gleece/common"
"github.com/gopher-fleece/gleece/graphs"
"github.com/gopher-fleece/gleece/graphs/dot"
+ "github.com/gopher-fleece/gleece/graphs/symboldg"
"github.com/gopher-fleece/gleece/infrastructure/logger"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
@@ -99,7 +100,7 @@ var _ = Describe("Unit Tests - Dot", func() {
builder.AddNode(from, common.SymKindStruct, "From")
builder.AddNode(to, common.SymKindInterface, "To")
- builder.AddEdge(from, to, "calls")
+ builder.AddEdge(from, to, "calls", nil)
out := builder.Finish()
Expect(out).To(ContainSubstring("N0 -> N1"))
@@ -114,7 +115,7 @@ var _ = Describe("Unit Tests - Dot", func() {
db.AddNode(from, common.SymKindStruct, "From")
db.AddNode(to, common.SymKindStruct, "To")
- db.AddEdge(from, to, "ty") // edge label and style present in theme
+ db.AddEdge(from, to, "ty", nil) // edge label and style present in theme
output := db.Finish()
Expect(output).To(ContainSubstring("label=\"Type\""))
Expect(output).To(ContainSubstring("color=\"black\""))
@@ -128,13 +129,27 @@ var _ = Describe("Unit Tests - Dot", func() {
db.AddNode(from, common.SymKindStruct, "From2")
unknownTo := graphs.SymbolKey{Name: "unknown_to"}
- db.AddEdge(from, unknownTo, "unknown_kind")
+ db.AddEdge(from, unknownTo, "unknown_kind", nil)
output := db.Finish()
// Should add error node and one edge to error node
Expect(strings.Count(output, "N_ERROR")).To(Equal(2))
Expect(strings.Count(output, "-> N_ERROR")).To(Equal(1))
})
+
+ It("Appends a given edge suffix string if provided", func() {
+ from := graphs.SymbolKey{Name: "FromNode"}
+ to := graphs.SymbolKey{Name: "ToNode"}
+
+ builder.AddNode(from, common.SymKindStruct, "From")
+ builder.AddNode(to, common.SymKindInterface, "To")
+
+ builder.AddEdge(from, to, string(symboldg.EdgeKindType), common.Ptr(" (Suffix Test)"))
+
+ out := builder.Finish()
+
+ Expect(out).To(ContainSubstring("N0 -> N1 [label=\"Type (Suffix Test)\""))
+ })
})
Describe("addErrorNodeOnce", func() {
@@ -151,7 +166,7 @@ var _ = Describe("Unit Tests - Dot", func() {
to := graphs.SymbolKey{Name: "to_unknown"}
db.AddNode(from, common.SymKindStruct, "From")
- db.AddEdge(from, to, "edgekind")
+ db.AddEdge(from, to, "edgekind", nil)
output := db.Finish()
Expect(output).To(ContainSubstring("fillcolor=\"red\""))
@@ -170,12 +185,12 @@ var _ = Describe("Unit Tests - Dot", func() {
// If it's not, it'll appear twice for each edge.
db.AddNode(from, common.SymKindStruct, "From")
- db.AddEdge(from, to, "edgekind")
+ db.AddEdge(from, to, "edgekind", nil)
before := db.Finish()
// Add another edge to unknown node, should not duplicate error node
to2 := graphs.SymbolKey{Name: "unknown2"}
- db.AddEdge(from, to2, "edgekind")
+ db.AddEdge(from, to2, "edgekind", nil)
after := db.Finish()
Expect(strings.Count(before, "N_ERROR")).To(Equal(2))
@@ -184,7 +199,16 @@ var _ = Describe("Unit Tests - Dot", func() {
})
Describe("RenderLegend", func() {
+ It("Does not render legend for an empty graph", func() {
+ builder.RenderLegend()
+
+ out := builder.Finish()
+ Expect(out).ToNot(ContainSubstring("subgraph cluster_legend"))
+ Expect(out).ToNot(ContainSubstring("label = \"Legend\""))
+ })
+
It("Renders legend with expected content", func() {
+ builder.AddNode(graphs.NewUniverseSymbolKey("string"), common.SymKindBuiltin, "Test")
builder.RenderLegend()
out := builder.Finish()
@@ -197,17 +221,17 @@ var _ = Describe("Unit Tests - Dot", func() {
It("Renders legend with default fallback color and shape", func() {
theme := dot.DotTheme{
NodeStyles: map[common.SymKind]dot.DotStyle{
- common.SymKindStruct: {Color: "", Shape: ""},
- common.SymKindField: {Color: "", Shape: "circle"},
+ common.SymKindBuiltin: {Color: "", Shape: ""},
},
}
db := dot.NewDotBuilder(&theme)
+ // Add a node to trigger legend render
+ db.AddNode(graphs.NewUniverseSymbolKey("string"), common.SymKindBuiltin, "Test")
db.RenderLegend()
output := db.Finish()
Expect(output).To(ContainSubstring("fillcolor=\"gray90\""))
- Expect(output).To(ContainSubstring("shape=ellipse")) // for empty shape
- Expect(output).To(ContainSubstring("shape=circle")) // for set shape
+ Expect(output).To(ContainSubstring("shape=ellipse"))
})
})
})
diff --git a/test/units/graphs/graph_enumerations_test.go b/test/units/graphs/graph_enumerations_test.go
index 6b8cb97..1ac74b1 100644
--- a/test/units/graphs/graph_enumerations_test.go
+++ b/test/units/graphs/graph_enumerations_test.go
@@ -1,7 +1,7 @@
package graphs_test
import (
- "github.com/gopher-fleece/gleece/graphs/symboldg"
+ "github.com/gopher-fleece/gleece/common"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
@@ -9,83 +9,83 @@ import (
var _ = Describe("Unit Tests - SymbolGraph", func() {
Context("ToPrimitiveType", func() {
It("Returns true for a valid primitive type", func() {
- t, ok := symboldg.ToPrimitiveType("int64")
+ t, ok := common.ToPrimitiveType("int64")
Expect(ok).To(BeTrue())
- Expect(t).To(Equal(symboldg.PrimitiveTypeInt64))
+ Expect(t).To(Equal(common.PrimitiveTypeInt64))
})
It("Returns true for an alias type like byte", func() {
- t, ok := symboldg.ToPrimitiveType("byte")
+ t, ok := common.ToPrimitiveType("byte")
Expect(ok).To(BeTrue())
- Expect(t).To(Equal(symboldg.PrimitiveTypeByte))
+ Expect(t).To(Equal(common.PrimitiveTypeByte))
})
It("Returns false for an unknown type", func() {
- t, ok := symboldg.ToPrimitiveType("notatype")
+ t, ok := common.ToPrimitiveType("notatype")
Expect(ok).To(BeFalse())
- Expect(t).To(Equal(symboldg.PrimitiveType("")))
+ Expect(t).To(Equal(common.PrimitiveType("")))
})
})
Context("SpecialType.IsUniverse", func() {
It("Returns true for error", func() {
- Expect(symboldg.SpecialTypeError.IsUniverse()).To(BeTrue())
+ Expect(common.SpecialTypeError.IsUniverse()).To(BeTrue())
})
It("Returns true for interface{}", func() {
- Expect(symboldg.SpecialTypeEmptyInterface.IsUniverse()).To(BeTrue())
+ Expect(common.SpecialTypeEmptyInterface.IsUniverse()).To(BeTrue())
})
It("Returns true for any", func() {
- Expect(symboldg.SpecialTypeAny.IsUniverse()).To(BeTrue())
+ Expect(common.SpecialTypeAny.IsUniverse()).To(BeTrue())
})
It("Returns false for non-universe type", func() {
- Expect(symboldg.SpecialTypeTime.IsUniverse()).To(BeFalse())
+ Expect(common.SpecialTypeTime.IsUniverse()).To(BeFalse())
})
})
Context("ToSpecialType", func() {
It("Returns true for error", func() {
- t, ok := symboldg.ToSpecialType("error")
+ t, ok := common.ToSpecialType("error")
Expect(ok).To(BeTrue())
- Expect(t).To(Equal(symboldg.SpecialTypeError))
+ Expect(t).To(Equal(common.SpecialTypeError))
})
It("Returns true for interface{}", func() {
- t, ok := symboldg.ToSpecialType("interface{}")
+ t, ok := common.ToSpecialType("interface{}")
Expect(ok).To(BeTrue())
- Expect(t).To(Equal(symboldg.SpecialTypeEmptyInterface))
+ Expect(t).To(Equal(common.SpecialTypeEmptyInterface))
})
It("Returns true for any", func() {
- t, ok := symboldg.ToSpecialType("any")
+ t, ok := common.ToSpecialType("any")
Expect(ok).To(BeTrue())
- Expect(t).To(Equal(symboldg.SpecialTypeAny))
+ Expect(t).To(Equal(common.SpecialTypeAny))
})
It("Returns true for context.Context", func() {
- t, ok := symboldg.ToSpecialType("context.Context")
+ t, ok := common.ToSpecialType("context.Context")
Expect(ok).To(BeTrue())
- Expect(t).To(Equal(symboldg.SpecialTypeContext))
+ Expect(t).To(Equal(common.SpecialTypeContext))
})
It("Returns true for time.Time", func() {
- t, ok := symboldg.ToSpecialType("time.Time")
+ t, ok := common.ToSpecialType("time.Time")
Expect(ok).To(BeTrue())
- Expect(t).To(Equal(symboldg.SpecialTypeTime))
+ Expect(t).To(Equal(common.SpecialTypeTime))
})
It("Returns true for unsafe.Pointer", func() {
- t, ok := symboldg.ToSpecialType("unsafe.Pointer")
+ t, ok := common.ToSpecialType("unsafe.Pointer")
Expect(ok).To(BeTrue())
- Expect(t).To(Equal(symboldg.SpecialTypeUnsafePointer))
+ Expect(t).To(Equal(common.SpecialTypeUnsafePointer))
})
It("Returns false for an unknown special type", func() {
- t, ok := symboldg.ToSpecialType("does.NotExist")
+ t, ok := common.ToSpecialType("does.NotExist")
Expect(ok).To(BeFalse())
- Expect(t).To(Equal(symboldg.SpecialType("")))
+ Expect(t).To(Equal(common.SpecialType("")))
})
})
diff --git a/test/units/graphs/symbol/graph_dumping_test.go b/test/units/graphs/symbol/graph_dumping_test.go
index 84fc96f..37df155 100644
--- a/test/units/graphs/symbol/graph_dumping_test.go
+++ b/test/units/graphs/symbol/graph_dumping_test.go
@@ -37,9 +37,6 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
Value: "Some Value",
Type: metadata.TypeUsageMeta{
SymNodeMeta: metadata.SymNodeMeta{Name: "string", FVersion: fVersion},
- Layers: []metadata.TypeLayer{
- metadata.NewBaseLayer(common.Ptr(graphs.NewUniverseSymbolKey("string"))),
- },
},
},
})
@@ -70,15 +67,12 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
Value: "Some Value",
Type: metadata.TypeUsageMeta{
SymNodeMeta: metadata.SymNodeMeta{Name: "string", FVersion: fVersion},
- Layers: []metadata.TypeLayer{
- metadata.NewBaseLayer(common.Ptr(graphs.NewUniverseSymbolKey("string"))),
- },
},
},
})
Expect(err).To(BeNil())
- strNode := graph.AddPrimitive(symboldg.PrimitiveTypeString)
+ strNode := graph.AddPrimitive(common.PrimitiveTypeString)
graph.AddEdge(constNode.Id, strNode.Id, symboldg.EdgeKindType, nil)
text := graph.String()
@@ -117,9 +111,6 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
Value: "Some Value",
Type: metadata.TypeUsageMeta{
SymNodeMeta: metadata.SymNodeMeta{Name: "string", FVersion: fVersion},
- Layers: []metadata.TypeLayer{
- metadata.NewBaseLayer(common.Ptr(graphs.NewUniverseSymbolKey("string"))),
- },
},
},
})
@@ -149,36 +140,13 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
It("Outputs correct empty graph with default style when empty", func() {
text := graph.ToDot(nil)
- const expectedDotGraph = "digraph SymbolGraph {\n" +
- " rankdir=TB;\n" +
- " subgraph cluster_legend {\n" +
- " label = \"Legend\";\n" +
- " style = dashed;\n" +
- " L0 [label=\"Alias\", style=filled, shape=note, fillcolor=\"palegreen\"];\n" +
- " L1 [label=\"Builtin\", style=filled, shape=box, fillcolor=\"gray80\"];\n" +
- " L2 [label=\"Constant\", style=filled, shape=egg, fillcolor=\"plum\"];\n" +
- " L3 [label=\"Controller\", style=filled, shape=octagon, fillcolor=\"lightcyan\"];\n" +
- " L4 [label=\"Enum\", style=filled, shape=folder, fillcolor=\"mediumpurple\"];\n" +
- " L5 [label=\"EnumValue\", style=filled, shape=note, fillcolor=\"plum\"];\n" +
- " L6 [label=\"Field\", style=filled, shape=ellipse, fillcolor=\"gold\"];\n" +
- " L7 [label=\"Function\", style=filled, shape=oval, fillcolor=\"darkseagreen\"];\n" +
- " L8 [label=\"Interface\", style=filled, shape=component, fillcolor=\"lightskyblue\"];\n" +
- " L9 [label=\"Package\", style=filled, shape=folder, fillcolor=\"lightyellow\"];\n" +
- " L10 [label=\"Parameter\", style=filled, shape=parallelogram, fillcolor=\"khaki\"];\n" +
- " L11 [label=\"Receiver\", style=filled, shape=hexagon, fillcolor=\"orange\"];\n" +
- " L12 [label=\"RetType\", style=filled, shape=diamond, fillcolor=\"lightgrey\"];\n" +
- " L13 [label=\"Struct\", style=filled, shape=box, fillcolor=\"lightblue\"];\n" +
- " L14 [label=\"Unknown\", style=filled, shape=triangle, fillcolor=\"lightcoral\"];\n" +
- " L15 [label=\"Variable\", style=filled, shape=circle, fillcolor=\"lightsteelblue\"];\n" +
- " }\n" +
- "}\n"
-
- Expect(text).To(Equal(expectedDotGraph))
+ // Graph renders legend only if not empty
+ Expect(text).To(Equal("digraph SymbolGraph {\n rankdir=TB;\n}\n"))
})
It("Outputs nodes and their edges", func() {
- anyNode := graph.AddSpecial(symboldg.SpecialTypeAny)
- strNode := graph.AddPrimitive(symboldg.PrimitiveTypeString)
+ anyNode := graph.AddSpecial(common.SpecialTypeAny)
+ strNode := graph.AddPrimitive(common.PrimitiveTypeString)
graph.AddEdge(anyNode.Id, strNode.Id, symboldg.EdgeKindType, nil)
text := graph.ToDot(nil)
diff --git a/test/units/graphs/symbol/node_crud_test.go b/test/units/graphs/symbol/node_crud_test.go
index 0dc07a3..badacb2 100644
--- a/test/units/graphs/symbol/node_crud_test.go
+++ b/test/units/graphs/symbol/node_crud_test.go
@@ -22,14 +22,20 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
Context("AddController", func() {
It("Adds a controller node successfully", func() {
- controllerMeta := metadata.StructMeta{
+ structMeta := metadata.StructMeta{
SymNodeMeta: metadata.SymNodeMeta{
Node: utils.MakeIdent("MyController"),
FVersion: fVersion,
},
}
+
+ data := metadata.ControllerMeta{
+ Struct: structMeta,
+ Receivers: []metadata.ReceiverMeta{},
+ }
+
request := symboldg.CreateControllerNode{
- Data: controllerMeta,
+ Data: data,
Annotations: nil,
}
@@ -37,19 +43,22 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
Expect(err).ToNot(HaveOccurred())
Expect(node).ToNot(BeNil())
Expect(node.Kind).To(Equal(common.SymKindController))
- Expect(node.Data).To(Equal(controllerMeta))
+ Expect(node.Data).To(Equal(data))
})
It("Returns an error when createAndAddSymNode returns an error", func() {
// Pass a request with nil node to cause idempotencyGuard error
- controllerMeta := metadata.StructMeta{
+ structMeta := metadata.StructMeta{
SymNodeMeta: metadata.SymNodeMeta{
Node: nil,
FVersion: fVersion,
},
}
request := symboldg.CreateControllerNode{
- Data: controllerMeta,
+ Data: metadata.ControllerMeta{
+ Struct: structMeta,
+ Receivers: []metadata.ReceiverMeta{},
+ },
Annotations: nil,
}
@@ -93,7 +102,7 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
&symboldg.SymbolNode{Id: ctrlMeta.SymbolKey()},
&symboldg.TraversalBehavior{
Filtering: symboldg.TraversalFilter{
- EdgeKind: common.Ptr(symboldg.EdgeKindReceiver),
+ EdgeKinds: []symboldg.SymbolEdgeKind{symboldg.EdgeKindReceiver},
},
})
Expect(children).To(HaveLen(1))
@@ -143,7 +152,9 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
},
},
}
- _, err := graph.AddController(symboldg.CreateControllerNode{Data: controllerMeta.Struct})
+ _, err := graph.AddController(symboldg.CreateControllerNode{
+ Data: *controllerMeta,
+ })
Expect(err).ToNot(HaveOccurred())
// create a route (parent for params)
@@ -180,9 +191,6 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
Node: nil,
FVersion: fVersion,
},
- Layers: []metadata.TypeLayer{
- metadata.NewBaseLayer(common.Ptr(graphs.NewUniverseSymbolKey("int"))),
- },
},
}
@@ -207,7 +215,7 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
// Route should have the param as a child via EdgeKindParam
children := graph.Children(routeNode, &symboldg.TraversalBehavior{
Filtering: symboldg.TraversalFilter{
- EdgeKind: common.Ptr(symboldg.EdgeKindParam),
+ EdgeKinds: []symboldg.SymbolEdgeKind{symboldg.EdgeKindParam},
},
})
Expect(children).To(HaveLen(1))
@@ -259,7 +267,7 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
Receivers: nil,
}
_, err := graph.AddController(symboldg.CreateControllerNode{
- Data: controllerMeta.Struct,
+ Data: *controllerMeta,
})
Expect(err).ToNot(HaveOccurred())
@@ -297,9 +305,6 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
Node: nil,
FVersion: fVersion,
},
- Layers: []metadata.TypeLayer{
- metadata.NewBaseLayer(common.Ptr(graphs.NewUniverseSymbolKey("string"))),
- },
},
}
@@ -320,7 +325,7 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
children := graph.Children(routeNode, &symboldg.TraversalBehavior{
Filtering: symboldg.TraversalFilter{
- EdgeKind: common.Ptr(symboldg.EdgeKindRetVal),
+ EdgeKinds: []symboldg.SymbolEdgeKind{symboldg.EdgeKindRetVal},
},
})
Expect(children).To(HaveLen(1))
@@ -360,9 +365,6 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
SymNodeMeta: metadata.SymNodeMeta{Node: utils.MakeIdent("FieldA"), FVersion: fVersion},
Type: metadata.TypeUsageMeta{
SymNodeMeta: metadata.SymNodeMeta{Name: "int", FVersion: fVersion},
- Layers: []metadata.TypeLayer{
- metadata.NewBaseLayer(common.Ptr(graphs.NewUniverseSymbolKey("int"))),
- },
},
}
@@ -441,7 +443,7 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
// Enum should have Value edges to its constants
valueChildren := graph.Children(enumNode, &symboldg.TraversalBehavior{
Filtering: symboldg.TraversalFilter{
- EdgeKind: common.Ptr(symboldg.EdgeKindValue),
+ EdgeKinds: []symboldg.SymbolEdgeKind{symboldg.EdgeKindValue},
},
})
Expect(valueChildren).To(HaveLen(len(enumMeta.Values)))
@@ -451,7 +453,7 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
for _, valueChild := range valueChildren {
referenceEdges := graph.Children(valueChild, &symboldg.TraversalBehavior{
Filtering: symboldg.TraversalFilter{
- EdgeKind: common.Ptr(symboldg.EdgeKindReference),
+ EdgeKinds: []symboldg.SymbolEdgeKind{symboldg.EdgeKindReference},
},
})
Expect(referenceEdges).To(HaveLen(1))
@@ -535,10 +537,8 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
Node: nil,
FVersion: fVersion,
},
+ Root: utils.MakeUniverseRoot("float32"),
Import: common.ImportTypeNone,
- Layers: []metadata.TypeLayer{
- metadata.NewBaseLayer(&baseTypeKey),
- },
},
IsEmbedded: false,
}
@@ -549,15 +549,16 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
Data: fieldMeta,
Annotations: nil,
})
+
Expect(err).ToNot(HaveOccurred())
Expect(fieldNode).ToNot(BeNil())
// Add the type node so links actually exist
- graph.AddPrimitive(symboldg.PrimitiveTypeFloat32)
+ graph.AddPrimitive(common.PrimitiveTypeFloat32)
children := graph.Children(fieldNode, &symboldg.TraversalBehavior{
Filtering: symboldg.TraversalFilter{
- EdgeKind: common.Ptr(symboldg.EdgeKindType),
+ EdgeKinds: []symboldg.SymbolEdgeKind{symboldg.EdgeKindType},
},
})
Expect(children).To(HaveLen(1))
@@ -578,13 +579,14 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
It("Returns an error when getTypeRef fails due to missing base type", func() {
badMeta := fieldMeta
- badMeta.Type.Layers = nil // no base layer → getTypeRef error
+ badMeta.Type.Root = nil
fieldNode, err := graph.AddField(symboldg.CreateFieldNode{
Data: badMeta,
Annotations: nil,
})
- Expect(err).To(HaveOccurred())
+
+ Expect(err).To(MatchError(ContainSubstring("missing Root TypeRef")))
Expect(fieldNode).To(BeNil())
})
})
@@ -601,9 +603,6 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
Value: "some value",
Type: metadata.TypeUsageMeta{
SymNodeMeta: metadata.SymNodeMeta{Name: "string", FVersion: fVersion},
- Layers: []metadata.TypeLayer{
- metadata.NewBaseLayer(common.Ptr(graphs.NewUniverseSymbolKey("string"))),
- },
},
},
})
@@ -618,14 +617,14 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
Context("AddPrimitive", func() {
It("Adds a primitive and returns a non-nil node", func() {
graph := symboldg.NewSymbolGraph()
- node := graph.AddPrimitive(symboldg.PrimitiveTypeBool)
+ node := graph.AddPrimitive(common.PrimitiveTypeBool)
Expect(node).ToNot(BeNil())
})
It("Returns the same node when adding a duplicate primitive", func() {
graph := symboldg.NewSymbolGraph()
- n1 := graph.AddPrimitive(symboldg.PrimitiveTypeBool)
- n2 := graph.AddPrimitive(symboldg.PrimitiveTypeBool)
+ n1 := graph.AddPrimitive(common.PrimitiveTypeBool)
+ n2 := graph.AddPrimitive(common.PrimitiveTypeBool)
Expect(n2).To(Equal(n1))
})
})
@@ -633,7 +632,7 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
Context("AddSpecial", func() {
It("Adds a special type and returns a non-nil node", func() {
graph := symboldg.NewSymbolGraph()
- node := graph.AddSpecial(symboldg.SpecialTypeError)
+ node := graph.AddSpecial(common.SpecialTypeError)
Expect(node).ToNot(BeNil())
})
})
@@ -691,10 +690,13 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
// dependent -> other (so dependent should NOT be orphaned when target removed)
target, err := graph.AddField(symboldg.CreateFieldNode{
Data: metadata.FieldMeta{
- SymNodeMeta: metadata.SymNodeMeta{Node: utils.MakeIdent("TargetField"), FVersion: fVersion},
+ SymNodeMeta: metadata.SymNodeMeta{
+ Node: utils.MakeIdent("TargetField"),
+ FVersion: fVersion,
+ },
Type: metadata.TypeUsageMeta{
SymNodeMeta: metadata.SymNodeMeta{FVersion: fVersion},
- Layers: []metadata.TypeLayer{metadata.NewBaseLayer(common.Ptr(graphs.NewUniverseSymbolKey("int")))},
+ Root: utils.MakeUniverseRoot("float32"),
},
},
})
@@ -707,7 +709,7 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
})
Expect(err).ToNot(HaveOccurred())
- other := graph.AddPrimitive(symboldg.PrimitiveTypeBool) // another node for dep to point to
+ other := graph.AddPrimitive(common.PrimitiveTypeBool) // another node for dep to point to
Expect(other).ToNot(BeNil())
// Add dependent->target and dependent->other
@@ -741,7 +743,7 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
SymNodeMeta: metadata.SymNodeMeta{Node: utils.MakeIdent("A_field"), FVersion: fVersion},
Type: metadata.TypeUsageMeta{
SymNodeMeta: metadata.SymNodeMeta{FVersion: fVersion},
- Layers: []metadata.TypeLayer{metadata.NewBaseLayer(common.Ptr(graphs.NewUniverseSymbolKey("int")))},
+ Root: utils.MakeUniverseRoot("float32"),
},
},
})
@@ -816,7 +818,7 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
SymNodeMeta: metadata.SymNodeMeta{Node: utils.MakeIdent("CleanupChild"), FVersion: fVersion},
Type: metadata.TypeUsageMeta{
SymNodeMeta: metadata.SymNodeMeta{FVersion: fVersion},
- Layers: []metadata.TypeLayer{metadata.NewBaseLayer(common.Ptr(graphs.NewUniverseSymbolKey("int")))},
+ Root: utils.MakeUniverseRoot("float32"),
},
},
})
@@ -829,7 +831,7 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
})
Expect(err).ToNot(HaveOccurred())
- p := graph.AddPrimitive(symboldg.PrimitiveTypeInt)
+ p := graph.AddPrimitive(common.PrimitiveTypeInt)
Expect(p).ToNot(BeNil())
// Parent -> Child and Parent -> p
diff --git a/test/units/graphs/symbol/node_lookups_test.go b/test/units/graphs/symbol/node_lookups_test.go
index 3c9496e..afb10a4 100644
--- a/test/units/graphs/symbol/node_lookups_test.go
+++ b/test/units/graphs/symbol/node_lookups_test.go
@@ -17,13 +17,13 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
Context("FindByKind", func() {
It("Correctly finds elements by the symbol kind", func() {
// Add a couple of relevant nodes
- anyNode := graph.AddSpecial(symboldg.SpecialTypeAny)
- errNode := graph.AddSpecial(symboldg.SpecialTypeError)
+ anyNode := graph.AddSpecial(common.SpecialTypeAny)
+ errNode := graph.AddSpecial(common.SpecialTypeError)
// Add a couple unrelated nodes that should be ignored
- graph.AddPrimitive(symboldg.PrimitiveTypeBool)
- graph.AddPrimitive(symboldg.PrimitiveTypeString)
+ graph.AddPrimitive(common.PrimitiveTypeBool)
+ graph.AddPrimitive(common.PrimitiveTypeString)
results := graph.FindByKind(common.SymKindSpecialBuiltin)
@@ -35,26 +35,26 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
Context("IsPrimitivePresent", func() {
It("Recognizes that a previously added primitive is present", func() {
graph := symboldg.NewSymbolGraph()
- graph.AddPrimitive(symboldg.PrimitiveTypeBool)
- Expect(graph.IsPrimitivePresent(symboldg.PrimitiveTypeBool)).To(BeTrue())
+ graph.AddPrimitive(common.PrimitiveTypeBool)
+ Expect(graph.IsPrimitivePresent(common.PrimitiveTypeBool)).To(BeTrue())
})
It("Returns false for primitives that have not been added", func() {
graph := symboldg.NewSymbolGraph()
- Expect(graph.IsPrimitivePresent(symboldg.PrimitiveTypeInt)).To(BeFalse())
+ Expect(graph.IsPrimitivePresent(common.PrimitiveTypeInt)).To(BeFalse())
})
})
Context("IsSpecialPresent", func() {
It("Recognizes a previously added special type", func() {
graph := symboldg.NewSymbolGraph()
- graph.AddSpecial(symboldg.SpecialTypeError)
- Expect(graph.IsSpecialPresent(symboldg.SpecialTypeError)).To(BeTrue())
+ graph.AddSpecial(common.SpecialTypeError)
+ Expect(graph.IsSpecialPresent(common.SpecialTypeError)).To(BeTrue())
})
It("Returns false for special types not added", func() {
graph := symboldg.NewSymbolGraph()
- Expect(graph.IsSpecialPresent(symboldg.SpecialTypeTime)).To(BeFalse())
+ Expect(graph.IsSpecialPresent(common.SpecialTypeTime)).To(BeFalse())
})
})
})
diff --git a/test/units/graphs/symbol/node_relations_test.go b/test/units/graphs/symbol/node_relations_test.go
index 56e3dd8..bca950d 100644
--- a/test/units/graphs/symbol/node_relations_test.go
+++ b/test/units/graphs/symbol/node_relations_test.go
@@ -40,7 +40,7 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
Expect(structNode).ToNot(BeNil())
// Create a builtin/type node
- typeNode = graph.AddPrimitive(symboldg.PrimitiveTypeInt)
+ typeNode = graph.AddPrimitive(common.PrimitiveTypeInt)
Expect(typeNode).ToNot(BeNil())
})
@@ -73,12 +73,14 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
structNode, _ = graph.AddStruct(symboldg.CreateStructNode{Data: structMeta})
fieldMeta := metadata.FieldMeta{
- SymNodeMeta: metadata.SymNodeMeta{Node: utils.MakeIdent("Child"), FVersion: fVersion},
+ SymNodeMeta: metadata.SymNodeMeta{
+ Node: utils.MakeIdent("Child"),
+ FVersion: fVersion,
+ SymbolKind: common.SymKindField,
+ },
Type: metadata.TypeUsageMeta{
SymNodeMeta: metadata.SymNodeMeta{Name: "int", FVersion: fVersion},
- Layers: []metadata.TypeLayer{
- metadata.NewBaseLayer(common.Ptr(graphs.NewUniverseSymbolKey("int"))),
- },
+ Root: utils.MakeUniverseRoot("int"),
},
}
fieldNode, _ = graph.AddField(symboldg.CreateFieldNode{Data: fieldMeta})
@@ -90,7 +92,7 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
It("Returns only children matching the filter", func() {
filter := &symboldg.TraversalBehavior{
Filtering: symboldg.TraversalFilter{
- EdgeKind: common.Ptr(symboldg.EdgeKindField),
+ EdgeKinds: []symboldg.SymbolEdgeKind{symboldg.EdgeKindField},
},
}
result := graph.Children(structNode, filter)
@@ -101,7 +103,7 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
It("Skips edges that do not match a given filter", func() {
filter := &symboldg.TraversalBehavior{
Filtering: symboldg.TraversalFilter{
- EdgeKind: common.Ptr(symboldg.EdgeKindCall), // This should drop the edge
+ EdgeKinds: []symboldg.SymbolEdgeKind{symboldg.EdgeKindCall}, // This should drop the edge
},
}
result := graph.Children(structNode, filter)
@@ -122,9 +124,7 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
SymNodeMeta: metadata.SymNodeMeta{Node: utils.MakeIdent("AnotherChild"), FVersion: fVersion},
Type: metadata.TypeUsageMeta{
SymNodeMeta: metadata.SymNodeMeta{Name: "int", FVersion: fVersion},
- Layers: []metadata.TypeLayer{
- metadata.NewBaseLayer(common.Ptr(graphs.NewUniverseSymbolKey("int"))),
- },
+ Root: utils.MakeUniverseRoot("int"),
},
}
fieldNode, _ = graph.AddField(symboldg.CreateFieldNode{Data: fieldMeta})
@@ -144,13 +144,13 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
BeforeEach(func() {
// The graph itself doesn't verify semantics so we can use this un-real linkage
// to test filtering logic
- primNode := graph.AddPrimitive(symboldg.PrimitiveTypeString)
+ primNode := graph.AddPrimitive(common.PrimitiveTypeString)
graph.AddEdge(structNode.Id, primNode.Id, symboldg.EdgeKindType, nil)
- timeNode = graph.AddSpecial(symboldg.SpecialTypeTime)
+ timeNode = graph.AddSpecial(common.SpecialTypeTime)
graph.AddEdge(structNode.Id, timeNode.Id, symboldg.EdgeKindField, nil)
- anyNode = graph.AddSpecial(symboldg.SpecialTypeAny)
+ anyNode = graph.AddSpecial(common.SpecialTypeAny)
graph.AddEdge(structNode.Id, anyNode.Id, symboldg.EdgeKindField, nil)
})
@@ -159,7 +159,7 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
filter := &symboldg.TraversalBehavior{
Sorting: symboldg.TraversalSortingOrdinalDesc,
Filtering: symboldg.TraversalFilter{
- EdgeKind: common.Ptr(symboldg.EdgeKindCall),
+ EdgeKinds: []symboldg.SymbolEdgeKind{symboldg.EdgeKindCall},
},
}
result := graph.Children(structNode, filter)
@@ -170,8 +170,8 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
filter := &symboldg.TraversalBehavior{
Sorting: symboldg.TraversalSortingOrdinalDesc,
Filtering: symboldg.TraversalFilter{
- EdgeKind: common.Ptr(symboldg.EdgeKindField),
- NodeKind: common.Ptr(common.SymKindSpecialBuiltin),
+ EdgeKinds: []symboldg.SymbolEdgeKind{symboldg.EdgeKindField},
+ NodeKinds: []common.SymKind{common.SymKindSpecialBuiltin},
},
}
result := graph.Children(structNode, filter)
@@ -200,9 +200,7 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
SymNodeMeta: metadata.SymNodeMeta{Node: utils.MakeIdent("Child"), FVersion: fVersion},
Type: metadata.TypeUsageMeta{
SymNodeMeta: metadata.SymNodeMeta{Name: "int", FVersion: fVersion},
- Layers: []metadata.TypeLayer{
- metadata.NewBaseLayer(common.Ptr(graphs.NewUniverseSymbolKey("int"))),
- },
+ Root: utils.MakeUniverseRoot("int"),
},
},
})
@@ -212,9 +210,7 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
SymNodeMeta: metadata.SymNodeMeta{Node: utils.MakeIdent("OtherChild"), FVersion: fVersion},
Type: metadata.TypeUsageMeta{
SymNodeMeta: metadata.SymNodeMeta{Name: "string", FVersion: fVersion},
- Layers: []metadata.TypeLayer{
- metadata.NewBaseLayer(common.Ptr(graphs.NewUniverseSymbolKey("string"))),
- },
+ Root: utils.MakeUniverseRoot("string"),
},
},
})
@@ -258,7 +254,7 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
// We set filter EdgeKind to a different kind, so shouldIncludeEdge returns false
filter := &symboldg.TraversalBehavior{
Filtering: symboldg.TraversalFilter{
- EdgeKind: common.Ptr(symboldg.EdgeKindValue), // deliberately a different edge kind
+ EdgeKinds: []symboldg.SymbolEdgeKind{symboldg.EdgeKindValue}, // deliberately a different edge kind
},
}
@@ -270,7 +266,7 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
When("Sorted", func() {
var strNode *symboldg.SymbolNode
BeforeEach(func() {
- strNode = graph.AddPrimitive(symboldg.PrimitiveTypeString)
+ strNode = graph.AddPrimitive(common.PrimitiveTypeString)
// As before this is *not* a valid tree but as the graph does not validate
// semantics we can use this for testing
graph.AddEdge(strNode.Id, fieldNode.Id, symboldg.EdgeKindType, nil)
@@ -320,7 +316,7 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
filter := &symboldg.TraversalBehavior{
Sorting: symboldg.TraversalSortingOrdinalAsc,
Filtering: symboldg.TraversalFilter{
- EdgeKind: common.Ptr(symboldg.EdgeKindValue), // deliberately a different edge kind
+ EdgeKinds: []symboldg.SymbolEdgeKind{symboldg.EdgeKindValue}, // deliberately a different edge kind
},
}
@@ -346,9 +342,7 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
SymNodeMeta: metadata.SymNodeMeta{Node: utils.MakeIdent("Child"), FVersion: fVersion},
Type: metadata.TypeUsageMeta{
SymNodeMeta: metadata.SymNodeMeta{Name: "int", FVersion: fVersion},
- Layers: []metadata.TypeLayer{
- metadata.NewBaseLayer(common.Ptr(graphs.NewUniverseSymbolKey("int"))),
- },
+ Root: utils.MakeUniverseRoot("int"),
},
},
})
@@ -357,9 +351,7 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
SymNodeMeta: metadata.SymNodeMeta{Node: utils.MakeIdent("GrandChild"), FVersion: fVersion},
Type: metadata.TypeUsageMeta{
SymNodeMeta: metadata.SymNodeMeta{Name: "int", FVersion: fVersion},
- Layers: []metadata.TypeLayer{
- metadata.NewBaseLayer(common.Ptr(graphs.NewUniverseSymbolKey("int"))),
- },
+ Root: utils.MakeUniverseRoot("int"),
},
},
})
@@ -370,8 +362,14 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
It("Recursively traverses all descendants", func() {
result := graph.Descendants(structNode, nil)
- Expect(result).To(HaveLen(2))
- Expect(result).To(ContainElements(childNode, grandChildNode))
+ Expect(result).To(HaveLen(3))
+
+ // The graph materializes some primitives like integers so we actually do expect
+ // an 'int' node here.
+ intNode := graph.Get(graphs.NewUniverseSymbolKey("int"))
+ Expect(intNode).ToNot(BeNil())
+
+ Expect(result).To(ContainElements(childNode, grandChildNode, intNode))
})
It("Does not revisit already visited nodes", func() {
@@ -379,9 +377,15 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
graph.AddEdge(grandChildNode.Id, childNode.Id, symboldg.EdgeKindField, nil)
result := graph.Descendants(structNode, nil)
+
+ // The graph materializes some primitives like integers so we actually do expect
+ // an 'int' node here.
+ intNode := graph.Get(graphs.NewUniverseSymbolKey("int"))
+ Expect(intNode).ToNot(BeNil())
+
// Should still only include each node once
- Expect(result).To(HaveLen(2))
- Expect(result).To(ContainElements(childNode, grandChildNode))
+ Expect(result).To(HaveLen(3))
+ Expect(result).To(ContainElements(childNode, grandChildNode, intNode))
})
It("Respects the filter to exclude some children", func() {
@@ -392,7 +396,7 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
// Create a filter that excludes the grandChildNode by NodeKind mismatch
filter := &symboldg.TraversalBehavior{
Filtering: symboldg.TraversalFilter{
- NodeKind: common.Ptr(common.SymKindSpecialBuiltin),
+ NodeKinds: []common.SymKind{common.SymKindSpecialBuiltin},
},
}
diff --git a/test/units/graphs/symbol/symbolgraph_test.go b/test/units/graphs/symbol/symbolgraph_test.go
index 32cea42..7fbb423 100644
--- a/test/units/graphs/symbol/symbolgraph_test.go
+++ b/test/units/graphs/symbol/symbolgraph_test.go
@@ -5,6 +5,7 @@ import (
"github.com/gopher-fleece/gleece/common"
"github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/core/metadata/typeref"
"github.com/gopher-fleece/gleece/graphs"
"github.com/gopher-fleece/gleece/graphs/symboldg"
"github.com/gopher-fleece/gleece/infrastructure/logger"
@@ -18,7 +19,7 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
It("Creates a usable SymbolGraph", func() {
g := symboldg.NewSymbolGraph()
// Graph should be usable: add a primitive and check presence
- p := symboldg.PrimitiveTypeString
+ p := common.PrimitiveTypeString
n := g.AddPrimitive(p)
Expect(n).ToNot(BeNil())
Expect(g.IsPrimitivePresent(p)).To(BeTrue())
@@ -33,6 +34,10 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
})
It("Returns an error from the idempotency guard when the given FileVersion is nil", func() {
+ // Build a TypeUsageMeta using the new Root-based representation (universe "string")
+ k := graphs.NewUniverseSymbolKey("string")
+ root := typeref.NewNamedTypeRef(&k, nil)
+
_, err := graph.AddConst(symboldg.CreateConstNode{
Data: metadata.ConstMeta{
SymNodeMeta: metadata.SymNodeMeta{
@@ -42,10 +47,11 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
},
Value: "some value",
Type: metadata.TypeUsageMeta{
- SymNodeMeta: metadata.SymNodeMeta{Name: "string", FVersion: nil},
- Layers: []metadata.TypeLayer{
- metadata.NewBaseLayer(common.Ptr(graphs.NewUniverseSymbolKey("string"))),
+ SymNodeMeta: metadata.SymNodeMeta{
+ Name: "string",
+ FVersion: nil,
},
+ Root: &root,
},
},
})
@@ -69,6 +75,10 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
Expect(n1).ToNot(BeNil())
// Add a dependent: add a field belonging to this struct (so revDeps get populated)
+ // Field type -> universe "int"
+ intKey := graphs.NewUniverseSymbolKey("int")
+ intRoot := typeref.NewNamedTypeRef(&intKey, nil)
+
fieldMeta := metadata.FieldMeta{
SymNodeMeta: metadata.SymNodeMeta{
Node: utils.MakeIdent("F1"),
@@ -78,9 +88,7 @@ var _ = Describe("Unit Tests - SymbolGraph", func() {
SymNodeMeta: metadata.SymNodeMeta{
FVersion: fv1,
},
- Layers: []metadata.TypeLayer{
- metadata.NewBaseLayer(common.Ptr(graphs.NewUniverseSymbolKey("int"))),
- },
+ Root: &intRoot,
},
}
_, err = graph.AddField(symboldg.CreateFieldNode{Data: fieldMeta})
diff --git a/test/units/graphs/symbolkey_test.go b/test/units/graphs/symbolkey_test.go
index 0ace228..c220479 100644
--- a/test/units/graphs/symbolkey_test.go
+++ b/test/units/graphs/symbolkey_test.go
@@ -22,60 +22,321 @@ func (f fakeNode) End() token.Pos { return token.Pos(99) }
var _ = Describe("Unit Tests - SymbolKey", func() {
- Describe("Id", func() {
- It("returns the correct ID for universe types", func() {
- key := graphs.NewUniverseSymbolKey("string")
- Expect(key.Id()).To(Equal("UniverseType:string"))
+ Context("Id", func() {
+
+ It("Returns the correct ID for universe types", func() {
+ universeKey := graphs.NewUniverseSymbolKey("string")
+ Expect(universeKey.Id()).To(Equal("UniverseType:string"))
})
- It("returns the correct ID for named symbol", func() {
- key := graphs.SymbolKey{
+ It("Returns the correct ID for named symbol", func() {
+ namedKey := graphs.SymbolKey{
Name: "Foo",
Position: 123,
FileId: "path/to/file|mod|abc123",
}
- Expect(key.Id()).To(Equal("Foo@123@path/to/file|mod|abc123"))
+ Expect(namedKey.Id()).To(Equal("Foo@123@path/to/file|mod|abc123"))
})
- It("returns the correct ID for unnamed symbol", func() {
- key := graphs.SymbolKey{
+ It("Returns the correct ID for unnamed symbol", func() {
+ unnamedKey := graphs.SymbolKey{
Position: 456,
FileId: "path/to/file|mod|xyz999",
}
- Expect(key.Id()).To(Equal("@456@path/to/file|mod|xyz999"))
+ Expect(unnamedKey.Id()).To(Equal("@456@path/to/file|mod|xyz999"))
})
})
- Describe("ShortLabel", func() {
- It("includes base file name and short hash", func() {
+ Context("ShortLabel", func() {
+
+ It("Includes base file name and short hash", func() {
key := graphs.SymbolKey{
Name: "Bar",
FileId: "path/to/thing.go|mymod|deadbeef1234",
}
- Expect(key.ShortLabel()).To(Equal("Bar@thing.go|deadbee"))
+
+ Expect(key.ShortLabel()).To(Equal("Bar @thing.go|deadbee"))
})
- It("handles missing name", func() {
+ It("Handles missing name and uses file info only", func() {
key := graphs.SymbolKey{
FileId: "some/path.go|mod|abcdef1234567890",
}
+
Expect(key.ShortLabel()).To(Equal("path.go|abcdef1"))
})
+
+ It("Shows built-in (non-universe) names unchanged", func() {
+ builtInKey := graphs.NewNonUniverseBuiltInSymbolKey("error")
+
+ // no file info attached -> label should be exactly the name
+ Expect(builtInKey.ShortLabel()).To(Equal("error"))
+ })
+
+ It("Formats type-params and attaches file info", func() {
+ fileVersion := &gast.FileVersion{
+ Path: "pkg/param.go",
+ ModTime: time.Unix(99, 0),
+ Hash: "paramhash",
+ }
+
+ paramKey := graphs.NewParamSymbolKey(fileVersion, "TA", 0)
+
+ shortLabel := paramKey.ShortLabel()
+
+ Expect(shortLabel).To(ContainSubstring("TA#0"))
+ Expect(shortLabel).To(ContainSubstring("param.go"))
+ Expect(shortLabel).To(ContainSubstring("paramha")) // short hash (7 chars)
+ })
+
+ It("Formats instantiated names with universe and regular args", func() {
+ baseKey := graphs.SymbolKey{
+ Name: "MultiGenericStruct",
+ Position: 1,
+ FileId: "base.go|100|bhashvalue",
+ FilePath: "base.go",
+ }
+
+ universeKey := graphs.NewUniverseSymbolKey("string")
+
+ regularArg := graphs.SymbolKey{
+ Name: "T",
+ Position: 2,
+ FileId: "arg.go|200|ahash",
+ FilePath: "arg.go",
+ }
+
+ instKey := graphs.NewInstSymbolKey(
+ baseKey,
+ []graphs.SymbolKey{
+ universeKey,
+ regularArg,
+ },
+ )
+
+ shortLabel := instKey.ShortLabel()
+
+ Expect(shortLabel).To(ContainSubstring("MultiGenericStruct["))
+ Expect(shortLabel).To(ContainSubstring("string"))
+ Expect(shortLabel).To(ContainSubstring("T"))
+ Expect(shortLabel).To(ContainSubstring("base.go"))
+ })
+
+ It("Formats instantiated name with no args as just the base", func() {
+ baseKey := graphs.SymbolKey{
+ Name: "OnlyBase",
+ Position: 5,
+ FileId: "only.go|5|ohash",
+ FilePath: "only.go",
+ }
+
+ instKey := graphs.NewInstSymbolKey(baseKey, nil)
+
+ shortLabel := instKey.ShortLabel()
+
+ Expect(shortLabel).To(ContainSubstring("OnlyBase"))
+ Expect(shortLabel).To(ContainSubstring("only.go"))
+ })
+
+ It("Pretty prints composite slice/ptr/array/map/func/unknown kinds", func() {
+ fileVersion := &gast.FileVersion{
+ Path: "pkg/thing.go",
+ ModTime: time.Unix(1000, 0),
+ Hash: "complhash",
+ }
+
+ opSimple := graphs.SymbolKey{
+ Name: "SimpleStruct",
+ FileId: "op.go|1|ohash",
+ }
+
+ // slice
+ sliceKey := graphs.NewCompositeTypeKey(
+ graphs.CompositeKindSlice,
+ fileVersion,
+ []graphs.SymbolKey{
+ opSimple,
+ },
+ )
+
+ Expect(sliceKey.Name).To(ContainSubstring("comp:slice"))
+ Expect(sliceKey.ShortLabel()).To(ContainSubstring("[]SimpleStruct"))
+ Expect(sliceKey.ShortLabel()).To(ContainSubstring("thing.go"))
+
+ // ptr
+ ptrKey := graphs.NewCompositeTypeKey(
+ graphs.CompositeKindPtr,
+ fileVersion,
+ []graphs.SymbolKey{
+ {Name: "Foo", FileId: "foo.go|1|fhash"},
+ },
+ )
+
+ Expect(ptrKey.ShortLabel()).To(ContainSubstring("*Foo"))
+
+ // array: if operand name is "10" -> "[10]"
+ arrayOperand := graphs.SymbolKey{
+ Name: "10",
+ FileId: "alen.go|1|alen",
+ }
+
+ arrayKey := graphs.NewCompositeTypeKey(
+ graphs.CompositeKindArray,
+ fileVersion,
+ []graphs.SymbolKey{
+ arrayOperand,
+ },
+ )
+
+ Expect(arrayKey.ShortLabel()).To(ContainSubstring("[10]"))
+
+ // map with two operands
+ mapKey := graphs.NewCompositeTypeKey(
+ graphs.CompositeKindMap,
+ fileVersion,
+ []graphs.SymbolKey{
+ {Name: "Key", FileId: "k.go|1|k"},
+ {Name: "Val", FileId: "v.go|1|v"},
+ },
+ )
+
+ Expect(mapKey.ShortLabel()).To(ContainSubstring("map[Key]Val"))
+
+ // map with single operand -> fallback: map[inner]
+ mapSingleKey := graphs.NewCompositeTypeKey(
+ graphs.CompositeKindMap,
+ fileVersion,
+ []graphs.SymbolKey{
+ {Name: "Only", FileId: "o.go|1|o"},
+ },
+ )
+
+ Expect(mapSingleKey.ShortLabel()).To(ContainSubstring("map[Only]"))
+
+ // func with two operands
+ funcKey := graphs.NewCompositeTypeKey(
+ graphs.CompositeKindFunc,
+ fileVersion,
+ []graphs.SymbolKey{
+ {Name: "Args", FileId: "a.go|1|a"},
+ {Name: "Rets", FileId: "r.go|1|r"},
+ },
+ )
+
+ Expect(funcKey.ShortLabel()).To(ContainSubstring("func(Args)(Rets)"))
+
+ // func with single operand -> fallback
+ funcSingleKey := graphs.NewCompositeTypeKey(
+ graphs.CompositeKindFunc,
+ fileVersion,
+ []graphs.SymbolKey{
+ {Name: "OnlyArgs", FileId: "oa.go|1|oa"},
+ },
+ )
+
+ Expect(funcSingleKey.ShortLabel()).To(ContainSubstring("func(OnlyArgs)"))
+
+ // unknown kind -> trim inner
+ unknownKey := graphs.NewCompositeTypeKey(
+ graphs.CompositeKind("weird"),
+ fileVersion,
+ []graphs.SymbolKey{
+ {Name: "WeirdInner", FileId: "w.go|1|w"},
+ },
+ )
+
+ Expect(unknownKey.ShortLabel()).To(ContainSubstring("WeirdInner"))
+ })
+
+ It("Works with nil fileVersion (no file info attached)", func() {
+ op := graphs.SymbolKey{
+ Name: "Elem",
+ FileId: "e.go|1|ehash",
+ }
+
+ compositeNoFile := graphs.NewCompositeTypeKey(
+ graphs.CompositeKindSlice,
+ nil,
+ []graphs.SymbolKey{
+ op,
+ },
+ )
+
+ shortLabel := compositeNoFile.ShortLabel()
+
+ Expect(shortLabel).To(ContainSubstring("[]Elem"))
+ Expect(shortLabel).ToNot(ContainSubstring("@"))
+ })
+
+ It("Attaches file base without short hash when present", func() {
+ key := graphs.SymbolKey{
+ Name: "Label",
+ FileId: "path/to/file.go|mymod", // no hash part -> shortHash == ""
+ }
+
+ shortLabel := key.ShortLabel()
+
+ // exact expected format: "Label @file.go"
+ Expect(shortLabel).To(Equal("Label @file.go"))
+ })
+
+ It("Returns file base only when name is empty and no short hash", func() {
+ key := graphs.SymbolKey{
+ Name: "", // empty name -> fall back to file info only
+ FileId: "some/dir/file.go|mod", // no hash part
+ }
+
+ shortLabel := key.ShortLabel()
+
+ // should return only the file base
+ Expect(shortLabel).To(Equal("file.go"))
+ })
+
+ It("Returns '?' when both name and file info are missing", func() {
+ key := graphs.SymbolKey{
+ Name: "",
+ FileId: "",
+ }
+
+ shortLabel := key.ShortLabel()
+
+ Expect(shortLabel).To(Equal("?"))
+ })
+
+ It("Handles composite names that lack brackets (parseCompositeName br<0 branch)", func() {
+ compositeKey := graphs.SymbolKey{
+ Name: "comp:slice", // no '[...]' -> parseCompositeName will return kind="slice", args=""
+ FileId: "pkg/nomod/sym.go|mod|comp1234567", // include a hash so file info is attached
+ FilePath: "sym.go",
+ }
+
+ shortLabel := compositeKey.ShortLabel()
+
+ // composite kind "slice" with empty args should produce "[]" prefix
+ Expect(shortLabel).To(ContainSubstring("[]"))
+
+ // file base should be attached; short hash is truncated to 7 chars
+ Expect(shortLabel).To(ContainSubstring("sym.go"))
+ })
+
})
- Describe("PrettyPrint", func() {
+ Context("PrettyPrint", func() {
+
It("Prints cleanly for universe types", func() {
key := graphs.NewUniverseSymbolKey("UniverseType:string")
Expect(key.PrettyPrint()).To(Equal("string"))
})
It("Prints details for named symbol", func() {
- key := graphs.SymbolKey{
+ namedKey := graphs.SymbolKey{
Name: "MyFunc",
Position: 789,
FileId: "src/main.go|mod|abcd1234",
}
- result := key.PrettyPrint()
+
+ result := namedKey.PrettyPrint()
+
Expect(result).To(ContainSubstring("MyFunc"))
Expect(result).To(ContainSubstring("• src/main.go"))
Expect(result).To(ContainSubstring("• mod"))
@@ -83,64 +344,65 @@ var _ = Describe("Unit Tests - SymbolKey", func() {
})
It("Pretty prints a symbol with no name using position fallback", func() {
- fSet := token.NewFileSet()
+ fileSet := token.NewFileSet()
src := `package main; var _ = 123`
- file, err := parser.ParseFile(fSet, "dummy.go", src, parser.AllErrors)
+ fileNode, err := parser.ParseFile(fileSet, "dummy.go", src, parser.AllErrors)
Expect(err).To(BeNil())
- // Use the whole file node — not one of the recognized node types
- version := &gast.FileVersion{
+ fileVersion := &gast.FileVersion{
Path: "dummy.go",
ModTime: time.Unix(1234, 0),
Hash: "abcd1234",
}
- // Trigger default case in NewSymbolKey and missing name fallback
- sk := graphs.NewSymbolKey(file, version)
+ // Use a node type not explicitly handled by NewSymbolKey to exercise fallback
+ symbolKey := graphs.NewSymbolKey(fileNode, fileVersion)
+
+ Expect(symbolKey.Name).To(Equal(""))
+ Expect(symbolKey.Position).ToNot(Equal(token.NoPos))
- Expect(sk.Name).To(Equal("")) // Should trigger fallback
- Expect(sk.Position).ToNot(Equal(token.NoPos))
+ output := symbolKey.PrettyPrint()
- output := sk.PrettyPrint()
- Expect(output).To(HavePrefix(fmt.Sprintf("@%d\n", sk.Position)))
+ Expect(output).To(HavePrefix(fmt.Sprintf("@%d\n", symbolKey.Position)))
Expect(output).To(ContainSubstring("• dummy.go"))
Expect(output).To(ContainSubstring("• abcd1234"))
})
-
})
- Describe("Equals", func() {
- It("returns true for equal universe keys", func() {
+ Context("Equals", func() {
+
+ It("Returns true for equal universe keys", func() {
a := graphs.NewUniverseSymbolKey("bool")
b := graphs.NewUniverseSymbolKey("bool")
Expect(a.Equals(b)).To(BeTrue())
})
- It("returns false for unequal universe keys", func() {
+ It("Returns false for unequal universe keys", func() {
a := graphs.NewUniverseSymbolKey("string")
b := graphs.NewUniverseSymbolKey("int")
Expect(a.Equals(b)).To(BeFalse())
})
- It("returns true for equal non-universe keys", func() {
+ It("Returns true for equal non-universe keys", func() {
a := graphs.SymbolKey{Name: "Foo", Position: 1, FileId: "id"}
b := graphs.SymbolKey{Name: "Foo", Position: 1, FileId: "id"}
Expect(a.Equals(b)).To(BeTrue())
})
- It("returns false if one field differs", func() {
+ It("Returns false if one field differs", func() {
a := graphs.SymbolKey{Name: "Foo", Position: 1, FileId: "id"}
b := graphs.SymbolKey{Name: "Foo", Position: 2, FileId: "id"}
Expect(a.Equals(b)).To(BeFalse())
})
})
- Describe("NewSymbolKey", func() {
- var version *gast.FileVersion
+ Context("NewSymbolKey", func() {
+
+ var fileVersion *gast.FileVersion
BeforeEach(func() {
- version = &gast.FileVersion{
+ fileVersion = &gast.FileVersion{
Path: "/abs/path/to/file.go",
ModTime: time.Now(),
Hash: "cafebabe12345678",
@@ -153,14 +415,14 @@ var _ = Describe("Unit Tests - SymbolKey", func() {
})
It("Creates from a real FuncDecl AST with valid token.Pos", func() {
- fSet := token.NewFileSet()
+ fileSet := token.NewFileSet()
src := `package main; func MyFunc() {}`
- file, err := parser.ParseFile(fSet, "fake.go", src, parser.AllErrors)
+ parsedFile, err := parser.ParseFile(fileSet, "fake.go", src, parser.AllErrors)
Expect(err).To(BeNil())
var fn *ast.FuncDecl
- for _, decl := range file.Decls {
+ for _, decl := range parsedFile.Decls {
if fd, ok := decl.(*ast.FuncDecl); ok {
fn = fd
break
@@ -168,13 +430,13 @@ var _ = Describe("Unit Tests - SymbolKey", func() {
}
Expect(fn).ToNot(BeNil())
- version := &gast.FileVersion{
+ funcFileVersion := &gast.FileVersion{
Path: "fake.go",
ModTime: time.Unix(123, 0),
Hash: "abc123",
}
- symKey := graphs.NewSymbolKey(fn, version)
+ symKey := graphs.NewSymbolKey(fn, funcFileVersion)
Expect(symKey.Name).To(Equal("MyFunc"))
Expect(symKey.FileId).To(Equal("fake.go|123|abc123"))
@@ -186,30 +448,30 @@ var _ = Describe("Unit Tests - SymbolKey", func() {
spec := &ast.TypeSpec{
Name: &ast.Ident{Name: "MyType"},
}
- key := graphs.NewSymbolKey(spec, version)
- Expect(key.Name).To(Equal("MyType"))
+ typedKey := graphs.NewSymbolKey(spec, fileVersion)
+ Expect(typedKey.Name).To(Equal("MyType"))
})
It("Creates from ValueSpec", func() {
- spec := &ast.ValueSpec{
+ valueSpec := &ast.ValueSpec{
Names: []*ast.Ident{{Name: "A"}, {Name: "B"}},
}
- key := graphs.NewSymbolKey(spec, version)
- Expect(key.Name).To(Equal("A,B"))
+ valueKey := graphs.NewSymbolKey(valueSpec, fileVersion)
+ Expect(valueKey.Name).To(Equal("A,B"))
})
It("Creates from Field", func() {
field := &ast.Field{
Names: []*ast.Ident{{Name: "FieldName"}},
}
- key := graphs.NewSymbolKey(field, version)
- Expect(key.Name).To(Equal("FieldName"))
+ fieldKey := graphs.NewSymbolKey(field, fileVersion)
+ Expect(fieldKey.Name).To(Equal("FieldName"))
})
It("Creates from Ident", func() {
id := &ast.Ident{Name: "VarX"}
- key := graphs.NewSymbolKey(id, version)
- Expect(key.Name).To(Equal("VarX"))
+ idKey := graphs.NewSymbolKey(id, fileVersion)
+ Expect(idKey.Name).To(Equal("VarX"))
})
It("Falls back to default in NewSymbolKey for unknown node types", func() {
@@ -226,19 +488,20 @@ var _ = Describe("Unit Tests - SymbolKey", func() {
Expect(sk.Position).To(Equal(token.Pos(42)))
Expect(sk.FileId).To(Equal("somefile.go|4567|def456"))
})
-
})
- Describe("NewUniverseSymbolKey", func() {
- It("sets IsUniverse and IsBuiltIn", func() {
+ Context("NewUniverseSymbolKey", func() {
+
+ It("Sets IsUniverse and IsBuiltIn", func() {
key := graphs.NewUniverseSymbolKey("string")
Expect(key.IsUniverse).To(BeTrue())
Expect(key.IsBuiltIn).To(BeTrue())
})
})
- Describe("NewNonUniverseBuiltInSymbolKey", func() {
- It("sets IsBuiltIn but not IsUniverse", func() {
+ Context("NewNonUniverseBuiltInSymbolKey", func() {
+
+ It("Sets IsBuiltIn but not IsUniverse", func() {
key := graphs.NewNonUniverseBuiltInSymbolKey("error")
Expect(key.IsBuiltIn).To(BeTrue())
Expect(key.IsUniverse).To(BeFalse())
diff --git a/test/units/metadata/field_test.go b/test/units/metadata/field_test.go
new file mode 100644
index 0000000..ae26508
--- /dev/null
+++ b/test/units/metadata/field_test.go
@@ -0,0 +1,23 @@
+package metadata_test
+
+import (
+ "github.com/gopher-fleece/gleece/core/metadata"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("Unit Tests - Metadata - Fields", func() {
+ Context("Reduce", func() {
+ It("Returns an error if the encapsulated node is not an AST Field", func() {
+ f := metadata.FieldMeta{
+ SymNodeMeta: metadata.SymNodeMeta{
+ Name: "F",
+ Node: nil,
+ },
+ }
+
+ _, err := f.Reduce(metadata.ReductionContext{})
+ Expect(err).To(MatchError(Equal("field 'F' has a non-field node type")))
+ })
+ })
+})
diff --git a/test/units/metadata/metadata_test.go b/test/units/metadata/metadata_test.go
index 0748805..e14ca75 100644
--- a/test/units/metadata/metadata_test.go
+++ b/test/units/metadata/metadata_test.go
@@ -18,7 +18,7 @@ var _ = BeforeSuite(func() {
ctx = utils.GetVisitContextByRelativeConfigOrFail("gleece.test.config.json")
})
-func TestUnitCommons(t *testing.T) {
+func TestUnitMetadata(t *testing.T) {
logger.SetLogLevel(logger.LogLevelNone)
RegisterFailHandler(Fail)
RunSpecs(t, "Unit Tests - Metadata")
diff --git a/test/units/metadata/retval_test.go b/test/units/metadata/retval_test.go
new file mode 100644
index 0000000..16ca49d
--- /dev/null
+++ b/test/units/metadata/retval_test.go
@@ -0,0 +1,37 @@
+package metadata_test
+
+import (
+ "github.com/gopher-fleece/gleece/common"
+ "github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/core/metadata/typeref"
+ "github.com/gopher-fleece/gleece/definitions"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("Unit Tests - Metadata", func() {
+ Describe("FuncReturnValue", func() {
+ Context("Reduce", func() {
+ It("Returns an error when unable to resolve the underlying type", func() {
+ retVal := metadata.FuncReturnValue{
+ SymNodeMeta: metadata.SymNodeMeta{
+ Name: "RetVal1",
+ },
+ Type: metadata.TypeUsageMeta{
+ SymNodeMeta: metadata.SymNodeMeta{
+ Name: "string",
+ },
+ Root: common.Ptr(
+ // Use a broken type ref to trigger an early failure
+ typeref.NewNamedTypeRef(nil, nil),
+ ),
+ },
+ }
+
+ result, err := retVal.Reduce(metadata.ReductionContext{})
+ Expect(err).To(MatchError(ContainSubstring("failed to derive cache lookup symbol key for type usage")))
+ Expect(result).To(Equal(definitions.FuncReturnValue{}))
+ })
+ })
+ })
+})
diff --git a/test/units/metadata/structs_test.go b/test/units/metadata/structs_test.go
index 10d5933..a3db4a9 100644
--- a/test/units/metadata/structs_test.go
+++ b/test/units/metadata/structs_test.go
@@ -46,7 +46,7 @@ var _ = Describe("Unit Tests - Metadata", func() {
PkgPath: "",
Annotations: &holder,
},
- Layers: []metadata.TypeLayer{},
+ Root: utils.MakeUniverseRoot("string"),
},
},
{
@@ -61,14 +61,17 @@ var _ = Describe("Unit Tests - Metadata", func() {
PkgPath: "",
Annotations: &holder,
},
- Layers: []metadata.TypeLayer{},
+ Root: utils.MakeUniverseRoot("int"),
},
IsEmbedded: true,
},
},
}
- reduced := structMeta.Reduce()
+ // An empty context here is OK, for now - this is done to maintain a uniform reducer signature and is
+ // is not currently being used
+ reduced, err := structMeta.Reduce(metadata.ReductionContext{})
+ Expect(err).To(BeNil())
Expect(reduced.Name).To(Equal("TestStruct"))
Expect(reduced.PkgPath).To(Equal("example.com/mypkg"))
@@ -116,7 +119,12 @@ var _ = Describe("Unit Tests - Metadata", func() {
controller.Struct.Annotations = holder
- result, err := controller.Reduce(ctx.GleeceConfig, ctx.MetadataCache, ctx.SyncedProvider)
+ result, err := controller.Reduce(metadata.ReductionContext{
+ GleeceConfig: ctx.GleeceConfig,
+ MetaCache: ctx.MetadataCache,
+ SyncedProvider: ctx.SyncedProvider,
+ })
+
Expect(err).ToNot(HaveOccurred())
Expect(result.Name).To(Equal("ExampleController"))
Expect(result.Description).To(Equal("Example controller"))
@@ -139,7 +147,12 @@ var _ = Describe("Unit Tests - Metadata", func() {
)
controller.Struct.Annotations = holder
- result, err := controller.Reduce(ctx.GleeceConfig, ctx.MetadataCache, ctx.SyncedProvider)
+ result, err := controller.Reduce(metadata.ReductionContext{
+ GleeceConfig: ctx.GleeceConfig,
+ MetaCache: ctx.MetadataCache,
+ SyncedProvider: ctx.SyncedProvider,
+ })
+
Expect(err).ToNot(HaveOccurred())
Expect(result.Security).To(Equal(metadata.GetDefaultSecurity(ctx.GleeceConfig)))
})
@@ -168,7 +181,12 @@ var _ = Describe("Unit Tests - Metadata", func() {
controller.Struct.Annotations = &holder
- _, err := controller.Reduce(ctx.GleeceConfig, ctx.MetadataCache, ctx.SyncedProvider)
+ _, err := controller.Reduce(metadata.ReductionContext{
+ GleeceConfig: ctx.GleeceConfig,
+ MetaCache: ctx.MetadataCache,
+ SyncedProvider: ctx.SyncedProvider,
+ })
+
Expect(err).To(MatchError(ContainSubstring("a security schema's name cannot be empty")))
})
})
diff --git a/test/units/metadata/type.layer_test.go b/test/units/metadata/type.layer_test.go
deleted file mode 100644
index 030306b..0000000
--- a/test/units/metadata/type.layer_test.go
+++ /dev/null
@@ -1,58 +0,0 @@
-package metadata_test
-
-import (
- "github.com/gopher-fleece/gleece/core/metadata"
- "github.com/gopher-fleece/gleece/graphs"
- . "github.com/onsi/ginkgo/v2"
- . "github.com/onsi/gomega"
-)
-
-var _ = Describe("Unit Tests - Metadata", func() {
- var _ = Describe("TypeLayer", func() {
-
- Context("NewPointerLayer", func() {
- It("creates a pointer layer with correct kind", func() {
- layer := metadata.NewPointerLayer()
- Expect(layer.Kind).To(Equal(metadata.TypeLayerKindPointer))
- Expect(layer.KeyType).To(BeNil())
- Expect(layer.ValueType).To(BeNil())
- Expect(layer.BaseTypeRef).To(BeNil())
- })
- })
-
- Context("NewArrayLayer", func() {
- It("creates an array layer with correct kind", func() {
- layer := metadata.NewArrayLayer()
- Expect(layer.Kind).To(Equal(metadata.TypeLayerKindArray))
- Expect(layer.KeyType).To(BeNil())
- Expect(layer.ValueType).To(BeNil())
- Expect(layer.BaseTypeRef).To(BeNil())
- })
- })
-
- Context("NewMapLayer", func() {
- It("creates a map layer with correct key and value", func() {
- key := graphs.NewUniverseSymbolKey("string")
- value := graphs.NewUniverseSymbolKey("int")
- layer := metadata.NewMapLayer(&key, &value)
-
- Expect(layer.Kind).To(Equal(metadata.TypeLayerKindMap))
- Expect(layer.KeyType).To(Equal(&key))
- Expect(layer.ValueType).To(Equal(&value))
- Expect(layer.BaseTypeRef).To(BeNil())
- })
- })
-
- Context("NewBaseLayer", func() {
- It("creates a base layer with correct base reference", func() {
- base := graphs.NewUniverseSymbolKey("MyStruct")
- layer := metadata.NewBaseLayer(&base)
-
- Expect(layer.Kind).To(Equal(metadata.TypeLayerKindBase))
- Expect(layer.BaseTypeRef).To(Equal(&base))
- Expect(layer.KeyType).To(BeNil())
- Expect(layer.ValueType).To(BeNil())
- })
- })
- })
-})
diff --git a/test/units/metadata/typeref/array.typeref_test.go b/test/units/metadata/typeref/array.typeref_test.go
new file mode 100644
index 0000000..2edaf08
--- /dev/null
+++ b/test/units/metadata/typeref/array.typeref_test.go
@@ -0,0 +1,103 @@
+package metadata_test
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/core/metadata/typeref"
+ "github.com/gopher-fleece/gleece/graphs"
+ "github.com/gopher-fleece/gleece/test/utils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("Unit Tests - ArrayTypeRef", func() {
+ var (
+ elemKey = graphs.NewUniverseSymbolKey("elem")
+ fVersion = utils.MakeFileVersion("file", "")
+ )
+
+ Context("Kind", func() {
+ It("Returns the Array TypeRefKind", func() {
+ a := &typeref.ArrayTypeRef{Len: nil, Elem: &utils.FakeTypeRef{RefKind: metadata.TypeRefKindArray}}
+ Expect(a.Kind()).To(Equal(metadata.TypeRefKindArray))
+ })
+ })
+
+ Context("CanonicalString", func() {
+ It("Formats as slice when Len is nil", func() {
+ elem := &utils.FakeTypeRef{CanonicalStr: "MyElem"}
+ a := &typeref.ArrayTypeRef{Len: nil, Elem: elem}
+ Expect(a.CanonicalString()).To(Equal("[]MyElem"))
+ })
+
+ It("Formats as fixed-length array when Len is set", func() {
+ n := 5
+ elem := &utils.FakeTypeRef{CanonicalStr: "X"}
+ a := &typeref.ArrayTypeRef{Len: &n, Elem: elem}
+ Expect(a.CanonicalString()).To(Equal("[5]X"))
+ })
+ })
+
+ Context("SimpleTypeString", func() {
+ It("Prefixes element simple string with []", func() {
+ elem := &utils.FakeTypeRef{SimpleStr: "T"}
+ a := &typeref.ArrayTypeRef{Len: nil, Elem: elem}
+ Expect(a.SimpleTypeString()).To(Equal("[]T"))
+ })
+ })
+
+ Context("CacheLookupKey", func() {
+ It("Delegates to element CacheLookupKey", func() {
+ f := &utils.FakeTypeRef{SymKey: elemKey}
+ a := &typeref.ArrayTypeRef{Len: nil, Elem: f}
+ k, err := a.CacheLookupKey(fVersion)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(k).To(Equal(elemKey))
+ })
+ })
+
+ Context("ToSymKey", func() {
+ It("Builds a composite array key when element ToSymKey succeeds", func() {
+ f := &utils.FakeTypeRef{SymKey: elemKey}
+ a := &typeref.ArrayTypeRef{Len: nil, Elem: f}
+
+ got, err := a.ToSymKey(fVersion)
+ Expect(err).ToNot(HaveOccurred())
+
+ want := graphs.NewCompositeTypeKey(graphs.CompositeKindArray, fVersion, []graphs.SymbolKey{elemKey})
+ Expect(got).To(Equal(want))
+ })
+
+ It("Propagates element ToSymKey errors", func() {
+ expErr := errors.New("boom")
+ f := &utils.FakeTypeRef{ToSymKeyErr: expErr}
+ a := &typeref.ArrayTypeRef{Len: nil, Elem: f}
+
+ _, err := a.ToSymKey(fVersion)
+ Expect(err).To(MatchError(ContainSubstring("boom")))
+ })
+ })
+
+ Context("Flatten", func() {
+ It("Returns a non-nil slice of TypeRefs and canonical strings are usable", func() {
+ // provide a fake Flatten response and ensure result is returned and sane
+ elem := &utils.FakeTypeRef{
+ CanonicalStr: "E",
+ FlattenResponse: []metadata.TypeRef{&utils.FakeTypeRef{CanonicalStr: "leaf"}},
+ }
+ a := &typeref.ArrayTypeRef{Len: nil, Elem: elem}
+
+ res := a.Flatten()
+ Expect(res).ToNot(BeNil())
+ Expect(len(res)).To(BeNumerically(">=", 0))
+
+ // ensure calling CanonicalString on each element doesn't panic and returns non-empty
+ for i, r := range res {
+ cs := r.CanonicalString()
+ Expect(cs).ToNot(BeEmpty(), fmt.Sprintf("element %d had empty canonical string", i))
+ }
+ })
+ })
+})
diff --git a/test/units/metadata/typeref/common.typeref_test.go b/test/units/metadata/typeref/common.typeref_test.go
new file mode 100644
index 0000000..93b3cd4
--- /dev/null
+++ b/test/units/metadata/typeref/common.typeref_test.go
@@ -0,0 +1,118 @@
+package metadata_test
+
+import (
+ "github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/core/metadata/typeref"
+ "github.com/gopher-fleece/gleece/graphs"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+)
+
+// These cases are to test the 'private' TypeRef commons logic.
+// We're using concrete implementations of TypeRef here as the entrypoints to the package.
+var _ = Describe("Unit Tests - TypeRef Commons", func() {
+ Context("Flatten", func() {
+ It("Returns Elem for Ptr, Slice and Array types", func() {
+ base := mustNamedPtr("Base", "")
+ ptr := &typeref.PtrTypeRef{Elem: base}
+ slice := &typeref.SliceTypeRef{Elem: base}
+ array := &typeref.ArrayTypeRef{Elem: base}
+
+ Expect(ptr.Flatten()).To(HaveLen(1))
+ Expect(ptr.Flatten()[0].CanonicalString()).To(Equal(base.CanonicalString()))
+
+ Expect(slice.Flatten()).To(HaveLen(1))
+ Expect(slice.Flatten()[0].CanonicalString()).To(Equal(base.CanonicalString()))
+
+ Expect(array.Flatten()).To(HaveLen(1))
+ Expect(array.Flatten()[0].CanonicalString()).To(Equal(base.CanonicalString()))
+ })
+
+ It("Returns Key and Value for Map types", func() {
+ k := mustNamedPtr("K", "")
+ v := mustNamedPtr("V", "")
+ m := &typeref.MapTypeRef{Key: k, Value: v}
+
+ out := m.Flatten()
+ Expect(out).To(HaveLen(2))
+ Expect(out[0].CanonicalString()).To(Equal(k.CanonicalString()))
+ Expect(out[1].CanonicalString()).To(Equal(v.CanonicalString()))
+ })
+
+ It("Returns Params and Results for Func types", func() {
+ p1 := mustNamedPtr("P1", "")
+ p2 := mustNamedPtr("P2", "")
+ r1 := mustNamedPtr("R1", "")
+ f := &typeref.FuncTypeRef{Params: []metadata.TypeRef{p1, p2}, Results: []metadata.TypeRef{r1}}
+
+ out := f.Flatten()
+ Expect(out).To(HaveLen(3))
+ Expect(out[0].CanonicalString()).To(Equal(p1.CanonicalString()))
+ Expect(out[1].CanonicalString()).To(Equal(p2.CanonicalString()))
+ Expect(out[2].CanonicalString()).To(Equal(r1.CanonicalString()))
+ })
+
+ It("Returns TypeArgs for Named types", func() {
+ a1 := mustNamedPtr("A1", "")
+ a2 := mustNamedPtr("A2", "")
+ key := graphs.SymbolKey{Name: "MyType", FileId: "f1"}
+ n := typeref.NewNamedTypeRef(&key, []metadata.TypeRef{a1, a2})
+ out := (&n).Flatten()
+ Expect(out).To(HaveLen(2))
+ Expect(out[0].CanonicalString()).To(Equal(a1.CanonicalString()))
+ Expect(out[1].CanonicalString()).To(Equal(a2.CanonicalString()))
+ })
+
+ It("Returns field root types for InlineStruct types", func() {
+ f1 := mustNamedPtr("F1", "")
+ fields := []metadata.FieldMeta{
+ {Type: metadata.TypeUsageMeta{Root: f1}},
+ {Type: metadata.TypeUsageMeta{Root: mustNamedPtr("Anon", "")}},
+ }
+ in := &typeref.InlineStructTypeRef{Fields: fields}
+
+ out := in.Flatten()
+ Expect(out).To(HaveLen(2))
+ Expect(out[0].CanonicalString()).To(Equal(f1.CanonicalString()))
+ Expect(out[1].CanonicalString()).To(Equal(fields[1].Type.Root.CanonicalString()))
+ })
+ })
+
+ Context("Canonical Sym Key through public APIs", func() {
+ It("NamedTypeRef CanonicalString uses universe/builtin name only", func() {
+ k := graphs.SymbolKey{Name: "int", IsBuiltIn: true}
+ n := typeref.NewNamedTypeRef(&k, nil)
+ Expect((&n).CanonicalString()).To(Equal("int"))
+ })
+
+ It("NamedTypeRef CanonicalString prefers FileId over FilePath", func() {
+ k := graphs.SymbolKey{Name: "T", FileId: "file-123", FilePath: "some/path"}
+ n := typeref.NewNamedTypeRef(&k, nil)
+ Expect((&n).CanonicalString()).To(Equal("T|file-123"))
+ })
+
+ It("NamedTypeRef CanonicalString falls back to FilePath when no FileId", func() {
+ k := graphs.SymbolKey{Name: "T", FilePath: "some/path"}
+ n := typeref.NewNamedTypeRef(&k, nil)
+ Expect((&n).CanonicalString()).To(Equal("T|some/path"))
+ })
+
+ It("InlineStruct CanonicalString appends repkey when present", func() {
+ f1 := mustNamedPtr("S1", "")
+ fields := []metadata.FieldMeta{{Type: metadata.TypeUsageMeta{Root: f1}}}
+ rep := graphs.SymbolKey{Name: "anon", FileId: "rep-1"}
+ in := &typeref.InlineStructTypeRef{Fields: fields, RepKey: rep}
+ cs := in.CanonicalString()
+ Expect(cs).To(ContainSubstring("inline{"))
+ // repkey should be appended after a pipe
+ Expect(cs).To(ContainSubstring("|" + rep.Name + "|"))
+ })
+ })
+})
+
+// ---------------- helpers ----------------
+func mustNamedPtr(name, fileId string) *typeref.NamedTypeRef {
+ k := graphs.SymbolKey{Name: name, FileId: fileId}
+ r := typeref.NewNamedTypeRef(&k, nil)
+ return &r
+}
diff --git a/test/units/metadata/typeref/func.typeref_test.go b/test/units/metadata/typeref/func.typeref_test.go
new file mode 100644
index 0000000..07ba9f9
--- /dev/null
+++ b/test/units/metadata/typeref/func.typeref_test.go
@@ -0,0 +1,128 @@
+package metadata_test
+
+import (
+ "fmt"
+
+ "github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/core/metadata/typeref"
+ "github.com/gopher-fleece/gleece/gast"
+ "github.com/gopher-fleece/gleece/graphs"
+ "github.com/gopher-fleece/gleece/test/utils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("Unit Tests - FuncTypeRef", func() {
+ var fVersion *gast.FileVersion
+
+ BeforeEach(func() {
+ fVersion = utils.MakeFileVersion("file", "")
+ })
+
+ Context("Kind", func() {
+ It("Returns Func kind", func() {
+ funcRef := &typeref.FuncTypeRef{}
+ Expect(funcRef.Kind()).To(Equal(metadata.TypeRefKindFunc))
+ })
+ })
+
+ Context("string representations", func() {
+ It("Produces canonical string using params/results canonical strings", func() {
+ paramRef := &utils.FakeTypeRef{
+ RefKind: metadata.TypeRefKindNamed,
+ CanonicalStr: "pkg.A",
+ SimpleStr: "A",
+ }
+ resultRef := &utils.FakeTypeRef{
+ RefKind: metadata.TypeRefKindNamed,
+ CanonicalStr: "pkg.B",
+ SimpleStr: "B",
+ }
+
+ funcRef := &typeref.FuncTypeRef{
+ Params: []metadata.TypeRef{paramRef},
+ Results: []metadata.TypeRef{resultRef},
+ }
+
+ Expect(funcRef.CanonicalString()).To(Equal("func(pkg.A)(pkg.B)"))
+ Expect(funcRef.SimpleTypeString()).To(Equal("func(A)(B)"))
+ })
+
+ It("Handles multiple params/results and empty lists", func() {
+ p1 := &utils.FakeTypeRef{CanonicalStr: "X1", SimpleStr: "X1"}
+ p2 := &utils.FakeTypeRef{CanonicalStr: "X2", SimpleStr: "X2"}
+ r1 := &utils.FakeTypeRef{CanonicalStr: "R1", SimpleStr: "R1"}
+
+ funcRef := &typeref.FuncTypeRef{
+ Params: []metadata.TypeRef{p1, p2},
+ Results: []metadata.TypeRef{r1},
+ }
+
+ Expect(funcRef.CanonicalString()).To(Equal("func(X1,X2)(R1)"))
+ Expect(funcRef.SimpleTypeString()).To(Equal("func(X1,X2)(R1)"))
+ })
+ })
+
+ Context("ToSymKey / CacheLookupKey", func() {
+ It("Returns composite symkey combining param and result symkeys", func() {
+ paramSymKey := graphs.NewUniverseSymbolKey("int")
+ resultSymKey := graphs.NewUniverseSymbolKey("string")
+
+ paramRef := &utils.FakeTypeRef{SymKey: paramSymKey}
+ resultRef := &utils.FakeTypeRef{SymKey: resultSymKey}
+
+ funcRef := &typeref.FuncTypeRef{
+ Params: []metadata.TypeRef{paramRef},
+ Results: []metadata.TypeRef{resultRef},
+ }
+
+ expectedKey := graphs.NewCompositeTypeKey(graphs.CompositeKindFunc, fVersion, []graphs.SymbolKey{
+ paramSymKey, resultSymKey,
+ })
+
+ gotKey, err := funcRef.ToSymKey(fVersion)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(gotKey).To(Equal(expectedKey))
+
+ // CacheLookupKey delegates to ToSymKey
+ cacheKey, err := funcRef.CacheLookupKey(fVersion)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(cacheKey).To(Equal(expectedKey))
+ })
+
+ It("Propagates error when a param's ToSymKey fails", func() {
+ paramErr := fmt.Errorf("param failure")
+ badParam := &utils.FakeTypeRef{ToSymKeyErr: paramErr}
+
+ funcRef := &typeref.FuncTypeRef{
+ Params: []metadata.TypeRef{badParam},
+ Results: nil,
+ }
+
+ _, err := funcRef.ToSymKey(fVersion)
+ Expect(err).To(MatchError(ContainSubstring("param failure")))
+ })
+ })
+
+ Context("Flatten", func() {
+ It("Returns flattened list of params then results", func() {
+ paramRef := &utils.FakeTypeRef{FlattenResponse: []metadata.TypeRef{}}
+ resultRef := &utils.FakeTypeRef{FlattenResponse: []metadata.TypeRef{}}
+
+ // We expect flatten to return concatenation of param.Flatten() and result.Flatten().
+ // Provide non-empty marker slices on each FakeTypeRef.
+ paramRef.FlattenResponse = []metadata.TypeRef{paramRef}
+ resultRef.FlattenResponse = []metadata.TypeRef{resultRef}
+
+ funcRef := &typeref.FuncTypeRef{
+ Params: []metadata.TypeRef{paramRef},
+ Results: []metadata.TypeRef{resultRef},
+ }
+
+ got := funcRef.Flatten()
+ Expect(got).To(HaveLen(2))
+ Expect(got[0]).To(BeIdenticalTo(paramRef))
+ Expect(got[1]).To(BeIdenticalTo(resultRef))
+ })
+ })
+})
diff --git a/test/units/metadata/typeref/inline.strcut.typeref_test.go b/test/units/metadata/typeref/inline.strcut.typeref_test.go
new file mode 100644
index 0000000..f7d6545
--- /dev/null
+++ b/test/units/metadata/typeref/inline.strcut.typeref_test.go
@@ -0,0 +1,186 @@
+package metadata_test
+
+import (
+ "strings"
+
+ "github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/core/metadata/typeref"
+ "github.com/gopher-fleece/gleece/graphs"
+ "github.com/gopher-fleece/gleece/test/utils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("Unit Tests - InlineStructTypeRef", func() {
+ Describe("Kind", func() {
+ It("Returns the InlineStruct kind", func() {
+ inline := &typeref.InlineStructTypeRef{}
+ Expect(inline.Kind()).To(Equal(metadata.TypeRefKindInlineStruct))
+ })
+ })
+
+ Describe("string representations", func() {
+ var (
+ fieldTypeA *utils.FakeTypeRef
+ fieldTypeB *utils.FakeTypeRef
+ )
+
+ BeforeEach(func() {
+ fieldTypeA = &utils.FakeTypeRef{
+ RefKind: metadata.TypeRefKindNamed,
+ CanonicalStr: "pkg.A",
+ SimpleStr: "A",
+ }
+ fieldTypeB = &utils.FakeTypeRef{
+ RefKind: metadata.TypeRefKindNamed,
+ CanonicalStr: "pkg.B",
+ SimpleStr: "B",
+ }
+ })
+
+ It("Builds canonical string including field names and canonical element strings", func() {
+ inline := &typeref.InlineStructTypeRef{
+ Fields: []metadata.FieldMeta{
+ {
+ SymNodeMeta: metadata.SymNodeMeta{
+ Name: "First",
+ },
+ Type: metadata.TypeUsageMeta{Root: fieldTypeA}},
+ {
+ SymNodeMeta: metadata.SymNodeMeta{
+ Name: "",
+ },
+ Type: metadata.TypeUsageMeta{Root: fieldTypeB},
+ },
+ },
+ }
+
+ canon := inline.CanonicalString()
+ Expect(canon).To(ContainSubstring("inline{"))
+ Expect(canon).To(ContainSubstring("First:pkg.A"))
+ Expect(canon).To(ContainSubstring("pkg.B"))
+ // ensure canonical and simple are different in this case
+ simple := inline.SimpleTypeString()
+ Expect(simple).To(ContainSubstring("First:A"))
+ Expect(simple).To(ContainSubstring("B"))
+ Expect(simple).ToNot(Equal(canon))
+ })
+
+ It("Returns minimal form for empty field list", func() {
+ inline := &typeref.InlineStructTypeRef{}
+ Expect(inline.CanonicalString()).To(Equal("inline{}"))
+ Expect(inline.SimpleTypeString()).To(Equal("inline{}"))
+ })
+
+ It("Appends representative key suffix when RepKey is present", func() {
+ repKey := graphs.NewNonUniverseBuiltInSymbolKey("somepkg.SomeAnon")
+ inline := &typeref.InlineStructTypeRef{
+ Fields: []metadata.FieldMeta{
+ {
+ SymNodeMeta: metadata.SymNodeMeta{
+ Name: "X",
+ },
+ Type: metadata.TypeUsageMeta{Root: fieldTypeA},
+ },
+ },
+ RepKey: repKey,
+ }
+
+ canon := inline.CanonicalString()
+ Expect(canon).To(ContainSubstring("inline{"))
+ Expect(canon).To(ContainSubstring("X:pkg.A"))
+ // should have the '|' suffix appended for RepKey
+ Expect(strings.Contains(canon, "|")).To(BeTrue(),
+ "Canonical should include '|' when RepKey is present; got: %s", canon)
+ })
+ })
+
+ Describe("ToSymKey / CacheLookupKey", func() {
+ It("Errors when receiver is nil", func() {
+ var inline *typeref.InlineStructTypeRef = nil
+ _, err := inline.ToSymKey(nil)
+ Expect(err).To(MatchError(ContainSubstring("nil InlineStructTypeRef")))
+ })
+
+ It("Errors when RepKey is not set", func() {
+ inline := &typeref.InlineStructTypeRef{
+ Fields: []metadata.FieldMeta{
+ {
+ SymNodeMeta: metadata.SymNodeMeta{
+ Name: "f",
+ },
+ Type: metadata.TypeUsageMeta{Root: &utils.FakeTypeRef{}},
+ },
+ },
+ RepKey: graphs.SymbolKey{}, // zero
+ }
+ _, err := inline.ToSymKey(nil)
+ Expect(err).To(MatchError(ContainSubstring("inline struct missing RepKey")))
+ })
+
+ It("Returns the representative key when present", func() {
+ repKey := graphs.NewNonUniverseBuiltInSymbolKey("mypkg.MyAnon")
+ inline := &typeref.InlineStructTypeRef{
+ Fields: []metadata.FieldMeta{
+ {
+ SymNodeMeta: metadata.SymNodeMeta{
+ Name: "f",
+ },
+ Type: metadata.TypeUsageMeta{Root: &utils.FakeTypeRef{}},
+ },
+ },
+ RepKey: repKey,
+ }
+
+ gotKey, err := inline.ToSymKey(nil)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(gotKey).To(Equal(repKey))
+
+ // CacheLookupKey delegates to ToSymKey
+ cacheKey, err := inline.CacheLookupKey(nil)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(cacheKey).To(Equal(repKey))
+ })
+ })
+
+ Describe("Flatten", func() {
+ It("Returns the TypeRef roots of each field in order", func() {
+ fType1 := &utils.FakeTypeRef{
+ RefKind: metadata.TypeRefKindNamed,
+ CanonicalStr: "pkg.One",
+ SimpleStr: "One",
+ }
+ fType2 := &utils.FakeTypeRef{
+ RefKind: metadata.TypeRefKindNamed,
+ CanonicalStr: "pkg.Two",
+ SimpleStr: "Two",
+ }
+
+ inline := &typeref.InlineStructTypeRef{
+ Fields: []metadata.FieldMeta{
+ {
+ SymNodeMeta: metadata.SymNodeMeta{
+ Name: "A",
+ }, Type: metadata.TypeUsageMeta{Root: fType1}},
+ {
+ SymNodeMeta: metadata.SymNodeMeta{
+ Name: "B",
+ }, Type: metadata.TypeUsageMeta{Root: fType2},
+ },
+ },
+ }
+
+ flattened := inline.Flatten()
+ Expect(flattened).To(HaveLen(2))
+ Expect(flattened[0]).To(Equal(metadata.TypeRef(fType1)))
+ Expect(flattened[1]).To(Equal(metadata.TypeRef(fType2)))
+ })
+
+ It("Returns empty slice when no fields present", func() {
+ inline := &typeref.InlineStructTypeRef{}
+ flattened := inline.Flatten()
+ // As flatten returns nil for default/leaf types, InlineStruct with no fields yields nil
+ Expect(len(flattened)).To(BeZero())
+ })
+ })
+})
diff --git a/test/units/metadata/typeref/map.typeref_test.go b/test/units/metadata/typeref/map.typeref_test.go
new file mode 100644
index 0000000..83e418f
--- /dev/null
+++ b/test/units/metadata/typeref/map.typeref_test.go
@@ -0,0 +1,93 @@
+package metadata_test
+
+import (
+ "fmt"
+
+ "github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/core/metadata/typeref"
+ "github.com/gopher-fleece/gleece/gast"
+ "github.com/gopher-fleece/gleece/graphs"
+ "github.com/gopher-fleece/gleece/test/utils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("Unit Tests - MapTypeRef", func() {
+ var fVersion *gast.FileVersion
+
+ BeforeEach(func() {
+ fVersion = utils.MakeFileVersion("file", "")
+ })
+
+ Context("Kind", func() {
+ It("Returns map kind", func() {
+ m := &typeref.MapTypeRef{}
+ Expect(m.Kind()).To(Equal(metadata.TypeRefKindMap))
+ })
+ })
+
+ Context("String representations", func() {
+ It("Builds canonical and simple strings from key/value", func() {
+ keyRef := &utils.FakeTypeRef{CanonicalStr: "pkg.Key", SimpleStr: "Key"}
+ valRef := &utils.FakeTypeRef{CanonicalStr: "pkg.Val", SimpleStr: "Val"}
+
+ m := &typeref.MapTypeRef{Key: keyRef, Value: valRef}
+ Expect(m.CanonicalString()).To(Equal("map[pkg.Key]pkg.Val"))
+ Expect(m.SimpleTypeString()).To(Equal("map[Key]Val"))
+ })
+ })
+
+ Context("ToSymKey / CacheLookupKey", func() {
+ It("Creates composite key when both operands produce keys", func() {
+ keySym := graphs.NewNonUniverseBuiltInSymbolKey("kpkg.K")
+ valSym := graphs.NewNonUniverseBuiltInSymbolKey("vpkg.V")
+ keyRef := &utils.FakeTypeRef{SymKey: keySym}
+ valRef := &utils.FakeTypeRef{SymKey: valSym}
+
+ m := &typeref.MapTypeRef{Key: keyRef, Value: valRef}
+ got, err := m.ToSymKey(fVersion)
+ Expect(err).ToNot(HaveOccurred())
+
+ expected := graphs.NewCompositeTypeKey(graphs.CompositeKindMap, fVersion, []graphs.SymbolKey{keySym, valSym})
+ Expect(got).To(Equal(expected))
+
+ // CacheLookupKey delegates to ToSymKey
+ cacheKey, err := m.CacheLookupKey(fVersion)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(cacheKey).To(Equal(expected))
+ })
+
+ It("Returns error when key ToSymKey fails", func() {
+ keyErr := fmt.Errorf("key-fail")
+ keyRef := &utils.FakeTypeRef{ToSymKeyErr: keyErr}
+ valRef := &utils.FakeTypeRef{SymKey: graphs.NewNonUniverseBuiltInSymbolKey("vpkg.V")}
+
+ m := &typeref.MapTypeRef{Key: keyRef, Value: valRef}
+ _, err := m.ToSymKey(fVersion)
+ Expect(err).To(MatchError(ContainSubstring("key-fail")))
+ })
+
+ It("Returns error when value ToSymKey fails", func() {
+ keyRef := &utils.FakeTypeRef{SymKey: graphs.NewNonUniverseBuiltInSymbolKey("kpkg.K")}
+ valErr := fmt.Errorf("val-fail")
+ valRef := &utils.FakeTypeRef{ToSymKeyErr: valErr}
+
+ m := &typeref.MapTypeRef{Key: keyRef, Value: valRef}
+ _, err := m.ToSymKey(fVersion)
+ Expect(err).To(MatchError(ContainSubstring("val-fail")))
+ })
+ })
+
+ Context("Flatten", func() {
+ It("Returns key and value TypeRefs in order", func() {
+ keyRef := &utils.FakeTypeRef{CanonicalStr: "pkg.Key"}
+ valRef := &utils.FakeTypeRef{CanonicalStr: "pkg.Val"}
+
+ m := &typeref.MapTypeRef{Key: keyRef, Value: valRef}
+ flattened := m.Flatten()
+ Expect(flattened).To(HaveLen(2))
+ Expect(flattened[0]).To(Equal(metadata.TypeRef(keyRef)))
+ Expect(flattened[1]).To(Equal(metadata.TypeRef(valRef)))
+ })
+ })
+})
diff --git a/test/units/metadata/typeref/named.typeref_test.go b/test/units/metadata/typeref/named.typeref_test.go
new file mode 100644
index 0000000..b164a99
--- /dev/null
+++ b/test/units/metadata/typeref/named.typeref_test.go
@@ -0,0 +1,139 @@
+package metadata_test
+
+import (
+ "fmt"
+
+ "github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/core/metadata/typeref"
+ "github.com/gopher-fleece/gleece/gast"
+ "github.com/gopher-fleece/gleece/graphs"
+ "github.com/gopher-fleece/gleece/test/utils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("Unit Tests - NamedTypeRef", func() {
+ var fVersion *gast.FileVersion
+
+ BeforeEach(func() {
+ fVersion = utils.MakeFileVersion("file", "")
+ })
+
+ Context("Kind", func() {
+ It("Returns named kind", func() {
+ named := &typeref.NamedTypeRef{}
+ Expect(named.Kind()).To(Equal(metadata.TypeRefKindNamed))
+ })
+ })
+
+ Context("String representations", func() {
+ It("Builds simple string for plain named ref", func() {
+ baseKey := graphs.NewNonUniverseBuiltInSymbolKey("pkg.Simple")
+ named := &typeref.NamedTypeRef{Key: baseKey}
+ Expect(named.SimpleTypeString()).To(Equal(baseKey.Name))
+ })
+
+ It("Includes type args in simple and canonical strings", func() {
+ baseKey := graphs.NewNonUniverseBuiltInSymbolKey("pkg.Generic")
+ arg1 := &utils.FakeTypeRef{CanonicalStr: "pkg.A", SimpleStr: "A", SymKey: graphs.NewNonUniverseBuiltInSymbolKey("pkg.A")}
+ arg2 := &utils.FakeTypeRef{CanonicalStr: "pkg.B", SimpleStr: "B", SymKey: graphs.NewNonUniverseBuiltInSymbolKey("pkg.B")}
+
+ named := &typeref.NamedTypeRef{
+ Key: baseKey,
+ TypeArgs: []metadata.TypeRef{arg1, arg2},
+ }
+
+ simple := named.SimpleTypeString()
+ Expect(simple).To(Equal("pkg.Generic[A,B]"))
+
+ canonical := named.CanonicalString()
+ // canonicalSymKey may include more structure; ensure base + arg canonical pieces are present
+ Expect(canonical).To(ContainSubstring("pkg.Generic"))
+ Expect(canonical).To(ContainSubstring("pkg.A"))
+ Expect(canonical).To(ContainSubstring("pkg.B"))
+ })
+ })
+
+ Context("ToSymKey and CacheLookupKey", func() {
+ It("Returns base key when no type args", func() {
+ baseKey := graphs.NewNonUniverseBuiltInSymbolKey("pkg.Base")
+ named := &typeref.NamedTypeRef{Key: baseKey}
+
+ got, err := named.ToSymKey(fVersion)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(got).To(Equal(baseKey))
+
+ cacheKey, err := named.CacheLookupKey(fVersion)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(cacheKey).To(Equal(baseKey))
+ })
+
+ It("Builds instantiation key when type args present", func() {
+ baseKey := graphs.NewNonUniverseBuiltInSymbolKey("pkg.G")
+ argKey1 := graphs.NewNonUniverseBuiltInSymbolKey("pkg.Arg1")
+ argKey2 := graphs.NewNonUniverseBuiltInSymbolKey("pkg.Arg2")
+
+ argRef1 := &utils.FakeTypeRef{SymKey: argKey1}
+ argRef2 := &utils.FakeTypeRef{SymKey: argKey2}
+
+ named := &typeref.NamedTypeRef{
+ Key: baseKey,
+ TypeArgs: []metadata.TypeRef{argRef1, argRef2},
+ }
+
+ got, err := named.ToSymKey(fVersion)
+ Expect(err).ToNot(HaveOccurred())
+
+ expected := graphs.NewInstSymbolKey(baseKey, []graphs.SymbolKey{argKey1, argKey2})
+ Expect(got).To(Equal(expected))
+ })
+
+ It("Propagates error if an arg ToSymKey fails", func() {
+ baseKey := graphs.NewNonUniverseBuiltInSymbolKey("pkg.G")
+ argErr := fmt.Errorf("arg-to-key-failed")
+ badArg := &utils.FakeTypeRef{ToSymKeyErr: argErr}
+
+ named := &typeref.NamedTypeRef{
+ Key: baseKey,
+ TypeArgs: []metadata.TypeRef{badArg},
+ }
+
+ _, err := named.ToSymKey(fVersion)
+ Expect(err).To(MatchError(ContainSubstring("arg-to-key-failed")))
+ })
+
+ It("Errors when no base Key but type args exist", func() {
+ argRef := &utils.FakeTypeRef{SymKey: graphs.NewNonUniverseBuiltInSymbolKey("pkg.X")}
+ named := &typeref.NamedTypeRef{
+ Key: graphs.SymbolKey{}, // empty
+ TypeArgs: []metadata.TypeRef{argRef},
+ }
+
+ _, err := named.ToSymKey(fVersion)
+ Expect(err).To(MatchError(ContainSubstring("cannot instantiate named type without base Key")))
+ })
+
+ It("Errors when Key missing and no type args", func() {
+ named := &typeref.NamedTypeRef{
+ Key: graphs.SymbolKey{}, // empty
+ TypeArgs: nil,
+ }
+ _, err := named.ToSymKey(fVersion)
+ Expect(err).To(MatchError(ContainSubstring("named type ref missing Key")))
+ })
+ })
+
+ Context("Flatten", func() {
+ It("Returns the TypeArgs slice (or nil when none)", func() {
+ argRef := &utils.FakeTypeRef{CanonicalStr: "pkg.X"}
+ namedWithArgs := &typeref.NamedTypeRef{Key: graphs.NewNonUniverseBuiltInSymbolKey("pkg.T"), TypeArgs: []metadata.TypeRef{argRef}}
+ flat := namedWithArgs.Flatten()
+ Expect(flat).To(HaveLen(1))
+ Expect(flat[0]).To(Equal(metadata.TypeRef(argRef)))
+
+ namedNoArgs := &typeref.NamedTypeRef{Key: graphs.NewNonUniverseBuiltInSymbolKey("pkg.T")}
+ flat2 := namedNoArgs.Flatten()
+ Expect(flat2).To(BeNil())
+ })
+ })
+})
diff --git a/test/units/metadata/typeref/param.typeref_test.go b/test/units/metadata/typeref/param.typeref_test.go
new file mode 100644
index 0000000..12390bd
--- /dev/null
+++ b/test/units/metadata/typeref/param.typeref_test.go
@@ -0,0 +1,80 @@
+package metadata_test
+
+import (
+ "github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/core/metadata/typeref"
+ "github.com/gopher-fleece/gleece/gast"
+ "github.com/gopher-fleece/gleece/graphs"
+ "github.com/gopher-fleece/gleece/test/utils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("Unit Tests - ParamTypeRef", func() {
+
+ var _ = Describe("ParamTypeRef", func() {
+ Context("Kind", func() {
+ It("Returns TypeRefKindParam", func() {
+ param := &typeref.ParamTypeRef{Name: "T", Index: 0}
+ Expect(param.Kind()).To(Equal(metadata.TypeRefKindParam))
+ })
+ })
+
+ Context("CanonicalString", func() {
+ It("Shows index when Index >= 0", func() {
+ param := &typeref.ParamTypeRef{Name: "T", Index: 2}
+ Expect(param.CanonicalString()).To(Equal("P#2"))
+ })
+
+ It("Shows name when Index is negative", func() {
+ param := &typeref.ParamTypeRef{Name: "T", Index: -1}
+ Expect(param.CanonicalString()).To(Equal("P{T}"))
+ })
+ })
+
+ Context("SimpleTypeString", func() {
+ It("Delegates to canonical representation", func() {
+ param := &typeref.ParamTypeRef{Name: "T", Index: 3}
+ Expect(param.SimpleTypeString()).To(Equal(param.CanonicalString()))
+ })
+ })
+
+ Context("ToSymKey and CacheLookupKey", func() {
+ var fVersion *gast.FileVersion
+
+ BeforeEach(func() {
+ fVersion = utils.MakeFileVersion("file", "")
+ })
+
+ It("Errors when fileVersion is nil", func() {
+ param := &typeref.ParamTypeRef{Name: "T", Index: 1}
+ _, err := param.ToSymKey(nil)
+ Expect(err).To(HaveOccurred())
+ _, err2 := param.CacheLookupKey(nil)
+ Expect(err2).To(HaveOccurred())
+ })
+
+ It("Returns a Param SymbolKey when fileVersion is provided", func() {
+ param := &typeref.ParamTypeRef{Name: "MyT", Index: 5}
+ key, err := param.ToSymKey(fVersion)
+ Expect(err).ToNot(HaveOccurred())
+
+ expected := graphs.NewParamSymbolKey(fVersion, "MyT", 5)
+ Expect(key.Equals(expected)).To(BeTrue())
+
+ // CacheLookupKey delegates to ToSymKey
+ cacheKey, err := param.CacheLookupKey(fVersion)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(cacheKey.Equals(expected)).To(BeTrue())
+ })
+ })
+
+ Context("Flatten", func() {
+ It("Returns nil when flattening a type parameter", func() {
+ param := &typeref.ParamTypeRef{Name: "Z", Index: -1}
+ flat := param.Flatten()
+ Expect(flat).To(BeNil())
+ })
+ })
+ })
+})
diff --git a/test/units/metadata/typeref/ptr.typeref_test.go b/test/units/metadata/typeref/ptr.typeref_test.go
new file mode 100644
index 0000000..72a1501
--- /dev/null
+++ b/test/units/metadata/typeref/ptr.typeref_test.go
@@ -0,0 +1,85 @@
+package metadata_test
+
+import (
+ "errors"
+
+ "github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/core/metadata/typeref"
+ "github.com/gopher-fleece/gleece/gast"
+ "github.com/gopher-fleece/gleece/graphs"
+ "github.com/gopher-fleece/gleece/test/utils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("Unit Tests - PtrTypeRef", func() {
+ var fVersion *gast.FileVersion
+
+ BeforeEach(func() {
+ fVersion = utils.MakeFileVersion("file", "")
+ })
+
+ Context("Kind", func() {
+ It("Returns pointer kind", func() {
+ ptrRef := &typeref.PtrTypeRef{Elem: &utils.FakeTypeRef{}}
+ Expect(ptrRef.Kind()).To(Equal(metadata.TypeRefKindPtr))
+ })
+ })
+
+ Context("String representations", func() {
+ It("Prefixes canonical string with '*' but omits it for simple string", func() {
+ elemRef := &utils.FakeTypeRef{
+ CanonicalStr: "pkg.MyType",
+ SimpleStr: "MyType",
+ }
+ ptrRef := &typeref.PtrTypeRef{Elem: elemRef}
+
+ Expect(ptrRef.CanonicalString()).To(Equal("*pkg.MyType"))
+ Expect(ptrRef.SimpleTypeString()).To(Equal("MyType"))
+ })
+ })
+
+ Context("ToSymKey and CacheLookupKey", func() {
+ It("Builds composite ptr key from element symbol key", func() {
+ elemKey := graphs.NewNonUniverseBuiltInSymbolKey("pkg.Elem")
+ elemRef := &utils.FakeTypeRef{SymKey: elemKey}
+ ptrRef := &typeref.PtrTypeRef{Elem: elemRef}
+
+ gotKey, err := ptrRef.ToSymKey(fVersion)
+ Expect(err).ToNot(HaveOccurred())
+
+ expectedKey := graphs.NewCompositeTypeKey(graphs.CompositeKindPtr, fVersion, []graphs.SymbolKey{elemKey})
+ Expect(gotKey).To(Equal(expectedKey))
+ })
+
+ It("Propagates error when element ToSymKey fails", func() {
+ elemErr := errors.New("elem-to-key-failed")
+ badElem := &utils.FakeTypeRef{ToSymKeyErr: elemErr}
+ ptrRef := &typeref.PtrTypeRef{Elem: badElem}
+
+ _, err := ptrRef.ToSymKey(fVersion)
+ Expect(err).To(MatchError(ContainSubstring("elem-to-key-failed")))
+ })
+
+ It("CacheLookupKey delegates to element ToSymKey (returns element key)", func() {
+ elemKey := graphs.NewNonUniverseBuiltInSymbolKey("pkg.Elem")
+ elemRef := &utils.FakeTypeRef{SymKey: elemKey}
+ ptrRef := &typeref.PtrTypeRef{Elem: elemRef}
+
+ cacheKey, err := ptrRef.CacheLookupKey(fVersion)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(cacheKey).To(Equal(elemKey))
+ })
+ })
+
+ Context("Flatten", func() {
+ It("Returns a single-element slice containing the element TypeRef", func() {
+ elemRef := &utils.FakeTypeRef{CanonicalStr: "pkg.Elem"}
+ ptrRef := &typeref.PtrTypeRef{Elem: elemRef}
+
+ flat := ptrRef.Flatten()
+ Expect(flat).To(HaveLen(1))
+ Expect(flat[0]).To(Equal(metadata.TypeRef(elemRef)))
+ })
+ })
+})
diff --git a/test/units/metadata/typeref/slice.typeref_test.go b/test/units/metadata/typeref/slice.typeref_test.go
new file mode 100644
index 0000000..4152b53
--- /dev/null
+++ b/test/units/metadata/typeref/slice.typeref_test.go
@@ -0,0 +1,85 @@
+package metadata_test
+
+import (
+ "errors"
+
+ "github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/core/metadata/typeref"
+ "github.com/gopher-fleece/gleece/gast"
+ "github.com/gopher-fleece/gleece/graphs"
+ "github.com/gopher-fleece/gleece/test/utils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("Unit Tests - SliceTypeRef", func() {
+ var fVersion *gast.FileVersion
+
+ BeforeEach(func() {
+ fVersion = utils.MakeFileVersion("file", "")
+ })
+
+ Context("Kind", func() {
+ It("Returns slice kind", func() {
+ sliceRef := &typeref.SliceTypeRef{Elem: &utils.FakeTypeRef{}}
+ Expect(sliceRef.Kind()).To(Equal(metadata.TypeRefKindSlice))
+ })
+ })
+
+ Context("String representations", func() {
+ It("Prefixes canonical and simple strings with '[]'", func() {
+ elemRef := &utils.FakeTypeRef{
+ CanonicalStr: "pkg.MyType",
+ SimpleStr: "MyType",
+ }
+ sliceRef := &typeref.SliceTypeRef{Elem: elemRef}
+
+ Expect(sliceRef.CanonicalString()).To(Equal("[]pkg.MyType"))
+ Expect(sliceRef.SimpleTypeString()).To(Equal("[]MyType"))
+ })
+ })
+
+ Context("ToSymKey and CacheLookupKey", func() {
+ It("Builds composite slice key from element symkey", func() {
+ elemKey := graphs.NewNonUniverseBuiltInSymbolKey("pkg.Elem")
+ elemRef := &utils.FakeTypeRef{SymKey: elemKey}
+ sliceRef := &typeref.SliceTypeRef{Elem: elemRef}
+
+ gotKey, err := sliceRef.ToSymKey(fVersion)
+ Expect(err).ToNot(HaveOccurred())
+
+ expectedKey := graphs.NewCompositeTypeKey(graphs.CompositeKindSlice, fVersion, []graphs.SymbolKey{elemKey})
+ Expect(gotKey).To(Equal(expectedKey))
+ })
+
+ It("Propagates error when element ToSymKey fails", func() {
+ elemErr := errors.New("elem-to-key-failed")
+ badElem := &utils.FakeTypeRef{ToSymKeyErr: elemErr}
+ sliceRef := &typeref.SliceTypeRef{Elem: badElem}
+
+ _, err := sliceRef.ToSymKey(fVersion)
+ Expect(err).To(MatchError(ContainSubstring("elem-to-key-failed")))
+ })
+
+ It("CacheLookupKey delegates to element CacheLookupKey", func() {
+ elemKey := graphs.NewNonUniverseBuiltInSymbolKey("pkg.Elem")
+ elemRef := &utils.FakeTypeRef{SymKey: elemKey}
+ sliceRef := &typeref.SliceTypeRef{Elem: elemRef}
+
+ cacheKey, err := sliceRef.CacheLookupKey(fVersion)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(cacheKey).To(Equal(elemKey))
+ })
+ })
+
+ Context("Flatten", func() {
+ It("Returns a single-element slice containing the element TypeRef", func() {
+ elemRef := &utils.FakeTypeRef{CanonicalStr: "pkg.Elem"}
+ sliceRef := &typeref.SliceTypeRef{Elem: elemRef}
+
+ flat := sliceRef.Flatten()
+ Expect(flat).To(HaveLen(1))
+ Expect(flat[0]).To(Equal(metadata.TypeRef(elemRef)))
+ })
+ })
+})
diff --git a/test/units/metadata/typeref/typeref_test.go b/test/units/metadata/typeref/typeref_test.go
new file mode 100644
index 0000000..48d3786
--- /dev/null
+++ b/test/units/metadata/typeref/typeref_test.go
@@ -0,0 +1,15 @@
+package metadata_test
+
+import (
+ "testing"
+
+ "github.com/gopher-fleece/gleece/infrastructure/logger"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+)
+
+func TestUnitTyperef(t *testing.T) {
+ logger.SetLogLevel(logger.LogLevelNone)
+ RegisterFailHandler(Fail)
+ RunSpecs(t, "Unit Tests - TypeRef")
+}
diff --git a/test/units/metadata/typeusage_test.go b/test/units/metadata/typeusage_test.go
new file mode 100644
index 0000000..fbf7fae
--- /dev/null
+++ b/test/units/metadata/typeusage_test.go
@@ -0,0 +1,64 @@
+package metadata_test
+
+import (
+ "go/ast"
+
+ "github.com/gopher-fleece/gleece/common"
+ "github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/core/metadata/typeref"
+ "github.com/gopher-fleece/gleece/test/utils"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+)
+
+// Partial tests for now - a lot of this is covered elsewhere
+var _ = Describe("Unit Tests - TypeUsage", func() {
+ Context("Resolve", func() {
+ When("Usage is a 'Universe' type", func() {
+
+ It("Returns correct metadata for non-pointers", func() {
+ usage := metadata.TypeUsageMeta{
+ SymNodeMeta: metadata.SymNodeMeta{
+ Name: "string",
+ SymbolKind: common.SymKindBuiltin,
+ },
+ Root: utils.MakeUniverseRoot("string"),
+ }
+
+ result, err := usage.Reduce(metadata.ReductionContext{})
+ Expect(err).To(BeNil())
+ Expect(result.Name).To(Equal("string"))
+ Expect(result.Import).To(Equal(common.ImportTypeNone))
+ Expect(result.IsUniverseType).To(BeTrue())
+ Expect(result.IsByAddress).To(BeFalse())
+ Expect(result.SymbolKind).To(Equal(common.SymKindBuiltin))
+ Expect(result.AliasMetadata).To(BeNil())
+ })
+
+ It("Returns correct metadata for pointers", func() {
+ usage := metadata.TypeUsageMeta{
+ SymNodeMeta: metadata.SymNodeMeta{
+ Name: "string",
+ SymbolKind: common.SymKindBuiltin,
+ Node: common.Ptr(ast.StarExpr{
+ Star: 15,
+ X: ast.NewIdent("string"),
+ }),
+ },
+ Root: &typeref.PtrTypeRef{
+ Elem: utils.MakeUniverseRoot("string"),
+ },
+ }
+
+ result, err := usage.Reduce(metadata.ReductionContext{})
+ Expect(err).To(BeNil())
+ Expect(result.Name).To(Equal("string"))
+ Expect(result.Import).To(Equal(common.ImportTypeNone))
+ Expect(result.IsUniverseType).To(BeTrue())
+ Expect(result.IsByAddress).To(BeTrue())
+ Expect(result.SymbolKind).To(Equal(common.SymKindBuiltin))
+ Expect(result.AliasMetadata).To(BeNil())
+ })
+ })
+ })
+})
diff --git a/test/utils/graph.utils.go b/test/utils/graph.utils.go
new file mode 100644
index 0000000..87f82bd
--- /dev/null
+++ b/test/utils/graph.utils.go
@@ -0,0 +1,304 @@
+package utils
+
+import (
+ "fmt"
+ "slices"
+
+ "github.com/gopher-fleece/gleece/common"
+ "github.com/gopher-fleece/gleece/common/linq"
+ "github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/core/metadata/typeref"
+ "github.com/gopher-fleece/gleece/graphs/symboldg"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+)
+
+type ControllerInfo struct {
+ Node *symboldg.SymbolNode
+ Data metadata.ControllerMeta
+}
+
+type ReceiverInfo struct {
+ Node *symboldg.SymbolNode
+ Data metadata.ReceiverMeta
+}
+
+type FuncParamInfo struct {
+ Node *symboldg.SymbolNode
+ Data metadata.FieldMeta
+}
+
+type FuncRetValInfo struct {
+ Node *symboldg.SymbolNode
+ Data metadata.FieldMeta
+}
+
+type ApiEndpointInfo struct {
+ Controller ControllerInfo
+ Receiver ReceiverInfo
+ Params []FuncParamInfo
+ RetVals []FuncRetValInfo
+}
+
+type TypeParamInstantiationInfo struct {
+ Node *symboldg.SymbolNode
+ UsedInIndex int
+}
+
+// MustFindController finds a single controller node and asserts it's present.
+func MustFindController(g symboldg.SymbolGraphBuilder, name string) (*symboldg.SymbolNode, metadata.ControllerMeta) {
+ controllers := g.FindByKind(common.SymKindController)
+
+ for _, controllerNode := range controllers {
+ ctrl, ok := controllerNode.Data.(metadata.ControllerMeta)
+ // Check the cast even for unrelated entities - a wrong value here means
+ // something has gone horribly wrong.
+ Expect(ok).To(BeTrue(), "A controller node had an unexpected Data type")
+ if ctrl.Struct.Name == name {
+ return controllerNode, ctrl
+ }
+ }
+
+ Fail(fmt.Sprintf("Could not locate controller '%s'", name))
+ return nil, metadata.ControllerMeta{} // Appease the compiler
+}
+
+func MustFindControllerReceiver(
+ g symboldg.SymbolGraphBuilder,
+ controllerNode *symboldg.SymbolNode,
+ name string,
+) (*symboldg.SymbolNode, metadata.ReceiverMeta) {
+ recvEdge := MustFindOutgoingEdgeToName(
+ g,
+ controllerNode,
+ []symboldg.SymbolEdgeKind{symboldg.EdgeKindReceiver},
+ name,
+ )
+
+ recvNode := g.Get(recvEdge.Edge.To)
+ Expect(recvNode).ToNot(BeNil())
+
+ recvMeta, isRecv := recvNode.Data.(*metadata.ReceiverMeta)
+ Expect(isRecv).To(BeTrue())
+
+ return recvNode, *recvMeta
+}
+
+// MustFindOutgoingEdgeToName finds an outgoing edge from `from` whose To.Name matches `toName`.
+// kindFilter can be nil to match any kind.
+func MustFindOutgoingEdgeToName(
+ g symboldg.SymbolGraphBuilder,
+ from *symboldg.SymbolNode,
+ kindsFilter []symboldg.SymbolEdgeKind,
+ toName string,
+) symboldg.SymbolEdgeDescriptor {
+ edges := common.MapValues(g.GetEdges(from.Id, kindsFilter))
+
+ found := linq.First(edges, func(e symboldg.SymbolEdgeDescriptor) bool {
+ return e.Edge.To.Name == toName
+ })
+
+ Expect(found).ToNot(BeNil(), "couldn't find outgoing edge to %s", toName)
+ return *found
+}
+
+// CollectParamsAndRetVals returns Param and RetVal edges for a Receiver node.
+//
+// Note that this function will FAIL the test if anything other than Param or RetVal edges are encountered
+func CollectAssertParamsAndRetVals(
+ g symboldg.SymbolGraphBuilder,
+ receiverNode *symboldg.SymbolNode,
+) ([]*symboldg.SymbolNode, []*symboldg.SymbolNode) {
+ paramNodes := []*symboldg.SymbolNode{}
+ retValNodes := []*symboldg.SymbolNode{}
+
+ edges := common.MapValues(g.GetEdges(
+ receiverNode.Id,
+ []symboldg.SymbolEdgeKind{symboldg.EdgeKindParam, symboldg.EdgeKindRetVal},
+ ))
+
+ for _, e := range edges {
+ node := g.Get(e.Edge.To)
+ Expect(node).ToNot(BeNil(), fmt.Sprintf("Could not obtain node with key '%v'", e.Edge.To))
+ switch e.Edge.Kind {
+ case symboldg.EdgeKindParam:
+ paramNodes = append(paramNodes, node)
+ case symboldg.EdgeKindRetVal:
+ retValNodes = append(retValNodes, node)
+ }
+ }
+
+ return paramNodes, retValNodes
+}
+
+func GetSingularChildNode(
+ g symboldg.SymbolGraphBuilder,
+ node *symboldg.SymbolNode,
+ targetEdgeKind symboldg.SymbolEdgeKind,
+) *symboldg.SymbolNode {
+ relevantEdges := common.MapValues(g.GetEdges(node.Id, []symboldg.SymbolEdgeKind{targetEdgeKind}))
+ Expect(relevantEdges).To(HaveLen(1), fmt.Sprintf("Node '%s' has more than one '%v' edges", targetEdgeKind, node.Id.Name))
+
+ target := g.Get(relevantEdges[0].Edge.To)
+ Expect(target).ToNot(BeNil())
+
+ return target
+}
+
+func GetSingularChildTypeNode(g symboldg.SymbolGraphBuilder, node *symboldg.SymbolNode) *symboldg.SymbolNode {
+ return GetSingularChildNode(g, node, symboldg.EdgeKindType)
+}
+
+// MustGetTypeNodeForEdge returns the node the edge points to (convenience).
+func MustGetTypeNodeForEdge(g symboldg.SymbolGraphBuilder, edge symboldg.SymbolEdgeDescriptor) *symboldg.SymbolNode {
+ node := g.Get(edge.Edge.To)
+ Expect(node).ToNot(BeNil())
+ return node
+}
+
+// MustStructMeta converts node.Data to StructMeta and asserts it.
+func MustStructMeta(node *symboldg.SymbolNode) metadata.StructMeta {
+ sm, ok := node.Data.(metadata.StructMeta)
+ Expect(ok).To(BeTrue(), "expected node to contain StructMeta")
+ return sm
+}
+
+// MustFieldMeta converts node.Data to FieldMeta and asserts it.
+func MustFieldMeta(node *symboldg.SymbolNode) metadata.FieldMeta {
+ fm, ok := node.Data.(metadata.FieldMeta)
+ Expect(ok).To(BeTrue(), "expected node to contain FieldMeta")
+ return fm
+}
+
+// MustAliasMeta converts node.Data to AliasMeta and asserts it.
+func MustAliasMeta(node *symboldg.SymbolNode) metadata.AliasMeta {
+ am, ok := node.Data.(metadata.AliasMeta)
+ Expect(ok).To(BeTrue(), "expected node to contain AliasMeta")
+ return am
+}
+
+// AssertFieldIsMap asserts a field exists with given name and that its type is a Map with key/value canonical strings.
+func AssertFieldIsMap(structMeta metadata.StructMeta, fieldName, wantKey, wantValue string) {
+ var f metadata.FieldMeta
+ found := false
+ for _, fld := range structMeta.Fields {
+ if fld.Name == fieldName {
+ f = fld
+ found = true
+ break
+ }
+ }
+ Expect(found).To(BeTrue(), "field %s not found on struct", fieldName)
+ root := f.Type.Root
+ Expect(root.Kind()).To(Equal(metadata.TypeRefKindMap), "expected field %s to be a map type", fieldName)
+ mapRef, ok := root.(*typeref.MapTypeRef)
+ Expect(ok).To(BeTrue(), "map type assertion failed for field %s", fieldName)
+ Expect(mapRef.Key.CanonicalString()).To(Equal(wantKey))
+ Expect(mapRef.Value.CanonicalString()).To(Equal(wantValue))
+}
+
+func AssertGetField(
+ g symboldg.SymbolGraphBuilder,
+ structNode *symboldg.SymbolNode,
+ fieldName string,
+) (*symboldg.SymbolNode, metadata.FieldMeta) {
+ Expect(structNode).ToNot(BeNil())
+ MustStructMeta(structNode)
+
+ edges := common.MapValues(g.GetEdges(structNode.Id, []symboldg.SymbolEdgeKind{symboldg.EdgeKindField}))
+
+ relevantEdge := linq.First(edges, func(edge symboldg.SymbolEdgeDescriptor) bool {
+ return edge.Edge.To.Name == fieldName
+ })
+ Expect(relevantEdge).ToNot(BeNil())
+
+ fieldNode := g.Get(relevantEdge.Edge.To)
+ Expect(fieldNode).ToNot(BeNil())
+ fieldMeta := MustFieldMeta(fieldNode)
+
+ return fieldNode, fieldMeta
+}
+
+func GetApiEndpointHierarchy(
+ g symboldg.SymbolGraphBuilder,
+ controllerName, receiverName string,
+ paramNames []string,
+) ApiEndpointInfo {
+ controllerNode, controllerMeta := MustFindController(g, controllerName)
+ receiverNode, receiverMeta := MustFindControllerReceiver(g, controllerNode, receiverName)
+ paramNodes, retValNodes := CollectAssertParamsAndRetVals(g, receiverNode)
+
+ var fParams []FuncParamInfo
+ if len(paramNames) > 0 {
+ fParams = linq.Map(paramNodes, func(pNode *symboldg.SymbolNode) FuncParamInfo {
+ fMeta, isFMeta := pNode.Data.(metadata.FieldMeta)
+ Expect(isFMeta).To(BeTrue())
+ return FuncParamInfo{Node: pNode, Data: fMeta}
+ })
+
+ fParams = linq.Filter(fParams, func(fpi FuncParamInfo) bool {
+ return slices.Contains(paramNames, fpi.Data.Name)
+ })
+ }
+
+ // If we've missing parameters, length will differ and we fail
+ Expect(fParams).To(HaveLen(len(paramNames)))
+
+ fRetVals := linq.Map(retValNodes, func(pNode *symboldg.SymbolNode) FuncRetValInfo {
+ fMeta, isFMeta := pNode.Data.(metadata.FieldMeta)
+ Expect(isFMeta).To(BeTrue())
+ return FuncRetValInfo{Node: pNode, Data: fMeta}
+ })
+
+ return ApiEndpointInfo{
+ Controller: ControllerInfo{Node: controllerNode, Data: controllerMeta},
+ Receiver: ReceiverInfo{Node: receiverNode, Data: receiverMeta},
+ Params: fParams,
+ RetVals: fRetVals,
+ }
+}
+
+func FollowThroughCompositeToTypeParams(
+ g symboldg.SymbolGraphBuilder,
+ fromNode *symboldg.SymbolNode,
+) []*symboldg.SymbolNode {
+ compositeNodes := g.Children(
+ fromNode,
+ &symboldg.TraversalBehavior{Filtering: symboldg.TraversalFilter{
+ EdgeKinds: []symboldg.SymbolEdgeKind{symboldg.EdgeKindType},
+ }},
+ )
+ Expect(compositeNodes).To(HaveLen(1))
+
+ compNode := compositeNodes[0]
+
+ _, isCompData := compNode.Data.(*metadata.CompositeMeta)
+ Expect(isCompData).To(BeTrue())
+
+ return g.Children(
+ compositeNodes[0],
+ &symboldg.TraversalBehavior{Filtering: symboldg.TraversalFilter{
+ EdgeKinds: []symboldg.SymbolEdgeKind{symboldg.EdgeKindTypeParameter},
+ }},
+ )
+}
+
+func AssertFollowEdgesToNode(
+ g symboldg.SymbolGraphBuilder,
+ startNode *symboldg.SymbolNode,
+ edgeTypeToFollow symboldg.SymbolEdgeKind,
+ targetNodeFilter func(node *symboldg.SymbolNode) bool,
+) *symboldg.SymbolNode {
+ edges := g.GetEdges(startNode.Id, []symboldg.SymbolEdgeKind{edgeTypeToFollow})
+ for _, edgeDesc := range common.MapValues(edges) {
+ node := g.Get(edgeDesc.Edge.To)
+ Expect(node).ToNot(BeNil())
+
+ if targetNodeFilter(node) {
+ return node
+ }
+ }
+
+ Fail("AssertFollowEdgesToNode concluded with no result")
+ return nil
+}
diff --git a/test/utils/matchers/graph.go b/test/utils/matchers/graph.go
new file mode 100644
index 0000000..ba823d3
--- /dev/null
+++ b/test/utils/matchers/graph.go
@@ -0,0 +1,22 @@
+package matchers
+
+import (
+ "github.com/gopher-fleece/gleece/common/linq"
+ "github.com/gopher-fleece/gleece/graphs/symboldg"
+ . "github.com/onsi/gomega"
+ "github.com/onsi/gomega/types"
+)
+
+func MatchNodeIdNames(expected []string) types.GomegaMatcher {
+ return WithTransform(
+ func(nodes []*symboldg.SymbolNode) []string {
+ return linq.Map(
+ nodes,
+ func(node *symboldg.SymbolNode) string {
+ return node.Id.Name
+ },
+ )
+ },
+ ContainElements(expected),
+ )
+}
diff --git a/test/utils/matchers/metadata.go b/test/utils/matchers/metadata.go
new file mode 100644
index 0000000..5954827
--- /dev/null
+++ b/test/utils/matchers/metadata.go
@@ -0,0 +1,25 @@
+package matchers
+
+import (
+ "github.com/gopher-fleece/gleece/core/metadata"
+ . "github.com/onsi/gomega"
+ "github.com/onsi/gomega/types"
+)
+
+type FieldDesc struct {
+ Name string
+ TypeName string
+}
+
+func HaveStructFields(fields []FieldDesc) types.GomegaMatcher {
+ return WithTransform(
+ func(structMeta metadata.StructMeta) []FieldDesc {
+ fields := []FieldDesc{}
+ for _, field := range structMeta.Fields {
+ fields = append(fields, FieldDesc{Name: field.Name, TypeName: field.Type.Name})
+ }
+ return fields
+ },
+ ContainElements(fields),
+ )
+}
diff --git a/test/utils/test.utils.go b/test/utils/test.utils.go
index 34095b9..ee9bb35 100644
--- a/test/utils/test.utils.go
+++ b/test/utils/test.utils.go
@@ -17,16 +17,23 @@ import (
"github.com/gopher-fleece/gleece/core/annotations"
"github.com/gopher-fleece/gleece/core/arbitrators/caching"
"github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/core/metadata/typeref"
"github.com/gopher-fleece/gleece/core/pipeline"
"github.com/gopher-fleece/gleece/core/visitors"
"github.com/gopher-fleece/gleece/core/visitors/providers"
"github.com/gopher-fleece/gleece/definitions"
"github.com/gopher-fleece/gleece/gast"
+ "github.com/gopher-fleece/gleece/graphs"
"github.com/gopher-fleece/gleece/graphs/symboldg"
. "github.com/onsi/ginkgo/v2"
"golang.org/x/tools/go/packages"
)
+type StdTestCtx struct {
+ VisitCtx visitors.VisitContext
+ Orc *visitors.VisitorOrchestrator
+}
+
func GetMetadataByRelativeConfig(relativeConfigPath string) (pipeline.GleeceFlattenedMetadata, error) {
_, meta, err := cmd.GetConfigAndMetadata(
arguments.CliArguments{
@@ -272,14 +279,7 @@ func GetVisitContextByRelativeConfigOrFail(relativeConfigPath string) visitors.V
Fail(fmt.Sprintf("could not load Gleece Config - %v", err))
}
- var globs []string
- if len(config.CommonConfig.ControllerGlobs) > 0 {
- globs = config.CommonConfig.ControllerGlobs
- } else {
- globs = []string{"./*.go", "./**/*.go"}
- }
-
- arbProvider, err := providers.NewArbitrationProvider(globs)
+ arbProvider, err := providers.NewArbitrationProviderFromGleeceConfig(config)
if err != nil {
Fail(fmt.Sprintf("could not create an arbitration provider - %v", err))
}
@@ -291,7 +291,8 @@ func GetVisitContextByRelativeConfigOrFail(relativeConfigPath string) visitors.V
GleeceConfig: config,
ArbitrationProvider: arbProvider,
MetadataCache: metaCache,
- GraphBuilder: &symGraph,
+ Graph: &symGraph,
+ SyncedProvider: common.Ptr(providers.NewSyncedProvider()),
}
}
@@ -399,3 +400,36 @@ func GetMockRetVals(number int) []metadata.FuncReturnValue {
}
return retVals
}
+
+// MakeUniverseRoot is a tiny test helper that builds a NamedTypeRef pointing at a universe type.
+func MakeUniverseRoot(universeName string) *typeref.NamedTypeRef {
+ k := graphs.NewUniverseSymbolKey(universeName)
+ r := typeref.NewNamedTypeRef(&k, nil)
+ return &r
+}
+
+// MakeNonUniverseBuiltinRoot is a tiny test helper that builds a NamedTypeRef pointing at a built-in though non-universe type.
+func MakeNonUniverseBuiltinRoot(typeName string) *typeref.NamedTypeRef {
+ k := graphs.NewNonUniverseBuiltInSymbolKey(typeName)
+ r := typeref.NewNamedTypeRef(&k, nil)
+ return &r
+}
+
+func CreateStdTestCtx(configRelPath string) StdTestCtx {
+ testCtx := StdTestCtx{
+ VisitCtx: GetVisitContextByRelativeConfigOrFail(configRelPath),
+ }
+
+ orc, err := visitors.NewVisitorOrchestrator(&testCtx.VisitCtx)
+ if err != nil {
+ Fail("Failed to create a VisitorOrchestrator for test setup")
+ return testCtx
+ }
+
+ testCtx.Orc = orc
+ if err != nil {
+ Fail("Failed to construct a new controller visitor")
+ }
+
+ return testCtx
+}
diff --git a/test/utils/typref.go b/test/utils/typref.go
new file mode 100644
index 0000000..eac0ded
--- /dev/null
+++ b/test/utils/typref.go
@@ -0,0 +1,30 @@
+package utils
+
+import (
+ "github.com/gopher-fleece/gleece/core/metadata"
+ "github.com/gopher-fleece/gleece/gast"
+ "github.com/gopher-fleece/gleece/graphs"
+)
+
+type FakeTypeRef struct {
+ RefKind metadata.TypeRefKind
+ CanonicalStr string
+ SimpleStr string
+ SymKey graphs.SymbolKey
+ ToSymKeyErr error
+ FlattenResponse []metadata.TypeRef
+}
+
+func (f *FakeTypeRef) Kind() metadata.TypeRefKind { return f.RefKind }
+func (f *FakeTypeRef) CanonicalString() string { return f.CanonicalStr }
+func (f *FakeTypeRef) SimpleTypeString() string { return f.SimpleStr }
+func (f *FakeTypeRef) CacheLookupKey(_ *gast.FileVersion) (graphs.SymbolKey, error) {
+ return f.SymKey, nil
+}
+func (f *FakeTypeRef) ToSymKey(_ *gast.FileVersion) (graphs.SymbolKey, error) {
+ if f.ToSymKeyErr != nil {
+ return graphs.SymbolKey{}, f.ToSymKeyErr
+ }
+ return f.SymKey, nil
+}
+func (f *FakeTypeRef) Flatten() []metadata.TypeRef { return f.FlattenResponse }
diff --git a/test/visitors/controller/controller_test.go b/test/visitors/controller/controller_test.go
index 276e6f1..f25507e 100644
--- a/test/visitors/controller/controller_test.go
+++ b/test/visitors/controller/controller_test.go
@@ -3,31 +3,19 @@ package controller_test
import (
"testing"
- "github.com/gopher-fleece/gleece/core/arbitrators/caching"
"github.com/gopher-fleece/gleece/core/visitors"
- "github.com/gopher-fleece/gleece/core/visitors/providers"
"github.com/gopher-fleece/gleece/definitions"
- "github.com/gopher-fleece/gleece/graphs/symboldg"
"github.com/gopher-fleece/gleece/infrastructure/logger"
+ "github.com/gopher-fleece/gleece/test/utils"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
-const controllerFileRelPath = "./resources/micro.valid.controller.go"
-
-type TestCtx struct {
- arbProvider *providers.ArbitrationProvider
- metaCache *caching.MetadataCache
- symGraph symboldg.SymbolGraph
- visitCtx *visitors.VisitContext
- controllerVisitor *visitors.ControllerVisitor
-}
-
var _ = Describe("ControllerVisitor", func() {
- var ctx TestCtx
+ var ctx utils.StdTestCtx
BeforeEach(func() {
- ctx = createTestCtx([]string{controllerFileRelPath})
+ ctx = utils.CreateStdTestCtx("gleece.test.config.json")
})
Context("NewControllerVisitor", func() {
@@ -56,38 +44,12 @@ var _ = Describe("ControllerVisitor", func() {
Context("GetFormatterDiagnosticStack", func() {
It("Correctly prints the diagnostic stack when empty", func() {
- formattedStack := ctx.controllerVisitor.GetFormattedDiagnosticStack()
+ formattedStack := ctx.Orc.GetFormattedDiagnosticStack()
Expect(formattedStack).To(ContainSubstring(""))
})
})
})
-func createTestCtx(fileGlobs []string) TestCtx {
- ctx := TestCtx{}
-
- // Pass the real controller file so the providers actually load it
- arbProvider, err := providers.NewArbitrationProvider(fileGlobs)
- Expect(err).To(BeNil())
- ctx.arbProvider = arbProvider
-
- // Verify files were properly loaded
- srcFiles := arbProvider.Pkg().GetAllSourceFiles()
- Expect(srcFiles).ToNot(BeEmpty(), "Arbitration provider parsed zero files; check glob and file contents")
-
- // Build the VisitContext and routeVisitor as before using arbProvider
- ctx.metaCache = caching.NewMetadataCache()
- ctx.symGraph = symboldg.NewSymbolGraph()
- ctx.visitCtx = &visitors.VisitContext{
- ArbitrationProvider: arbProvider,
- MetadataCache: ctx.metaCache,
- GraphBuilder: &ctx.symGraph,
- }
-
- ctx.controllerVisitor, err = visitors.NewControllerVisitor(ctx.visitCtx)
- Expect(err).To(BeNil(), "Failed to construct a new controller visitor")
- return ctx
-}
-
func TestControllerVisitor(t *testing.T) {
logger.SetLogLevel(logger.LogLevelNone)
RegisterFailHandler(Fail)
diff --git a/test/visitors/controller/gleece.test.config.json b/test/visitors/controller/gleece.test.config.json
new file mode 100644
index 0000000..1aba1ba
--- /dev/null
+++ b/test/visitors/controller/gleece.test.config.json
@@ -0,0 +1,54 @@
+{
+ "commonConfig": {
+ "controllerGlobs": [
+ "./resources/micro.valid.controller.go"
+ ]
+ },
+ "routesConfig": {
+ "engine": "gin",
+ "outputPath": "./dist/gleece.go",
+ "outputFilePerms": "0644",
+ "authorizationConfig": {
+ "authFileFullPackageName": "github.com/gopher-fleece/gleece/test/fixtures",
+ "enforceSecurityOnAllRoutes": true
+ }
+ },
+ "openapiGeneratorConfig": {
+ "openapi": "3.0.0",
+ "info": {
+ "title": "Sample API",
+ "description": "This is a sample API",
+ "termsOfService": "http://example.com/terms/",
+ "contact": {
+ "name": "API Support",
+ "url": "http://www.example.com/support",
+ "email": "support@example.com"
+ },
+ "license": {
+ "name": "Apache 2.0",
+ "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
+ },
+ "version": "1.0.0"
+ },
+ "baseUrl": "https://api.example.com",
+ "securitySchemes": [
+ {
+ "description": "API Key for accessing the API",
+ "name": "securitySchemaName",
+ "fieldName": "x-header-name",
+ "type": "apiKey",
+ "in": "header"
+ }
+ ],
+ "defaultSecurity": {
+ "name": "sanitySchema",
+ "scopes": [
+ "read",
+ "write"
+ ]
+ },
+ "specGeneratorConfig": {
+ "outputPath": "./dist/swagger.json"
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/visitors/orchestrator/gleece.test.config.json b/test/visitors/orchestrator/gleece.test.config.json
new file mode 100644
index 0000000..1aba1ba
--- /dev/null
+++ b/test/visitors/orchestrator/gleece.test.config.json
@@ -0,0 +1,54 @@
+{
+ "commonConfig": {
+ "controllerGlobs": [
+ "./resources/micro.valid.controller.go"
+ ]
+ },
+ "routesConfig": {
+ "engine": "gin",
+ "outputPath": "./dist/gleece.go",
+ "outputFilePerms": "0644",
+ "authorizationConfig": {
+ "authFileFullPackageName": "github.com/gopher-fleece/gleece/test/fixtures",
+ "enforceSecurityOnAllRoutes": true
+ }
+ },
+ "openapiGeneratorConfig": {
+ "openapi": "3.0.0",
+ "info": {
+ "title": "Sample API",
+ "description": "This is a sample API",
+ "termsOfService": "http://example.com/terms/",
+ "contact": {
+ "name": "API Support",
+ "url": "http://www.example.com/support",
+ "email": "support@example.com"
+ },
+ "license": {
+ "name": "Apache 2.0",
+ "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
+ },
+ "version": "1.0.0"
+ },
+ "baseUrl": "https://api.example.com",
+ "securitySchemes": [
+ {
+ "description": "API Key for accessing the API",
+ "name": "securitySchemaName",
+ "fieldName": "x-header-name",
+ "type": "apiKey",
+ "in": "header"
+ }
+ ],
+ "defaultSecurity": {
+ "name": "sanitySchema",
+ "scopes": [
+ "read",
+ "write"
+ ]
+ },
+ "specGeneratorConfig": {
+ "outputPath": "./dist/swagger.json"
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/visitors/orchestrator/orchestrator_test.go b/test/visitors/orchestrator/orchestrator_test.go
new file mode 100644
index 0000000..312d2c4
--- /dev/null
+++ b/test/visitors/orchestrator/orchestrator_test.go
@@ -0,0 +1,111 @@
+package orchestrator_test
+
+import (
+ "go/ast"
+ "testing"
+
+ "github.com/gopher-fleece/gleece/core/visitors"
+ "github.com/gopher-fleece/gleece/infrastructure/logger"
+ "github.com/gopher-fleece/gleece/test/utils"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("VisitorOrchestrator", func() {
+ const testConfigPath = "./gleece.test.config.json"
+
+ Describe("NewVisitorOrchestrator", func() {
+ Context("When a nil VisitContext is provided", func() {
+ It("Returns an error indicating a nil context was provided", func() {
+ orchestrator, err := visitors.NewVisitorOrchestrator(nil)
+ Expect(orchestrator).To(BeNil())
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring(
+ "nil context was provided to VisitorOrchestrator"))
+ })
+ })
+
+ Context("When a VisitContext missing required fields is provided", func() {
+ It("Returns a joined error listing missing VisitContext fields", func() {
+ emptyVisitContext := visitors.VisitContext{}
+ orchestrator, err := visitors.NewVisitorOrchestrator(&emptyVisitContext)
+ Expect(orchestrator).To(BeNil())
+ Expect(err).To(HaveOccurred())
+
+ errorMessage := err.Error()
+ Expect(errorMessage).To(ContainSubstring("arbitration provider"))
+ Expect(errorMessage).To(ContainSubstring("Gleece Config"))
+ Expect(errorMessage).To(ContainSubstring("graph builder"))
+ Expect(errorMessage).To(ContainSubstring("metadata cache"))
+ Expect(errorMessage).To(ContainSubstring("synchronized provider"))
+ })
+ })
+
+ Context("When a valid VisitContext is provided", func() {
+ It("Constructs the orchestrator without error and exposes expected methods",
+ func() {
+ orchestrator, err := buildOrchestratorFromConfig(testConfigPath)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(orchestrator).NotTo(BeNil())
+
+ fieldVisitor := orchestrator.GetFieldVisitor()
+ Expect(fieldVisitor).NotTo(BeNil())
+
+ allFiles := orchestrator.GetAllSourceFiles()
+ // Ensure the returned value is a slice of *ast.File (may be empty)
+ var sample []*ast.File
+ Expect(allFiles).To(BeAssignableToTypeOf(sample))
+ })
+ })
+ })
+
+ Describe("Visit", func() {
+ Context("When delegating to the internal controller visitor", func() {
+ It("Returns a non-nil ast.Visitor for a basic AST node", func() {
+ orchestrator, err := buildOrchestratorFromConfig(testConfigPath)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(orchestrator).NotTo(BeNil())
+
+ testNode := &ast.Ident{Name: "SampleIdentifier"}
+ returnedVisitor := orchestrator.Visit(testNode)
+ Expect(returnedVisitor).NotTo(BeNil())
+ })
+ })
+ })
+
+ Describe("Diagnostic and error accessors", func() {
+ Context("When inspecting last error and diagnostic stack", func() {
+ It("Returns nil for last error and a string for formatted diagnostic stack",
+ func() {
+ orchestrator, err := buildOrchestratorFromConfig(testConfigPath)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(orchestrator).NotTo(BeNil())
+
+ lastError := orchestrator.GetLastError()
+ Expect(lastError).To(BeNil())
+
+ formattedStack := orchestrator.GetFormattedDiagnosticStack()
+ // Ensure we get a string (may be empty)
+ Expect(formattedStack).To(BeAssignableToTypeOf(""))
+ })
+ })
+ })
+})
+
+// Constructs a valid VisitContext using the project test helper.
+func buildValidVisitContextOrFail(relativeConfigPath string) visitors.VisitContext {
+ return utils.GetVisitContextByRelativeConfigOrFail(relativeConfigPath)
+}
+
+// Builds an orchestrator from a relative config path.
+func buildOrchestratorFromConfig(relativeConfigPath string) (*visitors.VisitorOrchestrator, error) {
+ visitContext := buildValidVisitContextOrFail(relativeConfigPath)
+ return visitors.NewVisitorOrchestrator(&visitContext)
+}
+
+func TestVisitorOrchestrator(t *testing.T) {
+ logger.SetLogLevel(logger.LogLevelNone)
+ RegisterFailHandler(Fail)
+ RunSpecs(t, "VisitorOrchestrator")
+}
diff --git a/test/visitors/route/gleece.test.config.json b/test/visitors/route/gleece.test.config.json
new file mode 100644
index 0000000..1aba1ba
--- /dev/null
+++ b/test/visitors/route/gleece.test.config.json
@@ -0,0 +1,54 @@
+{
+ "commonConfig": {
+ "controllerGlobs": [
+ "./resources/micro.valid.controller.go"
+ ]
+ },
+ "routesConfig": {
+ "engine": "gin",
+ "outputPath": "./dist/gleece.go",
+ "outputFilePerms": "0644",
+ "authorizationConfig": {
+ "authFileFullPackageName": "github.com/gopher-fleece/gleece/test/fixtures",
+ "enforceSecurityOnAllRoutes": true
+ }
+ },
+ "openapiGeneratorConfig": {
+ "openapi": "3.0.0",
+ "info": {
+ "title": "Sample API",
+ "description": "This is a sample API",
+ "termsOfService": "http://example.com/terms/",
+ "contact": {
+ "name": "API Support",
+ "url": "http://www.example.com/support",
+ "email": "support@example.com"
+ },
+ "license": {
+ "name": "Apache 2.0",
+ "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
+ },
+ "version": "1.0.0"
+ },
+ "baseUrl": "https://api.example.com",
+ "securitySchemes": [
+ {
+ "description": "API Key for accessing the API",
+ "name": "securitySchemaName",
+ "fieldName": "x-header-name",
+ "type": "apiKey",
+ "in": "header"
+ }
+ ],
+ "defaultSecurity": {
+ "name": "sanitySchema",
+ "scopes": [
+ "read",
+ "write"
+ ]
+ },
+ "specGeneratorConfig": {
+ "outputPath": "./dist/swagger.json"
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/visitors/route/gleece.test.config.pointing.to.locked.json b/test/visitors/route/gleece.test.config.pointing.to.locked.json
new file mode 100644
index 0000000..85bfec2
--- /dev/null
+++ b/test/visitors/route/gleece.test.config.pointing.to.locked.json
@@ -0,0 +1,54 @@
+{
+ "commonConfig": {
+ "controllerGlobs": [
+ "./temp/locked.controller.go"
+ ]
+ },
+ "routesConfig": {
+ "engine": "gin",
+ "outputPath": "./dist/gleece.go",
+ "outputFilePerms": "0644",
+ "authorizationConfig": {
+ "authFileFullPackageName": "github.com/gopher-fleece/gleece/test/fixtures",
+ "enforceSecurityOnAllRoutes": true
+ }
+ },
+ "openapiGeneratorConfig": {
+ "openapi": "3.0.0",
+ "info": {
+ "title": "Sample API",
+ "description": "This is a sample API",
+ "termsOfService": "http://example.com/terms/",
+ "contact": {
+ "name": "API Support",
+ "url": "http://www.example.com/support",
+ "email": "support@example.com"
+ },
+ "license": {
+ "name": "Apache 2.0",
+ "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
+ },
+ "version": "1.0.0"
+ },
+ "baseUrl": "https://api.example.com",
+ "securitySchemes": [
+ {
+ "description": "API Key for accessing the API",
+ "name": "securitySchemaName",
+ "fieldName": "x-header-name",
+ "type": "apiKey",
+ "in": "header"
+ }
+ ],
+ "defaultSecurity": {
+ "name": "sanitySchema",
+ "scopes": [
+ "read",
+ "write"
+ ]
+ },
+ "specGeneratorConfig": {
+ "outputPath": "./dist/swagger.json"
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/visitors/route/route.visitor_test.go b/test/visitors/route/route.visitor_test.go
index 2dd8f0f..2a81e18 100644
--- a/test/visitors/route/route.visitor_test.go
+++ b/test/visitors/route/route.visitor_test.go
@@ -8,13 +8,9 @@ import (
"testing"
"time"
- "github.com/gopher-fleece/gleece/core/arbitrators/caching"
"github.com/gopher-fleece/gleece/core/metadata"
"github.com/gopher-fleece/gleece/core/visitors"
- "github.com/gopher-fleece/gleece/core/visitors/providers"
- "github.com/gopher-fleece/gleece/definitions"
"github.com/gopher-fleece/gleece/gast"
- "github.com/gopher-fleece/gleece/graphs/symboldg"
"github.com/gopher-fleece/gleece/infrastructure/logger"
"github.com/gopher-fleece/gleece/test/utils"
"github.com/titanous/json5"
@@ -23,30 +19,27 @@ import (
. "github.com/onsi/gomega"
)
-const controllerFileRelPath = "./resources/micro.valid.controller.go"
+const configRelPath = "gleece.test.config.json"
const controllerName = "RouteVisitorTestController"
const receiver1Name = "Receiver1"
const receiver2Name = "Receiver2"
const receiver3Name = "Receiver3"
-type TestCtx struct {
+type ExtendedTestCtx struct {
+ utils.StdTestCtx
+
controllerAstFile *ast.File
receiver1Decl *ast.FuncDecl
receiver2Decl *ast.FuncDecl
receiver3Decl *ast.FuncDecl
-
- arbProvider *providers.ArbitrationProvider
- metaCache *caching.MetadataCache
- symGraph symboldg.SymbolGraph
- visitCtx *visitors.VisitContext
- parentCtx visitors.RouteParentContext
- routeVisitor *visitors.RouteVisitor
+ parentCtx visitors.RouteParentContext
+ routeVisitor *visitors.RouteVisitor
}
var _ = Describe("RouteVisitor", func() {
- var ctx TestCtx
+ var ctx ExtendedTestCtx
BeforeEach(func() {
- ctx = createTestCtx([]string{controllerFileRelPath})
+ ctx = createTestCtx(configRelPath)
})
Context("NewRouteVisitor", func() {
@@ -54,17 +47,6 @@ var _ = Describe("RouteVisitor", func() {
_, err := visitors.NewRouteVisitor(nil, ctx.parentCtx)
Expect(err).To(MatchError(ContainSubstring("nil context was given to contextInitGuard")))
})
-
- It("Returns an error if nested TypeVisitor initialization fails", func() {
- ctx.visitCtx.ArbitrationProvider = nil
- ctx.visitCtx.GleeceConfig = &definitions.GleeceConfig{
- CommonConfig: definitions.CommonConfig{
- ControllerGlobs: []string{"./non.go.file.txt"},
- },
- }
- _, err := visitors.NewRouteVisitor(ctx.visitCtx, ctx.parentCtx)
- Expect(err).To(MatchError(ContainSubstring("failed to parse file")))
- })
})
Context("VisitMethod", func() {
@@ -212,7 +194,7 @@ var _ = Describe("RouteVisitor", func() {
// Next, copy the original controller file to a temp file - we'll be locking it and we don't want
// to potentially affect other tests
- originalControllerFile := utils.ReadFileByRelativePathOrFail(controllerFileRelPath)
+ originalControllerFile := utils.ReadFileByRelativePathOrFail("resources/micro.valid.controller.go")
tempFile := "./temp/locked.controller.go"
utils.WriteFileByRelativePathOrFail(tempFile, []byte(originalControllerFile))
@@ -220,7 +202,7 @@ var _ = Describe("RouteVisitor", func() {
defer utils.DeleteRelativeFolderOrFail(tempDir)
// Create a new context for this specific test
- thisCtx := createTestCtx([]string{tempFile})
+ thisCtx := createTestCtx("gleece.test.config.pointing.to.locked.json")
// Chmod the temp file so FileVersion's hash calculation fails downstream
err := os.Chmod(tempFile, 0)
@@ -239,43 +221,37 @@ var _ = Describe("RouteVisitor", func() {
It("Returns a proper error if a receiver parameter has an invalid type", func() {
_, err := ctx.routeVisitor.VisitMethod(ctx.receiver2Decl, ctx.controllerAstFile)
Expect(err).To(MatchError(ContainSubstring(
- "could not create type usage metadata for field paramWithInvalidType - " +
- "failed to build type layers for expression with type name 'string' - " +
- "unsupported type expression: *ast.ChanType",
+ "cannot visit field type expression for field/s [paramWithInvalidType] - " +
+ "unsupported type expression '*ast.ChanType'",
)))
})
It("Returns a proper error if a receiver return value has ann invalid type", func() {
_, err := ctx.routeVisitor.VisitMethod(ctx.receiver3Decl, ctx.controllerAstFile)
Expect(err).To(MatchError(ContainSubstring(
- "could not create type usage metadata for field string - " +
- "failed to build type layers for expression with type name 'string' - " +
- "unsupported type expression: *ast.ChanType",
+ "cannot visit field type expression for an anonymous field - " +
+ "unsupported type expression '*ast.ChanType'",
)))
})
It("Returns a proper error if a receiver return value has ann invalid type", func() {
_, err := ctx.routeVisitor.VisitMethod(ctx.receiver3Decl, ctx.controllerAstFile)
Expect(err).To(MatchError(ContainSubstring(
- "could not create type usage metadata for field string - " +
- "failed to build type layers for expression with type name 'string' - " +
- "unsupported type expression: *ast.ChanType",
+ "cannot visit field type expression for an anonymous field - " +
+ "unsupported type expression '*ast.ChanType'",
)))
})
})
})
-func createTestCtx(fileGlobs []string) TestCtx {
- ctx := TestCtx{}
-
- // Pass the real controller file so the providers actually load it
- arbProvider, err := providers.NewArbitrationProvider(fileGlobs)
- Expect(err).To(BeNil())
- ctx.arbProvider = arbProvider
+func createTestCtx(configRelPath string) ExtendedTestCtx {
+ ctx := ExtendedTestCtx{
+ StdTestCtx: utils.CreateStdTestCtx(configRelPath),
+ }
// Verify files were properly loaded
- srcFiles := arbProvider.Pkg().GetAllSourceFiles()
+ srcFiles := ctx.VisitCtx.ArbitrationProvider.Pkg().GetAllSourceFiles()
Expect(srcFiles).ToNot(BeEmpty(), "Arbitration provider parsed zero files; check glob and file contents")
// Get the controller's source file so we can use it directly with the visitor
@@ -300,15 +276,6 @@ func createTestCtx(fileGlobs []string) TestCtx {
Expect(ctx.controllerAstFile).ToNot(BeNil(), fmt.Sprintf("Expected to find %s in parsed AST", receiver1Name))
Expect(ctx.receiver1Decl).ToNot(BeNil(), fmt.Sprintf("Expected to find %s func in parsed AST", receiver1Name))
- // Build the VisitContext and routeVisitor as before using arbProvider
- ctx.metaCache = caching.NewMetadataCache()
- ctx.symGraph = symboldg.NewSymbolGraph()
- ctx.visitCtx = &visitors.VisitContext{
- ArbitrationProvider: arbProvider,
- MetadataCache: ctx.metaCache,
- GraphBuilder: &ctx.symGraph,
- }
-
ctx.parentCtx = visitors.RouteParentContext{
Controller: &metadata.ControllerMeta{
Struct: metadata.StructMeta{
@@ -320,9 +287,15 @@ func createTestCtx(fileGlobs []string) TestCtx {
},
}
- ctx.routeVisitor, err = visitors.NewRouteVisitor(ctx.visitCtx, ctx.parentCtx)
+ rVisitor, err := visitors.NewRouteVisitorFromVisitor(
+ &ctx.VisitCtx,
+ ctx.parentCtx,
+ ctx.Orc.GetFieldVisitor(),
+ )
+
Expect(err).To(BeNil())
- Expect(ctx.routeVisitor).ToNot(BeNil())
+ Expect(rVisitor).ToNot(BeNil())
+ ctx.routeVisitor = rVisitor
return ctx
}