Improving LSP

This commit is contained in:
Martino Ferrari
2026-01-27 14:42:46 +01:00
parent 71a3c40108
commit 213fc81cfb
9 changed files with 432 additions and 59 deletions

View File

@@ -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"`
@@ -404,7 +405,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 +646,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.#direction", cls))
val := GlobalSchema.Value.LookupPath(classPath)
if val.Err() == nil {
var s string
@@ -1342,5 +1342,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)
}

View File

@@ -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)
}

View File

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

View File

@@ -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) {
@@ -314,7 +316,7 @@ func (v *Validator) validateGAMSignal(gamNode, signalNode *index.ProjectNode, di
if dsClass != "" {
// Lookup class definition in Schema
// path: #Classes.ClassName.direction
path := cue.ParsePath(fmt.Sprintf("#Classes.%s.direction", dsClass))
path := cue.ParsePath(fmt.Sprintf("#Classes.%s.#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,140 @@ 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 {
fields := v.getFields(ds)
if mt, ok := fields["#multithreaded"]; ok && len(mt) > 0 {
val := v.getFieldValue(mt[0])
return val == "true"
}
return false
}