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.

1119 lines
37 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. if (property.GetAttributes().Any())
  415. return false;
  416. if (getter.GetReturnTypeAttributes().Any())
  417. return false;
  418. var attrs = getter.GetAttributes().ToList();
  419. if (attrs.Count != 1)
  420. return false;
  421. if (!attrs[0].AttributeType.IsKnownType(KnownAttribute.CompilerGenerated))
  422. return false;
  423. var body = DecompileBody(getter);
  424. if (body == null || body.Instructions.Count != 1)
  425. return false;
  426. if (!(body.Instructions.Single() is Leave leave))
  427. return false;
  428. // leave IL_0000 (call GetTypeFromHandle(ldtypetoken R))
  429. if (!TransformExpressionTrees.MatchGetTypeFromHandle(leave.Value, out IType ty))
  430. return false;
  431. return IsRecordType(ty);
  432. }
  433. private bool IsGeneratedPrintMembers(IMethod method)
  434. {
  435. Debug.Assert(method.Name == "PrintMembers");
  436. if (method.Parameters.Count != 1)
  437. return false;
  438. if (!isSealed && !method.IsOverridable)
  439. return false;
  440. if (method.GetAttributes().Any(attr => !IsAllowedAttribute(attr)) || method.GetReturnTypeAttributes().Any())
  441. return false;
  442. if (method.Accessibility != Accessibility.Protected && (!isSealed || method.Accessibility != Accessibility.Private))
  443. return false;
  444. if (orderedMembers == null)
  445. return false;
  446. var body = DecompileBody(method);
  447. if (body == null)
  448. return false;
  449. var variables = body.Ancestors.OfType<ILFunction>().Single().Variables;
  450. var builder = variables.Single(v => v.Kind == VariableKind.Parameter && v.Index == 0);
  451. if (builder.Type.ReflectionName != "System.Text.StringBuilder")
  452. return false;
  453. int pos = 0;
  454. //Roslyn 4.0.0-3.final start to insert an call to RuntimeHelpers.EnsureSufficientExecutionStack()
  455. if (!isStruct && !isInheritedRecord && body.Instructions[pos] is Call {
  456. Arguments: { Count: 0 },
  457. Method: { Name: "EnsureSufficientExecutionStack", DeclaringType: { Namespace: "System.Runtime.CompilerServices", Name: "RuntimeHelpers" } }
  458. })
  459. {
  460. pos++;
  461. }
  462. if (isInheritedRecord)
  463. {
  464. // Special case: inherited record adding no new members
  465. if (body.Instructions[pos].MatchReturn(out var returnValue)
  466. && IsBaseCall(returnValue) && !orderedMembers.Any(IsPrintedMember))
  467. {
  468. return true;
  469. }
  470. // if (call PrintMembers(ldloc this, ldloc builder)) Block IL_000f {
  471. // callvirt Append(ldloc builder, ldstr ", ")
  472. // }
  473. if (!body.Instructions[pos].MatchIfInstruction(out var condition, out var trueInst))
  474. return false;
  475. if (!IsBaseCall(condition))
  476. return false;
  477. // trueInst = callvirt Append(ldloc builder, ldstr ", ")
  478. trueInst = Block.Unwrap(trueInst);
  479. if (!MatchStringBuilderAppend(trueInst, builder, out var val))
  480. return false;
  481. if (!(val.MatchLdStr(out string text) && text == ", "))
  482. return false;
  483. pos++;
  484. bool IsBaseCall(ILInstruction inst)
  485. {
  486. if (!(inst is CallInstruction { Method: { Name: "PrintMembers" } } call))
  487. return false;
  488. if (call.Arguments.Count != 2)
  489. return false;
  490. if (!call.Arguments[0].MatchLdThis())
  491. return false;
  492. if (!call.Arguments[1].MatchLdLoc(builder))
  493. return false;
  494. return true;
  495. }
  496. }
  497. bool needsComma = false;
  498. foreach (var member in orderedMembers)
  499. {
  500. if (!IsPrintedMember(member))
  501. continue;
  502. cancellationToken.ThrowIfCancellationRequested();
  503. /*
  504. callvirt Append(ldloc builder, ldstr "A")
  505. callvirt Append(ldloc builder, ldstr " = ")
  506. callvirt Append(ldloc builder, constrained[System.Int32].callvirt ToString(addressof System.Int32(call get_A(ldloc this))))
  507. callvirt Append(ldloc builder, ldstr ", ")
  508. callvirt Append(ldloc builder, ldstr "B")
  509. callvirt Append(ldloc builder, ldstr " = ")
  510. callvirt Append(ldloc builder, constrained[System.Int32].callvirt ToString(ldflda B(ldloc this)))
  511. leave IL_0000 (ldc.i4 1) */
  512. if (!MatchStringBuilderAppendConstant(out string text))
  513. return false;
  514. string expectedText = (needsComma ? ", " : "") + member.Name + " = ";
  515. if (text != expectedText)
  516. return false;
  517. if (!MatchStringBuilderAppend(body.Instructions[pos], builder, out var val))
  518. return false;
  519. if (val is CallInstruction { Method: { Name: "ToString", IsStatic: false } } toStringCall)
  520. {
  521. if (toStringCall.Arguments.Count != 1)
  522. return false;
  523. val = toStringCall.Arguments[0];
  524. if (val is AddressOf addressOf)
  525. {
  526. val = addressOf.Value;
  527. }
  528. }
  529. else if (val is Box box)
  530. {
  531. if (!NormalizeTypeVisitor.TypeErasure.EquivalentTypes(box.Type, member.ReturnType))
  532. return false;
  533. val = box.Argument;
  534. }
  535. if (val is CallInstruction getterCall && member is IProperty property)
  536. {
  537. if (!getterCall.Method.Equals(property.Getter))
  538. return false;
  539. if (getterCall.Arguments.Count != 1)
  540. return false;
  541. if (!getterCall.Arguments[0].MatchLdThis())
  542. return false;
  543. }
  544. else if (val.MatchLdFld(out var target, out var field) || val.MatchLdFlda(out target, out field))
  545. {
  546. if (!target.MatchLdThis())
  547. return false;
  548. if (!field.Equals(member))
  549. return false;
  550. }
  551. else
  552. {
  553. return false;
  554. }
  555. pos++;
  556. needsComma = true;
  557. }
  558. // leave IL_0000 (ldc.i4 1)
  559. return body.Instructions[pos].MatchReturn(out var retVal)
  560. && retVal.MatchLdcI4(needsComma ? 1 : 0);
  561. bool IsPrintedMember(IMember member)
  562. {
  563. if (member.IsStatic)
  564. {
  565. return false; // static fields/properties are not printed
  566. }
  567. if (!isStruct && member.Name == "EqualityContract")
  568. {
  569. return false; // EqualityContract is never printed
  570. }
  571. if (member.IsExplicitInterfaceImplementation)
  572. {
  573. return false; // explicit interface impls are not printed
  574. }
  575. if (member.IsOverride)
  576. {
  577. return false; // override is not printed (again), the virtual base property was already printed
  578. }
  579. return true;
  580. }
  581. bool MatchStringBuilderAppendConstant(out string text)
  582. {
  583. text = null;
  584. while (MatchStringBuilderAppend(body.Instructions[pos], builder, out var val) && val.MatchLdStr(out string valText))
  585. {
  586. text += valText;
  587. pos++;
  588. }
  589. return text != null;
  590. }
  591. }
  592. private bool MatchStringBuilderAppend(ILInstruction inst, ILVariable sb, out ILInstruction val)
  593. {
  594. val = null;
  595. if (!(inst is CallVirt { Method: { Name: "Append", DeclaringType: { Namespace: "System.Text", Name: "StringBuilder" } } } call))
  596. return false;
  597. if (call.Arguments.Count != 2)
  598. return false;
  599. if (!call.Arguments[0].MatchLdLoc(sb))
  600. return false;
  601. val = call.Arguments[1];
  602. return true;
  603. }
  604. private bool IsGeneratedToString(IMethod method)
  605. {
  606. Debug.Assert(method.Name == "ToString" && method.Parameters.Count == 0);
  607. if (!method.IsOverride)
  608. return false;
  609. if (method.IsSealed)
  610. return false;
  611. if (method.GetAttributes().Any(attr => !IsAllowedAttribute(attr)) || method.GetReturnTypeAttributes().Any())
  612. return false;
  613. var body = DecompileBody(method);
  614. if (body == null)
  615. return false;
  616. // stloc stringBuilder(newobj StringBuilder..ctor())
  617. if (!body.Instructions[0].MatchStLoc(out var stringBuilder, out var stringBuilderInit))
  618. return false;
  619. if (!(stringBuilderInit is NewObj { Arguments: { Count: 0 }, Method: { DeclaringTypeDefinition: { Name: "StringBuilder", Namespace: "System.Text" } } }))
  620. return false;
  621. // callvirt Append(ldloc stringBuilder, ldstr "R")
  622. if (!MatchAppendCallWithValue(body.Instructions[1], recordTypeDef.Name))
  623. return false;
  624. // callvirt Append(ldloc stringBuilder, ldstr " { ")
  625. if (!MatchAppendCallWithValue(body.Instructions[2], " { "))
  626. return false;
  627. // if (callvirt PrintMembers(ldloc this, ldloc stringBuilder)) { trueInst }
  628. if (!body.Instructions[3].MatchIfInstruction(out var condition, out var trueInst))
  629. return true;
  630. if (!((condition is CallInstruction { Method: { Name: "PrintMembers" } } printMembersCall) &&
  631. (condition is CallVirt || (isSealed && condition is Call))))
  632. return false;
  633. if (printMembersCall.Arguments.Count != 2)
  634. return false;
  635. if (!printMembersCall.Arguments[0].MatchLdThis())
  636. return false;
  637. if (!printMembersCall.Arguments[1].MatchLdLoc(stringBuilder))
  638. return false;
  639. // trueInst: callvirt Append(ldloc stringBuilder, ldstr " ")
  640. if (!MatchAppendCallWithValue(Block.Unwrap(trueInst), " "))
  641. return false;
  642. // callvirt Append(ldloc stringBuilder, ldstr "}")
  643. if (!MatchAppendCallWithValue(body.Instructions[4], "}"))
  644. return false;
  645. // leave IL_0000 (callvirt ToString(ldloc stringBuilder))
  646. if (!(body.Instructions[5] is Leave leave))
  647. return false;
  648. if (!(leave.Value is CallVirt { Method: { Name: "ToString" } } toStringCall))
  649. return false;
  650. if (toStringCall.Arguments.Count != 1)
  651. return false;
  652. return toStringCall.Arguments[0].MatchLdLoc(stringBuilder);
  653. bool MatchAppendCallWithValue(ILInstruction inst, string val)
  654. {
  655. if (!(inst is CallVirt { Method: { Name: "Append" } } call))
  656. return false;
  657. if (call.Arguments.Count != 2)
  658. return false;
  659. if (!call.Arguments[0].MatchLdLoc(stringBuilder))
  660. return false;
  661. //Roslyn 4.0.0-3.final start to use char for 1 length string
  662. if (call.Method.Parameters[0].Type.IsKnownType(KnownTypeCode.Char))
  663. {
  664. return val != null && val.Length == 1 && call.Arguments[1].MatchLdcI4(val[0]);
  665. }
  666. return call.Arguments[1].MatchLdStr(out string val1) && val1 == val;
  667. }
  668. }
  669. private bool IsGeneratedEquals(IMethod method)
  670. {
  671. // virtual bool Equals(R? other) {
  672. // return other != null && EqualityContract == other.EqualityContract && EqualityComparer<int>.Default.Equals(A, other.A) && ...;
  673. // }
  674. // Starting with Roslyn 3.10, it's:
  675. // virtual bool Equals(R? other) {
  676. // return this == other || other != null && EqualityContract == other.EqualityContract && EqualityComparer<int>.Default.Equals(A, other.A) && ...;
  677. // }
  678. Debug.Assert(method.Name == "Equals" && method.Parameters.Count == 1);
  679. if (method.Parameters.Count != 1)
  680. return false;
  681. if (!isSealed && !method.IsOverridable)
  682. return false;
  683. if (method.GetAttributes().Any(attr => !IsAllowedAttribute(attr)) || method.GetReturnTypeAttributes().Any())
  684. return false;
  685. if (orderedMembers == null)
  686. return false;
  687. var body = DecompileBody(method);
  688. if (body == null)
  689. return false;
  690. if (!body.Instructions[0].MatchReturn(out var returnValue))
  691. return false;
  692. // special case for empty record struct; always returns true;
  693. if (returnValue.MatchLdcI4(1))
  694. return true;
  695. var variables = body.Ancestors.OfType<ILFunction>().Single().Variables;
  696. var other = variables.Single(v => v.Kind == VariableKind.Parameter && v.Index == 0);
  697. Debug.Assert(IsRecordType(other.Type));
  698. if (returnValue.MatchLogicOr(out var lhs, out var rhs))
  699. {
  700. // this == other || ...
  701. if (!lhs.MatchCompEquals(out var compLeft, out var compRight))
  702. return false;
  703. if (!compLeft.MatchLdThis())
  704. return false;
  705. if (!compRight.MatchLdLoc(other))
  706. return false;
  707. returnValue = rhs;
  708. }
  709. var conditions = UnpackLogicAndChain(returnValue);
  710. Debug.Assert(conditions.Count >= 1);
  711. int pos = 0;
  712. if (!isStruct)
  713. {
  714. if (isInheritedRecord)
  715. {
  716. // call BaseClass::Equals(ldloc this, ldloc other)
  717. if (pos >= conditions.Count)
  718. return false;
  719. if (!(conditions[pos] is Call { Method: { Name: "Equals" } } call))
  720. return false;
  721. if (!NormalizeTypeVisitor.TypeErasure.EquivalentTypes(call.Method.DeclaringType, baseClass))
  722. return false;
  723. if (call.Arguments.Count != 2)
  724. return false;
  725. if (!call.Arguments[0].MatchLdThis())
  726. return false;
  727. if (!call.Arguments[1].MatchLdLoc(other))
  728. return false;
  729. pos++;
  730. }
  731. else
  732. {
  733. // comp.o(ldloc other != ldnull)
  734. if (pos >= conditions.Count)
  735. return false;
  736. if (!conditions[pos].MatchCompNotEqualsNull(out var arg))
  737. return false;
  738. if (!arg.MatchLdLoc(other))
  739. return false;
  740. pos++;
  741. // call op_Equality(callvirt get_EqualityContract(ldloc this), callvirt get_EqualityContract(ldloc other))
  742. // Special-cased because Roslyn isn't using EqualityComparer<T> here.
  743. if (pos >= conditions.Count)
  744. return false;
  745. if (!(conditions[pos] is Call { Method: { IsOperator: true, Name: "op_Equality" } } opEqualityCall))
  746. return false;
  747. if (!opEqualityCall.Method.DeclaringType.IsKnownType(KnownTypeCode.Type))
  748. return false;
  749. if (opEqualityCall.Arguments.Count != 2)
  750. return false;
  751. if (!MatchGetEqualityContract(opEqualityCall.Arguments[0], out var target1))
  752. return false;
  753. if (!MatchGetEqualityContract(opEqualityCall.Arguments[1], out var target2))
  754. return false;
  755. if (!target1.MatchLdThis())
  756. return false;
  757. if (!target2.MatchLdLoc(other))
  758. return false;
  759. pos++;
  760. }
  761. }
  762. foreach (var member in orderedMembers)
  763. {
  764. if (!MemberConsideredForEquality(member))
  765. continue;
  766. if (!isStruct && member.Name == "EqualityContract")
  767. {
  768. continue; // already special-cased
  769. }
  770. // EqualityComparer<int>.Default.Equals(A, other.A)
  771. // callvirt Equals(call get_Default(), ldfld <A>k__BackingField(ldloc this), ldfld <A>k__BackingField(ldloc other))
  772. if (pos >= conditions.Count)
  773. return false;
  774. if (!(conditions[pos] is CallVirt { Method: { Name: "Equals" } } equalsCall))
  775. return false;
  776. if (equalsCall.Arguments.Count != 3)
  777. return false;
  778. if (!IsEqualityComparerGetDefaultCall(equalsCall.Arguments[0], member.ReturnType))
  779. return false;
  780. if (!MatchMemberAccess(equalsCall.Arguments[1], out var target1, out var member1))
  781. return false;
  782. if (!MatchMemberAccess(equalsCall.Arguments[2], out var target2, out var member2))
  783. return false;
  784. if (!target1.MatchLdThis())
  785. return false;
  786. if (!member1.Equals(member))
  787. return false;
  788. if (!(isStruct ? target2.MatchLdLoca(other) : target2.MatchLdLoc(other)))
  789. return false;
  790. if (!member2.Equals(member))
  791. return false;
  792. pos++;
  793. }
  794. return pos == conditions.Count;
  795. }
  796. static List<ILInstruction> UnpackLogicAndChain(ILInstruction rootOfChain)
  797. {
  798. var result = new List<ILInstruction>();
  799. Visit(rootOfChain);
  800. return result;
  801. void Visit(ILInstruction inst)
  802. {
  803. if (inst.MatchLogicAnd(out var lhs, out var rhs))
  804. {
  805. Visit(lhs);
  806. Visit(rhs);
  807. }
  808. else
  809. {
  810. result.Add(inst);
  811. }
  812. }
  813. }
  814. private bool MatchGetEqualityContract(ILInstruction inst, out ILInstruction target)
  815. {
  816. target = null;
  817. if (!(inst is CallInstruction { Method: { Name: "get_EqualityContract" } } call))
  818. return false;
  819. if (!(inst is CallVirt || (isSealed && inst is Call)))
  820. return false;
  821. if (call.Arguments.Count != 1)
  822. return false;
  823. target = call.Arguments[0];
  824. return true;
  825. }
  826. private static bool IsEqualityComparerGetDefaultCall(ILInstruction inst, IType type)
  827. {
  828. if (!(inst is Call { Method: { Name: "get_Default", IsStatic: true } } call))
  829. return false;
  830. if (!(call.Method.DeclaringType is { Name: "EqualityComparer", Namespace: "System.Collections.Generic" }))
  831. return false;
  832. if (call.Method.DeclaringType.TypeArguments.Count != 1)
  833. return false;
  834. if (!NormalizeTypeVisitor.TypeErasure.EquivalentTypes(call.Method.DeclaringType.TypeArguments[0], type))
  835. return false;
  836. return call.Arguments.Count == 0;
  837. }
  838. bool MemberConsideredForEquality(IMember member)
  839. {
  840. if (member.IsStatic)
  841. return false;
  842. if (member is IProperty property)
  843. {
  844. if (!isStruct && property.Name == "EqualityContract")
  845. return !isInheritedRecord;
  846. return autoPropertyToBackingField.ContainsKey(property);
  847. }
  848. else
  849. {
  850. return member is IField;
  851. }
  852. }
  853. bool IsGeneratedGetHashCode(IMethod method)
  854. {
  855. /*
  856. return (
  857. (
  858. EqualityComparer<Type>.Default.GetHashCode(EqualityContract) * -1521134295 + EqualityComparer<int>.Default.GetHashCode(A)
  859. ) * -1521134295 + EqualityComparer<int>.Default.GetHashCode(B)
  860. ) * -1521134295 + EqualityComparer<object>.Default.GetHashCode(C);
  861. */
  862. Debug.Assert(method.Name == "GetHashCode");
  863. if (method.Parameters.Count != 0)
  864. return false;
  865. if (!method.IsOverride || method.IsSealed)
  866. return false;
  867. if (method.GetAttributes().Any(attr => !IsAllowedAttribute(attr)) || method.GetReturnTypeAttributes().Any())
  868. return false;
  869. if (orderedMembers == null)
  870. return false;
  871. var body = DecompileBody(method);
  872. if (body == null)
  873. return false;
  874. if (!body.Instructions[0].MatchReturn(out var returnValue))
  875. return false;
  876. // special case for empty record struct; always returns false;
  877. if (returnValue.MatchLdcI4(0))
  878. return true;
  879. var hashedMembers = new List<IMember>();
  880. bool foundBaseClassHash = false;
  881. if (!Visit(returnValue))
  882. return false;
  883. if (foundBaseClassHash != isInheritedRecord)
  884. return false;
  885. return orderedMembers.Where(MemberConsideredForEquality).SequenceEqual(hashedMembers);
  886. bool Visit(ILInstruction inst)
  887. {
  888. if (inst is BinaryNumericInstruction {
  889. Operator: BinaryNumericOperator.Add,
  890. CheckForOverflow: false,
  891. Left: BinaryNumericInstruction {
  892. Operator: BinaryNumericOperator.Mul,
  893. CheckForOverflow: false,
  894. Left: var left,
  895. Right: LdcI4 { Value: -1521134295 }
  896. },
  897. Right: var right
  898. })
  899. {
  900. if (!Visit(left))
  901. return false;
  902. return ProcessIndividualHashCode(right);
  903. }
  904. else
  905. {
  906. return ProcessIndividualHashCode(inst);
  907. }
  908. }
  909. bool ProcessIndividualHashCode(ILInstruction inst)
  910. {
  911. // base.GetHashCode(): call GetHashCode(ldloc this)
  912. if (inst is Call { Method: { Name: "GetHashCode" } } baseHashCodeCall)
  913. {
  914. if (baseHashCodeCall.Arguments.Count != 1)
  915. return false;
  916. if (!baseHashCodeCall.Arguments[0].MatchLdThis())
  917. return false;
  918. if (foundBaseClassHash || hashedMembers.Count > 0)
  919. return false; // must be first
  920. foundBaseClassHash = true;
  921. return baseHashCodeCall.Method.DeclaringType.Equals(baseClass);
  922. }
  923. // callvirt GetHashCode(call get_Default(), callvirt get_EqualityContract(ldloc this))
  924. // callvirt GetHashCode(call get_Default(), ldfld <A>k__BackingField(ldloc this)))
  925. if (!(inst is CallVirt { Method: { Name: "GetHashCode" } } getHashCodeCall))
  926. return false;
  927. if (getHashCodeCall.Arguments.Count != 2)
  928. return false;
  929. // getHashCodeCall.Arguments[0] checked later
  930. if (!MatchMemberAccess(getHashCodeCall.Arguments[1], out var target, out var member))
  931. return false;
  932. if (!target.MatchLdThis())
  933. return false;
  934. if (!IsEqualityComparerGetDefaultCall(getHashCodeCall.Arguments[0], member.ReturnType))
  935. return false;
  936. hashedMembers.Add(member);
  937. return true;
  938. }
  939. }
  940. bool IsGeneratedDeconstruct(IMethod method)
  941. {
  942. Debug.Assert(method.Name == "Deconstruct" && method.Parameters.Count == primaryCtor.Parameters.Count);
  943. if (!method.ReturnType.IsKnownType(KnownTypeCode.Void))
  944. return false;
  945. for (int i = 0; i < method.Parameters.Count; i++)
  946. {
  947. var deconstruct = method.Parameters[i];
  948. var ctor = primaryCtor.Parameters[i];
  949. if (!deconstruct.IsOut)
  950. return false;
  951. IType ctorType = ctor.Type;
  952. if (ctor.IsIn)
  953. ctorType = ((ByReferenceType)ctorType).ElementType;
  954. if (!ctorType.Equals(((ByReferenceType)deconstruct.Type).ElementType))
  955. return false;
  956. if (ctor.Name != deconstruct.Name)
  957. return false;
  958. }
  959. var body = DecompileBody(method);
  960. if (body == null || body.Instructions.Count != method.Parameters.Count + 1)
  961. return false;
  962. for (int i = 0; i < body.Instructions.Count - 1; i++)
  963. {
  964. // stobj T(ldloc parameter, call getter(ldloc this))
  965. if (!body.Instructions[i].MatchStObj(out var targetInst, out var getter, out _))
  966. return false;
  967. if (!targetInst.MatchLdLoc(out var target))
  968. return false;
  969. if (!(target.Kind == VariableKind.Parameter && target.Index == i))
  970. return false;
  971. if (getter is not Call call || call.Arguments.Count != 1)
  972. return false;
  973. if (!call.Arguments[0].MatchLdThis())
  974. return false;
  975. if (!call.Method.IsAccessor)
  976. return false;
  977. var autoProperty = (IProperty)call.Method.AccessorOwner;
  978. if (!autoPropertyToBackingField.ContainsKey(autoProperty))
  979. return false;
  980. }
  981. var returnInst = body.Instructions.LastOrDefault();
  982. return returnInst != null && returnInst.MatchReturn(out var retVal) && retVal.MatchNop();
  983. }
  984. bool MatchMemberAccess(ILInstruction inst, out ILInstruction target, out IMember member)
  985. {
  986. target = null;
  987. member = null;
  988. if (inst is CallInstruction {
  989. Method: {
  990. AccessorKind: System.Reflection.MethodSemanticsAttributes.Getter,
  991. AccessorOwner: IProperty property
  992. }
  993. } call && (call is CallVirt || (isSealed && call is Call)))
  994. {
  995. if (call.Arguments.Count != 1)
  996. return false;
  997. target = call.Arguments[0];
  998. member = property;
  999. return true;
  1000. }
  1001. else if (inst.MatchLdFld(out target, out IField field))
  1002. {
  1003. if (backingFieldToAutoProperty.TryGetValue(field, out property))
  1004. member = property;
  1005. else
  1006. member = field;
  1007. return true;
  1008. }
  1009. else
  1010. {
  1011. return false;
  1012. }
  1013. }
  1014. Block DecompileBody(IMethod method)
  1015. {
  1016. if (method == null || method.MetadataToken.IsNil)
  1017. return null;
  1018. var metadata = typeSystem.MainModule.metadata;
  1019. var methodDefHandle = (MethodDefinitionHandle)method.MetadataToken;
  1020. var methodDef = metadata.GetMethodDefinition(methodDefHandle);
  1021. if (!methodDef.HasBody())
  1022. return null;
  1023. var genericContext = new GenericContext(
  1024. classTypeParameters: recordTypeDef.TypeParameters,
  1025. methodTypeParameters: null);
  1026. var body = typeSystem.MainModule.PEFile.Reader.GetMethodBody(methodDef.RelativeVirtualAddress);
  1027. var ilReader = new ILReader(typeSystem.MainModule);
  1028. var il = ilReader.ReadIL(methodDefHandle, body, genericContext, ILFunctionKind.TopLevelFunction, cancellationToken);
  1029. var settings = new DecompilerSettings(LanguageVersion.CSharp1);
  1030. var transforms = CSharpDecompiler.GetILTransforms();
  1031. // Remove the last couple transforms -- we don't need variable names etc. here
  1032. int lastBlockTransform = transforms.FindLastIndex(t => t is BlockILTransform);
  1033. transforms.RemoveRange(lastBlockTransform + 1, transforms.Count - (lastBlockTransform + 1));
  1034. // Use CombineExitsTransform so that "return other != null && ...;" is a single statement even in release builds
  1035. transforms.Add(new CombineExitsTransform());
  1036. il.RunTransforms(transforms,
  1037. new ILTransformContext(il, typeSystem, debugInfo: null, settings) {
  1038. CancellationToken = cancellationToken
  1039. });
  1040. if (il.Body is BlockContainer container)
  1041. {
  1042. return container.EntryPoint;
  1043. }
  1044. else if (il.Body is Block block)
  1045. {
  1046. return block;
  1047. }
  1048. else
  1049. {
  1050. return null;
  1051. }
  1052. }
  1053. }
  1054. }