Added project schema feature

This commit is contained in:
Martino Ferrari
2026-01-21 18:26:43 +01:00
parent 92dfa38294
commit 970b5697bd
16 changed files with 327 additions and 33 deletions

View File

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

View File

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

View File

@@ -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": [] },

View File

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

View File

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

View File

@@ -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.

View File

@@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 {

View File

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