You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1131 lines
38 KiB

  1. // Copyright (c) 2020 Daniel Grunwald
  2. //
  3. // Permission is hereby granted, free of charge, to any person obtaining a copy of this
  4. // software and associated documentation files (the "Software"), to deal in the Software
  5. // without restriction, including without limitation the rights to use, copy, modify, merge,
  6. // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
  7. // to whom the Software is furnished to do so, subject to the following conditions:
  8. //
  9. // The above copyright notice and this permission notice shall be included in all copies or
  10. // substantial portions of the Software.
  11. //
  12. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
  13. // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
  14. // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
  15. // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
  16. // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  17. // DEALINGS IN THE SOFTWARE.
  18. using System;
  19. using System.Collections.Generic;
  20. using System.Diagnostics;
  21. using System.Linq;
  22. using System.Reflection.Metadata;
  23. using System.Threading;
  24. using ICSharpCode.Decompiler.IL;
  25. using ICSharpCode.Decompiler.IL.Transforms;
  26. using ICSharpCode.Decompiler.TypeSystem;
  27. namespace ICSharpCode.Decompiler.CSharp
  28. {
  29. class RecordDecompiler
  30. {
  31. readonly IDecompilerTypeSystem typeSystem;
  32. readonly ITypeDefinition recordTypeDef;
  33. readonly DecompilerSettings settings;
  34. readonly CancellationToken cancellationToken;
  35. readonly List<IMember> orderedMembers;
  36. readonly bool isInheritedRecord;
  37. readonly bool isStruct;
  38. readonly bool isSealed;
  39. readonly IMethod primaryCtor;
  40. readonly IType baseClass;
  41. readonly Dictionary<IField, IProperty> backingFieldToAutoProperty = new Dictionary<IField, IProperty>();
  42. readonly Dictionary<IProperty, IField> autoPropertyToBackingField = new Dictionary<IProperty, IField>();
  43. readonly Dictionary<IParameter, IProperty> primaryCtorParameterToAutoProperty = new Dictionary<IParameter, IProperty>();
  44. readonly Dictionary<IProperty, IParameter> autoPropertyToPrimaryCtorParameter = new Dictionary<IProperty, IParameter>();
  45. public RecordDecompiler(IDecompilerTypeSystem dts, ITypeDefinition recordTypeDef, DecompilerSettings settings, CancellationToken cancellationToken)
  46. {
  47. this.typeSystem = dts;
  48. this.recordTypeDef = recordTypeDef;
  49. this.settings = settings;
  50. this.cancellationToken = cancellationToken;
  51. this.baseClass = recordTypeDef.DirectBaseTypes.FirstOrDefault(b => b.Kind == TypeKind.Class);
  52. this.isStruct = baseClass.IsKnownType(KnownTypeCode.ValueType);
  53. this.isInheritedRecord = !isStruct && !baseClass.IsKnownType(KnownTypeCode.Object);
  54. this.isSealed = recordTypeDef.IsSealed;
  55. DetectAutomaticProperties();
  56. this.orderedMembers = DetectMemberOrder(recordTypeDef, backingFieldToAutoProperty);
  57. this.primaryCtor = DetectPrimaryConstructor();
  58. }
  59. void DetectAutomaticProperties()
  60. {
  61. var subst = recordTypeDef.AsParameterizedType().GetSubstitution();
  62. foreach (var property in recordTypeDef.Properties)
  63. {
  64. cancellationToken.ThrowIfCancellationRequested();
  65. var p = (IProperty)property.Specialize(subst);
  66. if (IsAutoProperty(p, out var field))
  67. {
  68. backingFieldToAutoProperty.Add(field, p);
  69. autoPropertyToBackingField.Add(p, field);
  70. }
  71. }
  72. bool IsAutoProperty(IProperty p, out IField field)
  73. {
  74. field = null;
  75. if (p.Parameters.Count != 0)
  76. return false;
  77. if (p.Getter != null)
  78. {
  79. if (!IsAutoGetter(p.Getter, out field))
  80. return false;
  81. }
  82. if (p.Setter != null)
  83. {
  84. if (!IsAutoSetter(p.Setter, out var field2))
  85. return false;
  86. if (field != null)
  87. {
  88. if (!field.Equals(field2))
  89. return false;
  90. }
  91. else
  92. {
  93. field = field2;
  94. }
  95. }
  96. if (field == null)
  97. return false;
  98. if (!IsRecordType(field.DeclaringType))
  99. return false;
  100. return field.Name == $"<{p.Name}>k__BackingField";
  101. }
  102. bool IsAutoGetter(IMethod method, out IField field)
  103. {
  104. field = null;
  105. var body = DecompileBody(method);
  106. if (body == null)
  107. return false;
  108. // return this.field;
  109. if (!body.Instructions[0].MatchReturn(out var retVal))
  110. return false;
  111. if (method.IsStatic)
  112. {
  113. return retVal.MatchLdsFld(out field);
  114. }
  115. else
  116. {
  117. if (!retVal.MatchLdFld(out var target, out field))
  118. return false;
  119. return target.MatchLdThis();
  120. }
  121. }
  122. bool IsAutoSetter(IMethod method, out IField field)
  123. {
  124. field = null;
  125. Debug.Assert(!method.IsStatic);
  126. var body = DecompileBody(method);
  127. if (body == null)
  128. return false;
  129. // this.field = value;
  130. ILInstruction valueInst;
  131. if (method.IsStatic)
  132. {
  133. if (!body.Instructions[0].MatchStsFld(out field, out valueInst))
  134. return false;
  135. }
  136. else
  137. {
  138. if (!body.Instructions[0].MatchStFld(out var target, out field, out valueInst))
  139. return false;
  140. if (!target.MatchLdThis())
  141. return false;
  142. }
  143. if (!valueInst.MatchLdLoc(out var value))
  144. return false;
  145. if (!(value.Kind == VariableKind.Parameter && value.Index == 0))
  146. return false;
  147. return body.Instructions[1].MatchReturn(out var retVal) && retVal.MatchNop();
  148. }
  149. }
  150. IMethod DetectPrimaryConstructor()
  151. {
  152. if (!settings.UsePrimaryConstructorSyntax)
  153. return null;
  154. var subst = recordTypeDef.AsParameterizedType().GetSubstitution();
  155. foreach (var method in recordTypeDef.Methods)
  156. {
  157. cancellationToken.ThrowIfCancellationRequested();
  158. if (method.IsStatic || !method.IsConstructor)
  159. continue;
  160. var m = method.Specialize(subst);
  161. if (IsPrimaryConstructor(m, method))
  162. return method;
  163. primaryCtorParameterToAutoProperty.Clear();
  164. autoPropertyToPrimaryCtorParameter.Clear();
  165. }
  166. return null;
  167. bool IsPrimaryConstructor(IMethod method, IMethod unspecializedMethod)
  168. {
  169. Debug.Assert(method.IsConstructor);
  170. var body = DecompileBody(method);
  171. if (body == null)
  172. return false;
  173. if (method.Parameters.Count == 0)
  174. return false;
  175. var addonInst = isStruct ? 1 : 2;
  176. if (body.Instructions.Count < method.Parameters.Count + addonInst)
  177. return false;
  178. for (int i = 0; i < method.Parameters.Count; i++)
  179. {
  180. if (!body.Instructions[i].MatchStFld(out var target, out var field, out var valueInst))
  181. return false;
  182. if (!target.MatchLdThis())
  183. return false;
  184. if (method.Parameters[i].IsIn)
  185. {
  186. if (!valueInst.MatchLdObj(out valueInst, out _))
  187. return false;
  188. }
  189. if (!valueInst.MatchLdLoc(out var value))
  190. return false;
  191. if (!(value.Kind == VariableKind.Parameter && value.Index == i))
  192. return false;
  193. if (!backingFieldToAutoProperty.TryGetValue(field, out var property))
  194. return false;
  195. primaryCtorParameterToAutoProperty.Add(unspecializedMethod.Parameters[i], property);
  196. autoPropertyToPrimaryCtorParameter.Add(property, unspecializedMethod.Parameters[i]);
  197. }
  198. if (!isStruct)
  199. {
  200. var baseCtorCall = body.Instructions.SecondToLastOrDefault() as CallInstruction;
  201. if (baseCtorCall == null)
  202. return false;
  203. }
  204. var returnInst = body.Instructions.LastOrDefault();
  205. return returnInst != null && returnInst.MatchReturn(out var retVal) && retVal.MatchNop();
  206. }
  207. }
  208. static List<IMember> DetectMemberOrder(ITypeDefinition recordTypeDef, Dictionary<IField, IProperty> backingFieldToAutoProperty)
  209. {
  210. // For records, the order of members is important:
  211. // Equals/GetHashCode/PrintMembers must agree on an order of fields+properties.
  212. // The IL metadata has the order of fields and the order of properties, but we
  213. // need to detect the correct interleaving.
  214. // We could try to detect this from the PrintMembers body, but let's initially
  215. // restrict ourselves to the common case where the record only uses properties.
  216. var subst = recordTypeDef.AsParameterizedType().GetSubstitution();
  217. return recordTypeDef.Properties.Select(p => p.Specialize(subst)).Concat(
  218. recordTypeDef.Fields.Select(f => (IField)f.Specialize(subst)).Where(f => !backingFieldToAutoProperty.ContainsKey(f))
  219. ).ToList();
  220. }
  221. /// <summary>
  222. /// Gets the fields and properties of the record type, interleaved as necessary to
  223. /// maintain Equals/ToString/etc. semantics.
  224. /// </summary>
  225. public IEnumerable<IMember> FieldsAndProperties => orderedMembers;
  226. /// <summary>
  227. /// Gets the detected primary constructor. Returns null, if there was no primary constructor detected.
  228. /// </summary>
  229. public IMethod PrimaryConstructor => primaryCtor;
  230. public bool IsInheritedRecord => isInheritedRecord;
  231. bool IsRecordType(IType type)
  232. {
  233. return type.GetDefinition() == recordTypeDef
  234. && type.TypeArguments.SequenceEqual(recordTypeDef.TypeParameters);
  235. }
  236. /// <summary>
  237. /// Gets whether the member of the record type will be automatically generated by the compiler.
  238. /// </summary>
  239. public bool MethodIsGenerated(IMethod method)
  240. {
  241. if (IsCopyConstructor(method))
  242. {
  243. return IsGeneratedCopyConstructor(method);
  244. }
  245. switch (method.Name)
  246. {
  247. // Some members in records are always compiler-generated and lead to a
  248. // "duplicate definition" error if we emit the generated code.
  249. case "op_Equality":
  250. case "op_Inequality":
  251. {
  252. // Don't emit comparison operators into C# record definition
  253. // Note: user can declare additional operator== as long as they have
  254. // different parameter types.
  255. return method.Parameters.Count == 2
  256. && method.Parameters.All(p => IsRecordType(p.Type));
  257. }
  258. case "Equals" when method.Parameters.Count == 1:
  259. {
  260. IType paramType = method.Parameters[0].Type;
  261. if (paramType.IsKnownType(KnownTypeCode.Object) && method.IsOverride)
  262. {
  263. // override bool Equals(object? obj): always generated
  264. return true;
  265. }
  266. else if (IsRecordType(paramType))
  267. {
  268. // virtual bool Equals(R? other): generated unless user-declared
  269. return IsGeneratedEquals(method);
  270. }
  271. else if (isInheritedRecord && NormalizeTypeVisitor.TypeErasure.EquivalentTypes(paramType, baseClass) && method.IsOverride)
  272. {
  273. // override bool Equals(BaseClass? obj): always generated
  274. return true;
  275. }
  276. else
  277. {
  278. return false;
  279. }
  280. }
  281. case "GetHashCode":
  282. return IsGeneratedGetHashCode(method);
  283. case "<Clone>$" when method.Parameters.Count == 0:
  284. // Always generated; Method name cannot be expressed in C#
  285. return true;
  286. case "PrintMembers":
  287. return IsGeneratedPrintMembers(method);
  288. case "ToString" when method.Parameters.Count == 0:
  289. return IsGeneratedToString(method);
  290. case "Deconstruct" when primaryCtor != null && method.Parameters.Count == primaryCtor.Parameters.Count:
  291. return IsGeneratedDeconstruct(method);
  292. default:
  293. return false;
  294. }
  295. }
  296. internal bool PropertyIsGenerated(IProperty property)
  297. {
  298. switch (property.Name)
  299. {
  300. case "EqualityContract" when !isStruct:
  301. return IsGeneratedEqualityContract(property);
  302. default:
  303. return IsPropertyDeclaredByPrimaryConstructor(property);
  304. }
  305. }
  306. public bool IsPropertyDeclaredByPrimaryConstructor(IProperty property)
  307. {
  308. var subst = recordTypeDef.AsParameterizedType().GetSubstitution();
  309. return primaryCtor != null
  310. && autoPropertyToPrimaryCtorParameter.ContainsKey((IProperty)property.Specialize(subst));
  311. }
  312. internal (IProperty prop, IField field) GetPropertyInfoByPrimaryConstructorParameter(IParameter parameter)
  313. {
  314. var prop = primaryCtorParameterToAutoProperty[parameter];
  315. return (prop, autoPropertyToBackingField[prop]);
  316. }
  317. public bool IsCopyConstructor(IMethod method)
  318. {
  319. if (method == null)
  320. return false;
  321. Debug.Assert(method.DeclaringTypeDefinition == recordTypeDef);
  322. return method.IsConstructor
  323. && method.Parameters.Count == 1
  324. && IsRecordType(method.Parameters[0].Type);
  325. }
  326. private bool IsAllowedAttribute(IAttribute attribute)
  327. {
  328. switch (attribute.AttributeType.ReflectionName)
  329. {
  330. case "System.Runtime.CompilerServices.CompilerGeneratedAttribute":
  331. return true;
  332. default:
  333. return false;
  334. }
  335. }
  336. private bool IsGeneratedCopyConstructor(IMethod method)
  337. {
  338. /*
  339. call BaseClass..ctor(ldloc this, ldloc original)
  340. stfld <X>k__BackingField(ldloc this, ldfld <X>k__BackingField(ldloc original))
  341. leave IL_0000 (nop)
  342. */
  343. Debug.Assert(method.IsConstructor && method.Parameters.Count == 1);
  344. if (method.GetAttributes().Any(attr => !IsAllowedAttribute(attr)) || method.GetReturnTypeAttributes().Any())
  345. return false;
  346. if (method.Accessibility != Accessibility.Protected && (!isSealed || method.Accessibility != Accessibility.Private))
  347. return false;
  348. if (orderedMembers == null)
  349. return false;
  350. var body = DecompileBody(method);
  351. if (body == null)
  352. return false;
  353. var variables = body.Ancestors.OfType<ILFunction>().Single().Variables;
  354. var other = variables.Single(v => v.Kind == VariableKind.Parameter && v.Index == 0);
  355. Debug.Assert(IsRecordType(other.Type));
  356. int pos = 0;
  357. // First instruction is the base constructor call
  358. if (!(body.Instructions[pos] is Call { Method: { IsConstructor: true } } baseCtorCall))
  359. return false;
  360. if (!object.Equals(baseCtorCall.Method.DeclaringType, baseClass))
  361. return false;
  362. if (baseCtorCall.Arguments.Count != (isInheritedRecord ? 2 : 1))
  363. return false;
  364. if (!baseCtorCall.Arguments[0].MatchLdThis())
  365. return false;
  366. if (isInheritedRecord)
  367. {
  368. if (!baseCtorCall.Arguments[1].MatchLdLoc(other))
  369. return false;
  370. }
  371. pos++;
  372. // Then all the fields are copied over
  373. foreach (var member in orderedMembers)
  374. {
  375. if (!(member is IField field))
  376. {
  377. if (!autoPropertyToBackingField.TryGetValue((IProperty)member, out field))
  378. continue;
  379. }
  380. if (pos >= body.Instructions.Count)
  381. return false;
  382. if (!body.Instructions[pos].MatchStFld(out var lhsTarget, out var lhsField, out var valueInst))
  383. return false;
  384. if (!lhsTarget.MatchLdThis())
  385. return false;
  386. if (!lhsField.Equals(field))
  387. return false;
  388. if (!valueInst.MatchLdFld(out var rhsTarget, out var rhsField))
  389. return false;
  390. if (!rhsTarget.MatchLdLoc(other))
  391. return false;
  392. if (!rhsField.Equals(field))
  393. return false;
  394. pos++;
  395. }
  396. return body.Instructions[pos] is Leave;
  397. }
  398. private bool IsGeneratedEqualityContract(IProperty property)
  399. {
  400. // Generated member:
  401. // protected virtual Type EqualityContract {
  402. // [CompilerGenerated] get => typeof(R);
  403. // }
  404. Debug.Assert(!isStruct && property.Name == "EqualityContract");
  405. if (property.Accessibility != Accessibility.Protected && (!isSealed || property.Accessibility != Accessibility.Private))
  406. return false;
  407. if (!(isSealed || property.IsVirtual || property.IsOverride))
  408. return false;
  409. if (property.IsSealed)
  410. return false;
  411. var getter = property.Getter;
  412. if (!(getter != null && !property.CanSet))
  413. return false;
  414. var attrs = property.GetAttributes().ToList();
  415. switch (attrs.Count)
  416. {
  417. case 0:
  418. // Roslyn 3.x does not emit a CompilerGeneratedAttribute on the property itself.
  419. break;
  420. case 1:
  421. // Roslyn 4.4 started doing so.
  422. if (!attrs[0].AttributeType.IsKnownType(KnownAttribute.CompilerGenerated))
  423. return false;
  424. break;
  425. default:
  426. return false;
  427. }
  428. if (getter.GetReturnTypeAttributes().Any())
  429. return false;
  430. attrs = getter.GetAttributes().ToList();
  431. if (attrs.Count != 1)
  432. return false;
  433. if (!attrs[0].AttributeType.IsKnownType(KnownAttribute.CompilerGenerated))
  434. return false;
  435. var body = DecompileBody(getter);
  436. if (body == null || body.Instructions.Count != 1)
  437. return false;
  438. if (!(body.Instructions.Single() is Leave leave))
  439. return false;
  440. // leave IL_0000 (call GetTypeFromHandle(ldtypetoken R))
  441. if (!TransformExpressionTrees.MatchGetTypeFromHandle(leave.Value, out IType ty))
  442. return false;
  443. return IsRecordType(ty);
  444. }
  445. private bool IsGeneratedPrintMembers(IMethod method)
  446. {
  447. Debug.Assert(method.Name == "PrintMembers");
  448. if (method.Parameters.Count != 1)
  449. return false;
  450. if (!isSealed && !method.IsOverridable)
  451. return false;
  452. if (method.GetAttributes().Any(attr => !IsAllowedAttribute(attr)) || method.GetReturnTypeAttributes().Any())
  453. return false;
  454. if (method.Accessibility != Accessibility.Protected && (!isSealed || method.Accessibility != Accessibility.Private))
  455. return false;
  456. if (orderedMembers == null)
  457. return false;
  458. var body = DecompileBody(method);
  459. if (body == null)
  460. return false;
  461. var variables = body.Ancestors.OfType<ILFunction>().Single().Variables;
  462. var builder = variables.Single(v => v.Kind == VariableKind.Parameter && v.Index == 0);
  463. if (builder.Type.ReflectionName != "System.Text.StringBuilder")
  464. return false;
  465. int pos = 0;
  466. //Roslyn 4.0.0-3.final start to insert an call to RuntimeHelpers.EnsureSufficientExecutionStack()
  467. if (!isStruct && !isInheritedRecord && body.Instructions[pos] is Call {
  468. Arguments: { Count: 0 },
  469. Method: { Name: "EnsureSufficientExecutionStack", DeclaringType: { Namespace: "System.Runtime.CompilerServices", Name: "RuntimeHelpers" } }
  470. })
  471. {
  472. pos++;
  473. }
  474. if (isInheritedRecord)
  475. {
  476. // Special case: inherited record adding no new members
  477. if (body.Instructions[pos].MatchReturn(out var returnValue)
  478. && IsBaseCall(returnValue) && !orderedMembers.Any(IsPrintedMember))
  479. {
  480. return true;
  481. }
  482. // if (call PrintMembers(ldloc this, ldloc builder)) Block IL_000f {
  483. // callvirt Append(ldloc builder, ldstr ", ")
  484. // }
  485. if (!body.Instructions[pos].MatchIfInstruction(out var condition, out var trueInst))
  486. return false;
  487. if (!IsBaseCall(condition))
  488. return false;
  489. // trueInst = callvirt Append(ldloc builder, ldstr ", ")
  490. trueInst = Block.Unwrap(trueInst);
  491. if (!MatchStringBuilderAppend(trueInst, builder, out var val))
  492. return false;
  493. if (!(val.MatchLdStr(out string text) && text == ", "))
  494. return false;
  495. pos++;
  496. bool IsBaseCall(ILInstruction inst)
  497. {
  498. if (!(inst is CallInstruction { Method: { Name: "PrintMembers" } } call))
  499. return false;
  500. if (call.Arguments.Count != 2)
  501. return false;
  502. if (!call.Arguments[0].MatchLdThis())
  503. return false;
  504. if (!call.Arguments[1].MatchLdLoc(builder))
  505. return false;
  506. return true;
  507. }
  508. }
  509. bool needsComma = false;
  510. foreach (var member in orderedMembers)
  511. {
  512. if (!IsPrintedMember(member))
  513. continue;
  514. cancellationToken.ThrowIfCancellationRequested();
  515. /*
  516. callvirt Append(ldloc builder, ldstr "A")
  517. callvirt Append(ldloc builder, ldstr " = ")
  518. callvirt Append(ldloc builder, constrained[System.Int32].callvirt ToString(addressof System.Int32(call get_A(ldloc this))))
  519. callvirt Append(ldloc builder, ldstr ", ")
  520. callvirt Append(ldloc builder, ldstr "B")
  521. callvirt Append(ldloc builder, ldstr " = ")
  522. callvirt Append(ldloc builder, constrained[System.Int32].callvirt ToString(ldflda B(ldloc this)))
  523. leave IL_0000 (ldc.i4 1) */
  524. if (!MatchStringBuilderAppendConstant(out string text))
  525. return false;
  526. string expectedText = (needsComma ? ", " : "") + member.Name + " = ";
  527. if (text != expectedText)
  528. return false;
  529. if (!MatchStringBuilderAppend(body.Instructions[pos], builder, out var val))
  530. return false;
  531. if (val is CallInstruction { Method: { Name: "ToString", IsStatic: false } } toStringCall)
  532. {
  533. if (toStringCall.Arguments.Count != 1)
  534. return false;
  535. val = toStringCall.Arguments[0];
  536. if (val is AddressOf addressOf)
  537. {
  538. val = addressOf.Value;
  539. }
  540. }
  541. else if (val is Box box)
  542. {
  543. if (!NormalizeTypeVisitor.TypeErasure.EquivalentTypes(box.Type, member.ReturnType))
  544. return false;
  545. val = box.Argument;
  546. }
  547. if (val is CallInstruction getterCall && member is IProperty property)
  548. {
  549. if (!getterCall.Method.Equals(property.Getter))
  550. return false;
  551. if (getterCall.Arguments.Count != 1)
  552. return false;
  553. if (!getterCall.Arguments[0].MatchLdThis())
  554. return false;
  555. }
  556. else if (val.MatchLdFld(out var target, out var field) || val.MatchLdFlda(out target, out field))
  557. {
  558. if (!target.MatchLdThis())
  559. return false;
  560. if (!field.Equals(member))
  561. return false;
  562. }
  563. else
  564. {
  565. return false;
  566. }
  567. pos++;
  568. needsComma = true;
  569. }
  570. // leave IL_0000 (ldc.i4 1)
  571. return body.Instructions[pos].MatchReturn(out var retVal)
  572. && retVal.MatchLdcI4(needsComma ? 1 : 0);
  573. bool IsPrintedMember(IMember member)
  574. {
  575. if (member.IsStatic)
  576. {
  577. return false; // static fields/properties are not printed
  578. }
  579. if (!isStruct && member.Name == "EqualityContract")
  580. {
  581. return false; // EqualityContract is never printed
  582. }
  583. if (member.IsExplicitInterfaceImplementation)
  584. {
  585. return false; // explicit interface impls are not printed
  586. }
  587. if (member.IsOverride)
  588. {
  589. return false; // override is not printed (again), the virtual base property was already printed
  590. }
  591. return true;
  592. }
  593. bool MatchStringBuilderAppendConstant(out string text)
  594. {
  595. text = null;
  596. while (MatchStringBuilderAppend(body.Instructions[pos], builder, out var val) && val.MatchLdStr(out string valText))
  597. {
  598. text += valText;
  599. pos++;
  600. }
  601. return text != null;
  602. }
  603. }
  604. private bool MatchStringBuilderAppend(ILInstruction inst, ILVariable sb, out ILInstruction val)
  605. {
  606. val = null;
  607. if (!(inst is CallVirt { Method: { Name: "Append", DeclaringType: { Namespace: "System.Text", Name: "StringBuilder" } } } call))
  608. return false;
  609. if (call.Arguments.Count != 2)
  610. return false;
  611. if (!call.Arguments[0].MatchLdLoc(sb))
  612. return false;
  613. val = call.Arguments[1];
  614. return true;
  615. }
  616. private bool IsGeneratedToString(IMethod method)
  617. {
  618. Debug.Assert(method.Name == "ToString" && method.Parameters.Count == 0);
  619. if (!method.IsOverride)
  620. return false;
  621. if (method.IsSealed)
  622. return false;
  623. if (method.GetAttributes().Any(attr => !IsAllowedAttribute(attr)) || method.GetReturnTypeAttributes().Any())
  624. return false;
  625. var body = DecompileBody(method);
  626. if (body == null)
  627. return false;
  628. // stloc stringBuilder(newobj StringBuilder..ctor())
  629. if (!body.Instructions[0].MatchStLoc(out var stringBuilder, out var stringBuilderInit))
  630. return false;
  631. if (!(stringBuilderInit is NewObj { Arguments: { Count: 0 }, Method: { DeclaringTypeDefinition: { Name: "StringBuilder", Namespace: "System.Text" } } }))
  632. return false;
  633. // callvirt Append(ldloc stringBuilder, ldstr "R")
  634. if (!MatchAppendCallWithValue(body.Instructions[1], recordTypeDef.Name))
  635. return false;
  636. // callvirt Append(ldloc stringBuilder, ldstr " { ")
  637. if (!MatchAppendCallWithValue(body.Instructions[2], " { "))
  638. return false;
  639. // if (callvirt PrintMembers(ldloc this, ldloc stringBuilder)) { trueInst }
  640. if (!body.Instructions[3].MatchIfInstruction(out var condition, out var trueInst))
  641. return true;
  642. if (!((condition is CallInstruction { Method: { Name: "PrintMembers" } } printMembersCall) &&
  643. (condition is CallVirt || (isSealed && condition is Call))))
  644. return false;
  645. if (printMembersCall.Arguments.Count != 2)
  646. return false;
  647. if (!printMembersCall.Arguments[0].MatchLdThis())
  648. return false;
  649. if (!printMembersCall.Arguments[1].MatchLdLoc(stringBuilder))
  650. return false;
  651. // trueInst: callvirt Append(ldloc stringBuilder, ldstr " ")
  652. if (!MatchAppendCallWithValue(Block.Unwrap(trueInst), " "))
  653. return false;
  654. // callvirt Append(ldloc stringBuilder, ldstr "}")
  655. if (!MatchAppendCallWithValue(body.Instructions[4], "}"))
  656. return false;
  657. // leave IL_0000 (callvirt ToString(ldloc stringBuilder))
  658. if (!(body.Instructions[5] is Leave leave))
  659. return false;
  660. if (!(leave.Value is CallVirt { Method: { Name: "ToString" } } toStringCall))
  661. return false;
  662. if (toStringCall.Arguments.Count != 1)
  663. return false;
  664. return toStringCall.Arguments[0].MatchLdLoc(stringBuilder);
  665. bool MatchAppendCallWithValue(ILInstruction inst, string val)
  666. {
  667. if (!(inst is CallVirt { Method: { Name: "Append" } } call))
  668. return false;
  669. if (call.Arguments.Count != 2)
  670. return false;
  671. if (!call.Arguments[0].MatchLdLoc(stringBuilder))
  672. return false;
  673. //Roslyn 4.0.0-3.final start to use char for 1 length string
  674. if (call.Method.Parameters[0].Type.IsKnownType(KnownTypeCode.Char))
  675. {
  676. return val != null && val.Length == 1 && call.Arguments[1].MatchLdcI4(val[0]);
  677. }
  678. return call.Arguments[1].MatchLdStr(out string val1) && val1 == val;
  679. }
  680. }
  681. private bool IsGeneratedEquals(IMethod method)
  682. {
  683. // virtual bool Equals(R? other) {
  684. // return other != null && EqualityContract == other.EqualityContract && EqualityComparer<int>.Default.Equals(A, other.A) && ...;
  685. // }
  686. // Starting with Roslyn 3.10, it's:
  687. // virtual bool Equals(R? other) {
  688. // return this == other || other != null && EqualityContract == other.EqualityContract && EqualityComparer<int>.Default.Equals(A, other.A) && ...;
  689. // }
  690. Debug.Assert(method.Name == "Equals" && method.Parameters.Count == 1);
  691. if (method.Parameters.Count != 1)
  692. return false;
  693. if (!isSealed && !method.IsOverridable)
  694. return false;
  695. if (method.GetAttributes().Any(attr => !IsAllowedAttribute(attr)) || method.GetReturnTypeAttributes().Any())
  696. return false;
  697. if (orderedMembers == null)
  698. return false;
  699. var body = DecompileBody(method);
  700. if (body == null)
  701. return false;
  702. if (!body.Instructions[0].MatchReturn(out var returnValue))
  703. return false;
  704. // special case for empty record struct; always returns true;
  705. if (returnValue.MatchLdcI4(1))
  706. return true;
  707. var variables = body.Ancestors.OfType<ILFunction>().Single().Variables;
  708. var other = variables.Single(v => v.Kind == VariableKind.Parameter && v.Index == 0);
  709. Debug.Assert(IsRecordType(other.Type));
  710. if (returnValue.MatchLogicOr(out var lhs, out var rhs))
  711. {
  712. // this == other || ...
  713. if (!lhs.MatchCompEquals(out var compLeft, out var compRight))
  714. return false;
  715. if (!compLeft.MatchLdThis())
  716. return false;
  717. if (!compRight.MatchLdLoc(other))
  718. return false;
  719. returnValue = rhs;
  720. }
  721. var conditions = UnpackLogicAndChain(returnValue);
  722. Debug.Assert(conditions.Count >= 1);
  723. int pos = 0;
  724. if (!isStruct)
  725. {
  726. if (isInheritedRecord)
  727. {
  728. // call BaseClass::Equals(ldloc this, ldloc other)
  729. if (pos >= conditions.Count)
  730. return false;
  731. if (!(conditions[pos] is Call { Method: { Name: "Equals" } } call))
  732. return false;
  733. if (!NormalizeTypeVisitor.TypeErasure.EquivalentTypes(call.Method.DeclaringType, baseClass))
  734. return false;
  735. if (call.Arguments.Count != 2)
  736. return false;
  737. if (!call.Arguments[0].MatchLdThis())
  738. return false;
  739. if (!call.Arguments[1].MatchLdLoc(other))
  740. return false;
  741. pos++;
  742. }
  743. else
  744. {
  745. // comp.o(ldloc other != ldnull)
  746. if (pos >= conditions.Count)
  747. return false;
  748. if (!conditions[pos].MatchCompNotEqualsNull(out var arg))
  749. return false;
  750. if (!arg.MatchLdLoc(other))
  751. return false;
  752. pos++;
  753. // call op_Equality(callvirt get_EqualityContract(ldloc this), callvirt get_EqualityContract(ldloc other))
  754. // Special-cased because Roslyn isn't using EqualityComparer<T> here.
  755. if (pos >= conditions.Count)
  756. return false;
  757. if (!(conditions[pos] is Call { Method: { IsOperator: true, Name: "op_Equality" } } opEqualityCall))
  758. return false;
  759. if (!opEqualityCall.Method.DeclaringType.IsKnownType(KnownTypeCode.Type))
  760. return false;
  761. if (opEqualityCall.Arguments.Count != 2)
  762. return false;
  763. if (!MatchGetEqualityContract(opEqualityCall.Arguments[0], out var target1))
  764. return false;
  765. if (!MatchGetEqualityContract(opEqualityCall.Arguments[1], out var target2))
  766. return false;
  767. if (!target1.MatchLdThis())
  768. return false;
  769. if (!target2.MatchLdLoc(other))
  770. return false;
  771. pos++;
  772. }
  773. }
  774. foreach (var member in orderedMembers)
  775. {
  776. if (!MemberConsideredForEquality(member))
  777. continue;
  778. if (!isStruct && member.Name == "EqualityContract")
  779. {
  780. continue; // already special-cased
  781. }
  782. // EqualityComparer<int>.Default.Equals(A, other.A)
  783. // callvirt Equals(call get_Default(), ldfld <A>k__BackingField(ldloc this), ldfld <A>k__BackingField(ldloc other))
  784. if (pos >= conditions.Count)
  785. return false;
  786. if (!(conditions[pos] is CallVirt { Method: { Name: "Equals" } } equalsCall))
  787. return false;
  788. if (equalsCall.Arguments.Count != 3)
  789. return false;
  790. if (!IsEqualityComparerGetDefaultCall(equalsCall.Arguments[0], member.ReturnType))
  791. return false;
  792. if (!MatchMemberAccess(equalsCall.Arguments[1], out var target1, out var member1))
  793. return false;
  794. if (!MatchMemberAccess(equalsCall.Arguments[2], out var target2, out var member2))
  795. return false;
  796. if (!target1.MatchLdThis())
  797. return false;
  798. if (!member1.Equals(member))
  799. return false;
  800. if (!(isStruct ? target2.MatchLdLoca(other) : target2.MatchLdLoc(other)))
  801. return false;
  802. if (!member2.Equals(member))
  803. return false;
  804. pos++;
  805. }
  806. return pos == conditions.Count;
  807. }
  808. static List<ILInstruction> UnpackLogicAndChain(ILInstruction rootOfChain)
  809. {
  810. var result = new List<ILInstruction>();
  811. Visit(rootOfChain);
  812. return result;
  813. void Visit(ILInstruction inst)
  814. {
  815. if (inst.MatchLogicAnd(out var lhs, out var rhs))
  816. {
  817. Visit(lhs);
  818. Visit(rhs);
  819. }
  820. else
  821. {
  822. result.Add(inst);
  823. }
  824. }
  825. }
  826. private bool MatchGetEqualityContract(ILInstruction inst, out ILInstruction target)
  827. {
  828. target = null;
  829. if (!(inst is CallInstruction { Method: { Name: "get_EqualityContract" } } call))
  830. return false;
  831. if (!(inst is CallVirt || (isSealed && inst is Call)))
  832. return false;
  833. if (call.Arguments.Count != 1)
  834. return false;
  835. target = call.Arguments[0];
  836. return true;
  837. }
  838. private static bool IsEqualityComparerGetDefaultCall(ILInstruction inst, IType type)
  839. {
  840. if (!(inst is Call { Method: { Name: "get_Default", IsStatic: true } } call))
  841. return false;
  842. if (!(call.Method.DeclaringType is { Name: "EqualityComparer", Namespace: "System.Collections.Generic" }))
  843. return false;
  844. if (call.Method.DeclaringType.TypeArguments.Count != 1)
  845. return false;
  846. if (!NormalizeTypeVisitor.TypeErasure.EquivalentTypes(call.Method.DeclaringType.TypeArguments[0], type))
  847. return false;
  848. return call.Arguments.Count == 0;
  849. }
  850. bool MemberConsideredForEquality(IMember member)
  851. {
  852. if (member.IsStatic)
  853. return false;
  854. if (member is IProperty property)
  855. {
  856. if (!isStruct && property.Name == "EqualityContract")
  857. return !isInheritedRecord;
  858. return autoPropertyToBackingField.ContainsKey(property);
  859. }
  860. else
  861. {
  862. return member is IField;
  863. }
  864. }
  865. bool IsGeneratedGetHashCode(IMethod method)
  866. {
  867. /*
  868. return (
  869. (
  870. EqualityComparer<Type>.Default.GetHashCode(EqualityContract) * -1521134295 + EqualityComparer<int>.Default.GetHashCode(A)
  871. ) * -1521134295 + EqualityComparer<int>.Default.GetHashCode(B)
  872. ) * -1521134295 + EqualityComparer<object>.Default.GetHashCode(C);
  873. */
  874. Debug.Assert(method.Name == "GetHashCode");
  875. if (method.Parameters.Count != 0)
  876. return false;
  877. if (!method.IsOverride || method.IsSealed)
  878. return false;
  879. if (method.GetAttributes().Any(attr => !IsAllowedAttribute(attr)) || method.GetReturnTypeAttributes().Any())
  880. return false;
  881. if (orderedMembers == null)
  882. return false;
  883. var body = DecompileBody(method);
  884. if (body == null)
  885. return false;
  886. if (!body.Instructions[0].MatchReturn(out var returnValue))
  887. return false;
  888. // special case for empty record struct; always returns false;
  889. if (returnValue.MatchLdcI4(0))
  890. return true;
  891. var hashedMembers = new List<IMember>();
  892. bool foundBaseClassHash = false;
  893. if (!Visit(returnValue))
  894. return false;
  895. if (foundBaseClassHash != isInheritedRecord)
  896. return false;
  897. return orderedMembers.Where(MemberConsideredForEquality).SequenceEqual(hashedMembers);
  898. bool Visit(ILInstruction inst)
  899. {
  900. if (inst is BinaryNumericInstruction {
  901. Operator: BinaryNumericOperator.Add,
  902. CheckForOverflow: false,
  903. Left: BinaryNumericInstruction {
  904. Operator: BinaryNumericOperator.Mul,
  905. CheckForOverflow: false,
  906. Left: var left,
  907. Right: LdcI4 { Value: -1521134295 }
  908. },
  909. Right: var right
  910. })
  911. {
  912. if (!Visit(left))
  913. return false;
  914. return ProcessIndividualHashCode(right);
  915. }
  916. else
  917. {
  918. return ProcessIndividualHashCode(inst);
  919. }
  920. }
  921. bool ProcessIndividualHashCode(ILInstruction inst)
  922. {
  923. // base.GetHashCode(): call GetHashCode(ldloc this)
  924. if (inst is Call { Method: { Name: "GetHashCode" } } baseHashCodeCall)
  925. {
  926. if (baseHashCodeCall.Arguments.Count != 1)
  927. return false;
  928. if (!baseHashCodeCall.Arguments[0].MatchLdThis())
  929. return false;
  930. if (foundBaseClassHash || hashedMembers.Count > 0)
  931. return false; // must be first
  932. foundBaseClassHash = true;
  933. return baseHashCodeCall.Method.DeclaringType.Equals(baseClass);
  934. }
  935. // callvirt GetHashCode(call get_Default(), callvirt get_EqualityContract(ldloc this))
  936. // callvirt GetHashCode(call get_Default(), ldfld <A>k__BackingField(ldloc this)))
  937. if (!(inst is CallVirt { Method: { Name: "GetHashCode" } } getHashCodeCall))
  938. return false;
  939. if (getHashCodeCall.Arguments.Count != 2)
  940. return false;
  941. // getHashCodeCall.Arguments[0] checked later
  942. if (!MatchMemberAccess(getHashCodeCall.Arguments[1], out var target, out var member))
  943. return false;
  944. if (!target.MatchLdThis())
  945. return false;
  946. if (!IsEqualityComparerGetDefaultCall(getHashCodeCall.Arguments[0], member.ReturnType))
  947. return false;
  948. hashedMembers.Add(member);
  949. return true;
  950. }
  951. }
  952. bool IsGeneratedDeconstruct(IMethod method)
  953. {
  954. Debug.Assert(method.Name == "Deconstruct" && method.Parameters.Count == primaryCtor.Parameters.Count);
  955. if (!method.ReturnType.IsKnownType(KnownTypeCode.Void))
  956. return false;
  957. for (int i = 0; i < method.Parameters.Count; i++)
  958. {
  959. var deconstruct = method.Parameters[i];
  960. var ctor = primaryCtor.Parameters[i];
  961. if (!deconstruct.IsOut)
  962. return false;
  963. IType ctorType = ctor.Type;
  964. if (ctor.IsIn)
  965. ctorType = ((ByReferenceType)ctorType).ElementType;
  966. if (!ctorType.Equals(((ByReferenceType)deconstruct.Type).ElementType))
  967. return false;
  968. if (ctor.Name != deconstruct.Name)
  969. return false;
  970. }
  971. var body = DecompileBody(method);
  972. if (body == null || body.Instructions.Count != method.Parameters.Count + 1)
  973. return false;
  974. for (int i = 0; i < body.Instructions.Count - 1; i++)
  975. {
  976. // stobj T(ldloc parameter, call getter(ldloc this))
  977. if (!body.Instructions[i].MatchStObj(out var targetInst, out var getter, out _))
  978. return false;
  979. if (!targetInst.MatchLdLoc(out var target))
  980. return false;
  981. if (!(target.Kind == VariableKind.Parameter && target.Index == i))
  982. return false;
  983. if (getter is not Call call || call.Arguments.Count != 1)
  984. return false;
  985. if (!call.Arguments[0].MatchLdThis())
  986. return false;
  987. if (!call.Method.IsAccessor)
  988. return false;
  989. var autoProperty = (IProperty)call.Method.AccessorOwner;
  990. if (!autoPropertyToBackingField.ContainsKey(autoProperty))
  991. return false;
  992. }
  993. var returnInst = body.Instructions.LastOrDefault();
  994. return returnInst != null && returnInst.MatchReturn(out var retVal) && retVal.MatchNop();
  995. }
  996. bool MatchMemberAccess(ILInstruction inst, out ILInstruction target, out IMember member)
  997. {
  998. target = null;
  999. member = null;
  1000. if (inst is CallInstruction {
  1001. Method: {
  1002. AccessorKind: System.Reflection.MethodSemanticsAttributes.Getter,
  1003. AccessorOwner: IProperty property
  1004. }
  1005. } call && (call is CallVirt || (isSealed && call is Call)))
  1006. {
  1007. if (call.Arguments.Count != 1)
  1008. return false;
  1009. target = call.Arguments[0];
  1010. member = property;
  1011. return true;
  1012. }
  1013. else if (inst.MatchLdFld(out target, out IField field))
  1014. {
  1015. if (backingFieldToAutoProperty.TryGetValue(field, out property))
  1016. member = property;
  1017. else
  1018. member = field;
  1019. return true;
  1020. }
  1021. else
  1022. {
  1023. return false;
  1024. }
  1025. }
  1026. Block DecompileBody(IMethod method)
  1027. {
  1028. if (method == null || method.MetadataToken.IsNil)
  1029. return null;
  1030. var metadata = typeSystem.MainModule.metadata;
  1031. var methodDefHandle = (MethodDefinitionHandle)method.MetadataToken;
  1032. var methodDef = metadata.GetMethodDefinition(methodDefHandle);
  1033. if (!methodDef.HasBody())
  1034. return null;
  1035. var genericContext = new GenericContext(
  1036. classTypeParameters: recordTypeDef.TypeParameters,
  1037. methodTypeParameters: null);
  1038. var body = typeSystem.MainModule.PEFile.Reader.GetMethodBody(methodDef.RelativeVirtualAddress);
  1039. var ilReader = new ILReader(typeSystem.MainModule);
  1040. var il = ilReader.ReadIL(methodDefHandle, body, genericContext, ILFunctionKind.TopLevelFunction, cancellationToken);
  1041. var settings = new DecompilerSettings(LanguageVersion.CSharp1);
  1042. var transforms = CSharpDecompiler.GetILTransforms();
  1043. // Remove the last couple transforms -- we don't need variable names etc. here
  1044. int lastBlockTransform = transforms.FindLastIndex(t => t is BlockILTransform);
  1045. transforms.RemoveRange(lastBlockTransform + 1, transforms.Count - (lastBlockTransform + 1));
  1046. // Use CombineExitsTransform so that "return other != null && ...;" is a single statement even in release builds
  1047. transforms.Add(new CombineExitsTransform());
  1048. il.RunTransforms(transforms,
  1049. new ILTransformContext(il, typeSystem, debugInfo: null, settings) {
  1050. CancellationToken = cancellationToken
  1051. });
  1052. if (il.Body is BlockContainer container)
  1053. {
  1054. return container.EntryPoint;
  1055. }
  1056. else if (il.Body is Block block)
  1057. {
  1058. return block;
  1059. }
  1060. else
  1061. {
  1062. return null;
  1063. }
  1064. }
  1065. }
  1066. }