• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SamboyCoding / Cpp2IL / 30769998286

02 Aug 2026 10:23PM UTC coverage: 34.169% (-1.8%) from 35.984%
30769998286

push

github

SamboyCoding
Decompiler: Support szarray, virtual calls, address of local, plus clean up ILSpy output by adjusting the IL

2899 of 9930 branches covered (29.19%)

Branch coverage included in aggregate %.

27 of 614 new or added lines in 19 files covered. (4.4%)

9 existing lines in 4 files now uncovered.

5316 of 14112 relevant lines covered (37.67%)

156656.13 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/Cpp2IL.Core/Analysis/MetadataResolver.cs
1
using System.Collections.Generic;
2
using System.Linq;
3
using Cpp2IL.Core.Extensions;
4
using Cpp2IL.Core.Graphs;
5
using Cpp2IL.Core.Il2CppApiFunctions;
6
using Cpp2IL.Core.ISIL;
7
using Cpp2IL.Core.Model.Contexts;
8
using Cpp2IL.Core.Utils;
9
using LibCpp2IL;
10

11
namespace Cpp2IL.Core.Analysis;
12

13
public static class MetadataResolver
14
{
15
    public static void ResolveAll(MethodAnalysisContext method)
16
    {
17
        ResolveCalls(method);
×
18
        ResolveGetter(method);
×
19
        ResolveMetadataUsages(method);
×
20
    }
×
21

22
    /// <summary>
23
    /// Resolves <c>Move local, [absoluteAddress]</c> loads of IL2CPP metadata-usage globals into a
24
    /// strongly-typed operand: a string literal, a <see cref="TypeAnalysisContext"/> (an Il2CppType*/
25
    /// Il2CppClass* usage) or, for a MethodInfo* usage, a <see cref="RuntimeMethodInfoAnalysisContext"/>
26
    /// naming the method it refers to (also used to type the local - see <see cref="LocalVariables"/>).
27
    /// </summary>
28
    private static void ResolveMetadataUsages(MethodAnalysisContext method)
29
    {
30
        var libContext = method.AppContext.LibCpp2IlContext;
×
31

32
        foreach (var instruction in method.ControlFlowGraph!.Instructions)
×
33
        {
34
            if (instruction.OpCode != OpCode.Move)
×
35
                continue;
36

37
            // Only an absolute-address load [addr] (no base/index/scale) can be a metadata-usage global.
38
            if (instruction.Operands[0] is not LocalVariable
×
39
                || instruction.Operands[1] is not MemoryOperand { Base: null, Index: null, Scale: 0 } memory)
×
40
                continue;
41

42
            var address = (ulong)memory.Addend;
×
43

44
            // String literal.
45
            var stringLiteral = libContext.GetLiteralByAddress(address);
×
46
            if (stringLiteral != null)
×
47
            {
48
                instruction.SetOperand(1, new StringLiteral(stringLiteral));
×
49
                continue;
×
50
            }
51

52
            // Type metadata usage (Il2CppType* / Il2CppClass*).
53
            if (method.DeclaringType is { } declaringType)
×
54
            {
55
                var typeGlobal = libContext.GetTypeGlobalByAddress(address);
×
56
                if (typeGlobal != null)
×
57
                {
58
                    instruction.SetOperand(1, declaringType.AppContext.ResolveIl2CppType(typeGlobal));
×
59
                    continue;
×
60
                }
61
            }
62

63
            // Method metadata usage (MethodInfo*). On metadata v27+ GetMethodGlobalByAddress can return
64
            // any global, so confirm it is actually a method before resolving - the resolver's switch
65
            // throws on other usage kinds.
66
            var methodUsage = libContext.GetMethodGlobalByAddress(address);
×
67
            if (methodUsage?.Type is MetadataUsageType.MethodDef or MetadataUsageType.MethodRef
×
68
                && method.AppContext.ResolveContextForMethod(methodUsage) is { DeclaringType: { } methodDeclaringType } methodContext)
×
69
                instruction.SetOperand(1, new RuntimeMethodInfoAnalysisContext(methodContext, methodDeclaringType.DeclaringAssembly));
×
70
        }
71
    }
×
72

73
    /// <summary>
74
    /// Replaces every <c>[base + addend]</c> memory operand whose base is a typed local with a
75
    /// <see cref="FieldReference"/> to the field at that offset. Returns whether any operand was
76
    /// resolved this pass, so the type/field fixpoint can detect convergence: as more bases become
77
    /// typed (a field load types its result, which is the base of the next load), more offsets
78
    /// resolve, so this is re-run until it stops finding new fields.
79
    /// </summary>
80
    public static bool ResolveFieldOffsets(MethodAnalysisContext method)
81
    {
82
        var changed = false;
×
83

84
        foreach (var instruction in method.ControlFlowGraph!.Instructions)
×
85
        {
86
            for (var i = 0; i < instruction.Operands.Count; i++)
×
87
            {
88
                var operand = instruction.Operands[i];
×
89

90
                if (operand is not MemoryOperand memory)
×
91
                    continue;
92

93
                // Has to be [base (local) + addend (field offset)]
94
                if (memory.Index != null || memory.Scale != 0)
×
95
                    continue;
96

97
                if (memory.Base is not LocalVariable local || local?.Type == null)
×
98
                    continue;
99

100
                // check if static field access
101
                var staticOwner = (local.Type as StaticFieldStorageTypeAnalysisContext)?.OwnerType;
×
102
                var owner = staticOwner ?? local.Type;
×
103
                var genericOwner = owner as GenericInstanceTypeAnalysisContext;
×
104

105
                FieldAnalysisContext? field;
106
                if (genericOwner != null && staticOwner == null)
×
107
                {
108
                    // metadata has all-0 offsets for generic definitions, so recompute layout
109
                    // TODO support user-defined value types
110
                    if (genericOwner.GenericArguments.Any(a => a.IsValueType))
×
111
                        continue;
112

113
                    field = GenericInstanceFieldLayout.FindFieldAtOffset(genericOwner, memory.Addend);
×
114
                }
115
                else
116
                {
117
                    // an inherited field exists on the base type but sits at the same offset in the
118
                    // derived layout, so the whole chain is searched
NEW
119
                    field = null;
×
NEW
120
                    for (var candidateOwner = genericOwner?.GenericType ?? owner; candidateOwner != null && field == null; candidateOwner = candidateOwner.BaseType)
×
NEW
121
                        field = candidateOwner.Fields.FirstOrDefault(f => f.IsStatic == (staticOwner != null) && f.BackingData?.FieldOffset == memory.Addend);
×
122
                }
123

124
                if (field == null) // TODO: Support nested fields (Field1.Field2.Field3)
×
125
                    continue;
126

127
                // make sure we have a full GIT for field access. open type is bad.
128
                if (genericOwner != null)
×
129
                    field = new ConcreteGenericFieldAnalysisContext(field, genericOwner);
×
130

131
                instruction.SetOperand(i, new FieldReference(field, local, (int)memory.Addend));
×
132
                changed = true;
×
133
            }
134
        }
135

136
        return changed;
×
137
    }
138

139
    private static void ResolveCalls(MethodAnalysisContext method)
140
    {
141
        foreach (var block in method.ControlFlowGraph!.Blocks)
×
142
        {
143
            if (block.BlockType != BlockType.Call && block.BlockType != BlockType.TailCall)
×
144
                continue;
145

146
            var callInstruction = block.Instructions[^1];
×
147
            if (callInstruction.Operands[0] is not Immediate dest)
×
148
                continue;
149

150
            var target = dest.UnsignedValue;
×
151

152
            var keyFunctionAddresses = method.AppContext.GetOrCreateKeyFunctionAddresses();
×
153

154
            if (keyFunctionAddresses.IsKeyFunctionAddress(target))
×
155
            {
156
                HandleKeyFunction(method.AppContext, callInstruction, target, keyFunctionAddresses);
×
157
                continue;
×
158
            }
159

160
            //Non-key function call. Try to find a single match
161
            if (!method.AppContext.MethodsByAddress.TryGetValue(target, out var targetMethods))
×
162
            {
163
                // Not a managed method at all. It may be one of the runtime helpers that exist purely to
164
                // throw, in which case restore the throw itself
165
                if (ThrowHelperRecovery.GetThrownException(method.AppContext, target) is { } thrown)
×
166
                {
167
                    callInstruction.OpCode = OpCode.Throw;
×
168
                    callInstruction.SetOperands(thrown);
×
169
                }
170

171
                continue;
×
172
            }
173

174
            // Duplicated/Shared method bodies are resolved later in ResolveCallsViaMethodInfo/ResolveAmbiguousCalls.
175
            if (targetMethods is not [{ } singleTargetMethod])
×
176
                continue;
177

178
            callInstruction.SetOperand(0, singleTargetMethod);
×
179
        }
180

181
        method.ControlFlowGraph.MergeCallBlocks();
×
182
    }
×
183

184
    /// <summary>
185
    /// Resolves calls whose address maps to more than one method by matching the receiver's known
186
    /// type against the candidates' declaring types. Runs inside the type/field fixpoint and so
187
    /// re-fires as receivers become typed - a resolved call types its return value, which can type
188
    /// the receiver of a further call. Returns whether any call was resolved this pass.
189
    ///
190
    /// Conservative by design: it commits only when exactly one non-static candidate's declaring
191
    /// type matches the receiver's type. Anything still untyped or ambiguous is left for a later
192
    /// pass, or left unresolved - it never guesses.
193
    /// </summary>
194
    public static bool ResolveAmbiguousCalls(MethodAnalysisContext method)
195
    {
196
        var changed = false;
×
197

198
        foreach (var instruction in method.ControlFlowGraph!.Instructions)
×
199
        {
200
            if (!instruction.IsCall)
×
201
                continue;
202

203
            // A resolved call's target is a method/key-function name; only unresolved ones are still numeric.
204
            if (instruction.Operands[0] is not Immediate target)
×
205
                continue;
206

207
            if (!method.AppContext.MethodsByAddress.TryGetValue(target.UnsignedValue, out var candidates) || candidates.Count < 2)
×
208
                continue;
209

210
            // e.g. string.Equals and string.op_Equality, identical params, instance type, and bodies are shared
211
            // we can't differentiate which is being called but it doesn't matter
NEW
212
            if (AreInterchangeable(candidates))
×
213
            {
NEW
214
                instruction.SetOperand(0, PreferredOf(candidates));
×
NEW
215
                changed = true;
×
NEW
216
                continue;
×
217
            }
218

UNCOV
219
            if (GetReceiver(instruction) is not { Type: { } receiverType } receiver)
×
220
                continue;
221

222
            // Prefer picking base ctor if we are a ctor
223
            var callerIsCtor = method.Name == ".ctor" && receiver.IsThis;
×
224

225
            // Handle methods with shared bodies
226
            var match = default(MethodAnalysisContext);
×
227

228
            for (var type = receiverType; type != null && match == null; type = type.BaseType)
×
229
            {
230
                var matches = candidates.Where(c => !c.IsStatic && ReferenceEquals(c.DeclaringType, type)).ToList();
×
231

232
                if (matches.Count > 1 && callerIsCtor)
×
233
                    matches = matches.Where(c => c.Name == ".ctor").ToList();
×
234

235
                if (matches.Count > 1)
×
236
                    break;
237

238
                match = matches.SingleOrDefault();
×
239
            }
240

241
            if (match == null)
×
242
                continue;
243

244
            instruction.SetOperand(0, match);
×
245
            changed = true;
×
246
        }
247

248
        return changed;
×
249
    }
250

251
    private static bool AreInterchangeable(List<MethodAnalysisContext> candidates)
252
    {
NEW
253
        var first = candidates[0];
×
254

NEW
255
        return candidates.All(c => c.IsStatic == first.IsStatic
×
NEW
256
            && ReferenceEquals(c.DeclaringType, first.DeclaringType)
×
NEW
257
            && ReferenceEquals(c.ReturnType, first.ReturnType)
×
NEW
258
            && c.Parameters.Count == first.Parameters.Count
×
NEW
259
            && SameParameterTypes(c, first));
×
260
    }
261

262
    private static bool SameParameterTypes(MethodAnalysisContext a, MethodAnalysisContext b)
263
    {
NEW
264
        for (var i = 0; i < a.Parameters.Count; i++)
×
265
        {
NEW
266
            if (!ReferenceEquals(a.Parameters[i].ParameterType, b.Parameters[i].ParameterType))
×
NEW
267
                return false;
×
268
        }
269

NEW
270
        return true;
×
271
    }
272

273
    // Prefer operators if possible
274
    private static MethodAnalysisContext PreferredOf(List<MethodAnalysisContext> candidates) =>
NEW
275
        candidates.FirstOrDefault(c => c.Name.StartsWith("op_")) ?? candidates[0];
×
276

277
    // The receiver ('this') of a call is the first integer-slot argument: operand 1 for CallVoid
278
    // (after the target), operand 2 for Call (after the target and the return value).
279
    private static LocalVariable? GetReceiver(Instruction call)
280
    {
281
        var index = call.OpCode == OpCode.CallVoid ? 1 : 2;
×
282
        return index < call.Operands.Count ? call.Operands[index] as LocalVariable : null;
×
283
    }
284

285
    /// <summary>
286
    /// Resolves any Call (theoretically should always be a CallVoid) target directly after a Newobj to a constructor call.
287
    /// </summary>
288
    public static bool ResolveConstructorCalls(MethodAnalysisContext method)
289
    {
290
        var definitions = new Dictionary<LocalVariable, Instruction>();
×
291
        foreach (var instruction in method.ControlFlowGraph!.Instructions)
×
292
            if (instruction.Destination is LocalVariable definition)
×
293
                definitions[definition] = instruction;
×
294

295
        var changed = false;
×
296

297
        foreach (var instruction in method.ControlFlowGraph.Instructions)
×
298
        {
299
            if (!instruction.IsCall || instruction.Operands[0] is not Immediate callTarget)
×
300
                continue;
301

302
            if (!method.AppContext.MethodsByAddress.TryGetValue(callTarget.UnsignedValue, out var candidates))
×
303
                continue;
304

305
            if (GetReceiver(instruction) is not { } receiver || AllocatedType(receiver, definitions) is not { } allocatedType)
×
306
                continue;
307

308
            var constructor = candidates.FirstOrDefault(c => !c.IsStatic && c.Name == ".ctor" && ReferenceEquals(c.DeclaringType, allocatedType));
×
309
            if (constructor == null)
×
310
                continue;
311

312
            instruction.SetOperand(0, constructor);
×
313
            changed = true;
×
314
        }
315

316
        return changed;
×
317
    }
318

319
    // Follow SSA copies from a local back to the Newobj that produced the value
320
    private static TypeAnalysisContext? AllocatedType(LocalVariable local, Dictionary<LocalVariable, Instruction> definitions)
321
    {
322
        var visited = new HashSet<LocalVariable>();
×
323

324
        while (visited.Add(local) && definitions.TryGetValue(local, out var definition))
×
325
        {
326
            switch (definition.OpCode)
×
327
            {
328
                case OpCode.Newobj:
329
                    return (definition.Operands[0] as LocalVariable)?.Type;
×
330
                case OpCode.Move when definition.Operands[1] is LocalVariable source:
×
331
                    local = source;
×
332
                    continue;
×
333
            }
334

335
            break;
336
        }
337

338
        return null;
×
339
    }
340

341
    /// <summary>
342
    /// Resolves calls whose address maps to more than one method by reading the runtime
343
    /// <c>MethodInfo*</c> the caller passes in, if there is one.
344
    /// </summary>
345
    public static bool ResolveCallsViaMethodInfo(MethodAnalysisContext method)
346
    {
347
        var changed = false;
×
348

349
        foreach (var instruction in method.ControlFlowGraph!.Instructions)
×
350
        {
351
            if (!instruction.IsCall)
×
352
                continue;
353

354
            if (instruction.Operands[0] is not Immediate target)
×
355
                //Already resolved
356
                continue;
357

358
            if (GetMethodInfoArgument(instruction) is not { RepresentedMethod: { } representedMethod })
×
359
                //No MethodInfo to work with
360
                continue;
361

362
            if (!method.AppContext.MethodsByAddress.TryGetValue(target.UnsignedValue, out var candidates))
×
363
            {
364
                // Some shared generic bodies aren't in the address map at all (todo investigate?).
365
                // Il2cpp still passes the concrete MethodInfo as the hidden final parameter, so we can use a methodof there if we have one.
366
                var firstArg = instruction.OpCode == OpCode.CallVoid ? 1 : 2;
×
367
                var hiddenParamIndex = firstArg + (representedMethod.IsStatic ? 0 : 1) + representedMethod.Parameters.Count;
×
368

369
                if (hiddenParamIndex >= instruction.Operands.Count
×
370
                    || AsMethodInfo(instruction.Operands[hiddenParamIndex]) == null)
×
371
                    continue;
372

373
                instruction.SetOperand(0, representedMethod);
×
374
                changed = true;
×
375
                continue;
×
376
            }
377

378
            if (candidates.Count < 2)
×
379
                continue;
380

381
            //Try to actually match on the method name so we don't just replace a call with something else.
382
            var representedBase = BaseMethodOf(representedMethod);
×
383
            if (!candidates.Any(candidate => ReferenceEquals(BaseMethodOf(candidate), representedBase)))
×
384
                continue;
385

386
            instruction.SetOperand(0, representedMethod);
×
387
            changed = true;
×
388
        }
389

390
        return changed;
×
391
    }
392

393
    // Offset of Il2CppClass::vtable, VirtualInvokeData entries of {methodPtr, MethodInfo*}.
394
    // TODO this is almost certainly not correct on every version
395
    private const long VTableOffset64 = 0x138;
396
    private const long VTableOffset32 = 0xC0;
397
    
398
    // Resolves virtual dispatch through <c>[klass + vtableOffset + slot * sizeof(VirtualInvokeData)]</c>
399
    // as long as the klass local's represented type is known.
400
    public static bool ResolveVirtualCalls(MethodAnalysisContext method)
401
    {
NEW
402
        var pointerSize = method.AppContext.Binary.PointerSizeBytes;
×
NEW
403
        var vtableOffset = pointerSize == 8 ? VTableOffset64 : VTableOffset32;
×
NEW
404
        var invokeDataSize = 2L * pointerSize;
×
NEW
405
        var changed = false;
×
406

NEW
407
        var loads = new Dictionary<LocalVariable, MemoryOperand>();
×
NEW
408
        foreach (var instruction in method.ControlFlowGraph!.Instructions)
×
409
        {
NEW
410
            if (instruction.OpCode == OpCode.Move
×
NEW
411
                && instruction.Operands[0] is LocalVariable destination
×
NEW
412
                && instruction.Operands[1] is MemoryOperand { Index: null, Scale: 0 } load)
×
NEW
413
                loads[destination] = load;
×
414
        }
415

NEW
416
        foreach (var instruction in method.ControlFlowGraph.Instructions)
×
417
        {
NEW
418
            if (instruction.OpCode != OpCode.IndirectCall)
×
419
                continue;
420

NEW
421
            if (SlotLoad(instruction.Operands[0]) is not { } target
×
NEW
422
                || target.Base is not LocalVariable { Type: RuntimeClassTypeAnalysisContext { RepresentedType: { } receiverType } } klassLocal)
×
423
                continue;
424

NEW
425
            var offset = target.Addend - vtableOffset;
×
NEW
426
            if (offset < 0 || offset % invokeDataSize != 0)
×
427
                continue;
428

NEW
429
            var slot = (int)(offset / invokeDataSize);
×
NEW
430
            if (ResolveVTableSlot(method.AppContext, receiverType, slot) is not { } resolved)
×
431
                continue;
432

NEW
433
            var assembly = resolved.DeclaringType?.DeclaringAssembly ?? method.DeclaringType?.DeclaringAssembly;
×
434

435
            // the MethodInfo field is also the same method, name it, for cleanliness and so it can
436
            // serve as a hidden final parameter if needed
NEW
437
            for (var i = 1; i < instruction.Operands.Count && assembly != null; i++)
×
438
            {
NEW
439
                if (SlotLoad(instruction.Operands[i]) is { } methodInfoLoad
×
NEW
440
                    && ReferenceEquals(methodInfoLoad.Base, klassLocal)
×
NEW
441
                    && methodInfoLoad.Addend == target.Addend + pointerSize)
×
NEW
442
                    instruction.SetOperand(i, new RuntimeMethodInfoAnalysisContext(resolved, assembly));
×
443
            }
444

NEW
445
            instruction.OpCode = OpCode.Call; // same operand layout as IndirectCall, and we've resolved it now
×
NEW
446
            instruction.SetOperand(0, resolved);
×
NEW
447
            changed = true;
×
448
        }
449

NEW
450
        return changed;
×
451

NEW
452
        MemoryOperand? SlotLoad(IOperand operand) => operand switch
×
NEW
453
        {
×
NEW
454
            MemoryOperand { Index: null, Scale: 0 } inlined => inlined,
×
NEW
455
            LocalVariable local when loads.TryGetValue(local, out var load) => load,
×
NEW
456
            _ => null
×
NEW
457
        };
×
458
    }
459

460
    private static MethodAnalysisContext? ResolveVTableSlot(ApplicationAnalysisContext appContext, TypeAnalysisContext type, int slot)
461
    {
NEW
462
        var definition = (type as GenericInstanceTypeAnalysisContext)?.GenericType.Definition ?? type.Definition;
×
463

NEW
464
        if (definition == null || slot >= definition.VtableCount)
×
NEW
465
            return null;
×
466

NEW
467
        if (appContext.ResolveContextForMethod(definition.VTable[slot]) is { } implementation)
×
NEW
468
            return implementation;
×
469

470
        // an abstract method has no implementation, try to resolve it
NEW
471
        for (var declarer = type; declarer != null; declarer = declarer.BaseType)
×
472
        {
NEW
473
            if (declarer.Methods.FirstOrDefault(m => m.Definition?.slot == slot) is { } declaration)
×
NEW
474
                return declaration;
×
475
        }
476

NEW
477
        return null;
×
478
    }
479

480
    private static MethodAnalysisContext BaseMethodOf(MethodAnalysisContext method) =>
481
        method is ConcreteGenericMethodAnalysisContext { BaseMethodContext: { } baseMethod } ? baseMethod : method;
×
482

483
    private static RuntimeMethodInfoAnalysisContext? GetMethodInfoArgument(Instruction call)
484
    {
485
        var firstArg = call.OpCode == OpCode.CallVoid ? 1 : 2;
×
486

487
        for (var i = call.Operands.Count - 1; i >= firstArg; i--)
×
488
        {
489
            if (AsMethodInfo(call.Operands[i]) is { } methodInfo)
×
490
                return methodInfo;
×
491
        }
492

493
        return null;
×
494
    }
495

496
    private static RuntimeMethodInfoAnalysisContext? AsMethodInfo(IOperand operand) =>
497
        operand switch
×
498
        {
×
499
            RuntimeMethodInfoAnalysisContext methodInfo => methodInfo,
×
500
            LocalVariable { Type: RuntimeMethodInfoAnalysisContext methodInfoLocal } => methodInfoLocal,
×
501
            _ => null
×
502
        };
×
503

504
    private static void HandleKeyFunction(ApplicationAnalysisContext appContext, Instruction instruction, ulong target, BaseKeyFunctionAddresses kFA)
505
    {
506
        var method = "";
×
507
        if (target == kFA.il2cpp_codegen_initialize_method || target == kFA.il2cpp_codegen_initialize_runtime_metadata)
×
508
        {
509
            if (appContext.MetadataVersion < 27)
×
510
            {
511
                method = nameof(kFA.il2cpp_codegen_initialize_method);
×
512
            }
513
            else
514
            {
515
                method = nameof(kFA.il2cpp_codegen_initialize_runtime_metadata);
×
516
            }
517
        }
518
        else
519
        {
520
            var pairs = kFA.Pairs.ToList();
×
521
            var key = pairs.FirstOrDefault(pair => pair.Value == target).Key;
×
522
            if (key == null)
×
523
                return;
×
524
            method = key;
×
525
        }
526

527
        if (method != "")
×
528
        {
529
            instruction.SetOperand(0, new StringLiteral(method));
×
530
        }
531
    }
×
532

533
    // Because of il2cpp fields (like cctor_finished_or_no_cctor) [local @ reg+offset] sometimes can't be resolved, but this works for now
534
    private static void ResolveGetter(MethodAnalysisContext method)
535
    {
536
        if (!method.Name.StartsWith("get_"))
×
537
            return;
×
538

539
        // Default get: Return [this @ reg+offset]
540
        var instructions = method.ControlFlowGraph!.Instructions;
×
541
        if (instructions.Count == 1)
×
542
        {
543
            var instr = instructions[0];
×
544

545
            if (instr.OpCode != OpCode.Return
×
546
                || instr.Operands.Count < 1
×
547
                || instr.Operands[0] is not MemoryOperand memory
×
548
                || memory.Index != null || memory.Scale != 0
×
549
                || memory.Base is not LocalVariable local)
×
550
                return;
×
551

552
            var fieldName = $"<{method.Name[4..]}>k__BackingField";
×
553

554
            var field = method.DeclaringType!.Fields.Find(f => f.Name == fieldName);
×
555
            if (field == null)
×
556
                return;
×
557

558
            instr.SetOperand(0, new FieldReference(field, local, (int)memory.Addend));
×
559
        }
560
    }
×
561
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc