Implemented more robust LSP diagnostics and better parsing logic
This commit is contained in:
@@ -138,6 +138,7 @@ func runCheck(args []string) {
|
||||
}
|
||||
|
||||
tree := index.NewProjectTree()
|
||||
syntaxErrors := 0
|
||||
|
||||
for _, file := range args {
|
||||
content, err := os.ReadFile(file)
|
||||
@@ -147,14 +148,18 @@ func runCheck(args []string) {
|
||||
}
|
||||
|
||||
p := parser.NewParser(string(content))
|
||||
config, err := p.Parse()
|
||||
if err != nil {
|
||||
logger.Printf("%s: Grammar error: %v\n", file, err)
|
||||
continue
|
||||
config, _ := p.Parse()
|
||||
if len(p.Errors()) > 0 {
|
||||
syntaxErrors += len(p.Errors())
|
||||
for _, e := range p.Errors() {
|
||||
logger.Printf("%s: Grammar error: %v\n", file, e)
|
||||
}
|
||||
}
|
||||
|
||||
if config != nil {
|
||||
tree.AddFile(file, config)
|
||||
}
|
||||
}
|
||||
|
||||
v := validator.NewValidator(tree, ".")
|
||||
v.ValidateProject()
|
||||
@@ -167,8 +172,9 @@ func runCheck(args []string) {
|
||||
logger.Printf("%s:%d:%d: %s: %s\n", diag.File, diag.Position.Line, diag.Position.Column, level, diag.Message)
|
||||
}
|
||||
|
||||
if len(v.Diagnostics) > 0 {
|
||||
logger.Printf("\nFound %d issues.\n", len(v.Diagnostics))
|
||||
totalIssues := len(v.Diagnostics) + syntaxErrors
|
||||
if totalIssues > 0 {
|
||||
logger.Printf("\nFound %d issues.\n", totalIssues)
|
||||
} else {
|
||||
logger.Println("No issues found.")
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ The brain of the system. It maintains a holistic view of the project.
|
||||
* **ProjectTree**: The central data structure. It holds the root of the configuration hierarchy (`Root`), references, and isolated files.
|
||||
* **ProjectNode**: Represents a logical node in the configuration. Since a node can be defined across multiple files (fragments), `ProjectNode` aggregates these fragments. It also stores locally defined variables in its `Variables` map.
|
||||
* **NodeMap**: A hash map index (`map[string][]*ProjectNode`) for $O(1)$ symbol lookups, optimizing `FindNode` operations.
|
||||
* **Reference Resolution**: The `ResolveReferences` method links `Reference` objects to their target `ProjectNode` or `VariableDefinition`. It uses `resolveScopedName` to respect lexical scoping rules, searching up the hierarchy from the reference's container.
|
||||
* **Reference Resolution**: The `ResolveReferences` method links `Reference` objects to their target `ProjectNode` or `VariableDefinition`. It uses `ResolveName` (exported) which respects lexical scoping rules by searching the hierarchy upwards from the reference's container, using `FindNode` for deep searches within each scope.
|
||||
|
||||
### 3. `internal/validator`
|
||||
|
||||
@@ -100,12 +100,13 @@ Manages CUE schemas.
|
||||
5. Diagnostics are printed (CLI) or published via `textDocument/publishDiagnostics` (LSP).
|
||||
|
||||
### Threading Check Logic
|
||||
1. Finds the `RealTimeApplication` node.
|
||||
2. Iterates through `States` and `Threads`.
|
||||
3. For each Thread, resolves the `Functions` (GAMs).
|
||||
4. For each GAM, resolves connected `DataSources` via Input/Output signals.
|
||||
5. Maps `DataSource -> Thread` within the context of a State.
|
||||
6. If a DataSource is seen in >1 Thread, it checks the `#meta.multithreaded` property. If false (default), an error is raised.
|
||||
1. Iterates all `RealTimeApplication` nodes found in the project.
|
||||
2. For each App:
|
||||
1. Finds `States` and `Threads`.
|
||||
2. For each Thread, resolves the `Functions` (GAMs).
|
||||
3. For each GAM, resolves connected `DataSources` via Input/Output signals.
|
||||
4. Maps `DataSource -> Thread` within the context of a State.
|
||||
5. If a DataSource is seen in >1 Thread, it checks the `#meta.multithreaded` property. If false (default), an error is raised.
|
||||
|
||||
### INOUT Ordering Logic
|
||||
1. Iterates Threads.
|
||||
|
||||
@@ -173,9 +173,11 @@ You can define variables using `#var`. The type expression supports CUE syntax.
|
||||
```
|
||||
|
||||
### Usage
|
||||
Reference a variable using `@`:
|
||||
Reference a variable using `$` (preferred) or `@`:
|
||||
|
||||
```marte
|
||||
Field = $MyVar
|
||||
// or
|
||||
Field = @MyVar
|
||||
```
|
||||
|
||||
@@ -187,7 +189,7 @@ You can use operators in field values. Supported operators:
|
||||
```marte
|
||||
Field1 = 10 + 20 * 2 // 50
|
||||
Field2 = "Hello " .. "World"
|
||||
Field3 = @MyVar + 5
|
||||
Field3 = $MyVar + 5
|
||||
```
|
||||
|
||||
### Build Override
|
||||
@@ -197,3 +199,21 @@ You can override variable values during build:
|
||||
mdt build -vMyVar=200 -vEnv="PROD" src/*.marte
|
||||
```
|
||||
|
||||
## 7. Validation Rules (Detail)
|
||||
|
||||
### Data Flow Validation
|
||||
`mdt` checks for logical data flow errors:
|
||||
- **Consumed before Produced**: If a GAM reads an INOUT signal that hasn't been written by a previous GAM in the same cycle, an error is reported.
|
||||
- **Produced but not Consumed**: If a GAM writes an INOUT signal that is never read by subsequent GAMs, a warning is reported.
|
||||
- **Initialization**: Providing a `Value` field in an `InputSignal` treats it as "produced" (initialized), resolving "Consumed before Produced" errors.
|
||||
|
||||
### Threading Rules
|
||||
A DataSource that is **not** marked as multithreaded (default) cannot be used by GAMs running in different threads within the same State.
|
||||
|
||||
To allow sharing, the DataSource class in the schema must have `#meta: multithreaded: true`.
|
||||
|
||||
### Implicit vs Explicit Signals
|
||||
- **Explicit**: Signal defined in `DataSource.Signals`.
|
||||
- **Implicit**: Signal used in GAM but not defined in DataSource. `mdt` reports a warning unless suppressed.
|
||||
|
||||
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
`mdt` includes a Language Server Protocol (LSP) implementation that provides features like:
|
||||
|
||||
- Syntax highlighting and error reporting
|
||||
- Syntax highlighting and error reporting (Parser & Semantic)
|
||||
- Auto-completion
|
||||
- Go to Definition / References
|
||||
- Hover documentation
|
||||
- Symbol renaming
|
||||
- Incremental synchronization (Robust)
|
||||
|
||||
The LSP server is started via the command:
|
||||
|
||||
|
||||
@@ -435,7 +435,7 @@ func (pt *ProjectTree) ResolveReferences() {
|
||||
continue
|
||||
}
|
||||
|
||||
ref.Target = pt.resolveScopedName(container, ref.Name)
|
||||
ref.Target = pt.ResolveName(container, ref.Name, nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,51 +617,19 @@ func (pt *ProjectTree) findNodeContaining(node *ProjectNode, file string, pos pa
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pt *ProjectTree) resolveScopedName(ctx *ProjectNode, name string) *ProjectNode {
|
||||
func (pt *ProjectTree) ResolveName(ctx *ProjectNode, name string, predicate func(*ProjectNode) bool) *ProjectNode {
|
||||
if ctx == nil {
|
||||
return pt.FindNode(pt.Root, name, nil)
|
||||
return pt.FindNode(pt.Root, name, predicate)
|
||||
}
|
||||
|
||||
parts := strings.Split(name, ".")
|
||||
first := parts[0]
|
||||
normFirst := NormalizeName(first)
|
||||
|
||||
var startNode *ProjectNode
|
||||
curr := ctx
|
||||
|
||||
for curr != nil {
|
||||
if child, ok := curr.Children[normFirst]; ok {
|
||||
startNode = child
|
||||
break
|
||||
if found := pt.FindNode(curr, name, predicate); found != nil {
|
||||
return found
|
||||
}
|
||||
curr = curr.Parent
|
||||
}
|
||||
|
||||
if startNode == nil && ctx != pt.Root {
|
||||
if child, ok := pt.Root.Children[normFirst]; ok {
|
||||
startNode = child
|
||||
}
|
||||
}
|
||||
|
||||
if startNode == nil {
|
||||
// Fallback to deep search from context root
|
||||
root := ctx
|
||||
for root.Parent != nil {
|
||||
root = root.Parent
|
||||
}
|
||||
return pt.FindNode(root, name, nil)
|
||||
}
|
||||
|
||||
curr = startNode
|
||||
for i := 1; i < len(parts); i++ {
|
||||
norm := NormalizeName(parts[i])
|
||||
if child, ok := curr.Children[norm]; ok {
|
||||
curr = child
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return curr
|
||||
}
|
||||
|
||||
func (pt *ProjectTree) ResolveVariable(ctx *ProjectNode, name string) *VariableInfo {
|
||||
|
||||
@@ -336,13 +336,9 @@ func HandleDidOpen(params DidOpenTextDocumentParams) {
|
||||
path := uriToPath(params.TextDocument.URI)
|
||||
Documents[params.TextDocument.URI] = params.TextDocument.Text
|
||||
p := parser.NewParser(params.TextDocument.Text)
|
||||
config, err := p.Parse()
|
||||
config, _ := p.Parse()
|
||||
|
||||
if err != nil {
|
||||
publishParserError(params.TextDocument.URI, err)
|
||||
} else {
|
||||
publishParserError(params.TextDocument.URI, nil)
|
||||
}
|
||||
publishParserErrors(params.TextDocument.URI, p.Errors())
|
||||
|
||||
if config != nil {
|
||||
Tree.AddFile(path, config)
|
||||
@@ -369,13 +365,9 @@ func HandleDidChange(params DidChangeTextDocumentParams) {
|
||||
Documents[uri] = text
|
||||
path := uriToPath(uri)
|
||||
p := parser.NewParser(text)
|
||||
config, err := p.Parse()
|
||||
config, _ := p.Parse()
|
||||
|
||||
if err != nil {
|
||||
publishParserError(uri, err)
|
||||
} else {
|
||||
publishParserError(uri, nil)
|
||||
}
|
||||
publishParserErrors(uri, p.Errors())
|
||||
|
||||
if config != nil {
|
||||
Tree.AddFile(path, config)
|
||||
@@ -465,6 +457,9 @@ func runValidation(_ string) {
|
||||
// Collect all known files to ensure we clear diagnostics for fixed files
|
||||
knownFiles := make(map[string]bool)
|
||||
collectFiles(Tree.Root, knownFiles)
|
||||
for _, node := range Tree.IsolatedFiles {
|
||||
collectFiles(node, knownFiles)
|
||||
}
|
||||
|
||||
// Initialize all known files with empty diagnostics
|
||||
for f := range knownFiles {
|
||||
@@ -473,8 +468,10 @@ func runValidation(_ string) {
|
||||
|
||||
for _, d := range v.Diagnostics {
|
||||
severity := 1 // Error
|
||||
levelStr := "ERROR"
|
||||
if d.Level == validator.LevelWarning {
|
||||
severity = 2 // Warning
|
||||
levelStr = "WARNING"
|
||||
}
|
||||
|
||||
diag := LSPDiagnostic{
|
||||
@@ -483,7 +480,7 @@ func runValidation(_ string) {
|
||||
End: Position{Line: d.Position.Line - 1, Character: d.Position.Column - 1 + 10}, // Arbitrary length
|
||||
},
|
||||
Severity: severity,
|
||||
Message: d.Message,
|
||||
Message: fmt.Sprintf("%s: %s", levelStr, d.Message),
|
||||
Source: "mdt",
|
||||
}
|
||||
|
||||
@@ -508,20 +505,10 @@ func runValidation(_ string) {
|
||||
}
|
||||
}
|
||||
|
||||
func publishParserError(uri string, err error) {
|
||||
if err == nil {
|
||||
notification := JsonRpcMessage{
|
||||
Jsonrpc: "2.0",
|
||||
Method: "textDocument/publishDiagnostics",
|
||||
Params: mustMarshal(PublishDiagnosticsParams{
|
||||
URI: uri,
|
||||
Diagnostics: []LSPDiagnostic{},
|
||||
}),
|
||||
}
|
||||
send(notification)
|
||||
return
|
||||
}
|
||||
func publishParserErrors(uri string, errors []error) {
|
||||
diagnostics := []LSPDiagnostic{}
|
||||
|
||||
for _, err := range errors {
|
||||
var line, col int
|
||||
var msg string
|
||||
// Try parsing "line:col: message"
|
||||
@@ -547,19 +534,24 @@ func publishParserError(uri string, err error) {
|
||||
Message: msg,
|
||||
Source: "mdt-parser",
|
||||
}
|
||||
diagnostics = append(diagnostics, diag)
|
||||
}
|
||||
|
||||
notification := JsonRpcMessage{
|
||||
Jsonrpc: "2.0",
|
||||
Method: "textDocument/publishDiagnostics",
|
||||
Params: mustMarshal(PublishDiagnosticsParams{
|
||||
URI: uri,
|
||||
Diagnostics: []LSPDiagnostic{diag},
|
||||
Diagnostics: diagnostics,
|
||||
}),
|
||||
}
|
||||
send(notification)
|
||||
}
|
||||
|
||||
func collectFiles(node *index.ProjectNode, files map[string]bool) {
|
||||
if node == nil {
|
||||
return
|
||||
}
|
||||
for _, frag := range node.Fragments {
|
||||
files[frag.File] = true
|
||||
}
|
||||
|
||||
@@ -299,6 +299,8 @@ func (p *Parser) parseAtom() (Value, bool) {
|
||||
return &ReferenceValue{Position: tok.Position, Value: tok.Value}, true
|
||||
case TokenVariableReference:
|
||||
return &VariableReferenceValue{Position: tok.Position, Name: tok.Value}, true
|
||||
case TokenObjectIdentifier:
|
||||
return &VariableReferenceValue{Position: tok.Position, Name: tok.Value}, true
|
||||
case TokenLBrace:
|
||||
arr := &ArrayValue{Position: tok.Position}
|
||||
for {
|
||||
@@ -380,3 +382,7 @@ func (p *Parser) parseVariableDefinition(startTok Token) (Definition, bool) {
|
||||
DefaultValue: defVal,
|
||||
}, true
|
||||
}
|
||||
|
||||
func (p *Parser) Errors() []error {
|
||||
return p.errors
|
||||
}
|
||||
|
||||
@@ -304,7 +304,7 @@ func (v *Validator) validateGAMSignal(gamNode, signalNode *index.ProjectNode, di
|
||||
return // Ignore implicit signals or missing datasource (handled elsewhere if mandatory)
|
||||
}
|
||||
|
||||
dsNode := v.resolveReference(dsName, v.getNodeFile(signalNode), isDataSource)
|
||||
dsNode := v.resolveReference(dsName, signalNode, isDataSource)
|
||||
if dsNode == nil {
|
||||
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
||||
Level: LevelError,
|
||||
@@ -565,17 +565,8 @@ func (v *Validator) getFieldValue(f *parser.Field, ctx *index.ProjectNode) strin
|
||||
return ""
|
||||
}
|
||||
|
||||
func (v *Validator) resolveReference(name string, file string, predicate func(*index.ProjectNode) bool) *index.ProjectNode {
|
||||
if isoNode, ok := v.Tree.IsolatedFiles[file]; ok {
|
||||
if found := v.Tree.FindNode(isoNode, name, predicate); found != nil {
|
||||
return found
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if v.Tree.Root == nil {
|
||||
return nil
|
||||
}
|
||||
return v.Tree.FindNode(v.Tree.Root, name, predicate)
|
||||
func (v *Validator) resolveReference(name string, ctx *index.ProjectNode, predicate func(*index.ProjectNode) bool) *index.ProjectNode {
|
||||
return v.Tree.ResolveName(ctx, name, predicate)
|
||||
}
|
||||
|
||||
func (v *Validator) getNodeClass(node *index.ProjectNode) string {
|
||||
@@ -740,7 +731,7 @@ func (v *Validator) checkFunctionsArray(node *index.ProjectNode, fields map[stri
|
||||
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)
|
||||
target := v.resolveReference(ref.Value, node, isGAM)
|
||||
if target == nil {
|
||||
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
||||
Level: LevelError,
|
||||
@@ -799,19 +790,20 @@ func (v *Validator) CheckDataSourceThreading() {
|
||||
return
|
||||
}
|
||||
|
||||
// 1. Find RealTimeApplication
|
||||
var appNode *index.ProjectNode
|
||||
var appNodes []*index.ProjectNode
|
||||
findApp := func(n *index.ProjectNode) {
|
||||
if cls, ok := n.Metadata["Class"]; ok && cls == "RealTimeApplication" {
|
||||
appNode = n
|
||||
appNodes = append(appNodes, n)
|
||||
}
|
||||
}
|
||||
v.Tree.Walk(findApp)
|
||||
|
||||
if appNode == nil {
|
||||
return
|
||||
for _, appNode := range appNodes {
|
||||
v.checkAppDataSourceThreading(appNode)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Validator) checkAppDataSourceThreading(appNode *index.ProjectNode) {
|
||||
// 2. Find States
|
||||
var statesNode *index.ProjectNode
|
||||
if s, ok := appNode.Children["States"]; ok {
|
||||
@@ -882,7 +874,7 @@ func (v *Validator) getThreadGAMs(thread *index.ProjectNode) []*index.ProjectNod
|
||||
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)
|
||||
target := v.resolveReference(ref.Value, thread, isGAM)
|
||||
if target != nil {
|
||||
gams = append(gams, target)
|
||||
}
|
||||
@@ -904,7 +896,7 @@ func (v *Validator) getGAMDataSources(gam *index.ProjectNode) []*index.ProjectNo
|
||||
fields := v.getFields(sig)
|
||||
if dsFields, ok := fields["DataSource"]; ok && len(dsFields) > 0 {
|
||||
dsName := v.getFieldValue(dsFields[0], sig)
|
||||
dsNode := v.resolveReference(dsName, v.getNodeFile(sig), isDataSource)
|
||||
dsNode := v.resolveReference(dsName, sig, isDataSource)
|
||||
if dsNode != nil {
|
||||
dsMap[dsNode] = true
|
||||
}
|
||||
@@ -938,18 +930,20 @@ func (v *Validator) CheckINOUTOrdering() {
|
||||
return
|
||||
}
|
||||
|
||||
var appNode *index.ProjectNode
|
||||
var appNodes []*index.ProjectNode
|
||||
findApp := func(n *index.ProjectNode) {
|
||||
if cls, ok := n.Metadata["Class"]; ok && cls == "RealTimeApplication" {
|
||||
appNode = n
|
||||
appNodes = append(appNodes, n)
|
||||
}
|
||||
}
|
||||
v.Tree.Walk(findApp)
|
||||
|
||||
if appNode == nil {
|
||||
return
|
||||
for _, appNode := range appNodes {
|
||||
v.checkAppINOUTOrdering(appNode)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Validator) checkAppINOUTOrdering(appNode *index.ProjectNode) {
|
||||
var statesNode *index.ProjectNode
|
||||
if s, ok := appNode.Children["States"]; ok {
|
||||
statesNode = s
|
||||
@@ -1049,7 +1043,7 @@ func (v *Validator) processGAMSignalsForOrdering(gam *index.ProjectNode, contain
|
||||
if dsNode == nil {
|
||||
if dsFields, ok := fields["DataSource"]; ok && len(dsFields) > 0 {
|
||||
dsName := v.getFieldValue(dsFields[0], sig)
|
||||
dsNode = v.resolveReference(dsName, v.getNodeFile(sig), isDataSource)
|
||||
dsNode = v.resolveReference(dsName, sig, isDataSource)
|
||||
}
|
||||
if aliasFields, ok := fields["Alias"]; ok && len(aliasFields) > 0 {
|
||||
sigName = v.getFieldValue(aliasFields[0], sig)
|
||||
|
||||
101
test/lsp_fuzz_test.go
Normal file
101
test/lsp_fuzz_test.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/marte-community/marte-dev-tools/internal/lsp"
|
||||
)
|
||||
|
||||
func TestIncrementalFuzz(t *testing.T) {
|
||||
// Initialize
|
||||
lsp.Documents = make(map[string]string)
|
||||
uri := "file://fuzz.marte"
|
||||
currentText := ""
|
||||
lsp.Documents[uri] = currentText
|
||||
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
// Apply 1000 random edits
|
||||
for i := 0; i < 1000; i++ {
|
||||
// Randomly choose Insert or Delete
|
||||
isInsert := rand.Intn(2) == 0
|
||||
|
||||
change := lsp.TextDocumentContentChangeEvent{}
|
||||
|
||||
// Use simple ascii string
|
||||
length := len(currentText)
|
||||
|
||||
if isInsert || length == 0 {
|
||||
// Insert
|
||||
pos := 0
|
||||
if length > 0 {
|
||||
pos = rand.Intn(length + 1)
|
||||
}
|
||||
|
||||
insertStr := "X"
|
||||
if rand.Intn(5) == 0 { insertStr = "\n" }
|
||||
if rand.Intn(10) == 0 { insertStr = "longstring" }
|
||||
|
||||
// Calculate Line/Char for pos
|
||||
line, char := offsetToLineChar(currentText, pos)
|
||||
|
||||
change.Range = &lsp.Range{
|
||||
Start: lsp.Position{Line: line, Character: char},
|
||||
End: lsp.Position{Line: line, Character: char},
|
||||
}
|
||||
change.Text = insertStr
|
||||
|
||||
// Expected
|
||||
currentText = currentText[:pos] + insertStr + currentText[pos:]
|
||||
} else {
|
||||
// Delete
|
||||
start := rand.Intn(length)
|
||||
end := start + 1 + rand.Intn(length - start) // at least 1 char
|
||||
|
||||
// Range
|
||||
l1, c1 := offsetToLineChar(currentText, start)
|
||||
l2, c2 := offsetToLineChar(currentText, end)
|
||||
|
||||
change.Range = &lsp.Range{
|
||||
Start: lsp.Position{Line: l1, Character: c1},
|
||||
End: lsp.Position{Line: l2, Character: c2},
|
||||
}
|
||||
change.Text = ""
|
||||
|
||||
currentText = currentText[:start] + currentText[end:]
|
||||
}
|
||||
|
||||
// Apply
|
||||
lsp.HandleDidChange(lsp.DidChangeTextDocumentParams{
|
||||
TextDocument: lsp.VersionedTextDocumentIdentifier{URI: uri, Version: i},
|
||||
ContentChanges: []lsp.TextDocumentContentChangeEvent{change},
|
||||
})
|
||||
|
||||
// Verify
|
||||
if lsp.Documents[uri] != currentText {
|
||||
t.Fatalf("Fuzz iteration %d failed.\nExpected len: %d\nGot len: %d\nChange: %+v", i, len(currentText), len(lsp.Documents[uri]), change)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func offsetToLineChar(text string, offset int) (int, int) {
|
||||
line := 0
|
||||
char := 0
|
||||
for i, r := range text {
|
||||
if i == offset {
|
||||
return line, char
|
||||
}
|
||||
if r == '\n' {
|
||||
line++
|
||||
char = 0
|
||||
} else {
|
||||
char++
|
||||
}
|
||||
}
|
||||
if offset == len(text) {
|
||||
return line, char
|
||||
}
|
||||
return -1, -1
|
||||
}
|
||||
204
test/lsp_incremental_correctness_test.go
Normal file
204
test/lsp_incremental_correctness_test.go
Normal file
@@ -0,0 +1,204 @@
|
||||
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 TestIncrementalCorrectness(t *testing.T) {
|
||||
lsp.Documents = make(map[string]string)
|
||||
uri := "file://test.txt"
|
||||
initial := "12345\n67890"
|
||||
lsp.Documents[uri] = initial
|
||||
|
||||
// Edit 1: Insert "A" at 0:1 -> "1A2345\n67890"
|
||||
change1 := lsp.TextDocumentContentChangeEvent{
|
||||
Range: &lsp.Range{Start: lsp.Position{Line: 0, Character: 1}, End: lsp.Position{Line: 0, Character: 1}},
|
||||
Text: "A",
|
||||
}
|
||||
lsp.HandleDidChange(lsp.DidChangeTextDocumentParams{
|
||||
TextDocument: lsp.VersionedTextDocumentIdentifier{URI: uri},
|
||||
ContentChanges: []lsp.TextDocumentContentChangeEvent{change1},
|
||||
})
|
||||
|
||||
if lsp.Documents[uri] != "1A2345\n67890" {
|
||||
t.Errorf("Edit 1 failed: %q", lsp.Documents[uri])
|
||||
}
|
||||
|
||||
// Edit 2: Delete newline (merge lines)
|
||||
// "1A2345\n67890" -> "1A234567890"
|
||||
// \n is at index 6.
|
||||
// 0:6 points to \n? "1A2345" length is 6.
|
||||
// So 0:6 is AFTER '5', at '\n'.
|
||||
// 1:0 is AFTER '\n', at '6'.
|
||||
// Range 0:6 - 1:0 covers '\n'.
|
||||
change2 := lsp.TextDocumentContentChangeEvent{
|
||||
Range: &lsp.Range{Start: lsp.Position{Line: 0, Character: 6}, End: lsp.Position{Line: 1, Character: 0}},
|
||||
Text: "",
|
||||
}
|
||||
lsp.HandleDidChange(lsp.DidChangeTextDocumentParams{
|
||||
TextDocument: lsp.VersionedTextDocumentIdentifier{URI: uri},
|
||||
ContentChanges: []lsp.TextDocumentContentChangeEvent{change2},
|
||||
})
|
||||
|
||||
if lsp.Documents[uri] != "1A234567890" {
|
||||
t.Errorf("Edit 2 failed: %q", lsp.Documents[uri])
|
||||
}
|
||||
|
||||
// Edit 3: Add newline at end
|
||||
// "1A234567890" len 11.
|
||||
// 0:11.
|
||||
change3 := lsp.TextDocumentContentChangeEvent{
|
||||
Range: &lsp.Range{Start: lsp.Position{Line: 0, Character: 11}, End: lsp.Position{Line: 0, Character: 11}},
|
||||
Text: "\n",
|
||||
}
|
||||
lsp.HandleDidChange(lsp.DidChangeTextDocumentParams{
|
||||
TextDocument: lsp.VersionedTextDocumentIdentifier{URI: uri},
|
||||
ContentChanges: []lsp.TextDocumentContentChangeEvent{change3},
|
||||
})
|
||||
|
||||
if lsp.Documents[uri] != "1A234567890\n" {
|
||||
t.Errorf("Edit 3 failed: %q", lsp.Documents[uri])
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncrementalAppValidation(t *testing.T) {
|
||||
// Setup
|
||||
lsp.Tree = index.NewProjectTree()
|
||||
lsp.Documents = make(map[string]string)
|
||||
lsp.GlobalSchema = schema.LoadFullSchema(".")
|
||||
var buf bytes.Buffer
|
||||
lsp.Output = &buf
|
||||
|
||||
content := `// Test app
|
||||
+App = {
|
||||
Class = RealTimeApplication
|
||||
+Data = {
|
||||
Class = ReferenceContainer
|
||||
DefaultDataSource = DDB
|
||||
+DDB = {
|
||||
Class = GAMDataSource
|
||||
}
|
||||
+TimingDataSource = {
|
||||
Class = TimingDataSource
|
||||
}
|
||||
}
|
||||
+Functions = {
|
||||
Class = ReferenceContainer
|
||||
+A = {
|
||||
Class = IOGAM
|
||||
InputSignals = {
|
||||
A = {
|
||||
DataSource = DDB
|
||||
Type = uint32
|
||||
// Placeholder
|
||||
}
|
||||
}
|
||||
OutputSignals = {
|
||||
B = {
|
||||
DataSource = DDB
|
||||
Type = uint32
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+States = {
|
||||
Class = ReferenceContainer
|
||||
+State = {
|
||||
Class =RealTimeState
|
||||
Threads = {
|
||||
+Th1 = {
|
||||
Class = RealTimeThread
|
||||
Functions = {A}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+Scheduler = {
|
||||
Class = GAMScheduler
|
||||
TimingDataSource = TimingDataSource
|
||||
}
|
||||
}
|
||||
`
|
||||
uri := "file://app_inc.marte"
|
||||
|
||||
// 1. Open
|
||||
lsp.HandleDidOpen(lsp.DidOpenTextDocumentParams{
|
||||
TextDocument: lsp.TextDocumentItem{URI: uri, Text: content},
|
||||
})
|
||||
|
||||
out := buf.String()
|
||||
|
||||
// Signal A is never produced. Should have consumed error.
|
||||
if !strings.Contains(out, "ERROR: INOUT Signal 'A'") {
|
||||
t.Error("Missing consumed error for A")
|
||||
}
|
||||
// Signal B is Output, never consumed.
|
||||
if !strings.Contains(out, "WARNING: INOUT Signal 'B'") {
|
||||
t.Error("Missing produced error for B")
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
|
||||
// 2. Insert comment at start
|
||||
// Expecting same errors
|
||||
change1 := lsp.TextDocumentContentChangeEvent{
|
||||
Range: &lsp.Range{Start: lsp.Position{Line: 0, Character: 0}, End: lsp.Position{Line: 0, Character: 0}},
|
||||
Text: "// Comment\n",
|
||||
}
|
||||
lsp.HandleDidChange(lsp.DidChangeTextDocumentParams{
|
||||
TextDocument: lsp.VersionedTextDocumentIdentifier{URI: uri},
|
||||
ContentChanges: []lsp.TextDocumentContentChangeEvent{change1},
|
||||
})
|
||||
|
||||
out = buf.String()
|
||||
// Signal A is never produced. Should have consumed error.
|
||||
if !strings.Contains(out, "ERROR: INOUT Signal 'A'") {
|
||||
t.Error("Missing consumed error for A")
|
||||
}
|
||||
// Signal B is Output, never consumed.
|
||||
if !strings.Contains(out, "WARNING: INOUT Signal 'B'") {
|
||||
t.Error("Missing produced error for B")
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
|
||||
// 3. Add Value to A
|
||||
currentText := lsp.Documents[uri]
|
||||
idx := strings.Index(currentText, "Placeholder")
|
||||
if idx == -1 {
|
||||
t.Fatal("Could not find anchor string")
|
||||
}
|
||||
|
||||
idx = strings.Index(currentText[idx:], "\n") + idx
|
||||
insertPos := idx + 1
|
||||
|
||||
line, char := offsetToLineChar(currentText, insertPos)
|
||||
|
||||
change2 := lsp.TextDocumentContentChangeEvent{
|
||||
Range: &lsp.Range{Start: lsp.Position{Line: line, Character: char}, End: lsp.Position{Line: line, Character: char}},
|
||||
Text: "Value = 10\n",
|
||||
}
|
||||
|
||||
lsp.HandleDidChange(lsp.DidChangeTextDocumentParams{
|
||||
TextDocument: lsp.VersionedTextDocumentIdentifier{URI: uri},
|
||||
ContentChanges: []lsp.TextDocumentContentChangeEvent{change2},
|
||||
})
|
||||
|
||||
out = buf.String()
|
||||
|
||||
// Signal A has now a Value field and so it is produced. Should NOT have consumed error.
|
||||
if strings.Contains(out, "ERROR: INOUT Signal 'A'") {
|
||||
t.Error("Unexpected consumed error for A")
|
||||
}
|
||||
// Signal B is Output, never consumed.
|
||||
if !strings.Contains(out, "WARNING: INOUT Signal 'B'") {
|
||||
t.Error("Missing produced error for B")
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user