1136 lines
31 KiB
Go
1136 lines
31 KiB
Go
package validator
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"cuelang.org/go/cue"
|
|
"cuelang.org/go/cue/errors"
|
|
|
|
"github.com/marte-community/marte-dev-tools/internal/index"
|
|
"github.com/marte-community/marte-dev-tools/internal/parser"
|
|
"github.com/marte-community/marte-dev-tools/internal/schema"
|
|
)
|
|
|
|
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
|
|
Schema *schema.Schema
|
|
}
|
|
|
|
func NewValidator(tree *index.ProjectTree, projectRoot string) *Validator {
|
|
return &Validator{
|
|
Tree: tree,
|
|
Schema: schema.LoadFullSchema(projectRoot),
|
|
}
|
|
}
|
|
|
|
func (v *Validator) ValidateProject() {
|
|
if v.Tree == nil {
|
|
return
|
|
}
|
|
// Ensure references are resolved (if not already done by builder/lsp)
|
|
v.Tree.ResolveReferences()
|
|
|
|
if v.Tree.Root != nil {
|
|
v.validateNode(v.Tree.Root)
|
|
}
|
|
for _, node := range v.Tree.IsolatedFiles {
|
|
v.validateNode(node)
|
|
}
|
|
v.CheckUnused()
|
|
v.CheckDataSourceThreading()
|
|
v.CheckINOUTOrdering()
|
|
v.CheckVariables()
|
|
v.CheckUnresolvedVariables()
|
|
}
|
|
|
|
func (v *Validator) validateNode(node *index.ProjectNode) {
|
|
// Check for invalid content in Signals container of DataSource
|
|
if node.RealName == "Signals" && node.Parent != nil && isDataSource(node.Parent) {
|
|
for _, frag := range node.Fragments {
|
|
for _, def := range frag.Definitions {
|
|
if f, ok := def.(*parser.Field); ok {
|
|
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
|
Level: LevelError,
|
|
Message: fmt.Sprintf("Invalid content in Signals container: Field '%s' is not allowed. Only Signal objects are allowed.", f.Name),
|
|
Position: f.Position,
|
|
File: frag.File,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fields := v.getFields(node)
|
|
|
|
// 1. Check for duplicate fields (Go logic)
|
|
for name, defs := range fields {
|
|
if len(defs) > 1 {
|
|
firstFile := v.getFileForField(defs[0], node)
|
|
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
|
Level: LevelError,
|
|
Message: fmt.Sprintf("Duplicate Field Definition: '%s' is already defined in %s", name, firstFile),
|
|
Position: defs[1].Position,
|
|
File: v.getFileForField(defs[1], node),
|
|
})
|
|
}
|
|
}
|
|
|
|
// 2. Check for mandatory Class if it's an object node (+/$)
|
|
className := ""
|
|
if node.RealName != "" && (node.RealName[0] == '+' || node.RealName[0] == '$') {
|
|
if classFields, ok := fields["Class"]; ok && len(classFields) > 0 {
|
|
className = v.getFieldValue(classFields[0], node)
|
|
}
|
|
|
|
hasType := false
|
|
if _, ok := fields["Type"]; ok {
|
|
hasType = true
|
|
}
|
|
|
|
if className == "" && !hasType {
|
|
pos := v.getNodePosition(node)
|
|
file := v.getNodeFile(node)
|
|
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,
|
|
})
|
|
}
|
|
|
|
if className == "RealTimeThread" {
|
|
v.checkFunctionsArray(node, fields)
|
|
}
|
|
}
|
|
|
|
// 3. CUE Validation
|
|
if className != "" && v.Schema != nil {
|
|
v.validateWithCUE(node, className)
|
|
}
|
|
|
|
// 4. Signal Validation (for DataSource signals)
|
|
if isSignal(node) {
|
|
v.validateSignal(node, fields)
|
|
}
|
|
|
|
// 5. GAM Validation (Signal references)
|
|
if isGAM(node) {
|
|
v.validateGAM(node)
|
|
}
|
|
|
|
// Recursively validate children
|
|
for _, child := range node.Children {
|
|
v.validateNode(child)
|
|
}
|
|
}
|
|
|
|
func (v *Validator) validateWithCUE(node *index.ProjectNode, className string) {
|
|
// Check if class exists in schema
|
|
classPath := cue.ParsePath(fmt.Sprintf("#Classes.%s", className))
|
|
if v.Schema.Value.LookupPath(classPath).Err() != nil {
|
|
return // Unknown class, skip validation
|
|
}
|
|
|
|
// Convert node to map
|
|
data := v.nodeToMap(node)
|
|
|
|
// Encode data to CUE
|
|
dataVal := v.Schema.Context.Encode(data)
|
|
|
|
// Unify with #Object
|
|
// #Object requires "Class" field, which is present in data.
|
|
objDef := v.Schema.Value.LookupPath(cue.ParsePath("#Object"))
|
|
|
|
// Unify
|
|
res := objDef.Unify(dataVal)
|
|
|
|
if err := res.Validate(cue.Concrete(true)); err != nil {
|
|
// Report errors
|
|
|
|
// Parse CUE error to diagnostic
|
|
v.reportCUEError(err, node)
|
|
}
|
|
}
|
|
|
|
func (v *Validator) reportCUEError(err error, node *index.ProjectNode) {
|
|
list := errors.Errors(err)
|
|
for _, e := range list {
|
|
msg := e.Error()
|
|
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
|
Level: LevelError,
|
|
Message: fmt.Sprintf("Schema Validation Error: %v", msg),
|
|
Position: v.getNodePosition(node),
|
|
File: v.getNodeFile(node),
|
|
})
|
|
}
|
|
}
|
|
|
|
func (v *Validator) nodeToMap(node *index.ProjectNode) map[string]interface{} {
|
|
m := make(map[string]interface{})
|
|
fields := v.getFields(node)
|
|
|
|
for name, defs := range fields {
|
|
if len(defs) > 0 {
|
|
// Use the last definition (duplicates checked elsewhere)
|
|
m[name] = v.valueToInterface(defs[len(defs)-1].Value, node)
|
|
}
|
|
}
|
|
|
|
// Children as nested maps?
|
|
// CUE schema expects nested structs for "node" type fields.
|
|
// But `node.Children` contains ALL children (even those defined as +Child).
|
|
// If schema expects `States: { ... }`, we map children.
|
|
|
|
for name, child := range node.Children {
|
|
// normalize name? CUE keys are strings.
|
|
// If child real name is "+States", key in Children is "States".
|
|
// We use "States" as key in map.
|
|
m[name] = v.nodeToMap(child)
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
func (v *Validator) valueToInterface(val parser.Value, ctx *index.ProjectNode) interface{} {
|
|
switch t := val.(type) {
|
|
case *parser.StringValue:
|
|
return t.Value
|
|
case *parser.IntValue:
|
|
i, _ := strconv.ParseInt(t.Raw, 0, 64)
|
|
return i
|
|
case *parser.FloatValue:
|
|
f, _ := strconv.ParseFloat(t.Raw, 64)
|
|
return f
|
|
case *parser.BoolValue:
|
|
return t.Value
|
|
case *parser.ReferenceValue:
|
|
return t.Value
|
|
case *parser.VariableReferenceValue:
|
|
name := strings.TrimPrefix(t.Name, "@")
|
|
if info := v.Tree.ResolveVariable(ctx, name); info != nil {
|
|
if info.Def.DefaultValue != nil {
|
|
return v.valueToInterface(info.Def.DefaultValue, ctx)
|
|
}
|
|
}
|
|
return nil
|
|
case *parser.ArrayValue:
|
|
var arr []interface{}
|
|
for _, e := range t.Elements {
|
|
arr = append(arr, v.valueToInterface(e, ctx))
|
|
}
|
|
return arr
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (v *Validator) validateSignal(node *index.ProjectNode, fields map[string][]*parser.Field) {
|
|
// ... (same as before)
|
|
if typeFields, ok := fields["Type"]; !ok || len(typeFields) == 0 {
|
|
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
|
Level: LevelError,
|
|
Message: fmt.Sprintf("Signal '%s' is missing mandatory field 'Type'", node.RealName),
|
|
Position: v.getNodePosition(node),
|
|
File: v.getNodeFile(node),
|
|
})
|
|
} else {
|
|
typeVal := typeFields[0].Value
|
|
var typeStr string
|
|
switch t := typeVal.(type) {
|
|
case *parser.StringValue:
|
|
typeStr = t.Value
|
|
case *parser.ReferenceValue:
|
|
typeStr = t.Value
|
|
default:
|
|
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
|
Level: LevelError,
|
|
Message: fmt.Sprintf("Field 'Type' in Signal '%s' must be a type name", node.RealName),
|
|
Position: typeFields[0].Position,
|
|
File: v.getFileForField(typeFields[0], node),
|
|
})
|
|
return
|
|
}
|
|
|
|
if !isValidType(typeStr) {
|
|
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
|
Level: LevelError,
|
|
Message: fmt.Sprintf("Invalid Type '%s' for Signal '%s'", typeStr, node.RealName),
|
|
Position: typeFields[0].Position,
|
|
File: v.getFileForField(typeFields[0], node),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func (v *Validator) validateGAM(node *index.ProjectNode) {
|
|
if inputs, ok := node.Children["InputSignals"]; ok {
|
|
v.validateGAMSignals(node, inputs, "Input")
|
|
}
|
|
if outputs, ok := node.Children["OutputSignals"]; ok {
|
|
v.validateGAMSignals(node, outputs, "Output")
|
|
}
|
|
}
|
|
|
|
func (v *Validator) validateGAMSignals(gamNode, signalsContainer *index.ProjectNode, direction string) {
|
|
for _, signal := range signalsContainer.Children {
|
|
v.validateGAMSignal(gamNode, signal, direction)
|
|
}
|
|
}
|
|
|
|
func (v *Validator) validateGAMSignal(gamNode, signalNode *index.ProjectNode, direction string) {
|
|
fields := v.getFields(signalNode)
|
|
var dsName string
|
|
if dsFields, ok := fields["DataSource"]; ok && len(dsFields) > 0 {
|
|
dsName = v.getFieldValue(dsFields[0], signalNode)
|
|
}
|
|
|
|
if dsName == "" {
|
|
return // Ignore implicit signals or missing datasource (handled elsewhere if mandatory)
|
|
}
|
|
|
|
dsNode := v.resolveReference(dsName, v.getNodeFile(signalNode), isDataSource)
|
|
if dsNode == nil {
|
|
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
|
Level: LevelError,
|
|
Message: fmt.Sprintf("Unknown DataSource '%s' referenced in signal '%s'", dsName, signalNode.RealName),
|
|
Position: v.getNodePosition(signalNode),
|
|
File: v.getNodeFile(signalNode),
|
|
})
|
|
return
|
|
}
|
|
|
|
// Link DataSource reference
|
|
if dsFields, ok := fields["DataSource"]; ok && len(dsFields) > 0 {
|
|
if val, ok := dsFields[0].Value.(*parser.ReferenceValue); ok {
|
|
v.updateReferenceTarget(v.getNodeFile(signalNode), val.Position, dsNode)
|
|
}
|
|
}
|
|
|
|
// Check Direction using CUE Schema
|
|
dsClass := v.getNodeClass(dsNode)
|
|
if dsClass != "" {
|
|
// Lookup class definition in Schema
|
|
// path: #Classes.ClassName.#meta.direction
|
|
path := cue.ParsePath(fmt.Sprintf("#Classes.%s.#meta.direction", dsClass))
|
|
val := v.Schema.Value.LookupPath(path)
|
|
|
|
if val.Err() == nil {
|
|
dsDir, err := val.String()
|
|
if err == nil && dsDir != "" {
|
|
if direction == "Input" && dsDir == "OUT" {
|
|
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
|
Level: LevelError,
|
|
Message: fmt.Sprintf("DataSource '%s' (Class %s) is Output-only but referenced in InputSignals of GAM '%s'", dsName, dsClass, gamNode.RealName),
|
|
Position: v.getNodePosition(signalNode),
|
|
File: v.getNodeFile(signalNode),
|
|
})
|
|
}
|
|
if direction == "Output" && dsDir == "IN" {
|
|
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
|
Level: LevelError,
|
|
Message: fmt.Sprintf("DataSource '%s' (Class %s) is Input-only but referenced in OutputSignals of GAM '%s'", dsName, dsClass, gamNode.RealName),
|
|
Position: v.getNodePosition(signalNode),
|
|
File: v.getNodeFile(signalNode),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check Signal Existence
|
|
targetSignalName := index.NormalizeName(signalNode.RealName)
|
|
if aliasFields, ok := fields["Alias"]; ok && len(aliasFields) > 0 {
|
|
targetSignalName = v.getFieldValue(aliasFields[0], signalNode) // Alias is usually the name in DataSource
|
|
}
|
|
|
|
var targetNode *index.ProjectNode
|
|
if signalsContainer, ok := dsNode.Children["Signals"]; ok {
|
|
targetNorm := index.NormalizeName(targetSignalName)
|
|
|
|
if child, ok := signalsContainer.Children[targetNorm]; ok {
|
|
targetNode = child
|
|
} else {
|
|
// Fallback check
|
|
for _, child := range signalsContainer.Children {
|
|
if index.NormalizeName(child.RealName) == targetNorm {
|
|
targetNode = child
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if targetNode == nil {
|
|
suppressed := v.isGloballyAllowed("implicit", v.getNodeFile(signalNode))
|
|
if !suppressed {
|
|
for _, p := range signalNode.Pragmas {
|
|
if strings.HasPrefix(p, "implicit:") || strings.HasPrefix(p, "ignore(implicit)") {
|
|
suppressed = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if !suppressed {
|
|
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
|
Level: LevelWarning,
|
|
Message: fmt.Sprintf("Implicitly Defined Signal: '%s' is defined in GAM '%s' but not in DataSource '%s'", targetSignalName, gamNode.RealName, dsName),
|
|
Position: v.getNodePosition(signalNode),
|
|
File: v.getNodeFile(signalNode),
|
|
})
|
|
}
|
|
|
|
if typeFields, ok := fields["Type"]; !ok || len(typeFields) == 0 {
|
|
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
|
Level: LevelError,
|
|
Message: fmt.Sprintf("Implicit signal '%s' must define Type", targetSignalName),
|
|
Position: v.getNodePosition(signalNode),
|
|
File: v.getNodeFile(signalNode),
|
|
})
|
|
} else {
|
|
// Check Type validity even for implicit
|
|
typeVal := v.getFieldValue(typeFields[0], signalNode)
|
|
if !isValidType(typeVal) {
|
|
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
|
Level: LevelError,
|
|
Message: fmt.Sprintf("Invalid Type '%s' for Signal '%s'", typeVal, signalNode.RealName),
|
|
Position: typeFields[0].Position,
|
|
File: v.getNodeFile(signalNode),
|
|
})
|
|
}
|
|
}
|
|
} else {
|
|
signalNode.Target = targetNode
|
|
// Link Alias reference
|
|
if aliasFields, ok := fields["Alias"]; ok && len(aliasFields) > 0 {
|
|
if val, ok := aliasFields[0].Value.(*parser.ReferenceValue); ok {
|
|
v.updateReferenceTarget(v.getNodeFile(signalNode), val.Position, targetNode)
|
|
}
|
|
}
|
|
|
|
// Property checks
|
|
v.checkSignalProperty(signalNode, targetNode, "Type")
|
|
v.checkSignalProperty(signalNode, targetNode, "NumberOfElements")
|
|
v.checkSignalProperty(signalNode, targetNode, "NumberOfDimensions")
|
|
|
|
// Check Type validity if present
|
|
if typeFields, ok := fields["Type"]; ok && len(typeFields) > 0 {
|
|
typeVal := v.getFieldValue(typeFields[0], signalNode)
|
|
if !isValidType(typeVal) {
|
|
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
|
Level: LevelError,
|
|
Message: fmt.Sprintf("Invalid Type '%s' for Signal '%s'", typeVal, signalNode.RealName),
|
|
Position: typeFields[0].Position,
|
|
File: v.getNodeFile(signalNode),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (v *Validator) checkSignalProperty(gamSig, dsSig *index.ProjectNode, prop string) {
|
|
gamVal := gamSig.Metadata[prop]
|
|
dsVal := dsSig.Metadata[prop]
|
|
|
|
if gamVal == "" {
|
|
return
|
|
}
|
|
|
|
if dsVal != "" && gamVal != dsVal {
|
|
if prop == "Type" {
|
|
if v.checkCastPragma(gamSig, dsVal, gamVal) {
|
|
return
|
|
}
|
|
}
|
|
|
|
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
|
Level: LevelError,
|
|
Message: fmt.Sprintf("Signal '%s' property '%s' mismatch: defined '%s', referenced '%s'", gamSig.RealName, prop, dsVal, gamVal),
|
|
Position: v.getNodePosition(gamSig),
|
|
File: v.getNodeFile(gamSig),
|
|
})
|
|
}
|
|
}
|
|
|
|
func (v *Validator) checkCastPragma(node *index.ProjectNode, defType, curType string) bool {
|
|
for _, p := range node.Pragmas {
|
|
if strings.HasPrefix(p, "cast(") {
|
|
content := strings.TrimPrefix(p, "cast(")
|
|
if idx := strings.Index(content, ")"); idx != -1 {
|
|
content = content[:idx]
|
|
parts := strings.Split(content, ",")
|
|
if len(parts) == 2 {
|
|
d := strings.TrimSpace(parts[0])
|
|
c := strings.TrimSpace(parts[1])
|
|
if d == defType && c == curType {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (v *Validator) updateReferenceTarget(file string, pos parser.Position, target *index.ProjectNode) {
|
|
for i := range v.Tree.References {
|
|
ref := &v.Tree.References[i]
|
|
if ref.File == file && ref.Position == pos {
|
|
ref.Target = target
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helpers
|
|
|
|
func (v *Validator) getFields(node *index.ProjectNode) map[string][]*parser.Field {
|
|
fields := make(map[string][]*parser.Field)
|
|
for _, frag := range node.Fragments {
|
|
for _, def := range frag.Definitions {
|
|
if f, ok := def.(*parser.Field); ok {
|
|
fields[f.Name] = append(fields[f.Name], f)
|
|
}
|
|
}
|
|
}
|
|
return fields
|
|
}
|
|
|
|
func (v *Validator) getFieldValue(f *parser.Field, ctx *index.ProjectNode) string {
|
|
switch val := f.Value.(type) {
|
|
case *parser.StringValue:
|
|
return val.Value
|
|
case *parser.ReferenceValue:
|
|
return val.Value
|
|
case *parser.IntValue:
|
|
return val.Raw
|
|
case *parser.FloatValue:
|
|
return val.Raw
|
|
case *parser.BoolValue:
|
|
return strconv.FormatBool(val.Value)
|
|
case *parser.VariableReferenceValue:
|
|
name := strings.TrimPrefix(val.Name, "@")
|
|
if info := v.Tree.ResolveVariable(ctx, name); info != nil {
|
|
if info.Def.DefaultValue != nil {
|
|
return v.getFieldValue(&parser.Field{Value: info.Def.DefaultValue}, ctx)
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (v *Validator) resolveReference(name string, file string, predicate func(*index.ProjectNode) bool) *index.ProjectNode {
|
|
if isoNode, ok := v.Tree.IsolatedFiles[file]; ok {
|
|
if found := v.Tree.FindNode(isoNode, name, predicate); found != nil {
|
|
return found
|
|
}
|
|
return nil
|
|
}
|
|
if v.Tree.Root == nil {
|
|
return nil
|
|
}
|
|
return v.Tree.FindNode(v.Tree.Root, name, predicate)
|
|
}
|
|
|
|
func (v *Validator) getNodeClass(node *index.ProjectNode) string {
|
|
if cls, ok := node.Metadata["Class"]; ok {
|
|
return cls
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func isValidType(t string) bool {
|
|
switch t {
|
|
case "uint8", "int8", "uint16", "int16", "uint32", "int32", "uint64", "int64",
|
|
"float32", "float64", "string", "bool", "char8":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (v *Validator) getFileForField(f *parser.Field, node *index.ProjectNode) string {
|
|
for _, frag := range node.Fragments {
|
|
for _, def := range frag.Definitions {
|
|
if def == f {
|
|
return frag.File
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (v *Validator) CheckUnused() {
|
|
referencedNodes := make(map[*index.ProjectNode]bool)
|
|
for _, ref := range v.Tree.References {
|
|
if ref.Target != nil {
|
|
referencedNodes[ref.Target] = true
|
|
}
|
|
}
|
|
|
|
if v.Tree.Root != nil {
|
|
v.collectTargetUsage(v.Tree.Root, referencedNodes)
|
|
}
|
|
for _, node := range v.Tree.IsolatedFiles {
|
|
v.collectTargetUsage(node, referencedNodes)
|
|
}
|
|
|
|
if v.Tree.Root != nil {
|
|
v.checkUnusedRecursive(v.Tree.Root, referencedNodes)
|
|
}
|
|
for _, node := range v.Tree.IsolatedFiles {
|
|
v.checkUnusedRecursive(node, referencedNodes)
|
|
}
|
|
}
|
|
|
|
func (v *Validator) collectTargetUsage(node *index.ProjectNode, referenced map[*index.ProjectNode]bool) {
|
|
if node.Target != nil {
|
|
referenced[node.Target] = true
|
|
}
|
|
for _, child := range node.Children {
|
|
v.collectTargetUsage(child, referenced)
|
|
}
|
|
}
|
|
|
|
func (v *Validator) checkUnusedRecursive(node *index.ProjectNode, referenced map[*index.ProjectNode]bool) {
|
|
// Heuristic for GAM
|
|
if isGAM(node) {
|
|
if !referenced[node] {
|
|
suppress := v.isGloballyAllowed("unused", v.getNodeFile(node))
|
|
if !suppress {
|
|
for _, p := range node.Pragmas {
|
|
if strings.HasPrefix(p, "unused:") || strings.HasPrefix(p, "ignore(unused)") {
|
|
suppress = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if !suppress {
|
|
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) {
|
|
if signalsNode, ok := node.Children["Signals"]; ok {
|
|
for _, signal := range signalsNode.Children {
|
|
if !referenced[signal] {
|
|
if v.isGloballyAllowed("unused", v.getNodeFile(signal)) {
|
|
continue
|
|
}
|
|
suppress := false
|
|
for _, p := range signal.Pragmas {
|
|
if strings.HasPrefix(p, "unused:") || strings.HasPrefix(p, "ignore(unused)") {
|
|
suppress = true
|
|
break
|
|
}
|
|
}
|
|
if !suppress {
|
|
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
|
|
}
|
|
_, hasSignals := node.Children["Signals"]
|
|
return hasSignals
|
|
}
|
|
|
|
func isSignal(node *index.ProjectNode) bool {
|
|
if node.Parent != nil && node.Parent.Name == "Signals" {
|
|
if isDataSource(node.Parent.Parent) {
|
|
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 ""
|
|
}
|
|
|
|
func (v *Validator) checkFunctionsArray(node *index.ProjectNode, fields map[string][]*parser.Field) {
|
|
if funcs, ok := fields["Functions"]; ok && len(funcs) > 0 {
|
|
f := funcs[0]
|
|
if arr, ok := f.Value.(*parser.ArrayValue); ok {
|
|
for _, elem := range arr.Elements {
|
|
if ref, ok := elem.(*parser.ReferenceValue); ok {
|
|
target := v.resolveReference(ref.Value, v.getNodeFile(node), isGAM)
|
|
if target == nil {
|
|
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
|
Level: LevelError,
|
|
Message: fmt.Sprintf("Function '%s' not found or is not a valid GAM", ref.Value),
|
|
Position: ref.Position,
|
|
File: v.getNodeFile(node),
|
|
})
|
|
}
|
|
} else {
|
|
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
|
Level: LevelError,
|
|
Message: "Functions array must contain references",
|
|
Position: f.Position,
|
|
File: v.getNodeFile(node),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (v *Validator) isGloballyAllowed(warningType string, contextFile string) bool {
|
|
prefix1 := fmt.Sprintf("allow(%s)", warningType)
|
|
prefix2 := fmt.Sprintf("ignore(%s)", warningType)
|
|
|
|
// If context file is isolated, only check its own pragmas
|
|
if _, isIsolated := v.Tree.IsolatedFiles[contextFile]; isIsolated {
|
|
if pragmas, ok := v.Tree.GlobalPragmas[contextFile]; ok {
|
|
for _, p := range pragmas {
|
|
normalized := strings.ReplaceAll(p, " ", "")
|
|
if strings.HasPrefix(normalized, prefix1) || strings.HasPrefix(normalized, prefix2) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// If project file, check all non-isolated files
|
|
for file, pragmas := range v.Tree.GlobalPragmas {
|
|
if _, isIsolated := v.Tree.IsolatedFiles[file]; isIsolated {
|
|
continue
|
|
}
|
|
for _, p := range pragmas {
|
|
normalized := strings.ReplaceAll(p, " ", "")
|
|
if strings.HasPrefix(normalized, prefix1) || strings.HasPrefix(normalized, prefix2) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (v *Validator) CheckDataSourceThreading() {
|
|
if v.Tree.Root == nil {
|
|
return
|
|
}
|
|
|
|
// 1. Find RealTimeApplication
|
|
var appNode *index.ProjectNode
|
|
findApp := func(n *index.ProjectNode) {
|
|
if cls, ok := n.Metadata["Class"]; ok && cls == "RealTimeApplication" {
|
|
appNode = n
|
|
}
|
|
}
|
|
v.Tree.Walk(findApp)
|
|
|
|
if appNode == nil {
|
|
return
|
|
}
|
|
|
|
// 2. Find States
|
|
var statesNode *index.ProjectNode
|
|
if s, ok := appNode.Children["States"]; ok {
|
|
statesNode = s
|
|
} else {
|
|
for _, child := range appNode.Children {
|
|
if cls, ok := child.Metadata["Class"]; ok && cls == "StateMachine" {
|
|
statesNode = child
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if statesNode == nil {
|
|
return
|
|
}
|
|
|
|
// 3. Iterate States
|
|
for _, state := range statesNode.Children {
|
|
dsUsage := make(map[*index.ProjectNode]string) // DS Node -> Thread Name
|
|
var threads []*index.ProjectNode
|
|
|
|
// Search for threads in the state (either direct children or inside "Threads" container)
|
|
for _, child := range state.Children {
|
|
if child.RealName == "Threads" {
|
|
for _, t := range child.Children {
|
|
if cls, ok := t.Metadata["Class"]; ok && cls == "RealTimeThread" {
|
|
threads = append(threads, t)
|
|
}
|
|
}
|
|
} else {
|
|
if cls, ok := child.Metadata["Class"]; ok && cls == "RealTimeThread" {
|
|
threads = append(threads, child)
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, thread := range threads {
|
|
gams := v.getThreadGAMs(thread)
|
|
for _, gam := range gams {
|
|
dss := v.getGAMDataSources(gam)
|
|
for _, ds := range dss {
|
|
if existingThread, ok := dsUsage[ds]; ok {
|
|
if existingThread != thread.RealName {
|
|
if !v.isMultithreaded(ds) {
|
|
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
|
Level: LevelError,
|
|
Message: fmt.Sprintf("DataSource '%s' is not multithreaded but used in multiple threads (%s, %s) in state '%s'", ds.RealName, existingThread, thread.RealName, state.RealName),
|
|
Position: v.getNodePosition(gam),
|
|
File: v.getNodeFile(gam),
|
|
})
|
|
}
|
|
}
|
|
} else {
|
|
dsUsage[ds] = thread.RealName
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (v *Validator) getThreadGAMs(thread *index.ProjectNode) []*index.ProjectNode {
|
|
var gams []*index.ProjectNode
|
|
fields := v.getFields(thread)
|
|
if funcs, ok := fields["Functions"]; ok && len(funcs) > 0 {
|
|
f := funcs[0]
|
|
if arr, ok := f.Value.(*parser.ArrayValue); ok {
|
|
for _, elem := range arr.Elements {
|
|
if ref, ok := elem.(*parser.ReferenceValue); ok {
|
|
target := v.resolveReference(ref.Value, v.getNodeFile(thread), isGAM)
|
|
if target != nil {
|
|
gams = append(gams, target)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return gams
|
|
}
|
|
|
|
func (v *Validator) getGAMDataSources(gam *index.ProjectNode) []*index.ProjectNode {
|
|
dsMap := make(map[*index.ProjectNode]bool)
|
|
|
|
processSignals := func(container *index.ProjectNode) {
|
|
if container == nil {
|
|
return
|
|
}
|
|
for _, sig := range container.Children {
|
|
fields := v.getFields(sig)
|
|
if dsFields, ok := fields["DataSource"]; ok && len(dsFields) > 0 {
|
|
dsName := v.getFieldValue(dsFields[0], sig)
|
|
dsNode := v.resolveReference(dsName, v.getNodeFile(sig), isDataSource)
|
|
if dsNode != nil {
|
|
dsMap[dsNode] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
processSignals(gam.Children["InputSignals"])
|
|
processSignals(gam.Children["OutputSignals"])
|
|
|
|
var dss []*index.ProjectNode
|
|
for ds := range dsMap {
|
|
dss = append(dss, ds)
|
|
}
|
|
return dss
|
|
}
|
|
|
|
func (v *Validator) isMultithreaded(ds *index.ProjectNode) bool {
|
|
if meta, ok := ds.Children["#meta"]; ok {
|
|
fields := v.getFields(meta)
|
|
if mt, ok := fields["multithreaded"]; ok && len(mt) > 0 {
|
|
val := v.getFieldValue(mt[0], meta)
|
|
return val == "true"
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (v *Validator) CheckINOUTOrdering() {
|
|
if v.Tree.Root == nil {
|
|
return
|
|
}
|
|
|
|
var appNode *index.ProjectNode
|
|
findApp := func(n *index.ProjectNode) {
|
|
if cls, ok := n.Metadata["Class"]; ok && cls == "RealTimeApplication" {
|
|
appNode = n
|
|
}
|
|
}
|
|
v.Tree.Walk(findApp)
|
|
|
|
if appNode == nil {
|
|
return
|
|
}
|
|
|
|
var statesNode *index.ProjectNode
|
|
if s, ok := appNode.Children["States"]; ok {
|
|
statesNode = s
|
|
} else {
|
|
for _, child := range appNode.Children {
|
|
if cls, ok := child.Metadata["Class"]; ok && cls == "StateMachine" {
|
|
statesNode = child
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if statesNode == nil {
|
|
return
|
|
}
|
|
|
|
for _, state := range statesNode.Children {
|
|
var threads []*index.ProjectNode
|
|
for _, child := range state.Children {
|
|
if child.RealName == "Threads" {
|
|
for _, t := range child.Children {
|
|
if cls, ok := t.Metadata["Class"]; ok && cls == "RealTimeThread" {
|
|
threads = append(threads, t)
|
|
}
|
|
}
|
|
} else {
|
|
if cls, ok := child.Metadata["Class"]; ok && cls == "RealTimeThread" {
|
|
threads = append(threads, child)
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, thread := range threads {
|
|
producedSignals := make(map[*index.ProjectNode]map[string][]*index.ProjectNode)
|
|
consumedSignals := make(map[*index.ProjectNode]map[string]bool)
|
|
|
|
gams := v.getThreadGAMs(thread)
|
|
for _, gam := range gams {
|
|
v.processGAMSignalsForOrdering(gam, "InputSignals", producedSignals, consumedSignals, true, thread, state)
|
|
v.processGAMSignalsForOrdering(gam, "OutputSignals", producedSignals, consumedSignals, false, thread, state)
|
|
}
|
|
|
|
// Check for produced but not consumed
|
|
for ds, signals := range producedSignals {
|
|
for sigName, producers := range signals {
|
|
consumed := false
|
|
if cSet, ok := consumedSignals[ds]; ok {
|
|
if cSet[sigName] {
|
|
consumed = true
|
|
}
|
|
}
|
|
if !consumed {
|
|
for _, prod := range producers {
|
|
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
|
Level: LevelWarning,
|
|
Message: fmt.Sprintf("INOUT Signal '%s' (DS '%s') is produced in thread '%s' but never consumed in the same thread.", sigName, ds.RealName, thread.RealName),
|
|
Position: v.getNodePosition(prod),
|
|
File: v.getNodeFile(prod),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (v *Validator) processGAMSignalsForOrdering(gam *index.ProjectNode, containerName string, produced map[*index.ProjectNode]map[string][]*index.ProjectNode, consumed map[*index.ProjectNode]map[string]bool, isInput bool, thread, state *index.ProjectNode) {
|
|
container := gam.Children[containerName]
|
|
if container == nil {
|
|
return
|
|
}
|
|
|
|
for _, sig := range container.Children {
|
|
fields := v.getFields(sig)
|
|
var dsNode *index.ProjectNode
|
|
var sigName string
|
|
|
|
if sig.Target != nil {
|
|
if sig.Target.Parent != nil && sig.Target.Parent.Parent != nil {
|
|
dsNode = sig.Target.Parent.Parent
|
|
sigName = sig.Target.RealName
|
|
}
|
|
}
|
|
|
|
if dsNode == nil {
|
|
if dsFields, ok := fields["DataSource"]; ok && len(dsFields) > 0 {
|
|
dsName := v.getFieldValue(dsFields[0], sig)
|
|
dsNode = v.resolveReference(dsName, v.getNodeFile(sig), isDataSource)
|
|
}
|
|
if aliasFields, ok := fields["Alias"]; ok && len(aliasFields) > 0 {
|
|
sigName = v.getFieldValue(aliasFields[0], sig)
|
|
} else {
|
|
sigName = sig.RealName
|
|
}
|
|
}
|
|
|
|
if dsNode == nil || sigName == "" {
|
|
continue
|
|
}
|
|
|
|
sigName = index.NormalizeName(sigName)
|
|
|
|
if v.isMultithreaded(dsNode) {
|
|
continue
|
|
}
|
|
|
|
dir := v.getDataSourceDirection(dsNode)
|
|
if dir != "INOUT" {
|
|
continue
|
|
}
|
|
|
|
if isInput {
|
|
isProduced := false
|
|
if set, ok := produced[dsNode]; ok {
|
|
if len(set[sigName]) > 0 {
|
|
isProduced = true
|
|
}
|
|
}
|
|
|
|
if !isProduced {
|
|
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
|
Level: LevelError,
|
|
Message: fmt.Sprintf("INOUT Signal '%s' (DS '%s') is consumed by GAM '%s' in thread '%s' (State '%s') before being produced by any previous GAM.", sigName, dsNode.RealName, gam.RealName, thread.RealName, state.RealName),
|
|
Position: v.getNodePosition(sig),
|
|
File: v.getNodeFile(sig),
|
|
})
|
|
}
|
|
|
|
if consumed[dsNode] == nil {
|
|
consumed[dsNode] = make(map[string]bool)
|
|
}
|
|
consumed[dsNode][sigName] = true
|
|
} else {
|
|
if produced[dsNode] == nil {
|
|
produced[dsNode] = make(map[string][]*index.ProjectNode)
|
|
}
|
|
produced[dsNode][sigName] = append(produced[dsNode][sigName], sig)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (v *Validator) getDataSourceDirection(ds *index.ProjectNode) string {
|
|
cls := v.getNodeClass(ds)
|
|
if cls == "" {
|
|
return ""
|
|
}
|
|
if v.Schema == nil {
|
|
return ""
|
|
}
|
|
path := cue.ParsePath(fmt.Sprintf("#Classes.%s.#meta.direction", cls))
|
|
val := v.Schema.Value.LookupPath(path)
|
|
if val.Err() == nil {
|
|
s, _ := val.String()
|
|
return s
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (v *Validator) CheckVariables() {
|
|
if v.Schema == nil {
|
|
return
|
|
}
|
|
ctx := v.Schema.Context
|
|
|
|
checkNodeVars := func(node *index.ProjectNode) {
|
|
for _, info := range node.Variables {
|
|
def := info.Def
|
|
|
|
// Compile Type
|
|
typeVal := ctx.CompileString(def.TypeExpr)
|
|
if typeVal.Err() != nil {
|
|
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
|
Level: LevelError,
|
|
Message: fmt.Sprintf("Invalid type expression for variable '%s': %v", def.Name, typeVal.Err()),
|
|
Position: def.Position,
|
|
File: info.File,
|
|
})
|
|
continue
|
|
}
|
|
|
|
if def.DefaultValue != nil {
|
|
valInterface := v.valueToInterface(def.DefaultValue, node)
|
|
valVal := ctx.Encode(valInterface)
|
|
|
|
// Unify
|
|
res := typeVal.Unify(valVal)
|
|
if err := res.Validate(cue.Concrete(true)); err != nil {
|
|
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
|
Level: LevelError,
|
|
Message: fmt.Sprintf("Variable '%s' value mismatch: %v", def.Name, err),
|
|
Position: def.Position,
|
|
File: info.File,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
v.Tree.Walk(checkNodeVars)
|
|
}
|
|
func (v *Validator) CheckUnresolvedVariables() {
|
|
for _, ref := range v.Tree.References {
|
|
if ref.IsVariable && ref.TargetVariable == nil {
|
|
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
|
Level: LevelError,
|
|
Message: fmt.Sprintf("Unresolved variable reference: '@%s'", ref.Name),
|
|
Position: ref.Position,
|
|
File: ref.File,
|
|
})
|
|
}
|
|
}
|
|
}
|