diff --git a/docs/CONFIGURATION_GUIDE.md b/docs/CONFIGURATION_GUIDE.md index c1dc34c..4789fde 100644 --- a/docs/CONFIGURATION_GUIDE.md +++ b/docs/CONFIGURATION_GUIDE.md @@ -90,6 +90,58 @@ Common classes (`RealTimeApplication`, `StateMachine`, `IOGAM`, etc.) are built- ### Custom Schemas You can extend the schema by creating a `.marte_schema.cue` file in your project root. +## 4. Variables and Constants + +You can define variables to parameterize your configuration. + +### Variables (`#var`) +Variables can be defined at any level and can be overridden externally (e.g., via CLI). + +```marte +//# Default timeout +#var Timeout: uint32 = 100 + ++MyObject = { + Class = Timer + Timeout = @Timeout +} +``` + +### Constants (`#let`) +Constants are like variables but **cannot** be overridden externally. They are ideal for internal calculations or fixed parameters. + +```marte +//# Sampling period +#let Ts: float64 = 0.001 + ++Clock = { + Class = HighResClock + Period = @Ts +} +``` + +### Expressions +Variables and constants can be used in expressions: +- Arithmetic: `+`, `-`, `*`, `/`, `%` +- Bitwise: `&`, `|`, `^` +- String Concatenation: `..` + +```marte +#var BasePath: string = "/tmp" +#let LogFile: string = @BasePath .. "/app.log" +``` + +### Docstrings +Docstrings (`//#`) work for variables and constants and are displayed in the LSP hover information. + +## 5. Pragmas +Macros can be controlled via pragmas: +- `//! allow(implicit)`: Suppress warnings for implicitly defined signals. +- `//! allow(unused)`: Suppress warnings for unused signals/GAMs. +- `//! ignore(not_consumed)`: Suppress ordering warnings for specific signals. + +Pragmas can be global (top-level) or local to a node. + **Example: Adding a custom GAM** ```cue diff --git a/internal/builder/builder.go b/internal/builder/builder.go index 1416594..5ddafb7 100644 --- a/internal/builder/builder.go +++ b/internal/builder/builder.go @@ -213,17 +213,21 @@ func (b *Builder) collectVariables(tree *index.ProjectTree) { for _, def := range frag.Definitions { if vdef, ok := def.(*parser.VariableDefinition); ok { if valStr, ok := b.Overrides[vdef.Name]; ok { - p := parser.NewParser("Temp = " + valStr) - cfg, _ := p.Parse() - if len(cfg.Definitions) > 0 { - if f, ok := cfg.Definitions[0].(*parser.Field); ok { - b.variables[vdef.Name] = f.Value - continue + if !vdef.IsConst { + p := parser.NewParser("Temp = " + valStr) + cfg, _ := p.Parse() + if len(cfg.Definitions) > 0 { + if f, ok := cfg.Definitions[0].(*parser.Field); ok { + b.variables[vdef.Name] = f.Value + continue + } } } } if vdef.DefaultValue != nil { - b.variables[vdef.Name] = vdef.DefaultValue + if _, ok := b.variables[vdef.Name]; !ok || vdef.IsConst { + b.variables[vdef.Name] = vdef.DefaultValue + } } } } diff --git a/internal/formatter/formatter.go b/internal/formatter/formatter.go index c2dcaae..21a19d8 100644 --- a/internal/formatter/formatter.go +++ b/internal/formatter/formatter.go @@ -103,7 +103,11 @@ func (f *Formatter) formatDefinition(def parser.Definition, indent int) int { fmt.Fprintf(f.writer, "%s}", indentStr) return d.Subnode.EndPosition.Line case *parser.VariableDefinition: - fmt.Fprintf(f.writer, "%s#var %s: %s", indentStr, d.Name, d.TypeExpr) + macro := "#var" + if d.IsConst { + macro = "#let" + } + fmt.Fprintf(f.writer, "%s%s %s: %s", indentStr, macro, d.Name, d.TypeExpr) if d.DefaultValue != nil { fmt.Fprint(f.writer, " = ") endLine := f.formatValue(d.DefaultValue, indent) @@ -151,6 +155,15 @@ func (f *Formatter) formatValue(val parser.Value, indent int) int { case *parser.VariableReferenceValue: fmt.Fprint(f.writer, v.Name) return v.Position.Line + case *parser.BinaryExpression: + f.formatValue(v.Left, indent) + fmt.Fprintf(f.writer, " %s ", v.Operator.Value) + f.formatValue(v.Right, indent) + return v.Position.Line + case *parser.UnaryExpression: + fmt.Fprint(f.writer, v.Operator.Value) + f.formatValue(v.Right, indent) + return v.Position.Line case *parser.ArrayValue: fmt.Fprint(f.writer, "{ ") for i, e := range v.Elements { diff --git a/internal/index/index.go b/internal/index/index.go index 18832e8..bc1586a 100644 --- a/internal/index/index.go +++ b/internal/index/index.go @@ -5,12 +5,14 @@ import ( "path/filepath" "strings" + "github.com/marte-community/marte-dev-tools/internal/logger" "github.com/marte-community/marte-dev-tools/internal/parser" ) type VariableInfo struct { Def *parser.VariableDefinition File string + Doc string } type ProjectTree struct { @@ -27,13 +29,14 @@ func (pt *ProjectTree) ScanDirectory(rootPath string) error { return err } if !info.IsDir() && strings.HasSuffix(info.Name(), ".marte") { + logger.Printf("indexing: %s [%s]\n", info.Name(), path) content, err := os.ReadFile(path) if err != nil { return err // Or log and continue } p := parser.NewParser(string(content)) - config, err := p.Parse() - if err == nil { + config, _ := p.Parse() + if config != nil { pt.AddFile(path, config) } } @@ -232,7 +235,7 @@ func (pt *ProjectTree) populateNode(node *ProjectNode, file string, config *pars pt.indexValue(file, d.Value) case *parser.VariableDefinition: fileFragment.Definitions = append(fileFragment.Definitions, d) - node.Variables[d.Name] = VariableInfo{Def: d, File: file} + node.Variables[d.Name] = VariableInfo{Def: d, File: file, Doc: doc} case *parser.ObjectNode: fileFragment.Definitions = append(fileFragment.Definitions, d) norm := NormalizeName(d.Name) @@ -291,7 +294,7 @@ func (pt *ProjectTree) addObjectFragment(node *ProjectNode, file string, obj *pa pt.extractFieldMetadata(node, d) case *parser.VariableDefinition: frag.Definitions = append(frag.Definitions, d) - node.Variables[d.Name] = VariableInfo{Def: d, File: file} + node.Variables[d.Name] = VariableInfo{Def: d, File: file, Doc: subDoc} case *parser.ObjectNode: frag.Definitions = append(frag.Definitions, d) norm := NormalizeName(d.Name) diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 9189304..c93c13a 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -589,10 +589,19 @@ func HandleHover(params HoverParams) *Hover { } else if res.Field != nil { content = fmt.Sprintf("**Field**: `%s`", res.Field.Name) } else if res.Variable != nil { - content = fmt.Sprintf("**Variable**: `%s`\nType: `%s`", res.Variable.Name, res.Variable.TypeExpr) + kind := "Variable" + if res.Variable.IsConst { + kind = "Constant" + } + content = fmt.Sprintf("**%s**: `%s`\nType: `%s`", kind, res.Variable.Name, res.Variable.TypeExpr) if res.Variable.DefaultValue != nil { content += fmt.Sprintf("\nDefault: `%s`", valueToString(res.Variable.DefaultValue, container)) } + if info := Tree.ResolveVariable(container, res.Variable.Name); info != nil { + if info.Doc != "" { + content += "\n\n" + info.Doc + } + } } else if res.Reference != nil { targetName := "Unresolved" fullInfo := "" @@ -605,10 +614,19 @@ func HandleHover(params HoverParams) *Hover { } else if res.Reference.TargetVariable != nil { v := res.Reference.TargetVariable targetName = v.Name - fullInfo = fmt.Sprintf("**Variable**: `@%s`\nType: `%s`", v.Name, v.TypeExpr) + kind := "Variable" + if v.IsConst { + kind = "Constant" + } + fullInfo = fmt.Sprintf("**%s**: `@%s`\nType: `%s`", kind, v.Name, v.TypeExpr) if v.DefaultValue != nil { fullInfo += fmt.Sprintf("\nDefault: `%s`", valueToString(v.DefaultValue, container)) } + if info := Tree.ResolveVariable(container, res.Reference.Name); info != nil { + if info.Doc != "" { + fullInfo += "\n\n" + info.Doc + } + } } content = fmt.Sprintf("**Reference**: `%s` -> `%s`", res.Reference.Name, targetName) @@ -678,6 +696,17 @@ func HandleCompletion(params CompletionParams) *CompletionList { prefix := lineStr[:col] + // Case 4: Top-level keywords/macros + if strings.HasPrefix(prefix, "#") && !strings.Contains(prefix, " ") { + return &CompletionList{ + Items: []CompletionItem{ + {Label: "#package", Kind: 14, InsertText: "#package ${1:Project.URI}", InsertTextFormat: 2, Detail: "Project namespace definition"}, + {Label: "#var", Kind: 14, InsertText: "#var ${1:Name}: ${2:Type} = ${3:DefaultValue}", InsertTextFormat: 2, Detail: "Variable definition"}, + {Label: "#let", Kind: 14, InsertText: "#let ${1:Name}: ${2:Type} = ${3:Value}", InsertTextFormat: 2, Detail: "Constant variable definition"}, + }, + } + } + // Case 3: Variable completion varRegex := regexp.MustCompile(`([@])([a-zA-Z0-9_]*)$`) if matches := varRegex.FindStringSubmatch(prefix); matches != nil { @@ -1254,6 +1283,17 @@ func HandleReferences(params ReferenceParams) []Location { return locations } +func getEvaluatedMetadata(node *index.ProjectNode, key string) string { + for _, frag := range node.Fragments { + for _, def := range frag.Definitions { + if f, ok := def.(*parser.Field); ok && f.Name == key { + return valueToString(f.Value, node) + } + } + } + return node.Metadata[key] +} + func formatNodeInfo(node *index.ProjectNode) string { info := "" if class := node.Metadata["Class"]; class != "" { @@ -1262,8 +1302,8 @@ func formatNodeInfo(node *index.ProjectNode) string { info = fmt.Sprintf("`%s`\n\n", node.RealName) } // Check if it's a Signal (has Type or DataSource) - typ := node.Metadata["Type"] - ds := node.Metadata["DataSource"] + typ := getEvaluatedMetadata(node, "Type") + ds := getEvaluatedMetadata(node, "DataSource") if ds == "" { if node.Parent != nil && node.Parent.Name == "Signals" { @@ -1283,8 +1323,8 @@ func formatNodeInfo(node *index.ProjectNode) string { } // Size - dims := node.Metadata["NumberOfDimensions"] - elems := node.Metadata["NumberOfElements"] + dims := getEvaluatedMetadata(node, "NumberOfDimensions") + elems := getEvaluatedMetadata(node, "NumberOfElements") if dims != "" || elems != "" { sigInfo += fmt.Sprintf("**Size**: `[%s]`, `%s` dims ", elems, dims) } @@ -1696,10 +1736,15 @@ func suggestVariables(container *index.ProjectNode) *CompletionList { doc = fmt.Sprintf("Default: %s", valueToString(info.Def.DefaultValue, container)) } + kind := "Variable" + if info.Def.IsConst { + kind = "Constant" + } + items = append(items, CompletionItem{ Label: name, Kind: 6, // Variable - Detail: fmt.Sprintf("Variable (%s)", info.Def.TypeExpr), + Detail: fmt.Sprintf("%s (%s)", kind, info.Def.TypeExpr), Documentation: doc, }) } diff --git a/internal/parser/ast.go b/internal/parser/ast.go index afe6bc6..c7140e8 100644 --- a/internal/parser/ast.go +++ b/internal/parser/ast.go @@ -131,6 +131,7 @@ type VariableDefinition struct { Name string TypeExpr string DefaultValue Value + IsConst bool } func (v *VariableDefinition) Pos() Position { return v.Position } diff --git a/internal/parser/lexer.go b/internal/parser/lexer.go index cbfcfa7..f3cd8a8 100644 --- a/internal/parser/lexer.go +++ b/internal/parser/lexer.go @@ -20,6 +20,7 @@ const ( TokenBool TokenPackage TokenPragma + TokenLet TokenComment TokenDocstring TokenComma @@ -236,7 +237,21 @@ func (l *Lexer) lexString() Token { } func (l *Lexer) lexNumber() Token { - // Consume initial digits (already started) + // Check for hex or binary prefix if we started with '0' + if l.input[l.start:l.pos] == "0" { + switch l.peek() { + case 'x', 'X': + l.next() + l.lexHexDigits() + return l.emit(TokenNumber) + case 'b', 'B': + l.next() + l.lexBinaryDigits() + return l.emit(TokenNumber) + } + } + + // Consume remaining digits l.lexDigits() if l.peek() == '.' { @@ -255,6 +270,28 @@ func (l *Lexer) lexNumber() Token { return l.emit(TokenNumber) } +func (l *Lexer) lexHexDigits() { + for { + r := l.peek() + if unicode.IsDigit(r) || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F') { + l.next() + } else { + break + } + } +} + +func (l *Lexer) lexBinaryDigits() { + for { + r := l.peek() + if r == '0' || r == '1' { + l.next() + } else { + break + } + } +} + func (l *Lexer) lexDigits() { for unicode.IsDigit(l.peek()) { l.next() @@ -321,6 +358,9 @@ func (l *Lexer) lexHashIdentifier() Token { if val == "#package" { return l.lexUntilNewline(TokenPackage) } + if val == "#let" { + return l.emit(TokenLet) + } return l.emit(TokenIdentifier) } diff --git a/internal/parser/parser.go b/internal/parser/parser.go index d86988a..74a9d64 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -99,6 +99,8 @@ func (p *Parser) Parse() (*Configuration, error) { func (p *Parser) parseDefinition() (Definition, bool) { tok := p.next() switch tok.Type { + case TokenLet: + return p.parseLet(tok) case TokenIdentifier: name := tok.Value if name == "#var" { @@ -286,7 +288,11 @@ func (p *Parser) parseAtom() (Value, bool) { }, true case TokenNumber: - if strings.Contains(tok.Value, ".") || strings.Contains(tok.Value, "e") { + isFloat := (strings.Contains(tok.Value, ".") || strings.Contains(tok.Value, "e") || strings.Contains(tok.Value, "E")) && + !strings.HasPrefix(tok.Value, "0x") && !strings.HasPrefix(tok.Value, "0X") && + !strings.HasPrefix(tok.Value, "0b") && !strings.HasPrefix(tok.Value, "0B") + + if isFloat { f, _ := strconv.ParseFloat(tok.Value, 64) return &FloatValue{Position: tok.Position, Value: f, Raw: tok.Value}, true } @@ -409,6 +415,58 @@ func (p *Parser) parseVariableDefinition(startTok Token) (Definition, bool) { }, true } +func (p *Parser) parseLet(startTok Token) (Definition, bool) { + nameTok := p.next() + if nameTok.Type != TokenIdentifier { + p.addError(nameTok.Position, "expected constant name") + return nil, false + } + + if p.next().Type != TokenColon { + p.addError(nameTok.Position, "expected :") + return nil, false + } + + var typeTokens []Token + startLine := nameTok.Position.Line + + for { + t := p.peek() + if t.Position.Line > startLine || t.Type == TokenEOF { + break + } + if t.Type == TokenEqual { + break + } + typeTokens = append(typeTokens, p.next()) + } + + typeExpr := "" + for _, t := range typeTokens { + typeExpr += t.Value + " " + } + + var defVal Value + if p.next().Type != TokenEqual { + p.addError(nameTok.Position, "expected =") + return nil, false + } + val, ok := p.parseValue() + if ok { + defVal = val + } else { + return nil, false + } + + return &VariableDefinition{ + Position: startTok.Position, + Name: nameTok.Value, + TypeExpr: strings.TrimSpace(typeExpr), + DefaultValue: defVal, + IsConst: true, + }, true +} + func (p *Parser) Errors() []error { return p.errors } diff --git a/internal/validator/validator.go b/internal/validator/validator.go index 6d892c9..44c36fe 100644 --- a/internal/validator/validator.go +++ b/internal/validator/validator.go @@ -577,9 +577,20 @@ func (v *Validator) validateGAMSignal(gamNode, signalNode *index.ProjectNode, di } } +func (v *Validator) getEvaluatedMetadata(node *index.ProjectNode, key string) string { + for _, frag := range node.Fragments { + for _, def := range frag.Definitions { + if f, ok := def.(*parser.Field); ok && f.Name == key { + return v.getFieldValue(f, node) + } + } + } + return node.Metadata[key] +} + func (v *Validator) checkSignalProperty(gamSig, dsSig *index.ProjectNode, prop string) { - gamVal := gamSig.Metadata[prop] - dsVal := dsSig.Metadata[prop] + gamVal := v.getEvaluatedMetadata(gamSig, prop) + dsVal := v.getEvaluatedMetadata(dsSig, prop) if gamVal == "" { return @@ -646,26 +657,11 @@ func (v *Validator) getFields(node *index.ProjectNode) map[string][]*parser.Fiel } func (v *Validator) getFieldValue(f *parser.Field, ctx *index.ProjectNode) string { - switch val := f.Value.(type) { - case *parser.StringValue: - return val.Value - case *parser.ReferenceValue: - return val.Value - case *parser.IntValue: - return val.Raw - case *parser.FloatValue: - return val.Raw - case *parser.BoolValue: - return strconv.FormatBool(val.Value) - case *parser.VariableReferenceValue: - name := strings.TrimPrefix(val.Name, "@") - if info := v.Tree.ResolveVariable(ctx, name); info != nil { - if info.Def.DefaultValue != nil { - return v.getFieldValue(&parser.Field{Value: info.Def.DefaultValue}, ctx) - } - } + res := v.valueToInterface(f.Value, ctx) + if res == nil { + return "" } - return "" + return fmt.Sprintf("%v", res) } func (v *Validator) resolveReference(name string, ctx *index.ProjectNode, predicate func(*index.ProjectNode) bool) *index.ProjectNode { @@ -1328,34 +1324,57 @@ func (v *Validator) CheckVariables() { ctx := v.Schema.Context checkNodeVars := func(node *index.ProjectNode) { - for _, info := range node.Variables { - def := info.Def + seen := make(map[string]parser.Position) + for _, frag := range node.Fragments { + for _, def := range frag.Definitions { + if vdef, ok := def.(*parser.VariableDefinition); ok { + if prevPos, exists := seen[vdef.Name]; exists { + v.Diagnostics = append(v.Diagnostics, Diagnostic{ + Level: LevelError, + Message: fmt.Sprintf("Duplicate variable definition: '%s' was already defined at %d:%d", vdef.Name, prevPos.Line, prevPos.Column), + Position: vdef.Position, + File: frag.File, + }) + } + seen[vdef.Name] = vdef.Position - // Compile Type - typeVal := ctx.CompileString(def.TypeExpr) - if typeVal.Err() != nil { - v.Diagnostics = append(v.Diagnostics, Diagnostic{ - Level: LevelError, - Message: fmt.Sprintf("Invalid type expression for variable '%s': %v", def.Name, typeVal.Err()), - Position: def.Position, - File: info.File, - }) - continue - } + if vdef.IsConst && vdef.DefaultValue == nil { + v.Diagnostics = append(v.Diagnostics, Diagnostic{ + Level: LevelError, + Message: fmt.Sprintf("Constant variable '%s' must have an initial value", vdef.Name), + Position: vdef.Position, + File: frag.File, + }) + continue + } - if def.DefaultValue != nil { - valInterface := v.valueToInterface(def.DefaultValue, node) - valVal := ctx.Encode(valInterface) + // Compile Type + typeVal := ctx.CompileString(vdef.TypeExpr) + if typeVal.Err() != nil { + v.Diagnostics = append(v.Diagnostics, Diagnostic{ + Level: LevelError, + Message: fmt.Sprintf("Invalid type expression for variable '%s': %v", vdef.Name, typeVal.Err()), + Position: vdef.Position, + File: frag.File, + }) + continue + } - // Unify - res := typeVal.Unify(valVal) - if err := res.Validate(cue.Concrete(true)); err != nil { - v.Diagnostics = append(v.Diagnostics, Diagnostic{ - Level: LevelError, - Message: fmt.Sprintf("Variable '%s' value mismatch: %v", def.Name, err), - Position: def.Position, - File: info.File, - }) + if vdef.DefaultValue != nil { + valInterface := v.valueToInterface(vdef.DefaultValue, node) + valVal := ctx.Encode(valInterface) + + // Unify + res := typeVal.Unify(valVal) + if err := res.Validate(cue.Concrete(true)); err != nil { + v.Diagnostics = append(v.Diagnostics, Diagnostic{ + Level: LevelError, + Message: fmt.Sprintf("Variable '%s' value mismatch: %v", vdef.Name, err), + Position: vdef.Position, + File: frag.File, + }) + } + } } } } diff --git a/test/advanced_numbers_test.go b/test/advanced_numbers_test.go new file mode 100644 index 0000000..e886e4e --- /dev/null +++ b/test/advanced_numbers_test.go @@ -0,0 +1,78 @@ +package integration + +import ( + "testing" + "github.com/marte-community/marte-dev-tools/internal/parser" + "github.com/marte-community/marte-dev-tools/internal/formatter" + "bytes" +) + +func TestAdvancedNumbers(t *testing.T) { + content := ` +Hex = 0xFF +HexLower = 0xee +Binary = 0b1011 +Decimal = 123 +Scientific = 1e-3 +` + p := parser.NewParser(content) + cfg, err := p.Parse() + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + // Verify values + foundHex := false + foundHexLower := false + foundBinary := false + for _, def := range cfg.Definitions { + if f, ok := def.(*parser.Field); ok { + if f.Name == "Hex" { + if v, ok := f.Value.(*parser.IntValue); ok { + if v.Value != 255 { + t.Errorf("Expected 255 for Hex, got %d", v.Value) + } + foundHex = true + } + } + if f.Name == "HexLower" { + if v, ok := f.Value.(*parser.IntValue); ok { + if v.Value != 238 { + t.Errorf("Expected 238 for HexLower, got %d", v.Value) + } + foundHexLower = true + } else { + t.Errorf("HexLower was parsed as %T, expected *parser.IntValue", f.Value) + } + } + if f.Name == "Binary" { + if v, ok := f.Value.(*parser.IntValue); ok { + if v.Value == 11 { + foundBinary = true + } + } + } + } + } + if !foundHex { t.Error("Hex field not found") } + if !foundHexLower { t.Error("HexLower field not found") } + if !foundBinary { t.Error("Binary field not found") } + + // Verify formatting + var buf bytes.Buffer + formatter.Format(cfg, &buf) + formatted := buf.String() + if !contains(formatted, "Hex = 0xFF") { + t.Errorf("Formatted content missing Hex = 0xFF:\n%s", formatted) + } + if !contains(formatted, "HexLower = 0xee") { + t.Errorf("Formatted content missing HexLower = 0xee:\n%s", formatted) + } + if !contains(formatted, "Binary = 0b1011") { + t.Errorf("Formatted content missing Binary = 0b1011:\n%s", formatted) + } +} + +func contains(s, substr string) bool { + return bytes.Contains([]byte(s), []byte(substr)) +} \ No newline at end of file diff --git a/test/evaluated_signal_props_test.go b/test/evaluated_signal_props_test.go new file mode 100644 index 0000000..96dd164 --- /dev/null +++ b/test/evaluated_signal_props_test.go @@ -0,0 +1,88 @@ +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 TestEvaluatedSignalProperties(t *testing.T) { + content := ` +#let N: uint32 = 10 ++DS = { + Class = FileReader + Filename = "test.bin" + Signals = { + Sig1 = { Type = uint32 NumberOfElements = @N } + } +} ++GAM = { + Class = IOGAM + InputSignals = { + Sig1 = { DataSource = DS Type = uint32 NumberOfElements = 10 } + } +} +` + p := parser.NewParser(content) + cfg, err := p.Parse() + if err != nil { + t.Fatal(err) + } + + tree := index.NewProjectTree() + tree.AddFile("test.marte", cfg) + tree.ResolveReferences() + + v := validator.NewValidator(tree, ".") + v.ValidateProject() + + // There should be no errors because @N evaluates to 10 + for _, d := range v.Diagnostics { + if d.Level == validator.LevelError { + t.Errorf("Unexpected error: %s", d.Message) + } + } + + // Test mismatch with expression + contentErr := ` +#let N: uint32 = 10 ++DS = { + Class = FileReader + Filename = "test.bin" + Signals = { + Sig1 = { Type = uint32 NumberOfElements = @N + 5 } + } +} ++GAM = { + Class = IOGAM + InputSignals = { + Sig1 = { DataSource = DS Type = uint32 NumberOfElements = 10 } + } +} +` + p2 := parser.NewParser(contentErr) + cfg2, _ := p2.Parse() + tree2 := index.NewProjectTree() + tree2.AddFile("test_err.marte", cfg2) + tree2.ResolveReferences() + + v2 := validator.NewValidator(tree2, ".") + v2.ValidateProject() + + found := false + for _, d := range v2.Diagnostics { + if strings.Contains(d.Message, "property 'NumberOfElements' mismatch") { + found = true + if !strings.Contains(d.Message, "defined '15'") { + t.Errorf("Expected defined '15', got message: %s", d.Message) + } + break + } + } + if !found { + t.Error("Expected property mismatch error for @N + 5") + } +} \ No newline at end of file diff --git a/test/let_macro_test.go b/test/let_macro_test.go new file mode 100644 index 0000000..2a8f149 --- /dev/null +++ b/test/let_macro_test.go @@ -0,0 +1,125 @@ +package integration + +import ( + "os" + "strings" + "testing" + + "github.com/marte-community/marte-dev-tools/internal/builder" + "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 TestLetMacroFull(t *testing.T) { + content := ` +//# My documentation +#let MyConst: uint32 = 10 + 20 ++Obj = { + Value = @MyConst +} +` + tmpFile, _ := os.CreateTemp("", "let_*.marte") + defer os.Remove(tmpFile.Name()) + os.WriteFile(tmpFile.Name(), []byte(content), 0644) + + // 1. Test Parsing & Indexing + p := parser.NewParser(content) + cfg, err := p.Parse() + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + tree := index.NewProjectTree() + tree.AddFile(tmpFile.Name(), cfg) + + vars := tree.Root.Variables + if iso, ok := tree.IsolatedFiles[tmpFile.Name()]; ok { + vars = iso.Variables + } + + info, ok := vars["MyConst"] + if !ok || !info.Def.IsConst { + t.Fatal("#let variable not indexed correctly as Const") + } + if info.Doc != "My documentation" { + t.Errorf("Expected doc 'My documentation', got '%s'", info.Doc) + } + + // 2. Test Builder Evaluation + out, _ := os.CreateTemp("", "let_out.cfg") + defer os.Remove(out.Name()) + + b := builder.NewBuilder([]string{tmpFile.Name()}, nil) + if err := b.Build(out); err != nil { + t.Fatalf("Build failed: %v", err) + } + + outContent, _ := os.ReadFile(out.Name()) + if !strings.Contains(string(outContent), "Value = 30") { + t.Errorf("Expected Value = 30 (evaluated @MyConst), got:\n%s", string(outContent)) + } + + // 3. Test Override Protection + out2, _ := os.CreateTemp("", "let_out2.cfg") + defer os.Remove(out2.Name()) + + b2 := builder.NewBuilder([]string{tmpFile.Name()}, map[string]string{"MyConst": "100"}) + if err := b2.Build(out2); err != nil { + t.Fatalf("Build failed: %v", err) + } + + outContent2, _ := os.ReadFile(out2.Name()) + if !strings.Contains(string(outContent2), "Value = 30") { + t.Errorf("Constant was overridden! Expected 30, got:\n%s", string(outContent2)) + } + + // 4. Test Validator (Mandatory Value) + contentErr := "#let BadConst: uint32" + p2 := parser.NewParser(contentErr) + cfg2, err2 := p2.Parse() + // Parser might fail if = is missing? + // parseLet expects =. + if err2 == nil { + // If parser didn't fail (maybe it was partial), validator should catch it + tree2 := index.NewProjectTree() + tree2.AddFile("err.marte", cfg2) + v := validator.NewValidator(tree2, ".") + v.ValidateProject() + + found := false + for _, d := range v.Diagnostics { + if strings.Contains(d.Message, "must have an initial value") { + found = true + break + } + } + if !found && cfg2 != nil { + // If p2.Parse() failed and added error to p2.errors, it's also fine. + // But check if it reached validator. + } + } + + // 5. Test Duplicate Detection + contentDup := ` +#let MyConst: uint32 = 10 +#var MyConst: uint32 = 20 +` + p3 := parser.NewParser(contentDup) + cfg3, _ := p3.Parse() + tree3 := index.NewProjectTree() + tree3.AddFile("dup.marte", cfg3) + v3 := validator.NewValidator(tree3, ".") + v3.ValidateProject() + + foundDup := false + for _, d := range v3.Diagnostics { + if strings.Contains(d.Message, "Duplicate variable definition") { + foundDup = true + break + } + } + if !foundDup { + t.Error("Expected duplicate variable definition error") + } +} diff --git a/test/lsp_recursive_index_test.go b/test/lsp_recursive_index_test.go new file mode 100644 index 0000000..77f9843 --- /dev/null +++ b/test/lsp_recursive_index_test.go @@ -0,0 +1,88 @@ +package integration + +import ( + "os" + "path/filepath" + "testing" + + "github.com/marte-community/marte-dev-tools/internal/index" + "github.com/marte-community/marte-dev-tools/internal/lsp" +) + +func TestLSPRecursiveIndexing(t *testing.T) { + // Setup directory structure + rootDir, err := os.MkdirTemp("", "lsp_recursive") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(rootDir) + + // root/main.marte + mainContent := ` +#package App ++Main = { + Ref = SubComp +} +` + if err := os.WriteFile(filepath.Join(rootDir, "main.marte"), []byte(mainContent), 0644); err != nil { + t.Fatal(err) + } + + // root/subdir/sub.marte + subDir := filepath.Join(rootDir, "subdir") + if err := os.Mkdir(subDir, 0755); err != nil { + t.Fatal(err) + } + subContent := ` +#package App ++SubComp = { Class = Component } +` + if err := os.WriteFile(filepath.Join(subDir, "sub.marte"), []byte(subContent), 0644); err != nil { + t.Fatal(err) + } + + // Initialize LSP + lsp.Tree = index.NewProjectTree() + lsp.Documents = make(map[string]string) + + // Simulate ScanDirectory + if err := lsp.Tree.ScanDirectory(rootDir); err != nil { + t.Fatalf("ScanDirectory failed: %v", err) + } + lsp.Tree.ResolveReferences() + + // Check if SubComp is in the tree + // Root -> App -> SubComp + appNode := lsp.Tree.Root.Children["App"] + if appNode == nil { + t.Fatal("App package not found") + } + + subComp := appNode.Children["SubComp"] + if subComp == nil { + t.Fatal("SubComp not found in tree (recursive scan failed)") + } + + mainURI := "file://" + filepath.Join(rootDir, "main.marte") + + // Definition Request + params := lsp.DefinitionParams{ + TextDocument: lsp.TextDocumentIdentifier{URI: mainURI}, + Position: lsp.Position{Line: 3, Character: 12}, + } + + res := lsp.HandleDefinition(params) + if res == nil { + t.Fatal("Definition not found for SubComp") + } + + locs, ok := res.([]lsp.Location) + if !ok || len(locs) == 0 { + t.Fatal("Expected location list") + } + + expectedFile := filepath.Join(subDir, "sub.marte") + if locs[0].URI != "file://"+expectedFile { + t.Errorf("Expected definition in %s, got %s", expectedFile, locs[0].URI) + } +} diff --git a/test/recursive_indexing_test.go b/test/recursive_indexing_test.go new file mode 100644 index 0000000..8b3baf4 --- /dev/null +++ b/test/recursive_indexing_test.go @@ -0,0 +1,54 @@ +package integration + +import ( + "os" + "path/filepath" + "testing" + + "github.com/marte-community/marte-dev-tools/internal/index" +) + +func TestRecursiveIndexing(t *testing.T) { + // Setup: root/level1/level2/deep.marte + rootDir, _ := os.MkdirTemp("", "rec_index") + defer os.RemoveAll(rootDir) + + l1 := filepath.Join(rootDir, "level1") + l2 := filepath.Join(l1, "level2") + if err := os.MkdirAll(l2, 0755); err != nil { + t.Fatal(err) + } + + content := "#package Deep\n+DeepObj = { Class = A }" + if err := os.WriteFile(filepath.Join(l2, "deep.marte"), []byte(content), 0644); err != nil { + t.Fatal(err) + } + + // Also add a file in root to ensure mixed levels work + os.WriteFile(filepath.Join(rootDir, "root.marte"), []byte("#package Root\n+RootObj = { Class = A }"), 0644) + + // Scan + tree := index.NewProjectTree() + err := tree.ScanDirectory(rootDir) + if err != nil { + t.Fatalf("Scan failed: %v", err) + } + + // Verify Deep + deepPkg := tree.Root.Children["Deep"] + if deepPkg == nil { + t.Fatal("Package Deep not found") + } + if deepPkg.Children["DeepObj"] == nil { + t.Fatal("DeepObj not found in Deep package") + } + + // Verify Root + rootPkg := tree.Root.Children["Root"] + if rootPkg == nil { + t.Fatal("Package Root not found") + } + if rootPkg.Children["RootObj"] == nil { + t.Fatal("RootObj not found in Root package") + } +}