diff options
author | Sven Boemer <sbomer@gmail.com> | 2022-04-20 21:37:22 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-04-20 21:37:22 +0300 |
commit | fb5b3396eda1fb41765f04cad8b2cbd34d1fd53c (patch) | |
tree | f19f1ba18b1ac7f1d5a0b4793ed60a11eb3511ca | |
parent | 36b61348be2b93f2add8886174f19c0ed4aacd36 (diff) |
Support assignment to flow capture references (#2730)
* Support assignment to flow capture references
This uses code copied from Roslyn to detect whether a flow capture is used as an r-value or l-value (or both).
L-value captures are tracked in a separate dictionary which is used to look up captured values when assigning to
l-value capture references. The capture references are not first-class dataflow values under SingleValue, in
order to keep the general dataflow logic separated from the trim-analysis-specific values.
15 files changed, 682 insertions, 121 deletions
diff --git a/src/ILLink.RoslynAnalyzer/DataFlow/BasicBlockExtensions.cs b/src/ILLink.RoslynAnalyzer/DataFlow/BasicBlockExtensions.cs new file mode 100644 index 000000000..f4f1c85d6 --- /dev/null +++ b/src/ILLink.RoslynAnalyzer/DataFlow/BasicBlockExtensions.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.CodeAnalysis.FlowAnalysis +{ + // Copied from https://github.com/dotnet/roslyn/blob/fdd40b21d59c13e8fa6c718c7aaf9d50634da754/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/BasicBlockExtensions.cs + internal static partial class BasicBlockExtensions + { + public static IEnumerable<IOperation> DescendantOperations (this BasicBlock basicBlock) + { + foreach (var statement in basicBlock.Operations) { + foreach (var operation in statement.DescendantsAndSelf ()) { + yield return operation; + } + } + + if (basicBlock.BranchValue != null) { + foreach (var operation in basicBlock.BranchValue.DescendantsAndSelf ()) { + yield return operation; + } + } + } + } +}
\ No newline at end of file diff --git a/src/ILLink.RoslynAnalyzer/DataFlow/CapturedReferenceValue.cs b/src/ILLink.RoslynAnalyzer/DataFlow/CapturedReferenceValue.cs new file mode 100644 index 000000000..3bb47df6c --- /dev/null +++ b/src/ILLink.RoslynAnalyzer/DataFlow/CapturedReferenceValue.cs @@ -0,0 +1,50 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using ILLink.Shared.DataFlow; +using Microsoft.CodeAnalysis; + +namespace ILLink.RoslynAnalyzer.DataFlow +{ + public struct CapturedReferenceValue : IEquatable<CapturedReferenceValue> + { + public readonly IOperation? Reference; + + public CapturedReferenceValue (IOperation operation) + { + switch (operation.Kind) { + case OperationKind.PropertyReference: + case OperationKind.LocalReference: + case OperationKind.FieldReference: + case OperationKind.ParameterReference: + case OperationKind.ArrayElementReference: + break; + default: + throw new NotImplementedException (operation.Kind.ToString ()); + } + Reference = operation; + } + + public bool Equals (CapturedReferenceValue other) => Reference == other.Reference; + } + + + public struct CapturedReferenceLattice : ILattice<CapturedReferenceValue> + { + public CapturedReferenceValue Top => new CapturedReferenceValue (); + + public CapturedReferenceValue Meet (CapturedReferenceValue left, CapturedReferenceValue right) + { + if (left.Equals (right)) + return left; + if (left.Reference == null) + return right; + if (right.Reference == null) + return left; + // Both non-null and different shouldn't happen. + // We assume that a flow capture can capture only a single property. + throw new InvalidOperationException (); + } + } +}
\ No newline at end of file diff --git a/src/ILLink.RoslynAnalyzer/DataFlow/ControlFlowGraphExtensions.cs b/src/ILLink.RoslynAnalyzer/DataFlow/ControlFlowGraphExtensions.cs new file mode 100644 index 000000000..6f188c53a --- /dev/null +++ b/src/ILLink.RoslynAnalyzer/DataFlow/ControlFlowGraphExtensions.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.FlowAnalysis; + +namespace ILLink.RoslynAnalyzer.DataFlow +{ + // Copied from https://github.com/dotnet/roslyn/blob/fdd40b21d59c13e8fa6c718c7aaf9d50634da754/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/ControlFlowGraphExtensions.cs + internal static partial class ControlFlowGraphExtensions + { + public static BasicBlock EntryBlock (this ControlFlowGraph cfg) + { + var firstBlock = cfg.Blocks[0]; + Debug.Assert (firstBlock.Kind == BasicBlockKind.Entry); + return firstBlock; + } + + public static BasicBlock ExitBlock (this ControlFlowGraph cfg) + { + var lastBlock = cfg.Blocks.Last (); + Debug.Assert (lastBlock.Kind == BasicBlockKind.Exit); + return lastBlock; + } + + public static IEnumerable<IOperation> DescendantOperations (this ControlFlowGraph cfg) + => cfg.Blocks.SelectMany (b => b.DescendantOperations ()); + + public static IEnumerable<T> DescendantOperations<T> (this ControlFlowGraph cfg, OperationKind operationKind) + where T : IOperation + => cfg.DescendantOperations ().Where (d => d?.Kind == operationKind).Cast<T> (); + } +} diff --git a/src/ILLink.RoslynAnalyzer/DataFlow/ControlFlowGraphProxy.cs b/src/ILLink.RoslynAnalyzer/DataFlow/ControlFlowGraphProxy.cs index 8eb5befd2..d5182c531 100644 --- a/src/ILLink.RoslynAnalyzer/DataFlow/ControlFlowGraphProxy.cs +++ b/src/ILLink.RoslynAnalyzer/DataFlow/ControlFlowGraphProxy.cs @@ -49,7 +49,7 @@ namespace ILLink.RoslynAnalyzer.DataFlow } } - public BlockProxy Entry => new BlockProxy (ControlFlowGraph.Blocks[0]); + public BlockProxy Entry => new BlockProxy (ControlFlowGraph.EntryBlock ()); // This is implemented by getting predecessors of the underlying Roslyn BasicBlock. // This is fine as long as the blocks come from the correct control-flow graph. diff --git a/src/ILLink.RoslynAnalyzer/DataFlow/FlowCaptureKind.cs b/src/ILLink.RoslynAnalyzer/DataFlow/FlowCaptureKind.cs new file mode 100644 index 000000000..78feb6f8b --- /dev/null +++ b/src/ILLink.RoslynAnalyzer/DataFlow/FlowCaptureKind.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable disable + +namespace ILLink.RoslynAnalyzer.DataFlow +{ + // Copied from https://github.com/dotnet/roslyn/blob/c8ebc8682889b395fcb84c85bf4ff54577377d26/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/FlowAnalysis/FlowCaptureKind.cs + /// <summary> + /// Indicates the kind of flow capture in an <see cref="IFlowCaptureOperation"/>. + /// </summary> + public enum FlowCaptureKind + { + /// <summary> + /// Indicates an R-Value flow capture, i.e. capture of a symbol's value. + /// </summary> + RValueCapture, + + /// <summary> + /// Indicates an L-Value flow capture, i.e. captures of a symbol's location/address. + /// </summary> + LValueCapture, + + /// <summary> + /// Indicates both an R-Value and an L-Value flow capture, i.e. captures of a symbol's value and location/address. + /// These are generated for left of a compound assignment operation, such that there is conditional code on the right side of the compound assignment. + /// </summary> + LValueAndRValueCapture + } +} diff --git a/src/ILLink.RoslynAnalyzer/DataFlow/LValueFlowCaptureProvider.cs b/src/ILLink.RoslynAnalyzer/DataFlow/LValueFlowCaptureProvider.cs new file mode 100644 index 000000000..610a4cfd4 --- /dev/null +++ b/src/ILLink.RoslynAnalyzer/DataFlow/LValueFlowCaptureProvider.cs @@ -0,0 +1,80 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable disable + +using System.Collections.Generic; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Operations; +using Microsoft.CodeAnalysis.FlowAnalysis; + +#if DEBUG +using System.Diagnostics; +#endif + +namespace ILLink.RoslynAnalyzer.DataFlow +{ + // Copied from https://github.com/dotnet/roslyn/blob/c8ebc8682889b395fcb84c85bf4ff54577377d26/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/FlowAnalysis/LValueFlowCaptureProvider.cs + /// <summary> + /// Helper class to detect <see cref="IFlowCaptureOperation"/>s that are l-value captures. + /// L-value captures are essentially captures of a symbol's location/address. + /// Corresponding <see cref="IFlowCaptureReferenceOperation"/>s which share the same + /// <see cref="CaptureId"/> as this flow capture, dereferences and writes to this location + /// subsequently in the flow graph. + /// For example, consider the below code: + /// a[i] = x ?? a[j]; + /// The control flow graph contains an initial flow capture of "a[i]" to capture the l-value + /// of this array element: + /// FC0 (a[i]) + /// Then it evaluates the right hand side, which can have different + /// values on different control flow paths, and the resultant value is then written + /// to the captured location: + /// FCR0 = result + /// </summary> + /// <remarks> + /// NOTE: This type is a workaround for https://github.com/dotnet/roslyn/issues/31007 + /// and it can be deleted once that feature is implemented. + /// </remarks> + internal static class LValueFlowCapturesProvider + { + public static ImmutableDictionary<CaptureId, FlowCaptureKind> CreateLValueFlowCaptures (ControlFlowGraph cfg) + { + // This method identifies flow capture reference operations that are target of an assignment + // and marks them as lvalue flow captures. + // Control flow graph can also contain flow captures + // that are r-value captures at some point and l-values captures at other point in + // the flow graph. Specifically, for an ICoalesceOperation a flow capture acts + // as both an r-value and l-value flow capture. + + ImmutableDictionary<CaptureId, FlowCaptureKind>.Builder lvalueFlowCaptureIdBuilder = null; + var rvalueFlowCaptureIds = new HashSet<CaptureId> (); + + foreach (var flowCaptureReference in cfg.DescendantOperations<IFlowCaptureReferenceOperation> (OperationKind.FlowCaptureReference)) { + if (flowCaptureReference.Parent is IAssignmentOperation assignment && + assignment.Target == flowCaptureReference || + flowCaptureReference.IsInLeftOfDeconstructionAssignment (out _)) { + lvalueFlowCaptureIdBuilder ??= ImmutableDictionary.CreateBuilder<CaptureId, FlowCaptureKind> (); + var captureKind = flowCaptureReference.Parent.IsAnyCompoundAssignment () || rvalueFlowCaptureIds.Contains (flowCaptureReference.Id) + ? FlowCaptureKind.LValueAndRValueCapture + : FlowCaptureKind.LValueCapture; + lvalueFlowCaptureIdBuilder.Add (flowCaptureReference.Id, captureKind); + } else { + rvalueFlowCaptureIds.Add (flowCaptureReference.Id); + } + } + +#if DEBUG + if (lvalueFlowCaptureIdBuilder != null) { + foreach (var kvp in lvalueFlowCaptureIdBuilder) { + var captureId = kvp.Key; + var kind = kvp.Value; + Debug.Assert (kind == FlowCaptureKind.LValueAndRValueCapture || !rvalueFlowCaptureIds.Contains (captureId), "Flow capture used as both an r-value and an l-value, but with incorrect flow capture kind"); + } + } +#endif + + return lvalueFlowCaptureIdBuilder != null ? lvalueFlowCaptureIdBuilder.ToImmutable () : ImmutableDictionary<CaptureId, FlowCaptureKind>.Empty; + } + } +} diff --git a/src/ILLink.RoslynAnalyzer/DataFlow/LocalDataFlowVisitor.cs b/src/ILLink.RoslynAnalyzer/DataFlow/LocalDataFlowVisitor.cs index 48c3132d9..1f3c48323 100644 --- a/src/ILLink.RoslynAnalyzer/DataFlow/LocalDataFlowVisitor.cs +++ b/src/ILLink.RoslynAnalyzer/DataFlow/LocalDataFlowVisitor.cs @@ -30,8 +30,16 @@ namespace ILLink.RoslynAnalyzer.DataFlow protected TValue TopValue => LocalStateLattice.Lattice.ValueLattice.Top; - public LocalDataFlowVisitor (LocalStateLattice<TValue, TValueLattice> lattice, OperationBlockAnalysisContext context) => - (LocalStateLattice, Context) = (lattice, context); + private readonly ImmutableDictionary<CaptureId, FlowCaptureKind> lValueFlowCaptures; + + bool IsLValueFlowCapture (CaptureId captureId) + => lValueFlowCaptures.ContainsKey (captureId); + + bool IsRValueFlowCapture (CaptureId captureId) + => !lValueFlowCaptures.TryGetValue (captureId, out var captureKind) || captureKind != FlowCaptureKind.LValueCapture; + + public LocalDataFlowVisitor (LocalStateLattice<TValue, TValueLattice> lattice, OperationBlockAnalysisContext context, ImmutableDictionary<CaptureId, FlowCaptureKind> lValueFlowCaptures) => + (LocalStateLattice, Context, this.lValueFlowCaptures) = (lattice, context, lValueFlowCaptures); public void Transfer (BlockProxy block, LocalDataFlowState<TValue, TValueLattice> state) { @@ -89,60 +97,100 @@ namespace ILLink.RoslynAnalyzer.DataFlow public override TValue VisitSimpleAssignment (ISimpleAssignmentOperation operation, LocalDataFlowState<TValue, TValueLattice> state) { + // Visiting the target operation is a no-op except for operations which produce + // dataflow values that have the same representation as LValues or RValues. + // For example, field references. var targetValue = Visit (operation.Target, state); - var value = Visit (operation.Value, state); - switch (operation.Target) { - case ILocalReferenceOperation localRef: - state.Set (new LocalKey (localRef.Local), value); - break; + + var targetOperation = operation.Target; + if (targetOperation is IFlowCaptureReferenceOperation flowCaptureReference) { + Debug.Assert (IsLValueFlowCapture (flowCaptureReference.Id)); + var capturedReference = state.Current.CapturedReferences.Get (flowCaptureReference.Id).Reference; + targetOperation = capturedReference; + if (targetOperation == null) + throw new InvalidOperationException (); + + targetValue = Visit (targetOperation, state); + // Note: technically we should avoid visiting the target operation when assigning to a flow capture reference, + // because this should be done when the capture is created. For example, a flow capture used as both an LValue and a RValue + // should only evaluate the expression that computes the object instance of a property reference once. + // However, we just visit the instance again below for simplicity. This could be generalized if we encounter a dataflow + // behavior where this makes a difference. + } + + switch (targetOperation) { case IFieldReferenceOperation: - case IParameterReferenceOperation: - // Extension point for assignments to "interesting" targets. - // Doesn't get called for assignments to locals, which are handled above. - HandleAssignment (value, targetValue, operation); - break; - case IPropertyReferenceOperation propertyRef: - // A property assignment is really a call to the property setter. - var setMethod = propertyRef.Property.SetMethod; - if (setMethod == null) { - // This can happen in a constructor - there it is possible to assign to a property - // without a setter. This turns into an assignment to the compiler-generated backing field. - // To match the linker, this should warn about the compiler-generated backing field. - // For now, just don't warn. - break; + case IParameterReferenceOperation: { + // These assignment targets have the same dataflow representation for LValues and RValues, + // so we can use the target value that we already computed. This simplification is specific + // to the trim analysis, so would ideally be moved into TrimAnalysisVisitor. + TValue value = Visit (operation.Value, state); + HandleAssignment (value, targetValue, operation); + return value; } - TValue instanceValue = Visit (propertyRef.Instance, state); - // The return value of a property set expression is the value, - // even though a property setter has no return value. - HandleMethodCall ( - setMethod, - instanceValue, - ImmutableArray.Create (value), - operation); - break; - // TODO: when setting a property in an attribute, target is an IPropertyReference. - case IArrayElementReferenceOperation arrayElementRef: - if (arrayElementRef.Indices.Length != 1) - break; - HandleArrayElementWrite (Visit (arrayElementRef.ArrayReference, state), Visit (arrayElementRef.Indices[0], state), value, operation); - break; + // The remaining cases don't have a dataflow value that represents LValues, so we need + // to handle the LHS specially. + case IPropertyReferenceOperation propertyRef: { + var setMethod = propertyRef.Property.SetMethod; + if (setMethod == null) { + // This can happen in a constructor - there it is possible to assign to a property + // without a setter. This turns into an assignment to the compiler-generated backing field. + // To match the linker, this should warn about the compiler-generated backing field. + // For now, just don't warn. https://github.com/dotnet/linker/issues/2731 + break; + } + TValue instanceValue = Visit (propertyRef.Instance, state); + TValue value = Visit (operation.Value, state); + HandleMethodCall ( + setMethod, + instanceValue, + ImmutableArray.Create (value), + operation); + // The return value of a property set expression is the value, + // even though a property setter has no return value. + return value; + } + // TODO: when setting a property in an attribute, target is an IPropertyReference. + case ILocalReferenceOperation localRef: { + TValue value = Visit (operation.Value, state); + state.Set (new LocalKey (localRef.Local), value); + return value; + } + case IArrayElementReferenceOperation arrayElementRef: { + if (arrayElementRef.Indices.Length != 1) + break; + + TValue arrayRef = Visit (arrayElementRef.ArrayReference, state); + TValue index = Visit (arrayElementRef.Indices[0], state); + TValue value = Visit (operation.Value, state); + HandleArrayElementWrite (arrayRef, index, value, operation); + return value; + } case IDiscardOperation: // Assignments like "_ = SomeMethod();" don't need dataflow tracking. + // Seems like this can't happen with a flow capture operation. + Debug.Assert (operation.Target is not IFlowCaptureReferenceOperation); break; case IInvalidOperation: // This can happen for a field assignment in an attribute instance. // TODO: validate against the field attributes. break; default: - throw new NotImplementedException (operation.Target.GetType ().ToString ()); + throw new NotImplementedException (targetOperation.GetType ().ToString ()); } - return value; + return Visit (operation.Value, state); } // Similar to VisitLocalReference public override TValue VisitFlowCaptureReference (IFlowCaptureReferenceOperation operation, LocalDataFlowState<TValue, TValueLattice> state) { + if (!operation.GetValueUsageInfo (Context.OwningSymbol).HasFlag (ValueUsageInfo.Read)) { + Debug.Assert (IsLValueFlowCapture (operation.Id)); + return TopValue; + } + + Debug.Assert (IsRValueFlowCapture (operation.Id)); return state.Get (new LocalKey (operation.Id)); } @@ -152,8 +200,18 @@ namespace ILLink.RoslynAnalyzer.DataFlow public override TValue VisitFlowCapture (IFlowCaptureOperation operation, LocalDataFlowState<TValue, TValueLattice> state) { TValue value = Visit (operation.Value, state); - state.Set (new LocalKey (operation.Id), value); - return value; + if (IsLValueFlowCapture (operation.Id)) { + // Note: technically we should save some information about the value for LValue flow captures + // (for example, the object instance of a property reference) and avoid re-computing it when + // assigning to the FlowCaptureReference. + var currentState = state.Current; + currentState.CapturedReferences.Set (operation.Id, new CapturedReferenceValue (operation.Value)); + state.Current = currentState; + } + if (IsRValueFlowCapture (operation.Id)) + state.Set (new LocalKey (operation.Id), value); + + return TopValue; } public override TValue VisitExpressionStatement (IExpressionStatementOperation operation, LocalDataFlowState<TValue, TValueLattice> state) @@ -165,56 +223,34 @@ namespace ILLink.RoslynAnalyzer.DataFlow public override TValue VisitInvocation (IInvocationOperation operation, LocalDataFlowState<TValue, TValueLattice> state) => ProcessMethodCall (operation, operation.TargetMethod, operation.Instance, operation.Arguments, state); - public static IMethodSymbol GetPropertyMethod (IPropertyReferenceOperation operation) - { - // The IPropertyReferenceOperation doesn't tell us whether this reference is to the getter or setter. - // For this we need to look at the containing operation. - var parent = operation.Parent; - if (parent?.Kind == OperationKind.SimpleAssignment) { - var assignment = (ISimpleAssignmentOperation) parent; - if (assignment.Target == operation) { - var setMethod = operation.Property.SetMethod; - Debug.Assert (setMethod != null); - return setMethod!; - } - Debug.Assert (assignment.Value == operation); - } - - var getMethod = operation.Property.GetMethod; - Debug.Assert (getMethod != null); - return getMethod!; - } - public override TValue VisitPropertyReference (IPropertyReferenceOperation operation, LocalDataFlowState<TValue, TValueLattice> state) { - if (operation.GetValueUsageInfo (Context.OwningSymbol).HasFlag (ValueUsageInfo.Read)) { - // Accessing property for reading is really a call to the getter - // The setter case is handled in assignment operation since here we don't have access to the value to pass to the setter - TValue instanceValue = Visit (operation.Instance, state); - return HandleMethodCall ( - operation.Property.GetMethod!, - instanceValue, - ImmutableArray<TValue>.Empty, - operation); - } + if (!operation.GetValueUsageInfo (Context.OwningSymbol).HasFlag (ValueUsageInfo.Read)) + return TopValue; - return TopValue; + // Accessing property for reading is really a call to the getter + // The setter case is handled in assignment operation since here we don't have access to the value to pass to the setter + TValue instanceValue = Visit (operation.Instance, state); + return HandleMethodCall ( + operation.Property.GetMethod!, + instanceValue, + ImmutableArray<TValue>.Empty, + operation); } public override TValue VisitArrayElementReference (IArrayElementReferenceOperation operation, LocalDataFlowState<TValue, TValueLattice> state) { - if (operation.GetValueUsageInfo (Context.OwningSymbol).HasFlag (ValueUsageInfo.Read)) { - // Accessing an array element for reading is a call to the indexer - // or a plain array access. Just handle plain array access for now. + if (!operation.GetValueUsageInfo (Context.OwningSymbol).HasFlag (ValueUsageInfo.Read)) + return TopValue; - // Only handle simple index access - if (operation.Indices.Length != 1) - return TopValue; + // Accessing an array element for reading is a call to the indexer + // or a plain array access. Just handle plain array access for now. - return HandleArrayElementRead (Visit (operation.ArrayReference, state), Visit (operation.Indices[0], state), operation); - } + // Only handle simple index access + if (operation.Indices.Length != 1) + return TopValue; - return TopValue; + return HandleArrayElementRead (Visit (operation.ArrayReference, state), Visit (operation.Indices[0], state), operation); } public override TValue VisitArgument (IArgumentOperation operation, LocalDataFlowState<TValue, TValueLattice> state) diff --git a/src/ILLink.RoslynAnalyzer/DataFlow/LocalStateLattice.cs b/src/ILLink.RoslynAnalyzer/DataFlow/LocalStateLattice.cs index 09b0a649e..5792e55c6 100644 --- a/src/ILLink.RoslynAnalyzer/DataFlow/LocalStateLattice.cs +++ b/src/ILLink.RoslynAnalyzer/DataFlow/LocalStateLattice.cs @@ -29,13 +29,26 @@ namespace ILLink.RoslynAnalyzer.DataFlow } } - // Wrapper struct exists purely to substitute a concrete LocalKey for TKey of DefaultValueDictionary public struct LocalState<TValue> : IEquatable<LocalState<TValue>> where TValue : IEquatable<TValue> { public DefaultValueDictionary<LocalKey, TValue> Dictionary; - public LocalState (DefaultValueDictionary<LocalKey, TValue> dictionary) => Dictionary = dictionary; + // Stores any operations which are captured by reference in a FlowCaptureOperation. + // Only stores captures which are assigned through. Captures of the values of operations + // are tracked as part of the dictionary of values, keyed by LocalKey. + public DefaultValueDictionary<CaptureId, CapturedReferenceValue> CapturedReferences; + + public LocalState (DefaultValueDictionary<LocalKey, TValue> dictionary, DefaultValueDictionary<CaptureId, CapturedReferenceValue> capturedReferences) + { + Dictionary = dictionary; + CapturedReferences = capturedReferences; + } + + public LocalState (DefaultValueDictionary<LocalKey, TValue> dictionary) + : this (dictionary, new DefaultValueDictionary<CaptureId, CapturedReferenceValue> (new CapturedReferenceValue ())) + { + } public bool Equals (LocalState<TValue> other) => Dictionary.Equals (other.Dictionary); @@ -52,15 +65,22 @@ namespace ILLink.RoslynAnalyzer.DataFlow where TValueLattice : ILattice<TValue> { public readonly DictionaryLattice<LocalKey, TValue, TValueLattice> Lattice; + public readonly DictionaryLattice<CaptureId, CapturedReferenceValue, CapturedReferenceLattice> CapturedReferenceLattice; public LocalStateLattice (TValueLattice valueLattice) { Lattice = new DictionaryLattice<LocalKey, TValue, TValueLattice> (valueLattice); + CapturedReferenceLattice = new DictionaryLattice<CaptureId, CapturedReferenceValue, CapturedReferenceLattice> (new CapturedReferenceLattice ()); Top = new (Lattice.Top); } public LocalState<TValue> Top { get; } - public LocalState<TValue> Meet (LocalState<TValue> left, LocalState<TValue> right) => new (Lattice.Meet (left.Dictionary, right.Dictionary)); + public LocalState<TValue> Meet (LocalState<TValue> left, LocalState<TValue> right) + { + var dictionary = Lattice.Meet (left.Dictionary, right.Dictionary); + var capturedProperties = CapturedReferenceLattice.Meet (left.CapturedReferences, right.CapturedReferences); + return new LocalState<TValue> (dictionary, capturedProperties); + } } } diff --git a/src/ILLink.RoslynAnalyzer/TrimAnalysis/TrimAnalysisVisitor.cs b/src/ILLink.RoslynAnalyzer/TrimAnalysis/TrimAnalysisVisitor.cs index 13fc5ca6c..f1992f492 100644 --- a/src/ILLink.RoslynAnalyzer/TrimAnalysis/TrimAnalysisVisitor.cs +++ b/src/ILLink.RoslynAnalyzer/TrimAnalysis/TrimAnalysisVisitor.cs @@ -9,6 +9,7 @@ using ILLink.Shared.TrimAnalysis; using ILLink.Shared.TypeSystemProxy; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.FlowAnalysis; using Microsoft.CodeAnalysis.Operations; using MultiValue = ILLink.Shared.DataFlow.ValueSet<ILLink.Shared.DataFlow.SingleValue>; @@ -32,8 +33,9 @@ namespace ILLink.RoslynAnalyzer.TrimAnalysis public TrimAnalysisVisitor ( LocalStateLattice<MultiValue, ValueSetLattice<SingleValue>> lattice, - OperationBlockAnalysisContext context - ) : base (lattice, context) + OperationBlockAnalysisContext context, + ImmutableDictionary<CaptureId, FlowCaptureKind> lValueFlowCaptures + ) : base (lattice, context, lValueFlowCaptures) { _multiValueLattice = lattice.Lattice.ValueLattice; TrimAnalysisPatterns = new TrimAnalysisPatternStore (_multiValueLattice); diff --git a/src/ILLink.RoslynAnalyzer/TrimAnalysis/TrimDataFlowAnalysis.cs b/src/ILLink.RoslynAnalyzer/TrimAnalysis/TrimDataFlowAnalysis.cs index 05ddd21c7..f688b782d 100644 --- a/src/ILLink.RoslynAnalyzer/TrimAnalysis/TrimDataFlowAnalysis.cs +++ b/src/ILLink.RoslynAnalyzer/TrimAnalysis/TrimDataFlowAnalysis.cs @@ -39,7 +39,8 @@ namespace ILLink.RoslynAnalyzer.TrimAnalysis public TrimAnalysisPatternStore ComputeTrimAnalysisPatterns () { - var visitor = new TrimAnalysisVisitor (Lattice, Context); + var lValueFlowCaptures = LValueFlowCapturesProvider.CreateLValueFlowCaptures (ControlFlowGraph.ControlFlowGraph); + var visitor = new TrimAnalysisVisitor (Lattice, Context, lValueFlowCaptures); Fixpoint (ControlFlowGraph, Lattice, visitor); return visitor.TrimAnalysisPatterns; } diff --git a/test/Mono.Linker.Tests.Cases/DataFlow/ArrayDataFlow.cs b/test/Mono.Linker.Tests.Cases/DataFlow/ArrayDataFlow.cs index 161889350..fb40089e4 100644 --- a/test/Mono.Linker.Tests.Cases/DataFlow/ArrayDataFlow.cs +++ b/test/Mono.Linker.Tests.Cases/DataFlow/ArrayDataFlow.cs @@ -20,6 +20,8 @@ namespace Mono.Linker.Tests.Cases.DataFlow TestArrayWithInitializerMultipleElementsMix<TestType> (typeof (TestType)); TestArraySetElementOneElementStaticType (); + TestArraySetElementOneElementMix (); + TestArraySetElementOneElementMerged (); TestArraySetElementOneElementParameter (typeof (TestType)); TestArraySetElementMultipleElementsStaticType (); TestMergedArrayElement (1); @@ -39,6 +41,8 @@ namespace Mono.Linker.Tests.Cases.DataFlow TestArrayResetAfterCall (); TestArrayResetAfterAssignment (); TestMultiDimensionalArray.Test (); + + WriteCapturedArrayElement.Test (); } [ExpectedWarning ("IL2062", nameof (DataFlowTypeExtensions.RequiresPublicMethods))] @@ -89,6 +93,37 @@ namespace Mono.Linker.Tests.Cases.DataFlow arr[1].RequiresPublicMethods (); // Should warn - unknown value at this index } + [ExpectedWarning ("IL2072", nameof (GetUnknownType), nameof (DataFlowTypeExtensions.RequiresAll), + ProducedBy = ProducedBy.Trimmer)] + [ExpectedWarning ("IL2072", nameof (GetTypeWithPublicConstructors), nameof (DataFlowTypeExtensions.RequiresAll), + ProducedBy = ProducedBy.Trimmer)] + // https://github.com/dotnet/linker/issues/2736 + [ExpectedWarning ("IL2062", nameof (DataFlowTypeExtensions.RequiresAll), + ProducedBy = ProducedBy.Analyzer)] + static void TestArraySetElementOneElementMix () + { + Type[] arr = new Type[1]; + if (string.Empty.Length == 0) + arr[0] = GetUnknownType (); + else + arr[0] = GetTypeWithPublicConstructors (); + arr[0].RequiresAll (); + } + + [ExpectedWarning ("IL2072", nameof (GetUnknownType), nameof (DataFlowTypeExtensions.RequiresAll), + ProducedBy = ProducedBy.Analyzer)] + [ExpectedWarning ("IL2072", nameof (GetTypeWithPublicConstructors), nameof (DataFlowTypeExtensions.RequiresAll), + ProducedBy = ProducedBy.Analyzer)] + // https://github.com/dotnet/linker/issues/2737 + [ExpectedWarning ("IL2062", nameof (DataFlowTypeExtensions.RequiresAll), + ProducedBy = ProducedBy.Trimmer)] + static void TestArraySetElementOneElementMerged () + { + Type[] arr = new Type[1]; + arr[0] = string.Empty.Length == 0 ? GetUnknownType () : GetTypeWithPublicConstructors (); + arr[0].RequiresAll (); + } + [ExpectedWarning ("IL2062", nameof (DataFlowTypeExtensions.RequiresPublicMethods))] static void TestArraySetElementOneElementParameter ([DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] Type type) { @@ -483,6 +518,64 @@ namespace Mono.Linker.Tests.Cases.DataFlow static Type[,] _externalArray; } + class WriteCapturedArrayElement + { + [ExpectedWarning ("IL2072", nameof (GetUnknownType), nameof (DataFlowTypeExtensions.RequiresAll), + ProducedBy = ProducedBy.Analyzer)] + [ExpectedWarning ("IL2072", nameof (GetTypeWithPublicConstructors), nameof (DataFlowTypeExtensions.RequiresAll), + ProducedBy = ProducedBy.Analyzer)] + // https://github.com/dotnet/linker/issues/2737 + [ExpectedWarning ("IL2062", nameof (DataFlowTypeExtensions.RequiresAll), + ProducedBy = ProducedBy.Trimmer)] + static void TestNullCoalesce () + { + Type[] arr = new Type[1]; + arr[0] = GetUnknownType () ?? GetTypeWithPublicConstructors (); + arr[0].RequiresAll (); + } + + [ExpectedWarning ("IL2072", nameof (GetUnknownType), nameof (DataFlowTypeExtensions.RequiresAll), + ProducedBy = ProducedBy.Trimmer)] + // https://github.com/dotnet/linker/issues/2736 + [ExpectedWarning ("IL2062", nameof (DataFlowTypeExtensions.RequiresAll), + ProducedBy = ProducedBy.Analyzer)] + static void TestNullCoalescingAssignment () + { + Type[] arr = new Type[1]; + arr[0] ??= GetUnknownType (); + arr[0].RequiresAll (); + } + + // Both linker and analyzer get this wrong. They should produce IL2072 instead. + // https://github.com/dotnet/linker/issues/2736 + // https://github.com/dotnet/linker/issues/2737 + [ExpectedWarning ("IL2062", nameof (DataFlowTypeExtensions.RequiresAll))] + static void TestNullCoalescingAssignmentComplex () + { + Type[] arr = new Type[1]; + arr[0] ??= (GetUnknownType () ?? GetTypeWithPublicConstructors ()); + arr[0].RequiresAll (); + } + + public static void Test () + { + TestNullCoalesce (); + TestNullCoalescingAssignment (); + TestNullCoalescingAssignmentComplex (); + } + } + + [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors)] + private static Type GetTypeWithPublicConstructors () + { + return null; + } + + private static Type GetUnknownType () + { + return null; + } + public class TestType { } } } diff --git a/test/Mono.Linker.Tests.Cases/DataFlow/FieldDataFlow.cs b/test/Mono.Linker.Tests.Cases/DataFlow/FieldDataFlow.cs index 3809faa57..e432c6cc2 100644 --- a/test/Mono.Linker.Tests.Cases/DataFlow/FieldDataFlow.cs +++ b/test/Mono.Linker.Tests.Cases/DataFlow/FieldDataFlow.cs @@ -33,6 +33,8 @@ namespace Mono.Linker.Tests.Cases.DataFlow instance.WriteUnknownValue (); + WriteCapturedField.Test (); + _ = _annotationOnWrongType; TestStringEmpty (); @@ -155,6 +157,39 @@ namespace Mono.Linker.Tests.Cases.DataFlow RequirePublicMethods (string.Empty); } + class WriteCapturedField + { + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] + static Type field; + + [ExpectedWarning ("IL2074", nameof (GetUnknownType), nameof (field))] + [ExpectedWarning ("IL2074", nameof (GetTypeWithPublicConstructors), nameof (field))] + static void TestNullCoalesce () + { + field = GetUnknownType () ?? GetTypeWithPublicConstructors (); + } + + [ExpectedWarning ("IL2074", nameof (GetUnknownType), nameof (field))] + static void TestNullCoalescingAssignment () + { + field ??= GetUnknownType (); + } + + [ExpectedWarning ("IL2074", nameof (GetUnknownType), nameof (field))] + [ExpectedWarning ("IL2074", nameof (GetTypeWithPublicConstructors), nameof (field))] + static void TestNullCoalescingAssignmentComplex () + { + field ??= GetUnknownType () ?? GetTypeWithPublicConstructors (); + } + + public static void Test () + { + TestNullCoalesce (); + TestNullCoalescingAssignment (); + TestNullCoalescingAssignmentComplex (); + } + } + private static void RequirePublicMethods ( [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] string s) diff --git a/test/Mono.Linker.Tests.Cases/DataFlow/LocalDataFlow.cs b/test/Mono.Linker.Tests.Cases/DataFlow/LocalDataFlow.cs index 1f6347d3b..b864be13e 100644 --- a/test/Mono.Linker.Tests.Cases/DataFlow/LocalDataFlow.cs +++ b/test/Mono.Linker.Tests.Cases/DataFlow/LocalDataFlow.cs @@ -19,6 +19,10 @@ namespace Mono.Linker.Tests.Cases.DataFlow // These behave as expected TestBranchMergeGoto (); TestBranchMergeIf (); + TestBranchMergeNullCoalesce (); + TestBranchMergeNullCoalescingAssignment (); + TestBranchMergeNullCoalescingAssignmentComplex (); + TestBranchMergeDiscardNullCoalesce (); TestBranchMergeIfElse (); TestBranchMergeSwitch (); TestBranchMergeTry (); @@ -83,6 +87,43 @@ namespace Mono.Linker.Tests.Cases.DataFlow str.RequiresPublicMethods (); // warns for GetWithPublicFields } + [ExpectedWarning ("IL2072", nameof (GetWithPublicMethods), nameof (DataFlowStringExtensions.RequiresAll))] + [ExpectedWarning ("IL2072", nameof (GetWithPublicFields), nameof (DataFlowStringExtensions.RequiresAll))] + public static void TestBranchMergeNullCoalesce () + { + string str = GetWithPublicMethods () ?? GetWithPublicFields (); + + str.RequiresAll (); + } + + [ExpectedWarning ("IL2072", nameof (GetWithPublicMethods), nameof (DataFlowStringExtensions.RequiresAll))] + [ExpectedWarning ("IL2072", nameof (GetWithPublicFields), nameof (DataFlowStringExtensions.RequiresAll))] + public static void TestBranchMergeNullCoalescingAssignment () + { + string str = GetWithPublicMethods (); + str ??= GetWithPublicFields (); + + str.RequiresAll (); + } + + [ExpectedWarning ("IL2072", nameof (GetWithPublicMethods), nameof (DataFlowStringExtensions.RequiresAll))] + [ExpectedWarning ("IL2072", nameof (GetWithPublicFields), nameof (DataFlowStringExtensions.RequiresAll))] + [ExpectedWarning ("IL2072", nameof (GetWithPublicConstructors), nameof (DataFlowStringExtensions.RequiresAll))] + public static void TestBranchMergeNullCoalescingAssignmentComplex () + { + string str = GetWithPublicMethods (); + str ??= GetWithPublicFields () ?? GetWithPublicConstructors (); + + str.RequiresAll (); + } + + [ExpectedWarning ("IL2072", nameof (GetWithPublicMethods), nameof (DataFlowStringExtensions.RequiresAll))] + [ExpectedWarning ("IL2072", nameof (GetWithPublicFields), nameof (DataFlowStringExtensions.RequiresAll))] + public static void TestBranchMergeDiscardNullCoalesce () + { + (_ = GetWithPublicMethods () ?? GetWithPublicFields ()).RequiresAll (); + } + [ExpectedWarning ("IL2072", "Mono.Linker.Tests.Cases.DataFlow.LocalDataFlow.GetWithPublicMethods()", nameof (DataFlowStringExtensions) + "." + nameof (DataFlowStringExtensions.RequiresPublicFields) + "(String)")] @@ -411,35 +452,6 @@ namespace Mono.Linker.Tests.Cases.DataFlow return resultType; } - public static void RequireAll ( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] - string type) - { - } - - public static void RequirePublicFields ( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] - string type) - { - } - - public static void RequireNonPublicMethods ( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.NonPublicMethods)] - string type) - { - } - public static void RequirePublicMethods ( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] - string type) - { - } - - public static void RequirePublicConstructors ( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] - string type) - { - } - [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicFields)] public static string GetWithPublicFields () { diff --git a/test/Mono.Linker.Tests.Cases/DataFlow/MethodParametersDataFlow.cs b/test/Mono.Linker.Tests.Cases/DataFlow/MethodParametersDataFlow.cs index 7c4a0bdd3..77de9327a 100644 --- a/test/Mono.Linker.Tests.Cases/DataFlow/MethodParametersDataFlow.cs +++ b/test/Mono.Linker.Tests.Cases/DataFlow/MethodParametersDataFlow.cs @@ -42,6 +42,8 @@ namespace Mono.Linker.Tests.Cases.DataFlow ParametersPassedToInstanceCtor (typeof (TestType), typeof (TestType)); TestParameterOverwrite (typeof (TestType)); + + WriteCapturedParameter.Test (); } // Validate the error message when annotated parameter is passed to another annotated parameter @@ -235,11 +237,52 @@ namespace Mono.Linker.Tests.Cases.DataFlow type.GetFields (); } + class WriteCapturedParameter + { + [ExpectedWarning ("IL2072", nameof (GetUnknownType), "parameter")] + [ExpectedWarning ("IL2072", nameof (GetTypeWithPublicConstructors), "parameter")] + static void TestNullCoalesce ([DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] Type parameter = null) + { + parameter = GetUnknownType () ?? GetTypeWithPublicConstructors (); + } + + [ExpectedWarning ("IL2072", nameof (GetUnknownType), "parameter")] + static void TestNullCoalescingAssignment ([DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] Type parameter = null) + { + parameter ??= GetUnknownType (); + } + + [ExpectedWarning ("IL2072", nameof (GetUnknownType), "parameter")] + [ExpectedWarning ("IL2072", nameof (GetTypeWithPublicConstructors), "parameter")] + static void TestNullCoalescingAssignmentComplex ([DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] Type parameter = null) + { + parameter ??= (GetUnknownType () ?? GetTypeWithPublicConstructors ()); + } + + public static void Test () + { + TestNullCoalesce (); + TestNullCoalescingAssignment (); + TestNullCoalescingAssignmentComplex (); + } + } + class TestType { public TestType () { } public TestType (int arg) { } private TestType (int arg1, int arg2) { } } + + [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors)] + private static Type GetTypeWithPublicConstructors () + { + return null; + } + + private static Type GetUnknownType () + { + return null; + } } } diff --git a/test/Mono.Linker.Tests.Cases/DataFlow/PropertyDataFlow.cs b/test/Mono.Linker.Tests.Cases/DataFlow/PropertyDataFlow.cs index 424992e9d..188bc8672 100644 --- a/test/Mono.Linker.Tests.Cases/DataFlow/PropertyDataFlow.cs +++ b/test/Mono.Linker.Tests.Cases/DataFlow/PropertyDataFlow.cs @@ -35,8 +35,12 @@ namespace Mono.Linker.Tests.Cases.DataFlow TestAutomaticPropagation (); + WriteCapturedProperty.Test (); + WriteCapturedGetOnlyProperty.Test (); + ReadCapturedProperty.Test (); + PropertyWithAttributeMarkingItself.Test (); - new TestWriteToGetOnlyProperty (); + WriteToGetOnlyProperty.Test (); } [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors)] @@ -437,18 +441,110 @@ namespace Mono.Linker.Tests.Cases.DataFlow } } - class TestWriteToGetOnlyProperty + + class WriteToGetOnlyProperty { [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] public Type GetOnlyProperty { get; } - // Analyzer doesn't warn about compiler-generated backing field of property - [ExpectedWarning ("IL2074", nameof (TestWriteToGetOnlyProperty), nameof (GetUnknownType), + // Analyzer doesn't warn about compiler-generated backing field of property: https://github.com/dotnet/linker/issues/2731 + [ExpectedWarning ("IL2074", nameof (WriteToGetOnlyProperty), nameof (GetUnknownType), ProducedBy = ProducedBy.Trimmer)] - public TestWriteToGetOnlyProperty () + public WriteToGetOnlyProperty () { GetOnlyProperty = GetUnknownType (); } + + public static void Test () + { + new WriteToGetOnlyProperty (); + } + } + + class WriteCapturedProperty + { + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] + static Type Property { get; set; } + + [ExpectedWarning ("IL2072", nameof (GetUnknownType), nameof (Property))] + [ExpectedWarning ("IL2072", nameof (GetTypeWithPublicConstructors), nameof (Property))] + static void TestNullCoalesce () + { + Property = GetUnknownType () ?? GetTypeWithPublicConstructors (); + } + + [ExpectedWarning ("IL2072", nameof (GetUnknownType), nameof (Property))] + static void TestNullCoalescingAssignment () + { + Property ??= GetUnknownType (); + } + + class NestedType + { + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] + public Type Property { get; set; } + } + + NestedType NestedTypeProperty { get; set; } + + [ExpectedWarning ("IL2072", nameof (GetUnknownType), nameof (Property))] + [ExpectedWarning ("IL2072", nameof (GetTypeWithPublicConstructors), nameof (Property))] + void TestNestedNullCoalescingAssignment () + { + NestedTypeProperty.Property = GetUnknownType () ?? GetTypeWithPublicConstructors (); + } + + [ExpectedWarning ("IL2072", nameof (GetUnknownType), nameof (Property))] + [ExpectedWarning ("IL2072", nameof (GetTypeWithPublicConstructors), nameof (Property))] + static void TestNullCoalescingAssignmentComplex () + { + Property ??= (GetUnknownType () ?? GetTypeWithPublicConstructors ()); + } + + public static void Test () + { + TestNullCoalesce (); + TestNullCoalescingAssignment (); + TestNullCoalescingAssignmentComplex (); + new WriteCapturedProperty ().TestNestedNullCoalescingAssignment (); + } + } + + class WriteCapturedGetOnlyProperty + { + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] + Type GetOnlyProperty { get; } + + // Analyzer doesn't warn about compiler-generated backing field of property: https://github.com/dotnet/linker/issues/2731 + [ExpectedWarning ("IL2074", nameof (WriteCapturedGetOnlyProperty), nameof (GetUnknownType), + ProducedBy = ProducedBy.Trimmer)] + [ExpectedWarning ("IL2074", nameof (WriteCapturedGetOnlyProperty), nameof (GetTypeWithPublicConstructors), + ProducedBy = ProducedBy.Trimmer)] + public WriteCapturedGetOnlyProperty () + { + GetOnlyProperty = GetUnknownType () ?? GetTypeWithPublicConstructors (); + } + + public static void Test () + { + new WriteCapturedGetOnlyProperty (); + } + } + + class ReadCapturedProperty + { + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] + static Type PublicMethods { get; } + + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicFields)] + static Type PublicFields { get; } + + [ExpectedWarning ("IL2072", nameof (PublicMethods), nameof (DataFlowTypeExtensions.RequiresAll))] + [ExpectedWarning ("IL2072", nameof (PublicFields), nameof (DataFlowTypeExtensions.RequiresAll))] + public static void Test () + { + (PublicMethods ?? PublicFields).RequiresAll (); + } } [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] |