diff --git a/cmd/mdt/main.go b/cmd/mdt/main.go index 92f4887..220fe4c 100644 --- a/cmd/mdt/main.go +++ b/cmd/mdt/main.go @@ -81,7 +81,7 @@ func runCheck(args []string) { } // idx.ResolveReferences() // Not implemented in new tree yet, but Validator uses Tree directly - v := validator.NewValidator(tree) + v := validator.NewValidator(tree, ".") v.ValidateProject() // Legacy loop removed as ValidateProject covers it via recursion diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 1707155..8c75524 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -137,6 +137,7 @@ type TextEdit struct { var tree = index.NewProjectTree() var documents = make(map[string]string) +var projectRoot string func RunServer() { reader := bufio.NewReader(os.Stdin) @@ -193,6 +194,7 @@ func handleMessage(msg *JsonRpcMessage) { } if root != "" { + projectRoot = root logger.Printf("Scanning workspace: %s\n", root) tree.ScanDirectory(root) tree.ResolveReferences() @@ -323,7 +325,7 @@ func handleFormatting(params DocumentFormattingParams) []TextEdit { } func runValidation(uri string) { - v := validator.NewValidator(tree) + v := validator.NewValidator(tree, projectRoot) v.ValidateProject() v.CheckUnused() diff --git a/internal/schema/marte.json b/internal/schema/marte.json index 1706cff..ff5a90e 100644 --- a/internal/schema/marte.json +++ b/internal/schema/marte.json @@ -114,11 +114,32 @@ "BaseLib2GAM": { "fields": [] }, "ConversionGAM": { "fields": [] }, "DoubleHandshakeGAM": { "fields": [] }, - "FilterGAM": { "fields": [] }, - "HistogramGAM": { "fields": [] }, + "FilterGAM": { + "fields": [ + {"name": "Num", "type": "array", "mandatory": true}, + {"name": "Den", "type": "array", "mandatory": true}, + {"name": "ResetInEachState", "type": "any", "mandatory": false}, + {"name": "InputSignals", "type": "node", "mandatory": false}, + {"name": "OutputSignals", "type": "node", "mandatory": false} + ] + }, + "HistogramGAM": { + "fields": [ + {"name": "BeginCycleNumber", "type": "int", "mandatory": false}, + {"name": "StateChangeResetName", "type": "string", "mandatory": false}, + {"name": "InputSignals", "type": "node", "mandatory": false}, + {"name": "OutputSignals", "type": "node", "mandatory": false} + ] + }, "Interleaved2FlatGAM": { "fields": [] }, "FlattenedStructIOGAM": { "fields": [] }, - "MathExpressionGAM": { "fields": [] }, + "MathExpressionGAM": { + "fields": [ + {"name": "Expression", "type": "string", "mandatory": true}, + {"name": "InputSignals", "type": "node", "mandatory": false}, + {"name": "OutputSignals", "type": "node", "mandatory": false} + ] + }, "MessageGAM": { "fields": [] }, "MuxGAM": { "fields": [] }, "SimulinkWrapperGAM": { "fields": [] }, @@ -128,10 +149,42 @@ "TriggeredIOGAM": { "fields": [] }, "WaveformGAM": { "fields": [] }, "DAN": { "fields": [] }, - "LinuxTimer": { "fields": [] }, + "LinuxTimer": { + "fields": [ + {"name": "ExecutionMode", "type": "string", "mandatory": false}, + {"name": "SleepNature", "type": "string", "mandatory": false}, + {"name": "SleepPercentage", "type": "any", "mandatory": false}, + {"name": "Phase", "type": "int", "mandatory": false}, + {"name": "CPUMask", "type": "int", "mandatory": false}, + {"name": "TimeProvider", "type": "node", "mandatory": false}, + {"name": "Signals", "type": "node", "mandatory": true} + ] + }, "LinkDataSource": { "fields": [] }, - "MDSReader": { "fields": [] }, - "MDSWriter": { "fields": [] }, + "MDSReader": { + "fields": [ + {"name": "TreeName", "type": "string", "mandatory": true}, + {"name": "ShotNumber", "type": "int", "mandatory": true}, + {"name": "Frequency", "type": "float", "mandatory": true}, + {"name": "Signals", "type": "node", "mandatory": true} + ] + }, + "MDSWriter": { + "fields": [ + {"name": "NumberOfBuffers", "type": "int", "mandatory": true}, + {"name": "CPUMask", "type": "int", "mandatory": true}, + {"name": "StackSize", "type": "int", "mandatory": true}, + {"name": "TreeName", "type": "string", "mandatory": true}, + {"name": "PulseNumber", "type": "int", "mandatory": false}, + {"name": "StoreOnTrigger", "type": "int", "mandatory": true}, + {"name": "EventName", "type": "string", "mandatory": true}, + {"name": "TimeRefresh", "type": "float", "mandatory": true}, + {"name": "NumberOfPreTriggers", "type": "int", "mandatory": false}, + {"name": "NumberOfPostTriggers", "type": "int", "mandatory": false}, + {"name": "Signals", "type": "node", "mandatory": true}, + {"name": "Messages", "type": "node", "mandatory": false} + ] + }, "NI1588TimeStamp": { "fields": [] }, "NI6259ADC": { "fields": [] }, "NI6259DAC": { "fields": [] }, @@ -153,4 +206,4 @@ "OPCUA": { "fields": [] }, "SysLogger": { "fields": [] } } -} \ No newline at end of file +} diff --git a/internal/schema/schema.go b/internal/schema/schema.go index 06ebeea..33bc7f9 100644 --- a/internal/schema/schema.go +++ b/internal/schema/schema.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "os" + "path/filepath" ) //go:embed marte.json @@ -45,11 +46,89 @@ func LoadSchema(path string) (*Schema, error) { return &s, nil } -// DefaultSchema returns a built-in schema with core MARTe classes +// DefaultSchema returns the built-in embedded schema func DefaultSchema() *Schema { var s Schema if err := json.Unmarshal(defaultSchemaJSON, &s); err != nil { panic(fmt.Sprintf("failed to parse default embedded schema: %v", err)) } + if s.Classes == nil { + s.Classes = make(map[string]ClassDefinition) + } return &s } + +// Merge adds rules from 'other' to 's'. +// Rules for the same class are merged (new fields added, existing fields updated). +func (s *Schema) Merge(other *Schema) { + if other == nil { + return + } + for className, classDef := range other.Classes { + if existingClass, ok := s.Classes[className]; ok { + // Merge fields + fieldMap := make(map[string]FieldDefinition) + for _, f := range classDef.Fields { + fieldMap[f.Name] = f + } + + var mergedFields []FieldDefinition + seen := make(map[string]bool) + + // Keep existing fields, update if present in other + for _, f := range existingClass.Fields { + if newF, ok := fieldMap[f.Name]; ok { + mergedFields = append(mergedFields, newF) + } else { + mergedFields = append(mergedFields, f) + } + seen[f.Name] = true + } + + // Append new fields + for _, f := range classDef.Fields { + if !seen[f.Name] { + mergedFields = append(mergedFields, f) + } + } + + existingClass.Fields = mergedFields + if classDef.Ordered { + existingClass.Ordered = true + } + s.Classes[className] = existingClass + } else { + s.Classes[className] = classDef + } + } +} + +func LoadFullSchema(projectRoot string) *Schema { + s := DefaultSchema() + + // 1. System Paths + sysPaths := []string{ + "/usr/share/mdt/marte_schema.json", + } + + home, err := os.UserHomeDir() + if err == nil { + sysPaths = append(sysPaths, filepath.Join(home, ".local/share/mdt/marte_schema.json")) + } + + for _, path := range sysPaths { + if sysSchema, err := LoadSchema(path); err == nil { + s.Merge(sysSchema) + } + } + + // 2. Project Path + if projectRoot != "" { + projectSchemaPath := filepath.Join(projectRoot, ".marte_schema.json") + if projSchema, err := LoadSchema(projectSchemaPath); err == nil { + s.Merge(projSchema) + } + } + + return s +} \ No newline at end of file diff --git a/internal/validator/validator.go b/internal/validator/validator.go index 51ae815..ec09e6f 100644 --- a/internal/validator/validator.go +++ b/internal/validator/validator.go @@ -27,10 +27,10 @@ type Validator struct { Schema *schema.Schema } -func NewValidator(tree *index.ProjectTree) *Validator { +func NewValidator(tree *index.ProjectTree, projectRoot string) *Validator { return &Validator{ Tree: tree, - Schema: schema.DefaultSchema(), + Schema: schema.LoadFullSchema(projectRoot), } } diff --git a/specification.md b/specification.md index 095165f..36e568f 100644 --- a/specification.md +++ b/specification.md @@ -153,6 +153,12 @@ The tool must build an index of the configuration to support LSP features and va - **Schema Definition**: - Class validation rules must be defined in a separate schema file. - **Project-Specific Classes**: Developers can define their own project-specific classes and corresponding validation rules, expanding the validation capabilities for their specific needs. + - **Schema Loading**: + - **Default Schema**: The tool should look for a default schema file `marte_schema.json` in standard system locations: + - `/usr/share/mdt/marte_schema.json` + - `$HOME/.local/share/mdt/marte_schema.json` + - **Project Schema**: If a file named `.marte_schema.json` exists in the project root, it must be loaded. + - **Merging**: The final schema is a merge of the built-in schema, the system default schema (if found), and the project-specific schema. Rules in later sources (Project > System > Built-in) append to or override earlier ones. - **Duplicate Fields**: - **Constraint**: A field must not be defined more than once within the same object/node scope, even if those definitions are spread across different files. - **Multi-File Consideration**: Validation must account for nodes being defined across multiple files (merged) when checking for duplicates. diff --git a/test/integration_test.go b/test/integration_test.go index 814e462..1394d12 100644 --- a/test/integration_test.go +++ b/test/integration_test.go @@ -30,7 +30,7 @@ func TestCheckCommand(t *testing.T) { idx := index.NewProjectTree() idx.AddFile(inputFile, config) - v := validator.NewValidator(idx) + v := validator.NewValidator(idx, ".") v.ValidateProject() v.CheckUnused() @@ -63,7 +63,7 @@ func TestCheckDuplicate(t *testing.T) { idx := index.NewProjectTree() idx.AddFile(inputFile, config) - v := validator.NewValidator(idx) + v := validator.NewValidator(idx, ".") v.ValidateProject() foundError := false @@ -95,7 +95,7 @@ func TestSignalNoClassValidation(t *testing.T) { idx := index.NewProjectTree() idx.AddFile(inputFile, config) - v := validator.NewValidator(idx) + v := validator.NewValidator(idx, ".") v.ValidateProject() if len(v.Diagnostics) > 0 { diff --git a/test/lsp_test.go b/test/lsp_test.go index ec128d0..2374fd1 100644 --- a/test/lsp_test.go +++ b/test/lsp_test.go @@ -31,7 +31,7 @@ func TestLSPDiagnostics(t *testing.T) { idx := index.NewProjectTree() idx.AddFile(inputFile, config) - v := validator.NewValidator(idx) + v := validator.NewValidator(idx, ".") v.ValidateProject() // Check for expected diagnostics diff --git a/test/validator_analyzed_test.go b/test/validator_analyzed_test.go new file mode 100644 index 0000000..0ee0f32 --- /dev/null +++ b/test/validator_analyzed_test.go @@ -0,0 +1,83 @@ +package integration + +import ( + "strings" + "testing" + + "github.com/marte-dev/marte-dev-tools/internal/index" + "github.com/marte-dev/marte-dev-tools/internal/parser" + "github.com/marte-dev/marte-dev-tools/internal/validator" +) + +func TestMDSWriterValidation(t *testing.T) { + // MDSWriter requires TreeName, NumberOfBuffers, etc. + content := ` ++MyMDSWriter = { + Class = MDSWriter + NumberOfBuffers = 10 + CPUMask = 1 + StackSize = 1000000 + // Missing TreeName + StoreOnTrigger = 0 + EventName = "Update" + TimeRefresh = 1.0 + +Signals = {} +} +` + p := parser.NewParser(content) + config, err := p.Parse() + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + idx := index.NewProjectTree() + idx.AddFile("mdswriter.marte", config) + + v := validator.NewValidator(idx, ".") + v.ValidateProject() + + found := false + for _, d := range v.Diagnostics { + if strings.Contains(d.Message, "Missing mandatory field 'TreeName'") { + found = true + break + } + } + + if !found { + t.Error("Expected error for missing 'TreeName' in MDSWriter") + } +} + +func TestMathExpressionGAMValidation(t *testing.T) { + // MathExpressionGAM requires Expression + content := ` ++MyMath = { + Class = MathExpressionGAM + // Missing Expression +} +` + p := parser.NewParser(content) + config, err := p.Parse() + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + idx := index.NewProjectTree() + idx.AddFile("math.marte", config) + + v := validator.NewValidator(idx, ".") + v.ValidateProject() + + found := false + for _, d := range v.Diagnostics { + if strings.Contains(d.Message, "Missing mandatory field 'Expression'") { + found = true + break + } + } + + if !found { + t.Error("Expected error for missing 'Expression' in MathExpressionGAM") + } +} diff --git a/test/validator_components_test.go b/test/validator_components_test.go index 879f5ec..dd33c29 100644 --- a/test/validator_components_test.go +++ b/test/validator_components_test.go @@ -28,7 +28,7 @@ func TestPIDGAMValidation(t *testing.T) { idx := index.NewProjectTree() idx.AddFile("pid.marte", config) - v := validator.NewValidator(idx) + v := validator.NewValidator(idx, ".") v.ValidateProject() foundKi := false @@ -68,7 +68,7 @@ func TestFileDataSourceValidation(t *testing.T) { idx := index.NewProjectTree() idx.AddFile("file.marte", config) - v := validator.NewValidator(idx) + v := validator.NewValidator(idx, ".") v.ValidateProject() found := false diff --git a/test/validator_db_test.go b/test/validator_db_test.go index 3083aa1..0e158cc 100644 --- a/test/validator_db_test.go +++ b/test/validator_db_test.go @@ -28,7 +28,7 @@ func TestRealTimeApplicationValidation(t *testing.T) { idx := index.NewProjectTree() idx.AddFile("app.marte", config) - v := validator.NewValidator(idx) + v := validator.NewValidator(idx, ".") v.ValidateProject() missingData := false @@ -68,7 +68,7 @@ func TestGAMSchedulerValidation(t *testing.T) { idx := index.NewProjectTree() idx.AddFile("scheduler.marte", config) - v := validator.NewValidator(idx) + v := validator.NewValidator(idx, ".") v.ValidateProject() found := false diff --git a/test/validator_extra_test.go b/test/validator_extra_test.go index 293d70d..968d23c 100644 --- a/test/validator_extra_test.go +++ b/test/validator_extra_test.go @@ -27,7 +27,7 @@ func TestSDNSubscriberValidation(t *testing.T) { idx := index.NewProjectTree() idx.AddFile("sdn.marte", config) - v := validator.NewValidator(idx) + v := validator.NewValidator(idx, ".") v.ValidateProject() found := false @@ -60,7 +60,7 @@ func TestFileWriterValidation(t *testing.T) { idx := index.NewProjectTree() idx.AddFile("writer.marte", config) - v := validator.NewValidator(idx) + v := validator.NewValidator(idx, ".") v.ValidateProject() found := false diff --git a/test/validator_multifile_test.go b/test/validator_multifile_test.go index 6c0969f..72a2f0f 100644 --- a/test/validator_multifile_test.go +++ b/test/validator_multifile_test.go @@ -37,7 +37,7 @@ func TestMultiFileNodeValidation(t *testing.T) { // However, the spec says "The build tool, validator, and LSP must merge these definitions". // Let's assume the Validator or Index does the merging logic. - v := validator.NewValidator(idx) + v := validator.NewValidator(idx, ".") v.ValidateProject() // +MyNode is split. @@ -57,7 +57,7 @@ func TestMultiFileDuplicateField(t *testing.T) { parseAndAddToIndex(t, idx, "integration/multifile_dup_1.marte") parseAndAddToIndex(t, idx, "integration/multifile_dup_2.marte") - v := validator.NewValidator(idx) + v := validator.NewValidator(idx, ".") v.ValidateProject() foundError := false @@ -81,7 +81,7 @@ func TestMultiFileReference(t *testing.T) { idx.ResolveReferences() // Check if the reference in +SourceNode to TargetNode is resolved. - v := validator.NewValidator(idx) + v := validator.NewValidator(idx, ".") v.ValidateProject() if len(v.Diagnostics) > 0 { @@ -94,7 +94,7 @@ func TestHierarchicalPackageMerge(t *testing.T) { parseAndAddToIndex(t, idx, "integration/hierarchical_pkg_1.marte") parseAndAddToIndex(t, idx, "integration/hierarchical_pkg_2.marte") - v := validator.NewValidator(idx) + v := validator.NewValidator(idx, ".") v.ValidateProject() // +MyObj should have Class (from file 1) and FieldX (from file 2). @@ -135,7 +135,7 @@ func TestHierarchicalDuplicate(t *testing.T) { parseAndAddToIndex(t, idx, "integration/hierarchical_dup_1.marte") parseAndAddToIndex(t, idx, "integration/hierarchical_dup_2.marte") - v := validator.NewValidator(idx) + v := validator.NewValidator(idx, ".") v.ValidateProject() foundError := false diff --git a/test/validator_project_schema_test.go b/test/validator_project_schema_test.go new file mode 100644 index 0000000..9466dc1 --- /dev/null +++ b/test/validator_project_schema_test.go @@ -0,0 +1,71 @@ +package integration + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/marte-dev/marte-dev-tools/internal/index" + "github.com/marte-dev/marte-dev-tools/internal/parser" + "github.com/marte-dev/marte-dev-tools/internal/validator" +) + +func TestProjectSpecificSchema(t *testing.T) { + // Create temp dir + tmpDir, err := os.MkdirTemp("", "mdt_test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + // Define project schema + schemaContent := ` +{ + "classes": { + "ProjectClass": { + "fields": [ + {"name": "CustomField", "type": "int", "mandatory": true} + ] + } + } +} +` + err = os.WriteFile(filepath.Join(tmpDir, ".marte_schema.json"), []byte(schemaContent), 0644) + if err != nil { + t.Fatal(err) + } + + // Define MARTe file using ProjectClass + marteContent := ` ++Obj = { + Class = ProjectClass + // Missing CustomField +} +` + // We parse the content in memory, but we need the validator to look in tmpDir + p := parser.NewParser(marteContent) + config, err := p.Parse() + if err != nil { + t.Fatal(err) + } + + idx := index.NewProjectTree() + idx.AddFile("project.marte", config) + + // Pass tmpDir as projectRoot + v := validator.NewValidator(idx, tmpDir) + v.ValidateProject() + + found := false + for _, d := range v.Diagnostics { + if strings.Contains(d.Message, "Missing mandatory field 'CustomField'") { + found = true + break + } + } + + if !found { + t.Error("Expected error for missing 'CustomField' defined in project schema") + } +} diff --git a/test/validator_schema_test.go b/test/validator_schema_test.go index cd40862..f7ab12e 100644 --- a/test/validator_schema_test.go +++ b/test/validator_schema_test.go @@ -26,7 +26,7 @@ func TestSchemaValidationMandatory(t *testing.T) { idx := index.NewProjectTree() idx.AddFile("test.marte", config) - v := validator.NewValidator(idx) + v := validator.NewValidator(idx, ".") v.ValidateProject() found := false @@ -60,7 +60,7 @@ func TestSchemaValidationType(t *testing.T) { idx := index.NewProjectTree() idx.AddFile("test.marte", config) - v := validator.NewValidator(idx) + v := validator.NewValidator(idx, ".") v.ValidateProject() found := false @@ -94,7 +94,7 @@ func TestSchemaValidationOrder(t *testing.T) { idx := index.NewProjectTree() idx.AddFile("test.marte", config) - v := validator.NewValidator(idx) + v := validator.NewValidator(idx, ".") v.ValidateProject() found := false @@ -127,7 +127,7 @@ func TestSchemaValidationMandatoryNode(t *testing.T) { idx := index.NewProjectTree() idx.AddFile("test.marte", config) - v := validator.NewValidator(idx) + v := validator.NewValidator(idx, ".") v.ValidateProject() for _, d := range v.Diagnostics { diff --git a/test/validator_unused_test.go b/test/validator_unused_test.go index ecfd4a3..e9c1972 100644 --- a/test/validator_unused_test.go +++ b/test/validator_unused_test.go @@ -41,7 +41,7 @@ $App = { idx.AddFile("test.marte", config) idx.ResolveReferences() - v := validator.NewValidator(idx) + v := validator.NewValidator(idx, ".") v.CheckUnused() foundUnused := false @@ -85,7 +85,7 @@ $App = { idx.AddFile("test.marte", config) idx.ResolveReferences() - v := validator.NewValidator(idx) + v := validator.NewValidator(idx, ".") v.CheckUnused() foundUnusedSig2 := false