Source generator: support source-generating compiled companion NodeSets - #4062
Source generator: support source-generating compiled companion NodeSets#4062marcschier wants to merge 2 commits into
Conversation
Enhancements to the model source generator so that a *compiled* companion NodeSet2 (e.g. an OPC companion spec such as Robotics/IA, or the OpenUSD binding model) can be source-generated: - Add OpcUaStateTypeIndex and wire it through ModelCompilation / ModelSourceGenerator so standard (ns=0) node-state/method-state class names are resolved from the referenced Core(.Types) metadata. - Add StandardMethodStateFallback: when a compiled NodeSet instantiates a standard type that carries methods (e.g. a FileDirectoryType 'Delete' instance), the default naming can compute an 'Opc.Ua.*MethodState' name that Core never emits under that name. Degrade an unresolved standard 'global::Opc.Ua.*MethodState' reference to the base 'global::Opc.Ua. MethodState' only when that typed class is neither declared by the current pass nor present in the compilation, keyed on the exact Opc.Ua namespace and never entered by the Stack pass that builds Core.Types. This is the scoped interim workaround tracked by #4060 (the proper upstream fix is a name-resolution step, which this makes unnecessary once landed). - NodeStateGenerator / ModelDesignExtensions / FluentBuilderGenerator: supporting changes for the above. - Opc.Ua.Types: expose internals to Opc.Ua.Core.Encoders.Tests. Validated: generator builds (netstandard2.0), Opc.Ua.Core.Types generates and builds, and the full Opc.Ua.SourceGeneration.Core test suite passes (3715 passed / 8 skipped / 0 failed).
There was a problem hiding this comment.
Pull request overview
Note
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Enhances the OPC UA model source generator to support source-generating from compiled companion NodeSet2 inputs by resolving standard (ns=0) state/method-state types from referenced assemblies and introducing a scoped fallback when curated Core assemblies omit certain typed *MethodState classes.
Changes:
- Add an
OpcUaStateTypeIndexbuilt from RoslynCompilationmetadata and plumb it throughModelSourceGenerator→ModelCompilation. - Introduce
StandardMethodStateFallback(scoped to model generation) and apply it inModelDesignExtensions, while recording locally-declared standard-namespace method-state classes. - Fix XML doc generation in
FluentBuilderGeneratorby escaping browse names; expose internals toOpc.Ua.Core.Encoders.Tests.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tools/Opc.Ua.SourceGeneration/OpcUaStateTypeIndex.cs | New compilation-based index of available Opc.Ua.*State types to support fallback decisions. |
| tools/Opc.Ua.SourceGeneration/ModelSourceGenerator.cs | Wires the state-type index into the incremental generator pipeline. |
| tools/Opc.Ua.SourceGeneration/ModelCompilation.cs | Enters the fallback scope during model-generation passes. |
| tools/Opc.Ua.SourceGeneration.Core/Schema/StandardMethodStateFallback.cs | Adds scoped policy to degrade missing standard typed *MethodState references to MethodState. |
| tools/Opc.Ua.SourceGeneration.Core/Schema/ModelDesignExtensions.cs | Applies the fallback when emitting typed method-state references. |
| tools/Opc.Ua.SourceGeneration.Core/Generators/NodeStateGenerator.cs | Records declared method-state classes to prevent incorrect degradation on later references. |
| tools/Opc.Ua.SourceGeneration.Core/Generators/FluentBuilderGenerator.cs | Escapes XML doc content to prevent malformed XML (CS1570). |
| src/Opc.Ua.Types/Opc.Ua.Types.csproj | Adds InternalsVisibleTo for encoder tests. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #4062 +/- ##
==========================================
- Coverage 73.86% 73.57% -0.30%
==========================================
Files 1345 1347 +2
Lines 180038 180201 +163
Branches 31678 31704 +26
==========================================
- Hits 132993 132577 -416
- Misses 36290 36902 +612
+ Partials 10755 10722 -33
🚀 New features to boost your workflow:
|
…e writes (#4063) ## Summary Combines the compiled companion-model support from #4062 with the NodeState emission fixes already in this PR. The model source generator can now consume compiled companion `NodeSet2` models more reliably, while generated factories consistently target base `NodeState` members even when a generated subclass shadows them. ## Changes ### Compiled companion NodeSets - Add `OpcUaStateTypeIndex` and wire it through `ModelSourceGenerator` / `ModelCompilation` so standard (`ns=0`) node-state and method-state class names are resolved from referenced `Opc.Ua.Core(.Types)` metadata. - Add the model-generation-scoped `StandardMethodStateFallback`, which degrades an unresolved standard typed `*MethodState` reference to `global::Opc.Ua.MethodState` only when the type is neither available in the compilation nor declared by the current generation pass. - Keep declaration names outside the reference-only fallback policy, propagate fallback state through the logical execution context, reuse the immutable availability index, and use named incremental-generator inputs instead of deep `Left` / `Right` chains. - Escape generated fluent-builder XML documentation where browse names can contain XML-sensitive characters. ### Safe NodeState factory emission - Declare one base-typed `global::Opc.Ua.NodeState nodeState = state;` local per generated factory. - Use `nodeState` for base-member reads and writes to `NodeId`, `SymbolicName`, `BrowseName`, `DisplayName`, `Description`, `WriteMask`, `UserWriteMask`, `AccessRestrictions`, `ReleaseStatus`, `Categories`, `Specification` and `IsPartOfTypeHierarchy`. - Base-qualify the optional-child `previousNodeId` read, NodeId comparison, and existing-child NodeId update. - `global::`-qualify emitted `NodeId` / `ExpandedNodeId` namespace prefixes to avoid namespace collisions. ### Supporting updates - Expose `Opc.Ua.Types` internals to `Opc.Ua.Core.Encoders.Tests`. - Update generator assertions and add focused fallback regression coverage. ## Why Compiled companion models can instantiate standard types whose curated Core implementation does not expose the conventionally named typed method-state class. The scoped fallback lets those models generate without weakening the Stack generator or masking classes emitted by the current model. Separately, generated subclasses can shadow base `NodeState` properties with strongly typed `new` properties. Routing base attributes through a `NodeState`-typed local guarantees initialization reaches the intended base member while keeping generated code smaller and clearer than repeated casts. ## Validation - Full `Opc.Ua.SourceGeneration.Core.Tests` on net10.0: **3718 passed / 8 skipped / 0 failed**. - Combined focused generator coverage on net48: **21 passed / 0 failed**. - Full `Opc.Ua.SourceGeneration.Tests`: **68 passed / 0 failed**. - Full `Opc.Ua.SourceGeneration.Stack.Tests`: **90 passed / 0 failed**. ## Context This PR supersedes #4062 and contains both source-generator change sets. The companion-model fallback remains the scoped interim workaround tracked by #4060; a future upstream name-resolution step can remove it. These changes also feed the experimental data-encodings work in #4007. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
What
Enhancements to the model source generator so that a compiled companion
NodeSet2(an OPC companion spec such as Robotics/IA, or an application companion model such as an OpenUSD binding) can be source-generated:OpcUaStateTypeIndexand wire it throughModelCompilation/ModelSourceGeneratorso standard (ns=0) node-state / method-state class names are resolved from the referencedOpc.Ua.Core(.Types)metadata during the model pass.StandardMethodStateFallback: when a compiled NodeSet instantiates a standard type that carries methods (e.g. aFileDirectoryTypeobject whoseDeletemethod is modelled as a plain instance method), the default naming can compute aglobal::Opc.Ua.*MethodStateclass name thatOpc.Ua.Core.Typesnever emits under that name (Core emits the curated name derived from the method type, e.g.DeleteFileMethodState). This degrades an unresolved standardglobal::Opc.Ua.*MethodStatereference to the baseglobal::Opc.Ua.MethodState— only when that typed class is neither declared by the current pass nor present in the compilation. It is keyed on the exactOpc.Uanamespace, records declared method-state classes at their declaration site (so a model that legitimately emits its ownOpc.Ua.*MethodState, e.g. GDS, is never degraded), and the Stack pass that buildsOpc.Ua.Core.Typesnever enters the scope.NodeStateGenerator,ModelDesignExtensions, andFluentBuilderGenerator.Opc.Ua.Types: expose internals toOpc.Ua.Core.Encoders.Tests.Why
Today a companion model that (a) is consumed as a compiled
NodeSet2and (b) instantiates a standard type that carries methods or state-machine members cannot be source-generated without this fallback. This is the scoped, model-generation-only interim workaround tracked by #4060; the proper long-term fix is an upstream name-resolution step in the generator (also described in #4060), which would make the fallback unnecessary.Validation
netstandard2.0).Opc.Ua.Core.Typesgenerates and builds.Opc.Ua.SourceGeneration.Coretest suite passes: 3718 passed / 8 skipped / 0 failed.Opc.Ua.SourceGenerationtest suite passes: 68 passed / 0 skipped / 0 failed.Context
This is the generic, upstream-worthy prerequisite extracted from a larger OpenUSD/Robotics integration. The full integration lives on the stacked branch
marcschier/openusd-integration(this branch is a clean prefix of it) and is not part of this PR.