Files
marte_dev_tools/internal/validator/validator.go
2026-01-21 10:16:54 +01:00

175 lines
4.3 KiB
Go

package validator
import (
"fmt"
"github.com/marte-dev/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser"
)
type DiagnosticLevel int
const (
LevelError DiagnosticLevel = iota
LevelWarning
)
type Diagnostic struct {
Level DiagnosticLevel
Message string
Position parser.Position
File string
}
type Validator struct {
Diagnostics []Diagnostic
Tree *index.ProjectTree
}
func NewValidator(tree *index.ProjectTree) *Validator {
return &Validator{Tree: tree}
}
func (v *Validator) ValidateProject() {
if v.Tree == nil || v.Tree.Root == nil {
return
}
v.validateNode(v.Tree.Root)
}
func (v *Validator) validateNode(node *index.ProjectNode) {
// Check for duplicate fields in this node
fields := make(map[string]string) // FieldName -> File
for _, frag := range node.Fragments {
for _, def := range frag.Definitions {
if f, ok := def.(*parser.Field); ok {
if existingFile, exists := fields[f.Name]; exists {
// Duplicate field
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
Message: fmt.Sprintf("Duplicate Field Definition: '%s' is already defined in %s", f.Name, existingFile),
Position: f.Position,
File: frag.File,
})
} else {
fields[f.Name] = frag.File
}
}
}
}
// Check for mandatory Class if it's an object node (+/$)
if node.RealName != "" && (node.RealName[0] == '+' || node.RealName[0] == '$') {
hasClass := false
hasType := false
for _, frag := range node.Fragments {
for _, def := range frag.Definitions {
if f, ok := def.(*parser.Field); ok {
if f.Name == "Class" {
hasClass = true
}
if f.Name == "Type" {
hasType = true
}
}
}
if hasClass {
break
}
}
if !hasClass && !hasType {
// Report error on the first fragment's position
pos := parser.Position{Line: 1, Column: 1}
file := ""
if len(node.Fragments) > 0 {
pos = node.Fragments[0].ObjectPos
file = node.Fragments[0].File
}
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
Message: fmt.Sprintf("Node %s is an object and must contain a 'Class' field (or be a Signal with 'Type')", node.RealName),
Position: pos,
File: file,
})
}
}
// Recursively validate children
for _, child := range node.Children {
v.validateNode(child)
}
}
func (v *Validator) CheckUnused() {
referencedNodes := make(map[*index.ProjectNode]bool)
for _, ref := range v.Tree.References {
if ref.Target != nil {
referencedNodes[ref.Target] = true
}
}
v.checkUnusedRecursive(v.Tree.Root, referencedNodes)
}
func (v *Validator) checkUnusedRecursive(node *index.ProjectNode, referenced map[*index.ProjectNode]bool) {
// Heuristic for GAM
if isGAM(node) {
if !referenced[node] {
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelWarning,
Message: fmt.Sprintf("Unused GAM: %s is defined but not referenced in any thread or scheduler", node.RealName),
Position: v.getNodePosition(node),
File: v.getNodeFile(node),
})
}
}
// Heuristic for DataSource and its signals
if isDataSource(node) {
for _, signal := range node.Children {
if !referenced[signal] {
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelWarning,
Message: fmt.Sprintf("Unused Signal: %s is defined in DataSource %s but never referenced", signal.RealName, node.RealName),
Position: v.getNodePosition(signal),
File: v.getNodeFile(signal),
})
}
}
}
for _, child := range node.Children {
v.checkUnusedRecursive(child, referenced)
}
}
func isGAM(node *index.ProjectNode) bool {
if node.RealName == "" || (node.RealName[0] != '+' && node.RealName[0] != '$') {
return false
}
_, hasInput := node.Children["InputSignals"]
_, hasOutput := node.Children["OutputSignals"]
return hasInput || hasOutput
}
func isDataSource(node *index.ProjectNode) bool {
if node.Parent != nil && node.Parent.Name == "Data" {
return true
}
return false
}
func (v *Validator) getNodePosition(node *index.ProjectNode) parser.Position {
if len(node.Fragments) > 0 {
return node.Fragments[0].ObjectPos
}
return parser.Position{Line: 1, Column: 1}
}
func (v *Validator) getNodeFile(node *index.ProjectNode) string {
if len(node.Fragments) > 0 {
return node.Fragments[0].File
}
return ""
}