From 99170dd639e2a7d31c1f998b2a0377ac8fc3fe04 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 2 Nov 2026 01:16:07 +0000 Subject: [PATCH] fix(semantics): restrict reflective attributes to owning metaclasses Co-Authored-By: jason.han --- ...ugated-port-definition-attributes.fixed.md | 2 +- ...ective-metaclass-owned-attributes.fixed.md | 1 + internal/semantic/semantics/annotations.go | 12 +++-- .../reflective_conjugated_port_test.go | 8 ++- .../reflective_metaclass_attributes_test.go | 50 +++++++++++++++++++ 5 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 changes/unreleased/reflective-metaclass-owned-attributes.fixed.md create mode 100644 internal/semantic/semantics/reflective_metaclass_attributes_test.go diff --git a/changes/unreleased/conjugated-port-definition-attributes.fixed.md b/changes/unreleased/conjugated-port-definition-attributes.fixed.md index 5f59e064f3..e92fa3e02d 100644 --- a/changes/unreleased/conjugated-port-definition-attributes.fixed.md +++ b/changes/unreleased/conjugated-port-definition-attributes.fixed.md @@ -1 +1 @@ -- The implied `ConjugatedPortDefinition` a port definition owns (SysML v2 8.3.12.3) now answers its plain Type attributes natively — `isAbstract`, `isSufficient`, `isConstant`, `isParallel` read `false`, as the RDF export states them — where the reflective read left them underived. +- The implied `ConjugatedPortDefinition` a port definition owns (SysML v2 8.3.12.3) now answers its Type attributes `isAbstract` and `isSufficient` natively as `false`, where the reflective read left them underived. diff --git a/changes/unreleased/reflective-metaclass-owned-attributes.fixed.md b/changes/unreleased/reflective-metaclass-owned-attributes.fixed.md new file mode 100644 index 0000000000..36d7f4e759 --- /dev/null +++ b/changes/unreleased/reflective-metaclass-owned-attributes.fixed.md @@ -0,0 +1 @@ +- Reflective reads no longer answer `isConstant` on definitions or `isParallel` outside `StateDefinition` and `StateUsage`. diff --git a/internal/semantic/semantics/annotations.go b/internal/semantic/semantics/annotations.go index 591f9e11e6..d9949c110e 100644 --- a/internal/semantic/semantics/annotations.go +++ b/internal/semantic/semantics/annotations.go @@ -2282,7 +2282,7 @@ func (m *Model) reflectiveFeatureValue(sym *symbols.Symbol, feature string) (sym // so its plain Type attributes read their defaults. if sym.IsConjugatedPortDefinition() { switch feature { - case "isAbstract", "isSufficient", "isConstant", "isParallel": + case "isAbstract", "isSufficient": return boolValue(false), true } } @@ -2292,10 +2292,10 @@ func (m *Model) reflectiveFeatureValue(sym *symbols.Symbol, feature string) (sym return boolValue(d.IsAbstract || IsVariation(sym)), true case "isSufficient": return boolValue(d.IsAll || m.reflectiveMetaclassConforms(sym, "ConnectionDefinition")), true - case "isConstant": - return boolValue(d.IsConstant), true case "isParallel": - return boolValue(d.IsParallel), true + if m.reflectiveMetaclassConforms(sym, "StateDefinition") { + return boolValue(d.IsParallel), true + } } case *ast.Usage: switch feature { @@ -2304,7 +2304,9 @@ func (m *Model) reflectiveFeatureValue(sym *symbols.Symbol, feature string) (sym case "isVariant": return boolValue(IsVariant(sym)), true case "isParallel": - return boolValue(d.IsParallel), true + if m.reflectiveMetaclassConforms(sym, "StateUsage") { + return boolValue(d.IsParallel), true + } } } if sym.Recorded() && feature == "isIndividual" { diff --git a/internal/semantic/semantics/reflective_conjugated_port_test.go b/internal/semantic/semantics/reflective_conjugated_port_test.go index e0ba2d2b9e..493cd55dde 100644 --- a/internal/semantic/semantics/reflective_conjugated_port_test.go +++ b/internal/semantic/semantics/reflective_conjugated_port_test.go @@ -44,12 +44,16 @@ func TestPortDefinitionOwnsItsConjugate(t *testing.T) { if got, ok := m.LookupMember(q, "~Q"); !ok || got != conjugate { t.Errorf("Q::'~Q' resolves to %v, want the conjugate", got) } - // Declared by nothing, the conjugate's plain attributes read false, as the conversion states them. - for _, feature := range []string{"isAbstract", "isSufficient", "isConstant", "isParallel", "isIndividual", "isVariation"} { + for _, feature := range []string{"isAbstract", "isSufficient", "isIndividual", "isVariation"} { if got, ok := m.ReflectiveFeatureValue(conjugate, feature); !ok || got.Kind != symbols.FilterValueBool || got.Bool { t.Errorf("%s of ~Q = %+v (ok %v), want false", feature, got, ok) } } + for _, feature := range []string{"isConstant", "isParallel"} { + if got, ok := m.ReflectiveFeatureValue(conjugate, feature); ok { + t.Errorf("%s of ~Q = %+v, want absent", feature, got) + } + } rels := m.OwnedRelationships(conjugate) if len(rels) != 1 { diff --git a/internal/semantic/semantics/reflective_metaclass_attributes_test.go b/internal/semantic/semantics/reflective_metaclass_attributes_test.go new file mode 100644 index 0000000000..94938f2628 --- /dev/null +++ b/internal/semantic/semantics/reflective_metaclass_attributes_test.go @@ -0,0 +1,50 @@ +package semantics + +import ( + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" +) + +func TestReflectiveAttributesBelongToTheirMetaclasses(t *testing.T) { + m, root := buildModelWithStdlib(t, `package P { + part def Part; + port def Port; + state def Parallel parallel { state a; state b; } + state def Plain; + part p { + state parallelState parallel { state a; state b; } + state plainState; + } + constant part frozen; + }`) + for _, tc := range []struct { + path string + feature string + present bool + value bool + }{ + {path: "P::Part", feature: "isConstant"}, + {path: "P::Port", feature: "isConstant"}, + {path: "P::Part", feature: "isParallel"}, + {path: "P::Port", feature: "isParallel"}, + {path: "P::Parallel", feature: "isParallel", present: true, value: true}, + {path: "P::Plain", feature: "isParallel", present: true}, + {path: "P::p::parallelState", feature: "isParallel", present: true, value: true}, + {path: "P::p::plainState", feature: "isParallel", present: true}, + {path: "P::p", feature: "isParallel"}, + {path: "P::p", feature: "isConstant", present: true}, + {path: "P::frozen", feature: "isConstant", present: true, value: true}, + } { + t.Run(tc.path+"."+tc.feature, func(t *testing.T) { + sym := nestedSym(t, root, tc.path) + got, ok := m.ReflectiveFeatureValue(sym, tc.feature) + if ok != tc.present { + t.Fatalf("%s.%s = %+v (ok %t), want ok %t", tc.path, tc.feature, got, ok, tc.present) + } + if ok && (got.Kind != symbols.FilterValueBool || got.Bool != tc.value) { + t.Errorf("%s.%s = %+v, want %t", tc.path, tc.feature, got, tc.value) + } + }) + } +}