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.

545 lines
20 KiB

5 years ago
  1. // Copyright (c) 2017 Siegfried Pammer
  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. #nullable enable
  19. using System;
  20. using System.Collections.Generic;
  21. using System.Linq;
  22. using ICSharpCode.Decompiler.CSharp.Resolver;
  23. using ICSharpCode.Decompiler.CSharp.TypeSystem;
  24. using ICSharpCode.Decompiler.Semantics;
  25. using ICSharpCode.Decompiler.TypeSystem;
  26. using ICSharpCode.Decompiler.Util;
  27. namespace ICSharpCode.Decompiler.IL.Transforms
  28. {
  29. /// <summary>
  30. /// Transforms collection and object initialization patterns.
  31. /// </summary>
  32. public class TransformCollectionAndObjectInitializers : IStatementTransform
  33. {
  34. void IStatementTransform.Run(Block block, int pos, StatementTransformContext context)
  35. {
  36. if (!context.Settings.ObjectOrCollectionInitializers)
  37. return;
  38. ILInstruction inst = block.Instructions[pos];
  39. // Match stloc(v, newobj)
  40. if (!inst.MatchStLoc(out var v, out var initInst) || v.Kind != VariableKind.Local && v.Kind != VariableKind.StackSlot)
  41. return;
  42. IType instType;
  43. var blockKind = BlockKind.CollectionInitializer;
  44. var insertionPos = initInst.ChildIndex;
  45. var siblings = initInst.Parent!.Children;
  46. IMethod currentMethod = context.Function.Method!;
  47. switch (initInst)
  48. {
  49. case NewObj newObjInst:
  50. if (newObjInst.ILStackWasEmpty && v.Kind == VariableKind.Local
  51. && !currentMethod.IsConstructor
  52. && !currentMethod.IsCompilerGeneratedOrIsInCompilerGeneratedClass())
  53. {
  54. // on statement level (no other expressions on IL stack),
  55. // prefer to keep local variables (but not stack slots),
  56. // unless we are in a constructor (where inlining object initializers might be critical
  57. // for the base ctor call) or a compiler-generated delegate method, which might be used in a query expression.
  58. return;
  59. }
  60. // Do not try to transform delegate construction.
  61. // DelegateConstruction transform cannot deal with this.
  62. if (DelegateConstruction.MatchDelegateConstruction(newObjInst, out _, out _, out _)
  63. || TransformDisplayClassUsage.IsPotentialClosure(context, newObjInst))
  64. return;
  65. // Cannot build a collection/object initializer attached to an AnonymousTypeCreateExpression
  66. // anon = new { A = 5 } { 3,4,5 } is invalid syntax.
  67. if (newObjInst.Method.DeclaringType.ContainsAnonymousType())
  68. return;
  69. instType = newObjInst.Method.DeclaringType;
  70. break;
  71. case DefaultValue defaultVal:
  72. instType = defaultVal.Type;
  73. break;
  74. case Call c when c.Method.FullNameIs("System.Activator", "CreateInstance") && c.Method.TypeArguments.Count == 1:
  75. instType = c.Method.TypeArguments[0];
  76. blockKind = BlockKind.ObjectInitializer;
  77. break;
  78. case CallInstruction ci when context.Settings.WithExpressions && IsRecordCloneMethodCall(ci):
  79. instType = ci.Method.DeclaringType;
  80. blockKind = BlockKind.WithInitializer;
  81. initInst = ci.Arguments.Single();
  82. break;
  83. default:
  84. var typeDef = v.Type.GetDefinition();
  85. if (context.Settings.WithExpressions && typeDef?.IsReferenceType == false && typeDef.IsRecord)
  86. {
  87. instType = v.Type;
  88. blockKind = BlockKind.WithInitializer;
  89. break;
  90. }
  91. return;
  92. }
  93. int initializerItemsCount = 0;
  94. bool initializerContainsInitOnlyItems = false;
  95. possibleIndexVariables.Clear();
  96. currentPath.Clear();
  97. isCollection = false;
  98. pathStack.Clear();
  99. pathStack.Push(new HashSet<AccessPathElement>());
  100. // Detect initializer type by scanning the following statements
  101. // each must be a callvirt with ldloc v as first argument
  102. // if the method is a setter we're dealing with an object initializer
  103. // if the method is named Add and has at least 2 arguments we're dealing with a collection/dictionary initializer
  104. while (pos + initializerItemsCount + 1 < block.Instructions.Count
  105. && IsPartOfInitializer(block.Instructions, pos + initializerItemsCount + 1, v, instType, ref blockKind, ref initializerContainsInitOnlyItems, context))
  106. {
  107. initializerItemsCount++;
  108. }
  109. // Do not convert the statements into an initializer if there's an incompatible usage of the initializer variable
  110. // directly after the possible initializer.
  111. if (!initializerContainsInitOnlyItems && IsMethodCallOnVariable(block.Instructions[pos + initializerItemsCount + 1], v))
  112. return;
  113. // Calculate the correct number of statements inside the initializer:
  114. // All index variables that were used in the initializer have Index set to -1.
  115. // We fetch the first unused variable from the list and remove all instructions after its
  116. // first usage (i.e. the init store) from the initializer.
  117. var index = possibleIndexVariables.Where(info => info.Value.Index > -1).Min(info => (int?)info.Value.Index);
  118. if (index != null)
  119. {
  120. initializerItemsCount = index.Value - pos - 1;
  121. }
  122. // The initializer would be empty, there's nothing to do here.
  123. if (initializerItemsCount <= 0)
  124. return;
  125. context.Step("CollectionOrObjectInitializer", inst);
  126. // Create a new block and final slot (initializer target variable)
  127. var initializerBlock = new Block(blockKind);
  128. ILVariable finalSlot = context.Function.RegisterVariable(VariableKind.InitializerTarget, instType);
  129. initializerBlock.FinalInstruction = new LdLoc(finalSlot);
  130. initializerBlock.Instructions.Add(new StLoc(finalSlot, initInst));
  131. // Move all instructions to the initializer block.
  132. for (int i = 1; i <= initializerItemsCount; i++)
  133. {
  134. switch (block.Instructions[i + pos])
  135. {
  136. case CallInstruction call:
  137. if (!(call is CallVirt || call is Call))
  138. continue;
  139. var newCall = call;
  140. var newTarget = newCall.Arguments[0];
  141. foreach (var load in newTarget.Descendants.OfType<IInstructionWithVariableOperand>())
  142. if ((load is LdLoc || load is LdLoca) && load.Variable == v)
  143. load.Variable = finalSlot;
  144. initializerBlock.Instructions.Add(newCall);
  145. break;
  146. case StObj stObj:
  147. var newStObj = stObj;
  148. foreach (var load in newStObj.Target.Descendants.OfType<IInstructionWithVariableOperand>())
  149. if ((load is LdLoc || load is LdLoca) && load.Variable == v)
  150. load.Variable = finalSlot;
  151. initializerBlock.Instructions.Add(newStObj);
  152. break;
  153. case StLoc stLoc:
  154. var newStLoc = stLoc;
  155. initializerBlock.Instructions.Add(newStLoc);
  156. break;
  157. }
  158. }
  159. block.Instructions.RemoveRange(pos + 1, initializerItemsCount);
  160. siblings[insertionPos] = initializerBlock;
  161. ILInlining.InlineIfPossible(block, pos, context);
  162. }
  163. internal static bool IsRecordCloneMethodCall(CallInstruction ci)
  164. {
  165. if (ci.Method.DeclaringTypeDefinition?.IsRecord != true)
  166. return false;
  167. if (ci.Method.Name != "<Clone>$")
  168. return false;
  169. if (ci.Arguments.Count != 1)
  170. return false;
  171. return true;
  172. }
  173. bool IsMethodCallOnVariable(ILInstruction inst, ILVariable variable)
  174. {
  175. if (inst.MatchLdLocRef(variable))
  176. return true;
  177. if (inst is CallInstruction call && call.Arguments.Count > 0 && !call.Method.IsStatic)
  178. return IsMethodCallOnVariable(call.Arguments[0], variable);
  179. if (inst.MatchLdFld(out var target, out _) || inst.MatchStFld(out target, out _, out _) || inst.MatchLdFlda(out target, out _))
  180. return IsMethodCallOnVariable(target, variable);
  181. return false;
  182. }
  183. readonly Dictionary<ILVariable, (int Index, ILInstruction Value)> possibleIndexVariables = new Dictionary<ILVariable, (int Index, ILInstruction Value)>();
  184. readonly List<AccessPathElement> currentPath = new List<AccessPathElement>();
  185. bool isCollection;
  186. readonly Stack<HashSet<AccessPathElement>> pathStack = new Stack<HashSet<AccessPathElement>>();
  187. bool IsPartOfInitializer(InstructionCollection<ILInstruction> instructions, int pos, ILVariable target, IType rootType, ref BlockKind blockKind, ref bool initializerContainsInitOnlyItems, StatementTransformContext context)
  188. {
  189. // Include any stores to local variables that are single-assigned and do not reference the initializer-variable
  190. // in the list of possible index variables.
  191. // Index variables are used to implement dictionary initializers.
  192. if (instructions[pos] is StLoc stloc && stloc.Variable.Kind == VariableKind.Local && stloc.Variable.IsSingleDefinition)
  193. {
  194. if (!context.Settings.DictionaryInitializers)
  195. return false;
  196. if (stloc.Value.Descendants.OfType<IInstructionWithVariableOperand>().Any(ld => ld.Variable == target && (ld is LdLoc || ld is LdLoca)))
  197. return false;
  198. possibleIndexVariables.Add(stloc.Variable, (stloc.ChildIndex, stloc.Value));
  199. return true;
  200. }
  201. var resolveContext = new CSharpTypeResolveContext(context.TypeSystem.MainModule, context.UsingScope);
  202. (var kind, var newPath, var values, var targetVariable) = AccessPathElement.GetAccessPath(instructions[pos], rootType, context.Settings, resolveContext, possibleIndexVariables);
  203. if (kind == AccessPathKind.Invalid || target != targetVariable)
  204. return false;
  205. // Treat last element separately:
  206. // Can either be an Add method call or property setter.
  207. var lastElement = newPath.Last();
  208. newPath.RemoveLast();
  209. // Compare new path with current path:
  210. int minLen = Math.Min(currentPath.Count, newPath.Count);
  211. int firstDifferenceIndex = 0;
  212. while (firstDifferenceIndex < minLen && newPath[firstDifferenceIndex] == currentPath[firstDifferenceIndex])
  213. firstDifferenceIndex++;
  214. while (currentPath.Count > firstDifferenceIndex)
  215. {
  216. isCollection = false;
  217. currentPath.RemoveAt(currentPath.Count - 1);
  218. pathStack.Pop();
  219. }
  220. while (currentPath.Count < newPath.Count)
  221. {
  222. AccessPathElement newElement = newPath[currentPath.Count];
  223. currentPath.Add(newElement);
  224. if (isCollection || !pathStack.Peek().Add(newElement))
  225. return false;
  226. pathStack.Push(new HashSet<AccessPathElement>());
  227. }
  228. switch (kind)
  229. {
  230. case AccessPathKind.Adder:
  231. isCollection = true;
  232. if (pathStack.Peek().Count != 0)
  233. return false;
  234. return true;
  235. case AccessPathKind.Setter:
  236. if (isCollection || !pathStack.Peek().Add(lastElement))
  237. return false;
  238. if (values?.Count != 1 || !IsValidObjectInitializerTarget(currentPath))
  239. return false;
  240. if (blockKind != BlockKind.ObjectInitializer && blockKind != BlockKind.WithInitializer)
  241. blockKind = BlockKind.ObjectInitializer;
  242. initializerContainsInitOnlyItems |= lastElement.Member is IProperty { Setter.IsInitOnly: true };
  243. return true;
  244. default:
  245. return false;
  246. }
  247. }
  248. bool IsValidObjectInitializerTarget(List<AccessPathElement> path)
  249. {
  250. if (path.Count == 0)
  251. return true;
  252. var element = path.Last();
  253. var previous = path.SkipLast(1).LastOrDefault();
  254. if (element.Member is not IProperty p)
  255. return true;
  256. if (!p.IsIndexer)
  257. return true;
  258. if (previous != default)
  259. {
  260. return NormalizeTypeVisitor.IgnoreNullabilityAndTuples
  261. .EquivalentTypes(previous.Member.ReturnType, element.Member.DeclaringType);
  262. }
  263. return false;
  264. }
  265. }
  266. public enum AccessPathKind
  267. {
  268. Invalid,
  269. Setter,
  270. Adder
  271. }
  272. public struct AccessPathElement : IEquatable<AccessPathElement>
  273. {
  274. public AccessPathElement(OpCode opCode, IMember member, ILInstruction[]? indices = null)
  275. {
  276. this.OpCode = opCode;
  277. this.Member = member;
  278. this.Indices = indices;
  279. }
  280. public readonly OpCode OpCode;
  281. public readonly IMember Member;
  282. public readonly ILInstruction[]? Indices;
  283. public override string ToString() => $"[{Member}, {Indices}]";
  284. public static (AccessPathKind Kind, List<AccessPathElement> Path, List<ILInstruction>? Values, ILVariable? Target) GetAccessPath(
  285. ILInstruction instruction, IType rootType, DecompilerSettings? settings = null,
  286. CSharpTypeResolveContext? resolveContext = null,
  287. Dictionary<ILVariable, (int Index, ILInstruction Value)>? possibleIndexVariables = null)
  288. {
  289. List<AccessPathElement> path = new List<AccessPathElement>();
  290. ILVariable? target = null;
  291. AccessPathKind kind = AccessPathKind.Invalid;
  292. List<ILInstruction>? values = null;
  293. IMethod method;
  294. ILInstruction? inst = instruction;
  295. while (inst != null)
  296. {
  297. switch (inst)
  298. {
  299. case CallInstruction call:
  300. if (!(call is CallVirt || call is Call))
  301. goto default;
  302. method = call.Method;
  303. if (resolveContext != null && !IsMethodApplicable(method, call.Arguments, rootType, resolveContext, settings))
  304. goto default;
  305. inst = call.Arguments[0];
  306. if (method.IsAccessor)
  307. {
  308. if (method.AccessorOwner is IProperty property &&
  309. !CanBeUsedInInitializer(property, resolveContext, kind))
  310. {
  311. goto default;
  312. }
  313. var isGetter = method.AccessorKind == System.Reflection.MethodSemanticsAttributes.Getter;
  314. var indices = call.Arguments.Skip(1).Take(call.Arguments.Count - (isGetter ? 1 : 2)).ToArray();
  315. if (indices.Length > 0 && settings?.DictionaryInitializers == false)
  316. goto default;
  317. if (possibleIndexVariables != null)
  318. {
  319. // Mark all index variables as used
  320. foreach (var index in indices.OfType<IInstructionWithVariableOperand>())
  321. {
  322. if (possibleIndexVariables.TryGetValue(index.Variable, out var info))
  323. possibleIndexVariables[index.Variable] = (-1, info.Value);
  324. }
  325. }
  326. path.Insert(0, new AccessPathElement(call.OpCode, method.AccessorOwner, indices));
  327. }
  328. else
  329. {
  330. path.Insert(0, new AccessPathElement(call.OpCode, method));
  331. }
  332. if (values == null)
  333. {
  334. if (method.IsAccessor)
  335. {
  336. kind = AccessPathKind.Setter;
  337. values = new List<ILInstruction> { call.Arguments.Last() };
  338. }
  339. else
  340. {
  341. kind = AccessPathKind.Adder;
  342. values = new List<ILInstruction>(call.Arguments.Skip(1));
  343. if (values.Count == 0)
  344. goto default;
  345. }
  346. }
  347. break;
  348. case LdObj ldobj:
  349. {
  350. if (ldobj.Target is LdFlda ldflda && (kind != AccessPathKind.Setter || !ldflda.Field.IsReadOnly))
  351. {
  352. path.Insert(0, new AccessPathElement(ldobj.OpCode, ldflda.Field));
  353. inst = ldflda.Target;
  354. break;
  355. }
  356. goto default;
  357. }
  358. case StObj stobj:
  359. {
  360. if (stobj.Target is LdFlda ldflda)
  361. {
  362. path.Insert(0, new AccessPathElement(stobj.OpCode, ldflda.Field));
  363. inst = ldflda.Target;
  364. if (values == null)
  365. {
  366. values = new List<ILInstruction>(new[] { stobj.Value });
  367. kind = AccessPathKind.Setter;
  368. }
  369. break;
  370. }
  371. goto default;
  372. }
  373. case LdLoc ldloc:
  374. target = ldloc.Variable;
  375. inst = null;
  376. break;
  377. case LdLoca ldloca:
  378. target = ldloca.Variable;
  379. inst = null;
  380. break;
  381. case LdFlda ldflda:
  382. path.Insert(0, new AccessPathElement(ldflda.OpCode, ldflda.Field));
  383. inst = ldflda.Target;
  384. break;
  385. default:
  386. kind = AccessPathKind.Invalid;
  387. inst = null;
  388. break;
  389. }
  390. }
  391. if (kind != AccessPathKind.Invalid && values != null && values.SelectMany(v => v.Descendants).OfType<IInstructionWithVariableOperand>().Any(ld => ld.Variable == target && (ld is LdLoc || ld is LdLoca)))
  392. kind = AccessPathKind.Invalid;
  393. return (kind, path, values, target);
  394. }
  395. private static bool CanBeUsedInInitializer(IProperty property, CSharpTypeResolveContext? resolveContext, AccessPathKind kind)
  396. {
  397. if (property.CanSet && (property.Accessibility == property.Setter.Accessibility || IsAccessorAccessible(property.Setter, resolveContext)))
  398. return true;
  399. return kind != AccessPathKind.Setter;
  400. }
  401. private static bool IsAccessorAccessible(IMethod setter, CSharpTypeResolveContext? resolveContext)
  402. {
  403. if (resolveContext == null)
  404. return true;
  405. var lookup = new MemberLookup(resolveContext.CurrentTypeDefinition, resolveContext.CurrentModule);
  406. return lookup.IsAccessible(setter, allowProtectedAccess: setter.DeclaringTypeDefinition == resolveContext.CurrentTypeDefinition);
  407. }
  408. static bool IsMethodApplicable(IMethod method, IReadOnlyList<ILInstruction> arguments, IType rootType, CSharpTypeResolveContext resolveContext, DecompilerSettings? settings)
  409. {
  410. if (method.IsStatic && !method.IsExtensionMethod)
  411. return false;
  412. if (method.AccessorOwner is IProperty)
  413. return true;
  414. if (!"Add".Equals(method.Name, StringComparison.Ordinal) || arguments.Count == 0)
  415. return false;
  416. if (method.IsExtensionMethod)
  417. {
  418. if (settings?.ExtensionMethodsInCollectionInitializers == false)
  419. return false;
  420. if (!CSharp.Transforms.IntroduceExtensionMethods.CanTransformToExtensionMethodCall(method, resolveContext, ignoreTypeArguments: true))
  421. return false;
  422. }
  423. var targetType = GetReturnTypeFromInstruction(arguments[0]) ?? rootType;
  424. if (targetType == null)
  425. return false;
  426. if (!targetType.GetAllBaseTypes().Any(i => i.IsKnownType(KnownTypeCode.IEnumerable) || i.IsKnownType(KnownTypeCode.IEnumerableOfT)))
  427. return false;
  428. return CanInferTypeArgumentsFromParameters(method);
  429. bool CanInferTypeArgumentsFromParameters(IMethod method)
  430. {
  431. if (method.TypeParameters.Count == 0)
  432. return true;
  433. // always use unspecialized member, otherwise type inference fails
  434. method = (IMethod)method.MemberDefinition;
  435. new TypeInference(resolveContext.Compilation)
  436. .InferTypeArguments(
  437. method.TypeParameters,
  438. // TODO : this is not entirely correct... we need argument type information to resolve Add methods properly
  439. method.Parameters.SelectReadOnlyArray(p => new ResolveResult(p.Type)),
  440. method.Parameters.SelectReadOnlyArray(p => p.Type),
  441. out bool success
  442. );
  443. return success;
  444. }
  445. }
  446. static IType? GetReturnTypeFromInstruction(ILInstruction instruction)
  447. {
  448. switch (instruction)
  449. {
  450. case CallInstruction call:
  451. if (!(call is CallVirt || call is Call))
  452. goto default;
  453. return call.Method.ReturnType;
  454. case LdObj ldobj:
  455. if (ldobj.Target is LdFlda ldflda)
  456. return ldflda.Field.ReturnType;
  457. goto default;
  458. case StObj stobj:
  459. if (stobj.Target is LdFlda ldflda2)
  460. return ldflda2.Field.ReturnType;
  461. goto default;
  462. default:
  463. return null;
  464. }
  465. }
  466. public override bool Equals(object? obj)
  467. {
  468. if (obj is AccessPathElement)
  469. return Equals((AccessPathElement)obj);
  470. return false;
  471. }
  472. public override int GetHashCode()
  473. {
  474. int hashCode = 0;
  475. unchecked
  476. {
  477. if (Member != null)
  478. hashCode += 1000000007 * Member.GetHashCode();
  479. }
  480. return hashCode;
  481. }
  482. public bool Equals(AccessPathElement other)
  483. {
  484. return (other.Member == this.Member
  485. || this.Member.Equals(other.Member))
  486. && (other.Indices == this.Indices
  487. || (other.Indices != null && this.Indices != null && this.Indices.SequenceEqual(other.Indices, ILInstructionMatchComparer.Instance)));
  488. }
  489. public static bool operator ==(AccessPathElement lhs, AccessPathElement rhs)
  490. {
  491. return lhs.Equals(rhs);
  492. }
  493. public static bool operator !=(AccessPathElement lhs, AccessPathElement rhs)
  494. {
  495. return !(lhs == rhs);
  496. }
  497. }
  498. class ILInstructionMatchComparer : IEqualityComparer<ILInstruction>
  499. {
  500. public static readonly ILInstructionMatchComparer Instance = new ILInstructionMatchComparer();
  501. public bool Equals(ILInstruction? x, ILInstruction? y)
  502. {
  503. if (x == y)
  504. return true;
  505. if (x == null || y == null)
  506. return false;
  507. return SemanticHelper.IsPure(x.Flags)
  508. && SemanticHelper.IsPure(y.Flags)
  509. && x.Match(y).Success;
  510. }
  511. public int GetHashCode(ILInstruction obj)
  512. {
  513. throw new NotSupportedException();
  514. }
  515. }
  516. }