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.

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