mirror of
https://github.com/icsharpcode/ILSpy.git
synced 2026-10-12 02:15:58 +00:00
AccessedThroughProperty-backed controls need to remain addressable as fields so WinForms initialization can use compiler-generated backing storage without hiding the setter that maintains WithEvents subscriptions. Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
485 lines
20 KiB
C#
485 lines
20 KiB
C#
// Copyright (c) 2026 Siegfried Pammer
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
|
// software and associated documentation files (the "Software"), to deal in the Software
|
|
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
|
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
|
// to whom the Software is furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in all copies or
|
|
// substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
|
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
|
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
// DEALINGS IN THE SOFTWARE.
|
|
|
|
#nullable enable
|
|
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Linq;
|
|
using System.Reflection.Metadata;
|
|
using System.Threading;
|
|
|
|
using ICSharpCode.Decompiler.CSharp.Syntax;
|
|
using ICSharpCode.Decompiler.CSharp.Transforms;
|
|
using ICSharpCode.Decompiler.Semantics;
|
|
using ICSharpCode.Decompiler.IL;
|
|
using ICSharpCode.Decompiler.Metadata;
|
|
using ICSharpCode.Decompiler.TypeSystem;
|
|
|
|
namespace ICSharpCode.Decompiler.CSharp
|
|
{
|
|
/// <summary>
|
|
/// Recognizes automatic events by structurally matching the ILAst of the compiler-generated
|
|
/// add/remove accessors. The accessor bodies are decompiled with a fixed set of decompiler
|
|
/// settings (see <see cref="CSharpDecompiler.DecompileBodyForAnalysis"/>), so recognition is
|
|
/// independent of the user-visible settings that shape the final C# output.
|
|
/// </summary>
|
|
static class AutoEventDecompiler
|
|
{
|
|
internal sealed class VisualBasicWithEventsInfo
|
|
{
|
|
public IProperty Property { get; }
|
|
public IField BackingField { get; }
|
|
public IReadOnlyList<VisualBasicWithEventsSubscription> Subscriptions { get; }
|
|
|
|
public VisualBasicWithEventsInfo(IProperty property, IField backingField, IReadOnlyList<VisualBasicWithEventsSubscription> subscriptions)
|
|
{
|
|
Property = property;
|
|
BackingField = backingField;
|
|
Subscriptions = subscriptions;
|
|
}
|
|
}
|
|
|
|
internal sealed class VisualBasicWithEventsSubscription
|
|
{
|
|
public IEvent Event { get; }
|
|
public IMethod Handler { get; }
|
|
|
|
public VisualBasicWithEventsSubscription(IEvent ev, IMethod handler)
|
|
{
|
|
Event = ev;
|
|
Handler = handler;
|
|
}
|
|
}
|
|
|
|
public static bool TryAnalyzeVisualBasicWithEventsProperty(PropertyDeclaration propertyDeclaration,
|
|
[NotNullWhen(true)] out VisualBasicWithEventsInfo? info)
|
|
{
|
|
info = null;
|
|
if (propertyDeclaration.GetSymbol() is not IProperty property || propertyDeclaration.Setter?.Body == null)
|
|
return false;
|
|
if (property.Parameters.Count != 0 || property.Setter == null)
|
|
return false;
|
|
|
|
IField? backingField = null;
|
|
foreach (var assignment in propertyDeclaration.Setter.Body.Descendants.OfType<AssignmentExpression>())
|
|
{
|
|
if (assignment.Operator != AssignmentOperatorType.Assign)
|
|
continue;
|
|
if (assignment.Right is not IdentifierExpression { Identifier: "value" })
|
|
continue;
|
|
if (assignment.Left.GetSymbol() is not IField field)
|
|
continue;
|
|
field = (IField)field.MemberDefinition;
|
|
if (backingField != null && !backingField.Equals(field))
|
|
return false;
|
|
backingField = field;
|
|
}
|
|
if (backingField == null || !LooksLikeVisualBasicWithEventsBackingField(property, backingField))
|
|
return false;
|
|
|
|
var localHandlers = new Dictionary<string, IMethod>();
|
|
foreach (var declaration in propertyDeclaration.Setter.Body.Descendants.OfType<VariableDeclarationStatement>())
|
|
{
|
|
foreach (var variable in declaration.Variables)
|
|
{
|
|
if (TryGetEventHandler(variable.Initializer, out var handler))
|
|
{
|
|
localHandlers[variable.Name] = handler;
|
|
}
|
|
}
|
|
}
|
|
|
|
var added = new List<VisualBasicWithEventsSubscription>();
|
|
var removed = new List<VisualBasicWithEventsSubscription>();
|
|
foreach (var assignment in propertyDeclaration.Setter.Body.Descendants.OfType<AssignmentExpression>())
|
|
{
|
|
if (assignment.Operator is not (AssignmentOperatorType.Add or AssignmentOperatorType.Subtract))
|
|
continue;
|
|
if (assignment.Left.GetSymbol() is not IEvent ev)
|
|
continue;
|
|
if (!TryGetEventHandler(assignment.Right, out var handler))
|
|
{
|
|
if (assignment.Right is IdentifierExpression identifier
|
|
&& localHandlers.TryGetValue(identifier.Identifier, out var localHandler))
|
|
{
|
|
handler = localHandler;
|
|
}
|
|
else
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
var subscription = new VisualBasicWithEventsSubscription(ev, handler);
|
|
if (assignment.Operator == AssignmentOperatorType.Add)
|
|
added.Add(subscription);
|
|
else
|
|
removed.Add(subscription);
|
|
}
|
|
|
|
if (added.Count == 0 || removed.Count != added.Count)
|
|
return false;
|
|
foreach (var subscription in added)
|
|
{
|
|
if (!removed.Any(r => r.Event.Equals(subscription.Event) && r.Handler.Equals(subscription.Handler)))
|
|
return false;
|
|
}
|
|
|
|
info = new VisualBasicWithEventsInfo(property, backingField, added);
|
|
return true;
|
|
}
|
|
|
|
static bool LooksLikeVisualBasicWithEventsBackingField(IProperty property, IField field)
|
|
{
|
|
if (field.DeclaringTypeDefinition == null || !field.DeclaringTypeDefinition.Equals(property.DeclaringTypeDefinition))
|
|
return false;
|
|
if (field.IsStatic != property.IsStatic)
|
|
return false;
|
|
if (!NormalizeTypeVisitor.IgnoreNullability.EquivalentTypes(field.ReturnType, property.ReturnType))
|
|
return false;
|
|
if (field.Name == "_" + property.Name)
|
|
return true;
|
|
foreach (var attr in field.GetAttributes())
|
|
{
|
|
if (attr.AttributeType.FullName != "Microsoft.VisualBasic.CompilerServices.AccessedThroughPropertyAttribute")
|
|
continue;
|
|
if (attr.FixedArguments.Length == 1 && attr.FixedArguments[0].Value as string == property.Name)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool TryGetEventHandler(Expression? expression, [NotNullWhen(true)] out IMethod? handler)
|
|
{
|
|
handler = null;
|
|
Expression? target = expression;
|
|
if (target is ObjectCreateExpression objectCreate && objectCreate.Arguments.Count == 1)
|
|
{
|
|
target = objectCreate.Arguments.Single();
|
|
}
|
|
handler = target?.GetSymbol() as IMethod;
|
|
return handler != null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines whether <paramref name="ev"/> is an automatic event, memoizing the verdict
|
|
/// in <paramref name="decompileRun"/> so that all consumers decide from the same analysis.
|
|
/// </summary>
|
|
public static bool IsAutomaticEvent(IDecompilerTypeSystem typeSystem, IEvent ev, DecompileRun decompileRun, CancellationToken cancellationToken, [NotNullWhen(true)] out IField? backingField)
|
|
{
|
|
if (!decompileRun.AutomaticEvents.TryGetValue(ev, out backingField))
|
|
{
|
|
backingField = IsAutomaticEvent(typeSystem, ev, cancellationToken, out var field) ? field : null;
|
|
decompileRun.AutomaticEvents.Add(ev, backingField);
|
|
}
|
|
return backingField != null;
|
|
}
|
|
|
|
static bool IsAutomaticEvent(IDecompilerTypeSystem typeSystem, IEvent ev, CancellationToken cancellationToken, [NotNullWhen(true)] out IField? backingField)
|
|
{
|
|
backingField = null;
|
|
if (ev.IsExplicitInterfaceImplementation || ev.DeclaringTypeDefinition == null)
|
|
return false;
|
|
if (ev.AddAccessor is not { HasBody: true } addAccessor || ev.RemoveAccessor is not { HasBody: true } removeAccessor)
|
|
return false;
|
|
backingField = FindBackingField(ev);
|
|
if (backingField == null)
|
|
return false;
|
|
// ignore tuple element names, dynamic and nullability
|
|
if (!NormalizeTypeVisitor.TypeErasure.EquivalentTypes(ev.ReturnType, backingField.ReturnType))
|
|
return false;
|
|
return IsAutomaticAccessor(typeSystem, addAccessor, backingField, isAddAccessor: true, cancellationToken)
|
|
&& IsAutomaticAccessor(typeSystem, removeAccessor, backingField, isAddAccessor: false, cancellationToken);
|
|
}
|
|
|
|
static readonly string[] attributeTypesToRemoveFromAutoEventAccessors = new[] {
|
|
"System.Runtime.CompilerServices.CompilerGeneratedAttribute",
|
|
"System.Diagnostics.DebuggerBrowsableAttribute",
|
|
"System.Runtime.CompilerServices.MethodImplAttribute"
|
|
};
|
|
|
|
static readonly string[] attributeTypesToRemoveFromAutoEventFields = new[] {
|
|
"System.Runtime.CompilerServices.CompilerGeneratedAttribute",
|
|
"System.Diagnostics.DebuggerBrowsableAttribute"
|
|
};
|
|
|
|
/// <summary>
|
|
/// Adds the add-accessor and backing-field attributes of an automatic event to its
|
|
/// field-like declaration (as "method:" and "field:" sections), dropping the attributes
|
|
/// the compiler puts on automatic events.
|
|
/// </summary>
|
|
internal static void AddFieldLikeEventAttributes(EventDeclaration eventDecl, TypeSystemAstBuilder astBuilder, IEvent ev, IField backingField)
|
|
{
|
|
eventDecl.Attributes.AddRange(astBuilder.ConvertAttributes(
|
|
WithoutAttributeTypes(ev.AddAccessor!.GetAttributes(), attributeTypesToRemoveFromAutoEventAccessors), "method"));
|
|
eventDecl.Attributes.AddRange(astBuilder.ConvertAttributes(
|
|
WithoutAttributeTypes(backingField.GetAttributes(), attributeTypesToRemoveFromAutoEventFields), "field"));
|
|
}
|
|
|
|
static IEnumerable<IAttribute> WithoutAttributeTypes(IEnumerable<IAttribute> attributes, string[] attributeTypesToRemove)
|
|
{
|
|
return attributes.Where(a => !attributeTypesToRemove.Contains(a.AttributeType.FullName));
|
|
}
|
|
|
|
static IField? FindBackingField(IEvent ev)
|
|
{
|
|
if (ev.ParentModule is not MetadataModule module || ev.DeclaringTypeDefinition is not { } declaringType)
|
|
return null;
|
|
var lookup = module.MetadataFile.PropertyAndEventBackingFieldLookup;
|
|
foreach (var field in declaringType.Fields)
|
|
{
|
|
if (field.MetadataToken.IsNil)
|
|
continue;
|
|
if (!lookup.IsEventBackingField((FieldDefinitionHandle)field.MetadataToken, out var eventHandle))
|
|
continue;
|
|
if (eventHandle != (EventDefinitionHandle)ev.MetadataToken)
|
|
continue;
|
|
return field.Accessibility == Accessibility.Private && field.IsStatic == ev.IsStatic ? field : null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static bool IsAutomaticAccessor(IDecompilerTypeSystem typeSystem, IMethod accessor, IField field, bool isAddAccessor, CancellationToken cancellationToken)
|
|
{
|
|
var body = CSharpDecompiler.DecompileBodyForAnalysis(accessor, typeSystem, cancellationToken);
|
|
if (body == null)
|
|
return false;
|
|
if (body.Parent is not BlockContainer functionBody || functionBody.Blocks.Count != 1)
|
|
return false;
|
|
return MatchCompareExchangeLoop(body, functionBody, accessor, field, isAddAccessor)
|
|
|| MatchSimpleCombineAssignment(body, functionBody, accessor, field, isAddAccessor)
|
|
|| MatchCompareExchangeLoopMcs(body, functionBody, accessor, field, isAddAccessor);
|
|
}
|
|
|
|
// Matches the thread-safe compare-exchange loop generated by csc 4 and Roslyn (C# and VB):
|
|
// T oldValue = this.field;
|
|
// while (true) {
|
|
// T comparand = oldValue;
|
|
// T newValue = (T)Delegate.Combine(comparand, value);
|
|
// oldValue = Interlocked.CompareExchange(ref this.field, newValue, comparand);
|
|
// if (oldValue == comparand) break;
|
|
// }
|
|
static bool MatchCompareExchangeLoop(Block body, BlockContainer functionBody, IMethod accessor, IField field, bool isAddAccessor)
|
|
{
|
|
if (body.Instructions.Count != 3)
|
|
return false;
|
|
if (!body.Instructions[0].MatchStLoc(out var oldValue, out var init))
|
|
return false;
|
|
if (!MatchLoadOfField(init, accessor, field))
|
|
return false;
|
|
if (body.Instructions[1] is not BlockContainer { Kind: ContainerKind.Loop } loop || loop.Blocks.Count != 1)
|
|
return false;
|
|
if (!body.Instructions[2].MatchLeave(functionBody))
|
|
return false;
|
|
var loopBody = loop.EntryPoint;
|
|
if (loopBody.Instructions.Count != 5)
|
|
return false;
|
|
if (!loopBody.Instructions[0].MatchStLoc(out var comparand, out var comparandInit) || comparand == oldValue)
|
|
return false;
|
|
if (!comparandInit.MatchLdLoc(oldValue))
|
|
return false;
|
|
if (!loopBody.Instructions[1].MatchStLoc(out var newValue, out var newValueInit) || newValue == oldValue || newValue == comparand)
|
|
return false;
|
|
if (!MatchCastedDelegateCombineCall(newValueInit, field, comparand, isAddAccessor))
|
|
return false;
|
|
if (!loopBody.Instructions[2].MatchStLoc(oldValue, out var compareExchangeCall))
|
|
return false;
|
|
if (!MatchCompareExchangeCall(compareExchangeCall, accessor, field, valueArg: newValue, comparandArg: comparand))
|
|
return false;
|
|
return MatchLoopExit(loopBody, loop, oldValue, comparand);
|
|
}
|
|
|
|
// Matches the compare-exchange loop generated by mcs, which passes the combined delegate
|
|
// directly to Interlocked.CompareExchange and uses the previous exchange result as comparand:
|
|
// T oldValue = this.field;
|
|
// while (true) {
|
|
// T comparand = oldValue;
|
|
// oldValue = Interlocked.CompareExchange(ref this.field, (T)Delegate.Combine(comparand, value), oldValue);
|
|
// if (oldValue == comparand) break;
|
|
// }
|
|
static bool MatchCompareExchangeLoopMcs(Block body, BlockContainer functionBody, IMethod accessor, IField field, bool isAddAccessor)
|
|
{
|
|
if (body.Instructions.Count != 3)
|
|
return false;
|
|
if (!body.Instructions[0].MatchStLoc(out var oldValue, out var init))
|
|
return false;
|
|
if (!MatchLoadOfField(init, accessor, field))
|
|
return false;
|
|
if (body.Instructions[1] is not BlockContainer { Kind: ContainerKind.Loop } loop || loop.Blocks.Count != 1)
|
|
return false;
|
|
if (!body.Instructions[2].MatchLeave(functionBody))
|
|
return false;
|
|
var loopBody = loop.EntryPoint;
|
|
if (loopBody.Instructions.Count != 4)
|
|
return false;
|
|
if (!loopBody.Instructions[0].MatchStLoc(out var comparand, out var comparandInit) || comparand == oldValue)
|
|
return false;
|
|
if (!comparandInit.MatchLdLoc(oldValue))
|
|
return false;
|
|
if (!loopBody.Instructions[1].MatchStLoc(oldValue, out var compareExchangeCall))
|
|
return false;
|
|
if (compareExchangeCall is not Call call || !IsCompareExchangeMethod(call.Method) || call.Arguments.Count != 3)
|
|
return false;
|
|
if (!MatchAddressOfField(call.Arguments[0], accessor, field))
|
|
return false;
|
|
if (!MatchCastedDelegateCombineCall(call.Arguments[1], field, comparand, isAddAccessor))
|
|
return false;
|
|
if (!call.Arguments[2].MatchLdLoc(oldValue))
|
|
return false;
|
|
return MatchLoopExit(loopBody, loop, oldValue, comparand);
|
|
}
|
|
|
|
// Matches the non-thread-safe shape generated by csc before 4.0 (the accessors are
|
|
// [MethodImpl(Synchronized)] instead):
|
|
// this.field = (T)Delegate.Combine(this.field, value);
|
|
// mcs 2.x compiles the accessor as a compound assignment, evaluating 'this' once into
|
|
// a temporary (IL 'dup'), so the store may be preceded by "stloc S(ldloc this)" with
|
|
// both field accesses going through S.
|
|
static bool MatchSimpleCombineAssignment(Block body, BlockContainer functionBody, IMethod accessor, IField field, bool isAddAccessor)
|
|
{
|
|
ILVariable? thisAlias = null;
|
|
int pos = 0;
|
|
if (!accessor.IsStatic && body.Instructions.Count == 3)
|
|
{
|
|
if (!body.Instructions[0].MatchStLoc(out thisAlias, out var aliasInit) || !aliasInit.MatchLdThis())
|
|
return false;
|
|
if (thisAlias.StoreCount != 1 || thisAlias.LoadCount != 2 || thisAlias.AddressCount != 0)
|
|
return false;
|
|
pos = 1;
|
|
}
|
|
else if (body.Instructions.Count != 2)
|
|
{
|
|
return false;
|
|
}
|
|
ILInstruction? value;
|
|
if (accessor.IsStatic)
|
|
{
|
|
if (!body.Instructions[pos].MatchStsFld(out var f, out value) || !IsSameField(f, field))
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
if (!body.Instructions[pos].MatchStFld(out var target, out var f, out value) || !MatchLdThisOrAlias(target, thisAlias) || !IsSameField(f, field))
|
|
return false;
|
|
}
|
|
if (!body.Instructions[pos + 1].MatchLeave(functionBody))
|
|
return false;
|
|
if (value is not CastClass castclass)
|
|
return false;
|
|
if (!NormalizeTypeVisitor.TypeErasure.EquivalentTypes(castclass.Type, field.ReturnType))
|
|
return false;
|
|
if (castclass.Argument is not Call call || !IsDelegateCombineMethod(call.Method, isAddAccessor) || call.Arguments.Count != 2)
|
|
return false;
|
|
return MatchLoadOfField(call.Arguments[0], accessor, field, thisAlias)
|
|
&& MatchLdValueParameter(call.Arguments[1]);
|
|
}
|
|
|
|
static bool MatchLdThisOrAlias(ILInstruction inst, ILVariable? thisAlias)
|
|
{
|
|
return inst.MatchLdThis() || (thisAlias != null && inst.MatchLdLoc(thisAlias));
|
|
}
|
|
|
|
// Matches the loop exit at the end of the compare-exchange loop body:
|
|
// if (oldValue == comparand) break;
|
|
// i.e. "if (comp.o(ldloc oldValue == ldloc comparand)) leave loop" followed by the back-branch.
|
|
static bool MatchLoopExit(Block loopBody, BlockContainer loop, ILVariable oldValue, ILVariable comparand)
|
|
{
|
|
if (!loopBody.Instructions[loopBody.Instructions.Count - 2].MatchIfInstruction(out var condition, out var trueInst))
|
|
return false;
|
|
if (condition is not Comp { Kind: ComparisonKind.Equality } comp)
|
|
return false;
|
|
if (!comp.Left.MatchLdLoc(oldValue) || !comp.Right.MatchLdLoc(comparand))
|
|
return false;
|
|
if (!trueInst.MatchLeave(loop))
|
|
return false;
|
|
return loopBody.Instructions[loopBody.Instructions.Count - 1].MatchBranch(loopBody);
|
|
}
|
|
|
|
// Matches "(T)Delegate.Combine(comparand, value)" (Delegate.Remove for the remove accessor).
|
|
static bool MatchCastedDelegateCombineCall(ILInstruction inst, IField field, ILVariable comparand, bool isAddAccessor)
|
|
{
|
|
if (inst is not CastClass castclass)
|
|
return false;
|
|
if (!NormalizeTypeVisitor.TypeErasure.EquivalentTypes(castclass.Type, field.ReturnType))
|
|
return false;
|
|
if (castclass.Argument is not Call call || !IsDelegateCombineMethod(call.Method, isAddAccessor) || call.Arguments.Count != 2)
|
|
return false;
|
|
return call.Arguments[0].MatchLdLoc(comparand)
|
|
&& MatchLdValueParameter(call.Arguments[1]);
|
|
}
|
|
|
|
// Matches "Interlocked.CompareExchange(ref this.field, valueArg, comparandArg)".
|
|
static bool MatchCompareExchangeCall(ILInstruction inst, IMethod accessor, IField field, ILVariable valueArg, ILVariable comparandArg)
|
|
{
|
|
if (inst is not Call call || !IsCompareExchangeMethod(call.Method) || call.Arguments.Count != 3)
|
|
return false;
|
|
return MatchAddressOfField(call.Arguments[0], accessor, field)
|
|
&& call.Arguments[1].MatchLdLoc(valueArg)
|
|
&& call.Arguments[2].MatchLdLoc(comparandArg);
|
|
}
|
|
|
|
static bool IsDelegateCombineMethod(IMethod method, bool isAddAccessor)
|
|
{
|
|
return method.IsStatic
|
|
&& method.Name == (isAddAccessor ? "Combine" : "Remove")
|
|
&& method.DeclaringType.FullName == "System.Delegate";
|
|
}
|
|
|
|
static bool IsCompareExchangeMethod(IMethod method)
|
|
{
|
|
return method.IsStatic
|
|
&& method.Name == "CompareExchange"
|
|
&& method.DeclaringType.FullName == "System.Threading.Interlocked";
|
|
}
|
|
|
|
static bool MatchLoadOfField(ILInstruction inst, IMethod accessor, IField field, ILVariable? thisAlias = null)
|
|
{
|
|
if (accessor.IsStatic)
|
|
{
|
|
return inst.MatchLdsFld(out var f) && IsSameField(f, field);
|
|
}
|
|
else
|
|
{
|
|
return inst.MatchLdFld(out var target, out var f) && MatchLdThisOrAlias(target, thisAlias) && IsSameField(f, field);
|
|
}
|
|
}
|
|
|
|
static bool MatchAddressOfField(ILInstruction inst, IMethod accessor, IField field)
|
|
{
|
|
if (accessor.IsStatic)
|
|
{
|
|
return inst.MatchLdsFlda(out var f) && IsSameField(f, field);
|
|
}
|
|
else
|
|
{
|
|
return inst.MatchLdFlda(out var target, out var f) && target.MatchLdThis() && IsSameField(f, field);
|
|
}
|
|
}
|
|
|
|
static bool MatchLdValueParameter(ILInstruction inst)
|
|
{
|
|
return inst.MatchLdLoc(out var v) && v.Kind == VariableKind.Parameter && v.Index == 0;
|
|
}
|
|
|
|
static bool IsSameField(IField a, IField b)
|
|
{
|
|
return a.MemberDefinition.Equals(b.MemberDefinition);
|
|
}
|
|
}
|
|
}
|