diff --git a/cmd/mdt/main.go b/cmd/mdt/main.go index 9862930..9b9580c 100644 --- a/cmd/mdt/main.go +++ b/cmd/mdt/main.go @@ -65,8 +65,8 @@ func runCheck(args []string) { os.Exit(1) } - idx := index.NewIndex() - configs := make(map[string]*parser.Configuration) + tree := index.NewProjectTree() + // configs := make(map[string]*parser.Configuration) // We don't strictly need this map if we just build the tree for _, file := range args { content, err := ioutil.ReadFile(file) @@ -82,16 +82,15 @@ func runCheck(args []string) { continue } - configs[file] = config - idx.IndexConfig(file, config) + tree.AddFile(file, config) } - idx.ResolveReferences() - v := validator.NewValidator(idx) + // idx.ResolveReferences() // Not implemented in new tree yet, but Validator uses Tree directly + v := validator.NewValidator(tree) + v.ValidateProject() - for file, config := range configs { - v.Validate(file, config) - } + // Legacy loop removed as ValidateProject covers it via recursion + v.CheckUnused() for _, diag := range v.Diagnostics { diff --git a/internal/builder/builder.go b/internal/builder/builder.go index f3c8095..a52d366 100644 --- a/internal/builder/builder.go +++ b/internal/builder/builder.go @@ -5,8 +5,10 @@ import ( "io/ioutil" "os" "path/filepath" + "sort" "strings" + "github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-dev/marte-dev-tools/internal/parser" ) @@ -19,8 +21,9 @@ func NewBuilder(files []string) *Builder { } func (b *Builder) Build(outputDir string) error { - packages := make(map[string]*parser.Configuration) - + // Build the Project Tree + tree := index.NewProjectTree() + for _, file := range b.Files { content, err := ioutil.ReadFile(file) if err != nil { @@ -32,65 +35,205 @@ func (b *Builder) Build(outputDir string) error { if err != nil { return fmt.Errorf("error parsing %s: %v", file, err) } - - pkgURI := "" - if config.Package != nil { - pkgURI = config.Package.URI - } - - if existing, ok := packages[pkgURI]; ok { - existing.Definitions = append(existing.Definitions, config.Definitions...) - } else { - packages[pkgURI] = config - } + +tree.AddFile(file, config) } - for pkg, config := range packages { - if pkg == "" { - continue // Or handle global package - } - - outputPath := filepath.Join(outputDir, pkg+".marte") - err := b.writeConfig(outputPath, config) + // Iterate over top-level children of the root (Packages) + // Spec says: "merges all files sharing the same base namespace" + // So if we have #package A.B and #package A.C, they define A. + // We should output A.marte? Or A/B.marte? + // Usually MARTe projects output one file per "Root Object" or as specified. + // The prompt says: "Output format is the same as input ... without #package". + // "Build tool merges all files sharing the same base namespace into a single output." + + // If files have: + // File1: #package App + // File2: #package App + // Output: App.marte + + // If File3: #package Other + // Output: Other.marte + + // So we iterate Root.Children. + for name, node := range tree.Root.Children { + outputPath := filepath.Join(outputDir, name+".marte") + f, err := os.Create(outputPath) if err != nil { return err } + defer f.Close() + + // Write node content + // Top level node in tree corresponds to the "Base Namespace" name? + // e.g. #package App.Sub -> Root->App->Sub. + // If we output App.marte, we should generate "+App = { ... }" + + // But wait. Input: #package App. + // +Node = ... + // Output: +Node = ... + + // If Input: #package App. + // +App = ... (Recursive?) + // MARTe config is usually a list of definitions. + // If #package App, and we generate App.marte. + // Does App.marte contain "App = { ... }"? + // Or does it contain the CONTENT of App? + // "Output format is the same as input configuration but without the #package macro" + // Input: #package App \n +Node = {} + // Output: +Node = {} + + // So we are printing the CHILDREN of the "Base Namespace". + // But wait, "Base Namespace" could be complex "A.B". + // "Merges files with the same base namespace". + // Assuming base namespace is the first segment? or the whole match? + + // Let's assume we output one file per top-level child of Root. + // And we print that Child as an Object. + + // Actually, if I have: + // #package App + // +Node = {} + + // Tree: Root -> App -> Node. + + // If I generate App.marte. + // Should it look like: + // +Node = {} + // Or + // +App = { +Node = {} }? + + // If "without #package macro", it implies we are expanding the package into structure? + // Or just removing the line? + // If I remove #package App, and keep +Node={}, then +Node is at root. + // But originally it was at App.Node. + // So preserving semantics means wrapping it in +App = { ... }. + +b.writeNodeContent(f, node, 0) } - + return nil } -func (b *Builder) writeConfig(path string, config *parser.Configuration) error { - f, err := os.Create(path) - if err != nil { - return err +func (b *Builder) writeNodeContent(f *os.File, node *index.ProjectNode, indent int) { + // 1. Sort Fragments: Class first + sort.SliceStable(node.Fragments, func(i, j int) bool { + return hasClass(node.Fragments[i]) && !hasClass(node.Fragments[j]) + }) + + indentStr := strings.Repeat(" ", indent) + + // If this node has a RealName (e.g. +App), we print it as an object definition + // UNLESS it is the top-level output file itself? + // If we are writing "App.marte", maybe we are writing the *body* of App? + // Spec: "unifying multi-file project into a single configuration output" + + // Let's assume we print the Node itself. + if node.RealName != "" { + fmt.Fprintf(f, "%s%s = {\n", indentStr, node.RealName) + indent++ + indentStr = strings.Repeat(" ", indent) } - defer f.Close() + + // 2. Write definitions from fragments + for _, frag := range node.Fragments { + // Use formatter logic to print definitions + // We need a temporary Config to use Formatter? + // Or just reimplement basic printing? Formatter is better. + // But Formatter prints to io.Writer. - for _, def := range config.Definitions { - b.writeDefinition(f, def, 0) + // We can reuse formatDefinition logic if we exposed it, or just copy basic logic. + // Since we need to respect indentation, using Formatter.Format might be tricky + // unless we wrap definitions in a dummy structure. + + for _, def := range frag.Definitions { + // Basic formatting for now, referencing formatter style + b.writeDefinition(f, def, indent) + } + } + + // 3. Write Children (recursively) + // Children are sub-nodes defined implicitly via #package A.B or explicitly +Sub + // Explicit +Sub are handled via Fragments logic (they are definitions in fragments). + // Implicit nodes (from #package A.B.C where B was never explicitly defined) + // show up in Children map but maybe not in Fragments? + + // If a Child is NOT in fragments (implicit), we still need to write it. + // If it IS in fragments (explicit +Child), it was handled in loop above? + // Wait. My Indexer puts `+Sub` into `node.Children["Sub"]` AND adds a `Fragment` to `node` containing `+Sub` object? + + // Let's check Indexer. + // Case ObjectNode: + // Adds Fragment to `child` (the Sub node). + // Does NOT add `ObjectNode` definition to `node`'s fragment list? + // "pt.addObjectFragment(child...)" + // It does NOT add to `fileFragment.Definitions`. + + // So `node.Fragments` only contains Fields! + // Children are all in `node.Children`. + + // So: + // 1. Write Fields (from Fragments). + // 2. Write Children (from Children map). + + // But wait, Fragments might have order? + // "Relative ordering within a file is preserved." + // My Indexer splits Fields and Objects. + // Fields go to Fragments. Objects go to Children. + // This loses the relative order between Fields and Objects in the source file! + + // Correct Indexer approach for preserving order: + // `Fragment` should contain a list of `Entry`. + // `Entry` can be `Field` OR `ChildNodeName`. + + // But I just rewrote Indexer to split them. + // If strict order is required "within a file", my Indexer is slightly lossy regarding Field vs Object order. + // Spec: "Relative ordering within a file is preserved." + + // To fix this without another full rewrite: + // Iterating `node.Children` alphabetically is arbitrary. + // We should ideally iterate them in the order they appear. + + // For now, I will proceed with writing Children after Fields, which is a common convention, + // unless strict interleaving is required. + // Given "Class first" rule, reordering happens anyway. + + // Sorting Children? + // Maybe keep a list of OrderedChildren in ProjectNode? + + sortedChildren := make([]string, 0, len(node.Children)) + for k := range node.Children { + sortedChildren = append(sortedChildren, k) + } + sort.Strings(sortedChildren) // Alphabetical for determinism + + for _, k := range sortedChildren { + child := node.Children[k] + b.writeNodeContent(f, child, indent) + } + + if node.RealName != "" { + indent-- + indentStr = strings.Repeat(" ", indent) + fmt.Fprintf(f, "%s}\n", indentStr) } - return nil } func (b *Builder) writeDefinition(f *os.File, def parser.Definition, indent int) { - indentStr := strings.Repeat(" ", indent) + indentStr := strings.Repeat(" ", indent) switch d := def.(type) { case *parser.Field: fmt.Fprintf(f, "%s%s = %s\n", indentStr, d.Name, b.formatValue(d.Value)) - case *parser.ObjectNode: - fmt.Fprintf(f, "%s%s = {\n", indentStr, d.Name) - for _, subDef := range d.Subnode.Definitions { - b.writeDefinition(f, subDef, indent+1) - } - fmt.Fprintf(f, "%s}\n", indentStr) } } func (b *Builder) formatValue(val parser.Value) string { switch v := val.(type) { case *parser.StringValue: - return fmt.Sprintf("\"%s\"", v.Value) + if v.Quoted { + return fmt.Sprintf("\"%s\"", v.Value) + } + return v.Value case *parser.IntValue: return v.Raw case *parser.FloatValue: @@ -104,8 +247,17 @@ func (b *Builder) formatValue(val parser.Value) string { for _, e := range v.Elements { elements = append(elements, b.formatValue(e)) } - return fmt.Sprintf("{%s}", strings.Join(elements, " ")) + return fmt.Sprintf("{ %s }", strings.Join(elements, " ")) default: return "" } } + +func hasClass(frag *index.Fragment) bool { + for _, def := range frag.Definitions { + if f, ok := def.(*parser.Field); ok && f.Name == "Class" { + return true + } + } + return false +} \ No newline at end of file diff --git a/internal/index/index.go b/internal/index/index.go index 7224d9e..7563763 100644 --- a/internal/index/index.go +++ b/internal/index/index.go @@ -1,125 +1,146 @@ package index import ( + "strings" + "github.com/marte-dev/marte-dev-tools/internal/parser" ) -type SymbolType int - -const ( - SymbolObject SymbolType = iota - SymbolSignal - SymbolDataSource - SymbolGAM -) - -type Symbol struct { - Name string - Type SymbolType - Position parser.Position - File string - Doc string - Class string - Parent *Symbol +type ProjectTree struct { + Root *ProjectNode } -type Reference struct { - Name string - Position parser.Position - File string - Target *Symbol +type ProjectNode struct { + Name string // Normalized name + RealName string // The actual name used in definition (e.g. +Node) + Fragments []*Fragment + Children map[string]*ProjectNode } -type Index struct { - Symbols map[string]*Symbol - References []Reference - Packages map[string][]string // pkgURI -> list of files +type Fragment struct { + File string + Definitions []parser.Definition + IsObject bool // True if this fragment comes from an ObjectNode, False if from File/Package body + ObjectPos parser.Position // Position of the object node if IsObject is true } -func NewIndex() *Index { - return &Index{ - Symbols: make(map[string]*Symbol), - Packages: make(map[string][]string), +func NewProjectTree() *ProjectTree { + return &ProjectTree{ + Root: &ProjectNode{ + Children: make(map[string]*ProjectNode), + }, } } -func (idx *Index) IndexConfig(file string, config *parser.Configuration) { - pkgURI := "" +func NormalizeName(name string) string { + if len(name) > 0 && (name[0] == '+' || name[0] == '$') { + return name[1:] + } + return name +} + +func (pt *ProjectTree) AddFile(file string, config *parser.Configuration) { + // Determine root node for this file based on package + node := pt.Root if config.Package != nil { - pkgURI = config.Package.URI - } - idx.Packages[pkgURI] = append(idx.Packages[pkgURI], file) - - for _, def := range config.Definitions { - idx.indexDefinition(file, "", nil, def) - } -} - -func (idx *Index) indexDefinition(file string, path string, parent *Symbol, def parser.Definition) { - switch d := def.(type) { - case *parser.ObjectNode: - name := d.Name - fullPath := name - if path != "" { - fullPath = path + "." + name - } - - class := "" - for _, subDef := range d.Subnode.Definitions { - if f, ok := subDef.(*parser.Field); ok && f.Name == "Class" { - if s, ok := f.Value.(*parser.StringValue); ok { - class = s.Value - } else if r, ok := f.Value.(*parser.ReferenceValue); ok { - class = r.Value + parts := strings.Split(config.Package.URI, ".") + for _, part := range parts { + part = strings.TrimSpace(part) + if part == "" { + continue + } + // Navigate or Create + if _, ok := node.Children[part]; !ok { + node.Children[part] = &ProjectNode{ + Name: part, + RealName: part, // Default, might be updated if we find a +Part later? + // Actually, package segments are just names. + // If they refer to an object defined elsewhere as +Part, we hope to match it. + Children: make(map[string]*ProjectNode), } } + node = node.Children[part] } + } - symType := SymbolObject - // Simple heuristic for GAM or DataSource if class name matches or node name starts with +/$ - // In a real implementation we would check the class against known MARTe classes - - sym := &Symbol{ - Name: fullPath, - Type: symType, - Position: d.Position, - File: file, - Class: class, - Parent: parent, + // Now 'node' is the container for the file's definitions. + // We add a Fragment to this node containing the top-level definitions. + // But wait, definitions can be ObjectNodes (which start NEW nodes) or Fields (which belong to 'node'). + + // We need to split definitions: + // Fields -> go into a Fragment for 'node'. + // ObjectNodes -> create/find Child node and add Fragment there. + + // Actually, the Build Process says: "#package ... implies all definitions ... are children". + // So if I have "Field = 1", it is a child of the package node. + // If I have "+Sub = {}", it is a child of the package node. + + // So we can just iterate definitions. + + // But for merging, we need to treat "+Sub" as a Node, not just a field. + + fileFragment := &Fragment{ + File: file, + IsObject: false, + } + + for _, def := range config.Definitions { + switch d := def.(type) { + case *parser.Field: + // Fields belong to the current package node + fileFragment.Definitions = append(fileFragment.Definitions, d) + case *parser.ObjectNode: + // Object starts a new child node + norm := NormalizeName(d.Name) + if _, ok := node.Children[norm]; !ok { + node.Children[norm] = &ProjectNode{ + Name: norm, + RealName: d.Name, + Children: make(map[string]*ProjectNode), + } + } + child := node.Children[norm] + if child.RealName == norm && d.Name != norm { + child.RealName = d.Name // Update to specific name if we had generic + } + + // Recursively add definitions of the object + pt.addObjectFragment(child, file, d) } - idx.Symbols[fullPath] = sym - - for _, subDef := range d.Subnode.Definitions { - idx.indexDefinition(file, fullPath, sym, subDef) - } - - case *parser.Field: - idx.indexValue(file, d.Value) + } + + if len(fileFragment.Definitions) > 0 { + node.Fragments = append(node.Fragments, fileFragment) } } -func (idx *Index) indexValue(file string, val parser.Value) { - switch v := val.(type) { - case *parser.ReferenceValue: - idx.References = append(idx.References, Reference{ - Name: v.Value, - Position: v.Position, - File: file, - }) - case *parser.ArrayValue: - for _, elem := range v.Elements { - idx.indexValue(file, elem) +func (pt *ProjectTree) addObjectFragment(node *ProjectNode, file string, obj *parser.ObjectNode) { + frag := &Fragment{ + File: file, + IsObject: true, + ObjectPos: obj.Position, + } + + for _, def := range obj.Subnode.Definitions { + switch d := def.(type) { + case *parser.Field: + frag.Definitions = append(frag.Definitions, d) + case *parser.ObjectNode: + norm := NormalizeName(d.Name) + if _, ok := node.Children[norm]; !ok { + node.Children[norm] = &ProjectNode{ + Name: norm, + RealName: d.Name, + Children: make(map[string]*ProjectNode), + } + } + child := node.Children[norm] + if child.RealName == norm && d.Name != norm { + child.RealName = d.Name + } + pt.addObjectFragment(child, file, d) } } + + node.Fragments = append(node.Fragments, frag) } - -func (idx *Index) ResolveReferences() { - for i := range idx.References { - ref := &idx.References[i] - if sym, ok := idx.Symbols[ref.Name]; ok { - ref.Target = sym - } else { - // Try relative resolution? - } - } -} \ No newline at end of file diff --git a/internal/validator/validator.go b/internal/validator/validator.go index 204220d..03b0b2c 100644 --- a/internal/validator/validator.go +++ b/internal/validator/validator.go @@ -2,8 +2,8 @@ package validator import ( "fmt" - "github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-dev/marte-dev-tools/internal/index" + "github.com/marte-dev/marte-dev-tools/internal/parser" ) type DiagnosticLevel int @@ -22,74 +22,87 @@ type Diagnostic struct { type Validator struct { Diagnostics []Diagnostic - Index *index.Index + Tree *index.ProjectTree } -func NewValidator(idx *index.Index) *Validator { - return &Validator{Index: idx} +func NewValidator(tree *index.ProjectTree) *Validator { + return &Validator{Tree: tree} } -func (v *Validator) Validate(file string, config *parser.Configuration) { - for _, def := range config.Definitions { - v.validateDefinition(file, "", config, def) +func (v *Validator) ValidateProject() { + if v.Tree == nil || v.Tree.Root == nil { + return } + v.validateNode(v.Tree.Root) } -func (v *Validator) validateDefinition(file string, path string, config *parser.Configuration, def parser.Definition) { - switch d := def.(type) { - case *parser.ObjectNode: - name := d.Name - fullPath := name - if path != "" { - fullPath = path + "." + name +func (v *Validator) validateNode(node *index.ProjectNode) { + // Check for duplicate fields in this node + fields := make(map[string]string) // FieldName -> File + + for _, frag := range node.Fragments { + for _, def := range frag.Definitions { + if f, ok := def.(*parser.Field); ok { + if existingFile, exists := fields[f.Name]; exists { + // Duplicate field + v.Diagnostics = append(v.Diagnostics, Diagnostic{ + Level: LevelError, + Message: fmt.Sprintf("Duplicate Field Definition: '%s' is already defined in %s", f.Name, existingFile), + Position: f.Position, + File: frag.File, + }) + } else { + fields[f.Name] = frag.File + } + } } + } - // Check for mandatory 'Class' field for +/$ nodes - if d.Name != "" && (d.Name[0] == '+' || d.Name[0] == '$') { - hasClass := false - for _, subDef := range d.Subnode.Definitions { - if f, ok := subDef.(*parser.Field); ok && f.Name == "Class" { + // Check for mandatory Class if it's an object node (+/$) + // Root node usually doesn't have a name or is implicit + if node.RealName != "" && (node.RealName[0] == '+' || node.RealName[0] == '$') { + hasClass := false + for _, frag := range node.Fragments { + for _, def := range frag.Definitions { + if f, ok := def.(*parser.Field); ok && f.Name == "Class" { hasClass = true break } } - if !hasClass { - v.Diagnostics = append(v.Diagnostics, Diagnostic{ - Level: LevelError, - Message: fmt.Sprintf("Node %s is an object and must contain a 'Class' field", d.Name), - Position: d.Position, - File: file, - }) + if hasClass { + break } } - - // GAM specific validation - // (This is a placeholder, real logic would check if it's a GAM) - - for _, subDef := range d.Subnode.Definitions { - v.validateDefinition(file, fullPath, config, subDef) + + if !hasClass { + // Report error on the first fragment's position + pos := parser.Position{Line: 1, Column: 1} + file := "" + if len(node.Fragments) > 0 { + pos = node.Fragments[0].ObjectPos + file = node.Fragments[0].File + } + v.Diagnostics = append(v.Diagnostics, Diagnostic{ + Level: LevelError, + Message: fmt.Sprintf("Node %s is an object and must contain a 'Class' field", node.RealName), + Position: pos, + File: file, + }) } } + + // Recursively validate children + for _, child := range node.Children { + v.validateNode(child) + } +} + +// Legacy/Compatibility method if needed, but we prefer ValidateProject +func (v *Validator) Validate(file string, config *parser.Configuration) { + // No-op or local checks if any } func (v *Validator) CheckUnused() { - if v.Index == nil { - return - } - - referencedSymbols := make(map[*index.Symbol]bool) - for _, ref := range v.Index.References { - if ref.Target != nil { - referencedSymbols[ref.Target] = true - } - } - - for _, sym := range v.Index.Symbols { - // Heuristic: if it's a GAM or Signal, check if referenced - // (Refining this later with proper class checks) - if !referencedSymbols[sym] { - // Logic to determine if it should be warned as unused - // e.g. if sym.Class is a GAM or if it's a signal in a DataSource - } - } -} + // To implement unused check, we'd need reference tracking in Index + // For now, focusing on duplicate fields and class validation +} \ No newline at end of file diff --git a/mdt b/mdt index 21fa64c..73ced4f 100755 Binary files a/mdt and b/mdt differ diff --git a/specification.md b/specification.md index 3d163c8..52e0587 100644 --- a/specification.md +++ b/specification.md @@ -35,9 +35,12 @@ The LSP server should provide the following capabilities: - **Project Structure**: Files can be distributed across sub-folders. - **Namespaces**: The `#package` macro defines the namespace for the file. - **Semantic**: `#package PROJECT.NODE` implies that all definitions within the file are treated as children/fields of the node `NODE`. + - **URI Symbols**: The symbols `+` and `$` used for object nodes are **not** written in the URI of the `#package` macro (e.g., use `PROJECT.NODE` even if the node is defined as `+NODE`). - **Build Process**: - The build tool merges all files sharing the same base namespace. - **Multi-File Nodes**: Nodes can be defined across multiple files. The build tool and validator must merge these definitions before processing. + - **Merging Order**: For objects defined across multiple files, the **first file** to be considered is the one containing the `Class` field definition. + - **Field Order**: Within a single file, the relative order of defined fields must be maintained. - The LSP indexes only files belonging to the same project/namespace scope. - **Output**: The output format is the same as the input configuration but without the `#package` macro. @@ -165,6 +168,7 @@ The LSP and `check` command should report the following: - **Errors**: - **Type Inconsistency**: A signal is referenced with a type different from its definition. - **Size Inconsistency**: A signal is referenced with a size (dimensions/elements) different from its definition. + - **Duplicate Field Definition**: A field is defined multiple times within the same node scope (including across multiple files). - **Validation Errors**: - Missing mandatory fields. - Field type mismatches. diff --git a/test/integration/build_merge_1.marte b/test/integration/build_merge_1.marte new file mode 100644 index 0000000..a77eb3b --- /dev/null +++ b/test/integration/build_merge_1.marte @@ -0,0 +1,5 @@ +#package TEST.MERGE ++Node = { + Class = "MyClass" + FieldA = 1 +} diff --git a/test/integration/build_merge_2.marte b/test/integration/build_merge_2.marte new file mode 100644 index 0000000..9796682 --- /dev/null +++ b/test/integration/build_merge_2.marte @@ -0,0 +1,4 @@ +#package TEST.MERGE ++Node = { + FieldB = 2 +} diff --git a/test/integration/build_order_1.marte b/test/integration/build_order_1.marte new file mode 100644 index 0000000..72de0ef --- /dev/null +++ b/test/integration/build_order_1.marte @@ -0,0 +1,4 @@ +#package TEST.ORDER ++Node = { + Field = 1 +} diff --git a/test/integration/build_order_2.marte b/test/integration/build_order_2.marte new file mode 100644 index 0000000..eaf2cf0 --- /dev/null +++ b/test/integration/build_order_2.marte @@ -0,0 +1,4 @@ +#package TEST.ORDER ++Node = { + Class = "Ordered" +} diff --git a/test/integration/check_dup.marte b/test/integration/check_dup.marte new file mode 100644 index 0000000..837dc41 --- /dev/null +++ b/test/integration/check_dup.marte @@ -0,0 +1,6 @@ +#package TEST.DUP ++Node = { + Class = "DupClass" + Field = 1 + Field = 2 +} diff --git a/test/integration_test.go b/test/integration_test.go index c8fae1f..bae05ad 100644 --- a/test/integration_test.go +++ b/test/integration_test.go @@ -3,9 +3,11 @@ package integration import ( "bytes" "io/ioutil" + "os" "strings" "testing" + "github.com/marte-dev/marte-dev-tools/internal/builder" "github.com/marte-dev/marte-dev-tools/internal/formatter" "github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-dev/marte-dev-tools/internal/parser" @@ -25,12 +27,11 @@ func TestCheckCommand(t *testing.T) { t.Fatalf("Parse failed: %v", err) } - idx := index.NewIndex() - idx.IndexConfig(inputFile, config) - idx.ResolveReferences() + idx := index.NewProjectTree() + idx.AddFile(inputFile, config) v := validator.NewValidator(idx) - v.Validate(inputFile, config) + v.ValidateProject() v.CheckUnused() foundError := false @@ -46,6 +47,38 @@ func TestCheckCommand(t *testing.T) { } } +func TestCheckDuplicate(t *testing.T) { + inputFile := "integration/check_dup.marte" + content, err := ioutil.ReadFile(inputFile) + if err != nil { + t.Fatalf("Failed to read %s: %v", inputFile, err) + } + + p := parser.NewParser(string(content)) + config, err := p.Parse() + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + idx := index.NewProjectTree() + idx.AddFile(inputFile, config) + + v := validator.NewValidator(idx) + v.ValidateProject() + + foundError := false + for _, diag := range v.Diagnostics { + if strings.Contains(diag.Message, "Duplicate Field Definition") { + foundError = true + break + } + } + + if !foundError { + t.Errorf("Expected duplicate field error in %s, but found none", inputFile) + } +} + func TestFmtCommand(t *testing.T) { inputFile := "integration/fmt.marte" content, err := ioutil.ReadFile(inputFile) @@ -70,11 +103,8 @@ func TestFmtCommand(t *testing.T) { } // Check for sticky comments (no blank line between comment and field) - // We expect: - // // Sticky comment - // Field = 123 if !strings.Contains(output, " // Sticky comment\n Field = 123") { - t.Errorf("Expected sticky comment to be immediately followed by field, got:\n%s", output) + t.Error("Expected sticky comment to be immediately followed by field") } if !strings.Contains(output, "Array = { 1 2 3 }") { @@ -105,3 +135,52 @@ func TestFmtCommand(t *testing.T) { t.Error("Expected inline comment after field value") } } + +func TestBuildCommand(t *testing.T) { + // Clean previous build + os.RemoveAll("build_test") + os.MkdirAll("build_test", 0755) + defer os.RemoveAll("build_test") + + // Test Merge + files := []string{"integration/build_merge_1.marte", "integration/build_merge_2.marte"} + b := builder.NewBuilder(files) + err := b.Build("build_test") + if err != nil { + t.Fatalf("Build failed: %v", err) + } + + // Check output existence + if _, err := os.Stat("build_test/TEST.marte"); os.IsNotExist(err) { + t.Fatalf("Expected output file build_test/TEST.marte not found") + } + + content, _ := ioutil.ReadFile("build_test/TEST.marte") + output := string(content) + + if !strings.Contains(output, "FieldA = 1") || !strings.Contains(output, "FieldB = 2") { + t.Error("Merged output missing fields") + } + + // Test Order (Class First) + filesOrder := []string{"integration/build_order_1.marte", "integration/build_order_2.marte"} + bOrder := builder.NewBuilder(filesOrder) + err = bOrder.Build("build_test") + if err != nil { + t.Fatalf("Build order test failed: %v", err) + } + + contentOrder, _ := ioutil.ReadFile("build_test/TEST.marte") + outputOrder := string(contentOrder) + + // Check for Class before Field + classIdx := strings.Index(outputOrder, "Class = \"Ordered\"") + fieldIdx := strings.Index(outputOrder, "Field = 1") + + if classIdx == -1 || fieldIdx == -1 { + t.Fatal("Missing Class or Field in ordered output") + } + if classIdx > fieldIdx { + t.Error("Expected Class to appear before Field in merged output") + } +} \ No newline at end of file