Compare commits
3 Commits
d2b2750833
...
12615aa6d2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12615aa6d2 | ||
|
|
bd845aa859 | ||
|
|
b879766021 |
@@ -255,24 +255,7 @@ func (b *Builder) compute(left parser.Value, op parser.Token, right parser.Value
|
|||||||
return &parser.StringValue{Value: s1 + s2, Quoted: true}
|
return &parser.StringValue{Value: s1 + s2, Quoted: true}
|
||||||
}
|
}
|
||||||
|
|
||||||
lF, lIsF := b.valToFloat(left)
|
// Try Integer arithmetic first
|
||||||
rF, rIsF := b.valToFloat(right)
|
|
||||||
|
|
||||||
if lIsF || rIsF {
|
|
||||||
res := 0.0
|
|
||||||
switch op.Type {
|
|
||||||
case parser.TokenPlus:
|
|
||||||
res = lF + rF
|
|
||||||
case parser.TokenMinus:
|
|
||||||
res = lF - rF
|
|
||||||
case parser.TokenStar:
|
|
||||||
res = lF * rF
|
|
||||||
case parser.TokenSlash:
|
|
||||||
res = lF / rF
|
|
||||||
}
|
|
||||||
return &parser.FloatValue{Value: res, Raw: fmt.Sprintf("%g", res)}
|
|
||||||
}
|
|
||||||
|
|
||||||
lI, lIsI := b.valToInt(left)
|
lI, lIsI := b.valToInt(left)
|
||||||
rI, rIsI := b.valToInt(right)
|
rI, rIsI := b.valToInt(right)
|
||||||
|
|
||||||
@@ -303,6 +286,25 @@ func (b *Builder) compute(left parser.Value, op parser.Token, right parser.Value
|
|||||||
return &parser.IntValue{Value: res, Raw: fmt.Sprintf("%d", res)}
|
return &parser.IntValue{Value: res, Raw: fmt.Sprintf("%d", res)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback to Float arithmetic
|
||||||
|
lF, lIsF := b.valToFloat(left)
|
||||||
|
rF, rIsF := b.valToFloat(right)
|
||||||
|
|
||||||
|
if lIsF || rIsF {
|
||||||
|
res := 0.0
|
||||||
|
switch op.Type {
|
||||||
|
case parser.TokenPlus:
|
||||||
|
res = lF + rF
|
||||||
|
case parser.TokenMinus:
|
||||||
|
res = lF - rF
|
||||||
|
case parser.TokenStar:
|
||||||
|
res = lF * rF
|
||||||
|
case parser.TokenSlash:
|
||||||
|
res = lF / rF
|
||||||
|
}
|
||||||
|
return &parser.FloatValue{Value: res, Raw: fmt.Sprintf("%g", res)}
|
||||||
|
}
|
||||||
|
|
||||||
return left
|
return left
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -576,6 +576,8 @@ func HandleHover(params HoverParams) *Hover {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
container := Tree.GetNodeContaining(path, parser.Position{Line: line, Column: col})
|
||||||
|
|
||||||
var content string
|
var content string
|
||||||
|
|
||||||
if res.Node != nil {
|
if res.Node != nil {
|
||||||
@@ -589,7 +591,7 @@ func HandleHover(params HoverParams) *Hover {
|
|||||||
} else if res.Variable != nil {
|
} else if res.Variable != nil {
|
||||||
content = fmt.Sprintf("**Variable**: `%s`\nType: `%s`", res.Variable.Name, res.Variable.TypeExpr)
|
content = fmt.Sprintf("**Variable**: `%s`\nType: `%s`", res.Variable.Name, res.Variable.TypeExpr)
|
||||||
if res.Variable.DefaultValue != nil {
|
if res.Variable.DefaultValue != nil {
|
||||||
content += fmt.Sprintf("\nDefault: `%s`", valueToString(res.Variable.DefaultValue))
|
content += fmt.Sprintf("\nDefault: `%s`", valueToString(res.Variable.DefaultValue, container))
|
||||||
}
|
}
|
||||||
} else if res.Reference != nil {
|
} else if res.Reference != nil {
|
||||||
targetName := "Unresolved"
|
targetName := "Unresolved"
|
||||||
@@ -605,7 +607,7 @@ func HandleHover(params HoverParams) *Hover {
|
|||||||
targetName = v.Name
|
targetName = v.Name
|
||||||
fullInfo = fmt.Sprintf("**Variable**: `@%s`\nType: `%s`", v.Name, v.TypeExpr)
|
fullInfo = fmt.Sprintf("**Variable**: `@%s`\nType: `%s`", v.Name, v.TypeExpr)
|
||||||
if v.DefaultValue != nil {
|
if v.DefaultValue != nil {
|
||||||
fullInfo += fmt.Sprintf("\nDefault: `%s`", valueToString(v.DefaultValue))
|
fullInfo += fmt.Sprintf("\nDefault: `%s`", valueToString(v.DefaultValue, container))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -629,7 +631,8 @@ func HandleHover(params HoverParams) *Hover {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func valueToString(val parser.Value) string {
|
func valueToString(val parser.Value, ctx *index.ProjectNode) string {
|
||||||
|
val = evaluate(val, ctx)
|
||||||
switch v := val.(type) {
|
switch v := val.(type) {
|
||||||
case *parser.StringValue:
|
case *parser.StringValue:
|
||||||
if v.Quoted {
|
if v.Quoted {
|
||||||
@@ -649,7 +652,7 @@ func valueToString(val parser.Value) string {
|
|||||||
case *parser.ArrayValue:
|
case *parser.ArrayValue:
|
||||||
elements := []string{}
|
elements := []string{}
|
||||||
for _, e := range v.Elements {
|
for _, e := range v.Elements {
|
||||||
elements = append(elements, valueToString(e))
|
elements = append(elements, valueToString(e, ctx))
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("{ %s }", strings.Join(elements, " "))
|
return fmt.Sprintf("{ %s }", strings.Join(elements, " "))
|
||||||
default:
|
default:
|
||||||
@@ -676,7 +679,7 @@ func HandleCompletion(params CompletionParams) *CompletionList {
|
|||||||
prefix := lineStr[:col]
|
prefix := lineStr[:col]
|
||||||
|
|
||||||
// Case 3: Variable completion
|
// Case 3: Variable completion
|
||||||
varRegex := regexp.MustCompile(`([@$])([a-zA-Z0-9_]*)$`)
|
varRegex := regexp.MustCompile(`([@])([a-zA-Z0-9_]*)$`)
|
||||||
if matches := varRegex.FindStringSubmatch(prefix); matches != nil {
|
if matches := varRegex.FindStringSubmatch(prefix); matches != nil {
|
||||||
container := Tree.GetNodeContaining(path, parser.Position{Line: params.Position.Line + 1, Column: col + 1})
|
container := Tree.GetNodeContaining(path, parser.Position{Line: params.Position.Line + 1, Column: col + 1})
|
||||||
if container == nil {
|
if container == nil {
|
||||||
@@ -1292,6 +1295,55 @@ func formatNodeInfo(node *index.ProjectNode) string {
|
|||||||
info += fmt.Sprintf("\n\n%s", node.Doc)
|
info += fmt.Sprintf("\n\n%s", node.Doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if Implicit Signal peers exist
|
||||||
|
if ds, _ := getSignalInfo(node); ds != nil {
|
||||||
|
peers := findSignalPeers(node)
|
||||||
|
|
||||||
|
// 1. Explicit Definition Fields
|
||||||
|
var defNode *index.ProjectNode
|
||||||
|
for _, p := range peers {
|
||||||
|
if p.Parent != nil && p.Parent.Name == "Signals" {
|
||||||
|
defNode = p
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if defNode != nil {
|
||||||
|
for _, frag := range defNode.Fragments {
|
||||||
|
for _, def := range frag.Definitions {
|
||||||
|
if f, ok := def.(*parser.Field); ok {
|
||||||
|
key := f.Name
|
||||||
|
if key != "Type" && key != "NumberOfElements" && key != "NumberOfDimensions" && key != "Class" {
|
||||||
|
val := valueToString(f.Value, defNode)
|
||||||
|
info += fmt.Sprintf("\n**%s**: `%s`", key, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extraInfo := ""
|
||||||
|
for _, p := range peers {
|
||||||
|
if (p.Parent.Name == "InputSignals" || p.Parent.Name == "OutputSignals") && isGAM(p.Parent.Parent) {
|
||||||
|
gamName := p.Parent.Parent.RealName
|
||||||
|
for _, frag := range p.Fragments {
|
||||||
|
for _, def := range frag.Definitions {
|
||||||
|
if f, ok := def.(*parser.Field); ok {
|
||||||
|
key := f.Name
|
||||||
|
if key != "DataSource" && key != "Alias" && key != "Type" && key != "Class" && key != "NumberOfElements" && key != "NumberOfDimensions" {
|
||||||
|
val := valueToString(f.Value, p)
|
||||||
|
extraInfo += fmt.Sprintf("\n- **%s** (%s): `%s`", key, gamName, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if extraInfo != "" {
|
||||||
|
info += "\n\n**Usage Details**:" + extraInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Find references
|
// Find references
|
||||||
var refs []string
|
var refs []string
|
||||||
for _, ref := range Tree.References {
|
for _, ref := range Tree.References {
|
||||||
@@ -1440,6 +1492,81 @@ func HandleRename(params RenameParams) *WorkspaceEdit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if targetNode != nil {
|
if targetNode != nil {
|
||||||
|
// Special handling for Signals (Implicit/Explicit)
|
||||||
|
if ds, _ := getSignalInfo(targetNode); ds != nil {
|
||||||
|
peers := findSignalPeers(targetNode)
|
||||||
|
seenPeers := make(map[*index.ProjectNode]bool)
|
||||||
|
|
||||||
|
for _, peer := range peers {
|
||||||
|
if seenPeers[peer] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seenPeers[peer] = true
|
||||||
|
|
||||||
|
// Rename Peer Definition
|
||||||
|
prefix := ""
|
||||||
|
if len(peer.RealName) > 0 {
|
||||||
|
first := peer.RealName[0]
|
||||||
|
if first == '+' || first == '$' {
|
||||||
|
prefix = string(first)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
normNewName := strings.TrimLeft(params.NewName, "+$")
|
||||||
|
finalDefName := prefix + normNewName
|
||||||
|
|
||||||
|
hasAlias := false
|
||||||
|
for _, frag := range peer.Fragments {
|
||||||
|
for _, def := range frag.Definitions {
|
||||||
|
if f, ok := def.(*parser.Field); ok && f.Name == "Alias" {
|
||||||
|
hasAlias = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasAlias {
|
||||||
|
for _, frag := range peer.Fragments {
|
||||||
|
if frag.IsObject {
|
||||||
|
rng := Range{
|
||||||
|
Start: Position{Line: frag.ObjectPos.Line - 1, Character: frag.ObjectPos.Column - 1},
|
||||||
|
End: Position{Line: frag.ObjectPos.Line - 1, Character: frag.ObjectPos.Column - 1 + len(peer.RealName)},
|
||||||
|
}
|
||||||
|
addEdit(frag.File, rng, finalDefName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename References to this Peer
|
||||||
|
for _, ref := range Tree.References {
|
||||||
|
if ref.Target == peer {
|
||||||
|
// Handle qualified names
|
||||||
|
if strings.Contains(ref.Name, ".") {
|
||||||
|
if strings.HasSuffix(ref.Name, "."+peer.Name) {
|
||||||
|
prefixLen := len(ref.Name) - len(peer.Name)
|
||||||
|
rng := Range{
|
||||||
|
Start: Position{Line: ref.Position.Line - 1, Character: ref.Position.Column - 1 + prefixLen},
|
||||||
|
End: Position{Line: ref.Position.Line - 1, Character: ref.Position.Column - 1 + len(ref.Name)},
|
||||||
|
}
|
||||||
|
addEdit(ref.File, rng, normNewName)
|
||||||
|
} else if ref.Name == peer.Name {
|
||||||
|
rng := Range{
|
||||||
|
Start: Position{Line: ref.Position.Line - 1, Character: ref.Position.Column - 1},
|
||||||
|
End: Position{Line: ref.Position.Line - 1, Character: ref.Position.Column - 1 + len(ref.Name)},
|
||||||
|
}
|
||||||
|
addEdit(ref.File, rng, normNewName)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rng := Range{
|
||||||
|
Start: Position{Line: ref.Position.Line - 1, Character: ref.Position.Column - 1},
|
||||||
|
End: Position{Line: ref.Position.Line - 1, Character: ref.Position.Column - 1 + len(ref.Name)},
|
||||||
|
}
|
||||||
|
addEdit(ref.File, rng, normNewName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &WorkspaceEdit{Changes: changes}
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Rename Definitions
|
// 1. Rename Definitions
|
||||||
prefix := ""
|
prefix := ""
|
||||||
if len(targetNode.RealName) > 0 {
|
if len(targetNode.RealName) > 0 {
|
||||||
@@ -1566,7 +1693,7 @@ func suggestVariables(container *index.ProjectNode) *CompletionList {
|
|||||||
|
|
||||||
doc := ""
|
doc := ""
|
||||||
if info.Def.DefaultValue != nil {
|
if info.Def.DefaultValue != nil {
|
||||||
doc = fmt.Sprintf("Default: %s", valueToString(info.Def.DefaultValue))
|
doc = fmt.Sprintf("Default: %s", valueToString(info.Def.DefaultValue, container))
|
||||||
}
|
}
|
||||||
|
|
||||||
items = append(items, CompletionItem{
|
items = append(items, CompletionItem{
|
||||||
@@ -1581,3 +1708,196 @@ func suggestVariables(container *index.ProjectNode) *CompletionList {
|
|||||||
}
|
}
|
||||||
return &CompletionList{Items: items}
|
return &CompletionList{Items: items}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getSignalInfo(node *index.ProjectNode) (*index.ProjectNode, string) {
|
||||||
|
if node.Parent == nil {
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 1: Definition
|
||||||
|
if node.Parent.Name == "Signals" && isDataSource(node.Parent.Parent) {
|
||||||
|
return node.Parent.Parent, node.RealName
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2: Usage
|
||||||
|
if (node.Parent.Name == "InputSignals" || node.Parent.Name == "OutputSignals") && isGAM(node.Parent.Parent) {
|
||||||
|
dsName := ""
|
||||||
|
sigName := node.RealName
|
||||||
|
|
||||||
|
// Scan fields
|
||||||
|
for _, frag := range node.Fragments {
|
||||||
|
for _, def := range frag.Definitions {
|
||||||
|
if f, ok := def.(*parser.Field); ok {
|
||||||
|
if f.Name == "DataSource" {
|
||||||
|
if v, ok := f.Value.(*parser.StringValue); ok {
|
||||||
|
dsName = v.Value
|
||||||
|
}
|
||||||
|
if v, ok := f.Value.(*parser.ReferenceValue); ok {
|
||||||
|
dsName = v.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if f.Name == "Alias" {
|
||||||
|
if v, ok := f.Value.(*parser.StringValue); ok {
|
||||||
|
sigName = v.Value
|
||||||
|
}
|
||||||
|
if v, ok := f.Value.(*parser.ReferenceValue); ok {
|
||||||
|
sigName = v.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if dsName != "" {
|
||||||
|
dsNode := Tree.ResolveName(node, dsName, isDataSource)
|
||||||
|
return dsNode, sigName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func findSignalPeers(target *index.ProjectNode) []*index.ProjectNode {
|
||||||
|
dsNode, sigName := getSignalInfo(target)
|
||||||
|
if dsNode == nil || sigName == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var peers []*index.ProjectNode
|
||||||
|
|
||||||
|
// Add definition if exists (and not already target)
|
||||||
|
if signals, ok := dsNode.Children["Signals"]; ok {
|
||||||
|
if def, ok := signals.Children[index.NormalizeName(sigName)]; ok {
|
||||||
|
peers = append(peers, def)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find usages
|
||||||
|
Tree.Walk(func(n *index.ProjectNode) {
|
||||||
|
d, s := getSignalInfo(n)
|
||||||
|
if d == dsNode && s == sigName {
|
||||||
|
peers = append(peers, n)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return peers
|
||||||
|
}
|
||||||
|
|
||||||
|
func evaluate(val parser.Value, ctx *index.ProjectNode) parser.Value {
|
||||||
|
switch v := val.(type) {
|
||||||
|
case *parser.VariableReferenceValue:
|
||||||
|
name := strings.TrimLeft(v.Name, "@")
|
||||||
|
if info := Tree.ResolveVariable(ctx, name); info != nil {
|
||||||
|
if info.Def.DefaultValue != nil {
|
||||||
|
return evaluate(info.Def.DefaultValue, ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
case *parser.BinaryExpression:
|
||||||
|
left := evaluate(v.Left, ctx)
|
||||||
|
right := evaluate(v.Right, ctx)
|
||||||
|
return compute(left, v.Operator, right)
|
||||||
|
case *parser.UnaryExpression:
|
||||||
|
right := evaluate(v.Right, ctx)
|
||||||
|
return computeUnary(v.Operator, right)
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func compute(left parser.Value, op parser.Token, right parser.Value) parser.Value {
|
||||||
|
if op.Type == parser.TokenConcat {
|
||||||
|
getRaw := func(v parser.Value) string {
|
||||||
|
if s, ok := v.(*parser.StringValue); ok {
|
||||||
|
return s.Value
|
||||||
|
}
|
||||||
|
return valueToString(v, nil)
|
||||||
|
}
|
||||||
|
s1 := getRaw(left)
|
||||||
|
s2 := getRaw(right)
|
||||||
|
return &parser.StringValue{Value: s1 + s2, Quoted: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
toInt := func(v parser.Value) (int64, bool) {
|
||||||
|
if idx, ok := v.(*parser.IntValue); ok {
|
||||||
|
return idx.Value, true
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
toFloat := func(v parser.Value) (float64, bool) {
|
||||||
|
if f, ok := v.(*parser.FloatValue); ok {
|
||||||
|
return f.Value, true
|
||||||
|
}
|
||||||
|
if idx, ok := v.(*parser.IntValue); ok {
|
||||||
|
return float64(idx.Value), true
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
lI, lIsI := toInt(left)
|
||||||
|
rI, rIsI := toInt(right)
|
||||||
|
|
||||||
|
if lIsI && rIsI {
|
||||||
|
var res int64
|
||||||
|
switch op.Type {
|
||||||
|
case parser.TokenPlus:
|
||||||
|
res = lI + rI
|
||||||
|
case parser.TokenMinus:
|
||||||
|
res = lI - rI
|
||||||
|
case parser.TokenStar:
|
||||||
|
res = lI * rI
|
||||||
|
case parser.TokenSlash:
|
||||||
|
if rI != 0 {
|
||||||
|
res = lI / rI
|
||||||
|
}
|
||||||
|
case parser.TokenPercent:
|
||||||
|
if rI != 0 {
|
||||||
|
res = lI % rI
|
||||||
|
}
|
||||||
|
case parser.TokenAmpersand:
|
||||||
|
res = lI & rI
|
||||||
|
case parser.TokenPipe:
|
||||||
|
res = lI | rI
|
||||||
|
case parser.TokenCaret:
|
||||||
|
res = lI ^ rI
|
||||||
|
}
|
||||||
|
return &parser.IntValue{Value: res, Raw: fmt.Sprintf("%d", res)}
|
||||||
|
}
|
||||||
|
|
||||||
|
lF, lIsF := toFloat(left)
|
||||||
|
rF, rIsF := toFloat(right)
|
||||||
|
|
||||||
|
if lIsF || rIsF {
|
||||||
|
var res float64
|
||||||
|
switch op.Type {
|
||||||
|
case parser.TokenPlus:
|
||||||
|
res = lF + rF
|
||||||
|
case parser.TokenMinus:
|
||||||
|
res = lF - rF
|
||||||
|
case parser.TokenStar:
|
||||||
|
res = lF * rF
|
||||||
|
case parser.TokenSlash:
|
||||||
|
res = lF / rF
|
||||||
|
}
|
||||||
|
return &parser.FloatValue{Value: res, Raw: fmt.Sprintf("%g", res)}
|
||||||
|
}
|
||||||
|
|
||||||
|
return left
|
||||||
|
}
|
||||||
|
|
||||||
|
func computeUnary(op parser.Token, val parser.Value) parser.Value {
|
||||||
|
switch op.Type {
|
||||||
|
case parser.TokenMinus:
|
||||||
|
if i, ok := val.(*parser.IntValue); ok {
|
||||||
|
return &parser.IntValue{Value: -i.Value, Raw: fmt.Sprintf("%d", -i.Value)}
|
||||||
|
}
|
||||||
|
if f, ok := val.(*parser.FloatValue); ok {
|
||||||
|
return &parser.FloatValue{Value: -f.Value, Raw: fmt.Sprintf("%g", -f.Value)}
|
||||||
|
}
|
||||||
|
case parser.TokenSymbol:
|
||||||
|
if op.Value == "!" {
|
||||||
|
if b, ok := val.(*parser.BoolValue); ok {
|
||||||
|
return &parser.BoolValue{Value: !b.Value}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|||||||
@@ -319,6 +319,13 @@ func (p *Parser) parseAtom() (Value, bool) {
|
|||||||
}
|
}
|
||||||
return val, true
|
return val, true
|
||||||
}
|
}
|
||||||
|
if tok.Value == "!" {
|
||||||
|
val, ok := p.parseAtom()
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return &UnaryExpression{Position: tok.Position, Operator: tok, Right: val}, true
|
||||||
|
}
|
||||||
fallthrough
|
fallthrough
|
||||||
case TokenLBrace:
|
case TokenLBrace:
|
||||||
arr := &ArrayValue{Position: tok.Position}
|
arr := &ArrayValue{Position: tok.Position}
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ func (v *Validator) ValidateProject() {
|
|||||||
v.CheckUnused()
|
v.CheckUnused()
|
||||||
v.CheckDataSourceThreading()
|
v.CheckDataSourceThreading()
|
||||||
v.CheckINOUTOrdering()
|
v.CheckINOUTOrdering()
|
||||||
|
v.CheckSignalConsistency()
|
||||||
v.CheckVariables()
|
v.CheckVariables()
|
||||||
v.CheckUnresolvedVariables()
|
v.CheckUnresolvedVariables()
|
||||||
}
|
}
|
||||||
@@ -1233,6 +1234,93 @@ func (v *Validator) getDataSourceDirection(ds *index.ProjectNode) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *Validator) CheckSignalConsistency() {
|
||||||
|
// Map: DataSourceNode -> SignalName -> List of Signals
|
||||||
|
signals := make(map[*index.ProjectNode]map[string][]*index.ProjectNode)
|
||||||
|
|
||||||
|
// Helper to collect signals
|
||||||
|
collect := func(node *index.ProjectNode) {
|
||||||
|
if !isGAM(node) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Check Input and Output
|
||||||
|
for _, dir := range []string{"InputSignals", "OutputSignals"} {
|
||||||
|
if container, ok := node.Children[dir]; ok {
|
||||||
|
for _, sig := range container.Children {
|
||||||
|
fields := v.getFields(sig)
|
||||||
|
var dsNode *index.ProjectNode
|
||||||
|
var sigName string
|
||||||
|
|
||||||
|
// Resolve DS
|
||||||
|
if dsFields, ok := fields["DataSource"]; ok && len(dsFields) > 0 {
|
||||||
|
dsName := v.getFieldValue(dsFields[0], sig)
|
||||||
|
if dsName != "" {
|
||||||
|
dsNode = v.resolveReference(dsName, sig, isDataSource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve Name (Alias or RealName)
|
||||||
|
if aliasFields, ok := fields["Alias"]; ok && len(aliasFields) > 0 {
|
||||||
|
sigName = v.getFieldValue(aliasFields[0], sig)
|
||||||
|
} else {
|
||||||
|
sigName = sig.RealName
|
||||||
|
}
|
||||||
|
|
||||||
|
if dsNode != nil && sigName != "" {
|
||||||
|
sigName = index.NormalizeName(sigName)
|
||||||
|
if signals[dsNode] == nil {
|
||||||
|
signals[dsNode] = make(map[string][]*index.ProjectNode)
|
||||||
|
}
|
||||||
|
signals[dsNode][sigName] = append(signals[dsNode][sigName], sig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v.Tree.Walk(collect)
|
||||||
|
|
||||||
|
// Check Consistency
|
||||||
|
for ds, sigMap := range signals {
|
||||||
|
for sigName, usages := range sigMap {
|
||||||
|
if len(usages) <= 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Type consistency
|
||||||
|
var firstType string
|
||||||
|
var firstNode *index.ProjectNode
|
||||||
|
|
||||||
|
for _, u := range usages {
|
||||||
|
// Get Type
|
||||||
|
typeVal := ""
|
||||||
|
fields := v.getFields(u)
|
||||||
|
if typeFields, ok := fields["Type"]; ok && len(typeFields) > 0 {
|
||||||
|
typeVal = v.getFieldValue(typeFields[0], u)
|
||||||
|
}
|
||||||
|
|
||||||
|
if typeVal == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if firstNode == nil {
|
||||||
|
firstType = typeVal
|
||||||
|
firstNode = u
|
||||||
|
} else {
|
||||||
|
if typeVal != firstType {
|
||||||
|
v.Diagnostics = append(v.Diagnostics, Diagnostic{
|
||||||
|
Level: LevelError,
|
||||||
|
Message: fmt.Sprintf("Signal Type Mismatch: Signal '%s' (in DS '%s') is defined as '%s' in '%s' but as '%s' in '%s'", sigName, ds.RealName, firstType, firstNode.Parent.Parent.RealName, typeVal, u.Parent.Parent.RealName),
|
||||||
|
Position: v.getNodePosition(u),
|
||||||
|
File: v.getNodeFile(u),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (v *Validator) CheckVariables() {
|
func (v *Validator) CheckVariables() {
|
||||||
if v.Schema == nil {
|
if v.Schema == nil {
|
||||||
return
|
return
|
||||||
|
|||||||
56
test/builder_merge_test.go
Normal file
56
test/builder_merge_test.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/marte-community/marte-dev-tools/internal/builder"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuilderMergeNodes(t *testing.T) {
|
||||||
|
// Two files without package, defining SAME root node +App.
|
||||||
|
// This triggers merging logic in Builder.
|
||||||
|
|
||||||
|
content1 := `
|
||||||
|
+App = {
|
||||||
|
Field1 = 10
|
||||||
|
+Sub = { Val = 1 }
|
||||||
|
}
|
||||||
|
`
|
||||||
|
content2 := `
|
||||||
|
+App = {
|
||||||
|
Field2 = 20
|
||||||
|
+Sub = { Val2 = 2 }
|
||||||
|
}
|
||||||
|
`
|
||||||
|
f1, _ := os.CreateTemp("", "merge1.marte")
|
||||||
|
f1.WriteString(content1)
|
||||||
|
f1.Close()
|
||||||
|
defer os.Remove(f1.Name())
|
||||||
|
|
||||||
|
f2, _ := os.CreateTemp("", "merge2.marte")
|
||||||
|
f2.WriteString(content2)
|
||||||
|
f2.Close()
|
||||||
|
defer os.Remove(f2.Name())
|
||||||
|
|
||||||
|
b := builder.NewBuilder([]string{f1.Name(), f2.Name()}, nil)
|
||||||
|
|
||||||
|
outF, _ := os.CreateTemp("", "out_merge.marte")
|
||||||
|
defer os.Remove(outF.Name())
|
||||||
|
|
||||||
|
err := b.Build(outF)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Build failed: %v", err)
|
||||||
|
}
|
||||||
|
outF.Close()
|
||||||
|
|
||||||
|
outContent, _ := os.ReadFile(outF.Name())
|
||||||
|
outStr := string(outContent)
|
||||||
|
|
||||||
|
if !strings.Contains(outStr, "Field1 = 10") { t.Error("Missing Field1") }
|
||||||
|
if !strings.Contains(outStr, "Field2 = 20") { t.Error("Missing Field2") }
|
||||||
|
if !strings.Contains(outStr, "+Sub = {") { t.Error("Missing Sub") }
|
||||||
|
if !strings.Contains(outStr, "Val = 1") { t.Error("Missing Sub.Val") }
|
||||||
|
if !strings.Contains(outStr, "Val2 = 2") { t.Error("Missing Sub.Val2") }
|
||||||
|
}
|
||||||
55
test/formatter_coverage_test.go
Normal file
55
test/formatter_coverage_test.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/marte-community/marte-dev-tools/internal/formatter"
|
||||||
|
"github.com/marte-community/marte-dev-tools/internal/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFormatterCoverage(t *testing.T) {
|
||||||
|
content := `
|
||||||
|
// Head comment
|
||||||
|
#package Pkg
|
||||||
|
|
||||||
|
//# Doc for A
|
||||||
|
+A = {
|
||||||
|
Field = 10 // Trailing
|
||||||
|
Bool = true
|
||||||
|
Float = 1.23
|
||||||
|
Ref = SomeObj
|
||||||
|
Array = { 1 2 3 }
|
||||||
|
Expr = 1 + 2
|
||||||
|
|
||||||
|
// Inner
|
||||||
|
+B = {
|
||||||
|
Val = "Str"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final
|
||||||
|
`
|
||||||
|
p := parser.NewParser(content)
|
||||||
|
cfg, err := p.Parse()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
formatter.Format(cfg, &buf)
|
||||||
|
|
||||||
|
out := buf.String()
|
||||||
|
if !strings.Contains(out, "Field = 10") {
|
||||||
|
t.Error("Formatting failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check comments
|
||||||
|
if !strings.Contains(out, "// Head comment") {
|
||||||
|
t.Error("Head comment missing")
|
||||||
|
}
|
||||||
|
if !strings.Contains(out, "//# Doc for A") {
|
||||||
|
t.Error("Doc missing")
|
||||||
|
}
|
||||||
|
}
|
||||||
45
test/lexer_coverage_test.go
Normal file
45
test/lexer_coverage_test.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/marte-community/marte-dev-tools/internal/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLexerCoverage(t *testing.T) {
|
||||||
|
// 1. Comments
|
||||||
|
input := `
|
||||||
|
// Line comment
|
||||||
|
/* Block comment */
|
||||||
|
//# Docstring
|
||||||
|
//! Pragma
|
||||||
|
/* Unclosed block
|
||||||
|
`
|
||||||
|
l := parser.NewLexer(input)
|
||||||
|
for {
|
||||||
|
tok := l.NextToken()
|
||||||
|
if tok.Type == parser.TokenEOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Numbers
|
||||||
|
inputNum := `123 12.34 1.2e3 1.2E-3 0xFF`
|
||||||
|
lNum := parser.NewLexer(inputNum)
|
||||||
|
for {
|
||||||
|
tok := lNum.NextToken()
|
||||||
|
if tok.Type == parser.TokenEOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Identifiers
|
||||||
|
inputID := `Valid ID with-hyphen _under`
|
||||||
|
lID := parser.NewLexer(inputID)
|
||||||
|
for {
|
||||||
|
tok := lID.NextToken()
|
||||||
|
if tok.Type == parser.TokenEOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestLoggerPrint(t *testing.T) {
|
func TestLoggerPrint(t *testing.T) {
|
||||||
|
// Direct call for coverage
|
||||||
|
logger.Println("Coverage check")
|
||||||
|
|
||||||
if os.Getenv("TEST_LOGGER_PRINT") == "1" {
|
if os.Getenv("TEST_LOGGER_PRINT") == "1" {
|
||||||
logger.Printf("Test Printf %d", 123)
|
logger.Printf("Test Printf %d", 123)
|
||||||
logger.Println("Test Println")
|
logger.Println("Test Println")
|
||||||
|
|||||||
@@ -352,12 +352,12 @@ package schema
|
|||||||
t.Error("Expected @MyVar in suggestions for =")
|
t.Error("Expected @MyVar in suggestions for =")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Triggered by $
|
// 2. Triggered by @
|
||||||
// "Field = $"
|
// "Field = @"
|
||||||
lsp.Documents[uri] = `
|
lsp.Documents[uri] = `
|
||||||
#var MyVar: uint = 10
|
#var MyVar: uint = 10
|
||||||
+App = {
|
+App = {
|
||||||
Field = $
|
Field = @
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
params2 := lsp.CompletionParams{
|
params2 := lsp.CompletionParams{
|
||||||
@@ -366,7 +366,7 @@ package schema
|
|||||||
}
|
}
|
||||||
list2 := lsp.HandleCompletion(params2)
|
list2 := lsp.HandleCompletion(params2)
|
||||||
if list2 == nil {
|
if list2 == nil {
|
||||||
t.Fatal("Expected suggestions for $")
|
t.Fatal("Expected suggestions for @")
|
||||||
}
|
}
|
||||||
found = false
|
found = false
|
||||||
for _, item := range list2.Items {
|
for _, item := range list2.Items {
|
||||||
@@ -376,7 +376,7 @@ package schema
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
t.Error("Expected MyVar in suggestions for $")
|
t.Error("Expected MyVar in suggestions for @")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/marte-community/marte-dev-tools/internal/index"
|
||||||
"github.com/marte-community/marte-dev-tools/internal/lsp"
|
"github.com/marte-community/marte-dev-tools/internal/lsp"
|
||||||
|
"github.com/marte-community/marte-dev-tools/internal/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLSPIncrementalSync(t *testing.T) {
|
func TestLSPIncrementalSync(t *testing.T) {
|
||||||
@@ -108,3 +110,82 @@ func TestLSPMalformedParams(t *testing.T) {
|
|||||||
t.Errorf("Expected nil result for malformed params, got: %s", output)
|
t.Errorf("Expected nil result for malformed params, got: %s", output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLSPDispatch(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
lsp.Output = &buf
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
msgInit := &lsp.JsonRpcMessage{Method: "initialize", ID: 1, Params: json.RawMessage(`{}`)}
|
||||||
|
lsp.HandleMessage(msgInit)
|
||||||
|
|
||||||
|
// DidOpen
|
||||||
|
msgOpen := &lsp.JsonRpcMessage{Method: "textDocument/didOpen", Params: json.RawMessage(`{"textDocument":{"uri":"file://d.marte","text":""}}`)}
|
||||||
|
lsp.HandleMessage(msgOpen)
|
||||||
|
|
||||||
|
// DidChange
|
||||||
|
msgChange := &lsp.JsonRpcMessage{Method: "textDocument/didChange", Params: json.RawMessage(`{"textDocument":{"uri":"file://d.marte","version":2},"contentChanges":[{"text":"A"}]}`)}
|
||||||
|
lsp.HandleMessage(msgChange)
|
||||||
|
|
||||||
|
// Hover
|
||||||
|
msgHover := &lsp.JsonRpcMessage{Method: "textDocument/hover", ID: 2, Params: json.RawMessage(`{"textDocument":{"uri":"file://d.marte"},"position":{"line":0,"character":0}}`)}
|
||||||
|
lsp.HandleMessage(msgHover)
|
||||||
|
|
||||||
|
// Definition
|
||||||
|
msgDef := &lsp.JsonRpcMessage{Method: "textDocument/definition", ID: 3, Params: json.RawMessage(`{"textDocument":{"uri":"file://d.marte"},"position":{"line":0,"character":0}}`)}
|
||||||
|
lsp.HandleMessage(msgDef)
|
||||||
|
|
||||||
|
// References
|
||||||
|
msgRef := &lsp.JsonRpcMessage{Method: "textDocument/references", ID: 4, Params: json.RawMessage(`{"textDocument":{"uri":"file://d.marte"},"position":{"line":0,"character":0},"context":{"includeDeclaration":true}}`)}
|
||||||
|
lsp.HandleMessage(msgRef)
|
||||||
|
|
||||||
|
// Completion
|
||||||
|
msgComp := &lsp.JsonRpcMessage{Method: "textDocument/completion", ID: 5, Params: json.RawMessage(`{"textDocument":{"uri":"file://d.marte"},"position":{"line":0,"character":0}}`)}
|
||||||
|
lsp.HandleMessage(msgComp)
|
||||||
|
|
||||||
|
// Formatting
|
||||||
|
msgFmt := &lsp.JsonRpcMessage{Method: "textDocument/formatting", ID: 6, Params: json.RawMessage(`{"textDocument":{"uri":"file://d.marte"},"options":{"tabSize":4,"insertSpaces":true}}`)}
|
||||||
|
lsp.HandleMessage(msgFmt)
|
||||||
|
|
||||||
|
// Rename
|
||||||
|
msgRename := &lsp.JsonRpcMessage{Method: "textDocument/rename", ID: 7, Params: json.RawMessage(`{"textDocument":{"uri":"file://d.marte"},"position":{"line":0,"character":0},"newName":"B"}`)}
|
||||||
|
lsp.HandleMessage(msgRename)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLSPVariableDefinition(t *testing.T) {
|
||||||
|
lsp.Tree = index.NewProjectTree()
|
||||||
|
lsp.Documents = make(map[string]string)
|
||||||
|
|
||||||
|
content := `
|
||||||
|
#var MyVar: int = 10
|
||||||
|
+Obj = {
|
||||||
|
Field = @MyVar
|
||||||
|
}
|
||||||
|
`
|
||||||
|
uri := "file://var_def.marte"
|
||||||
|
lsp.Documents[uri] = content
|
||||||
|
|
||||||
|
p := parser.NewParser(content)
|
||||||
|
cfg, _ := p.Parse()
|
||||||
|
lsp.Tree.AddFile("var_def.marte", cfg)
|
||||||
|
lsp.Tree.ResolveReferences()
|
||||||
|
|
||||||
|
params := lsp.DefinitionParams{
|
||||||
|
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
|
||||||
|
Position: lsp.Position{Line: 3, Character: 13},
|
||||||
|
}
|
||||||
|
|
||||||
|
res := lsp.HandleDefinition(params)
|
||||||
|
if res == nil {
|
||||||
|
t.Fatal("Definition not found for variable")
|
||||||
|
}
|
||||||
|
|
||||||
|
locs, ok := res.([]lsp.Location)
|
||||||
|
if !ok || len(locs) == 0 {
|
||||||
|
t.Fatal("Expected location list")
|
||||||
|
}
|
||||||
|
|
||||||
|
if locs[0].Range.Start.Line != 1 {
|
||||||
|
t.Errorf("Expected line 1, got %d", locs[0].Range.Start.Line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,11 +15,23 @@ func TestOperators(t *testing.T) {
|
|||||||
#var B: int = 20
|
#var B: int = 20
|
||||||
#var S1: string = "Hello"
|
#var S1: string = "Hello"
|
||||||
#var S2: string = "World"
|
#var S2: string = "World"
|
||||||
|
#var FA: float = 1.5
|
||||||
|
#var FB: float = 2.0
|
||||||
|
|
||||||
+Obj = {
|
+Obj = {
|
||||||
Math = @A + @B
|
Math = @A + @B
|
||||||
Precedence = @A + @B * 2
|
Precedence = @A + @B * 2
|
||||||
Concat = @S1 .. " " .. @S2
|
Concat = @S1 .. " " .. @S2
|
||||||
|
FloatMath = @FA + @FB
|
||||||
|
Mix = @A + @FA
|
||||||
|
ConcatNum = "Num: " .. @A
|
||||||
|
ConcatFloat = "F: " .. @FA
|
||||||
|
ConcatArr = "A: " .. { 1 }
|
||||||
|
BoolVal = true
|
||||||
|
RefVal = Obj
|
||||||
|
ArrVal = { 1 2 }
|
||||||
|
Unres = @Unknown
|
||||||
|
InvalidMath = "A" + 1
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
// Check Parser
|
// Check Parser
|
||||||
@@ -55,4 +67,26 @@ func TestOperators(t *testing.T) {
|
|||||||
if !strings.Contains(outStr, "Concat = \"Hello World\"") {
|
if !strings.Contains(outStr, "Concat = \"Hello World\"") {
|
||||||
t.Errorf("Concat failed. Got:\n%s", outStr)
|
t.Errorf("Concat failed. Got:\n%s", outStr)
|
||||||
}
|
}
|
||||||
|
if !strings.Contains(outStr, "FloatMath = 3.5") {
|
||||||
|
t.Errorf("FloatMath failed. Got:\n%s", outStr)
|
||||||
|
}
|
||||||
|
// 10 + 1.5 = 11.5
|
||||||
|
if !strings.Contains(outStr, "Mix = 11.5") {
|
||||||
|
t.Errorf("Mix failed. Got:\n%s", outStr)
|
||||||
|
}
|
||||||
|
if !strings.Contains(outStr, "ConcatNum = \"Num: 10\"") {
|
||||||
|
t.Errorf("ConcatNum failed. Got:\n%s", outStr)
|
||||||
|
}
|
||||||
|
if !strings.Contains(outStr, "BoolVal = true") {
|
||||||
|
t.Errorf("BoolVal failed. Got:\n%s", outStr)
|
||||||
|
}
|
||||||
|
if !strings.Contains(outStr, "RefVal = Obj") {
|
||||||
|
t.Errorf("RefVal failed. Got:\n%s", outStr)
|
||||||
|
}
|
||||||
|
if !strings.Contains(outStr, "ArrVal = { 1 2 }") {
|
||||||
|
t.Errorf("ArrVal failed. Got:\n%s", outStr)
|
||||||
|
}
|
||||||
|
if !strings.Contains(outStr, "Unres = @Unknown") {
|
||||||
|
t.Errorf("Unres failed. Got:\n%s", outStr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
84
test/validator_expression_test.go
Normal file
84
test/validator_expression_test.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
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/schema"
|
||||||
|
"github.com/marte-community/marte-dev-tools/internal/validator"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidatorExpressionCoverage(t *testing.T) {
|
||||||
|
content := `
|
||||||
|
#var A: int = 10
|
||||||
|
#var B: int = 5
|
||||||
|
#var S1: string = "Hello"
|
||||||
|
#var S2: string = "World"
|
||||||
|
|
||||||
|
// Valid cases (execution hits evaluateBinary)
|
||||||
|
#var Sum: int = @A + @B // 15
|
||||||
|
#var Sub: int = @A - @B // 5
|
||||||
|
#var Mul: int = @A * @B // 50
|
||||||
|
#var Div: int = @A / @B // 2
|
||||||
|
#var Mod: int = @A % 3 // 1
|
||||||
|
#var Concat: string = @S1 .. " " .. @S2 // "Hello World"
|
||||||
|
#var Unary: int = -@A // -10
|
||||||
|
#var BitAnd: int = 10 & 5
|
||||||
|
#var BitOr: int = 10 | 5
|
||||||
|
#var BitXor: int = 10 ^ 5
|
||||||
|
|
||||||
|
#var FA: float = 1.5
|
||||||
|
#var FB: float = 2.0
|
||||||
|
#var FSum: float = @FA + @FB // 3.5
|
||||||
|
#var FSub: float = @FB - @FA // 0.5
|
||||||
|
#var FMul: float = @FA * @FB // 3.0
|
||||||
|
#var FDiv: float = @FB / @FA // 1.333...
|
||||||
|
|
||||||
|
#var BT: bool = true
|
||||||
|
#var BF: bool = !@BT
|
||||||
|
|
||||||
|
// Invalid cases (should error)
|
||||||
|
#var BadSum: int & > 20 = @A + @B // 15, should fail
|
||||||
|
#var BadUnary: bool = !10 // Should fail type check (nil result from evaluateUnary)
|
||||||
|
#var StrVar: string = "DS"
|
||||||
|
|
||||||
|
+InvalidDS = {
|
||||||
|
Class = IOGAM
|
||||||
|
InputSignals = {
|
||||||
|
S = { DataSource = 10 } // Int coverage
|
||||||
|
S2 = { DataSource = 1.5 } // Float coverage
|
||||||
|
S3 = { DataSource = true } // Bool coverage
|
||||||
|
S4 = { DataSource = @StrVar } // VarRef coverage -> String
|
||||||
|
S5 = { DataSource = { 1 } } // Array coverage (default case)
|
||||||
|
}
|
||||||
|
OutputSignals = {}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
pt := index.NewProjectTree()
|
||||||
|
p := parser.NewParser(content)
|
||||||
|
cfg, err := p.Parse()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse failed: %v", err)
|
||||||
|
}
|
||||||
|
pt.AddFile("expr.marte", cfg)
|
||||||
|
pt.ResolveReferences()
|
||||||
|
|
||||||
|
v := validator.NewValidator(pt, ".")
|
||||||
|
// Use NewSchema to ensure basic types
|
||||||
|
v.Schema = schema.NewSchema()
|
||||||
|
|
||||||
|
v.CheckVariables()
|
||||||
|
|
||||||
|
// Check for expected errors
|
||||||
|
foundBadSum := false
|
||||||
|
for _, diag := range v.Diagnostics {
|
||||||
|
if strings.Contains(diag.Message, "BadSum") && strings.Contains(diag.Message, "value mismatch") {
|
||||||
|
foundBadSum = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundBadSum {
|
||||||
|
t.Error("Expected error for BadSum")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user