Implementing pragmas

This commit is contained in:
Martino Ferrari
2026-01-22 02:51:36 +01:00
parent 8fe319de2d
commit b2e963fc04
8 changed files with 287 additions and 14 deletions

View File

@@ -748,10 +748,11 @@ $TbTestApp = {
DataSource = Timer DataSource = Timer
Type = uint32 Type = uint32
} }
//!cast(uint32, uint64): because...
Time = { Time = {
Frequency = 100 Frequency = 100
DataSource = Timer DataSource = Timer
Type = uint32 Type = uint64
} }
AbsoluteTime = { AbsoluteTime = {
DataSource = Timer DataSource = Timer
@@ -759,6 +760,7 @@ $TbTestApp = {
} }
} }
OutputSignals = { OutputSignals = {
//!implicit: defined because....
Counter_DDB1 = { Counter_DDB1 = {
DataSource = DDB1 DataSource = DDB1
Type = uint32 Type = uint32

View File

@@ -13,6 +13,7 @@ type ProjectTree struct {
Root *ProjectNode Root *ProjectNode
References []Reference References []Reference
IsolatedFiles map[string]*ProjectNode IsolatedFiles map[string]*ProjectNode
GlobalPragmas map[string][]string
} }
func (pt *ProjectTree) ScanDirectory(rootPath string) error { func (pt *ProjectTree) ScanDirectory(rootPath string) error {
@@ -59,6 +60,7 @@ type Fragment struct {
Definitions []parser.Definition Definitions []parser.Definition
IsObject bool IsObject bool
ObjectPos parser.Position ObjectPos parser.Position
EndPos parser.Position
Doc string // Documentation for this fragment (if object) Doc string // Documentation for this fragment (if object)
} }
@@ -69,6 +71,7 @@ func NewProjectTree() *ProjectTree {
Metadata: make(map[string]string), Metadata: make(map[string]string),
}, },
IsolatedFiles: make(map[string]*ProjectNode), IsolatedFiles: make(map[string]*ProjectNode),
GlobalPragmas: make(map[string][]string),
} }
} }
@@ -89,6 +92,7 @@ func (pt *ProjectTree) RemoveFile(file string) {
pt.References = newRefs pt.References = newRefs
delete(pt.IsolatedFiles, file) delete(pt.IsolatedFiles, file)
delete(pt.GlobalPragmas, file)
pt.removeFileFromNode(pt.Root, file) pt.removeFileFromNode(pt.Root, file)
} }
@@ -156,6 +160,14 @@ func (pt *ProjectTree) extractFieldMetadata(node *ProjectNode, f *parser.Field)
func (pt *ProjectTree) AddFile(file string, config *parser.Configuration) { func (pt *ProjectTree) AddFile(file string, config *parser.Configuration) {
pt.RemoveFile(file) pt.RemoveFile(file)
// Collect global pragmas
for _, p := range config.Pragmas {
txt := strings.TrimSpace(p.Text)
if strings.HasPrefix(txt, "//!allow(") {
pt.GlobalPragmas[file] = append(pt.GlobalPragmas[file], txt)
}
}
if config.Package == nil { if config.Package == nil {
node := &ProjectNode{ node := &ProjectNode{
Children: make(map[string]*ProjectNode), Children: make(map[string]*ProjectNode),
@@ -249,6 +261,7 @@ func (pt *ProjectTree) addObjectFragment(node *ProjectNode, file string, obj *pa
File: file, File: file,
IsObject: true, IsObject: true,
ObjectPos: obj.Position, ObjectPos: obj.Position,
EndPos: obj.Subnode.EndPosition,
Doc: doc, Doc: doc,
} }
@@ -462,3 +475,44 @@ func (pt *ProjectTree) queryNode(node *ProjectNode, file string, line, col int)
} }
return nil return nil
} }
func (pt *ProjectTree) GetNodeContaining(file string, pos parser.Position) *ProjectNode {
if isoNode, ok := pt.IsolatedFiles[file]; ok {
if found := pt.findNodeContaining(isoNode, file, pos); found != nil {
return found
}
return isoNode
}
if pt.Root != nil {
if found := pt.findNodeContaining(pt.Root, file, pos); found != nil {
return found
}
for _, frag := range pt.Root.Fragments {
if frag.File == file && !frag.IsObject {
return pt.Root
}
}
}
return nil
}
func (pt *ProjectTree) findNodeContaining(node *ProjectNode, file string, pos parser.Position) *ProjectNode {
for _, child := range node.Children {
if res := pt.findNodeContaining(child, file, pos); res != nil {
return res
}
}
for _, frag := range node.Fragments {
if frag.File == file && frag.IsObject {
start := frag.ObjectPos
end := frag.EndPos
if (pos.Line > start.Line || (pos.Line == start.Line && pos.Column >= start.Column)) &&
(pos.Line < end.Line || (pos.Line == end.Line && pos.Column <= end.Column)) {
return node
}
}
}
return nil
}

View File

@@ -581,8 +581,8 @@ func formatNodeInfo(node *index.ProjectNode) string {
} }
// Size // Size
dims := node.Metadata["NumberOfDimensions"] dims := node.Metadata["NumberOfDimensions"]
elems := node.Metadata["NumberOfElements"] elems := node.Metadata["NumberOfElements"]
if dims != "" || elems != "" { if dims != "" || elems != "" {
sigInfo += fmt.Sprintf("**Size**: `[%s]`, `%s` dims ", elems, dims) sigInfo += fmt.Sprintf("**Size**: `[%s]`, `%s` dims ", elems, dims)
} }
@@ -592,6 +592,57 @@ elems := node.Metadata["NumberOfElements"]
if node.Doc != "" { if node.Doc != "" {
info += fmt.Sprintf("\n\n%s", node.Doc) info += fmt.Sprintf("\n\n%s", node.Doc)
} }
// Find references
var refs []string
for _, ref := range tree.References {
if ref.Target == node {
container := tree.GetNodeContaining(ref.File, ref.Position)
if container != nil {
threadName := ""
stateName := ""
curr := container
for curr != nil {
if cls, ok := curr.Metadata["Class"]; ok {
if cls == "RealTimeThread" {
threadName = curr.RealName
}
if cls == "RealTimeState" {
stateName = curr.RealName
}
}
curr = curr.Parent
}
if threadName != "" || stateName != "" {
refStr := ""
if stateName != "" {
refStr += fmt.Sprintf("State: `%s`", stateName)
}
if threadName != "" {
if refStr != "" {
refStr += ", "
}
refStr += fmt.Sprintf("Thread: `%s`", threadName)
}
refs = append(refs, refStr)
}
}
}
}
if len(refs) > 0 {
uniqueRefs := make(map[string]bool)
info += "\n\n**Referenced in**:\n"
for _, r := range refs {
if !uniqueRefs[r] {
uniqueRefs[r] = true
info += fmt.Sprintf("- %s\n", r)
}
}
}
return info return info
} }

View File

@@ -354,15 +354,17 @@ func (v *Validator) validateGAMSignal(gamNode, signalNode *index.ProjectNode, di
} }
if targetNode == nil { if targetNode == nil {
suppress := false suppressed := v.isGloballyAllowed("implicit")
for _, p := range signalNode.Pragmas { if !suppressed {
if strings.HasPrefix(p, "implicit:") { for _, p := range signalNode.Pragmas {
suppress = true if strings.HasPrefix(p, "implicit:") {
break suppressed = true
break
}
} }
} }
if !suppress { if !suppressed {
v.Diagnostics = append(v.Diagnostics, Diagnostic{ v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelWarning, Level: LevelWarning,
Message: fmt.Sprintf("Implicitly Defined Signal: '%s' is defined in GAM '%s' but not in DataSource '%s'", targetSignalName, gamNode.RealName, dsName), Message: fmt.Sprintf("Implicitly Defined Signal: '%s' is defined in GAM '%s' but not in DataSource '%s'", targetSignalName, gamNode.RealName, dsName),
@@ -624,6 +626,9 @@ func (v *Validator) checkUnusedRecursive(node *index.ProjectNode, referenced map
// Heuristic for GAM // Heuristic for GAM
if isGAM(node) { if isGAM(node) {
if !referenced[node] { if !referenced[node] {
if v.isGloballyAllowed("unused") {
return
}
suppress := false suppress := false
for _, p := range node.Pragmas { for _, p := range node.Pragmas {
if strings.HasPrefix(p, "unused:") { if strings.HasPrefix(p, "unused:") {
@@ -647,6 +652,9 @@ func (v *Validator) checkUnusedRecursive(node *index.ProjectNode, referenced map
if signalsNode, ok := node.Children["Signals"]; ok { if signalsNode, ok := node.Children["Signals"]; ok {
for _, signal := range signalsNode.Children { for _, signal := range signalsNode.Children {
if !referenced[signal] { if !referenced[signal] {
if v.isGloballyAllowed("unused") {
continue
}
suppress := false suppress := false
for _, p := range signal.Pragmas { for _, p := range signal.Pragmas {
if strings.HasPrefix(p, "unused:") { if strings.HasPrefix(p, "unused:") {
@@ -710,3 +718,15 @@ func (v *Validator) getNodeFile(node *index.ProjectNode) string {
} }
return "" return ""
} }
func (v *Validator) isGloballyAllowed(warningType string) bool {
prefix := fmt.Sprintf("//!allow(%s)", warningType)
for _, pragmas := range v.Tree.GlobalPragmas {
for _, p := range pragmas {
if strings.HasPrefix(p, prefix) {
return true
}
}
}
return false
}

BIN
mdt

Binary file not shown.

View File

@@ -84,7 +84,11 @@ The LSP server should provide the following capabilities:
- **Nodes (`+` / `$`)**: The prefixes `+` and `$` indicate that the node represents an object. - **Nodes (`+` / `$`)**: The prefixes `+` and `$` indicate that the node represents an object.
- **Constraint**: These nodes _must_ contain a field named `Class` within their subnode definition (across all files where the node is defined). - **Constraint**: These nodes _must_ contain a field named `Class` within their subnode definition (across all files where the node is defined).
- **Signals**: Signals are considered nodes but **not** objects. They do not require a `Class` field. - **Signals**: Signals are considered nodes but **not** objects. They do not require a `Class` field.
- **Pragmas (`//!`)**: Used to suppress specific diagnostics. The developer can use these to explain why a rule is being ignored. - **Pragmas (`//!`)**: Used to suppress specific diagnostics. The developer can use these to explain why a rule is being ignored. Supported pragmas:
- `//!unused: REASON` - Suppress "Unused GAM" or "Unused Signal" warnings for a specific node.
- `//!implicit: REASON` - Suppress "Implicitly Defined Signal" warnings for a specific signal reference.
- `//!allow(WARNING_TYPE): REASON` - Global suppression for a specific warning type across the whole project (supported: `unused`, `implicit`).
- `//!cast(DEF_TYPE, CUR_TYPE): REASON` - Suppress "Type Inconsistency" errors if types match.
- **Structure**: A configuration is composed by one or more definitions. - **Structure**: A configuration is composed by one or more definitions.
### Core MARTe Classes ### Core MARTe Classes
@@ -105,6 +109,7 @@ MARTe configurations typically involve several main categories of objects:
- **Requirements**: - **Requirements**:
- All signal definitions **must** include a `Type` field with a valid value. - All signal definitions **must** include a `Type` field with a valid value.
- **Size Information**: Signals can optionally include `NumberOfDimensions` and `NumberOfElements` fields. If not explicitly defined, these default to `1`. - **Size Information**: Signals can optionally include `NumberOfDimensions` and `NumberOfElements` fields. If not explicitly defined, these default to `1`.
- **Property Matching**: Signal references in GAMs must match the properties (`Type`, `NumberOfElements`, `NumberOfDimensions`) of the defined signal in the `DataSource`.
- **Extensibility**: Signal definitions can include additional fields as required by the specific application context. - **Extensibility**: Signal definitions can include additional fields as required by the specific application context.
- **Signal Reference Syntax**: - **Signal Reference Syntax**:
- Signals are referenced or defined in `InputSignals` or `OutputSignals` sub-nodes using one of the following formats: - Signals are referenced or defined in `InputSignals` or `OutputSignals` sub-nodes using one of the following formats:
@@ -186,13 +191,14 @@ The `fmt` command must format the code according to the following rules:
The LSP and `check` command should report the following: The LSP and `check` command should report the following:
- **Warnings**: - **Warnings**:
- **Unused GAM**: A GAM is defined but not referenced in any thread or scheduler. - **Unused GAM**: A GAM is defined but not referenced in any thread or scheduler. (Suppress with `//!unused`)
- **Unused Signal**: A signal is explicitly defined in a `DataSource` but never referenced in any `GAM`. - **Unused Signal**: A signal is explicitly defined in a `DataSource` but never referenced in any `GAM`. (Suppress with `//!unused`)
- **Implicitly Defined Signal**: A signal is defined only within a `GAM` and not in its parent `DataSource`. - **Implicitly Defined Signal**: A signal is defined only within a `GAM` and not in its parent `DataSource`. (Suppress with `//!implicit`)
- **Errors**: - **Errors**:
- **Type Inconsistency**: A signal is referenced with a type different from its definition. - **Type Inconsistency**: A signal is referenced with a type different from its definition. (Suppress with `//!cast`)
- **Size Inconsistency**: A signal is referenced with a size (dimensions/elements) different from its definition. - **Size Inconsistency**: A signal is referenced with a size (dimensions/elements) different from its definition.
- **Invalid Signal Content**: The `Signals` container of a `DataSource` contains invalid elements (e.g., fields instead of nodes).
- **Duplicate Field Definition**: A field is defined multiple times within the same node scope (including across multiple files). - **Duplicate Field Definition**: A field is defined multiple times within the same node scope (including across multiple files).
- **Validation Errors**: - **Validation Errors**:
- Missing mandatory fields. - Missing mandatory fields.

View File

@@ -0,0 +1,73 @@
package integration
import (
"testing"
"github.com/marte-dev/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser"
)
func TestGetNodeContaining(t *testing.T) {
content := `
+App = {
Class = RealTimeApplication
+State1 = {
Class = RealTimeState
+Thread1 = {
Class = RealTimeThread
Functions = { GAM1 }
}
}
}
+GAM1 = { Class = IOGAM }
`
p := parser.NewParser(content)
config, err := p.Parse()
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
idx := index.NewProjectTree()
file := "hover_context.marte"
idx.AddFile(file, config)
idx.ResolveReferences()
// Find reference to GAM1
var gamRef *index.Reference
for i := range idx.References {
ref := &idx.References[i]
if ref.Name == "GAM1" {
gamRef = ref
break
}
}
if gamRef == nil {
t.Fatal("Reference to GAM1 not found")
}
// Check containing node
container := idx.GetNodeContaining(file, gamRef.Position)
if container == nil {
t.Fatal("Container not found")
}
if container.RealName != "+Thread1" {
t.Errorf("Expected container +Thread1, got %s", container.RealName)
}
// Check traversal up to State
curr := container
foundState := false
for curr != nil {
if curr.RealName == "+State1" {
foundState = true
break
}
curr = curr.Parent
}
if !foundState {
t.Error("State parent not found")
}
}

View File

@@ -0,0 +1,67 @@
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 TestGlobalPragma(t *testing.T) {
content := `
//!allow(unused): Suppress all unused
//!allow(implicit): Suppress all implicit
+Data = {
Class = ReferenceContainer
+MyDS = {
Class = FileReader
Filename = "test"
Signals = {
UnusedSig = { Type = uint32 }
}
}
}
+MyGAM = {
Class = IOGAM
InputSignals = {
ImplicitSig = { DataSource = MyDS Type = uint32 }
}
}
`
p := parser.NewParser(content)
config, err := p.Parse()
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
idx := index.NewProjectTree()
idx.AddFile("global_pragma.marte", config)
idx.ResolveReferences()
v := validator.NewValidator(idx, ".")
v.ValidateProject()
v.CheckUnused()
foundUnusedWarning := false
foundImplicitWarning := false
for _, d := range v.Diagnostics {
if strings.Contains(d.Message, "Unused Signal") {
foundUnusedWarning = true
}
if strings.Contains(d.Message, "Implicitly Defined Signal") {
foundImplicitWarning = true
}
}
if foundUnusedWarning {
t.Error("Expected warning for UnusedSig to be suppressed globally")
}
if foundImplicitWarning {
t.Error("Expected warning for ImplicitSig to be suppressed globally")
}
}