Compare commits

..

3 Commits

Author SHA1 Message Date
Martino Ferrari
fed39467fd improved doc and tests 2026-01-27 15:19:49 +01:00
Martino Ferrari
15afdc91f4 Improved performances and hover 2026-01-27 15:14:47 +01:00
Martino Ferrari
213fc81cfb Improving LSP 2026-01-27 14:42:46 +01:00
14 changed files with 729 additions and 87 deletions

View File

@@ -77,6 +77,10 @@ package schema
#Classes: { #Classes: {
MyCustomGAM: { MyCustomGAM: {
#meta: {
direction: "INOUT"
multithreaded: true
}
Param1: int Param1: int
Param2?: string Param2?: string
... ...

View File

@@ -86,7 +86,6 @@ func runCheck(args []string) {
// Legacy loop removed as ValidateProject covers it via recursion // Legacy loop removed as ValidateProject covers it via recursion
v.CheckUnused()
for _, diag := range v.Diagnostics { for _, diag := range v.Diagnostics {
level := "ERROR" level := "ERROR"

View File

@@ -13,6 +13,7 @@ type ProjectTree struct {
References []Reference References []Reference
IsolatedFiles map[string]*ProjectNode IsolatedFiles map[string]*ProjectNode
GlobalPragmas map[string][]string GlobalPragmas map[string][]string
NodeMap map[string][]*ProjectNode
} }
func (pt *ProjectTree) ScanDirectory(rootPath string) error { func (pt *ProjectTree) ScanDirectory(rootPath string) error {
@@ -385,7 +386,19 @@ func (pt *ProjectTree) indexValue(file string, val parser.Value) {
} }
} }
func (pt *ProjectTree) RebuildIndex() {
pt.NodeMap = make(map[string][]*ProjectNode)
visitor := func(n *ProjectNode) {
pt.NodeMap[n.Name] = append(pt.NodeMap[n.Name], n)
if n.RealName != n.Name {
pt.NodeMap[n.RealName] = append(pt.NodeMap[n.RealName], n)
}
}
pt.Walk(visitor)
}
func (pt *ProjectTree) ResolveReferences() { func (pt *ProjectTree) ResolveReferences() {
pt.RebuildIndex()
for i := range pt.References { for i := range pt.References {
ref := &pt.References[i] ref := &pt.References[i]
if isoNode, ok := pt.IsolatedFiles[ref.File]; ok { if isoNode, ok := pt.IsolatedFiles[ref.File]; ok {
@@ -397,14 +410,21 @@ func (pt *ProjectTree) ResolveReferences() {
} }
func (pt *ProjectTree) FindNode(root *ProjectNode, name string, predicate func(*ProjectNode) bool) *ProjectNode { func (pt *ProjectTree) FindNode(root *ProjectNode, name string, predicate func(*ProjectNode) bool) *ProjectNode {
if pt.NodeMap == nil {
pt.RebuildIndex()
}
if strings.Contains(name, ".") { if strings.Contains(name, ".") {
parts := strings.Split(name, ".") parts := strings.Split(name, ".")
rootName := parts[0] rootName := parts[0]
var candidates []*ProjectNode candidates := pt.NodeMap[rootName]
pt.findAllNodes(root, rootName, &candidates)
for _, cand := range candidates { for _, cand := range candidates {
if !pt.isDescendant(cand, root) {
continue
}
curr := cand curr := cand
valid := true valid := true
for i := 1; i < len(parts); i++ { for i := 1; i < len(parts); i++ {
@@ -426,26 +446,33 @@ func (pt *ProjectTree) FindNode(root *ProjectNode, name string, predicate func(*
return nil return nil
} }
if root.RealName == name || root.Name == name { candidates := pt.NodeMap[name]
if predicate == nil || predicate(root) { for _, cand := range candidates {
return root if !pt.isDescendant(cand, root) {
continue
} }
} if predicate == nil || predicate(cand) {
for _, child := range root.Children { return cand
if res := pt.FindNode(child, name, predicate); res != nil {
return res
} }
} }
return nil return nil
} }
func (pt *ProjectTree) findAllNodes(root *ProjectNode, name string, results *[]*ProjectNode) { func (pt *ProjectTree) isDescendant(node, root *ProjectNode) bool {
if root.RealName == name || root.Name == name { if node == root {
*results = append(*results, root) return true
} }
for _, child := range root.Children { if root == nil {
pt.findAllNodes(child, name, results) return true
} }
curr := node
for curr != nil {
if curr == root {
return true
}
curr = curr.Parent
}
return false
} }
type QueryResult struct { type QueryResult struct {

View File

@@ -49,6 +49,7 @@ var Tree = index.NewProjectTree()
var Documents = make(map[string]string) var Documents = make(map[string]string)
var ProjectRoot string var ProjectRoot string
var GlobalSchema *schema.Schema var GlobalSchema *schema.Schema
var Output io.Writer = os.Stdout
type JsonRpcMessage struct { type JsonRpcMessage struct {
Jsonrpc string `json:"jsonrpc"` Jsonrpc string `json:"jsonrpc"`
@@ -91,6 +92,8 @@ type VersionedTextDocumentIdentifier struct {
} }
type TextDocumentContentChangeEvent struct { type TextDocumentContentChangeEvent struct {
Range *Range `json:"range,omitempty"`
RangeLength int `json:"rangeLength,omitempty"`
Text string `json:"text"` Text string `json:"text"`
} }
@@ -252,7 +255,7 @@ func HandleMessage(msg *JsonRpcMessage) {
respond(msg.ID, map[string]any{ respond(msg.ID, map[string]any{
"capabilities": map[string]any{ "capabilities": map[string]any{
"textDocumentSync": 1, // Full sync "textDocumentSync": 2, // Incremental sync
"hoverProvider": true, "hoverProvider": true,
"definitionProvider": true, "definitionProvider": true,
"referencesProvider": true, "referencesProvider": true,
@@ -346,28 +349,76 @@ func HandleDidOpen(params DidOpenTextDocumentParams) {
} }
func HandleDidChange(params DidChangeTextDocumentParams) { func HandleDidChange(params DidChangeTextDocumentParams) {
if len(params.ContentChanges) == 0 { uri := params.TextDocument.URI
return text, ok := Documents[uri]
if !ok {
// If not found, rely on full sync being first or error
} }
text := params.ContentChanges[0].Text
Documents[params.TextDocument.URI] = text for _, change := range params.ContentChanges {
path := uriToPath(params.TextDocument.URI) if change.Range == nil {
text = change.Text
} else {
text = applyContentChange(text, change)
}
}
Documents[uri] = text
path := uriToPath(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) publishParserError(uri, err)
} else { } else {
publishParserError(params.TextDocument.URI, nil) publishParserError(uri, nil)
} }
if config != nil { if config != nil {
Tree.AddFile(path, config) Tree.AddFile(path, config)
Tree.ResolveReferences() Tree.ResolveReferences()
runValidation(params.TextDocument.URI) runValidation(uri)
} }
} }
func applyContentChange(text string, change TextDocumentContentChangeEvent) string {
startOffset := offsetAt(text, change.Range.Start)
endOffset := offsetAt(text, change.Range.End)
if startOffset == -1 || endOffset == -1 {
return text
}
return text[:startOffset] + change.Text + text[endOffset:]
}
func offsetAt(text string, pos Position) int {
line := 0
col := 0
for i, r := range text {
if line == pos.Line && col == pos.Character {
return i
}
if line > pos.Line {
break
}
if r == '\n' {
line++
col = 0
} else {
if r >= 0x10000 {
col += 2
} else {
col++
}
}
}
if line == pos.Line && col == pos.Character {
return len(text)
}
return -1
}
func HandleFormatting(params DocumentFormattingParams) []TextEdit { func HandleFormatting(params DocumentFormattingParams) []TextEdit {
uri := params.TextDocument.URI uri := params.TextDocument.URI
text, ok := Documents[uri] text, ok := Documents[uri]
@@ -404,7 +455,6 @@ func HandleFormatting(params DocumentFormattingParams) []TextEdit {
func runValidation(_ string) { func runValidation(_ string) {
v := validator.NewValidator(Tree, ProjectRoot) v := validator.NewValidator(Tree, ProjectRoot)
v.ValidateProject() v.ValidateProject()
v.CheckUnused()
// Group diagnostics by file // Group diagnostics by file
fileDiags := make(map[string][]LSPDiagnostic) fileDiags := make(map[string][]LSPDiagnostic)
@@ -646,7 +696,7 @@ func suggestGAMSignals(_ *index.ProjectNode, direction string) *CompletionList {
dir := "NIL" dir := "NIL"
if GlobalSchema != nil { if GlobalSchema != nil {
classPath := cue.ParsePath(fmt.Sprintf("#Classes.%s.direction", cls)) classPath := cue.ParsePath(fmt.Sprintf("#Classes.%s.#meta.direction", cls))
val := GlobalSchema.Value.LookupPath(classPath) val := GlobalSchema.Value.LookupPath(classPath)
if val.Err() == nil { if val.Err() == nil {
var s string var s string
@@ -1161,7 +1211,20 @@ func formatNodeInfo(node *index.ProjectNode) string {
curr := container curr := container
for curr != nil { for curr != nil {
if isGAM(curr) { if isGAM(curr) {
gams = append(gams, curr.RealName) suffix := ""
p := container
for p != nil && p != curr {
if p.Name == "InputSignals" {
suffix = " (Input)"
break
}
if p.Name == "OutputSignals" {
suffix = " (Output)"
break
}
p = p.Parent
}
gams = append(gams, curr.RealName+suffix)
break break
} }
curr = curr.Parent curr = curr.Parent
@@ -1175,7 +1238,11 @@ func formatNodeInfo(node *index.ProjectNode) string {
if n.Target == node { if n.Target == node {
if n.Parent != nil && (n.Parent.Name == "InputSignals" || n.Parent.Name == "OutputSignals") { if n.Parent != nil && (n.Parent.Name == "InputSignals" || n.Parent.Name == "OutputSignals") {
if n.Parent.Parent != nil && isGAM(n.Parent.Parent) { if n.Parent.Parent != nil && isGAM(n.Parent.Parent) {
gams = append(gams, n.Parent.Parent.RealName) suffix := " (Input)"
if n.Parent.Name == "OutputSignals" {
suffix = " (Output)"
}
gams = append(gams, n.Parent.Parent.RealName+suffix)
} }
} }
} }
@@ -1342,5 +1409,5 @@ func respond(id any, result any) {
func send(msg any) { func send(msg any) {
body, _ := json.Marshal(msg) body, _ := json.Marshal(msg)
fmt.Printf("Content-Length: %d\r\n\r\n%s", len(body), body) fmt.Fprintf(Output, "Content-Length: %d\r\n\r\n%s", len(body), body)
} }

View File

@@ -129,7 +129,7 @@ func (l *Lexer) NextToken() Token {
case '/': case '/':
return l.lexComment() return l.lexComment()
case '#': case '#':
return l.lexPackage() return l.lexHashIdentifier()
case '+': case '+':
fallthrough fallthrough
case '$': case '$':
@@ -243,18 +243,19 @@ func (l *Lexer) lexUntilNewline(t TokenType) Token {
} }
} }
func (l *Lexer) lexPackage() Token { func (l *Lexer) lexHashIdentifier() Token {
// We are at '#', l.start is just before it // We are at '#', l.start is just before it
for { for {
r := l.next() r := l.next()
if unicode.IsLetter(r) { if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '-' || r == '.' || r == ':' || r == '#' {
continue continue
} }
l.backup() l.backup()
break break
} }
if l.input[l.start:l.pos] == "#package" { val := l.input[l.start:l.pos]
if val == "#package" {
return l.lexUntilNewline(TokenPackage) return l.lexUntilNewline(TokenPackage)
} }
return l.emit(TokenError) return l.emit(TokenIdentifier)
} }

View File

@@ -43,7 +43,8 @@ package schema
... ...
} }
TimingDataSource: { TimingDataSource: {
direction: "IN" #meta: multithreaded: bool | *false
#meta: direction: "IN"
... ...
} }
IOGAM: { IOGAM: {
@@ -66,71 +67,84 @@ package schema
FileDataSource: { FileDataSource: {
Filename: string Filename: string
Format?: string Format?: string
direction: "INOUT" #meta: multithreaded: bool | *false
#meta: direction: "INOUT"
... ...
} }
LoggerDataSource: { LoggerDataSource: {
direction: "OUT" #meta: multithreaded: bool | *false
#meta: direction: "OUT"
... ...
} }
DANStream: { DANStream: {
Timeout?: int Timeout?: int
direction: "OUT" #meta: multithreaded: bool | *false
#meta: direction: "OUT"
... ...
} }
EPICSCAInput: { EPICSCAInput: {
direction: "IN" #meta: multithreaded: bool | *false
#meta: direction: "IN"
... ...
} }
EPICSCAOutput: { EPICSCAOutput: {
direction: "OUT" #meta: multithreaded: bool | *false
#meta: direction: "OUT"
... ...
} }
EPICSPVAInput: { EPICSPVAInput: {
direction: "IN" #meta: multithreaded: bool | *false
#meta: direction: "IN"
... ...
} }
EPICSPVAOutput: { EPICSPVAOutput: {
direction: "OUT" #meta: multithreaded: bool | *false
#meta: direction: "OUT"
... ...
} }
SDNSubscriber: { SDNSubscriber: {
Address: string Address: string
Port: int Port: int
Interface?: string Interface?: string
direction: "IN" #meta: multithreaded: bool | *false
#meta: direction: "IN"
... ...
} }
SDNPublisher: { SDNPublisher: {
Address: string Address: string
Port: int Port: int
Interface?: string Interface?: string
direction: "OUT" #meta: multithreaded: bool | *false
#meta: direction: "OUT"
... ...
} }
UDPReceiver: { UDPReceiver: {
Port: int Port: int
Address?: string Address?: string
direction: "IN" #meta: multithreaded: bool | *false
#meta: direction: "IN"
... ...
} }
UDPSender: { UDPSender: {
Destination: string Destination: string
direction: "OUT" #meta: multithreaded: bool | *false
#meta: direction: "OUT"
... ...
} }
FileReader: { FileReader: {
Filename: string Filename: string
Format?: string Format?: string
Interpolate?: string Interpolate?: string
direction: "IN" #meta: multithreaded: bool | *false
#meta: direction: "IN"
... ...
} }
FileWriter: { FileWriter: {
Filename: string Filename: string
Format?: string Format?: string
StoreOnTrigger?: int StoreOnTrigger?: int
direction: "OUT" #meta: multithreaded: bool | *false
#meta: direction: "OUT"
... ...
} }
OrderedClass: { OrderedClass: {
@@ -173,7 +187,8 @@ package schema
TriggeredIOGAM: {...} TriggeredIOGAM: {...}
WaveformGAM: {...} WaveformGAM: {...}
DAN: { DAN: {
direction: "OUT" #meta: multithreaded: bool | *false
#meta: direction: "OUT"
... ...
} }
LinuxTimer: { LinuxTimer: {
@@ -184,11 +199,13 @@ package schema
CPUMask?: int CPUMask?: int
TimeProvider?: {...} TimeProvider?: {...}
Signals: {...} Signals: {...}
direction: "IN" #meta: multithreaded: bool | *false
#meta: direction: "IN"
... ...
} }
LinkDataSource: { LinkDataSource: {
direction: "INOUT" #meta: multithreaded: bool | *false
#meta: direction: "INOUT"
... ...
} }
MDSReader: { MDSReader: {
@@ -196,7 +213,8 @@ package schema
ShotNumber: int ShotNumber: int
Frequency: float | int Frequency: float | int
Signals: {...} Signals: {...}
direction: "IN" #meta: multithreaded: bool | *false
#meta: direction: "IN"
... ...
} }
MDSWriter: { MDSWriter: {
@@ -212,57 +230,74 @@ package schema
NumberOfPostTriggers?: int NumberOfPostTriggers?: int
Signals: {...} Signals: {...}
Messages?: {...} Messages?: {...}
direction: "OUT" #meta: multithreaded: bool | *false
#meta: direction: "OUT"
... ...
} }
NI1588TimeStamp: { NI1588TimeStamp: {
direction: "IN" #meta: multithreaded: bool | *false
#meta: direction: "IN"
... ...
} }
NI6259ADC: { NI6259ADC: {
direction: "IN" #meta: multithreaded: bool | *false
#meta: direction: "IN"
... ...
} }
NI6259DAC: { NI6259DAC: {
direction: "OUT" #meta: multithreaded: bool | *false
#meta: direction: "OUT"
... ...
} }
NI6259DIO: { NI6259DIO: {
direction: "INOUT" #meta: multithreaded: bool | *false
#meta: direction: "INOUT"
... ...
} }
NI6368ADC: { NI6368ADC: {
direction: "IN" #meta: multithreaded: bool | *false
#meta: direction: "IN"
... ...
} }
NI6368DAC: { NI6368DAC: {
direction: "OUT" #meta: multithreaded: bool | *false
#meta: direction: "OUT"
... ...
} }
NI6368DIO: { NI6368DIO: {
direction: "INOUT" #meta: multithreaded: bool | *false
#meta: direction: "INOUT"
... ...
} }
NI9157CircularFifoReader: { NI9157CircularFifoReader: {
direction: "IN" #meta: multithreaded: bool | *false
#meta: direction: "IN"
... ...
} }
NI9157MxiDataSource: { NI9157MxiDataSource: {
direction: "INOUT" #meta: multithreaded: bool | *false
#meta: direction: "INOUT"
... ...
} }
OPCUADSInput: { OPCUADSInput: {
direction: "IN" #meta: multithreaded: bool | *false
#meta: direction: "IN"
... ...
} }
OPCUADSOutput: { OPCUADSOutput: {
direction: "OUT" #meta: multithreaded: bool | *false
#meta: direction: "OUT"
...
}
RealTimeThreadAsyncBridge: {
#meta: direction: "INOUT"
#meta: multithreaded: bool | true
... ...
} }
RealTimeThreadAsyncBridge: {...}
RealTimeThreadSynchronisation: {...} RealTimeThreadSynchronisation: {...}
UARTDataSource: { UARTDataSource: {
direction: "INOUT" #meta: multithreaded: bool | *false
#meta: direction: "INOUT"
... ...
} }
BaseLib2Wrapper: {...} BaseLib2Wrapper: {...}
@@ -272,7 +307,8 @@ package schema
OPCUA: {...} OPCUA: {...}
SysLogger: {...} SysLogger: {...}
GAMDataSource: { GAMDataSource: {
direction: "INOUT" #meta: multithreaded: bool | *false
#meta: direction: "INOUT"
... ...
} }
} }

View File

@@ -53,6 +53,8 @@ func (v *Validator) ValidateProject() {
for _, node := range v.Tree.IsolatedFiles { for _, node := range v.Tree.IsolatedFiles {
v.validateNode(node) v.validateNode(node)
} }
v.CheckUnused()
v.CheckDataSourceThreading()
} }
func (v *Validator) validateNode(node *index.ProjectNode) { func (v *Validator) validateNode(node *index.ProjectNode) {
@@ -313,8 +315,8 @@ func (v *Validator) validateGAMSignal(gamNode, signalNode *index.ProjectNode, di
dsClass := v.getNodeClass(dsNode) dsClass := v.getNodeClass(dsNode)
if dsClass != "" { if dsClass != "" {
// Lookup class definition in Schema // Lookup class definition in Schema
// path: #Classes.ClassName.direction // path: #Classes.ClassName.#meta.direction
path := cue.ParsePath(fmt.Sprintf("#Classes.%s.direction", dsClass)) path := cue.ParsePath(fmt.Sprintf("#Classes.%s.#meta.direction", dsClass))
val := v.Schema.Value.LookupPath(path) val := v.Schema.Value.LookupPath(path)
if val.Err() == nil { if val.Err() == nil {
@@ -509,6 +511,8 @@ func (v *Validator) getFieldValue(f *parser.Field) string {
return val.Raw return val.Raw
case *parser.FloatValue: case *parser.FloatValue:
return val.Raw return val.Raw
case *parser.BoolValue:
return strconv.FormatBool(val.Value)
} }
return "" return ""
} }
@@ -741,3 +745,142 @@ func (v *Validator) isGloballyAllowed(warningType string, contextFile string) bo
} }
return false return false
} }
func (v *Validator) CheckDataSourceThreading() {
if v.Tree.Root == nil {
return
}
// 1. Find RealTimeApplication
var appNode *index.ProjectNode
findApp := func(n *index.ProjectNode) {
if cls, ok := n.Metadata["Class"]; ok && cls == "RealTimeApplication" {
appNode = n
}
}
v.Tree.Walk(findApp)
if appNode == nil {
return
}
// 2. Find States
var statesNode *index.ProjectNode
if s, ok := appNode.Children["States"]; ok {
statesNode = s
} else {
for _, child := range appNode.Children {
if cls, ok := child.Metadata["Class"]; ok && cls == "StateMachine" {
statesNode = child
break
}
}
}
if statesNode == nil {
return
}
// 3. Iterate States
for _, state := range statesNode.Children {
dsUsage := make(map[*index.ProjectNode]string) // DS Node -> Thread Name
var threads []*index.ProjectNode
// Search for threads in the state (either direct children or inside "Threads" container)
for _, child := range state.Children {
if child.RealName == "Threads" {
for _, t := range child.Children {
if cls, ok := t.Metadata["Class"]; ok && cls == "RealTimeThread" {
threads = append(threads, t)
}
}
} else {
if cls, ok := child.Metadata["Class"]; ok && cls == "RealTimeThread" {
threads = append(threads, child)
}
}
}
for _, thread := range threads {
gams := v.getThreadGAMs(thread)
for _, gam := range gams {
dss := v.getGAMDataSources(gam)
for _, ds := range dss {
if existingThread, ok := dsUsage[ds]; ok {
if existingThread != thread.RealName {
if !v.isMultithreaded(ds) {
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
Message: fmt.Sprintf("DataSource '%s' is not multithreaded but used in multiple threads (%s, %s) in state '%s'", ds.RealName, existingThread, thread.RealName, state.RealName),
Position: v.getNodePosition(gam),
File: v.getNodeFile(gam),
})
}
}
} else {
dsUsage[ds] = thread.RealName
}
}
}
}
}
}
func (v *Validator) getThreadGAMs(thread *index.ProjectNode) []*index.ProjectNode {
var gams []*index.ProjectNode
fields := v.getFields(thread)
if funcs, ok := fields["Functions"]; ok && len(funcs) > 0 {
f := funcs[0]
if arr, ok := f.Value.(*parser.ArrayValue); ok {
for _, elem := range arr.Elements {
if ref, ok := elem.(*parser.ReferenceValue); ok {
target := v.resolveReference(ref.Value, v.getNodeFile(thread), isGAM)
if target != nil {
gams = append(gams, target)
}
}
}
}
}
return gams
}
func (v *Validator) getGAMDataSources(gam *index.ProjectNode) []*index.ProjectNode {
dsMap := make(map[*index.ProjectNode]bool)
processSignals := func(container *index.ProjectNode) {
if container == nil {
return
}
for _, sig := range container.Children {
fields := v.getFields(sig)
if dsFields, ok := fields["DataSource"]; ok && len(dsFields) > 0 {
dsName := v.getFieldValue(dsFields[0])
dsNode := v.resolveReference(dsName, v.getNodeFile(sig), isDataSource)
if dsNode != nil {
dsMap[dsNode] = true
}
}
}
}
processSignals(gam.Children["InputSignals"])
processSignals(gam.Children["OutputSignals"])
var dss []*index.ProjectNode
for ds := range dsMap {
dss = append(dss, ds)
}
return dss
}
func (v *Validator) isMultithreaded(ds *index.ProjectNode) bool {
if meta, ok := ds.Children["#meta"]; ok {
fields := v.getFields(meta)
if mt, ok := fields["multithreaded"]; ok && len(mt) > 0 {
val := v.getFieldValue(mt[0])
return val == "true"
}
}
return false
}

View File

@@ -21,11 +21,12 @@ The executable should support the following subcommands:
The LSP server should provide the following capabilities: The LSP server should provide the following capabilities:
- **Diagnostics**: Report syntax errors and validation issues. - **Diagnostics**: Report syntax errors and validation issues.
- **Incremental Sync**: Supports `textDocumentSync` kind 2 (Incremental) for better performance with large files.
- **Hover Documentation**: - **Hover Documentation**:
- **Objects**: Display `CLASS::Name` and any associated docstrings. - **Objects**: Display `CLASS::Name` and any associated docstrings.
- **Signals**: Display `DataSource.Name TYPE (SIZE) [IN/OUT/INOUT]` along with docstrings. - **Signals**: Display `DataSource.Name TYPE (SIZE) [IN/OUT/INOUT]` along with docstrings.
- **GAMs**: Show the list of States where the GAM is referenced. - **GAMs**: Show the list of States where the GAM is referenced.
- **Referenced Signals**: Show the list of GAMs where the signal is referenced. - **Referenced Signals**: Show the list of GAMs where the signal is referenced (indicating Input/Output direction).
- **Go to Definition**: Jump to the definition of a reference, supporting navigation across any file in the current project. - **Go to Definition**: Jump to the definition of a reference, supporting navigation across any file in the current project.
- **Go to References**: Find usages of a node or field, supporting navigation across any file in the current project. - **Go to References**: Find usages of a node or field, supporting navigation across any file in the current project.
- **Code Completion**: Autocomplete fields, values, and references. - **Code Completion**: Autocomplete fields, values, and references.
@@ -173,6 +174,7 @@ The tool must build an index of the configuration to support LSP features and va
- **Conditional Fields**: Validation of fields whose presence or value depends on the values of other fields within the same node or context. - **Conditional Fields**: Validation of fields whose presence or value depends on the values of other fields within the same node or context.
- **Schema Definition**: - **Schema Definition**:
- Class validation rules must be defined in a separate schema file using the **CUE** language. - Class validation rules must be defined in a separate schema file using the **CUE** language.
- **Metadata**: Class properties like direction (`#direction`) and multithreading support (`#multithreaded`) are stored within a `#meta` field in the class definition (e.g., `#meta: { direction: "IN", multithreaded: true }`).
- **Project-Specific Classes**: Developers can define their own project-specific classes and corresponding validation rules, expanding the validation capabilities for their specific needs. - **Project-Specific Classes**: Developers can define their own project-specific classes and corresponding validation rules, expanding the validation capabilities for their specific needs.
- **Schema Loading**: - **Schema Loading**:
- **Default Schema**: The tool should look for a default schema file `marte_schema.cue` in standard system locations: - **Default Schema**: The tool should look for a default schema file `marte_schema.cue` in standard system locations:
@@ -218,6 +220,7 @@ The LSP and `check` command should report the following:
- 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. - **Invalid Function Reference**: Elements in the `Functions` array of a `State.Thread` must be valid references to defined GAM nodes.
- **Threading Violation**: A DataSource that is not marked as multithreaded (via `#meta.multithreaded`) is used by GAMs running in different threads within the same State.
## Logging ## Logging

94
test/ast_test.go Normal file
View File

@@ -0,0 +1,94 @@
package integration
import (
"testing"
"github.com/marte-community/marte-dev-tools/internal/parser"
)
func TestASTCoverage(t *testing.T) {
pos := parser.Position{Line: 1, Column: 1}
var n parser.Node
// var d parser.Definition // Definition has unexported method, can't assign?
// Yes I can assign if I am using the interface type exported by parser.
// But I cannot call the method.
var d parser.Definition
var v parser.Value
// Field
f := &parser.Field{Position: pos}
n = f
d = f
if n.Pos() != pos {
t.Error("Field.Pos failed")
}
_ = d
// ObjectNode
o := &parser.ObjectNode{Position: pos}
n = o
d = o
if n.Pos() != pos {
t.Error("ObjectNode.Pos failed")
}
// StringValue
sv := &parser.StringValue{Position: pos}
n = sv
v = sv
if n.Pos() != pos {
t.Error("StringValue.Pos failed")
}
_ = v
// IntValue
iv := &parser.IntValue{Position: pos}
n = iv
v = iv
if n.Pos() != pos {
t.Error("IntValue.Pos failed")
}
// FloatValue
fv := &parser.FloatValue{Position: pos}
n = fv
v = fv
if n.Pos() != pos {
t.Error("FloatValue.Pos failed")
}
// BoolValue
bv := &parser.BoolValue{Position: pos}
n = bv
v = bv
if n.Pos() != pos {
t.Error("BoolValue.Pos failed")
}
// ReferenceValue
rv := &parser.ReferenceValue{Position: pos}
n = rv
v = rv
if n.Pos() != pos {
t.Error("ReferenceValue.Pos failed")
}
// ArrayValue
av := &parser.ArrayValue{Position: pos}
n = av
v = av
if n.Pos() != pos {
t.Error("ArrayValue.Pos failed")
}
// Package
pkg := &parser.Package{Position: pos}
// Package implements Node?
// ast.go: func (p *Package) Pos() Position { return p.Position }
// Yes.
n = pkg
if n.Pos() != pos {
t.Error("Package.Pos failed")
}
}

66
test/index_test.go Normal file
View File

@@ -0,0 +1,66 @@
package integration
import (
"testing"
"github.com/marte-community/marte-dev-tools/internal/index"
)
func TestNodeMap(t *testing.T) {
pt := index.NewProjectTree()
root := pt.Root
// Create structure: +A -> +B -> +C
nodeA := &index.ProjectNode{Name: "A", RealName: "+A", Children: make(map[string]*index.ProjectNode), Parent: root}
root.Children["A"] = nodeA
nodeB := &index.ProjectNode{Name: "B", RealName: "+B", Children: make(map[string]*index.ProjectNode), Parent: nodeA}
nodeA.Children["B"] = nodeB
nodeC := &index.ProjectNode{Name: "C", RealName: "+C", Children: make(map[string]*index.ProjectNode), Parent: nodeB}
nodeB.Children["C"] = nodeC
// Rebuild Index
pt.RebuildIndex()
// Find by Name
found := pt.FindNode(root, "C", nil)
if found != nodeC {
t.Errorf("FindNode(C) failed. Got %v, want %v", found, nodeC)
}
// Find by RealName
found = pt.FindNode(root, "+C", nil)
if found != nodeC {
t.Errorf("FindNode(+C) failed. Got %v, want %v", found, nodeC)
}
// Find by Path
found = pt.FindNode(root, "A.B.C", nil)
if found != nodeC {
t.Errorf("FindNode(A.B.C) failed. Got %v, want %v", found, nodeC)
}
// Find by Path with RealName
found = pt.FindNode(root, "+A.+B.+C", nil)
if found != nodeC {
t.Errorf("FindNode(+A.+B.+C) failed. Got %v, want %v", found, nodeC)
}
}
func TestResolveReferencesWithMap(t *testing.T) {
pt := index.NewProjectTree()
root := pt.Root
nodeA := &index.ProjectNode{Name: "A", RealName: "+A", Children: make(map[string]*index.ProjectNode), Parent: root}
root.Children["A"] = nodeA
ref := index.Reference{Name: "A", File: "test.marte"}
pt.References = append(pt.References, ref)
pt.ResolveReferences()
if pt.References[0].Target != nodeA {
t.Error("ResolveReferences failed to resolve A")
}
}

View File

@@ -20,7 +20,7 @@ func TestSuggestSignalsRobustness(t *testing.T) {
custom := []byte(` custom := []byte(`
package schema package schema
#Classes: { #Classes: {
InOutReader: { direction: "INOUT" } InOutReader: { #meta: direction: "INOUT" }
} }
`) `)
val := lsp.GlobalSchema.Context.CompileBytes(custom) val := lsp.GlobalSchema.Context.CompileBytes(custom)

View File

@@ -0,0 +1,77 @@
package integration
import (
"bytes"
"strings"
"testing"
"github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-community/marte-dev-tools/internal/lsp"
"github.com/marte-community/marte-dev-tools/internal/schema"
)
func TestLSPValidationThreading(t *testing.T) {
// Setup
lsp.Tree = index.NewProjectTree()
lsp.Documents = make(map[string]string)
lsp.ProjectRoot = "."
lsp.GlobalSchema = schema.NewSchema() // Empty schema but not nil
// Capture Output
var buf bytes.Buffer
lsp.Output = &buf
content := `
+Data = {
Class = ReferenceContainer
+SharedDS = {
Class = GAMDataSource
#meta = {
direction = "INOUT"
multithreaded = false
}
Signals = {
Sig1 = { Type = uint32 }
}
}
}
+GAM1 = { Class = IOGAM InputSignals = { Sig1 = { DataSource = SharedDS Type = uint32 } } }
+GAM2 = { Class = IOGAM OutputSignals = { Sig1 = { DataSource = SharedDS Type = uint32 } } }
+App = {
Class = RealTimeApplication
+States = {
Class = ReferenceContainer
+State1 = {
Class = RealTimeState
+Thread1 = { Class = RealTimeThread Functions = { GAM1 } }
+Thread2 = { Class = RealTimeThread Functions = { GAM2 } }
}
}
}
`
uri := "file://threading.marte"
// Call HandleDidOpen directly
params := lsp.DidOpenTextDocumentParams{
TextDocument: lsp.TextDocumentItem{
URI: uri,
Text: content,
},
}
lsp.HandleDidOpen(params)
// Check output
output := buf.String()
// We look for publishDiagnostics notification
if !strings.Contains(output, "textDocument/publishDiagnostics") {
t.Fatal("Did not receive publishDiagnostics")
}
// We look for the specific error message
expectedError := "DataSource '+SharedDS' is not multithreaded but used in multiple threads"
if !strings.Contains(output, expectedError) {
t.Errorf("Expected error '%s' not found in LSP output. Output:\n%s", expectedError, output)
}
}

View File

@@ -0,0 +1,124 @@
package integration
import (
"strings"
"testing"
"github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-community/marte-dev-tools/internal/parser"
"github.com/marte-community/marte-dev-tools/internal/validator"
)
func TestDataSourceThreadingValidation(t *testing.T) {
content := `
+Data = {
Class = ReferenceContainer
+SharedDS = {
Class = GAMDataSource
#meta = {
direction = "INOUT"
multithreaded = false
}
Signals = {
Sig1 = { Type = uint32 }
}
}
+MultiDS = {
Class = GAMDataSource
#meta = {
direction = "INOUT"
multithreaded = true
}
Signals = {
Sig1 = { Type = uint32 }
}
}
}
+GAM1 = {
Class = IOGAM
InputSignals = {
Sig1 = { DataSource = SharedDS Type = uint32 }
}
}
+GAM2 = {
Class = IOGAM
OutputSignals = {
Sig1 = { DataSource = SharedDS Type = uint32 }
}
}
+GAM3 = {
Class = IOGAM
InputSignals = {
Sig1 = { DataSource = MultiDS Type = uint32 }
}
}
+GAM4 = {
Class = IOGAM
OutputSignals = {
Sig1 = { DataSource = MultiDS Type = uint32 }
}
}
+App = {
Class = RealTimeApplication
+States = {
Class = ReferenceContainer
+State1 = {
Class = RealTimeState
+Thread1 = {
Class = RealTimeThread
Functions = { GAM1 }
}
+Thread2 = {
Class = RealTimeThread
Functions = { GAM2 }
}
}
+State2 = {
Class = RealTimeState
+Thread1 = {
Class = RealTimeThread
Functions = { GAM3 }
}
+Thread2 = {
Class = RealTimeThread
Functions = { GAM4 }
}
}
}
}
`
pt := index.NewProjectTree()
p := parser.NewParser(content)
cfg, err := p.Parse()
if err != nil {
t.Fatal(err)
}
pt.AddFile("main.marte", cfg)
// Since we don't load schema here (empty path), it won't validate classes via CUE,
// but CheckDataSourceThreading relies on parsing logic, not CUE schema unification.
// So it should work.
v := validator.NewValidator(pt, "")
v.ValidateProject()
foundError := false
for _, d := range v.Diagnostics {
if strings.Contains(d.Message, "not multithreaded but used in multiple threads") {
if strings.Contains(d.Message, "SharedDS") {
foundError = true
}
if strings.Contains(d.Message, "MultiDS") {
t.Error("Unexpected threading error for MultiDS")
}
}
}
if !foundError {
t.Error("Expected threading error for SharedDS")
// Debug
for _, d := range v.Diagnostics {
t.Logf("Diag: %s", d.Message)
}
}
}

View File

@@ -23,6 +23,7 @@ func TestGAMSignalLinking(t *testing.T) {
+MyGAM = { +MyGAM = {
Class = IOGAM Class = IOGAM
//! ignore(unused)
InputSignals = { InputSignals = {
MySig = { MySig = {
DataSource = MyDS DataSource = MyDS