175 lines
4.3 KiB
Go
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 ""
|
|
} |