Compare commits
3 Commits
71a3c40108
...
fed39467fd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fed39467fd | ||
|
|
15afdc91f4 | ||
|
|
213fc81cfb |
@@ -77,6 +77,10 @@ package schema
|
||||
|
||||
#Classes: {
|
||||
MyCustomGAM: {
|
||||
#meta: {
|
||||
direction: "INOUT"
|
||||
multithreaded: true
|
||||
}
|
||||
Param1: int
|
||||
Param2?: string
|
||||
...
|
||||
|
||||
@@ -86,7 +86,6 @@ func runCheck(args []string) {
|
||||
|
||||
// Legacy loop removed as ValidateProject covers it via recursion
|
||||
|
||||
v.CheckUnused()
|
||||
|
||||
for _, diag := range v.Diagnostics {
|
||||
level := "ERROR"
|
||||
|
||||
@@ -13,6 +13,7 @@ type ProjectTree struct {
|
||||
References []Reference
|
||||
IsolatedFiles map[string]*ProjectNode
|
||||
GlobalPragmas map[string][]string
|
||||
NodeMap map[string][]*ProjectNode
|
||||
}
|
||||
|
||||
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() {
|
||||
pt.RebuildIndex()
|
||||
for i := range pt.References {
|
||||
ref := &pt.References[i]
|
||||
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 {
|
||||
if pt.NodeMap == nil {
|
||||
pt.RebuildIndex()
|
||||
}
|
||||
|
||||
if strings.Contains(name, ".") {
|
||||
parts := strings.Split(name, ".")
|
||||
rootName := parts[0]
|
||||
|
||||
var candidates []*ProjectNode
|
||||
pt.findAllNodes(root, rootName, &candidates)
|
||||
candidates := pt.NodeMap[rootName]
|
||||
|
||||
for _, cand := range candidates {
|
||||
if !pt.isDescendant(cand, root) {
|
||||
continue
|
||||
}
|
||||
|
||||
curr := cand
|
||||
valid := true
|
||||
for i := 1; i < len(parts); i++ {
|
||||
@@ -426,26 +446,33 @@ func (pt *ProjectTree) FindNode(root *ProjectNode, name string, predicate func(*
|
||||
return nil
|
||||
}
|
||||
|
||||
if root.RealName == name || root.Name == name {
|
||||
if predicate == nil || predicate(root) {
|
||||
return root
|
||||
candidates := pt.NodeMap[name]
|
||||
for _, cand := range candidates {
|
||||
if !pt.isDescendant(cand, root) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
for _, child := range root.Children {
|
||||
if res := pt.FindNode(child, name, predicate); res != nil {
|
||||
return res
|
||||
if predicate == nil || predicate(cand) {
|
||||
return cand
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pt *ProjectTree) findAllNodes(root *ProjectNode, name string, results *[]*ProjectNode) {
|
||||
if root.RealName == name || root.Name == name {
|
||||
*results = append(*results, root)
|
||||
func (pt *ProjectTree) isDescendant(node, root *ProjectNode) bool {
|
||||
if node == root {
|
||||
return true
|
||||
}
|
||||
for _, child := range root.Children {
|
||||
pt.findAllNodes(child, name, results)
|
||||
if root == nil {
|
||||
return true
|
||||
}
|
||||
curr := node
|
||||
for curr != nil {
|
||||
if curr == root {
|
||||
return true
|
||||
}
|
||||
curr = curr.Parent
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type QueryResult struct {
|
||||
|
||||
@@ -49,6 +49,7 @@ var Tree = index.NewProjectTree()
|
||||
var Documents = make(map[string]string)
|
||||
var ProjectRoot string
|
||||
var GlobalSchema *schema.Schema
|
||||
var Output io.Writer = os.Stdout
|
||||
|
||||
type JsonRpcMessage struct {
|
||||
Jsonrpc string `json:"jsonrpc"`
|
||||
@@ -91,7 +92,9 @@ type VersionedTextDocumentIdentifier struct {
|
||||
}
|
||||
|
||||
type TextDocumentContentChangeEvent struct {
|
||||
Text string `json:"text"`
|
||||
Range *Range `json:"range,omitempty"`
|
||||
RangeLength int `json:"rangeLength,omitempty"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type HoverParams struct {
|
||||
@@ -252,7 +255,7 @@ func HandleMessage(msg *JsonRpcMessage) {
|
||||
|
||||
respond(msg.ID, map[string]any{
|
||||
"capabilities": map[string]any{
|
||||
"textDocumentSync": 1, // Full sync
|
||||
"textDocumentSync": 2, // Incremental sync
|
||||
"hoverProvider": true,
|
||||
"definitionProvider": true,
|
||||
"referencesProvider": true,
|
||||
@@ -346,28 +349,76 @@ func HandleDidOpen(params DidOpenTextDocumentParams) {
|
||||
}
|
||||
|
||||
func HandleDidChange(params DidChangeTextDocumentParams) {
|
||||
if len(params.ContentChanges) == 0 {
|
||||
return
|
||||
uri := params.TextDocument.URI
|
||||
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
|
||||
path := uriToPath(params.TextDocument.URI)
|
||||
|
||||
for _, change := range params.ContentChanges {
|
||||
if change.Range == nil {
|
||||
text = change.Text
|
||||
} else {
|
||||
text = applyContentChange(text, change)
|
||||
}
|
||||
}
|
||||
|
||||
Documents[uri] = text
|
||||
path := uriToPath(uri)
|
||||
p := parser.NewParser(text)
|
||||
config, err := p.Parse()
|
||||
|
||||
if err != nil {
|
||||
publishParserError(params.TextDocument.URI, err)
|
||||
publishParserError(uri, err)
|
||||
} else {
|
||||
publishParserError(params.TextDocument.URI, nil)
|
||||
publishParserError(uri, nil)
|
||||
}
|
||||
|
||||
if config != nil {
|
||||
Tree.AddFile(path, config)
|
||||
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 {
|
||||
uri := params.TextDocument.URI
|
||||
text, ok := Documents[uri]
|
||||
@@ -404,7 +455,6 @@ func HandleFormatting(params DocumentFormattingParams) []TextEdit {
|
||||
func runValidation(_ string) {
|
||||
v := validator.NewValidator(Tree, ProjectRoot)
|
||||
v.ValidateProject()
|
||||
v.CheckUnused()
|
||||
|
||||
// Group diagnostics by file
|
||||
fileDiags := make(map[string][]LSPDiagnostic)
|
||||
@@ -646,7 +696,7 @@ func suggestGAMSignals(_ *index.ProjectNode, direction string) *CompletionList {
|
||||
|
||||
dir := "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)
|
||||
if val.Err() == nil {
|
||||
var s string
|
||||
@@ -1161,7 +1211,20 @@ func formatNodeInfo(node *index.ProjectNode) string {
|
||||
curr := container
|
||||
for curr != nil {
|
||||
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
|
||||
}
|
||||
curr = curr.Parent
|
||||
@@ -1175,7 +1238,11 @@ func formatNodeInfo(node *index.ProjectNode) string {
|
||||
if n.Target == node {
|
||||
if n.Parent != nil && (n.Parent.Name == "InputSignals" || n.Parent.Name == "OutputSignals") {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ func (l *Lexer) NextToken() Token {
|
||||
case '/':
|
||||
return l.lexComment()
|
||||
case '#':
|
||||
return l.lexPackage()
|
||||
return l.lexHashIdentifier()
|
||||
case '+':
|
||||
fallthrough
|
||||
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
|
||||
for {
|
||||
r := l.next()
|
||||
if unicode.IsLetter(r) {
|
||||
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '-' || r == '.' || r == ':' || r == '#' {
|
||||
continue
|
||||
}
|
||||
l.backup()
|
||||
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.emit(TokenError)
|
||||
return l.emit(TokenIdentifier)
|
||||
}
|
||||
|
||||
@@ -43,7 +43,8 @@ package schema
|
||||
...
|
||||
}
|
||||
TimingDataSource: {
|
||||
direction: "IN"
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "IN"
|
||||
...
|
||||
}
|
||||
IOGAM: {
|
||||
@@ -64,73 +65,86 @@ package schema
|
||||
...
|
||||
}
|
||||
FileDataSource: {
|
||||
Filename: string
|
||||
Format?: string
|
||||
direction: "INOUT"
|
||||
Filename: string
|
||||
Format?: string
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "INOUT"
|
||||
...
|
||||
}
|
||||
LoggerDataSource: {
|
||||
direction: "OUT"
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "OUT"
|
||||
...
|
||||
}
|
||||
DANStream: {
|
||||
Timeout?: int
|
||||
direction: "OUT"
|
||||
Timeout?: int
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "OUT"
|
||||
...
|
||||
}
|
||||
EPICSCAInput: {
|
||||
direction: "IN"
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "IN"
|
||||
...
|
||||
}
|
||||
EPICSCAOutput: {
|
||||
direction: "OUT"
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "OUT"
|
||||
...
|
||||
}
|
||||
EPICSPVAInput: {
|
||||
direction: "IN"
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "IN"
|
||||
...
|
||||
}
|
||||
EPICSPVAOutput: {
|
||||
direction: "OUT"
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "OUT"
|
||||
...
|
||||
}
|
||||
SDNSubscriber: {
|
||||
Address: string
|
||||
Port: int
|
||||
Interface?: string
|
||||
direction: "IN"
|
||||
Address: string
|
||||
Port: int
|
||||
Interface?: string
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "IN"
|
||||
...
|
||||
}
|
||||
SDNPublisher: {
|
||||
Address: string
|
||||
Port: int
|
||||
Interface?: string
|
||||
direction: "OUT"
|
||||
Address: string
|
||||
Port: int
|
||||
Interface?: string
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "OUT"
|
||||
...
|
||||
}
|
||||
UDPReceiver: {
|
||||
Port: int
|
||||
Address?: string
|
||||
direction: "IN"
|
||||
Port: int
|
||||
Address?: string
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "IN"
|
||||
...
|
||||
}
|
||||
UDPSender: {
|
||||
Destination: string
|
||||
direction: "OUT"
|
||||
Destination: string
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "OUT"
|
||||
...
|
||||
}
|
||||
FileReader: {
|
||||
Filename: string
|
||||
Format?: string
|
||||
Interpolate?: string
|
||||
direction: "IN"
|
||||
Filename: string
|
||||
Format?: string
|
||||
Interpolate?: string
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "IN"
|
||||
...
|
||||
}
|
||||
FileWriter: {
|
||||
Filename: string
|
||||
Format?: string
|
||||
StoreOnTrigger?: int
|
||||
direction: "OUT"
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "OUT"
|
||||
...
|
||||
}
|
||||
OrderedClass: {
|
||||
@@ -173,7 +187,8 @@ package schema
|
||||
TriggeredIOGAM: {...}
|
||||
WaveformGAM: {...}
|
||||
DAN: {
|
||||
direction: "OUT"
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "OUT"
|
||||
...
|
||||
}
|
||||
LinuxTimer: {
|
||||
@@ -184,11 +199,13 @@ package schema
|
||||
CPUMask?: int
|
||||
TimeProvider?: {...}
|
||||
Signals: {...}
|
||||
direction: "IN"
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "IN"
|
||||
...
|
||||
}
|
||||
LinkDataSource: {
|
||||
direction: "INOUT"
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "INOUT"
|
||||
...
|
||||
}
|
||||
MDSReader: {
|
||||
@@ -196,7 +213,8 @@ package schema
|
||||
ShotNumber: int
|
||||
Frequency: float | int
|
||||
Signals: {...}
|
||||
direction: "IN"
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "IN"
|
||||
...
|
||||
}
|
||||
MDSWriter: {
|
||||
@@ -212,57 +230,74 @@ package schema
|
||||
NumberOfPostTriggers?: int
|
||||
Signals: {...}
|
||||
Messages?: {...}
|
||||
direction: "OUT"
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "OUT"
|
||||
...
|
||||
}
|
||||
NI1588TimeStamp: {
|
||||
direction: "IN"
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "IN"
|
||||
...
|
||||
}
|
||||
NI6259ADC: {
|
||||
direction: "IN"
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "IN"
|
||||
...
|
||||
}
|
||||
NI6259DAC: {
|
||||
direction: "OUT"
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "OUT"
|
||||
...
|
||||
}
|
||||
NI6259DIO: {
|
||||
direction: "INOUT"
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "INOUT"
|
||||
...
|
||||
}
|
||||
NI6368ADC: {
|
||||
direction: "IN"
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "IN"
|
||||
...
|
||||
}
|
||||
NI6368DAC: {
|
||||
direction: "OUT"
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "OUT"
|
||||
...
|
||||
}
|
||||
NI6368DIO: {
|
||||
direction: "INOUT"
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "INOUT"
|
||||
...
|
||||
}
|
||||
NI9157CircularFifoReader: {
|
||||
direction: "IN"
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "IN"
|
||||
...
|
||||
}
|
||||
NI9157MxiDataSource: {
|
||||
direction: "INOUT"
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "INOUT"
|
||||
...
|
||||
}
|
||||
OPCUADSInput: {
|
||||
direction: "IN"
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "IN"
|
||||
...
|
||||
}
|
||||
OPCUADSOutput: {
|
||||
direction: "OUT"
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "OUT"
|
||||
...
|
||||
}
|
||||
RealTimeThreadAsyncBridge: {
|
||||
#meta: direction: "INOUT"
|
||||
#meta: multithreaded: bool | true
|
||||
...
|
||||
}
|
||||
RealTimeThreadAsyncBridge: {...}
|
||||
RealTimeThreadSynchronisation: {...}
|
||||
UARTDataSource: {
|
||||
direction: "INOUT"
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "INOUT"
|
||||
...
|
||||
}
|
||||
BaseLib2Wrapper: {...}
|
||||
@@ -272,7 +307,8 @@ package schema
|
||||
OPCUA: {...}
|
||||
SysLogger: {...}
|
||||
GAMDataSource: {
|
||||
direction: "INOUT"
|
||||
#meta: multithreaded: bool | *false
|
||||
#meta: direction: "INOUT"
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,8 @@ func (v *Validator) ValidateProject() {
|
||||
for _, node := range v.Tree.IsolatedFiles {
|
||||
v.validateNode(node)
|
||||
}
|
||||
v.CheckUnused()
|
||||
v.CheckDataSourceThreading()
|
||||
}
|
||||
|
||||
func (v *Validator) validateNode(node *index.ProjectNode) {
|
||||
@@ -313,8 +315,8 @@ func (v *Validator) validateGAMSignal(gamNode, signalNode *index.ProjectNode, di
|
||||
dsClass := v.getNodeClass(dsNode)
|
||||
if dsClass != "" {
|
||||
// Lookup class definition in Schema
|
||||
// path: #Classes.ClassName.direction
|
||||
path := cue.ParsePath(fmt.Sprintf("#Classes.%s.direction", dsClass))
|
||||
// path: #Classes.ClassName.#meta.direction
|
||||
path := cue.ParsePath(fmt.Sprintf("#Classes.%s.#meta.direction", dsClass))
|
||||
val := v.Schema.Value.LookupPath(path)
|
||||
|
||||
if val.Err() == nil {
|
||||
@@ -509,6 +511,8 @@ func (v *Validator) getFieldValue(f *parser.Field) string {
|
||||
return val.Raw
|
||||
case *parser.FloatValue:
|
||||
return val.Raw
|
||||
case *parser.BoolValue:
|
||||
return strconv.FormatBool(val.Value)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -741,3 +745,142 @@ func (v *Validator) isGloballyAllowed(warningType string, contextFile string) bo
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -21,11 +21,12 @@ The executable should support the following subcommands:
|
||||
The LSP server should provide the following capabilities:
|
||||
|
||||
- **Diagnostics**: Report syntax errors and validation issues.
|
||||
- **Incremental Sync**: Supports `textDocumentSync` kind 2 (Incremental) for better performance with large files.
|
||||
- **Hover Documentation**:
|
||||
- **Objects**: Display `CLASS::Name` and any associated docstrings.
|
||||
- **Signals**: Display `DataSource.Name TYPE (SIZE) [IN/OUT/INOUT]` along with docstrings.
|
||||
- **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 References**: Find usages of a node or field, supporting navigation across any file in the current project.
|
||||
- **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.
|
||||
- **Schema Definition**:
|
||||
- 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.
|
||||
- **Schema Loading**:
|
||||
- **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.
|
||||
- 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.
|
||||
- **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
|
||||
|
||||
|
||||
94
test/ast_test.go
Normal file
94
test/ast_test.go
Normal 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
66
test/index_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ func TestSuggestSignalsRobustness(t *testing.T) {
|
||||
custom := []byte(`
|
||||
package schema
|
||||
#Classes: {
|
||||
InOutReader: { direction: "INOUT" }
|
||||
InOutReader: { #meta: direction: "INOUT" }
|
||||
}
|
||||
`)
|
||||
val := lsp.GlobalSchema.Context.CompileBytes(custom)
|
||||
|
||||
77
test/lsp_validation_threading_test.go
Normal file
77
test/lsp_validation_threading_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
124
test/validator_datasource_threading_test.go
Normal file
124
test/validator_datasource_threading_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ func TestGAMSignalLinking(t *testing.T) {
|
||||
|
||||
+MyGAM = {
|
||||
Class = IOGAM
|
||||
//! ignore(unused)
|
||||
InputSignals = {
|
||||
MySig = {
|
||||
DataSource = MyDS
|
||||
|
||||
Reference in New Issue
Block a user