diff --git a/README.md b/README.md index 04513486..686f836d 100644 --- a/README.md +++ b/README.md @@ -225,6 +225,13 @@ section above). | Multiple views, `--view ` | Render only the named view | | `--view ` names a view that does not exist | Error: lists available view names and exits non-zero | +For the General View diagram strategy (the diagram produced when no more specialized view kind +applies), a view's body `expose <...>;` statements now scope the rendered diagram to the union of +the exposed names' containment subtrees, instead of always rendering the full workspace; a view +with no `expose` statement continues to render the full workspace. `render ;` names a +rendering style (not yet honored) and has no effect on scope. This scoping is not yet extended to +the other layout strategies — see the *Introduction* user guide for details. + ## NuGet Packages | Package | Description | diff --git a/ROADMAP.md b/ROADMAP.md index c1155f2d..050b96a0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -69,6 +69,77 @@ primitives (bar, diamond, pentagon, note). `LayoutActivation`/`LayoutBand` alrea **Visual gate:** sequence shows activation bars + a fragment; action flow shows a fork/join and a decision/merge with correct shapes. +### View `filter [];` expression evaluation + +`GeneralViewLayoutStrategy` now scopes a rendered diagram to a view's `expose <...>;` subject +subtree, but a view's `filter [];` body statement is only parsed into +`SysmlViewNode.FilterExpressionText` (raw source text) — it is never evaluated, and a +layout warning ("parsed but not yet evaluated") is emitted in its place. SysML v2 view filtering +is part of the standard's view/viewpoint mechanism for selectively including/excluding elements +from a rendered diagram by predicate, beyond simple subject-subtree containment scoping (for +example, "only elements satisfying a given requirement" or "only elements with a given +stereotype"); without evaluation, a modeler's `filter` statement is silently ineffective beyond +the warning. + +- Design and implement an expression evaluator for the bracketed filter expression grammar + (boolean/membership predicates over the resolved scope's elements), reusing/aligning with + existing expression-parsing infrastructure where practical. +- Apply the evaluated predicate as an additional filter over the resolved (expose) scope in + `GeneralViewLayoutStrategy`, removing the "not yet evaluated" warning once a filter + expression is present and successfully evaluated. +- Surface a diagnostic for filter expressions that fail to parse or evaluate, mirroring the + unresolved-reference diagnostic pattern already used for `expose`. + +**Scope:** `AstBuilder`/`SysmlViewNode` (expression AST capture, if warranted, beyond raw text); +new expression-evaluation component; `GeneralViewLayoutStrategy` filter application. +**Visual gate:** a view with a `filter [];` statement renders only the elements +satisfying the predicate, with no "not yet evaluated" warning. + +### Expose-based scoping for the remaining layout strategies + +`GeneralViewLayoutStrategy` implements `expose`-based subject-scoping (containment-subtree +filtering driven by a view's `expose` body statements — the only content-scoping mechanism a +view has), but `InterconnectionView`, `StateTransitionView`, `ActionFlowView`, `SequenceView`, +`GridView`, and `BrowserView` layout strategies do not yet honor `ViewContext.ViewNode`'s +`Expose` edges and continue to render their full applicable scope regardless of a view's +declared `expose` statements. + +- Extend the same `ResolveExposedScope`/`IsInSubjectScope` containment-subtree idiom (or a + shared helper extracted from `GeneralViewLayoutStrategy`) to each of the six remaining layout + strategies, respecting the "no `Expose` edge → render everything unchanged" fallback used by + `GeneralViewLayoutStrategy`. +- Add regression tests per strategy mirroring `GeneralViewLayoutStrategyTests`'s expose-scoping, + expose-union, and no-expose-statement-regression scenarios. + +**Scope:** `InterconnectionViewLayoutStrategy`, `StateTransitionViewLayoutStrategy`, +`ActionFlowViewLayoutStrategy`, `SequenceViewLayoutStrategy`, `GridViewLayoutStrategy`, +`BrowserViewLayoutStrategy`; corresponding test files. +**Visual gate:** each of the six views renders a scoped diagram when its view declares an +`expose <...>;` statement naming a resolvable target, unchanged when it does not. + +### Support selecting rendering style via `render ;` + +A view's `render ;` member names a rendering style/format usage per the SysML v2 +grammar (e.g. `asTreeDiagram`, `asElementTable`, `asTextualNotation`, `asTextualNotationTable` — +`rendering` usages, a distinct usage/definition kind) — it is captured verbatim on +`SysmlViewNode.RenderTargetName` but currently has no effect on rendering; every view renders +through the single `DiagramTypeRouter` → `ILayoutStrategy` selection regardless of its declared +`render` member. This is a distinct future capability from content scoping, which is `expose`'s +exclusive role. + +- Design a mapping from recognized rendering-style names (`asTreeDiagram`, `asElementTable`, + and so on, once corresponding layout/rendering strategies exist) to an `ILayoutStrategy`/ + renderer selection, honored when a view declares a `render` member naming one. +- Leave `RenderTargetName` un-honored (as today) for rendering-style names with no corresponding + strategy, with no diagnostic — an unrecognized rendering-style name is not an error, since + `render` selects presentation, not content. + +**Scope:** `DiagramTypeRouter`/`RenderCommand` (rendering-style selection); new layout/rendering +strategies for tree-diagram/element-table/textual-notation styles, if not already covered by an +existing strategy. +**Visual gate:** a view declaring `render asTreeDiagram;` (once a tree-diagram strategy exists) +renders using that style instead of the default `GeneralView` layout. + --- ## Release & packaging diff --git a/docs/design/sysml2-tools-core.md b/docs/design/sysml2-tools-core.md index 321c7694..bb2a8a69 100644 --- a/docs/design/sysml2-tools-core.md +++ b/docs/design/sysml2-tools-core.md @@ -99,7 +99,8 @@ N/A — not a safety-classified software item. 1. `DiagramRenderer.RenderWorkspace` receives a `SysmlWorkspace`, an `IRenderer`, and `RenderOptions`. For each view declared in the workspace it constructs a `ViewContext` - containing the view name and workspace reference. + containing the view name, workspace reference, and (when available) the view's resolved + AST node. 2. `ILayoutStrategy.BuildLayout` is called with the `ViewContext` and `RenderOptions`. The Layout subsystem produces a fully resolved `LayoutTree` by delegating geometric placement and routing to the off-the-shelf `DemaConsulting.Rendering.Layout` layered algorithm (through the diff --git a/docs/design/sysml2-tools-core/layout/internal/general-view-layout-strategy.md b/docs/design/sysml2-tools-core/layout/internal/general-view-layout-strategy.md index 925e3973..d965467a 100644 --- a/docs/design/sysml2-tools-core/layout/internal/general-view-layout-strategy.md +++ b/docs/design/sysml2-tools-core/layout/internal/general-view-layout-strategy.md @@ -34,24 +34,53 @@ from the structural relationships. ###### `BuildLayout(ViewContext context, RenderOptions options)` -Entry point. Calls `CollectDefinitions` to gather user definitions; returns a minimal 200×100 empty -`LayoutTree` when none are found. Otherwise groups the definitions by package with `GroupByPackage`, -resolves the specialization/membership/attribute-typing relationships into qualified-name edges with +Entry point. First resolves the view's exposed-name scope via `ResolveExposedScope`, +then calls `CollectDefinitions` to gather user definitions restricted to that scope (or every +definition when no scope applies); returns a minimal 200×100 empty `LayoutTree` when none are +found. Otherwise groups the definitions by package with `GroupByPackage`, resolves the +specialization/membership/attribute-typing relationships into qualified-name edges with `BuildModelEdges`, builds the single input `LayoutGraph` with `BuildGraph`, and places the whole graph with one `HierarchicalLayoutAlgorithm().Apply(graph, LayoutOptions.ForAlgorithm("containment"))` call — passing the desired root-scope leaf algorithm through the options parameter (not `graph.Set(CoreOptions.Algorithm, …)`) so a caller going through `LayoutEngine.Layout(graph)` later is never misled into skipping the hierarchical engine. When any package folder was depth-truncated, `DecorateTruncatedFolders` stamps each truncated folder's "+N more…" ellipsis label onto its placed -box before returning. - -###### `CollectDefinitions(workspace, theme)` +box. Finally, when `context.ViewNode?.FilterExpressionText` is non-null, attaches the +"parsed but not yet evaluated" warning (from `LayoutWarnings.ForUnevaluatedFilter`) to the returned +tree's `Warnings` via the `LayoutTree with { Warnings = … }` record-copy idiom, leaving the +resolved (unfiltered) scope's content unchanged. + +###### `ResolveExposedScope(SysmlWorkspace workspace, SysmlViewNode? viewNode)` + +Resolves the qualified-name containment-subtree scope a view's `expose` statements restrict the +diagram to — the only content-scoping mechanism a view has (`render ;` names a +rendering style/format, e.g. `asTreeDiagram`/`asElementTable`, per the SysML v2 grammar, never +content, so `RenderTargetName` never affects this decision). Returns `null` — meaning "render +everything", byte-identical to the pre-scoping behavior — whenever the view has no resolved +`Expose`-kind `ResolvedEdges` entries: covering a `null` `viewNode` (the `--auto` synthesized +view, which never carries expose/render/filter data), a view with no `expose` statement, and a +view whose every `expose` entry failed to resolve, uniformly. Otherwise, for each resolved +`Expose` edge's target qualified name, adds that name to the scope; when +`workspace.Declarations` resolves the target to a `SysmlFeatureNode` (a usage, e.g. +`part myVehicle : Vehicle;`) rather than a `SysmlDefinitionNode`, additionally resolves the +usage's own `Typing`-kind `ResolvedEdges` entry (if any) and adds *that* type's qualified name to +the scope too — the usage-to-type resolution fix for the containment gap where a usage's own +(typically empty) subtree would otherwise silently produce zero content. + +###### `IsInSubjectScope(qualifiedName, subjects)` + +Returns `true` when `qualifiedName` equals one of `subjects` or lies within one of their +containment subtrees (a `"{subject}::"` prefix match) — the same qualified-name-prefix idiom +`StdlibFilter.IsStdlibElement` already uses for stdlib-prefix matching. Generic over any subject +list; unchanged by this fix. + +###### `CollectDefinitions(workspace, theme, scope)` Iterates `workspace.Declarations`, keeping each `SysmlDefinitionNode` that is not a -standard-library element (per `StdlibFilter.IsStdlibElement`). For each kept definition it builds -the compartments from the owned usage features (grouped by keyword, each formatted as a -`name : Type [n]` row), collects the typed memberships, and computes the box size from the title and -the longest compartment row. +standard-library element (per `StdlibFilter.IsStdlibElement`) and, when `scope` is non-null, is +within `scope` per `IsInSubjectScope`. For each kept definition it builds the compartments from +the owned usage features (grouped by keyword, each formatted as a `name : Type [n]` row), collects +the typed memberships, and computes the box size from the title and the longest compartment row. ###### `GroupByPackage(defs)` @@ -128,6 +157,10 @@ produces valid geometry, so no crossing warnings are emitted. - `BoxMetrics` (`DemaConsulting.Rendering.Abstractions`) — box title-area and folder-tab geometry. - `StdlibFilter` (Rendering Internal subsystem) — standard-library exclusion. - `SysmlWorkspace`, `SysmlDefinitionNode`, `SysmlFeatureNode` (Semantic subsystem) — model input. +- `SysmlViewNode`, `SysmlEdge`, `SysmlEdgeKind` (Semantic subsystem) — a view's resolved + `expose` data, read by `ResolveExposedScope`. +- `LayoutWarnings` (Layout Internal subsystem) — `ForUnevaluatedFilter` supplies the + "parsed but not yet evaluated" filter-expression warning text. - The `LayoutTree`, `LayoutBox`, `LayoutCompartment`, `LayoutLine`, `LayoutLabel`, and `Point2D` data types (`DemaConsulting.Rendering`). - `FeatureMembership` (private record) — carries the keyword and type reference of one owned feature. diff --git a/docs/design/sysml2-tools-core/layout/internal/layout-warnings.md b/docs/design/sysml2-tools-core/layout/internal/layout-warnings.md index a1c04737..6f46ca28 100644 --- a/docs/design/sysml2-tools-core/layout/internal/layout-warnings.md +++ b/docs/design/sysml2-tools-core/layout/internal/layout-warnings.md @@ -23,6 +23,18 @@ Returns the warnings for a view: is rendered in singular form for a count of one and plural form otherwise, and the count is formatted with the invariant culture. +###### `ForUnevaluatedFilter(viewName, filterExpressionText)` + +Returns the warnings for a view's declared filter expression: + +1. When `filterExpressionText` is `null` (the view has no `filter [];` member), an empty + list is returned. +2. Otherwise a single warning string is produced naming the view: `"View '{viewName}' declares a + filter expression, which is parsed but not yet evaluated; all elements in the resolved scope + are rendered unfiltered."` The raw expression text itself is not interpolated into the message + (only its presence matters) — full filter expression evaluation is deferred future work (see + ROADMAP.md). + ##### Error Handling N/A - the method performs no validation and does not throw; a non-positive count simply yields an @@ -36,4 +48,6 @@ empty list and any string view name is accepted. ##### Callers View layout strategies that route connectors call `LayoutWarnings.ForCrossings` to attach -crossing warnings to the `LayoutTree` they produce. +crossing warnings to the `LayoutTree` they produce. `GeneralViewLayoutStrategy` calls +`LayoutWarnings.ForUnevaluatedFilter` to attach the "not yet evaluated" filter warning when a +view's `FilterExpressionText` is non-null. diff --git a/docs/design/sysml2-tools-core/rendering.md b/docs/design/sysml2-tools-core/rendering.md index 81a07471..72ded57b 100644 --- a/docs/design/sysml2-tools-core/rendering.md +++ b/docs/design/sysml2-tools-core/rendering.md @@ -53,7 +53,14 @@ flowchart TD - *Type*: Sealed record. - *Role*: Data transfer object. -- *Contract*: `string ViewName`, `SysmlWorkspace Workspace`. +- *Contract*: `string ViewName`, `SysmlWorkspace Workspace`, `SysmlViewNode? ViewNode = null`. + `ViewNode` is the view's resolved AST node, giving a layout strategy access to the view's + declared `render`/`expose`/`filter` body statements. Of these, only `ExposedNames` (and its + resolved `Expose` edges) drives content scoping; `RenderTargetName` names a rendering + style/format per the SysML v2 grammar and is captured as inert metadata only (it never + affects scope or resolution); `FilterExpressionText` is captured as raw text and not yet + evaluated. `ViewNode` is `null` for the `--auto` synthesized view, which carries no AST node + of its own. **Theme**: Visual configuration record. @@ -127,9 +134,17 @@ flowchart TD that tree and the options to `IRenderer.Render`. Each rendered stream is wrapped in a `RenderOutput` and collected into the return list. -2. `ILayoutStrategy.BuildLayout` receives a `ViewContext` containing the workspace and the view - name, plus `RenderOptions` for size and scale hints. It produces a fully resolved - `LayoutTree` with all waypoints in absolute canvas coordinates. +2. `ILayoutStrategy.BuildLayout` receives a `ViewContext` containing the workspace, the view + name, and (when available) the view's resolved AST node, plus `RenderOptions` for size and + scale hints. It produces a fully resolved `LayoutTree` with all waypoints in absolute canvas + coordinates. `GeneralViewLayoutStrategy` is the only strategy that currently reads + `ViewContext.ViewNode` to scope its diagram: when the view has one or more resolved `Expose` + edges, the diagram is scoped to the union of the exposed targets' containment subtrees + (resolving through a usage's type to its definition's subtree where needed); a view with no + `Expose` edges renders the full workspace, unchanged from prior behavior. The view's + `render`/`filter` statements never affect this scope. Every other strategy ignores + `ViewContext.ViewNode` and renders as before (see the Layout subsystem's + `general-view-layout-strategy` design doc for the scoping algorithm). 3. `IRenderer.Render` receives the `LayoutTree` and `RenderOptions` and writes all rendered bytes to the supplied `Stream`. It must not perform any layout computation; it only reads @@ -160,6 +175,7 @@ flowchart TD | SysML2Tools-Core-Rendering-ThemeDepthWrap | `Theme.DepthFillColors` with modulo indexing documented in `Theme` | | SysML2Tools-Core-Rendering-RenderOptions | `RenderOptions` record with default values | | SysML2Tools-Core-Rendering-ILayoutStrategy | `ILayoutStrategy` interface and `ViewContext` record | +| SysML2Tools-Core-Rendering-ViewContextViewNode | `ViewContext.ViewNode` flows to `DiagramRenderer.RenderWorkspace` | | SysML2Tools-Core-Rendering-DiagramRenderer | `DiagramRenderer.RenderWorkspace`; `DiagramTypeRouter`; `StdlibFilter` | | SysML2Tools-Core-Rendering-RenderOutput | `RenderOutput` record | | SysML2Tools-Core-Rendering-BuiltinThemes | `Themes.Light`, `Themes.Dark`, `Themes.Print` | diff --git a/docs/design/sysml2-tools-core/rendering/diagram-renderer.md b/docs/design/sysml2-tools-core/rendering/diagram-renderer.md index a621bc01..514a6c4c 100644 --- a/docs/design/sysml2-tools-core/rendering/diagram-renderer.md +++ b/docs/design/sysml2-tools-core/rendering/diagram-renderer.md @@ -20,9 +20,11 @@ off-the-shelf `RenderOptions` and `RenderOutput` records from `DemaConsulting.Re For each declaration in the workspace it skips non-view nodes and standard-library views (via `StdlibFilter`), routes the view to an `ILayoutStrategy` (via `DiagramTypeRouter`), skips views with -no supporting strategy or that do not match `viewFilter`, builds the `LayoutTree`, renders it to an -in-memory stream, and collects a `RenderOutput` with a sanitized file name and the layout warnings. -Returns an empty list when the workspace declares no renderable views. +no supporting strategy or that do not match `viewFilter`, constructs a `ViewContext` carrying the +view's own `SysmlViewNode` (so a strategy such as `GeneralViewLayoutStrategy` can read its resolved +`render`/`expose`/`filter` data), builds the `LayoutTree`, renders it to an in-memory stream, and +collects a `RenderOutput` with a sanitized file name and the layout warnings. Returns an empty list +when the workspace declares no renderable views. ##### `GetViewNames(workspace)` diff --git a/docs/design/sysml2-tools-language/semantic.md b/docs/design/sysml2-tools-language/semantic.md index ec118328..3b60944c 100644 --- a/docs/design/sysml2-tools-language/semantic.md +++ b/docs/design/sysml2-tools-language/semantic.md @@ -57,8 +57,9 @@ optionally seeded with a pre-populated symbol table. - *Role*: Data container. - *Contract*: Exposes `IReadOnlyList Files`, `IReadOnlySet StdlibNames`, `IReadOnlyDictionary Declarations` mapping qualified names to declaration - nodes, and `SemanticIndex Index` — a reverse-lookup index over resolved supertype, typing, - and import edges (see Semantic Model Subsystem). + nodes, and `SemanticIndex Index` — a reverse-lookup index over all resolved edge kinds + (supertype, typing, import, satisfy, verify, allocate, connect, transition, and expose; see + Semantic Model Subsystem). ### Design diff --git a/docs/design/sysml2-tools-language/semantic/model.md b/docs/design/sysml2-tools-language/semantic/model.md index d2f2e76f..3048f3b8 100644 --- a/docs/design/sysml2-tools-language/semantic/model.md +++ b/docs/design/sysml2-tools-language/semantic/model.md @@ -57,7 +57,7 @@ over resolved edges. | --- | --- | | `AstBuilder` | Visits ANTLR4 CST; builds typed AST nodes with qualified names and supertype lists | | `SymbolTable` | Registry mapping fully-qualified names to their AST nodes | -| `ReferenceResolver` | Resolves supertype/typing/import/satisfy/verify/allocate/connect/transition refs; builds index | +| `ReferenceResolver` | Resolves supertype/typing/import/satisfy/verify/allocate/connect/transition/expose refs | | `SupertypeWalker` | Walks specialization chains; detects cyclic specialization | | `SysmlNode` | Public abstract base record (and subtypes) modeling one parsed AST element | | `SysmlEdge` | Public record modeling one resolved reference (Supertype/Typing/Import/Satisfy/Verify/Allocate/etc.) | diff --git a/docs/design/sysml2-tools-language/semantic/model/ast-builder.md b/docs/design/sysml2-tools-language/semantic/model/ast-builder.md index 7f2ef8f8..dae1912d 100644 --- a/docs/design/sysml2-tools-language/semantic/model/ast-builder.md +++ b/docs/design/sysml2-tools-language/semantic/model/ast-builder.md @@ -22,6 +22,7 @@ stack with `::` to form the fully-qualified name. | `VisitAttributeDefinition` | `AttributeDefinitionContext` | `SysmlDefinitionNode` | | `VisitItemDefinition` | `ItemDefinitionContext` | `SysmlDefinitionNode` | | `VisitViewDefinition` | `ViewDefinitionContext` | `SysmlViewNode` | +| `VisitViewUsage` | `ViewUsageContext` | `SysmlViewNode` | | `VisitViewpointDefinition` | `ViewpointDefinitionContext` | `SysmlViewpointNode` | | `VisitAllocationUsage` | `AllocationUsageContext` | `SysmlConnectionNode` (`ConnectionKeyword = "allocation"`) | | `VisitSatisfyRequirementUsage` | `SatisfyRequirementUsageContext` | `SysmlSatisfyNode` | @@ -88,6 +89,60 @@ satisfied requirement's raw reference text is taken from `ownedReferenceSubsetti text comes from `satisfactionSubjectMember()` (the `by ` clause), or is `null` when absent. +`VisitViewDefinition` builds a `SysmlViewNode` for `view def` definitions, additionally scanning +`context.viewDefinitionBody()?.viewDefinitionBodyItem()` via the shared +`ExtractViewRenderAndFilter` helper (see below) to populate `RenderTargetName` and +`FilterExpressionText`. `VisitViewUsage` builds a `SysmlViewNode` for named `view` usages (the +only body form that may additionally contain `expose` members) the same way, plus +`ExtractExposedNames` to populate `ExposedNames`. Unnamed view usages are skipped (no declared +name), mirroring the existing anonymous-element convention. + +**`VisitViewUsage` is an intentional capability addition, not merely an `expose`-capture +prerequisite.** Before this override existed, named `view Name { ... }` usages were silently +dropped by the default `VisitChildren` aggregation — only `view def` declarations were ever +visible as renderable top-level declarations. Adding `VisitViewUsage` means every named `view` +usage in a workspace (whether or not it declares `expose`) now becomes its own `SysmlViewNode` +declaration that the render subsystem discovers and renders. This is a deliberate, documented +increase in output surface area: for example, the OMG corpus fixture +`test/SysMLModels/OMG/validation/11-ViewAndViewpoint/11b-SafetyAndSecurityFeatureViews.sysml` +declares 2 `view def`s plus 3 named `view` usages, so rendering it with no `--view` filter now +produces 5 output files instead of 2 (see +`RenderSubsystemTests.RenderSubsystem_OmgSafetyFeatureViewsCorpus_RendersAllNamedViewUsages`). + +`ExtractViewRenderAndFilter(IEnumerable bodyItems)` is a single generic helper +shared by both `VisitViewDefinition` (`ViewDefinitionBodyItemContext`) and `VisitViewUsage` +(`ViewBodyItemContext`) — the two context types are unrelated in the generated parser's type +hierarchy but expose identically-shaped `viewRenderingMember()`/`elementFilterMember()` +accessors, so a type-switch pattern inside two small private helpers +(`GetViewRenderingMember`/`GetElementFilterMember`) lets one generic method serve both body-item +types without duplicating the scan loop. The first `render` member wins if more than one +appears (a defensive tie-break, not a validated SysML constraint). `ExtractRenderTargetName` +follows the same two-form fallback pattern `VisitSatisfyRequirementUsage` uses: the direct +reference form (`ownedReferenceSubsetting()`), falling back to the typed-placeholder form's +feature typing (`ExtractFeatureTyping`), falling back to the raw usage text. The filter +expression's raw source text is taken verbatim from +`elementFilterMember().ownedExpression()?.GetText()` — never evaluated. + +`ExtractExposedNames(IEnumerable bodyItems)` collects the raw reference +text of every `expose ;` member in source order, reusing the shared `ExtractImportTarget` +helper (see below) against each `expose` member's wrapped `namespaceImport()`/ +`membershipImport()` — the identical grammar shape `import` uses. + +`ExtractImportTarget(NamespaceImportContext?, MembershipImportContext?)` is a shared helper +extracted from `VisitImportRule`'s previously inline logic, returning the extracted +qualified/dotted name text and whether the reference is a wildcard, for either the +namespace-import form (`qualifiedName::*`, always a wildcard), the membership-import form +(`qualifiedName`, optionally `::**`), or the bracketed-filter form nested inside a +namespace-import (`qualifiedName::**[]`) — the dominant `expose` shape in the real +OMG corpus (e.g. `expose vehicle::**[@Safety];`). The grammar nests the qualified name two levels +deeper for that third form: `namespaceImport -> filterPackage -> filterPackageImportDeclaration -> +(membershipImport | namespaceImportDirect)`. `ExtractImportTarget` descends into +`filterPackage().filterPackageImportDeclaration()` and extracts the qualified name from whichever +of `membershipImport()`/`namespaceImportDirect()` is present there, rather than only checking the +direct `qualifiedName()` child (which is null for this alternative). `VisitImportRule` and +`ExtractExposedNames` both call this one helper rather than duplicating the extraction logic, per +the Copy-Paste Programming anti-pattern guidance in coding-principles.md. + `VisitRequirementUsage` performs a minimal capture (name/qualified-name only, so named requirement usages become resolvable symbols) and additionally invokes `FindVerificationMembers` against its own `requirementBody()` (when present) to populate `VerifiedRequirementNames` — diff --git a/docs/design/sysml2-tools-language/semantic/model/reference-resolver.md b/docs/design/sysml2-tools-language/semantic/model/reference-resolver.md index 578b2fca..d0c080cd 100644 --- a/docs/design/sysml2-tools-language/semantic/model/reference-resolver.md +++ b/docs/design/sysml2-tools-language/semantic/model/reference-resolver.md @@ -132,6 +132,12 @@ no edge): = resolved first end, `Target` = resolved second end) only when both ends resolve, using the identical both-sides-must-resolve contract as `Satisfy`. Regular `"connection"`/`"message"` keyword variants remain intentionally unresolved (out of scope for this unit). +- **`SysmlViewNode` (Expose)** — resolves each `ExposedNames` entry into its own + `SysmlEdgeKind.Expose` edge, or the standard unresolved-reference Warning diagnostic when it + does not resolve. `RenderTargetName` names a rendering style/format (e.g. `asTreeDiagram`, + `asElementTable`) per the SysML v2 grammar — never model content — so `ReferenceResolver` + never inspects it: no edge is produced and no diagnostic is emitted for it, exactly mirroring + how `FilterExpressionText` (raw source text, not a reference) is also never inspected here. ##### Deviations From Uniform Resolution (Behavior-Neutral Additive Fixes) @@ -178,7 +184,8 @@ unresolved names are present. `VerifiedRequirementNames`; checks for `SysmlFeatureNode.FeatureTyping`, `SysmlSatisfyNode` (`SubjectName`/`RequirementName`), `SysmlConnectionNode` with `ConnectionKeyword == "allocation"` (`EndpointA`/`EndpointB`), `SysmlConnectionNode` with `ConnectionKeyword == - "connection"` or `"message"`, and `SysmlTransitionNode` (`Source`/`Target`); reads + "connection"` or `"message"`, `SysmlTransitionNode` (`Source`/`Target`), and `SysmlViewNode` + (`ExposedNames`; `RenderTargetName`/`FilterExpressionText` are never read); reads `ResolvedEdges` (`Typing`/`Supertype` kinds) during feature-chain resolution. - `SysmlEdge`, `SemanticIndex` — resolved references are recorded as `SysmlEdge` instances and aggregated into the returned `SemanticIndex`. diff --git a/docs/design/sysml2-tools-language/semantic/model/sysml-edge.md b/docs/design/sysml2-tools-language/semantic/model/sysml-edge.md index 7a8f2857..d32f695a 100644 --- a/docs/design/sysml2-tools-language/semantic/model/sysml-edge.md +++ b/docs/design/sysml2-tools-language/semantic/model/sysml-edge.md @@ -4,12 +4,12 @@ `SysmlEdge` and `SysmlEdgeKind` model a single resolved directed reference between two qualified names in the semantic model. Edges are produced by `ReferenceResolver` while -walking supertype, feature-typing, import, satisfy, verify, allocate, connect, and transition -references, and are the raw material indexed by `SemanticIndex`. +walking supertype, feature-typing, import, satisfy, verify, allocate, connect, transition, +and expose references, and are the raw material indexed by `SemanticIndex`. ##### Types -`SysmlEdgeKind` is an enum with eight members: +`SysmlEdgeKind` is an enum with nine members: - `Supertype` — a specialization reference (`SupertypeNames` / `specializes` / `:>`). - `Typing` — a feature typing reference (`SysmlFeatureNode.FeatureTyping`, the type after `:`). @@ -33,6 +33,14 @@ references, and are the raw material indexed by `SemanticIndex`. from the source state and targeting the target state (`SysmlTransitionNode`). Either side may be a dotted feature chain, resolved the same way as `Connect`; recorded only when both the source and target resolve — an implied/omitted source produces no edge. +- `Expose` — a view usage's resolved exposed-name reference (`expose ;`, valid only inside + a `view` usage's body), sourced from the view's qualified name and targeting each resolvable + entry in `SysmlViewNode.ExposedNames`, one `Expose` edge per entry. `Expose` is the sole + view-scoping edge kind: `GeneralViewLayoutStrategy` scopes its diagram to the union of every + `Expose` edge's target containment subtree; a view with no `Expose` edges renders the full + workspace, unchanged from the pre-scoping baseline. `SysmlViewNode.RenderTargetName` (a + rendering-style/format selector, e.g. `asTreeDiagram`) is captured but never resolved into an + edge and has no effect on scope. `SysmlEdge` is a sealed positional record with three properties: diff --git a/docs/design/sysml2-tools-language/semantic/model/sysml-node.md b/docs/design/sysml2-tools-language/semantic/model/sysml-node.md index 00464c7e..b31fbfb9 100644 --- a/docs/design/sysml2-tools-language/semantic/model/sysml-node.md +++ b/docs/design/sysml2-tools-language/semantic/model/sysml-node.md @@ -15,7 +15,7 @@ requirement-satisfaction usages. | `SysmlDefinitionNode` | Definition element (part def, attribute def, etc.); adds DefinitionKeyword | | `SysmlFeatureNode` | Feature/usage element | | `SysmlImportNode` | Import declaration; adds ImportedNamespace, IsWildcard | -| `SysmlViewNode` | View definition | +| `SysmlViewNode` | View definition; adds RenderTargetName, ExposedNames, FilterExpressionText | | `SysmlViewpointNode` | Viewpoint definition | | `SysmlConnectionNode` | Connection/binding/allocation usage; adds ConnectionKeyword, EndpointA, EndpointB | | `SysmlTransitionNode` | State transition; adds Source, Target, Guard | @@ -42,7 +42,8 @@ All nodes carry: sourced from this node — there is no standalone verify-usage node type; this list is the sole producer of `Verify` edges. - `ResolvedEdges` — resolved outgoing `SysmlEdge` entries (supertype, typing, import, satisfy, - verify, allocate), populated post-construction by `ReferenceResolver`; a settable (not `init`) + verify, allocate, connect, transition, expose), populated post-construction by + `ReferenceResolver`; a settable (not `init`) property since resolution runs after the AST is built and the symbol table is fully populated. Empty for stdlib-only nodes, which are registered but never passed through `ReferenceResolver.ResolveAll`. @@ -94,6 +95,32 @@ There are no behavioral methods beyond the inherited `object` members. `SysmlImp - `SubjectName` — the raw reference text of the satisfying subject (from the `by ` clause), or null when no `by` clause is present. +`SysmlViewNode` adds: + +- `RenderTargetName` — the raw reference text of the view's `render ;` member (the + first one found if more than one appears), or null when the view has no `render` member. Per + the SysML v2 grammar this names a rendering style/format usage (e.g. `asTreeDiagram`, + `asElementTable`) — never a content-scoping subject. Extracted by the shared + `AstBuilder.ExtractRenderTargetName` helper, which follows the same two-form fallback pattern + (direct reference, then typed placeholder) `VisitSatisfyRequirementUsage` already uses. + Captured verbatim only: `ReferenceResolver` never inspects or resolves this value (no edge is + produced, no diagnostic is emitted), and it has no effect on `GeneralViewLayoutStrategy`'s + rendered scope. Reserved for a possible future capability that selects among rendering-style + strategies — see the project ROADMAP. +- `ExposedNames` — the raw reference text of each `expose ;` member in a `view` usage's + body, in source order, or empty when none are present (and always empty for a `view def` + definition — `expose` is only valid grammar inside a `view` usage's body). Extracted by + `AstBuilder.ExtractExposedNames`, sharing the same `ExtractImportTarget` helper `VisitImportRule` + uses for plain `import`. Each entry is independently resolved by `ReferenceResolver` into a + `SysmlEdgeKind.Expose` edge, or an unresolved-reference diagnostic (and no edge) for that + entry. This is the sole field `GeneralViewLayoutStrategy` uses to scope a rendered diagram. +- `FilterExpressionText` — the raw source text of the view's `filter [];` member's + bracketed expression, or null when absent. Captured verbatim by `AstBuilder` + (`elementFilterMember().ownedExpression().GetText()`) and never evaluated or inspected by + `ReferenceResolver` — full filter expression evaluation is deferred future work (see + ROADMAP.md). `GeneralViewLayoutStrategy` emits a "parsed but not yet evaluated" warning + whenever this is non-null. + ##### Error Handling N/A — node types are pure data containers with no logic or validation. Invalid or anonymous @@ -111,12 +138,17 @@ elements are filtered out by `AstBuilder` before a node is constructed. captured `comment`/`doc` annotating elements nested in the node's body; sets `VerifiedRequirementNames` via the recursive verification-member finder; builds `SysmlSatisfyNode` instances from `satisfyRequirementUsage` via `VisitSatisfyRequirementUsage`, and the - `"allocation"` `SysmlConnectionNode` variant via `VisitAllocationUsage`. + `"allocation"` `SysmlConnectionNode` variant via `VisitAllocationUsage`; builds `SysmlViewNode` + instances (setting `RenderTargetName`/`FilterExpressionText`, and additionally `ExposedNames` + for usages) from both `VisitViewDefinition` (`view def`) and `VisitViewUsage` (`view`, the + only form that can carry `expose`). - `SymbolTable` — traverses the node hierarchy via `Children`; reads `QualifiedName`. - `ReferenceResolver` — reads `SupertypeNames`, `FeatureTyping`, `ImportedNames`, `VerifiedRequirementNames`, `Children`; checks for `SysmlImportNode`, `SysmlSatisfyNode`, the `"allocation"` `SysmlConnectionNode` variant, the `"connection"`/`"message"` - `SysmlConnectionNode` variants, and `SysmlTransitionNode`; writes `ResolvedEdges` after - resolving references (in two passes — supertype/typing/import/satisfy/verify/allocate, then - feature-chain connect/transition). + `SysmlConnectionNode` variants, `SysmlTransitionNode`, and `SysmlViewNode` (reading + `ExposedNames`; `RenderTargetName`/`FilterExpressionText` are never read); writes + `ResolvedEdges` after resolving references (in two passes — + supertype/typing/import/satisfy/verify/allocate/expose, then feature-chain + connect/transition). - `SupertypeWalker` — reads `SupertypeNames` on each node retrieved from `SymbolTable`. diff --git a/docs/gallery/README.md b/docs/gallery/README.md index c43e817b..e0c89e6c 100644 --- a/docs/gallery/README.md +++ b/docs/gallery/README.md @@ -30,6 +30,20 @@ SVG: [`svg/DroneGeneralView.svg`](svg/DroneGeneralView.svg) ![Drone General View](png/DroneGeneralView.png) +### 1b. View-scoped rendering — `expose` narrows the same model to one subsystem + +The same `01-drone-general.sysml` model also declares a second, named `view` usage +that adds an `expose Battery;` statement. Instead of the full workspace, the diagram +is scoped to just the `Battery` definition's containment subtree — demonstrating +that `expose` (not `render`) is what controls diagram content scope. See the user +guide's [Expose vs. Render](../user_guide/introduction.md#expose-vs-render-worked-examples) +section for the full explanation and more worked examples. + +Model: [`models/01-drone-general.sysml`](models/01-drone-general.sysml) (`BatterySubsystemView`) · +SVG: [`svg/BatterySubsystemView.svg`](svg/BatterySubsystemView.svg) + +![Battery Subsystem View](png/BatterySubsystemView.png) + --- ## 2. Interconnection View — Desktop Workstation diff --git a/docs/gallery/images/01-drone-general.png/DroneGeneralView.png b/docs/gallery/images/01-drone-general.png/DroneGeneralView.png deleted file mode 100644 index 90093f99..00000000 Binary files a/docs/gallery/images/01-drone-general.png/DroneGeneralView.png and /dev/null differ diff --git a/docs/gallery/images/02-computer-interconnection.png/WorkstationInterconnectionView.png b/docs/gallery/images/02-computer-interconnection.png/WorkstationInterconnectionView.png deleted file mode 100644 index ae1b4e14..00000000 Binary files a/docs/gallery/images/02-computer-interconnection.png/WorkstationInterconnectionView.png and /dev/null differ diff --git a/docs/gallery/images/03-elevator-state.png/ElevatorStateTransitionView.png b/docs/gallery/images/03-elevator-state.png/ElevatorStateTransitionView.png deleted file mode 100644 index 7e755c33..00000000 Binary files a/docs/gallery/images/03-elevator-state.png/ElevatorStateTransitionView.png and /dev/null differ diff --git a/docs/gallery/images/04-pipeline-action-flow.png/PipelineActionFlowView.png b/docs/gallery/images/04-pipeline-action-flow.png/PipelineActionFlowView.png deleted file mode 100644 index 798ff3cb..00000000 Binary files a/docs/gallery/images/04-pipeline-action-flow.png/PipelineActionFlowView.png and /dev/null differ diff --git a/docs/gallery/images/05-oauth-sequence.png/OAuthSequenceView.png b/docs/gallery/images/05-oauth-sequence.png/OAuthSequenceView.png deleted file mode 100644 index 63b707b9..00000000 Binary files a/docs/gallery/images/05-oauth-sequence.png/OAuthSequenceView.png and /dev/null differ diff --git a/docs/gallery/images/06-vehicle-grid.png/TaxonomyMatrixView.png b/docs/gallery/images/06-vehicle-grid.png/TaxonomyMatrixView.png deleted file mode 100644 index 5599af41..00000000 Binary files a/docs/gallery/images/06-vehicle-grid.png/TaxonomyMatrixView.png and /dev/null differ diff --git a/docs/gallery/images/07-avionics-browser.png/AvionicsBrowserView.png b/docs/gallery/images/07-avionics-browser.png/AvionicsBrowserView.png deleted file mode 100644 index 91b6b1c1..00000000 Binary files a/docs/gallery/images/07-avionics-browser.png/AvionicsBrowserView.png and /dev/null differ diff --git a/docs/gallery/models/01-drone-general.sysml b/docs/gallery/models/01-drone-general.sysml index 3a49ab73..ce5676cb 100644 --- a/docs/gallery/models/01-drone-general.sysml +++ b/docs/gallery/models/01-drone-general.sysml @@ -64,4 +64,12 @@ package QuadcopterDrone { requirement def FlightTimeRequirement; view def DroneGeneralView {} + + // A view-scoped rendering example: `expose` narrows the diagram to just + // the Battery subsystem's containment subtree, instead of the whole + // workspace. See docs/gallery/README.md section 1b and the user guide's + // "Expose vs. Render" section for more detail. + view BatterySubsystemView { + expose Battery; + } } diff --git a/docs/gallery/png/BatterySubsystemView.png b/docs/gallery/png/BatterySubsystemView.png new file mode 100644 index 00000000..fa0eb4c9 Binary files /dev/null and b/docs/gallery/png/BatterySubsystemView.png differ diff --git a/docs/gallery/svg/BatterySubsystemView.svg b/docs/gallery/svg/BatterySubsystemView.svg new file mode 100644 index 00000000..249347d5 --- /dev/null +++ b/docs/gallery/svg/BatterySubsystemView.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + «package» + QuadcopterDrone + + «part def» + Battery + + attributes + capacity : Voltage + + ports + output : PowerPort + diff --git a/docs/reqstream/sysml2-tools-core/layout/internal/general-view-layout-strategy.yaml b/docs/reqstream/sysml2-tools-core/layout/internal/general-view-layout-strategy.yaml index 7e56dc49..9eb27212 100644 --- a/docs/reqstream/sysml2-tools-core/layout/internal/general-view-layout-strategy.yaml +++ b/docs/reqstream/sysml2-tools-core/layout/internal/general-view-layout-strategy.yaml @@ -150,3 +150,56 @@ sections: - GeneralViewLayoutStrategy_BuildLayout_AdaptiveGap_DenseModelProducesNonOverlappingBoxes - GeneralViewLayoutStrategy_BuildLayout_HeatLayout_ConnectedModelKeepsBoxesSeparated - GeneralViewLayoutStrategy_BuildLayout_HeatLayout_SparseModelProducesCompactCanvas + + - id: SysML2Tools-Core-Layout-Internal-GeneralViewLayoutStrategy-ExposeScoping + title: >- + When a view's ViewContext carries one or more resolved Expose edges, + GeneralViewLayoutStrategy shall scope the diagram to the union of each edge's target + containment subtree, resolved through the target's own type reference when the target + is a usage rather than a definition. + justification: | + `expose ;` is the only real content-scoping mechanism a view has in SysML v2 — + `render ;` names a rendering style/format (e.g. `asTreeDiagram`, + `asElementTable`), never content, so it plays no role in scoping. Scoping the diagram + to the union of every resolved `expose` target's containment subtree makes `expose` + statements finally have an observable effect, matching the SysML v2 view mechanism's + subject-scoping intent. When an exposed target resolves to a feature usage rather than + a definition, its own containment subtree is typically empty — the real content lives + under its type's subtree — so the target's own resolved type reference is additionally + followed to include that type's subtree, fixing the usage-vs-definition containment gap. + tests: + - GeneralViewLayoutStrategy_BuildLayout_ExposedName_UnionsAdditionalSubtree + - GeneralViewLayoutStrategy_BuildLayout_ExposedUsage_ResolvesThroughTypingToDefinitionSubtree + + - id: SysML2Tools-Core-Layout-Internal-GeneralViewLayoutStrategy-NoExposeFallback + title: >- + When a view's ViewContext carries no resolved Expose edge — because the view has no + `expose` statement, its ViewNode is null (e.g. the `--auto` synthesized view), or its + every declared `expose` entry failed to resolve — GeneralViewLayoutStrategy shall + render the full workspace, unchanged from its pre-scoping behavior, regardless of + whether `RenderTargetName`/`FilterExpressionText` are present. + justification: | + Nearly every real-world view declares no `expose` statement at all, so rendering the + full workspace must remain the default. This also guards against a bogus `expose ;` + silently rendering nothing: an unresolvable exposed name (e.g. a typo) falls back to + full-workspace rendering (with the unresolved-reference diagnostic already surfaced by + `ReferenceResolver`) rather than producing a broken or empty diagram. `RenderTargetName` + (a rendering-style/format selector, not content) never affects this decision. + tests: + - GeneralViewLayoutStrategy_BuildLayout_RenderTargetNameOnly_NoExposeEdges_RendersFullWorkspace + - GeneralViewLayoutStrategy_BuildLayout_NullViewNode_RendersFullWorkspaceUnchanged + + - id: SysML2Tools-Core-Layout-Internal-GeneralViewLayoutStrategy-UnevaluatedFilterWarning + title: >- + When a view's FilterExpressionText is non-null, GeneralViewLayoutStrategy shall emit a + diagnostic through the layout's Warnings channel stating that the view declares a + filter expression which is parsed but not yet evaluated, and shall continue rendering + the (unfiltered) resolved scope. + justification: | + Full `filter [];` expression evaluation is deferred future work (see ROADMAP.md). + Silently ignoring a declared filter would mislead a user into believing their diagram is + filtered when every element in the resolved scope is actually shown unfiltered; + surfacing an explicit warning through the existing layout-warnings channel makes this + limitation visible rather than a silent gap. + tests: + - GeneralViewLayoutStrategy_BuildLayout_FilterExpressionPresent_EmitsNotYetEvaluatedWarning diff --git a/docs/reqstream/sysml2-tools-core/layout/internal/layout-warnings.yaml b/docs/reqstream/sysml2-tools-core/layout/internal/layout-warnings.yaml index 976db4ea..74fa042c 100644 --- a/docs/reqstream/sysml2-tools-core/layout/internal/layout-warnings.yaml +++ b/docs/reqstream/sysml2-tools-core/layout/internal/layout-warnings.yaml @@ -30,3 +30,17 @@ sections: tests: - ForCrossings_One_ReturnsSingularWarning - ForCrossings_Many_ReturnsPluralWarning + + - id: SysML2Tools-Core-Layout-Internal-LayoutWarnings-UnevaluatedFilter + title: >- + When a view's filter expression text is non-null, LayoutWarnings shall produce a + warning naming the view and stating that its filter expression is parsed but not yet + evaluated; when null, LayoutWarnings shall produce no warning. + justification: | + Full `filter [];` expression evaluation is deferred future work (see + ROADMAP.md). Silently ignoring a declared filter would mislead a user into believing + their diagram is filtered when every element in the resolved scope actually renders + unfiltered; this warning makes the limitation visible. + tests: + - ForUnevaluatedFilter_NullText_ReturnsEmpty + - ForUnevaluatedFilter_NonNullText_ReturnsNotYetEvaluatedWarning diff --git a/docs/reqstream/sysml2-tools-core/rendering.yaml b/docs/reqstream/sysml2-tools-core/rendering.yaml index d34398de..51cba53a 100644 --- a/docs/reqstream/sysml2-tools-core/rendering.yaml +++ b/docs/reqstream/sysml2-tools-core/rendering.yaml @@ -102,6 +102,21 @@ sections: - DiagramRenderer_RenderWorkspace_SoftwareStructureModel_ReturnsSvgOutput - DiagramRenderer_RenderWorkspace_GeneralViewModel_SvgContainsElementNames + - id: SysML2Tools-Core-Rendering-ViewContextViewNode + title: >- + The view context passed to a layout strategy shall optionally carry the view's + resolved AST node, so that a layout strategy can access the view's declared + render target, exposed names, and filter expression when computing its layout. + justification: | + A layout strategy needs access to a view's declared `render`/`expose`/`filter` body + statements — not just its name and the whole workspace — to scope its diagram. + Threading the view's AST node through DiagramRenderer to each ILayoutStrategy is the + plumbing prerequisite for GeneralViewLayoutStrategy's `expose`-scoped rendering. + tests: + - ViewContext_Construction_WithViewNode_StoresViewNode + - GeneralViewLayoutStrategy_BuildLayout_ExposedName_UnionsAdditionalSubtree + - GeneralViewLayoutStrategy_BuildLayout_NullViewNode_RendersFullWorkspaceUnchanged + - id: SysML2Tools-Core-Rendering-DiagramRenderer title: >- For each view in a workspace, the Rendering subsystem shall lay the view out and diff --git a/docs/reqstream/sysml2-tools-language/semantic/model/ast-builder.yaml b/docs/reqstream/sysml2-tools-language/semantic/model/ast-builder.yaml index 30d5741d..7693db5d 100644 --- a/docs/reqstream/sysml2-tools-language/semantic/model/ast-builder.yaml +++ b/docs/reqstream/sysml2-tools-language/semantic/model/ast-builder.yaml @@ -38,3 +38,65 @@ sections: tests: - WorkspaceLoader_LoadAsync_SpecializesChain_Registered - WorkspaceLoader_LoadAsync_UnresolvedReference_ProducesWarning + + - id: SysML2Tools-Language-Semantic-Model-AstBuilder-ViewRenderTarget + title: >- + AstBuilder shall capture a view's render target reference text (from its + `render ;` member) as raw, unevaluated data on the corresponding + SysmlViewNode's RenderTargetName property. + justification: | + `RenderTargetName` names a rendering style/format, not content, so it is captured + verbatim only and never drives scoping or resolution. + tests: + - WorkspaceLoader_LoadAsync_ViewRenderTarget_CapturedRawNeverResolvedNoDiagnostic + - WorkspaceLoader_LoadAsync_ViewEmptyBody_AllNewFieldsNullOrEmpty + + - id: SysML2Tools-Language-Semantic-Model-AstBuilder-ViewFilterExpression + title: >- + AstBuilder shall capture a view's filter expression source text (from its + `filter [];` member) as raw, unevaluated data on the corresponding + SysmlViewNode's FilterExpressionText property. + justification: | + Full filter expression evaluation is deferred future work; capturing the raw source + text during AST construction preserves the information for a future capability + without requiring premature expression-evaluation logic in this unit. + tests: + - WorkspaceLoader_LoadAsync_ViewFilterExpression_CapturesTextVerbatimNoEdge + - WorkspaceLoader_LoadAsync_ViewEmptyBody_AllNewFieldsNullOrEmpty + + - id: SysML2Tools-Language-Semantic-Model-AstBuilder-ViewExposedNames + title: >- + AstBuilder shall capture the raw reference text of each `expose ;` member + (`view` usages only, in source order) on the corresponding SysmlViewNode's + ExposedNames property. + justification: | + Capturing this data during AST construction is the prerequisite for ReferenceResolver + to resolve the `expose` references into edges that GeneralViewLayoutStrategy uses for + scoping. `expose` is only valid grammar inside a `view` usage's body, not a `view def` + definition's body. + tests: + - WorkspaceLoader_LoadAsync_ViewUsageWithExpose_RecordsExposeEdge + - WorkspaceLoader_LoadAsync_ViewUsageWithBracketedFilterExpose_RecordsExposeEdge + - WorkspaceLoader_LoadAsync_ViewUsageWithPlainWildcardExpose_RecordsExposeEdge + - WorkspaceLoader_LoadAsync_ViewEmptyBody_AllNewFieldsNullOrEmpty + + - id: SysML2Tools-Language-Semantic-Model-AstBuilder-ViewUsageExpose + title: >- + AstBuilder shall build a SysmlViewNode from a named `view` usage (in addition to a + `view def` definition), capturing the same render/filter members plus its `expose` + members. + justification: | + `expose ;` is only valid grammar inside a `view` usage's body, not a `view def` + definition's body; without a dedicated VisitViewUsage override, named view usages + (including every `expose` statement) were never visited at all, so a view's exposed + names could never be captured, resolved, or scoped. This is also an intentional + capability addition beyond `expose` support alone: `VisitViewUsage` causes every named + `view` usage — whether or not it declares `expose` — to become its own renderable + top-level `SysmlViewNode` declaration, whereas previously only `view def` declarations + were ever visible to the render subsystem. For example, the OMG corpus fixture + `11b-SafetyAndSecurityFeatureViews.sysml` declares 2 `view def`s plus 3 named `view` + usages, so rendering it with no `--view` filter now produces 5 output files instead + of 2. + tests: + - WorkspaceLoader_LoadAsync_ViewUsageWithExpose_RecordsExposeEdge + - RenderSubsystem_OmgSafetyFeatureViewsCorpus_RendersAllNamedViewUsages diff --git a/docs/reqstream/sysml2-tools-language/semantic/model/reference-resolver.yaml b/docs/reqstream/sysml2-tools-language/semantic/model/reference-resolver.yaml index 859027a6..712fa961 100644 --- a/docs/reqstream/sysml2-tools-language/semantic/model/reference-resolver.yaml +++ b/docs/reqstream/sysml2-tools-language/semantic/model/reference-resolver.yaml @@ -26,3 +26,50 @@ sections: The cycle detection uses depth-first search with a traversal stack. tests: - WorkspaceLoader_LoadAsync_CircularImport_ProducesWarningNoInfiniteLoop + + - id: SysML2Tools-Language-Semantic-Model-ReferenceResolver-ViewExposeResolution + title: >- + ReferenceResolver.ResolveAll shall resolve each of a view's ExposedNames entries into + an Expose-kind edge. + justification: | + `expose ;` is the only reference a view's body statements resolve into model + content; resolving each ExposedNames entry into an Expose-kind edge is the + prerequisite for GeneralViewLayoutStrategy to scope a rendered diagram to that content. + tests: + - WorkspaceLoader_LoadAsync_ViewUsageWithExpose_RecordsExposeEdge + - WorkspaceLoader_LoadAsync_ViewUsageWithBracketedFilterExpose_RecordsExposeEdge + - WorkspaceLoader_LoadAsync_ViewUsageWithPlainWildcardExpose_RecordsExposeEdge + - WorkspaceLoader_LoadAsync_OmgSafetyFeatureViewsFixture_ResolvesBracketedExpose + - RenderSubsystem_ViewsWithDistinctExposeTargets_ProduceDifferingOutputsAndDiagnostic + + - id: SysML2Tools-Language-Semantic-Model-ReferenceResolver-ViewExposeUnresolvedWarning + title: >- + ReferenceResolver.ResolveAll shall produce an unresolved-reference Warning diagnostic + (with no corresponding edge) when a view's ExposedNames entry cannot resolve — + mirroring the existing SubjectName/EndpointA unresolved-reference diagnostic pattern. + justification: | + A bogus `expose ;` entry must be surfaced to the caller as model incompleteness, + the same way an unresolved supertype or import name is surfaced, so the diagnostic + signal for the render subsystem is genuinely useful. + tests: + - RenderSubsystem_ViewsWithDistinctExposeTargets_ProduceDifferingOutputsAndDiagnostic + + - id: SysML2Tools-Language-Semantic-Model-ReferenceResolver-ViewRenderFilterNeverResolved + title: >- + ReferenceResolver.ResolveAll shall never inspect or resolve a view's RenderTargetName + or FilterExpressionText: no edge is produced and no diagnostic is emitted for either, + regardless of content. + justification: | + RenderTargetName names a rendering style/format per the SysML v2 grammar (e.g. + `asTreeDiagram`, `asElementTable`), never model content — attempting to resolve it as a + reference could only ever produce a false "Unresolved reference" diagnostic or + coincidentally match an unrelated stdlib symbol of the same name (confirmed against the + OMG test corpus, where every `render` member names a rendering-style identifier). + FilterExpressionText is likewise raw source text, not a reference, and full filter + expression evaluation is deferred future work. Excluding both from resolution removes + that false-diagnostic risk while preserving the genuine unresolved-reference signal for + a bogus `expose` entry. + tests: + - WorkspaceLoader_LoadAsync_ViewRenderTarget_CapturedRawNeverResolvedNoDiagnostic + - WorkspaceLoader_LoadAsync_ViewFilterExpression_CapturesTextVerbatimNoEdge + - RenderSubsystem_OmgSafetyFeatureViewsCorpus_RendersAllNamedViewUsages diff --git a/docs/reqstream/sysml2-tools-language/semantic/model/sysml-node.yaml b/docs/reqstream/sysml2-tools-language/semantic/model/sysml-node.yaml index 2ea9204d..b8664419 100644 --- a/docs/reqstream/sysml2-tools-language/semantic/model/sysml-node.yaml +++ b/docs/reqstream/sysml2-tools-language/semantic/model/sysml-node.yaml @@ -28,3 +28,39 @@ sections: on element type without string comparisons. tests: - WorkspaceLoader_LoadAsync_PartDef_RegistersDefinition + + - id: SysML2Tools-Language-Semantic-Model-SysmlNode-ViewRenderTarget + title: >- + SysmlViewNode shall expose RenderTargetName: the raw reference text of its `render + ;` member, or null when absent. + justification: | + RenderTargetName is exposed as raw metadata only: per the SysML v2 grammar it names a + rendering style/format (e.g. `asTreeDiagram`, `asElementTable`), never content, so it + is captured but never resolved into an edge and has no effect on scope. + tests: + - WorkspaceLoader_LoadAsync_ViewRenderTarget_CapturedRawNeverResolvedNoDiagnostic + - WorkspaceLoader_LoadAsync_ViewEmptyBody_AllNewFieldsNullOrEmpty + + - id: SysML2Tools-Language-Semantic-Model-SysmlNode-ViewExposedNames + title: >- + SysmlViewNode shall expose ExposedNames: the raw reference text of each `expose + ;` member, empty when absent. + justification: | + Making a view's `expose` statements user-observable data is the prerequisite for + ReferenceResolver, which resolves ExposedNames into `Expose`-kind edges that + GeneralViewLayoutStrategy uses for scoping. + tests: + - WorkspaceLoader_LoadAsync_ViewUsageWithExpose_RecordsExposeEdge + - WorkspaceLoader_LoadAsync_ViewEmptyBody_AllNewFieldsNullOrEmpty + + - id: SysML2Tools-Language-Semantic-Model-SysmlNode-ViewFilterExpression + title: >- + SysmlViewNode shall expose FilterExpressionText: the raw source text of its `filter + [];` member, or null when absent. + justification: | + Making a view's declared filter expression user-observable data is required so + callers can observe it as raw source text; full filter expression evaluation is + deferred future work and is out of scope for this field. + tests: + - WorkspaceLoader_LoadAsync_ViewFilterExpression_CapturesTextVerbatimNoEdge + - WorkspaceLoader_LoadAsync_ViewEmptyBody_AllNewFieldsNullOrEmpty diff --git a/docs/user_guide/introduction.md b/docs/user_guide/introduction.md index 61fa20b3..1574e21d 100644 --- a/docs/user_guide/introduction.md +++ b/docs/user_guide/introduction.md @@ -108,6 +108,119 @@ sysml2tools render model.sysml --auto --output out --format svg | Multiple views, `--view ` | Render only the named view | | `--view ` names a view that does not exist | Error: lists available view names, exits non-zero | +## View Body Statements + +A `view def`/`view` declaration's body may contain `render ;` and `filter [];` +statements; a named `view` usage's body may additionally contain `expose ;` statements +(per the SysML v2 grammar, `expose` is only valid inside a `view` usage's body, not a `view def` +definition's body). For the General View strategy (the diagram produced when no more specialized +view kind applies), `expose` now scopes the rendered diagram instead of always rendering the +entire workspace: + +- `expose ;` (valid only inside a named `view` usage's body, not a `view def` + definition's body) — scopes the diagram to the union of every exposed name's containment + subtree: `` plus every declaration whose qualified name is `` or is contained + within it (a containment-subtree match, not just the exact element). If `` does not + resolve to any declaration in the workspace (for example, a typo), the tool falls back to + rendering the full workspace for that view — but now also reports a diagnostic identifying the + unresolved name, so the mistake is visible instead of silently rendering everything with no + signal. +- `render ;` — per the SysML v2 grammar, this names a rendering style/format (e.g. + `asTreeDiagram`, `asElementTable`) rather than content. It is captured but currently has **no + effect** on the rendered scope — see `ROADMAP.md` for the planned future capability to honor + it as a rendering-style selector. +- `filter [];` — the bracketed filter expression is parsed and captured, but **not yet + evaluated**: the resolved (`expose`) scope is rendered unfiltered, and a diagnostic reports + that the filter expression was parsed but not yet evaluated. Full filter-expression + evaluation is planned future work — see `ROADMAP.md`. +- A view with **no** `expose` statement (including the `--auto`-synthesized view) renders the + full workspace, exactly as before this scoping behavior was introduced. + +Only the General View layout strategy honors `expose` scoping in this release; the other view +kinds (Interconnection, State Transition, Action Flow, Sequence, Grid, Browser) continue to +render their full applicable scope regardless of a view's declared `expose` statements — see +`ROADMAP.md` for the planned follow-up extending scoping to those view kinds. + +Named `view Name { ... }` usages (not just `view def` declarations) are also now recognized as +their own renderable declarations: a workspace containing both `view def` declarations and named +`view` usages surfaces both kinds as views the `render` command discovers and renders. + +## Expose vs. Render: Worked Examples + +The three view body statements look similar but do very different jobs. This is a common point +of confusion, so it is worth stating plainly: + +> **`render ;` looks like it should select what's shown, but it does not — use +> `expose` for that.** + +| Statement | What it actually does | +| --- | --- | +| `expose ;` | The **only** mechanism scoping which model content appears in the diagram (see above). | +| `render ;` | Selects a rendering *style/format* (e.g. `asTreeDiagram`). Metadata only, no scoping. | +| `filter [];` | Captured as raw text only; not yet evaluated (see ROADMAP.md's filter-evaluation entry). | + +### Example A: exposing a definition to scope down to a subsystem + +```sysml +package Vehicle { + part def Engine { + part cylinder[4]; + } + part def Vehicle { + part engine : Engine; + part wheel[4]; + } + part myVehicle : Vehicle; + + view EngineOnlyView { + expose Engine; + render asTreeDiagram; + } +} +``` + +`EngineOnlyView` renders **only** the `Engine` definition's containment subtree (`Engine` and +its `cylinder` part) — `Vehicle`, `myVehicle`, and `wheel` are excluded entirely. Removing the +`expose Engine;` statement (leaving only `render asTreeDiagram;`, or an empty view body) renders +the **full workspace** instead: `render` never narrows the scope, only `expose` does. + +> **Note:** `expose` targets are qualified names (`::`-separated), not dotted member-access +> chains. `expose myVehicle.engine;` is a **syntax error**, not merely an unresolved reference — +> the grammar's `qualifiedName` rule does not accept `.`. To scope to a specific part *usage*, +> expose the usage itself by its own name (as in Example B below), not a dotted path into it. + +### Example B: exposing a usage vs. exposing a definition + +```sysml +package Vehicle { + part def Engine { + part cylinder[4]; + } + part def Vehicle { + part engine : Engine; + part wheel[4]; + } + part myVehicle : Vehicle; + + view UsageExposeView { + expose myVehicle; + render asTreeDiagram; + } +} +``` + +Here `expose myVehicle;` names a **usage** (`myVehicle : Vehicle`), not a `def`. The tool +resolves `myVehicle`'s own `Typing` edge to find the definition it is typed by (`Vehicle`), and +scopes the diagram to the union of `myVehicle`'s and `Vehicle`'s containment subtrees. The +rendered diagram therefore includes `Vehicle` (with its `engine` and `wheel` parts) and, because +`engine` is typed by `Engine`, the `Engine` definition (with its `cylinder` part) as well. + +Contrast this with `expose Vehicle;` (exposing the **definition** directly, as in Example A): +that scopes straight to `Vehicle`'s own containment subtree without needing to resolve any +`Typing` edge, since a definition's subtree is already the thing being scoped to. Exposing a +usage takes one extra hop — through the usage's type reference — to arrive at the same kind of +definition subtree that exposing a `def` reaches directly. + ## Depth Limiting Use `--depth ` to limit the nesting depth rendered. Parts beyond the limit are replaced diff --git a/docs/verification/sysml2-tools-core/layout/internal/general-view-layout-strategy.md b/docs/verification/sysml2-tools-core/layout/internal/general-view-layout-strategy.md index db704d46..7630d93b 100644 --- a/docs/verification/sysml2-tools-core/layout/internal/general-view-layout-strategy.md +++ b/docs/verification/sysml2-tools-core/layout/internal/general-view-layout-strategy.md @@ -39,6 +39,22 @@ configuration are required beyond a standard .NET SDK installation. delegated layout does not over-pad sparse diagrams. - Standard-library-only input (by prefix or by seed set) yields a minimal empty canvas. - An empty workspace yields a 200×100 canvas with no nodes. +- A view whose `ViewContext.ViewNode` carries a resolved `Expose` edge scopes the diagram to that + target's containment subtree, excluding unrelated sibling definitions and producing fewer boxes + than an unscoped (no-`ViewNode`) rendering of the same workspace. +- A view whose `ViewContext.ViewNode` carries a `RenderTargetName` but no resolved `Expose` edges + renders the full workspace, byte-identical to a view with a `null` `ViewNode` — proving + `RenderTargetName` never affects scope, regardless of `FilterExpressionText`. +- A view whose resolved `Expose` edge names a feature usage (not a definition) still renders that + usage's type's containment subtree, by additionally resolving the usage's own `Typing` edge — + the fix for the usage-vs-definition containment gap. +- A view whose `ViewContext.ViewNode` carries a non-null `FilterExpressionText` emits the "parsed + but not yet evaluated" warning through `LayoutTree.Warnings`, while still rendering the resolved + (unfiltered) scope. +- A view with a `null` `ViewContext.ViewNode` (the `--auto` synthesized-view path, and the + pre-scoping-change 2-argument `ViewContext` construction used throughout the rest of this test + file) renders every non-stdlib definition in the workspace, unchanged from before this feature — + the critical regression guard confirming full backward compatibility. ##### Test Scenarios @@ -78,3 +94,15 @@ configuration are required beyond a standard .NET SDK installation. Connected cross-referencing model keeps every definition box non-overlapping - `GeneralViewLayoutStrategy_BuildLayout_HeatLayout_SparseModelProducesCompactCanvas`: Sparse canvas stays compact with no warnings (no over-padding) +- `GeneralViewLayoutStrategy_BuildLayout_ExposedName_UnionsAdditionalSubtree`: + A resolved `Expose` edge scopes the diagram to the target's containment subtree, fewer boxes + than the full workspace +- `GeneralViewLayoutStrategy_BuildLayout_RenderTargetNameOnly_NoExposeEdges_RendersFullWorkspace`: + A `RenderTargetName` with no resolved `Expose` edges renders the full workspace unchanged +- `GeneralViewLayoutStrategy_BuildLayout_ExposedUsage_ResolvesThroughTypingToDefinitionSubtree`: + A resolved `Expose` edge naming a feature usage resolves through the usage's `Typing` edge to + include its type's containment subtree +- `GeneralViewLayoutStrategy_BuildLayout_FilterExpressionPresent_EmitsNotYetEvaluatedWarning`: + A non-null `FilterExpressionText` emits the "parsed but not yet evaluated" warning +- `GeneralViewLayoutStrategy_BuildLayout_NullViewNode_RendersFullWorkspaceUnchanged`: + A `null` `ViewNode` (`--auto`/default) renders every definition, unchanged (regression guard) diff --git a/docs/verification/sysml2-tools-core/layout/internal/layout-warnings.md b/docs/verification/sysml2-tools-core/layout/internal/layout-warnings.md index a4cee701..012e5fa9 100644 --- a/docs/verification/sysml2-tools-core/layout/internal/layout-warnings.md +++ b/docs/verification/sysml2-tools-core/layout/internal/layout-warnings.md @@ -3,8 +3,9 @@ ##### Verification Approach `LayoutWarnings` is verified through unit tests in `LayoutWarningsTests` that call `ForCrossings` -with a view name and a crossing count and assert on the returned list. The unit is a pure function, -so no mocking is required. +with a view name and a crossing count, and `ForUnevaluatedFilter` with a view name and a filter +expression text, asserting on the returned lists in each case. The unit is a pure function, so no +mocking is required. ##### Test Environment @@ -17,6 +18,9 @@ configuration are required beyond a standard .NET SDK installation. - A zero crossing count yields no warning. - A count of one yields a single singular-form warning naming the view. - A count greater than one yields a single plural-form warning reporting the count. +- A null filter expression text yields no warning. +- A non-null filter expression text yields a single warning naming the view and stating the + filter expression is parsed but not yet evaluated. ##### Test Scenarios @@ -25,3 +29,5 @@ configuration are required beyond a standard .NET SDK installation. | `ForCrossings_Zero_ReturnsEmpty` | Zero crossings yields an empty list | | `ForCrossings_One_ReturnsSingularWarning` | One crossing yields a singular warning naming the view | | `ForCrossings_Many_ReturnsPluralWarning` | Multiple crossings yield a plural warning with the count | +| `ForUnevaluatedFilter_NullText_ReturnsEmpty` | A null filter expression text yields an empty list | +| `ForUnevaluatedFilter_NonNullText_ReturnsNotYetEvaluatedWarning` | Non-null filter yields a warning naming the view | diff --git a/docs/verification/sysml2-tools-core/rendering.md b/docs/verification/sysml2-tools-core/rendering.md index 524993fc..9e1af3c9 100644 --- a/docs/verification/sysml2-tools-core/rendering.md +++ b/docs/verification/sysml2-tools-core/rendering.md @@ -32,6 +32,8 @@ All test inputs are constructed inline. No external network access or services a - `Themes.Print` is non-null and has `DepthFillColors.Count >= 1` and non-empty `StrokeColor`. - `new RenderOptions(Themes.Light)` has `Scale == 1.0`, `Dpi == 96.0`, `DepthLimit == 0`. - `RenderOutput` stores `SuggestedFileName`, `MediaType`, and `Data` as supplied. +- `ViewContext` constructed with only the required `ViewName`/`Workspace` arguments has a + `null` `ViewNode`; constructed with an explicit `SysmlViewNode` stores it unchanged. ### Test Scenarios @@ -96,3 +98,13 @@ all three properties are asserted to equal the supplied values. when a concrete `ILayoutStrategy` implementation is available. The scenario will construct a `ViewContext` with a minimal `SysmlWorkspace` and assert that `BuildLayout` returns a non-null `LayoutTree` with non-negative `Width` and `Height`. + +**ViewContext_Construction_StoresAllFields**: A `ViewContext` is constructed with only +`ViewName` and `Workspace`; both properties are asserted to equal the supplied values and +`ViewNode` is asserted to be `null`. + +**ViewContext_Construction_WithViewNode_StoresViewNode**: A `ViewContext` is constructed +with an explicit `SysmlViewNode` as the third argument; `ViewNode` is asserted to be the +same instance supplied. This confirms a layout strategy that reads `ViewContext.ViewNode` +(currently only `GeneralViewLayoutStrategy`) receives the view's resolved render/expose/ +filter data. diff --git a/docs/verification/sysml2-tools-core/rendering/diagram-renderer.md b/docs/verification/sysml2-tools-core/rendering/diagram-renderer.md index 8c659b02..431a7b37 100644 --- a/docs/verification/sysml2-tools-core/rendering/diagram-renderer.md +++ b/docs/verification/sysml2-tools-core/rendering/diagram-renderer.md @@ -20,6 +20,11 @@ external services are required. - Rendering a workspace with views produces one output per view, containing the rendered elements. - The same workspace renders successfully with both the SVG and PNG renderers. - A workspace with no views produces an empty result. +- `RenderWorkspace` threads each view's resolved `SysmlViewNode` into the `ViewContext` it + constructs, so `GeneralViewLayoutStrategy` can scope a rendered diagram to a view's declared + `expose` subject — verified end-to-end at the CLI level (see the `RenderCommand` + verification doc's `RenderSubsystem_ViewsWithDistinctExposeTargets_ProduceDifferingOutputsAndDiagnostic`) + and at the layout-strategy level (see the `GeneralViewLayoutStrategy` verification doc). #### Test Scenarios @@ -30,3 +35,4 @@ external services are required. | `DiagramRenderer_RenderWorkspace_GeneralViewModel_SvgContainsElementNames` | The SVG output contains element names | | `DiagramRenderer_RenderWorkspace_GeneralViewModel_PngProducesValidOutput` | The PNG output carries the PNG signature | | `DiagramRenderer_RenderWorkspace_NoViews_ReturnsEmptyList` | A view-free workspace yields an empty result | +| `RenderSubsystem_ViewsWithDistinctExposeTargets_ProduceDifferingOutputsAndDiagnostic` | Produces scoped output | diff --git a/docs/verification/sysml2-tools-language/semantic/model/ast-builder.md b/docs/verification/sysml2-tools-language/semantic/model/ast-builder.md index 489ebc74..e221b438 100644 --- a/docs/verification/sysml2-tools-language/semantic/model/ast-builder.md +++ b/docs/verification/sysml2-tools-language/semantic/model/ast-builder.md @@ -22,6 +22,13 @@ external services or additional configuration are required beyond a standard .NE - An element with only a short name `< shortName >` (no declared name) is not registered. - A definition with `specializes KnownType` produces a `SupertypeNames` entry that resolves without a Warning when `KnownType` is registered. +- `VisitViewDefinition` captures `render ;` and `filter [];` members' raw text on + the corresponding `SysmlViewNode`, and leaves both null for a view with an empty body. +- `VisitViewUsage` (a named `view` usage, not a `view def` definition) captures the same + render/filter members plus `expose ;` members, producing a `SysmlViewNode` with + populated `ExposedNames`. This also makes every named `view` usage its own renderable + declaration, an intentional capability addition beyond `expose` capture alone (see the + ast-builder design doc). ##### Test Scenarios @@ -31,3 +38,8 @@ external services or additional configuration are required beyond a standard .NE | Qualified name from namespace stack | `WorkspaceLoader_LoadAsync_NestedPackages_RegistersQualifiedNames` | | Definition registration | `WorkspaceLoader_LoadAsync_PartDef_RegistersDefinition` | | Supertype extraction | `WorkspaceLoader_LoadAsync_SpecializesChain_Registered` | +| `VisitViewDefinition` render | `WorkspaceLoader_LoadAsync_ViewRenderTarget_CapturedRawNeverResolvedNoDiagnostic` | +| `VisitViewDefinition` filter capture | `WorkspaceLoader_LoadAsync_ViewFilterExpression_CapturesTextVerbatimNoEdge` | +| `VisitViewUsage` expose capture | `WorkspaceLoader_LoadAsync_ViewUsageWithExpose_RecordsExposeEdge` | +| `VisitViewUsage` renderable declaration | `RenderSubsystem_OmgSafetyFeatureViewsCorpus_RendersAllNamedViewUsages` | +| Empty view body regression guard | `WorkspaceLoader_LoadAsync_ViewEmptyBody_AllNewFieldsNullOrEmpty` | diff --git a/docs/verification/sysml2-tools-language/semantic/model/reference-resolver.md b/docs/verification/sysml2-tools-language/semantic/model/reference-resolver.md index d6a2eb9e..e734d90f 100644 --- a/docs/verification/sysml2-tools-language/semantic/model/reference-resolver.md +++ b/docs/verification/sysml2-tools-language/semantic/model/reference-resolver.md @@ -27,6 +27,12 @@ external services or additional configuration are required beyond a standard .NE unresolved one produces a Warning diagnostic and no edge. - A resolved import reference (wildcard or named) is recorded as an `Import`-kind `SysmlEdge`; an unresolved import reference produces a Warning diagnostic without crashing. +- A view usage's resolved `expose ;` reference is recorded as an `Expose`-kind `SysmlEdge`; + an unresolved `expose` reference produces a Warning diagnostic naming the unresolved + identifier and no edge. A view's `render ;` member (a rendering style/format selector + per the SysML v2 grammar, never content) is captured on `SysmlViewNode.RenderTargetName` but + never inspected by `ReferenceResolver` — no edge is produced and no diagnostic is emitted for + it, even when the named identifier is not declared anywhere in the file. ##### Test Scenarios @@ -41,3 +47,6 @@ external services or additional configuration are required beyond a standard .NE | Wildcard import records edge | `WorkspaceLoader_LoadAsync_WildcardImport_RecordsImportEdge` | | Named import records edge | `WorkspaceLoader_LoadAsync_NamedImport_RecordsImportEdge` | | Unresolved import — Warning, no crash | `WorkspaceLoader_LoadAsync_UnresolvedImport_ProducesWarningNoCrash` | +| RenderTargetName captured raw | `WorkspaceLoader_LoadAsync_ViewRenderTarget_CapturedRawNeverResolvedNoDiagnostic` | +| Resolved expose name records edge | `WorkspaceLoader_LoadAsync_ViewUsageWithExpose_RecordsExposeEdge` | +| E2E diagnostic visibility | `RenderSubsystem_ViewsWithDistinctExposeTargets_ProduceDifferingOutputsAndDiagnostic` | diff --git a/docs/verification/sysml2-tools-language/semantic/model/sysml-edge.md b/docs/verification/sysml2-tools-language/semantic/model/sysml-edge.md index b3789939..73cd012c 100644 --- a/docs/verification/sysml2-tools-language/semantic/model/sysml-edge.md +++ b/docs/verification/sysml2-tools-language/semantic/model/sysml-edge.md @@ -20,6 +20,10 @@ external services or additional configuration are required beyond a standard .NE and the fully-qualified `TargetQualifiedName`. - A resolved feature typing reference produces a `SysmlEdge` with `Kind == SysmlEdgeKind.Typing`. - A resolved import reference produces a `SysmlEdge` with `Kind == SysmlEdgeKind.Import`. +- A resolved view-usage exposed-name reference produces a `SysmlEdge` with + `Kind == SysmlEdgeKind.Expose`; `SysmlEdgeKind.Render` no longer exists, since a view's + `render ;` member is never resolved into an edge (it names a rendering style/format, + not content). ##### Test Scenarios @@ -29,3 +33,5 @@ external services or additional configuration are required beyond a standard .NE | Typing edge recorded | `WorkspaceLoader_LoadAsync_ResolvedFeatureTyping_RecordsTypingEdge` | | Import edge recorded (wildcard) | `WorkspaceLoader_LoadAsync_WildcardImport_RecordsImportEdge` | | Import edge recorded (named) | `WorkspaceLoader_LoadAsync_NamedImport_RecordsImportEdge` | +| RenderTargetName never resolved | `WorkspaceLoader_LoadAsync_ViewRenderTarget_CapturedRawNeverResolvedNoDiagnostic` | +| Expose edge recorded | `WorkspaceLoader_LoadAsync_ViewUsageWithExpose_RecordsExposeEdge` | diff --git a/docs/verification/sysml2-tools-language/semantic/model/sysml-node.md b/docs/verification/sysml2-tools-language/semantic/model/sysml-node.md index 4b33aab5..9376e5ba 100644 --- a/docs/verification/sysml2-tools-language/semantic/model/sysml-node.md +++ b/docs/verification/sysml2-tools-language/semantic/model/sysml-node.md @@ -28,6 +28,11 @@ external services or additional configuration are required beyond a standard .NE - `SysmlNode.Annotations` is populated by `AstBuilder` with captured `comment`/`doc` text for a node whose body contains one or more annotating elements, and is empty (never null) for a node with none. +- `SysmlViewNode.RenderTargetName`/`FilterExpressionText`/`ExposedNames` are populated verbatim + from a view's `render`/`filter`/`expose` body members (raw reference/expression text, never + evaluated), and are `null`/empty for a view with no such members. `RenderTargetName` is + captured but never resolved into an edge or diagnostic (it names a rendering style/format, not + content); `ExposedNames` is the only field independently resolved by `ReferenceResolver`. ##### Test Scenarios @@ -38,3 +43,7 @@ external services or additional configuration are required beyond a standard .NE | `SupertypeNames` population | `WorkspaceLoader_LoadAsync_SpecializesChain_Registered` | | `ResolvedEdges` populated | `WorkspaceLoader_LoadAsync_ResolvedSupertype_RecordsSupertypeEdge` | | `Annotations` populated | `WorkspaceLoader_LoadAsync_CommentAndDocumentation_CapturesBothInSourceOrder` | +| `RenderTargetName` unresolved | `WorkspaceLoader_LoadAsync_ViewRenderTarget_CapturedRawNeverResolvedNoDiagnostic` | +| `FilterExpressionText` verbatim | `WorkspaceLoader_LoadAsync_ViewFilterExpression_CapturesTextVerbatimNoEdge` | +| `SysmlViewNode.ExposedNames` from a `view` usage | `WorkspaceLoader_LoadAsync_ViewUsageWithExpose_RecordsExposeEdge` | +| Empty view body leaves all fields null/empty | `WorkspaceLoader_LoadAsync_ViewEmptyBody_AllNewFieldsNullOrEmpty` | diff --git a/docs/verification/sysml2-tools-tool/render.md b/docs/verification/sysml2-tools-tool/render.md index b258097f..2a74eff6 100644 --- a/docs/verification/sysml2-tools-tool/render.md +++ b/docs/verification/sysml2-tools-tool/render.md @@ -39,6 +39,15 @@ on context output and exit code. File-writing scenarios use a temporary director - `render --help` prints render-specific usage and options (not the generic top-level command list), and is identical to `help render`'s output (see the Help subsystem verification document). +- A workspace with two views — one with an `expose ;` statement naming a resolvable + target, one with a bogus `expose thisIdentifierDoesNotExistAnywhere;` statement — produces + two output files whose content DIFFERS, and the bogus view's unresolved exposed name is + visible as a diagnostic in the captured log output. +- Rendering the real OMG corpus fixture `11b-SafetyAndSecurityFeatureViews.sysml` with no + `--view` filter produces exactly 5 output files (2 `view def`s plus 3 named `view` usages), + regression-guarding the `VisitViewUsage` capability addition, with no false + "Unresolved reference" diagnostic for its `render asTreeDiagram;`/`render asElementTable;` + rendering-style members. ### Test Scenarios @@ -115,6 +124,31 @@ Verifies that `render --help` prints the render-specific usage line and its `--o regression-proofing test added alongside the `help` command's command-aware `--help` dispatch (see `docs/design/sysml2-tools-tool/help.md`). +#### RenderSubsystem_ViewsWithDistinctExposeTargets_ProduceDifferingOutputsAndDiagnostic + +End-to-end regression test: a workspace declares two `view` usages — one with `expose TargetA;` +(resolving to a `part def` with a nested child), one with `expose +thisIdentifierDoesNotExistAnywhere;` (an unresolvable target). Verifies that rendering both +without `--view` produces `ViewValid.svg` and `ViewBogus.svg` whose content DIFFERS (a view with +no resolved `Expose` edges renders the full workspace, while the valid view's sole `Expose` entry +scopes to that target's subtree), and that the captured `--log` output contains +`"thisIdentifierDoesNotExistAnywhere"` — the unresolved-reference diagnostic surfaced by +`ReferenceResolver` for the bogus exposed name. `render ;` plays no role in scoping — +only `expose` does, per the corrected semantics. + +#### RenderSubsystem_OmgSafetyFeatureViewsCorpus_RendersAllNamedViewUsages + +Regression guard for the `AstBuilder.VisitViewUsage` capability addition: loads and renders the +real OMG corpus fixture +`test/SysMLModels/OMG/validation/11-ViewAndViewpoint/11b-SafetyAndSecurityFeatureViews.sysml` +with no `--view` filter, asserting exactly 5 output files are produced — the 2 `view def` +declarations (`SafetyFeatureView`, `SafetyOrSecurityFeatureView`) plus the 3 named `view` usages +(`vehicleSafetyFeatureView`, `vehicleMandatorySafetyFeatureView`, +`vehicleMandatorySafetyFeatureViewStandalone`), not just the 2 `view def`s that were the only +renderable declarations before `VisitViewUsage` was added. Also asserts the captured `--log` +output contains no `"asTreeDiagram"`/`"asElementTable"` text, confirming those rendering-style +`render` members never surface a false unresolved-reference diagnostic. + #### ResxResource_EveryKey_ResolvesToNonEmptyText / ResxResource_KeysAndAccessorProperties_AreInBidirectionalParity (ResxResourceTests.cs) For the `RenderStrings` resource base name/accessor pair (one of four covered by these theory diff --git a/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/GeneralViewLayoutStrategy.cs b/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/GeneralViewLayoutStrategy.cs index 3f48fb4a..1e88b7c6 100644 --- a/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/GeneralViewLayoutStrategy.cs +++ b/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/GeneralViewLayoutStrategy.cs @@ -108,8 +108,17 @@ public LayoutTree BuildLayout(ViewContext context, RenderOptions options) var theme = options.Theme; - // Collect all user-defined definitions, sized for rendering. - var defs = CollectDefinitions(context.Workspace, theme); + // Resolve the view's exposed-name scope (the union of each resolved Expose edge's + // containment subtree), or null when the view has no resolved Expose edges — including a + // null ViewNode (the --auto synthetic-view path), a view with no `expose` statement, and a + // view whose every `expose` entry failed to resolve. A null scope means "render + // everything", byte-identical to the pre-scoping behavior. RenderTargetName and + // FilterExpressionText never affect this decision. + var scope = ResolveExposedScope(context.Workspace, context.ViewNode); + + // Collect all user-defined definitions, sized for rendering, restricted to the resolved + // scope when one applies. + var defs = CollectDefinitions(context.Workspace, theme, scope); if (defs.Count == 0) { return new LayoutTree(200.0, 100.0, []); @@ -136,14 +145,93 @@ public LayoutTree BuildLayout(ViewContext context, RenderOptions options) // Stamp the "+N more…" ellipsis label onto each truncated folder's placed box. The leaf // algorithm emits one box per root node in Nodes order, so the boxes portion of the placed // tree aligns with graph.Nodes by index. - return truncated.Count == 0 ? tree : DecorateTruncatedFolders(tree, graph, truncated, theme); + var placed = truncated.Count == 0 ? tree : DecorateTruncatedFolders(tree, graph, truncated, theme); + + // A `filter [];` statement is parsed (SysmlViewNode.FilterExpressionText) but not + // yet evaluated — full expression evaluation is deferred future work (see ROADMAP.md). + // Surface this to the caller through the standard layout-warnings channel rather than + // silently rendering a diagram the user may believe is filtered. + var warnings = LayoutWarnings.ForUnevaluatedFilter(context.ViewName, context.ViewNode?.FilterExpressionText); + return warnings.Count == 0 ? placed : placed with { Warnings = warnings }; } + /// + /// Resolves the qualified-name containment-subtree scope a view's expose statements + /// restrict the diagram to, or when the view has no resolved + /// edge — meaning every non-stdlib definition is included, + /// unchanged from the pre-scoping behavior. This covers every "no scoping" case uniformly: a + /// null (the --auto synthetic view, which never carries + /// expose/render/filter data), a view with no expose statement, and a view whose every + /// expose entry failed to resolve. RenderTargetName (a rendering-style/format + /// selector, not content) and FilterExpressionText never affect this decision. + /// + /// + /// When an exposed target resolves to a (a usage, e.g. + /// part myVehicle : Vehicle;) rather than a , the + /// usage's own containment subtree is typically empty — the real content lives under its + /// type's subtree. To avoid silently scoping to nothing, this also resolves the usage's own + /// edge (if any) and adds that type's qualified name to the + /// scope as well, so both the usage and its type's subtree are included. + /// + /// The workspace, used to look up each exposed target's declaration. + /// The view's AST node, or null for the synthetic --auto view. + /// + /// The list of subject qualified names (each exposed name, plus the resolved type of any + /// exposed name that names a usage) whose containment subtrees are in scope, or null when no + /// scoping applies. + /// + private static IReadOnlyList? ResolveExposedScope(SysmlWorkspace workspace, SysmlViewNode? viewNode) + { + var exposedTargets = viewNode?.ResolvedEdges + .Where(edge => edge.Kind == SysmlEdgeKind.Expose) + .Select(edge => edge.TargetQualifiedName) + .ToList(); + if (exposedTargets is not { Count: > 0 }) + { + return null; + } + + var subjects = new List(); + foreach (var target in exposedTargets) + { + subjects.Add(target); + + if (workspace.Declarations.TryGetValue(target, out var declaration) && + declaration is SysmlFeatureNode { } feature) + { + var typeTarget = feature.ResolvedEdges + .FirstOrDefault(edge => edge.Kind == SysmlEdgeKind.Typing) + ?.TargetQualifiedName; + if (typeTarget is not null) + { + subjects.Add(typeTarget); + } + } + } + + return subjects; + } + + /// + /// Returns when is one of + /// or lies within one of their containment subtrees (a + /// "{subject}::" prefix match), reusing the same qualified-name-prefix idiom + /// already uses for + /// stdlib-prefix matching. + /// + private static bool IsInSubjectScope(string qualifiedName, IReadOnlyList subjects) => + subjects.Any(subject => + qualifiedName == subject || qualifiedName.StartsWith(subject + "::", StringComparison.Ordinal)); + /// /// Collects every user-defined from the workspace and computes - /// each box's intrinsic size from its keyword and name. + /// each box's intrinsic size from its keyword and name, restricted to + /// when non-null (the view's resolved expose containment subtrees). /// - private static IReadOnlyList CollectDefinitions(SysmlWorkspace workspace, Theme theme) + private static IReadOnlyList CollectDefinitions( + SysmlWorkspace workspace, + Theme theme, + IReadOnlyList? scope) { var result = new List(); @@ -159,6 +247,11 @@ private static IReadOnlyList CollectDefinitions(SysmlWorkspace workspace continue; } + if (scope is not null && !IsInSubjectScope(qualifiedName, scope)) + { + continue; + } + var simpleName = def.Name ?? qualifiedName; var keyword = string.IsNullOrEmpty(def.DefinitionKeyword) ? "def" : def.DefinitionKeyword; diff --git a/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/LayoutWarnings.cs b/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/LayoutWarnings.cs index c7c57aa5..fc0782e8 100644 --- a/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/LayoutWarnings.cs +++ b/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/LayoutWarnings.cs @@ -35,4 +35,29 @@ public static IReadOnlyList ForCrossings(string viewName, int crossings) "the diagram may be cluttered.", ]; } + + /// + /// Returns a single-element warning list stating that a view's filter [<expr>]; + /// statement was parsed but not evaluated, or an empty list when the view declares no filter + /// expression. + /// + /// Name of the view being laid out. + /// + /// The view's raw filter expression source text, or when the view + /// declares no filter member. + /// + /// The warning messages for the view. + public static IReadOnlyList ForUnevaluatedFilter(string viewName, string? filterExpressionText) + { + if (filterExpressionText is null) + { + return []; + } + + return + [ + $"View '{viewName}' declares a filter expression, which is parsed but not yet " + + "evaluated; all elements in the resolved scope are rendered unfiltered.", + ]; + } } diff --git a/src/DemaConsulting.SysML2Tools.Core/Rendering/DiagramRenderer.cs b/src/DemaConsulting.SysML2Tools.Core/Rendering/DiagramRenderer.cs index d734f54f..0d97172e 100644 --- a/src/DemaConsulting.SysML2Tools.Core/Rendering/DiagramRenderer.cs +++ b/src/DemaConsulting.SysML2Tools.Core/Rendering/DiagramRenderer.cs @@ -197,7 +197,7 @@ public IReadOnlyList RenderWorkspace( continue; } - var context = new ViewContext(viewName, workspace); + var context = new ViewContext(viewName, workspace, viewNode); // Build the layout tree for this view var layout = strategy.BuildLayout(context, options); diff --git a/src/DemaConsulting.SysML2Tools.Core/Rendering/ILayoutStrategy.cs b/src/DemaConsulting.SysML2Tools.Core/Rendering/ILayoutStrategy.cs index 0004c443..9770aa7b 100644 --- a/src/DemaConsulting.SysML2Tools.Core/Rendering/ILayoutStrategy.cs +++ b/src/DemaConsulting.SysML2Tools.Core/Rendering/ILayoutStrategy.cs @@ -5,6 +5,7 @@ using DemaConsulting.Rendering; using DemaConsulting.Rendering.Abstractions; using DemaConsulting.SysML2Tools.Semantic; +using DemaConsulting.SysML2Tools.Semantic.Model; namespace DemaConsulting.SysML2Tools.Rendering; @@ -21,9 +22,17 @@ namespace DemaConsulting.SysML2Tools.Rendering; /// resolve the view's target element(s) and traverse related declarations while building the /// layout tree. /// +/// +/// The resolved the view was declared from, carrying its resolved +/// Expose edges and raw FilterExpressionText so a strategy can scope +/// its diagram accordingly. Nullable to preserve the --auto synthetic-view path, whose +/// synthesized carries no render/expose/filter data; defaults to +/// so existing two-argument construction call sites remain unchanged. +/// public sealed record ViewContext( string ViewName, - SysmlWorkspace Workspace); + SysmlWorkspace Workspace, + SysmlViewNode? ViewNode = null); /// /// Computes a from a . diff --git a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs index dd4aacc0..f21a8805 100644 --- a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs +++ b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs @@ -797,14 +797,155 @@ private static (string? A, string? B) ExtractConnectorEnds(SysMLv2Parser.Connect var supertypeNames = GetSubclassificationSupertypes( context.definitionDeclaration()?.subclassificationPart()); + var (renderTargetName, filterExpressionText) = + ExtractViewRenderAndFilter(context.viewDefinitionBody()?.viewDefinitionBodyItem() ?? []); + return new SysmlViewNode { Name = name, QualifiedName = qualifiedName, SupertypeNames = supertypeNames, + RenderTargetName = renderTargetName, + FilterExpressionText = filterExpressionText, }; } + /// + /// Builds a from a view usage (as opposed to a + /// view def definition), capturing the same render/filter members as + /// plus expose members — the usage form's only + /// grammar addition over the definition form's body. + /// + /// + /// Unnamed view usages (no declared name) are not registered as symbols and are skipped, + /// mirroring the existing anonymous-element convention used by + /// and other usage visitors. + /// + public override SysmlNode? VisitViewUsage(SysMLv2Parser.ViewUsageContext context) + { + var name = GetDeclaredName(context.usageDeclaration()?.identification()); + if (name is null) + { + return null; + } + + var qualifiedName = QualifyName(name); + var bodyItems = context.viewBody()?.viewBodyItem() ?? []; + + var (renderTargetName, filterExpressionText) = ExtractViewRenderAndFilter(bodyItems); + var exposedNames = ExtractExposedNames(bodyItems); + + return new SysmlViewNode + { + Name = name, + QualifiedName = qualifiedName, + RenderTargetName = renderTargetName, + ExposedNames = exposedNames, + FilterExpressionText = filterExpressionText, + }; + } + + /// + /// Scans a view's body items for a render <target>; member and a + /// filter [<expression>]; member, returning the raw reference text and raw + /// expression source text respectively (or for either when absent). + /// Shared by (viewDefinitionBodyItem) and + /// (viewBodyItem) since both context types expose + /// identically-shaped viewRenderingMember()/elementFilterMember() accessors. + /// The first render member wins if more than one appears — SysML disallows more + /// than one render subject per view, so this is a defensive tie-break, not a validated + /// constraint enforced by this tool. + /// + private static (string? RenderTargetName, string? FilterExpressionText) ExtractViewRenderAndFilter( + IEnumerable bodyItems) + where TItem : Antlr4.Runtime.ParserRuleContext + { + string? renderTargetName = null; + string? filterExpressionText = null; + + foreach (var item in bodyItems) + { + if (renderTargetName is null && + GetViewRenderingMember(item) is { } renderingMember) + { + renderTargetName = ExtractRenderTargetName(renderingMember.viewRenderingUsage()); + } + + if (filterExpressionText is null && + GetElementFilterMember(item) is { } filterMember) + { + filterExpressionText = filterMember.ownedExpression()?.GetText(); + } + } + + return (renderTargetName, filterExpressionText); + } + + /// Extracts the viewRenderingMember() accessor common to both view body item types. + private static SysMLv2Parser.ViewRenderingMemberContext? GetViewRenderingMember(Antlr4.Runtime.ParserRuleContext item) => + item switch + { + SysMLv2Parser.ViewDefinitionBodyItemContext defItem => defItem.viewRenderingMember(), + SysMLv2Parser.ViewBodyItemContext usageItem => usageItem.viewRenderingMember(), + _ => null, + }; + + /// Extracts the elementFilterMember() accessor common to both view body item types. + private static SysMLv2Parser.ElementFilterMemberContext? GetElementFilterMember(Antlr4.Runtime.ParserRuleContext item) => + item switch + { + SysMLv2Parser.ViewDefinitionBodyItemContext defItem => defItem.elementFilterMember(), + SysMLv2Parser.ViewBodyItemContext usageItem => usageItem.elementFilterMember(), + _ => null, + }; + + /// + /// Extracts the raw reference text of a render <target>; statement, preferring + /// the direct-reference form (ownedReferenceSubsetting) and falling back to the + /// typed-placeholder form's feature typing — the same two-form fallback pattern + /// uses for satisfy's two grammar forms. + /// + private static string? ExtractRenderTargetName(SysMLv2Parser.ViewRenderingUsageContext? usage) + { + if (usage is null) + { + return null; + } + + return usage.ownedReferenceSubsetting()?.GetText() + ?? ExtractFeatureTyping(usage.featureSpecializationPart()) + ?? usage.usage()?.GetText(); + } + + /// + /// Collects the raw reference text of every expose <name>; member in a + /// view usage's body, in source order, reusing — + /// the same namespace/membership-import shape import statements use. + /// + private static IReadOnlyList ExtractExposedNames( + IEnumerable bodyItems) + { + var names = new List(); + foreach (var item in bodyItems) + { + var expose = item.expose(); + if (expose is null) + { + continue; + } + + var (qn, _) = ExtractImportTarget( + expose.namespaceExpose()?.namespaceImport(), + expose.membershipExpose()?.membershipImport()); + if (qn is { Length: > 0 }) + { + names.Add(qn); + } + } + + return names; + } + /// public override SysmlNode? VisitViewpointDefinition(SysMLv2Parser.ViewpointDefinitionContext context) { @@ -835,40 +976,91 @@ private static (string? A, string? B) ExtractConnectorEnds(SysMLv2Parser.Connect return null; } + var (qn, isWildcard) = ExtractImportTarget(decl.namespaceImport(), decl.membershipImport()); + if (qn is null) + { + return null; + } + + return new SysmlImportNode + { + ImportedNamespace = qn, + ImportedNames = [qn], + IsWildcard = isWildcard, + }; + } + + /// + /// Extracts the qualified/dotted name text and wildcard flag from either an import's + /// namespace form (qualifiedName::*, a wildcard by definition) or membership form + /// (qualifiedName, optionally ::** for a recursive wildcard). Shared by + /// 's import handling and 's + /// expose handling, since both grammar constructs wrap the identical + /// namespaceImport/membershipImport shapes — extracted here per the + /// Copy-Paste Programming anti-pattern in coding-principles.md rather than duplicating the + /// extraction logic at both call sites. + /// + /// The wildcard-import alternative, or null when not this form. + /// The membership-import alternative, or null when not this form. + /// + /// The extracted qualified/dotted name text (or null when neither alternative yielded + /// text) and whether the import/expose is a wildcard. + /// + private static (string? QualifiedName, bool IsWildcard) ExtractImportTarget( + SysMLv2Parser.NamespaceImportContext? namespaceImport, + SysMLv2Parser.MembershipImportContext? membershipImport) + { // Namespace import: qualifiedName::* — wildcard, all members of the namespace are in scope - var nsImport = decl.namespaceImport(); - if (nsImport is not null) + if (namespaceImport is not null) { - var qn = nsImport.qualifiedName()?.GetText(); + var qn = namespaceImport.qualifiedName()?.GetText(); if (qn is { Length: > 0 }) { - return new SysmlImportNode + return (qn, true); + } + + // Bracketed-filter form: qualifiedName::**[filterExpr] — the dominant expose form in + // the real OMG corpus. The grammar nests the qualified name two levels deeper here: + // namespaceImport -> filterPackage -> filterPackageImportDeclaration -> (membershipImport + // | namespaceImportDirect). Descend through that chain rather than only checking the + // direct qualifiedName() child (which is null for this alternative). + var filterDecl = namespaceImport.filterPackage()?.filterPackageImportDeclaration(); + if (filterDecl is not null) + { + var filterMembershipImport = filterDecl.membershipImport(); + if (filterMembershipImport is not null) { - ImportedNamespace = qn, - ImportedNames = [qn], - IsWildcard = true, - }; + var filterQn = filterMembershipImport.qualifiedName()?.GetText(); + if (filterQn is { Length: > 0 }) + { + return (filterQn, filterMembershipImport.STAR_STAR() is not null); + } + } + + var namespaceImportDirect = filterDecl.namespaceImportDirect(); + if (namespaceImportDirect is not null) + { + var directQn = namespaceImportDirect.qualifiedName()?.GetText(); + if (directQn is { Length: > 0 }) + { + return (directQn, true); + } + } } } // Membership import: qualifiedName (optional ::**) // The ** form is a recursive wildcard; either way it enables lookup under the namespace - var memImport = decl.membershipImport(); - if (memImport is not null) + if (membershipImport is not null) { - var qn = memImport.qualifiedName()?.GetText(); + var qn = membershipImport.qualifiedName()?.GetText(); if (qn is { Length: > 0 }) { - return new SysmlImportNode - { - ImportedNamespace = qn, - ImportedNames = [qn], - IsWildcard = memImport.STAR_STAR() is not null, - }; + return (qn, membershipImport.STAR_STAR() is not null); } } - return null; + return (null, false); } /// diff --git a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/ReferenceResolver.cs b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/ReferenceResolver.cs index d40bfb8c..d0931be0 100644 --- a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/ReferenceResolver.cs +++ b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/ReferenceResolver.cs @@ -534,6 +534,32 @@ private void ResolveNode( } } + // Views resolve only ExposedNames (a view's expose members) into Expose edges; each entry + // is resolved independently, and an unresolved entry produces the Warning diagnostic below + // with no edge. RenderTargetName (a view's render member) names a rendering style or + // format, not a content-scoping subject -- per the SysML v2 grammar it never refers to + // model content, so ReferenceResolver never inspects it: no edge is produced and no + // diagnostic is emitted for it, mirroring how FilterExpressionText (raw source text, not a + // reference) is also intentionally never touched here. + if (node is SysmlViewNode view) + { + foreach (var exposedName in view.ExposedNames) + { + if (TryResolve(exposedName, namespaceStack, imports, out var resolvedExposed)) + { + nodeEdges.Add(new SysmlEdge(node.QualifiedName, resolvedExposed, SysmlEdgeKind.Expose)); + } + else if (resolvedInFile.Add(exposedName)) + { + _diagnostics.Add(new SysmlDiagnostic( + filePath, + 0, 0, + DiagnosticSeverity.Warning, + $"Unresolved reference: '{exposedName}'")); + } + } + } + if (nodeEdges.Count > 0) { node.ResolvedEdges = nodeEdges; diff --git a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlEdge.cs b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlEdge.cs index 85538a42..184e2e43 100644 --- a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlEdge.cs +++ b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlEdge.cs @@ -65,6 +65,18 @@ public enum SysmlEdgeKind /// state to walk from) produces no edge, a documented limitation of this unit. /// Transition, + + /// + /// A view expose reference (expose <name>; nested in a view's body), from + /// the view to a resolved element whose containment subtree is included in the rendered + /// scope. One edge is recorded per resolvable entry in + /// ; an unresolved entry produces an + /// unresolved-reference diagnostic instead. GeneralViewLayoutStrategy scopes its + /// diagram to the union of every edge's target containment subtree; + /// a view with no edges renders the full workspace, unchanged from + /// the pre-scoping baseline. + /// + Expose, } /// diff --git a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlNode.cs b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlNode.cs index 008aa7ff..5bfcadf8 100644 --- a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlNode.cs +++ b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlNode.cs @@ -167,7 +167,7 @@ public sealed class SysmlImportNode : SysmlNode } /// -/// AST node representing a view definition. +/// AST node representing a view definition or view usage. /// /// /// Inherited from SysmlNode: Name, QualifiedName, Children, SupertypeNames, ImportedNames, @@ -175,6 +175,38 @@ public sealed class SysmlImportNode : SysmlNode /// public sealed class SysmlViewNode : SysmlNode { + /// + /// Gets the raw reference text of this view's render <target>; statement + /// (from viewRenderingMember().viewRenderingUsage()), or + /// when the view declares no rendering member. Per the SysML v2 grammar, this names a + /// rendering style/format usage (e.g. asTreeDiagram, asElementTable) — never + /// a content-scoping subject. Captured verbatim only: never + /// inspects or resolves this value (no edge is produced, no diagnostic is emitted), and it + /// has no effect on GeneralViewLayoutStrategy's rendered scope. Reserved for a + /// possible future capability that selects among rendering-style strategies — see the + /// project ROADMAP. + /// + public string? RenderTargetName { get; init; } + + /// + /// Gets the raw reference text of each expose <name>; member nested in this + /// view's body, in source order. Empty when the view declares no expose members. + /// Each entry is resolved by into a + /// edge when it resolves, or an unresolved-reference + /// diagnostic when it does not. + /// + public IReadOnlyList ExposedNames { get; init; } = Array.Empty(); + + /// + /// Gets the raw source text of this view's filter [<expression>]; statement + /// (from elementFilterMember().ownedExpression().GetText()), or + /// when the view declares no filter member. Captured verbatim only + /// — no expression tree is built and no evaluation is performed; a non-null value causes + /// GeneralViewLayoutStrategy to emit a "not yet evaluated" warning while still + /// rendering the (unfiltered) resolved scope. Full filter-expression evaluation is + /// deferred future work — see the project ROADMAP. + /// + public string? FilterExpressionText { get; init; } } /// diff --git a/src/DemaConsulting.SysML2Tools.Stdlib/Resources/stdlib.json.gz b/src/DemaConsulting.SysML2Tools.Stdlib/Resources/stdlib.json.gz index 634cde8f..f857ad42 100644 Binary files a/src/DemaConsulting.SysML2Tools.Stdlib/Resources/stdlib.json.gz and b/src/DemaConsulting.SysML2Tools.Stdlib/Resources/stdlib.json.gz differ diff --git a/test/DemaConsulting.SysML2Tools.Tests/Layout/GeneralViewLayoutStrategyTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Layout/GeneralViewLayoutStrategyTests.cs index e853f61b..03851be4 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Layout/GeneralViewLayoutStrategyTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Layout/GeneralViewLayoutStrategyTests.cs @@ -8,6 +8,7 @@ using DemaConsulting.SysML2Tools.Rendering; using DemaConsulting.SysML2Tools.Semantic; using DemaConsulting.SysML2Tools.Semantic.Model; +using DemaConsulting.SysML2Tools.Stdlib; namespace DemaConsulting.SysML2Tools.Tests.Layout; @@ -756,6 +757,297 @@ public void GeneralViewLayoutStrategy_BuildLayout_HeatLayout_SparseModelProduces $"Sparse canvas height {layout.Height} should be below 500px (no over-padding)"); } + /// + /// Builds the fixed three-definition workspace shared by the expose-scoping tests: + /// Root::A (an expose target), Root::A::Child (inside A's containment + /// subtree, qualified name prefixed "Root::A::"), and Root::B (an unrelated + /// sibling definition, outside A's subtree). + /// + private static SysmlWorkspace BuildScopingWorkspace() => new() + { + Declarations = new Dictionary + { + ["Root::A"] = new SysmlDefinitionNode { Name = "A", QualifiedName = "Root::A", DefinitionKeyword = "part def" }, + ["Root::A::Child"] = new SysmlDefinitionNode { Name = "Child", QualifiedName = "Root::A::Child", DefinitionKeyword = "part def" }, + ["Root::B"] = new SysmlDefinitionNode { Name = "B", QualifiedName = "Root::B", DefinitionKeyword = "part def" } + } + }; + + /// + /// Builds a workspace modeling the usage-vs-definition containment gap fix: a + /// Root::Vehicle definition with an owned child Root::Vehicle::Engine, and a + /// Root::myVehicle feature usage typed by Vehicle (a resolved Typing + /// edge to Root::Vehicle) plus an unrelated sibling Root::Other. + /// + private static SysmlWorkspace BuildUsageTypingWorkspace() => new() + { + Declarations = new Dictionary + { + ["Root::Vehicle"] = new SysmlDefinitionNode { Name = "Vehicle", QualifiedName = "Root::Vehicle", DefinitionKeyword = "part def" }, + ["Root::Vehicle::Engine"] = new SysmlDefinitionNode { Name = "Engine", QualifiedName = "Root::Vehicle::Engine", DefinitionKeyword = "part def" }, + ["Root::myVehicle"] = new SysmlFeatureNode + { + Name = "myVehicle", + QualifiedName = "Root::myVehicle", + FeatureTyping = "Vehicle", + ResolvedEdges = [new SysmlEdge("Root::myVehicle", "Root::Vehicle", SysmlEdgeKind.Typing)] + }, + ["Root::Other"] = new SysmlDefinitionNode { Name = "Other", QualifiedName = "Root::Other", DefinitionKeyword = "part def" } + } + }; + + /// + /// A view with a resolved Expose edge to Root::A scopes the diagram to + /// Root::A plus its containment subtree (Root::A::Child), excluding the + /// unrelated sibling Root::B — producing fewer boxes than rendering the full + /// workspace. + /// + [Fact] + public void GeneralViewLayoutStrategy_BuildLayout_ExposedName_UnionsAdditionalSubtree() + { + // Arrange: a view exposing only Root::A + var strategy = new GeneralViewLayoutStrategy(); + var workspace = BuildScopingWorkspace(); + var viewNode = new SysmlViewNode + { + Name = "V", + QualifiedName = "Root::V", + ExposedNames = ["A"], + ResolvedEdges = [new SysmlEdge("Root::V", "Root::A", SysmlEdgeKind.Expose)] + }; + var context = new ViewContext("v", workspace, viewNode); + var options = new RenderOptions(Themes.Light); + + // Act + var scoped = strategy.BuildLayout(context, options); + var full = strategy.BuildLayout(new ViewContext("full", workspace), options); + + // Assert: the scoped view contains A and A::Child but not B, and has fewer boxes than + // the unscoped full-workspace rendering. + var labels = CollectBoxes(scoped.Nodes).Select(b => b.Label).ToList(); + Assert.Contains("A", labels); + Assert.Contains("Child", labels); + Assert.DoesNotContain("B", labels); + Assert.True(CollectBoxes(scoped.Nodes).Count < CollectBoxes(full.Nodes).Count); + } + + /// + /// A view whose RenderTargetName is present but has no Expose edges renders + /// the full workspace, byte-identical (same box count/labels) to the null-ViewNode + /// case — proving RenderTargetName never affects scope, since it names a rendering + /// style/format, not content. + /// + [Fact] + public void GeneralViewLayoutStrategy_BuildLayout_RenderTargetNameOnly_NoExposeEdges_RendersFullWorkspace() + { + // Arrange: a view with a RenderTargetName but no resolved Expose edges + var strategy = new GeneralViewLayoutStrategy(); + var workspace = BuildScopingWorkspace(); + var viewNode = new SysmlViewNode + { + Name = "V", + QualifiedName = "Root::V", + RenderTargetName = "asTreeDiagram", + ResolvedEdges = [] + }; + var context = new ViewContext("v", workspace, viewNode); + var options = new RenderOptions(Themes.Light); + + // Act + var inert = strategy.BuildLayout(context, options); + var full = strategy.BuildLayout(new ViewContext("full", workspace), options); + + // Assert: identical box count/labels to the full-workspace (no-ViewNode) rendering. + var inertLabels = CollectBoxes(inert.Nodes).Select(b => b.Label).OrderBy(l => l).ToList(); + var fullLabels = CollectBoxes(full.Nodes).Select(b => b.Label).OrderBy(l => l).ToList(); + Assert.Equal(fullLabels, inertLabels); + } + + /// + /// A view whose Expose edge resolves to a feature usage (not a definition) still + /// renders that usage's type's containment subtree, by additionally resolving the usage's + /// own Typing edge — the fix for the usage-vs-definition containment gap. Excludes + /// the unrelated sibling Root::Other. + /// + [Fact] + public void GeneralViewLayoutStrategy_BuildLayout_ExposedUsage_ResolvesThroughTypingToDefinitionSubtree() + { + // Arrange: a view exposing Root::myVehicle, a usage typed by Root::Vehicle + var strategy = new GeneralViewLayoutStrategy(); + var workspace = BuildUsageTypingWorkspace(); + var viewNode = new SysmlViewNode + { + Name = "V", + QualifiedName = "Root::V", + ExposedNames = ["myVehicle"], + ResolvedEdges = [new SysmlEdge("Root::V", "Root::myVehicle", SysmlEdgeKind.Expose)] + }; + var context = new ViewContext("v", workspace, viewNode); + var options = new RenderOptions(Themes.Light); + + // Act + var layout = strategy.BuildLayout(context, options); + + // Assert: Vehicle and its Engine child are present (resolved through the usage's typing + // edge), but the unrelated Other definition is excluded. + var labels = CollectBoxes(layout.Nodes).Select(b => b.Label).ToList(); + Assert.Contains("Vehicle", labels); + Assert.Contains("Engine", labels); + Assert.DoesNotContain("Other", labels); + } + + /// + /// A view whose FilterExpressionText is non-null emits the "parsed but not yet + /// evaluated" diagnostic through , while still rendering + /// the (unfiltered) resolved scope — per the binding decision to defer filter expression + /// evaluation to a future roadmap item. + /// + [Fact] + public void GeneralViewLayoutStrategy_BuildLayout_FilterExpressionPresent_EmitsNotYetEvaluatedWarning() + { + // Arrange: a view declaring a filter expression + var strategy = new GeneralViewLayoutStrategy(); + var workspace = BuildScopingWorkspace(); + var viewNode = new SysmlViewNode + { + Name = "V", + QualifiedName = "Root::V", + FilterExpressionText = "@SysML::PartUsage" + }; + var context = new ViewContext("v", workspace, viewNode); + var options = new RenderOptions(Themes.Light); + + // Act + var layout = strategy.BuildLayout(context, options); + + // Assert: a warning about the unevaluated filter is present, and the resolved (unfiltered) + // scope — here, the full workspace, since no expose statement was declared — still renders. + Assert.Contains(layout.Warnings, w => w.Contains("filter expression") && w.Contains("not yet evaluated")); + var labels = CollectBoxes(layout.Nodes).Select(b => b.Label).ToList(); + Assert.Contains("A", labels); + Assert.Contains("B", labels); + } + + /// + /// A view with no expose statement (a null , e.g. + /// the --auto synthesized view) renders identically to the pre-scoping-change + /// baseline: every non-stdlib definition in the workspace. This is the critical regression + /// guard confirming the scoping feature is fully backward-compatible when unused. + /// + [Fact] + public void GeneralViewLayoutStrategy_BuildLayout_NullViewNode_RendersFullWorkspaceUnchanged() + { + // Arrange: no ViewNode at all — the pre-existing 2-arg ViewContext construction. + var strategy = new GeneralViewLayoutStrategy(); + var workspace = BuildScopingWorkspace(); + var context = new ViewContext("v", workspace); + var options = new RenderOptions(Themes.Light); + + // Act + var layout = strategy.BuildLayout(context, options); + + // Assert: all three definitions are present and no warnings are emitted. + var labels = CollectBoxes(layout.Nodes).Select(b => b.Label).ToList(); + Assert.Contains("A", labels); + Assert.Contains("B", labels); + Assert.Contains("Child", labels); + Assert.Empty(layout.Warnings); + } + + /// + /// Regression guard for the bracketed-filter expose parsing bug: + /// vehicleMandatorySafetyFeatureViewStandalone in the real OMG corpus fixture + /// 11b-SafetyAndSecurityFeatureViews.sysml declares + /// expose vehicle::**[@Safety and (as Safety).isMandatory]; — the bracketed-filter + /// grammar form that AstBuilder.ExtractImportTarget previously failed to descend + /// into, leaving the view with zero Expose edges and causing + /// to silently fall back to rendering the entire + /// workspace. After the fix, this view's layout must be scoped to the vehicle + /// subtree — which, in this fixture, contains no part def declarations (only + /// usages), so the correctly-scoped rendering drops to zero boxes, strictly fewer than the + /// unscoped full-workspace rendering (which still includes the unrelated + /// AnnotationDefinitions::Safety/Security metadata definitions). Before the + /// fix, scoped and full box counts were identical (both rendered everything). + /// + // cspell:ignore Feaure -- typo present verbatim in the real OMG corpus fixture's package name + [Fact] + public async Task GeneralViewLayoutStrategy_BuildLayout_OmgSafetyFeatureViewsFixture_ScopesToExposedVehicleSubtree() + { + // Arrange: load the real OMG corpus fixture. + var modelsRoot = FindSysMLModelsRoot(); + if (modelsRoot is null) + { + return; + } + + var fixturePath = Path.Combine( + modelsRoot, "OMG", "validation", "11-ViewAndViewpoint", "11b-SafetyAndSecurityFeatureViews.sysml"); + if (!File.Exists(fixturePath)) + { + return; + } + + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([fixturePath], stdlibTable); + Assert.NotNull(result.Workspace); + var workspace = result.Workspace!; + + const string viewQualifiedName = + "'11b-Safety and Security Feaure Views'::Views::vehicleMandatorySafetyFeatureViewStandalone"; + var viewNode = Assert.IsType(workspace.Declarations[viewQualifiedName]); + + // Confirm the fix actually resolved an Expose edge before asserting on layout scoping — + // otherwise this test would pass vacuously by comparing full-workspace to full-workspace. + Assert.NotEmpty(viewNode.ExposedNames); + Assert.Contains(workspace.Index.AllEdges, + e => e.Kind == SysmlEdgeKind.Expose && e.SourceQualifiedName == viewQualifiedName); + + var strategy = new GeneralViewLayoutStrategy(); + var options = new RenderOptions(Themes.Light); + + // Act + var scoped = strategy.BuildLayout(new ViewContext("scoped", workspace, viewNode), options); + var full = strategy.BuildLayout(new ViewContext("full", workspace), options); + + // Assert: the scoped view renders strictly fewer boxes than the full workspace. In this + // fixture the `vehicle` subtree is built entirely from part *usages* (not `part def` + // declarations), which `GeneralViewLayoutStrategy.CollectDefinitions` does not render as + // boxes — so the correctly-scoped view renders zero boxes here, while the unscoped full + // workspace still renders the two unrelated `AnnotationDefinitions` metadata definitions + // (`Safety`, `Security`). That drop from 2 boxes to 0 is itself the regression signal: + // before the fix, `ResolveExposedScope` saw no `Expose` edges and fell back to rendering + // the entire workspace (i.e. scoped would equal full, not be strictly smaller). + var scopedBoxes = CollectBoxes(scoped.Nodes); + var fullBoxes = CollectBoxes(full.Nodes); + Assert.True(scopedBoxes.Count < fullBoxes.Count, + $"expected scoped box count ({scopedBoxes.Count}) < full box count ({fullBoxes.Count})"); + var fullLabels = fullBoxes.Select(b => b.Label).ToList(); + Assert.Contains("Safety", fullLabels); + Assert.Contains("Security", fullLabels); + var scopedLabels = scopedBoxes.Select(b => b.Label).ToList(); + Assert.DoesNotContain("Safety", scopedLabels); + Assert.DoesNotContain("Security", scopedLabels); + } + + /// + /// Finds the test/SysMLModels directory relative to the test assembly. + /// + private static string? FindSysMLModelsRoot() + { + var dir = AppContext.BaseDirectory; + while (dir is not null) + { + var candidate = Path.Combine(dir, "test", "SysMLModels"); + if (Directory.Exists(candidate)) + { + return candidate; + } + dir = Directory.GetParent(dir)?.FullName; + } + + return null; + } + /// /// Asserts that no two rendered definition (rectangle-shaped) boxes overlap in the layout. /// diff --git a/test/DemaConsulting.SysML2Tools.Tests/Layout/LayoutWarningsTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Layout/LayoutWarningsTests.cs index ace82911..39f31a70 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Layout/LayoutWarningsTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Layout/LayoutWarningsTests.cs @@ -38,4 +38,26 @@ public void ForCrossings_Many_ReturnsPluralWarning() var message = Assert.Single(warnings); Assert.Contains("3 connectors", message); } + + /// A null filter expression text produces no warnings. + [Fact] + public void ForUnevaluatedFilter_NullText_ReturnsEmpty() + { + Assert.Empty(LayoutWarnings.ForUnevaluatedFilter("View", null)); + } + + /// + /// A non-null filter expression text produces a single warning naming the view and stating + /// that the filter expression is parsed but not yet evaluated. + /// + [Fact] + public void ForUnevaluatedFilter_NonNullText_ReturnsNotYetEvaluatedWarning() + { + var warnings = LayoutWarnings.ForUnevaluatedFilter("MyView", "@SysML::PartUsage"); + + var message = Assert.Single(warnings); + Assert.Contains("MyView", message); + Assert.Contains("filter expression", message); + Assert.Contains("not yet evaluated", message); + } } diff --git a/test/DemaConsulting.SysML2Tools.Tests/Rendering/RenderingTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Rendering/RenderingTests.cs index 1d86269a..e7f9ba88 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Rendering/RenderingTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Rendering/RenderingTests.cs @@ -8,6 +8,7 @@ using DemaConsulting.Rendering.Svg; using DemaConsulting.SysML2Tools.Rendering; using DemaConsulting.SysML2Tools.Semantic; +using DemaConsulting.SysML2Tools.Semantic.Model; namespace DemaConsulting.SysML2Tools.Tests.Rendering; @@ -262,5 +263,25 @@ public void ViewContext_Construction_StoresAllFields() // Assert: both fields equal the supplied values Assert.Equal("myView", context.ViewName); Assert.Same(workspace, context.Workspace); + Assert.Null(context.ViewNode); + } + + /// + /// Constructing a with an explicit ViewNode stores the + /// supplied node, giving layout strategies access to the view's resolved render/expose/ + /// filter data. + /// + [Fact] + public void ViewContext_Construction_WithViewNode_StoresViewNode() + { + // Arrange: a minimal SysmlWorkspace and a SysmlViewNode + var workspace = new SysmlWorkspace(); + var viewNode = new SysmlViewNode { Name = "V", QualifiedName = "P::V" }; + + // Act: construct a ViewContext with the view node supplied + var context = new ViewContext("myView", workspace, viewNode); + + // Assert: the supplied view node is stored unchanged + Assert.Same(viewNode, context.ViewNode); } } diff --git a/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs index 7bf4ff1d..ceeb7f37 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs @@ -2394,6 +2394,300 @@ public async Task WorkspaceLoader_LoadAsync_DocumentationExampleFixture_Captures Assert.Equal(" This documentation of Automobile. ", automobileDoc.Text); } + /// + /// A view def with a render <target>; member naming a rendering-style + /// identifier that is not declared anywhere in the file (which would have failed + /// resolution under the old, incorrect content-scoping semantics) produces zero + /// diagnostics and zero edges sourced from the view — ReferenceResolver never + /// inspects RenderTargetName — while + /// is still captured verbatim. + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_ViewRenderTarget_CapturedRawNeverResolvedNoDiagnostic() + { + // Arrange + var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".sysml"); + try + { + await File.WriteAllTextAsync(tempFile, """ + package P { + view def V { + render asTreeDiagram; + } + } + """, TestContext.Current.CancellationToken); + + // Act + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + // Assert + Assert.NotNull(result.Workspace); + var view = Assert.IsType( + result.Workspace!.Declarations["P::V"]); + Assert.Equal("asTreeDiagram", view.RenderTargetName); + Assert.Empty(result.Diagnostics); + Assert.DoesNotContain(result.Workspace!.Index.AllEdges, + e => e.SourceQualifiedName == "P::V"); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// A view def with a filter [<expr>]; member should capture the raw + /// expression source text verbatim on + /// without evaluating it, producing no diagnostic and no edge (per binding decision: filter + /// expression evaluation is explicitly deferred future work). + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_ViewFilterExpression_CapturesTextVerbatimNoEdge() + { + // Arrange + var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".sysml"); + try + { + await File.WriteAllTextAsync(tempFile, """ + package P { + view def V { + filter @SysML::PartUsage; + } + } + """, TestContext.Current.CancellationToken); + + // Act + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + // Assert + Assert.NotNull(result.Workspace); + var view = Assert.IsType( + result.Workspace!.Declarations["P::V"]); + Assert.Equal("@SysML::PartUsage", view.FilterExpressionText); + Assert.Empty(result.Diagnostics); + Assert.DoesNotContain(result.Workspace!.Index.AllEdges, + e => e.SourceQualifiedName == "P::V"); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// A named view usage (not a view def) with an expose <ns>; + /// member should build a + /// via AstBuilder.VisitViewUsage (the first test to exercise that visitor) and + /// resolve the exposed name into an + /// edge. + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_ViewUsageWithExpose_RecordsExposeEdge() + { + // Arrange + var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".sysml"); + try + { + await File.WriteAllTextAsync(tempFile, """ + package P { + part def Exposed {} + view V { + expose Exposed; + } + } + """, TestContext.Current.CancellationToken); + + // Act + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + // Assert + Assert.NotNull(result.Workspace); + var view = Assert.IsType( + result.Workspace!.Declarations["P::V"]); + Assert.Contains("Exposed", view.ExposedNames); + Assert.Contains(result.Workspace!.Index.AllEdges, + e => e.Kind == DemaConsulting.SysML2Tools.Semantic.Model.SysmlEdgeKind.Expose && + e.SourceQualifiedName == "P::V" && + e.TargetQualifiedName == "P::Exposed"); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// A view with the bracketed-filter expose <ns>::**[<filterExpr>]; form — + /// the dominant expose shape in the real OMG corpus (e.g. + /// expose vehicle::**[@Safety]; in 11b-SafetyAndSecurityFeatureViews.sysml) — + /// must resolve the exposed name into an + /// edge. This + /// grammar form nests the qualified name two levels deeper than the plain form + /// (namespaceImport -> filterPackage -> filterPackageImportDeclaration -> + /// membershipImport), which AstBuilder.ExtractImportTarget previously did not + /// descend into, silently dropping the exposed name with no diagnostic. + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_ViewUsageWithBracketedFilterExpose_RecordsExposeEdge() + { + // Arrange + var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".sysml"); + try + { + await File.WriteAllTextAsync(tempFile, """ + package P { + metadata def Safety; + part def Exposed {} + view V { + expose Exposed::**[@Safety]; + } + } + """, TestContext.Current.CancellationToken); + + // Act + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + // Assert + Assert.NotNull(result.Workspace); + var view = Assert.IsType( + result.Workspace!.Declarations["P::V"]); + Assert.Contains("Exposed", view.ExposedNames); + Assert.Contains(result.Workspace!.Index.AllEdges, + e => e.Kind == DemaConsulting.SysML2Tools.Semantic.Model.SysmlEdgeKind.Expose && + e.SourceQualifiedName == "P::V" && + e.TargetQualifiedName == "P::Exposed"); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// Regression guard for the plain (non-bracketed) wildcard expose <ns>::*::**; + /// form — the sibling grammar shape to the bracketed-filter form above — confirming it + /// still resolves correctly after the ExtractImportTarget fix that added support for + /// descending into filterPackage(). + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_ViewUsageWithPlainWildcardExpose_RecordsExposeEdge() + { + // Arrange + var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".sysml"); + try + { + await File.WriteAllTextAsync(tempFile, """ + package P { + part def Exposed {} + view V { + expose Exposed::*::**; + } + } + """, TestContext.Current.CancellationToken); + + // Act + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + // Assert + Assert.NotNull(result.Workspace); + var view = Assert.IsType( + result.Workspace!.Declarations["P::V"]); + Assert.Contains("Exposed", view.ExposedNames); + Assert.Contains(result.Workspace!.Index.AllEdges, + e => e.Kind == DemaConsulting.SysML2Tools.Semantic.Model.SysmlEdgeKind.Expose && + e.SourceQualifiedName == "P::V" && + e.TargetQualifiedName == "P::Exposed"); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// Loading the real OMG corpus fixture + /// 11b-SafetyAndSecurityFeatureViews.sysml must resolve + /// vehicleMandatorySafetyFeatureViewStandalone's bracketed-filter + /// expose vehicle::**[@Safety and (as Safety).isMandatory]; member into a non-empty + /// ExposedNames list and a resolved Expose edge to vehicle — the exact + /// scenario confirmed broken (empty ExposedNames, zero edges, no diagnostic) before + /// the ExtractImportTarget fix. + /// + // cspell:ignore Feaure -- typo present verbatim in the real OMG corpus fixture's package name + [Fact] + public async Task WorkspaceLoader_LoadAsync_OmgSafetyFeatureViewsFixture_ResolvesBracketedExpose() + { + // Arrange + var modelsRoot = FindSysMLModelsRoot(); + if (modelsRoot is null) + { + return; + } + + var fixturePath = Path.Combine( + modelsRoot, "OMG", "validation", "11-ViewAndViewpoint", "11b-SafetyAndSecurityFeatureViews.sysml"); + if (!File.Exists(fixturePath)) + { + return; + } + + // Act + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([fixturePath], stdlibTable); + + // Assert + Assert.NotNull(result.Workspace); + var view = Assert.IsType( + result.Workspace!.Declarations["'11b-Safety and Security Feaure Views'::Views::vehicleMandatorySafetyFeatureViewStandalone"]); + Assert.NotEmpty(view.ExposedNames); + Assert.Contains(result.Workspace!.Index.AllEdges, + e => e.Kind == DemaConsulting.SysML2Tools.Semantic.Model.SysmlEdgeKind.Expose && + e.SourceQualifiedName == "'11b-Safety and Security Feaure Views'::Views::vehicleMandatorySafetyFeatureViewStandalone" && + e.TargetQualifiedName == "'11b-Safety and Security Feaure Views'::PartsTree::vehicle"); + } + + /// + /// A view def with an empty body should leave + /// and + /// null and + /// empty — a regression guard for the "no render statement → render everything" fallback. + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_ViewEmptyBody_AllNewFieldsNullOrEmpty() + { + // Arrange + var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".sysml"); + try + { + await File.WriteAllTextAsync(tempFile, """ + package P { + view def V {} + } + """, TestContext.Current.CancellationToken); + + // Act + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + // Assert + Assert.NotNull(result.Workspace); + var view = Assert.IsType( + result.Workspace!.Declarations["P::V"]); + Assert.Null(view.RenderTargetName); + Assert.Null(view.FilterExpressionText); + Assert.Empty(view.ExposedNames); + } + finally + { + File.Delete(tempFile); + } + } + /// /// Finds the test/SysMLModels directory relative to the test assembly. /// diff --git a/test/DemaConsulting.SysML2Tools.Tool.Tests/Render/RenderSubsystemTests.cs b/test/DemaConsulting.SysML2Tools.Tool.Tests/Render/RenderSubsystemTests.cs index 3672f1bd..6489eb76 100644 --- a/test/DemaConsulting.SysML2Tools.Tool.Tests/Render/RenderSubsystemTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tool.Tests/Render/RenderSubsystemTests.cs @@ -532,6 +532,162 @@ public async Task RenderSubsystem_MultipleViews_WithViewFlag_RendersSelectedView } } + /// + /// End-to-end regression test: a workspace with two views — one with an + /// expose <target>; body statement naming a resolvable target, and one with a + /// bogus expose thisIdentifierDoesNotExistAnywhere; — must produce two visibly + /// DIFFERENT rendered outputs (a view with no expose edges renders the full + /// workspace, while a view whose sole expose entry resolves scopes to that target's + /// subtree), and the bogus view's unresolved exposed name must surface a diagnostic in the + /// captured log output. render <target>; plays no role in scoping — only + /// expose does, per the corrected semantics. + /// + [Fact] + public async Task RenderSubsystem_ViewsWithDistinctExposeTargets_ProduceDifferingOutputsAndDiagnostic() + { + // Arrange: a workspace with two named expose targets in disjoint subtrees, one view + // whose expose statement resolves to "TargetA", and one whose expose statement names a + // nonexistent identifier. + const string sysmlWithExposeTargets = """ + package ExposeScopeTest { + part def TargetA { + part childA1 : TargetA {} + } + part def TargetB { + part childB1 : TargetB {} + } + view ViewValid { + expose TargetA; + } + view ViewBogus { + expose thisIdentifierDoesNotExistAnywhere; + } + } + """; + + var tempDir = Path.Combine(Path.GetTempPath(), $"expose_scope_bug_{Guid.NewGuid():N}"); + Directory.CreateDirectory(tempDir); + var tempFile = Path.Combine(tempDir, "model.sysml"); + await File.WriteAllTextAsync(tempFile, sysmlWithExposeTargets, TestContext.Current.CancellationToken); + + var outputDir = Path.Combine(tempDir, "out"); + var originalOut = Console.Out; + try + { + using var outWriter = new StringWriter(); + Console.SetOut(outWriter); + + // Act: render without --view so both views are rendered; capture output via --log + var logFile = Path.Combine(tempDir, "output.log"); + using (var context = Context.Create( + ["render", "--log", logFile, "--output", outputDir, tempFile])) + { + await Program.RunAsync(context); + + // Assert: exit code indicates success + Assert.Equal(0, context.ExitCode); + } + var validSvg = Path.Combine(outputDir, "ViewValid.svg"); + var bogusSvg = Path.Combine(outputDir, "ViewBogus.svg"); + Assert.True(File.Exists(validSvg), "Expected ViewValid.svg to be produced"); + Assert.True(File.Exists(bogusSvg), "Expected ViewBogus.svg to be produced"); + var validContent = await File.ReadAllTextAsync(validSvg, TestContext.Current.CancellationToken); + var bogusContent = await File.ReadAllTextAsync(bogusSvg, TestContext.Current.CancellationToken); + Assert.NotEqual(validContent, bogusContent); + + // Assert: the bogus exposed name surfaces a visible diagnostic naming the unresolved + // identifier, rather than silently rendering everything with no signal. + var logContent = await File.ReadAllTextAsync(logFile, TestContext.Current.CancellationToken); + Assert.Contains("thisIdentifierDoesNotExistAnywhere", logContent); + } + finally + { + Console.SetOut(originalOut); + Directory.Delete(tempDir, recursive: true); + } + } + + /// + /// Regression guard for the AstBuilder.VisitViewUsage capability addition: rendering + /// the real OMG corpus fixture + /// test/SysMLModels/OMG/validation/11-ViewAndViewpoint/11b-SafetyAndSecurityFeatureViews.sysml + /// with no --view filter must produce exactly 5 output files — the 2 view def + /// declarations (SafetyFeatureView, SafetyOrSecurityFeatureView) plus the 3 + /// named view usages (vehicleSafetyFeatureView, + /// vehicleMandatorySafetyFeatureView, + /// vehicleMandatorySafetyFeatureViewStandalone) — not just the 2 view defs + /// that were the only renderable declarations before VisitViewUsage was added. This + /// also confirms rendering this file produces no false unresolved-reference diagnostics for + /// its render asTreeDiagram;/render asElementTable; rendering-style members. + /// + [Fact] + public async Task RenderSubsystem_OmgSafetyFeatureViewsCorpus_RendersAllNamedViewUsages() + { + // Arrange: locate the real OMG corpus fixture relative to the test assembly's repo root. + var fixturePath = Path.Combine( + FindOmgModelsRoot(), + "validation", "11-ViewAndViewpoint", "11b-SafetyAndSecurityFeatureViews.sysml"); + Assert.True(File.Exists(fixturePath), $"Expected OMG corpus fixture at {fixturePath}"); + + var tempDir = Path.Combine(Path.GetTempPath(), $"omg_11b_views_{Guid.NewGuid():N}"); + Directory.CreateDirectory(tempDir); + var outputDir = Path.Combine(tempDir, "out"); + var originalOut = Console.Out; + try + { + using var outWriter = new StringWriter(); + Console.SetOut(outWriter); + + // Act: render without --view so every declared view is rendered. + var logFile = Path.Combine(tempDir, "output.log"); + using (var context = Context.Create( + ["render", "--log", logFile, "--output", outputDir, fixturePath])) + { + await Program.RunAsync(context); + + // Assert: exit code indicates success + Assert.Equal(0, context.ExitCode); + } + + // Assert: exactly 5 output files were produced — the 2 view defs plus the 3 named + // view usages, once VisitViewUsage surfaces them as renderable declarations. + var outputFiles = Directory.GetFiles(outputDir, "*.svg"); + Assert.Equal(5, outputFiles.Length); + + // Assert: no false "Unresolved reference" diagnostic for the rendering-style names. + var logContent = await File.ReadAllTextAsync(logFile, TestContext.Current.CancellationToken); + Assert.DoesNotContain("asTreeDiagram", logContent); + Assert.DoesNotContain("asElementTable", logContent); + } + finally + { + Console.SetOut(originalOut); + Directory.Delete(tempDir, recursive: true); + } + } + + /// + /// Walks upward from the test assembly's base directory to find the repository's + /// test/SysMLModels/OMG directory (mirroring the same idiom used by + /// OmgModelsTests.FindOmgModelsRoot), so the OMG corpus fixture can be located + /// regardless of the test runner's working directory. + /// + private static string FindOmgModelsRoot() + { + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir != null && !Directory.Exists(Path.Combine(dir.FullName, "test", "SysMLModels", "OMG"))) + { + dir = dir.Parent; + } + + if (dir == null) + { + throw new DirectoryNotFoundException("Cannot locate test/SysMLModels/OMG from test assembly location."); + } + + return Path.Combine(dir.FullName, "test", "SysMLModels", "OMG"); + } + /// /// 'render --help' now prints render-specific usage (a regression-proofing test for the /// command-aware help dispatch added alongside the 'help' command).