Compare commits
10 Commits
970b5697bd
...
1ea518a58a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ea518a58a | ||
|
|
0654062d08 | ||
|
|
a88f833f49 | ||
|
|
b2e963fc04 | ||
|
|
8fe319de2d | ||
|
|
93d48bd3ed | ||
|
|
164dad896c | ||
|
|
f111bf1aaa | ||
|
|
4a624aa929 | ||
|
|
5b0834137b |
27
examples/pragma_test.marte
Normal file
27
examples/pragma_test.marte
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
//!allow(unused): Ignore unused GAMs in this file
|
||||||
|
//!allow(implicit): Ignore implicit signals in this file
|
||||||
|
|
||||||
|
+Data = {
|
||||||
|
Class = ReferenceContainer
|
||||||
|
+MyDS = {
|
||||||
|
Class = FileReader
|
||||||
|
Filename = "test"
|
||||||
|
Signals = {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
+MyGAM = {
|
||||||
|
Class = IOGAM
|
||||||
|
InputSignals = {
|
||||||
|
// Implicit signal (not in MyDS)
|
||||||
|
ImplicitSig = {
|
||||||
|
DataSource = MyDS
|
||||||
|
Type = uint32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unused GAM
|
||||||
|
+UnusedGAM = {
|
||||||
|
Class = IOGAM
|
||||||
|
}
|
||||||
6416
examples/test_app.marte
Normal file
6416
examples/test_app.marte
Normal file
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@ type ProjectTree struct {
|
|||||||
Root *ProjectNode
|
Root *ProjectNode
|
||||||
References []Reference
|
References []Reference
|
||||||
IsolatedFiles map[string]*ProjectNode
|
IsolatedFiles map[string]*ProjectNode
|
||||||
|
GlobalPragmas map[string][]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pt *ProjectTree) ScanDirectory(rootPath string) error {
|
func (pt *ProjectTree) ScanDirectory(rootPath string) error {
|
||||||
@@ -50,6 +51,8 @@ type ProjectNode struct {
|
|||||||
Children map[string]*ProjectNode
|
Children map[string]*ProjectNode
|
||||||
Parent *ProjectNode
|
Parent *ProjectNode
|
||||||
Metadata map[string]string // Store extra info like Class, Type, Size
|
Metadata map[string]string // Store extra info like Class, Type, Size
|
||||||
|
Target *ProjectNode // Points to referenced node (for Direct References/Links)
|
||||||
|
Pragmas []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Fragment struct {
|
type Fragment struct {
|
||||||
@@ -57,6 +60,7 @@ type Fragment struct {
|
|||||||
Definitions []parser.Definition
|
Definitions []parser.Definition
|
||||||
IsObject bool
|
IsObject bool
|
||||||
ObjectPos parser.Position
|
ObjectPos parser.Position
|
||||||
|
EndPos parser.Position
|
||||||
Doc string // Documentation for this fragment (if object)
|
Doc string // Documentation for this fragment (if object)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +71,7 @@ func NewProjectTree() *ProjectTree {
|
|||||||
Metadata: make(map[string]string),
|
Metadata: make(map[string]string),
|
||||||
},
|
},
|
||||||
IsolatedFiles: make(map[string]*ProjectNode),
|
IsolatedFiles: make(map[string]*ProjectNode),
|
||||||
|
GlobalPragmas: make(map[string][]string),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +92,7 @@ func (pt *ProjectTree) RemoveFile(file string) {
|
|||||||
pt.References = newRefs
|
pt.References = newRefs
|
||||||
|
|
||||||
delete(pt.IsolatedFiles, file)
|
delete(pt.IsolatedFiles, file)
|
||||||
|
delete(pt.GlobalPragmas, file)
|
||||||
pt.removeFileFromNode(pt.Root, file)
|
pt.removeFileFromNode(pt.Root, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,6 +160,15 @@ func (pt *ProjectTree) extractFieldMetadata(node *ProjectNode, f *parser.Field)
|
|||||||
func (pt *ProjectTree) AddFile(file string, config *parser.Configuration) {
|
func (pt *ProjectTree) AddFile(file string, config *parser.Configuration) {
|
||||||
pt.RemoveFile(file)
|
pt.RemoveFile(file)
|
||||||
|
|
||||||
|
// Collect global pragmas
|
||||||
|
for _, p := range config.Pragmas {
|
||||||
|
txt := strings.TrimSpace(strings.TrimPrefix(p.Text, "//!"))
|
||||||
|
normalized := strings.ReplaceAll(txt, " ", "")
|
||||||
|
if strings.HasPrefix(normalized, "allow(") || strings.HasPrefix(normalized, "ignore(") {
|
||||||
|
pt.GlobalPragmas[file] = append(pt.GlobalPragmas[file], txt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if config.Package == nil {
|
if config.Package == nil {
|
||||||
node := &ProjectNode{
|
node := &ProjectNode{
|
||||||
Children: make(map[string]*ProjectNode),
|
Children: make(map[string]*ProjectNode),
|
||||||
@@ -200,6 +215,7 @@ func (pt *ProjectTree) populateNode(node *ProjectNode, file string, config *pars
|
|||||||
|
|
||||||
for _, def := range config.Definitions {
|
for _, def := range config.Definitions {
|
||||||
doc := pt.findDoc(config.Comments, def.Pos())
|
doc := pt.findDoc(config.Comments, def.Pos())
|
||||||
|
pragmas := pt.findPragmas(config.Pragmas, def.Pos())
|
||||||
|
|
||||||
switch d := def.(type) {
|
switch d := def.(type) {
|
||||||
case *parser.Field:
|
case *parser.Field:
|
||||||
@@ -228,7 +244,11 @@ func (pt *ProjectTree) populateNode(node *ProjectNode, file string, config *pars
|
|||||||
child.Doc += doc
|
child.Doc += doc
|
||||||
}
|
}
|
||||||
|
|
||||||
pt.addObjectFragment(child, file, d, doc, config.Comments)
|
if len(pragmas) > 0 {
|
||||||
|
child.Pragmas = append(child.Pragmas, pragmas...)
|
||||||
|
}
|
||||||
|
|
||||||
|
pt.addObjectFragment(child, file, d, doc, config.Comments, config.Pragmas)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,16 +257,18 @@ func (pt *ProjectTree) populateNode(node *ProjectNode, file string, config *pars
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pt *ProjectTree) addObjectFragment(node *ProjectNode, file string, obj *parser.ObjectNode, doc string, comments []parser.Comment) {
|
func (pt *ProjectTree) addObjectFragment(node *ProjectNode, file string, obj *parser.ObjectNode, doc string, comments []parser.Comment, pragmas []parser.Pragma) {
|
||||||
frag := &Fragment{
|
frag := &Fragment{
|
||||||
File: file,
|
File: file,
|
||||||
IsObject: true,
|
IsObject: true,
|
||||||
ObjectPos: obj.Position,
|
ObjectPos: obj.Position,
|
||||||
|
EndPos: obj.Subnode.EndPosition,
|
||||||
Doc: doc,
|
Doc: doc,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, def := range obj.Subnode.Definitions {
|
for _, def := range obj.Subnode.Definitions {
|
||||||
subDoc := pt.findDoc(comments, def.Pos())
|
subDoc := pt.findDoc(comments, def.Pos())
|
||||||
|
subPragmas := pt.findPragmas(pragmas, def.Pos())
|
||||||
|
|
||||||
switch d := def.(type) {
|
switch d := def.(type) {
|
||||||
case *parser.Field:
|
case *parser.Field:
|
||||||
@@ -276,7 +298,11 @@ func (pt *ProjectTree) addObjectFragment(node *ProjectNode, file string, obj *pa
|
|||||||
child.Doc += subDoc
|
child.Doc += subDoc
|
||||||
}
|
}
|
||||||
|
|
||||||
pt.addObjectFragment(child, file, d, subDoc, comments)
|
if len(subPragmas) > 0 {
|
||||||
|
child.Pragmas = append(child.Pragmas, subPragmas...)
|
||||||
|
}
|
||||||
|
|
||||||
|
pt.addObjectFragment(child, file, d, subDoc, comments, pragmas)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,6 +347,30 @@ func (pt *ProjectTree) findDoc(comments []parser.Comment, pos parser.Position) s
|
|||||||
return docBuilder.String()
|
return docBuilder.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pt *ProjectTree) findPragmas(pragmas []parser.Pragma, pos parser.Position) []string {
|
||||||
|
var found []string
|
||||||
|
targetLine := pos.Line - 1
|
||||||
|
|
||||||
|
for i := len(pragmas) - 1; i >= 0; i-- {
|
||||||
|
p := pragmas[i]
|
||||||
|
if p.Position.Line > pos.Line {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if p.Position.Line == pos.Line {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Position.Line == targetLine {
|
||||||
|
txt := strings.TrimSpace(strings.TrimPrefix(p.Text, "//!"))
|
||||||
|
found = append(found, txt)
|
||||||
|
targetLine--
|
||||||
|
} else if p.Position.Line < targetLine {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
func (pt *ProjectTree) indexValue(file string, val parser.Value) {
|
func (pt *ProjectTree) indexValue(file string, val parser.Value) {
|
||||||
switch v := val.(type) {
|
switch v := val.(type) {
|
||||||
case *parser.ReferenceValue:
|
case *parser.ReferenceValue:
|
||||||
@@ -384,6 +434,22 @@ func (pt *ProjectTree) Query(file string, line, col int) *QueryResult {
|
|||||||
return pt.queryNode(pt.Root, file, line, col)
|
return pt.queryNode(pt.Root, file, line, col)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pt *ProjectTree) Walk(visitor func(*ProjectNode)) {
|
||||||
|
if pt.Root != nil {
|
||||||
|
pt.walkRecursive(pt.Root, visitor)
|
||||||
|
}
|
||||||
|
for _, node := range pt.IsolatedFiles {
|
||||||
|
pt.walkRecursive(node, visitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pt *ProjectTree) walkRecursive(node *ProjectNode, visitor func(*ProjectNode)) {
|
||||||
|
visitor(node)
|
||||||
|
for _, child := range node.Children {
|
||||||
|
pt.walkRecursive(child, visitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (pt *ProjectTree) queryNode(node *ProjectNode, file string, line, col int) *QueryResult {
|
func (pt *ProjectTree) queryNode(node *ProjectNode, file string, line, col int) *QueryResult {
|
||||||
for _, frag := range node.Fragments {
|
for _, frag := range node.Fragments {
|
||||||
if frag.File == file {
|
if frag.File == file {
|
||||||
@@ -410,3 +476,44 @@ func (pt *ProjectTree) queryNode(node *ProjectNode, file string, line, col int)
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pt *ProjectTree) GetNodeContaining(file string, pos parser.Position) *ProjectNode {
|
||||||
|
if isoNode, ok := pt.IsolatedFiles[file]; ok {
|
||||||
|
if found := pt.findNodeContaining(isoNode, file, pos); found != nil {
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
return isoNode
|
||||||
|
}
|
||||||
|
if pt.Root != nil {
|
||||||
|
if found := pt.findNodeContaining(pt.Root, file, pos); found != nil {
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
for _, frag := range pt.Root.Fragments {
|
||||||
|
if frag.File == file && !frag.IsObject {
|
||||||
|
return pt.Root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pt *ProjectTree) findNodeContaining(node *ProjectNode, file string, pos parser.Position) *ProjectNode {
|
||||||
|
for _, child := range node.Children {
|
||||||
|
if res := pt.findNodeContaining(child, file, pos); res != nil {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, frag := range node.Fragments {
|
||||||
|
if frag.File == file && frag.IsObject {
|
||||||
|
start := frag.ObjectPos
|
||||||
|
end := frag.EndPos
|
||||||
|
|
||||||
|
if (pos.Line > start.Line || (pos.Line == start.Line && pos.Column >= start.Column)) &&
|
||||||
|
(pos.Line < end.Line || (pos.Line == end.Line && pos.Column <= end.Column)) {
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -196,7 +196,9 @@ func handleMessage(msg *JsonRpcMessage) {
|
|||||||
if root != "" {
|
if root != "" {
|
||||||
projectRoot = root
|
projectRoot = root
|
||||||
logger.Printf("Scanning workspace: %s\n", root)
|
logger.Printf("Scanning workspace: %s\n", root)
|
||||||
tree.ScanDirectory(root)
|
if err := tree.ScanDirectory(root); err != nil {
|
||||||
|
logger.Printf("ScanDirectory failed: %v\n", err)
|
||||||
|
}
|
||||||
tree.ResolveReferences()
|
tree.ResolveReferences()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -268,11 +270,13 @@ func handleDidOpen(params DidOpenTextDocumentParams) {
|
|||||||
documents[params.TextDocument.URI] = params.TextDocument.Text
|
documents[params.TextDocument.URI] = params.TextDocument.Text
|
||||||
p := parser.NewParser(params.TextDocument.Text)
|
p := parser.NewParser(params.TextDocument.Text)
|
||||||
config, err := p.Parse()
|
config, err := p.Parse()
|
||||||
if err == nil {
|
if err != nil {
|
||||||
|
publishParserError(params.TextDocument.URI, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
tree.AddFile(path, config)
|
tree.AddFile(path, config)
|
||||||
tree.ResolveReferences()
|
tree.ResolveReferences()
|
||||||
runValidation(params.TextDocument.URI)
|
runValidation(params.TextDocument.URI)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleDidChange(params DidChangeTextDocumentParams) {
|
func handleDidChange(params DidChangeTextDocumentParams) {
|
||||||
@@ -284,11 +288,13 @@ func handleDidChange(params DidChangeTextDocumentParams) {
|
|||||||
path := uriToPath(params.TextDocument.URI)
|
path := uriToPath(params.TextDocument.URI)
|
||||||
p := parser.NewParser(text)
|
p := parser.NewParser(text)
|
||||||
config, err := p.Parse()
|
config, err := p.Parse()
|
||||||
if err == nil {
|
if err != nil {
|
||||||
|
publishParserError(params.TextDocument.URI, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
tree.AddFile(path, config)
|
tree.AddFile(path, config)
|
||||||
tree.ResolveReferences()
|
tree.ResolveReferences()
|
||||||
runValidation(params.TextDocument.URI)
|
runValidation(params.TextDocument.URI)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleFormatting(params DocumentFormattingParams) []TextEdit {
|
func handleFormatting(params DocumentFormattingParams) []TextEdit {
|
||||||
@@ -378,6 +384,44 @@ func runValidation(uri string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func publishParserError(uri string, err error) {
|
||||||
|
var line, col int
|
||||||
|
var msg string
|
||||||
|
// Try parsing "line:col: message"
|
||||||
|
n, _ := fmt.Sscanf(err.Error(), "%d:%d: ", &line, &col)
|
||||||
|
if n == 2 {
|
||||||
|
parts := strings.SplitN(err.Error(), ": ", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
msg = parts[1]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback
|
||||||
|
line = 1
|
||||||
|
col = 1
|
||||||
|
msg = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
diag := LSPDiagnostic{
|
||||||
|
Range: Range{
|
||||||
|
Start: Position{Line: line - 1, Character: col - 1},
|
||||||
|
End: Position{Line: line - 1, Character: col},
|
||||||
|
},
|
||||||
|
Severity: 1, // Error
|
||||||
|
Message: msg,
|
||||||
|
Source: "mdt-parser",
|
||||||
|
}
|
||||||
|
|
||||||
|
notification := JsonRpcMessage{
|
||||||
|
Jsonrpc: "2.0",
|
||||||
|
Method: "textDocument/publishDiagnostics",
|
||||||
|
Params: mustMarshal(PublishDiagnosticsParams{
|
||||||
|
URI: uri,
|
||||||
|
Diagnostics: []LSPDiagnostic{diag},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
send(notification)
|
||||||
|
}
|
||||||
|
|
||||||
func collectFiles(node *index.ProjectNode, files map[string]bool) {
|
func collectFiles(node *index.ProjectNode, files map[string]bool) {
|
||||||
for _, frag := range node.Fragments {
|
for _, frag := range node.Fragments {
|
||||||
files[frag.File] = true
|
files[frag.File] = true
|
||||||
@@ -406,7 +450,11 @@ func handleHover(params HoverParams) *Hover {
|
|||||||
var content string
|
var content string
|
||||||
|
|
||||||
if res.Node != nil {
|
if res.Node != nil {
|
||||||
|
if res.Node.Target != nil {
|
||||||
|
content = fmt.Sprintf("**Link**: `%s` -> `%s`\n\n%s", res.Node.RealName, res.Node.Target.RealName, formatNodeInfo(res.Node.Target))
|
||||||
|
} else {
|
||||||
content = formatNodeInfo(res.Node)
|
content = formatNodeInfo(res.Node)
|
||||||
|
}
|
||||||
} else if res.Field != nil {
|
} else if res.Field != nil {
|
||||||
content = fmt.Sprintf("**Field**: `%s`", res.Field.Name)
|
content = fmt.Sprintf("**Field**: `%s`", res.Field.Name)
|
||||||
} else if res.Reference != nil {
|
} else if res.Reference != nil {
|
||||||
@@ -454,8 +502,12 @@ func handleDefinition(params DefinitionParams) any {
|
|||||||
if res.Reference != nil && res.Reference.Target != nil {
|
if res.Reference != nil && res.Reference.Target != nil {
|
||||||
targetNode = res.Reference.Target
|
targetNode = res.Reference.Target
|
||||||
} else if res.Node != nil {
|
} else if res.Node != nil {
|
||||||
|
if res.Node.Target != nil {
|
||||||
|
targetNode = res.Node.Target
|
||||||
|
} else {
|
||||||
targetNode = res.Node
|
targetNode = res.Node
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if targetNode != nil {
|
if targetNode != nil {
|
||||||
var locations []Location
|
var locations []Location
|
||||||
@@ -497,23 +549,30 @@ func handleReferences(params ReferenceParams) []Location {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve canonical target (follow link if present)
|
||||||
|
canonical := targetNode
|
||||||
|
if targetNode.Target != nil {
|
||||||
|
canonical = targetNode.Target
|
||||||
|
}
|
||||||
|
|
||||||
var locations []Location
|
var locations []Location
|
||||||
if params.Context.IncludeDeclaration {
|
if params.Context.IncludeDeclaration {
|
||||||
for _, frag := range targetNode.Fragments {
|
for _, frag := range canonical.Fragments {
|
||||||
if frag.IsObject {
|
if frag.IsObject {
|
||||||
locations = append(locations, Location{
|
locations = append(locations, Location{
|
||||||
URI: "file://" + frag.File,
|
URI: "file://" + frag.File,
|
||||||
Range: Range{
|
Range: Range{
|
||||||
Start: Position{Line: frag.ObjectPos.Line - 1, Character: frag.ObjectPos.Column - 1},
|
Start: Position{Line: frag.ObjectPos.Line - 1, Character: frag.ObjectPos.Column - 1},
|
||||||
End: Position{Line: frag.ObjectPos.Line - 1, Character: frag.ObjectPos.Column - 1 + len(targetNode.RealName)},
|
End: Position{Line: frag.ObjectPos.Line - 1, Character: frag.ObjectPos.Column - 1 + len(canonical.RealName)},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1. References from index (Aliases)
|
||||||
for _, ref := range tree.References {
|
for _, ref := range tree.References {
|
||||||
if ref.Target == targetNode {
|
if ref.Target == canonical {
|
||||||
locations = append(locations, Location{
|
locations = append(locations, Location{
|
||||||
URI: "file://" + ref.File,
|
URI: "file://" + ref.File,
|
||||||
Range: Range{
|
Range: Range{
|
||||||
@@ -524,17 +583,33 @@ func handleReferences(params ReferenceParams) []Location {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. References from Node Targets (Direct References)
|
||||||
|
tree.Walk(func(node *index.ProjectNode) {
|
||||||
|
if node.Target == canonical {
|
||||||
|
for _, frag := range node.Fragments {
|
||||||
|
if frag.IsObject {
|
||||||
|
locations = append(locations, Location{
|
||||||
|
URI: "file://" + frag.File,
|
||||||
|
Range: Range{
|
||||||
|
Start: Position{Line: frag.ObjectPos.Line - 1, Character: frag.ObjectPos.Column - 1},
|
||||||
|
End: Position{Line: frag.ObjectPos.Line - 1, Character: frag.ObjectPos.Column - 1 + len(node.RealName)},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return locations
|
return locations
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatNodeInfo(node *index.ProjectNode) string {
|
func formatNodeInfo(node *index.ProjectNode) string {
|
||||||
class := node.Metadata["Class"]
|
info := ""
|
||||||
if class == "" {
|
if class := node.Metadata["Class"]; class != "" {
|
||||||
class = "Unknown"
|
info = fmt.Sprintf("`%s:%s`\n\n", class, node.RealName[1:])
|
||||||
|
} else {
|
||||||
|
info = fmt.Sprintf("`%s`\n\n", node.RealName)
|
||||||
}
|
}
|
||||||
|
|
||||||
info := fmt.Sprintf("**Object**: `%s`\n\n**Class**: `%s`", node.RealName, class)
|
|
||||||
|
|
||||||
// Check if it's a Signal (has Type or DataSource)
|
// Check if it's a Signal (has Type or DataSource)
|
||||||
typ := node.Metadata["Type"]
|
typ := node.Metadata["Type"]
|
||||||
ds := node.Metadata["DataSource"]
|
ds := node.Metadata["DataSource"]
|
||||||
@@ -550,7 +625,7 @@ func formatNodeInfo(node *index.ProjectNode) string {
|
|||||||
|
|
||||||
// Size
|
// Size
|
||||||
dims := node.Metadata["NumberOfDimensions"]
|
dims := node.Metadata["NumberOfDimensions"]
|
||||||
elems := node.Metadata["NumberOfElements"]
|
elems := node.Metadata["NumberOfElements"]
|
||||||
if dims != "" || elems != "" {
|
if dims != "" || elems != "" {
|
||||||
sigInfo += fmt.Sprintf("**Size**: `[%s]`, `%s` dims ", elems, dims)
|
sigInfo += fmt.Sprintf("**Size**: `[%s]`, `%s` dims ", elems, dims)
|
||||||
}
|
}
|
||||||
@@ -560,6 +635,57 @@ elems := node.Metadata["NumberOfElements"]
|
|||||||
if node.Doc != "" {
|
if node.Doc != "" {
|
||||||
info += fmt.Sprintf("\n\n%s", node.Doc)
|
info += fmt.Sprintf("\n\n%s", node.Doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find references
|
||||||
|
var refs []string
|
||||||
|
for _, ref := range tree.References {
|
||||||
|
if ref.Target == node {
|
||||||
|
container := tree.GetNodeContaining(ref.File, ref.Position)
|
||||||
|
if container != nil {
|
||||||
|
threadName := ""
|
||||||
|
stateName := ""
|
||||||
|
|
||||||
|
curr := container
|
||||||
|
for curr != nil {
|
||||||
|
if cls, ok := curr.Metadata["Class"]; ok {
|
||||||
|
if cls == "RealTimeThread" {
|
||||||
|
threadName = curr.RealName
|
||||||
|
}
|
||||||
|
if cls == "RealTimeState" {
|
||||||
|
stateName = curr.RealName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
curr = curr.Parent
|
||||||
|
}
|
||||||
|
|
||||||
|
if threadName != "" || stateName != "" {
|
||||||
|
refStr := ""
|
||||||
|
if stateName != "" {
|
||||||
|
refStr += fmt.Sprintf("State: `%s`", stateName)
|
||||||
|
}
|
||||||
|
if threadName != "" {
|
||||||
|
if refStr != "" {
|
||||||
|
refStr += ", "
|
||||||
|
}
|
||||||
|
refStr += fmt.Sprintf("Thread: `%s`", threadName)
|
||||||
|
}
|
||||||
|
refs = append(refs, refStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(refs) > 0 {
|
||||||
|
uniqueRefs := make(map[string]bool)
|
||||||
|
info += "\n\n**Referenced in**:\n"
|
||||||
|
for _, r := range refs {
|
||||||
|
if !uniqueRefs[r] {
|
||||||
|
uniqueRefs[r] = true
|
||||||
|
info += fmt.Sprintf("- %s\n", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const (
|
|||||||
TokenPragma
|
TokenPragma
|
||||||
TokenComment
|
TokenComment
|
||||||
TokenDocstring
|
TokenDocstring
|
||||||
|
TokenComma
|
||||||
)
|
)
|
||||||
|
|
||||||
type Token struct {
|
type Token struct {
|
||||||
@@ -121,6 +122,8 @@ func (l *Lexer) NextToken() Token {
|
|||||||
return l.emit(TokenLBrace)
|
return l.emit(TokenLBrace)
|
||||||
case '}':
|
case '}':
|
||||||
return l.emit(TokenRBrace)
|
return l.emit(TokenRBrace)
|
||||||
|
case ',':
|
||||||
|
return l.emit(TokenComma)
|
||||||
case '"':
|
case '"':
|
||||||
return l.lexString()
|
return l.lexString()
|
||||||
case '/':
|
case '/':
|
||||||
@@ -148,7 +151,7 @@ func (l *Lexer) NextToken() Token {
|
|||||||
func (l *Lexer) lexIdentifier() Token {
|
func (l *Lexer) lexIdentifier() Token {
|
||||||
for {
|
for {
|
||||||
r := l.next()
|
r := l.next()
|
||||||
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '-' {
|
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '-' || r == '.' || r == ':' {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
l.backup()
|
l.backup()
|
||||||
@@ -186,7 +189,7 @@ func (l *Lexer) lexString() Token {
|
|||||||
func (l *Lexer) lexNumber() Token {
|
func (l *Lexer) lexNumber() Token {
|
||||||
for {
|
for {
|
||||||
r := l.next()
|
r := l.next()
|
||||||
if unicode.IsDigit(r) || r == '.' || r == 'x' || r == 'b' || r == 'e' || r == '-' {
|
if unicode.IsDigit(r) || unicode.IsLetter(r) || r == '.' || r == '-' || r == '+' {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
l.backup()
|
l.backup()
|
||||||
@@ -206,6 +209,20 @@ func (l *Lexer) lexComment() Token {
|
|||||||
}
|
}
|
||||||
return l.lexUntilNewline(TokenComment)
|
return l.lexUntilNewline(TokenComment)
|
||||||
}
|
}
|
||||||
|
if r == '*' {
|
||||||
|
for {
|
||||||
|
r := l.next()
|
||||||
|
if r == -1 {
|
||||||
|
return l.emit(TokenError)
|
||||||
|
}
|
||||||
|
if r == '*' {
|
||||||
|
if l.peek() == '/' {
|
||||||
|
l.next() // consume /
|
||||||
|
return l.emit(TokenComment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
l.backup()
|
l.backup()
|
||||||
return l.emit(TokenError)
|
return l.emit(TokenError)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -235,6 +235,10 @@ func (p *Parser) parseValue() (Value, error) {
|
|||||||
arr.EndPosition = endTok.Position
|
arr.EndPosition = endTok.Position
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
if t.Type == TokenComma {
|
||||||
|
p.next()
|
||||||
|
continue
|
||||||
|
}
|
||||||
val, err := p.parseValue()
|
val, err := p.parseValue()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
35
internal/parser/parser_strictness_test.go
Normal file
35
internal/parser/parser_strictness_test.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package parser_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParserStrictness(t *testing.T) {
|
||||||
|
// Case 1: content not a definition (missing =)
|
||||||
|
invalidDef := `
|
||||||
|
A = {
|
||||||
|
Field = 10
|
||||||
|
XXX
|
||||||
|
}
|
||||||
|
`
|
||||||
|
p := parser.NewParser(invalidDef)
|
||||||
|
_, err := p.Parse()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error for invalid definition XXX, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2: Missing closing bracket
|
||||||
|
missingBrace := `
|
||||||
|
A = {
|
||||||
|
SUBNODE = {
|
||||||
|
FIELD = 10
|
||||||
|
}
|
||||||
|
`
|
||||||
|
p2 := parser.NewParser(missingBrace)
|
||||||
|
_, err2 := p2.Parse()
|
||||||
|
if err2 == nil {
|
||||||
|
t.Error("Expected error for missing closing bracket, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,17 @@
|
|||||||
},
|
},
|
||||||
"StateMachine": {
|
"StateMachine": {
|
||||||
"fields": [
|
"fields": [
|
||||||
{"name": "States", "type": "node", "mandatory": true}
|
{"name": "States", "type": "node", "mandatory": false}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"RealTimeState": {
|
||||||
|
"fields": [
|
||||||
|
{"name": "Threads", "type": "node", "mandatory": true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"RealTimeThread": {
|
||||||
|
"fields": [
|
||||||
|
{"name": "Functions", "type": "array", "mandatory": true}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"GAMScheduler": {
|
"GAMScheduler": {
|
||||||
@@ -18,7 +28,8 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"TimingDataSource": {
|
"TimingDataSource": {
|
||||||
"fields": []
|
"fields": [],
|
||||||
|
"direction": "IN"
|
||||||
},
|
},
|
||||||
"IOGAM": {
|
"IOGAM": {
|
||||||
"fields": [
|
"fields": [
|
||||||
@@ -43,66 +54,79 @@
|
|||||||
"fields": [
|
"fields": [
|
||||||
{"name": "Filename", "type": "string", "mandatory": true},
|
{"name": "Filename", "type": "string", "mandatory": true},
|
||||||
{"name": "Format", "type": "string", "mandatory": false}
|
{"name": "Format", "type": "string", "mandatory": false}
|
||||||
]
|
],
|
||||||
|
"direction": "INOUT"
|
||||||
},
|
},
|
||||||
"LoggerDataSource": {
|
"LoggerDataSource": {
|
||||||
"fields": []
|
"fields": [],
|
||||||
|
"direction": "OUT"
|
||||||
},
|
},
|
||||||
"DANStream": {
|
"DANStream": {
|
||||||
"fields": [
|
"fields": [
|
||||||
{"name": "Timeout", "type": "int", "mandatory": false}
|
{"name": "Timeout", "type": "int", "mandatory": false}
|
||||||
]
|
],
|
||||||
|
"direction": "OUT"
|
||||||
},
|
},
|
||||||
"EPICSCAInput": {
|
"EPICSCAInput": {
|
||||||
"fields": []
|
"fields": [],
|
||||||
|
"direction": "IN"
|
||||||
},
|
},
|
||||||
"EPICSCAOutput": {
|
"EPICSCAOutput": {
|
||||||
"fields": []
|
"fields": [],
|
||||||
|
"direction": "OUT"
|
||||||
},
|
},
|
||||||
"EPICSPVAInput": {
|
"EPICSPVAInput": {
|
||||||
"fields": []
|
"fields": [],
|
||||||
|
"direction": "IN"
|
||||||
},
|
},
|
||||||
"EPICSPVAOutput": {
|
"EPICSPVAOutput": {
|
||||||
"fields": []
|
"fields": [],
|
||||||
|
"direction": "OUT"
|
||||||
},
|
},
|
||||||
"SDNSubscriber": {
|
"SDNSubscriber": {
|
||||||
"fields": [
|
"fields": [
|
||||||
{"name": "Address", "type": "string", "mandatory": true},
|
{"name": "Address", "type": "string", "mandatory": true},
|
||||||
{"name": "Port", "type": "int", "mandatory": true},
|
{"name": "Port", "type": "int", "mandatory": true},
|
||||||
{"name": "Interface", "type": "string", "mandatory": false}
|
{"name": "Interface", "type": "string", "mandatory": false}
|
||||||
]
|
],
|
||||||
|
"direction": "IN"
|
||||||
},
|
},
|
||||||
"SDNPublisher": {
|
"SDNPublisher": {
|
||||||
"fields": [
|
"fields": [
|
||||||
{"name": "Address", "type": "string", "mandatory": true},
|
{"name": "Address", "type": "string", "mandatory": true},
|
||||||
{"name": "Port", "type": "int", "mandatory": true},
|
{"name": "Port", "type": "int", "mandatory": true},
|
||||||
{"name": "Interface", "type": "string", "mandatory": false}
|
{"name": "Interface", "type": "string", "mandatory": false}
|
||||||
]
|
],
|
||||||
|
"direction": "OUT"
|
||||||
},
|
},
|
||||||
"UDPReceiver": {
|
"UDPReceiver": {
|
||||||
"fields": [
|
"fields": [
|
||||||
{"name": "Port", "type": "int", "mandatory": true},
|
{"name": "Port", "type": "int", "mandatory": true},
|
||||||
{"name": "Address", "type": "string", "mandatory": false}
|
{"name": "Address", "type": "string", "mandatory": false}
|
||||||
]
|
],
|
||||||
|
"direction": "IN"
|
||||||
},
|
},
|
||||||
"UDPSender": {
|
"UDPSender": {
|
||||||
"fields": [
|
"fields": [
|
||||||
{"name": "Destination", "type": "string", "mandatory": true}
|
{"name": "Destination", "type": "string", "mandatory": true}
|
||||||
]
|
],
|
||||||
|
"direction": "OUT"
|
||||||
},
|
},
|
||||||
"FileReader": {
|
"FileReader": {
|
||||||
"fields": [
|
"fields": [
|
||||||
{"name": "Filename", "type": "string", "mandatory": true},
|
{"name": "Filename", "type": "string", "mandatory": true},
|
||||||
{"name": "Format", "type": "string", "mandatory": false},
|
{"name": "Format", "type": "string", "mandatory": false},
|
||||||
{"name": "Interpolate", "type": "string", "mandatory": false}
|
{"name": "Interpolate", "type": "string", "mandatory": false}
|
||||||
]
|
],
|
||||||
|
"direction": "IN"
|
||||||
},
|
},
|
||||||
"FileWriter": {
|
"FileWriter": {
|
||||||
"fields": [
|
"fields": [
|
||||||
{"name": "Filename", "type": "string", "mandatory": true},
|
{"name": "Filename", "type": "string", "mandatory": true},
|
||||||
{"name": "Format", "type": "string", "mandatory": false},
|
{"name": "Format", "type": "string", "mandatory": false},
|
||||||
{"name": "StoreOnTrigger", "type": "int", "mandatory": false}
|
{"name": "StoreOnTrigger", "type": "int", "mandatory": false}
|
||||||
]
|
],
|
||||||
|
"direction": "OUT"
|
||||||
},
|
},
|
||||||
"OrderedClass": {
|
"OrderedClass": {
|
||||||
"ordered": true,
|
"ordered": true,
|
||||||
@@ -148,7 +172,7 @@
|
|||||||
"TimeCorrectionGAM": { "fields": [] },
|
"TimeCorrectionGAM": { "fields": [] },
|
||||||
"TriggeredIOGAM": { "fields": [] },
|
"TriggeredIOGAM": { "fields": [] },
|
||||||
"WaveformGAM": { "fields": [] },
|
"WaveformGAM": { "fields": [] },
|
||||||
"DAN": { "fields": [] },
|
"DAN": { "fields": [], "direction": "OUT" },
|
||||||
"LinuxTimer": {
|
"LinuxTimer": {
|
||||||
"fields": [
|
"fields": [
|
||||||
{"name": "ExecutionMode", "type": "string", "mandatory": false},
|
{"name": "ExecutionMode", "type": "string", "mandatory": false},
|
||||||
@@ -158,16 +182,18 @@
|
|||||||
{"name": "CPUMask", "type": "int", "mandatory": false},
|
{"name": "CPUMask", "type": "int", "mandatory": false},
|
||||||
{"name": "TimeProvider", "type": "node", "mandatory": false},
|
{"name": "TimeProvider", "type": "node", "mandatory": false},
|
||||||
{"name": "Signals", "type": "node", "mandatory": true}
|
{"name": "Signals", "type": "node", "mandatory": true}
|
||||||
]
|
],
|
||||||
|
"direction": "IN"
|
||||||
},
|
},
|
||||||
"LinkDataSource": { "fields": [] },
|
"LinkDataSource": { "fields": [], "direction": "INOUT" },
|
||||||
"MDSReader": {
|
"MDSReader": {
|
||||||
"fields": [
|
"fields": [
|
||||||
{"name": "TreeName", "type": "string", "mandatory": true},
|
{"name": "TreeName", "type": "string", "mandatory": true},
|
||||||
{"name": "ShotNumber", "type": "int", "mandatory": true},
|
{"name": "ShotNumber", "type": "int", "mandatory": true},
|
||||||
{"name": "Frequency", "type": "float", "mandatory": true},
|
{"name": "Frequency", "type": "float", "mandatory": true},
|
||||||
{"name": "Signals", "type": "node", "mandatory": true}
|
{"name": "Signals", "type": "node", "mandatory": true}
|
||||||
]
|
],
|
||||||
|
"direction": "IN"
|
||||||
},
|
},
|
||||||
"MDSWriter": {
|
"MDSWriter": {
|
||||||
"fields": [
|
"fields": [
|
||||||
@@ -183,27 +209,29 @@
|
|||||||
{"name": "NumberOfPostTriggers", "type": "int", "mandatory": false},
|
{"name": "NumberOfPostTriggers", "type": "int", "mandatory": false},
|
||||||
{"name": "Signals", "type": "node", "mandatory": true},
|
{"name": "Signals", "type": "node", "mandatory": true},
|
||||||
{"name": "Messages", "type": "node", "mandatory": false}
|
{"name": "Messages", "type": "node", "mandatory": false}
|
||||||
]
|
],
|
||||||
|
"direction": "OUT"
|
||||||
},
|
},
|
||||||
"NI1588TimeStamp": { "fields": [] },
|
"NI1588TimeStamp": { "fields": [], "direction": "IN" },
|
||||||
"NI6259ADC": { "fields": [] },
|
"NI6259ADC": { "fields": [], "direction": "IN" },
|
||||||
"NI6259DAC": { "fields": [] },
|
"NI6259DAC": { "fields": [], "direction": "OUT" },
|
||||||
"NI6259DIO": { "fields": [] },
|
"NI6259DIO": { "fields": [], "direction": "INOUT" },
|
||||||
"NI6368ADC": { "fields": [] },
|
"NI6368ADC": { "fields": [], "direction": "IN" },
|
||||||
"NI6368DAC": { "fields": [] },
|
"NI6368DAC": { "fields": [], "direction": "OUT" },
|
||||||
"NI6368DIO": { "fields": [] },
|
"NI6368DIO": { "fields": [], "direction": "INOUT" },
|
||||||
"NI9157CircularFifoReader": { "fields": [] },
|
"NI9157CircularFifoReader": { "fields": [], "direction": "IN" },
|
||||||
"NI9157MxiDataSource": { "fields": [] },
|
"NI9157MxiDataSource": { "fields": [], "direction": "INOUT" },
|
||||||
"OPCUADSInput": { "fields": [] },
|
"OPCUADSInput": { "fields": [], "direction": "IN" },
|
||||||
"OPCUADSOutput": { "fields": [] },
|
"OPCUADSOutput": { "fields": [], "direction": "OUT" },
|
||||||
"RealTimeThreadAsyncBridge": { "fields": [] },
|
"RealTimeThreadAsyncBridge": { "fields": [] },
|
||||||
"RealTimeThreadSynchronisation": { "fields": [] },
|
"RealTimeThreadSynchronisation": { "fields": [] },
|
||||||
"UARTDataSource": { "fields": [] },
|
"UARTDataSource": { "fields": [], "direction": "INOUT" },
|
||||||
"BaseLib2Wrapper": { "fields": [] },
|
"BaseLib2Wrapper": { "fields": [] },
|
||||||
"EPICSCAClient": { "fields": [] },
|
"EPICSCAClient": { "fields": [] },
|
||||||
"EPICSPVA": { "fields": [] },
|
"EPICSPVA": { "fields": [] },
|
||||||
"MemoryGate": { "fields": [] },
|
"MemoryGate": { "fields": [] },
|
||||||
"OPCUA": { "fields": [] },
|
"OPCUA": { "fields": [] },
|
||||||
"SysLogger": { "fields": [] }
|
"SysLogger": { "fields": [] },
|
||||||
|
"GAMDataSource": { "fields": [], "direction": "INOUT" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type Schema struct {
|
|||||||
type ClassDefinition struct {
|
type ClassDefinition struct {
|
||||||
Fields []FieldDefinition `json:"fields"`
|
Fields []FieldDefinition `json:"fields"`
|
||||||
Ordered bool `json:"ordered"`
|
Ordered bool `json:"ordered"`
|
||||||
|
Direction string `json:"direction"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FieldDefinition struct {
|
type FieldDefinition struct {
|
||||||
@@ -96,6 +97,9 @@ func (s *Schema) Merge(other *Schema) {
|
|||||||
if classDef.Ordered {
|
if classDef.Ordered {
|
||||||
existingClass.Ordered = true
|
existingClass.Ordered = true
|
||||||
}
|
}
|
||||||
|
if classDef.Direction != "" {
|
||||||
|
existingClass.Direction = classDef.Direction
|
||||||
|
}
|
||||||
s.Classes[className] = existingClass
|
s.Classes[className] = existingClass
|
||||||
} else {
|
} else {
|
||||||
s.Classes[className] = classDef
|
s.Classes[className] = classDef
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package validator
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/marte-dev/marte-dev-tools/internal/index"
|
"github.com/marte-dev/marte-dev-tools/internal/index"
|
||||||
"github.com/marte-dev/marte-dev-tools/internal/parser"
|
"github.com/marte-dev/marte-dev-tools/internal/parser"
|
||||||
"github.com/marte-dev/marte-dev-tools/internal/schema"
|
"github.com/marte-dev/marte-dev-tools/internal/schema"
|
||||||
@@ -38,6 +40,9 @@ func (v *Validator) ValidateProject() {
|
|||||||
if v.Tree == nil {
|
if v.Tree == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Ensure references are resolved (if not already done by builder/lsp)
|
||||||
|
v.Tree.ResolveReferences()
|
||||||
|
|
||||||
if v.Tree.Root != nil {
|
if v.Tree.Root != nil {
|
||||||
v.validateNode(v.Tree.Root)
|
v.validateNode(v.Tree.Root)
|
||||||
}
|
}
|
||||||
@@ -47,17 +52,48 @@ func (v *Validator) ValidateProject() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (v *Validator) validateNode(node *index.ProjectNode) {
|
func (v *Validator) validateNode(node *index.ProjectNode) {
|
||||||
// Collect fields and their definitions
|
// Check for invalid content in Signals container of DataSource
|
||||||
fields := make(map[string][]*parser.Field)
|
if node.RealName == "Signals" && node.Parent != nil && isDataSource(node.Parent) {
|
||||||
fieldOrder := []string{} // Keep track of order of appearance (approximate across fragments)
|
|
||||||
|
|
||||||
for _, frag := range node.Fragments {
|
for _, frag := range node.Fragments {
|
||||||
for _, def := range frag.Definitions {
|
for _, def := range frag.Definitions {
|
||||||
if f, ok := def.(*parser.Field); ok {
|
if f, ok := def.(*parser.Field); ok {
|
||||||
if _, exists := fields[f.Name]; !exists {
|
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
||||||
fieldOrder = append(fieldOrder, f.Name)
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect fields and their definitions
|
||||||
|
fields := v.getFields(node)
|
||||||
|
fieldOrder := []string{}
|
||||||
|
for _, frag := range node.Fragments {
|
||||||
|
for _, def := range frag.Definitions {
|
||||||
|
if f, ok := def.(*parser.Field); ok {
|
||||||
|
if _, exists := fields[f.Name]; exists { // already collected
|
||||||
|
// Maintain order logic if needed, but getFields collects all.
|
||||||
|
// For strict order check we might need this loop.
|
||||||
|
// Let's assume getFields is enough for validation logic,
|
||||||
|
// but for "duplicate check" and "class validation" we iterate fields map.
|
||||||
|
// We need to construct fieldOrder.
|
||||||
|
// Just reuse loop for fieldOrder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Re-construct fieldOrder for order validation
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
for _, frag := range node.Fragments {
|
||||||
|
for _, def := range frag.Definitions {
|
||||||
|
if f, ok := def.(*parser.Field); ok {
|
||||||
|
if !seen[f.Name] {
|
||||||
|
fieldOrder = append(fieldOrder, f.Name)
|
||||||
|
seen[f.Name] = true
|
||||||
}
|
}
|
||||||
fields[f.Name] = append(fields[f.Name], f)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,7 +101,6 @@ func (v *Validator) validateNode(node *index.ProjectNode) {
|
|||||||
// 1. Check for duplicate fields
|
// 1. Check for duplicate fields
|
||||||
for name, defs := range fields {
|
for name, defs := range fields {
|
||||||
if len(defs) > 1 {
|
if len(defs) > 1 {
|
||||||
// Report error on the second definition
|
|
||||||
firstFile := v.getFileForField(defs[0], node)
|
firstFile := v.getFileForField(defs[0], node)
|
||||||
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
||||||
Level: LevelError,
|
Level: LevelError,
|
||||||
@@ -80,13 +115,7 @@ func (v *Validator) validateNode(node *index.ProjectNode) {
|
|||||||
className := ""
|
className := ""
|
||||||
if node.RealName != "" && (node.RealName[0] == '+' || node.RealName[0] == '$') {
|
if node.RealName != "" && (node.RealName[0] == '+' || node.RealName[0] == '$') {
|
||||||
if classFields, ok := fields["Class"]; ok && len(classFields) > 0 {
|
if classFields, ok := fields["Class"]; ok && len(classFields) > 0 {
|
||||||
// Extract class name from value
|
className = v.getFieldValue(classFields[0])
|
||||||
switch val := classFields[0].Value.(type) {
|
|
||||||
case *parser.StringValue:
|
|
||||||
className = val.Value
|
|
||||||
case *parser.ReferenceValue:
|
|
||||||
className = val.Value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hasType := false
|
hasType := false
|
||||||
@@ -104,6 +133,10 @@ func (v *Validator) validateNode(node *index.ProjectNode) {
|
|||||||
File: file,
|
File: file,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if className == "RealTimeThread" {
|
||||||
|
v.checkFunctionsArray(node, fields)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Schema Validation
|
// 3. Schema Validation
|
||||||
@@ -113,6 +146,16 @@ func (v *Validator) validateNode(node *index.ProjectNode) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Recursively validate children
|
||||||
for _, child := range node.Children {
|
for _, child := range node.Children {
|
||||||
v.validateNode(child)
|
v.validateNode(child)
|
||||||
@@ -120,14 +163,13 @@ func (v *Validator) validateNode(node *index.ProjectNode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (v *Validator) validateClass(node *index.ProjectNode, classDef schema.ClassDefinition, fields map[string][]*parser.Field, fieldOrder []string) {
|
func (v *Validator) validateClass(node *index.ProjectNode, classDef schema.ClassDefinition, fields map[string][]*parser.Field, fieldOrder []string) {
|
||||||
// Check Mandatory Fields
|
// ... (same as before)
|
||||||
for _, fieldDef := range classDef.Fields {
|
for _, fieldDef := range classDef.Fields {
|
||||||
if fieldDef.Mandatory {
|
if fieldDef.Mandatory {
|
||||||
found := false
|
found := false
|
||||||
if _, ok := fields[fieldDef.Name]; ok {
|
if _, ok := fields[fieldDef.Name]; ok {
|
||||||
found = true
|
found = true
|
||||||
} else if fieldDef.Type == "node" {
|
} else if fieldDef.Type == "node" {
|
||||||
// Check children for nodes
|
|
||||||
if _, ok := node.Children[fieldDef.Name]; ok {
|
if _, ok := node.Children[fieldDef.Name]; ok {
|
||||||
found = true
|
found = true
|
||||||
}
|
}
|
||||||
@@ -144,10 +186,9 @@ func (v *Validator) validateClass(node *index.ProjectNode, classDef schema.Class
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check Field Types
|
|
||||||
for _, fieldDef := range classDef.Fields {
|
for _, fieldDef := range classDef.Fields {
|
||||||
if fList, ok := fields[fieldDef.Name]; ok {
|
if fList, ok := fields[fieldDef.Name]; ok {
|
||||||
f := fList[0] // Check the first definition (duplicates handled elsewhere)
|
f := fList[0]
|
||||||
if !v.checkType(f.Value, fieldDef.Type) {
|
if !v.checkType(f.Value, fieldDef.Type) {
|
||||||
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
||||||
Level: LevelError,
|
Level: LevelError,
|
||||||
@@ -159,21 +200,14 @@ func (v *Validator) validateClass(node *index.ProjectNode, classDef schema.Class
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check Field Order
|
|
||||||
if classDef.Ordered {
|
if classDef.Ordered {
|
||||||
// Verify that fields present in the node appear in the order defined in the schema
|
|
||||||
// Only consider fields that are actually in the schema's field list
|
|
||||||
schemaIdx := 0
|
schemaIdx := 0
|
||||||
for _, nodeFieldName := range fieldOrder {
|
for _, nodeFieldName := range fieldOrder {
|
||||||
// Find this field in schema
|
|
||||||
foundInSchema := false
|
foundInSchema := false
|
||||||
for i, fd := range classDef.Fields {
|
for i, fd := range classDef.Fields {
|
||||||
if fd.Name == nodeFieldName {
|
if fd.Name == nodeFieldName {
|
||||||
foundInSchema = true
|
foundInSchema = true
|
||||||
// Check if this field appears AFTER the current expected position
|
|
||||||
if i < schemaIdx {
|
if i < schemaIdx {
|
||||||
// This field appears out of order (it should have appeared earlier, or previous fields were missing but this one came too late? No, simple relative order)
|
|
||||||
// Actually, simple check: `i` must be >= `lastSeenSchemaIdx`.
|
|
||||||
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
||||||
Level: LevelError,
|
Level: LevelError,
|
||||||
Message: fmt.Sprintf("Field '%s' is out of order", nodeFieldName),
|
Message: fmt.Sprintf("Field '%s' is out of order", nodeFieldName),
|
||||||
@@ -187,13 +221,340 @@ func (v *Validator) validateClass(node *index.ProjectNode, classDef schema.Class
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !foundInSchema {
|
if !foundInSchema {
|
||||||
// Ignore extra fields for order check? Spec doesn't say strict closed schema.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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])
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
dsClass := v.getNodeClass(dsNode)
|
||||||
|
if dsClass != "" {
|
||||||
|
if classDef, ok := v.Schema.Classes[dsClass]; ok {
|
||||||
|
dsDir := classDef.Direction
|
||||||
|
if 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]) // 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])
|
||||||
|
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])
|
||||||
|
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) 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
|
||||||
|
}
|
||||||
|
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.findNodeRecursive(isoNode, name, predicate); found != nil {
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if v.Tree.Root == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return v.findNodeRecursive(v.Tree.Root, name, predicate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Validator) findNodeRecursive(root *index.ProjectNode, name string, predicate func(*index.ProjectNode) bool) *index.ProjectNode {
|
||||||
|
// Simple recursive search matching name
|
||||||
|
if root.RealName == name || root.Name == index.NormalizeName(name) {
|
||||||
|
if predicate == nil || predicate(root) {
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursive
|
||||||
|
for _, child := range root.Children {
|
||||||
|
if found := v.findNodeRecursive(child, name, predicate); found != nil {
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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) checkType(val parser.Value, expectedType string) bool {
|
func (v *Validator) checkType(val parser.Value, expectedType string) bool {
|
||||||
|
// ... (same as before)
|
||||||
switch expectedType {
|
switch expectedType {
|
||||||
case "int":
|
case "int":
|
||||||
_, ok := val.(*parser.IntValue)
|
_, ok := val.(*parser.IntValue)
|
||||||
@@ -202,8 +563,9 @@ func (v *Validator) checkType(val parser.Value, expectedType string) bool {
|
|||||||
_, ok := val.(*parser.FloatValue)
|
_, ok := val.(*parser.FloatValue)
|
||||||
return ok
|
return ok
|
||||||
case "string":
|
case "string":
|
||||||
_, ok := val.(*parser.StringValue)
|
_, okStr := val.(*parser.StringValue)
|
||||||
return ok
|
_, okRef := val.(*parser.ReferenceValue)
|
||||||
|
return okStr || okRef
|
||||||
case "bool":
|
case "bool":
|
||||||
_, ok := val.(*parser.BoolValue)
|
_, ok := val.(*parser.BoolValue)
|
||||||
return ok
|
return ok
|
||||||
@@ -214,15 +576,7 @@ func (v *Validator) checkType(val parser.Value, expectedType string) bool {
|
|||||||
_, ok := val.(*parser.ReferenceValue)
|
_, ok := val.(*parser.ReferenceValue)
|
||||||
return ok
|
return ok
|
||||||
case "node":
|
case "node":
|
||||||
// This is tricky. A field cannot really be a "node" type in the parser sense (Node = { ... } is an ObjectNode, not a Field).
|
return true
|
||||||
// But if the schema says "FieldX" is type "node", maybe it means it expects a reference to a node?
|
|
||||||
// Or maybe it means it expects a Subnode?
|
|
||||||
// In MARTe, `Field = { ... }` is parsed as ArrayValue usually.
|
|
||||||
// If `Field = SubNode`, it's `ObjectNode`.
|
|
||||||
// Schema likely refers to `+SubNode = { ... }`.
|
|
||||||
// But `validateClass` iterates `fields`.
|
|
||||||
// If schema defines a "field" of type "node", it might mean it expects a child node with that name.
|
|
||||||
return true // skip for now
|
|
||||||
case "any":
|
case "any":
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -248,6 +602,13 @@ func (v *Validator) CheckUnused() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
if v.Tree.Root != nil {
|
||||||
v.checkUnusedRecursive(v.Tree.Root, referencedNodes)
|
v.checkUnusedRecursive(v.Tree.Root, referencedNodes)
|
||||||
}
|
}
|
||||||
@@ -256,10 +617,29 @@ func (v *Validator) CheckUnused() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
func (v *Validator) checkUnusedRecursive(node *index.ProjectNode, referenced map[*index.ProjectNode]bool) {
|
||||||
// Heuristic for GAM
|
// Heuristic for GAM
|
||||||
if isGAM(node) {
|
if isGAM(node) {
|
||||||
if !referenced[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{
|
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
||||||
Level: LevelWarning,
|
Level: LevelWarning,
|
||||||
Message: fmt.Sprintf("Unused GAM: %s is defined but not referenced in any thread or scheduler", node.RealName),
|
Message: fmt.Sprintf("Unused GAM: %s is defined but not referenced in any thread or scheduler", node.RealName),
|
||||||
@@ -268,11 +648,24 @@ func (v *Validator) checkUnusedRecursive(node *index.ProjectNode, referenced map
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Heuristic for DataSource and its signals
|
// Heuristic for DataSource and its signals
|
||||||
if isDataSource(node) {
|
if isDataSource(node) {
|
||||||
for _, signal := range node.Children {
|
if signalsNode, ok := node.Children["Signals"]; ok {
|
||||||
|
for _, signal := range signalsNode.Children {
|
||||||
if !referenced[signal] {
|
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{
|
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
||||||
Level: LevelWarning,
|
Level: LevelWarning,
|
||||||
Message: fmt.Sprintf("Unused Signal: %s is defined in DataSource %s but never referenced", signal.RealName, node.RealName),
|
Message: fmt.Sprintf("Unused Signal: %s is defined in DataSource %s but never referenced", signal.RealName, node.RealName),
|
||||||
@@ -282,6 +675,8 @@ func (v *Validator) checkUnusedRecursive(node *index.ProjectNode, referenced map
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, child := range node.Children {
|
for _, child := range node.Children {
|
||||||
v.checkUnusedRecursive(child, referenced)
|
v.checkUnusedRecursive(child, referenced)
|
||||||
@@ -304,6 +699,15 @@ func isDataSource(node *index.ProjectNode) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
func (v *Validator) getNodePosition(node *index.ProjectNode) parser.Position {
|
||||||
if len(node.Fragments) > 0 {
|
if len(node.Fragments) > 0 {
|
||||||
return node.Fragments[0].ObjectPos
|
return node.Fragments[0].ObjectPos
|
||||||
@@ -317,3 +721,63 @@ func (v *Validator) getNodeFile(node *index.ProjectNode) string {
|
|||||||
}
|
}
|
||||||
return ""
|
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
|
||||||
|
}
|
||||||
@@ -84,8 +84,13 @@ The LSP server should provide the following capabilities:
|
|||||||
- **Nodes (`+` / `$`)**: The prefixes `+` and `$` indicate that the node represents an object.
|
- **Nodes (`+` / `$`)**: The prefixes `+` and `$` indicate that the node represents an object.
|
||||||
- **Constraint**: These nodes _must_ contain a field named `Class` within their subnode definition (across all files where the node is defined).
|
- **Constraint**: These nodes _must_ contain a field named `Class` within their subnode definition (across all files where the node is defined).
|
||||||
- **Signals**: Signals are considered nodes but **not** objects. They do not require a `Class` field.
|
- **Signals**: Signals are considered nodes but **not** objects. They do not require a `Class` field.
|
||||||
- **Pragmas (`//!`)**: Used to suppress specific diagnostics. The developer can use these to explain why a rule is being ignored.
|
- **Pragmas (`//!`)**: Used to suppress specific diagnostics. The developer can use these to explain why a rule is being ignored. Supported pragmas:
|
||||||
|
- `//!unused: REASON` or `//!ignore(unused): REASON` - Suppress "Unused GAM" or "Unused Signal" warnings.
|
||||||
|
- `//!implicit: REASON` or `//!ignore(implicit): REASON` - Suppress "Implicitly Defined Signal" warnings.
|
||||||
|
- `//!allow(WARNING_TYPE): REASON` or `//!ignore(WARNING_TYPE): REASON` - Global suppression for a specific warning type across the whole project (supported: `unused`, `implicit`).
|
||||||
|
- `//!cast(DEF_TYPE, CUR_TYPE): REASON` - Suppress "Type Inconsistency" errors if types match.
|
||||||
- **Structure**: A configuration is composed by one or more definitions.
|
- **Structure**: A configuration is composed by one or more definitions.
|
||||||
|
- **Strictness**: Any content that is not a valid comment (or pragma/docstring) or a valid definition (Field, Node, or Object) is **not allowed** and must generate a parsing error.
|
||||||
|
|
||||||
### Core MARTe Classes
|
### Core MARTe Classes
|
||||||
|
|
||||||
@@ -105,29 +110,33 @@ MARTe configurations typically involve several main categories of objects:
|
|||||||
- **Requirements**:
|
- **Requirements**:
|
||||||
- All signal definitions **must** include a `Type` field with a valid value.
|
- All signal definitions **must** include a `Type` field with a valid value.
|
||||||
- **Size Information**: Signals can optionally include `NumberOfDimensions` and `NumberOfElements` fields. If not explicitly defined, these default to `1`.
|
- **Size Information**: Signals can optionally include `NumberOfDimensions` and `NumberOfElements` fields. If not explicitly defined, these default to `1`.
|
||||||
|
- **Property Matching**: Signal references in GAMs must match the properties (`Type`, `NumberOfElements`, `NumberOfDimensions`) of the defined signal in the `DataSource`.
|
||||||
- **Extensibility**: Signal definitions can include additional fields as required by the specific application context.
|
- **Extensibility**: Signal definitions can include additional fields as required by the specific application context.
|
||||||
- **Signal Reference Syntax**:
|
- **Signal Reference Syntax**:
|
||||||
- Signals are referenced or defined in `InputSignals` or `OutputSignals` sub-nodes using one of the following formats:
|
- Signals are referenced or defined in `InputSignals` or `OutputSignals` sub-nodes using one of the following formats:
|
||||||
1. **Direct Reference**:
|
1. **Direct Reference (Option 1)**:
|
||||||
```
|
```
|
||||||
SIGNAL_NAME = {
|
SIGNAL_NAME = {
|
||||||
DataSource = SIGNAL_DATASOURCE
|
DataSource = DATASOURCE_NAME
|
||||||
// Other fields if necessary
|
// Other fields if necessary
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
2. **Aliased Reference**:
|
In this case, the GAM signal name is the same as the DataSource signal name.
|
||||||
|
2. **Aliased Reference (Option 2)**:
|
||||||
```
|
```
|
||||||
NAME = {
|
GAM_SIGNAL_NAME = {
|
||||||
Alias = SIGNAL_NAME
|
Alias = SIGNAL_NAME
|
||||||
DataSource = SIGNAL_DATASOURCE
|
DataSource = DATASOURCE_NAME
|
||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
In this case, `Alias` points to the DataSource signal name.
|
||||||
- **Implicit Definition Constraint**: If a signal is implicitly defined within a GAM, the `Type` field **must** be present in the reference block to define the signal's properties.
|
- **Implicit Definition Constraint**: If a signal is implicitly defined within a GAM, the `Type` field **must** be present in the reference block to define the signal's properties.
|
||||||
- **Directionality**: DataSources and their signals are directional:
|
- **Directionality**: DataSources and their signals are directional:
|
||||||
- `Input`: Only providing data.
|
- `Input` (IN): Only providing data. Signals can only be used in `InputSignals`.
|
||||||
- `Output`: Only receiving data.
|
- `Output` (OUT): Only receiving data. Signals can only be used in `OutputSignals`.
|
||||||
- `Inout`: Bidirectional data flow.
|
- `Inout` (INOUT): Bidirectional data flow. Signals can be used in both `InputSignals` and `OutputSignals`.
|
||||||
|
- **Validation**: The tool must validate that signal usage in GAMs respects the direction of the referenced DataSource.
|
||||||
|
|
||||||
### Object Indexing & References
|
### Object Indexing & References
|
||||||
|
|
||||||
@@ -183,18 +192,20 @@ The `fmt` command must format the code according to the following rules:
|
|||||||
The LSP and `check` command should report the following:
|
The LSP and `check` command should report the following:
|
||||||
|
|
||||||
- **Warnings**:
|
- **Warnings**:
|
||||||
- **Unused GAM**: A GAM is defined but not referenced in any thread or scheduler.
|
- **Unused GAM**: A GAM is defined but not referenced in any thread or scheduler. (Suppress with `//!unused`)
|
||||||
- **Unused Signal**: A signal is explicitly defined in a `DataSource` but never referenced in any `GAM`.
|
- **Unused Signal**: A signal is explicitly defined in a `DataSource` but never referenced in any `GAM`. (Suppress with `//!unused`)
|
||||||
- **Implicitly Defined Signal**: A signal is defined only within a `GAM` and not in its parent `DataSource`.
|
- **Implicitly Defined Signal**: A signal is defined only within a `GAM` and not in its parent `DataSource`. (Suppress with `//!implicit`)
|
||||||
|
|
||||||
- **Errors**:
|
- **Errors**:
|
||||||
- **Type Inconsistency**: A signal is referenced with a type different from its definition.
|
- **Type Inconsistency**: A signal is referenced with a type different from its definition. (Suppress with `//!cast`)
|
||||||
- **Size Inconsistency**: A signal is referenced with a size (dimensions/elements) different from its definition.
|
- **Size Inconsistency**: A signal is referenced with a size (dimensions/elements) different from its definition.
|
||||||
|
- **Invalid Signal Content**: The `Signals` container of a `DataSource` contains invalid elements (e.g., fields instead of nodes).
|
||||||
- **Duplicate Field Definition**: A field is defined multiple times within the same node scope (including across multiple files).
|
- **Duplicate Field Definition**: A field is defined multiple times within the same node scope (including across multiple files).
|
||||||
- **Validation Errors**:
|
- **Validation Errors**:
|
||||||
- Missing mandatory fields.
|
- Missing mandatory fields.
|
||||||
- Field type mismatches.
|
- Field type mismatches.
|
||||||
- Grammar errors (e.g., missing closing brackets).
|
- Grammar errors (e.g., missing closing brackets).
|
||||||
|
- **Invalid Function Reference**: Elements in the `Functions` array of a `State.Thread` must be valid references to defined GAM nodes.
|
||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
|
|
||||||
|
|||||||
73
test/lsp_hover_context_test.go
Normal file
73
test/lsp_hover_context_test.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/index"
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetNodeContaining(t *testing.T) {
|
||||||
|
content := `
|
||||||
|
+App = {
|
||||||
|
Class = RealTimeApplication
|
||||||
|
+State1 = {
|
||||||
|
Class = RealTimeState
|
||||||
|
+Thread1 = {
|
||||||
|
Class = RealTimeThread
|
||||||
|
Functions = { GAM1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
+GAM1 = { Class = IOGAM }
|
||||||
|
`
|
||||||
|
p := parser.NewParser(content)
|
||||||
|
config, err := p.Parse()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
idx := index.NewProjectTree()
|
||||||
|
file := "hover_context.marte"
|
||||||
|
idx.AddFile(file, config)
|
||||||
|
idx.ResolveReferences()
|
||||||
|
|
||||||
|
// Find reference to GAM1
|
||||||
|
var gamRef *index.Reference
|
||||||
|
for i := range idx.References {
|
||||||
|
ref := &idx.References[i]
|
||||||
|
if ref.Name == "GAM1" {
|
||||||
|
gamRef = ref
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if gamRef == nil {
|
||||||
|
t.Fatal("Reference to GAM1 not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check containing node
|
||||||
|
container := idx.GetNodeContaining(file, gamRef.Position)
|
||||||
|
if container == nil {
|
||||||
|
t.Fatal("Container not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if container.RealName != "+Thread1" {
|
||||||
|
t.Errorf("Expected container +Thread1, got %s", container.RealName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check traversal up to State
|
||||||
|
curr := container
|
||||||
|
foundState := false
|
||||||
|
for curr != nil {
|
||||||
|
if curr.RealName == "+State1" {
|
||||||
|
foundState = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
curr = curr.Parent
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundState {
|
||||||
|
t.Error("State parent not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,16 +5,30 @@ import (
|
|||||||
|
|
||||||
"github.com/marte-dev/marte-dev-tools/internal/index"
|
"github.com/marte-dev/marte-dev-tools/internal/index"
|
||||||
"github.com/marte-dev/marte-dev-tools/internal/parser"
|
"github.com/marte-dev/marte-dev-tools/internal/parser"
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/validator"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLSPSignalMetadata(t *testing.T) {
|
func TestLSPSignalReferences(t *testing.T) {
|
||||||
content := `
|
content := `
|
||||||
+MySignal = {
|
+Data = {
|
||||||
Class = Signal
|
Class = ReferenceContainer
|
||||||
|
+MyDS = {
|
||||||
|
Class = FileReader
|
||||||
|
Filename = "test"
|
||||||
|
Signals = {
|
||||||
|
MySig = { Type = uint32 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
+MyGAM = {
|
||||||
|
Class = IOGAM
|
||||||
|
InputSignals = {
|
||||||
|
MySig = {
|
||||||
|
DataSource = MyDS
|
||||||
Type = uint32
|
Type = uint32
|
||||||
NumberOfElements = 10
|
}
|
||||||
NumberOfDimensions = 1
|
}
|
||||||
DataSource = DDB1
|
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
p := parser.NewParser(content)
|
p := parser.NewParser(content)
|
||||||
@@ -24,26 +38,50 @@ func TestLSPSignalMetadata(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
idx := index.NewProjectTree()
|
idx := index.NewProjectTree()
|
||||||
file := "signal.marte"
|
idx.AddFile("signal_refs.marte", config)
|
||||||
idx.AddFile(file, config)
|
idx.ResolveReferences()
|
||||||
|
|
||||||
res := idx.Query(file, 2, 2) // Query +MySignal
|
v := validator.NewValidator(idx, ".")
|
||||||
if res == nil || res.Node == nil {
|
v.ValidateProject()
|
||||||
t.Fatal("Query failed for signal definition")
|
|
||||||
|
// Find definition of MySig in MyDS
|
||||||
|
root := idx.IsolatedFiles["signal_refs.marte"]
|
||||||
|
if root == nil {
|
||||||
|
t.Fatal("Root node not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
meta := res.Node.Metadata
|
// Traverse to MySig
|
||||||
if meta["Class"] != "Signal" {
|
dataNode := root.Children["Data"]
|
||||||
t.Errorf("Expected Class Signal, got %s", meta["Class"])
|
if dataNode == nil { t.Fatal("Data node not found") }
|
||||||
}
|
|
||||||
if meta["Type"] != "uint32" {
|
myDS := dataNode.Children["MyDS"]
|
||||||
t.Errorf("Expected Type uint32, got %s", meta["Type"])
|
if myDS == nil { t.Fatal("MyDS node not found") }
|
||||||
}
|
|
||||||
if meta["NumberOfElements"] != "10" {
|
signals := myDS.Children["Signals"]
|
||||||
t.Errorf("Expected 10 elements, got %s", meta["NumberOfElements"])
|
if signals == nil { t.Fatal("Signals node not found") }
|
||||||
|
|
||||||
|
mySigDef := signals.Children["MySig"]
|
||||||
|
if mySigDef == nil {
|
||||||
|
t.Fatal("Definition of MySig not found in tree")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Since handleHover logic is in internal/lsp which we can't easily test directly without
|
// Now simulate "Find References" on mySigDef
|
||||||
// exposing formatNodeInfo, we rely on the fact that Metadata is populated correctly.
|
foundRefs := 0
|
||||||
// If Metadata is correct, server.go logic (verified by code review) should display it.
|
idx.Walk(func(node *index.ProjectNode) {
|
||||||
|
if node.Target == mySigDef {
|
||||||
|
foundRefs++
|
||||||
|
// Check if node is the GAM signal
|
||||||
|
if node.RealName != "MySig" { // In GAM it is MySig
|
||||||
|
t.Errorf("Unexpected reference node name: %s", node.RealName)
|
||||||
|
}
|
||||||
|
// Check parent is InputSignals -> MyGAM
|
||||||
|
if node.Parent == nil || node.Parent.Parent == nil || node.Parent.Parent.RealName != "+MyGAM" {
|
||||||
|
t.Errorf("Reference node not in MyGAM")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if foundRefs != 1 {
|
||||||
|
t.Errorf("Expected 1 reference (Direct), found %d", foundRefs)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -140,3 +140,16 @@ func TestLSPHover(t *testing.T) {
|
|||||||
t.Errorf("Expected +MyObject, got %s", res.Node.RealName)
|
t.Errorf("Expected +MyObject, got %s", res.Node.RealName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParserError(t *testing.T) {
|
||||||
|
invalidContent := `
|
||||||
|
A = {
|
||||||
|
Field =
|
||||||
|
}
|
||||||
|
`
|
||||||
|
p := parser.NewParser(invalidContent)
|
||||||
|
_, err := p.Parse()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Expected parser error, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
74
test/validator_functions_array_test.go
Normal file
74
test/validator_functions_array_test.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/index"
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/parser"
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/validator"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFunctionsArrayValidation(t *testing.T) {
|
||||||
|
content := `
|
||||||
|
+App = {
|
||||||
|
Class = RealTimeApplication
|
||||||
|
+State = {
|
||||||
|
Class = RealTimeState
|
||||||
|
+Thread = {
|
||||||
|
Class = RealTimeThread
|
||||||
|
Functions = {
|
||||||
|
ValidGAM,
|
||||||
|
InvalidGAM, // Not a GAM (DataSource)
|
||||||
|
MissingGAM, // Not found
|
||||||
|
"String", // Not reference
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
+ValidGAM = { Class = IOGAM InputSignals = {} }
|
||||||
|
+InvalidGAM = { Class = FileReader }
|
||||||
|
`
|
||||||
|
p := parser.NewParser(content)
|
||||||
|
config, err := p.Parse()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
idx := index.NewProjectTree()
|
||||||
|
idx.AddFile("funcs.marte", config)
|
||||||
|
idx.ResolveReferences()
|
||||||
|
|
||||||
|
v := validator.NewValidator(idx, ".")
|
||||||
|
v.ValidateProject()
|
||||||
|
|
||||||
|
foundInvalid := false
|
||||||
|
foundMissing := false
|
||||||
|
foundNotRef := false
|
||||||
|
|
||||||
|
for _, d := range v.Diagnostics {
|
||||||
|
if strings.Contains(d.Message, "not found or is not a valid GAM") {
|
||||||
|
// This covers both InvalidGAM and MissingGAM cases
|
||||||
|
if strings.Contains(d.Message, "InvalidGAM") {
|
||||||
|
foundInvalid = true
|
||||||
|
}
|
||||||
|
if strings.Contains(d.Message, "MissingGAM") {
|
||||||
|
foundMissing = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(d.Message, "must contain references") {
|
||||||
|
foundNotRef = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundInvalid {
|
||||||
|
t.Error("Expected error for InvalidGAM")
|
||||||
|
}
|
||||||
|
if !foundMissing {
|
||||||
|
t.Error("Expected error for MissingGAM")
|
||||||
|
}
|
||||||
|
if !foundNotRef {
|
||||||
|
t.Error("Expected error for non-reference element")
|
||||||
|
}
|
||||||
|
}
|
||||||
81
test/validator_gam_signals_linking_test.go
Normal file
81
test/validator_gam_signals_linking_test.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/index"
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/parser"
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/validator"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGAMSignalLinking(t *testing.T) {
|
||||||
|
content := `
|
||||||
|
+Data = {
|
||||||
|
Class = ReferenceContainer
|
||||||
|
+MyDS = {
|
||||||
|
Class = FileReader
|
||||||
|
Filename = "test.txt"
|
||||||
|
Signals = {
|
||||||
|
MySig = { Type = uint32 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
+MyGAM = {
|
||||||
|
Class = IOGAM
|
||||||
|
InputSignals = {
|
||||||
|
MySig = {
|
||||||
|
DataSource = MyDS
|
||||||
|
Type = uint32
|
||||||
|
}
|
||||||
|
AliasedSig = {
|
||||||
|
Alias = MySig
|
||||||
|
DataSource = MyDS
|
||||||
|
Type = uint32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
p := parser.NewParser(content)
|
||||||
|
config, err := p.Parse()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
idx := index.NewProjectTree()
|
||||||
|
idx.AddFile("gam_signals_linking.marte", config)
|
||||||
|
idx.ResolveReferences()
|
||||||
|
|
||||||
|
v := validator.NewValidator(idx, ".")
|
||||||
|
v.ValidateProject()
|
||||||
|
|
||||||
|
if len(v.Diagnostics) > 0 {
|
||||||
|
for _, d := range v.Diagnostics {
|
||||||
|
t.Logf("Diagnostic: %s", d.Message)
|
||||||
|
}
|
||||||
|
t.Fatalf("Validation failed with %d issues", len(v.Diagnostics))
|
||||||
|
}
|
||||||
|
|
||||||
|
foundMyDSRef := 0
|
||||||
|
foundAliasRef := 0
|
||||||
|
|
||||||
|
for _, ref := range idx.References {
|
||||||
|
if ref.Name == "MyDS" {
|
||||||
|
if ref.Target != nil && ref.Target.RealName == "+MyDS" {
|
||||||
|
foundMyDSRef++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ref.Name == "MySig" {
|
||||||
|
if ref.Target != nil && ref.Target.RealName == "MySig" {
|
||||||
|
foundAliasRef++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if foundMyDSRef < 2 {
|
||||||
|
t.Errorf("Expected at least 2 resolved MyDS references, found %d", foundMyDSRef)
|
||||||
|
}
|
||||||
|
if foundAliasRef < 1 {
|
||||||
|
t.Errorf("Expected at least 1 resolved Alias MySig reference, found %d", foundAliasRef)
|
||||||
|
}
|
||||||
|
}
|
||||||
108
test/validator_gam_signals_test.go
Normal file
108
test/validator_gam_signals_test.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/index"
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/parser"
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/validator"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGAMSignalValidation(t *testing.T) {
|
||||||
|
content := `
|
||||||
|
+Data = {
|
||||||
|
Class = ReferenceContainer
|
||||||
|
+InDS = {
|
||||||
|
Class = FileReader
|
||||||
|
Signals = {
|
||||||
|
SigIn = { Type = uint32 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
+OutDS = {
|
||||||
|
Class = FileWriter
|
||||||
|
Signals = {
|
||||||
|
SigOut = { Type = uint32 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
+MyGAM = {
|
||||||
|
Class = IOGAM
|
||||||
|
InputSignals = {
|
||||||
|
SigIn = {
|
||||||
|
DataSource = InDS
|
||||||
|
Type = uint32
|
||||||
|
}
|
||||||
|
// Error: OutDS is OUT only
|
||||||
|
BadInput = {
|
||||||
|
DataSource = OutDS
|
||||||
|
Alias = SigOut
|
||||||
|
Type = uint32
|
||||||
|
}
|
||||||
|
// Error: MissingSig not in InDS
|
||||||
|
Missing = {
|
||||||
|
DataSource = InDS
|
||||||
|
Alias = MissingSig
|
||||||
|
Type = uint32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OutputSignals = {
|
||||||
|
SigOut = {
|
||||||
|
DataSource = OutDS
|
||||||
|
Type = uint32
|
||||||
|
}
|
||||||
|
// Error: InDS is IN only
|
||||||
|
BadOutput = {
|
||||||
|
DataSource = InDS
|
||||||
|
Alias = SigIn
|
||||||
|
Type = uint32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
p := parser.NewParser(content)
|
||||||
|
config, err := p.Parse()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
idx := index.NewProjectTree()
|
||||||
|
idx.AddFile("gam_signals.marte", config)
|
||||||
|
idx.ResolveReferences()
|
||||||
|
|
||||||
|
v := validator.NewValidator(idx, ".")
|
||||||
|
v.ValidateProject()
|
||||||
|
|
||||||
|
foundBadInput := false
|
||||||
|
foundMissing := false
|
||||||
|
foundBadOutput := false
|
||||||
|
|
||||||
|
for _, d := range v.Diagnostics {
|
||||||
|
if strings.Contains(d.Message, "DataSource 'OutDS' (Class FileWriter) is Output-only but referenced in InputSignals") {
|
||||||
|
foundBadInput = true
|
||||||
|
}
|
||||||
|
if strings.Contains(d.Message, "Signal 'MissingSig' not found in DataSource 'InDS'") {
|
||||||
|
foundMissing = true
|
||||||
|
}
|
||||||
|
if strings.Contains(d.Message, "DataSource 'InDS' (Class FileReader) is Input-only but referenced in OutputSignals") {
|
||||||
|
foundBadOutput = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundBadInput || !foundMissing || !foundBadOutput {
|
||||||
|
for _, d := range v.Diagnostics {
|
||||||
|
t.Logf("Diagnostic: %s", d.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundBadInput {
|
||||||
|
t.Error("Expected error for OutDS in InputSignals")
|
||||||
|
}
|
||||||
|
if !foundMissing {
|
||||||
|
t.Error("Expected error for missing signal reference")
|
||||||
|
}
|
||||||
|
if !foundBadOutput {
|
||||||
|
t.Error("Expected error for InDS in OutputSignals")
|
||||||
|
}
|
||||||
|
}
|
||||||
65
test/validator_global_pragma_debug_test.go
Normal file
65
test/validator_global_pragma_debug_test.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/index"
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/parser"
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/validator"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGlobalPragmaDebug(t *testing.T) {
|
||||||
|
content := `//! allow(implicit): Debugging
|
||||||
|
//! allow(unused): Debugging
|
||||||
|
+Data={Class=ReferenceContainer}
|
||||||
|
+GAM={Class=IOGAM InputSignals={Impl={DataSource=Data Type=uint32}}}
|
||||||
|
+UnusedGAM={Class=IOGAM}
|
||||||
|
`
|
||||||
|
p := parser.NewParser(content)
|
||||||
|
config, err := p.Parse()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if pragma parsed
|
||||||
|
if len(config.Pragmas) == 0 {
|
||||||
|
t.Fatal("Pragma not parsed")
|
||||||
|
}
|
||||||
|
t.Logf("Parsed Pragma 0: %s", config.Pragmas[0].Text)
|
||||||
|
|
||||||
|
idx := index.NewProjectTree()
|
||||||
|
idx.AddFile("debug.marte", config)
|
||||||
|
idx.ResolveReferences()
|
||||||
|
|
||||||
|
// Check if added to GlobalPragmas
|
||||||
|
pragmas, ok := idx.GlobalPragmas["debug.marte"]
|
||||||
|
if !ok || len(pragmas) == 0 {
|
||||||
|
t.Fatal("GlobalPragmas not populated")
|
||||||
|
}
|
||||||
|
t.Logf("Global Pragma stored: %s", pragmas[0])
|
||||||
|
|
||||||
|
v := validator.NewValidator(idx, ".")
|
||||||
|
v.ValidateProject()
|
||||||
|
v.CheckUnused() // Must call this for unused check!
|
||||||
|
|
||||||
|
foundImplicitWarning := false
|
||||||
|
foundUnusedWarning := false
|
||||||
|
for _, d := range v.Diagnostics {
|
||||||
|
if strings.Contains(d.Message, "Implicitly Defined Signal") {
|
||||||
|
foundImplicitWarning = true
|
||||||
|
t.Logf("Found warning: %s", d.Message)
|
||||||
|
}
|
||||||
|
if strings.Contains(d.Message, "Unused GAM") {
|
||||||
|
foundUnusedWarning = true
|
||||||
|
t.Logf("Found warning: %s", d.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if foundImplicitWarning {
|
||||||
|
t.Error("Expected implicit warning to be suppressed")
|
||||||
|
}
|
||||||
|
if foundUnusedWarning {
|
||||||
|
t.Error("Expected unused warning to be suppressed")
|
||||||
|
}
|
||||||
|
}
|
||||||
67
test/validator_global_pragma_test.go
Normal file
67
test/validator_global_pragma_test.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/index"
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/parser"
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/validator"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGlobalPragma(t *testing.T) {
|
||||||
|
content := `
|
||||||
|
//!allow(unused): Suppress all unused
|
||||||
|
//!allow(implicit): Suppress all implicit
|
||||||
|
|
||||||
|
+Data = {
|
||||||
|
Class = ReferenceContainer
|
||||||
|
+MyDS = {
|
||||||
|
Class = FileReader
|
||||||
|
Filename = "test"
|
||||||
|
Signals = {
|
||||||
|
UnusedSig = { Type = uint32 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
+MyGAM = {
|
||||||
|
Class = IOGAM
|
||||||
|
InputSignals = {
|
||||||
|
ImplicitSig = { DataSource = MyDS Type = uint32 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
p := parser.NewParser(content)
|
||||||
|
config, err := p.Parse()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
idx := index.NewProjectTree()
|
||||||
|
idx.AddFile("global_pragma.marte", config)
|
||||||
|
idx.ResolveReferences()
|
||||||
|
|
||||||
|
v := validator.NewValidator(idx, ".")
|
||||||
|
v.ValidateProject()
|
||||||
|
v.CheckUnused()
|
||||||
|
|
||||||
|
foundUnusedWarning := false
|
||||||
|
foundImplicitWarning := false
|
||||||
|
|
||||||
|
for _, d := range v.Diagnostics {
|
||||||
|
if strings.Contains(d.Message, "Unused Signal") {
|
||||||
|
foundUnusedWarning = true
|
||||||
|
}
|
||||||
|
if strings.Contains(d.Message, "Implicitly Defined Signal") {
|
||||||
|
foundImplicitWarning = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if foundUnusedWarning {
|
||||||
|
t.Error("Expected warning for UnusedSig to be suppressed globally")
|
||||||
|
}
|
||||||
|
if foundImplicitWarning {
|
||||||
|
t.Error("Expected warning for ImplicitSig to be suppressed globally")
|
||||||
|
}
|
||||||
|
}
|
||||||
75
test/validator_global_pragma_update_test.go
Normal file
75
test/validator_global_pragma_update_test.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/index"
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/parser"
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/validator"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGlobalPragmaUpdate(t *testing.T) {
|
||||||
|
// Scenario: Project scope. File A has pragma. File B has warning.
|
||||||
|
|
||||||
|
fileA := "fileA.marte"
|
||||||
|
contentA_WithPragma := `
|
||||||
|
#package my.project
|
||||||
|
//!allow(unused): Suppress
|
||||||
|
`
|
||||||
|
contentA_NoPragma := `
|
||||||
|
#package my.project
|
||||||
|
// No pragma
|
||||||
|
`
|
||||||
|
|
||||||
|
fileB := "fileB.marte"
|
||||||
|
contentB := `
|
||||||
|
#package my.project
|
||||||
|
+Data={Class=ReferenceContainer +DS={Class=FileReader Filename="t" Signals={Unused={Type=uint32}}}}
|
||||||
|
`
|
||||||
|
|
||||||
|
idx := index.NewProjectTree()
|
||||||
|
|
||||||
|
// Helper to validate
|
||||||
|
check := func() bool {
|
||||||
|
idx.ResolveReferences()
|
||||||
|
v := validator.NewValidator(idx, ".")
|
||||||
|
v.ValidateProject()
|
||||||
|
v.CheckUnused()
|
||||||
|
for _, d := range v.Diagnostics {
|
||||||
|
if strings.Contains(d.Message, "Unused Signal") {
|
||||||
|
return true // Found warning
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Add A (with pragma) and B
|
||||||
|
pA := parser.NewParser(contentA_WithPragma)
|
||||||
|
cA, _ := pA.Parse()
|
||||||
|
idx.AddFile(fileA, cA)
|
||||||
|
|
||||||
|
pB := parser.NewParser(contentB)
|
||||||
|
cB, _ := pB.Parse()
|
||||||
|
idx.AddFile(fileB, cB)
|
||||||
|
|
||||||
|
if check() {
|
||||||
|
t.Error("Step 1: Expected warning to be suppressed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Update A (remove pragma)
|
||||||
|
pA2 := parser.NewParser(contentA_NoPragma)
|
||||||
|
cA2, _ := pA2.Parse()
|
||||||
|
idx.AddFile(fileA, cA2)
|
||||||
|
|
||||||
|
if !check() {
|
||||||
|
t.Error("Step 2: Expected warning to appear")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Update A (add pragma back)
|
||||||
|
idx.AddFile(fileA, cA) // Re-use config A
|
||||||
|
|
||||||
|
if check() {
|
||||||
|
t.Error("Step 3: Expected warning to be suppressed again")
|
||||||
|
}
|
||||||
|
}
|
||||||
59
test/validator_ignore_pragma_test.go
Normal file
59
test/validator_ignore_pragma_test.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/index"
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/parser"
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/validator"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIgnorePragma(t *testing.T) {
|
||||||
|
content := `
|
||||||
|
//!ignore(unused): Suppress global unused
|
||||||
|
+Data = {
|
||||||
|
Class = ReferenceContainer
|
||||||
|
+MyDS = {
|
||||||
|
Class = FileReader
|
||||||
|
Filename = "test"
|
||||||
|
Signals = {
|
||||||
|
Unused1 = { Type = uint32 }
|
||||||
|
|
||||||
|
//!ignore(unused): Suppress local unused
|
||||||
|
Unused2 = { Type = uint32 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
+MyGAM = {
|
||||||
|
Class = IOGAM
|
||||||
|
InputSignals = {
|
||||||
|
//!ignore(implicit): Suppress local implicit
|
||||||
|
ImplicitSig = { DataSource = MyDS Type = uint32 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
p := parser.NewParser(content)
|
||||||
|
config, err := p.Parse()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
idx := index.NewProjectTree()
|
||||||
|
idx.AddFile("ignore.marte", config)
|
||||||
|
idx.ResolveReferences()
|
||||||
|
|
||||||
|
v := validator.NewValidator(idx, ".")
|
||||||
|
v.ValidateProject()
|
||||||
|
v.CheckUnused()
|
||||||
|
|
||||||
|
for _, d := range v.Diagnostics {
|
||||||
|
if strings.Contains(d.Message, "Unused Signal") {
|
||||||
|
t.Errorf("Unexpected warning: %s", d.Message)
|
||||||
|
}
|
||||||
|
if strings.Contains(d.Message, "Implicitly Defined Signal") {
|
||||||
|
t.Errorf("Unexpected warning: %s", d.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
107
test/validator_implicit_signal_test.go
Normal file
107
test/validator_implicit_signal_test.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/index"
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/parser"
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/validator"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestImplicitSignal(t *testing.T) {
|
||||||
|
content := `
|
||||||
|
+Data = {
|
||||||
|
Class = ReferenceContainer
|
||||||
|
+MyDS = {
|
||||||
|
Class = FileReader
|
||||||
|
Filename = "test"
|
||||||
|
Signals = {
|
||||||
|
ExplicitSig = { Type = uint32 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
+MyGAM = {
|
||||||
|
Class = IOGAM
|
||||||
|
InputSignals = {
|
||||||
|
ExplicitSig = {
|
||||||
|
DataSource = MyDS
|
||||||
|
Type = uint32
|
||||||
|
}
|
||||||
|
ImplicitSig = {
|
||||||
|
DataSource = MyDS
|
||||||
|
Type = uint32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
p := parser.NewParser(content)
|
||||||
|
config, err := p.Parse()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
idx := index.NewProjectTree()
|
||||||
|
idx.AddFile("implicit_signal.marte", config)
|
||||||
|
idx.ResolveReferences()
|
||||||
|
|
||||||
|
v := validator.NewValidator(idx, ".")
|
||||||
|
v.ValidateProject()
|
||||||
|
|
||||||
|
foundWarning := false
|
||||||
|
foundError := false
|
||||||
|
|
||||||
|
for _, d := range v.Diagnostics {
|
||||||
|
if strings.Contains(d.Message, "Implicitly Defined Signal") {
|
||||||
|
if strings.Contains(d.Message, "ImplicitSig") {
|
||||||
|
foundWarning = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(d.Message, "Signal 'ExplicitSig' not found") {
|
||||||
|
foundError = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundWarning || foundError {
|
||||||
|
for _, d := range v.Diagnostics {
|
||||||
|
t.Logf("Diagnostic: %s", d.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundWarning {
|
||||||
|
t.Error("Expected warning for ImplicitSig")
|
||||||
|
}
|
||||||
|
if foundError {
|
||||||
|
t.Error("Unexpected error for ExplicitSig")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test missing Type for implicit
|
||||||
|
contentMissingType := `
|
||||||
|
+Data = { Class = ReferenceContainer +DS={Class=FileReader Filename="" Signals={}} }
|
||||||
|
+GAM = { Class = IOGAM InputSignals = { Impl = { DataSource = DS } } }
|
||||||
|
`
|
||||||
|
p2 := parser.NewParser(contentMissingType)
|
||||||
|
config2, err2 := p2.Parse()
|
||||||
|
if err2 != nil {
|
||||||
|
t.Fatalf("Parse2 failed: %v", err2)
|
||||||
|
}
|
||||||
|
idx2 := index.NewProjectTree()
|
||||||
|
idx2.AddFile("missing_type.marte", config2)
|
||||||
|
idx2.ResolveReferences()
|
||||||
|
v2 := validator.NewValidator(idx2, ".")
|
||||||
|
v2.ValidateProject()
|
||||||
|
|
||||||
|
foundTypeErr := false
|
||||||
|
for _, d := range v2.Diagnostics {
|
||||||
|
if strings.Contains(d.Message, "Implicit signal 'Impl' must define Type") {
|
||||||
|
foundTypeErr = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundTypeErr {
|
||||||
|
for _, d := range v2.Diagnostics {
|
||||||
|
t.Logf("Diagnostic2: %s", d.Message)
|
||||||
|
}
|
||||||
|
t.Error("Expected error for missing Type in implicit signal")
|
||||||
|
}
|
||||||
|
}
|
||||||
69
test/validator_pragma_test.go
Normal file
69
test/validator_pragma_test.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/index"
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/parser"
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/validator"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPragmaSuppression(t *testing.T) {
|
||||||
|
content := `
|
||||||
|
+Data = {
|
||||||
|
Class = ReferenceContainer
|
||||||
|
+MyDS = {
|
||||||
|
Class = FileReader
|
||||||
|
Filename = "test"
|
||||||
|
Signals = {
|
||||||
|
//!unused: Ignore this
|
||||||
|
UnusedSig = { Type = uint32 }
|
||||||
|
UsedSig = { Type = uint32 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
+MyGAM = {
|
||||||
|
Class = IOGAM
|
||||||
|
InputSignals = {
|
||||||
|
UsedSig = { DataSource = MyDS Type = uint32 }
|
||||||
|
|
||||||
|
//!implicit: Ignore this implicit
|
||||||
|
ImplicitSig = { DataSource = MyDS Type = uint32 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
p := parser.NewParser(content)
|
||||||
|
config, err := p.Parse()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
idx := index.NewProjectTree()
|
||||||
|
idx.AddFile("pragma.marte", config)
|
||||||
|
idx.ResolveReferences()
|
||||||
|
|
||||||
|
v := validator.NewValidator(idx, ".")
|
||||||
|
v.ValidateProject()
|
||||||
|
v.CheckUnused()
|
||||||
|
|
||||||
|
foundUnusedWarning := false
|
||||||
|
foundImplicitWarning := false
|
||||||
|
|
||||||
|
for _, d := range v.Diagnostics {
|
||||||
|
if strings.Contains(d.Message, "Unused Signal") && strings.Contains(d.Message, "UnusedSig") {
|
||||||
|
foundUnusedWarning = true
|
||||||
|
}
|
||||||
|
if strings.Contains(d.Message, "Implicitly Defined Signal") && strings.Contains(d.Message, "ImplicitSig") {
|
||||||
|
foundImplicitWarning = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if foundUnusedWarning {
|
||||||
|
t.Error("Expected warning for UnusedSig to be suppressed")
|
||||||
|
}
|
||||||
|
if foundImplicitWarning {
|
||||||
|
t.Error("Expected warning for ImplicitSig to be suppressed")
|
||||||
|
}
|
||||||
|
}
|
||||||
108
test/validator_signal_properties_test.go
Normal file
108
test/validator_signal_properties_test.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/index"
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/parser"
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/validator"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSignalProperties(t *testing.T) {
|
||||||
|
content := `
|
||||||
|
+Data = {
|
||||||
|
Class = ReferenceContainer
|
||||||
|
+MyDS = {
|
||||||
|
Class = FileReader
|
||||||
|
Filename = "test"
|
||||||
|
Signals = {
|
||||||
|
Correct = { Type = uint32 NumberOfElements = 10 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
+MyGAM = {
|
||||||
|
Class = IOGAM
|
||||||
|
InputSignals = {
|
||||||
|
// Correct reference
|
||||||
|
Correct = { DataSource = MyDS Type = uint32 NumberOfElements = 10 }
|
||||||
|
|
||||||
|
// Mismatch Type
|
||||||
|
BadType = {
|
||||||
|
Alias = Correct
|
||||||
|
DataSource = MyDS
|
||||||
|
Type = float32 // Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mismatch Elements
|
||||||
|
BadElements = {
|
||||||
|
Alias = Correct
|
||||||
|
DataSource = MyDS
|
||||||
|
Type = uint32
|
||||||
|
NumberOfElements = 20 // Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid Cast
|
||||||
|
//!cast(uint32, float32): Cast reason
|
||||||
|
CastSig = {
|
||||||
|
Alias = Correct
|
||||||
|
DataSource = MyDS
|
||||||
|
Type = float32 // OK
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalid Cast (Wrong definition type in pragma)
|
||||||
|
//!cast(int32, float32): Wrong def type
|
||||||
|
BadCast = {
|
||||||
|
Alias = Correct
|
||||||
|
DataSource = MyDS
|
||||||
|
Type = float32 // Error because pragma mismatch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
p := parser.NewParser(content)
|
||||||
|
config, err := p.Parse()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
idx := index.NewProjectTree()
|
||||||
|
idx.AddFile("signal_props.marte", config)
|
||||||
|
idx.ResolveReferences()
|
||||||
|
|
||||||
|
v := validator.NewValidator(idx, ".")
|
||||||
|
v.ValidateProject()
|
||||||
|
|
||||||
|
foundBadType := false
|
||||||
|
foundBadElements := false
|
||||||
|
foundBadCast := false
|
||||||
|
|
||||||
|
for _, d := range v.Diagnostics {
|
||||||
|
if strings.Contains(d.Message, "property 'Type' mismatch") {
|
||||||
|
if strings.Contains(d.Message, "'BadType'") {
|
||||||
|
foundBadType = true
|
||||||
|
}
|
||||||
|
if strings.Contains(d.Message, "'BadCast'") {
|
||||||
|
foundBadCast = true
|
||||||
|
}
|
||||||
|
if strings.Contains(d.Message, "'CastSig'") {
|
||||||
|
t.Error("Unexpected error for CastSig (should be suppressed by pragma)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(d.Message, "property 'NumberOfElements' mismatch") {
|
||||||
|
foundBadElements = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundBadType {
|
||||||
|
t.Error("Expected error for BadType")
|
||||||
|
}
|
||||||
|
if !foundBadElements {
|
||||||
|
t.Error("Expected error for BadElements")
|
||||||
|
}
|
||||||
|
if !foundBadCast {
|
||||||
|
t.Error("Expected error for BadCast (pragma mismatch)")
|
||||||
|
}
|
||||||
|
}
|
||||||
73
test/validator_signal_test.go
Normal file
73
test/validator_signal_test.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/index"
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/parser"
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/validator"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSignalValidation(t *testing.T) {
|
||||||
|
content := `
|
||||||
|
+Data = {
|
||||||
|
Class = ReferenceContainer
|
||||||
|
+ValidDS = {
|
||||||
|
Class = DataSource
|
||||||
|
Signals = {
|
||||||
|
ValidSig = {
|
||||||
|
Type = uint32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
+MissingTypeDS = {
|
||||||
|
Class = DataSource
|
||||||
|
Signals = {
|
||||||
|
InvalidSig = {
|
||||||
|
// Missing Type
|
||||||
|
Dummy = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
+InvalidTypeDS = {
|
||||||
|
Class = DataSource
|
||||||
|
Signals = {
|
||||||
|
InvalidSig = {
|
||||||
|
Type = invalid_type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
p := parser.NewParser(content)
|
||||||
|
config, err := p.Parse()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
idx := index.NewProjectTree()
|
||||||
|
idx.AddFile("signal_test.marte", config)
|
||||||
|
|
||||||
|
v := validator.NewValidator(idx, ".")
|
||||||
|
v.ValidateProject()
|
||||||
|
|
||||||
|
foundMissing := false
|
||||||
|
foundInvalid := false
|
||||||
|
|
||||||
|
for _, d := range v.Diagnostics {
|
||||||
|
if strings.Contains(d.Message, "missing mandatory field 'Type'") {
|
||||||
|
foundMissing = true
|
||||||
|
}
|
||||||
|
if strings.Contains(d.Message, "Invalid Type 'invalid_type'") {
|
||||||
|
foundInvalid = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundMissing {
|
||||||
|
t.Error("Expected error for missing Type field in Signal")
|
||||||
|
}
|
||||||
|
if !foundInvalid {
|
||||||
|
t.Error("Expected error for invalid Type value in Signal")
|
||||||
|
}
|
||||||
|
}
|
||||||
59
test/validator_signals_content_test.go
Normal file
59
test/validator_signals_content_test.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/index"
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/parser"
|
||||||
|
"github.com/marte-dev/marte-dev-tools/internal/validator"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSignalsContentValidation(t *testing.T) {
|
||||||
|
content := `
|
||||||
|
+Data = {
|
||||||
|
Class = ReferenceContainer
|
||||||
|
+BadDS = {
|
||||||
|
Class = DataSource
|
||||||
|
Signals = {
|
||||||
|
BadField = 1
|
||||||
|
BadArray = { 1 2 }
|
||||||
|
// Valid signal
|
||||||
|
ValidSig = {
|
||||||
|
Type = uint32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
p := parser.NewParser(content)
|
||||||
|
config, err := p.Parse()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
idx := index.NewProjectTree()
|
||||||
|
idx.AddFile("signals_content.marte", config)
|
||||||
|
|
||||||
|
v := validator.NewValidator(idx, ".")
|
||||||
|
v.ValidateProject()
|
||||||
|
|
||||||
|
foundBadField := false
|
||||||
|
foundBadArray := false
|
||||||
|
|
||||||
|
for _, d := range v.Diagnostics {
|
||||||
|
if strings.Contains(d.Message, "Field 'BadField' is not allowed") {
|
||||||
|
foundBadField = true
|
||||||
|
}
|
||||||
|
if strings.Contains(d.Message, "Field 'BadArray' is not allowed") {
|
||||||
|
foundBadArray = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundBadField {
|
||||||
|
t.Error("Expected error for BadField in Signals")
|
||||||
|
}
|
||||||
|
if !foundBadArray {
|
||||||
|
t.Error("Expected error for BadArray in Signals")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user