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.

542 lines
19 KiB

  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. using System.Linq;
  19. using ICSharpCode.Decompiler.TypeSystem;
  20. namespace ICSharpCode.Decompiler.IL.Transforms
  21. {
  22. public class UsingTransform : IBlockTransform
  23. {
  24. BlockTransformContext context;
  25. void IBlockTransform.Run(Block block, BlockTransformContext context)
  26. {
  27. if (!context.Settings.UsingStatement)
  28. return;
  29. this.context = context;
  30. for (int i = context.IndexOfFirstAlreadyTransformedInstruction - 1; i >= 0; i--)
  31. {
  32. if (TransformUsing(block, i))
  33. {
  34. context.IndexOfFirstAlreadyTransformedInstruction = block.Instructions.Count;
  35. continue;
  36. }
  37. if (TransformUsingVB(block, i))
  38. {
  39. context.IndexOfFirstAlreadyTransformedInstruction = block.Instructions.Count;
  40. continue;
  41. }
  42. if (TransformAsyncUsing(block, i))
  43. {
  44. context.IndexOfFirstAlreadyTransformedInstruction = block.Instructions.Count;
  45. continue;
  46. }
  47. }
  48. }
  49. /// <summary>
  50. /// stloc obj(resourceExpression)
  51. /// .try BlockContainer {
  52. /// Block IL_0003(incoming: 1) {
  53. /// call WriteLine(ldstr "using (null)")
  54. /// leave IL_0003(nop)
  55. /// }
  56. /// } finally BlockContainer {
  57. /// Block IL_0012(incoming: 1) {
  58. /// if (comp(ldloc obj != ldnull)) Block IL_001a {
  59. /// callvirt Dispose(ldnull)
  60. /// }
  61. /// leave IL_0012(nop)
  62. /// }
  63. /// }
  64. /// leave IL_0000(nop)
  65. /// =>
  66. /// using (resourceExpression) {
  67. /// BlockContainer {
  68. /// Block IL_0003(incoming: 1) {
  69. /// call WriteLine(ldstr "using (null)")
  70. /// leave IL_0003(nop)
  71. /// }
  72. /// }
  73. /// }
  74. /// </summary>
  75. bool TransformUsing(Block block, int i)
  76. {
  77. if (i + 1 >= block.Instructions.Count)
  78. return false;
  79. if (!(block.Instructions[i + 1] is TryFinally tryFinally) || !(block.Instructions[i] is StLoc storeInst))
  80. return false;
  81. if (!(storeInst.Value.MatchLdNull() || CheckResourceType(storeInst.Variable.Type)))
  82. return false;
  83. if (storeInst.Variable.Kind != VariableKind.Local)
  84. return false;
  85. if (storeInst.Variable.LoadInstructions.Any(ld => !ld.IsDescendantOf(tryFinally)))
  86. return false;
  87. if (!storeInst.Variable.AddressInstructions.All(ValidateAddressUse))
  88. return false;
  89. if (storeInst.Variable.StoreInstructions.Count > 1)
  90. return false;
  91. if (!(tryFinally.FinallyBlock is BlockContainer container))
  92. return false;
  93. if (!MatchDisposeBlock(container, storeInst.Variable, storeInst.Value.MatchLdNull()))
  94. return false;
  95. context.Step("UsingTransform", tryFinally);
  96. storeInst.Variable.Kind = VariableKind.UsingLocal;
  97. block.Instructions.RemoveAt(i + 1);
  98. block.Instructions[i] = new UsingInstruction(storeInst.Variable, storeInst.Value, tryFinally.TryBlock) {
  99. IsRefStruct = context.Settings.IntroduceRefModifiersOnStructs && storeInst.Variable.Type.Kind == TypeKind.Struct && storeInst.Variable.Type.IsByRefLike
  100. }.WithILRange(storeInst);
  101. return true;
  102. bool ValidateAddressUse(LdLoca la)
  103. {
  104. if (!la.IsDescendantOf(tryFinally))
  105. return false;
  106. if (la.IsDescendantOf(tryFinally.TryBlock))
  107. {
  108. if (!(ILInlining.IsUsedAsThisPointerInCall(la) || ILInlining.IsPassedToInParameter(la)))
  109. return false;
  110. }
  111. return true;
  112. }
  113. }
  114. /// <summary>
  115. /// .try BlockContainer {
  116. /// Block IL_0003(incoming: 1) {
  117. /// stloc obj(resourceExpression)
  118. /// call WriteLine(ldstr "using (null)")
  119. /// leave IL_0003(nop)
  120. /// }
  121. /// } finally BlockContainer {
  122. /// Block IL_0012(incoming: 1) {
  123. /// if (comp(ldloc obj != ldnull)) Block IL_001a {
  124. /// callvirt Dispose(ldnull)
  125. /// }
  126. /// leave IL_0012(nop)
  127. /// }
  128. /// }
  129. /// leave IL_0000(nop)
  130. /// =>
  131. /// using (resourceExpression) {
  132. /// BlockContainer {
  133. /// Block IL_0003(incoming: 1) {
  134. /// call WriteLine(ldstr "using (null)")
  135. /// leave IL_0003(nop)
  136. /// }
  137. /// }
  138. /// }
  139. /// </summary>
  140. bool TransformUsingVB(Block block, int i)
  141. {
  142. if (i >= block.Instructions.Count)
  143. return false;
  144. if (!(block.Instructions[i] is TryFinally tryFinally))
  145. return false;
  146. if (!(tryFinally.TryBlock is BlockContainer tryContainer && tryContainer.EntryPoint.Instructions.FirstOrDefault() is StLoc storeInst))
  147. return false;
  148. if (!(storeInst.Value.MatchLdNull() || CheckResourceType(storeInst.Variable.Type)))
  149. return false;
  150. if (storeInst.Variable.Kind != VariableKind.Local)
  151. return false;
  152. if (storeInst.Variable.LoadInstructions.Any(ld => !ld.IsDescendantOf(tryFinally)))
  153. return false;
  154. if (storeInst.Variable.AddressInstructions.Any(la => !la.IsDescendantOf(tryFinally) || (la.IsDescendantOf(tryFinally.TryBlock) && !ILInlining.IsUsedAsThisPointerInCall(la))))
  155. return false;
  156. if (storeInst.Variable.StoreInstructions.Count > 1)
  157. return false;
  158. if (!(tryFinally.FinallyBlock is BlockContainer container) || !MatchDisposeBlock(container, storeInst.Variable, storeInst.Value.MatchLdNull()))
  159. return false;
  160. context.Step("UsingTransformVB", tryFinally);
  161. storeInst.Variable.Kind = VariableKind.UsingLocal;
  162. tryContainer.EntryPoint.Instructions.RemoveAt(0);
  163. block.Instructions[i] = new UsingInstruction(storeInst.Variable, storeInst.Value, tryFinally.TryBlock);
  164. return true;
  165. }
  166. bool CheckResourceType(IType type)
  167. {
  168. // non-generic IEnumerator does not implement IDisposable.
  169. // This is a workaround for non-generic foreach.
  170. if (type.IsKnownType(KnownTypeCode.IEnumerator) || type.GetAllBaseTypes().Any(b => b.IsKnownType(KnownTypeCode.IEnumerator)))
  171. return true;
  172. if (NullableType.GetUnderlyingType(type).GetAllBaseTypes().Any(b => b.IsKnownType(KnownTypeCode.IDisposable)))
  173. return true;
  174. if (context.Settings.IntroduceRefModifiersOnStructs && type.Kind == TypeKind.Struct && type.IsByRefLike)
  175. return true;
  176. // General GetEnumerator-pattern?
  177. if (!type.GetMethods(m => m.Name == "GetEnumerator" && m.TypeParameters.Count == 0 && m.Parameters.Count == 0).Any(m => ImplementsForeachPattern(m.ReturnType)))
  178. return false;
  179. return true;
  180. }
  181. bool ImplementsForeachPattern(IType type)
  182. {
  183. if (!type.GetMethods(m => m.Name == "MoveNext" && m.TypeParameters.Count == 0 && m.Parameters.Count == 0).Any(m => m.ReturnType.IsKnownType(KnownTypeCode.Boolean)))
  184. return false;
  185. if (!type.GetProperties(p => p.Name == "Current" && p.CanGet && !p.IsIndexer).Any())
  186. return false;
  187. return true;
  188. }
  189. /// finally BlockContainer {
  190. /// Block IL_0012(incoming: 1) {
  191. /// if (comp(ldloc obj != ldnull)) Block IL_001a {
  192. /// callvirt Dispose(obj)
  193. /// }
  194. /// leave IL_0012(nop)
  195. /// }
  196. /// }
  197. bool MatchDisposeBlock(BlockContainer container, ILVariable objVar, bool usingNull,
  198. in string disposeMethodFullName = "System.IDisposable.Dispose",
  199. KnownTypeCode disposeTypeCode = KnownTypeCode.IDisposable)
  200. {
  201. var entryPoint = container.EntryPoint;
  202. if (entryPoint.IncomingEdgeCount != 1)
  203. return false;
  204. int pos = 0;
  205. bool isReference = objVar.Type.IsReferenceType != false;
  206. // optional:
  207. // stloc temp(isinst TDisposable(ldloc obj))
  208. if (entryPoint.Instructions.ElementAtOrDefault(pos).MatchStLoc(out var tempVar, out var isinst))
  209. {
  210. if (!isinst.MatchIsInst(out var load, out var disposableType) || !load.MatchLdLoc(objVar)
  211. || !disposableType.IsKnownType(disposeTypeCode))
  212. {
  213. return false;
  214. }
  215. if (!tempVar.IsSingleDefinition)
  216. return false;
  217. isReference = true;
  218. pos++;
  219. objVar = tempVar;
  220. }
  221. // if (comp(ldloc obj != ldnull)) Block IL_001a {
  222. // callvirt Dispose(obj)
  223. // }
  224. var checkInst = entryPoint.Instructions.ElementAtOrDefault(pos);
  225. if (checkInst == null)
  226. return false;
  227. if (!MatchDisposeCheck(objVar, checkInst, isReference, usingNull,
  228. out int numObjVarLoadsInCheck, disposeMethodFullName, disposeTypeCode))
  229. {
  230. return false;
  231. }
  232. // make sure the (optional) temporary is used only in the dispose check
  233. if (pos > 0 && objVar.LoadCount != numObjVarLoadsInCheck)
  234. return false;
  235. pos++;
  236. // make sure, the finally ends in a leave(nop) instruction.
  237. if (!entryPoint.Instructions.ElementAtOrDefault(pos).MatchLeave(container, out var returnValue))
  238. return false;
  239. if (!returnValue.MatchNop())
  240. return false;
  241. // leave is the last instruction
  242. return (pos + 1) == entryPoint.Instructions.Count;
  243. }
  244. bool MatchDisposeCheck(ILVariable objVar, ILInstruction checkInst, bool isReference, bool usingNull,
  245. out int numObjVarLoadsInCheck, string disposeMethodFullName, KnownTypeCode disposeTypeCode)
  246. {
  247. numObjVarLoadsInCheck = 2;
  248. ILInstruction disposeInvocation;
  249. CallInstruction disposeCall;
  250. if (objVar.Type.IsKnownType(KnownTypeCode.NullableOfT))
  251. {
  252. if (checkInst.MatchIfInstruction(out var condition, out var disposeInst))
  253. {
  254. if (!NullableLiftingTransform.MatchHasValueCall(condition, objVar))
  255. return false;
  256. if (!(disposeInst is Block disposeBlock) || disposeBlock.Instructions.Count != 1)
  257. return false;
  258. disposeInvocation = disposeBlock.Instructions[0];
  259. }
  260. else if (checkInst.MatchNullableRewrap(out disposeInst))
  261. {
  262. disposeInvocation = disposeInst;
  263. }
  264. else
  265. {
  266. return false;
  267. }
  268. if (disposeTypeCode == KnownTypeCode.IAsyncDisposable)
  269. {
  270. if (!UnwrapAwait(ref disposeInvocation))
  271. return false;
  272. }
  273. disposeCall = disposeInvocation as CallVirt;
  274. if (disposeCall == null)
  275. return false;
  276. if (disposeCall.Method.FullName != disposeMethodFullName)
  277. return false;
  278. if (disposeCall.Method.Parameters.Count > 0)
  279. return false;
  280. if (disposeCall.Arguments.Count != 1)
  281. return false;
  282. var firstArg = disposeCall.Arguments.FirstOrDefault();
  283. if (!(firstArg.MatchUnboxAny(out var innerArg1, out var unboxType) && unboxType.IsKnownType(disposeTypeCode)))
  284. {
  285. if (!firstArg.MatchAddressOf(out var innerArg2, out _))
  286. return false;
  287. return NullableLiftingTransform.MatchGetValueOrDefault(innerArg2, objVar)
  288. || (innerArg2 is NullableUnwrap unwrap
  289. && unwrap.Argument.MatchLdLoc(objVar));
  290. }
  291. else
  292. {
  293. if (!(innerArg1.MatchBox(out firstArg, out var boxType) && boxType.IsKnownType(KnownTypeCode.NullableOfT) &&
  294. NullableType.GetUnderlyingType(boxType).Equals(NullableType.GetUnderlyingType(objVar.Type))))
  295. return false;
  296. return firstArg.MatchLdLoc(objVar);
  297. }
  298. }
  299. else
  300. {
  301. ILInstruction target;
  302. bool boxedValue = false;
  303. if (isReference && checkInst is NullableRewrap rewrap)
  304. {
  305. // the null check of reference types might have been transformed into "objVar?.Dispose();"
  306. if (!(rewrap.Argument is CallVirt cv))
  307. return false;
  308. if (!(cv.Arguments.FirstOrDefault() is NullableUnwrap unwrap))
  309. return false;
  310. numObjVarLoadsInCheck = 1;
  311. disposeCall = cv;
  312. target = unwrap.Argument;
  313. }
  314. else if (isReference)
  315. {
  316. // reference types have a null check.
  317. if (!checkInst.MatchIfInstruction(out var condition, out var disposeInst))
  318. return false;
  319. if (!MatchNullCheckOrTypeCheck(condition, ref objVar, disposeTypeCode, out var isInlinedIsInst))
  320. return false;
  321. if (!(disposeInst is Block disposeBlock) || disposeBlock.Instructions.Count != 1)
  322. return false;
  323. disposeInvocation = disposeBlock.Instructions[0];
  324. if (disposeTypeCode == KnownTypeCode.IAsyncDisposable)
  325. {
  326. if (!UnwrapAwait(ref disposeInvocation))
  327. return false;
  328. }
  329. if (!(disposeInvocation is CallVirt cv))
  330. return false;
  331. target = cv.Arguments.FirstOrDefault();
  332. if (target == null)
  333. return false;
  334. if (target.MatchBox(out var newTarget, out var type) && type.Equals(objVar.Type))
  335. target = newTarget;
  336. else if (isInlinedIsInst && target.MatchIsInst(out newTarget, out type) && type.IsKnownType(disposeTypeCode))
  337. target = newTarget;
  338. disposeCall = cv;
  339. }
  340. else if (objVar.Type.Kind == TypeKind.Struct && objVar.Type.IsByRefLike)
  341. {
  342. if (!(checkInst is Call call && call.Method.DeclaringType == objVar.Type))
  343. return false;
  344. target = call.Arguments.FirstOrDefault();
  345. if (target == null)
  346. return false;
  347. if (call.Method.Name != "Dispose")
  348. return false;
  349. disposeMethodFullName = call.Method.FullName;
  350. disposeCall = call;
  351. }
  352. else
  353. {
  354. if (disposeTypeCode == KnownTypeCode.IAsyncDisposable)
  355. {
  356. if (!UnwrapAwait(ref checkInst))
  357. return false;
  358. }
  359. if (!(checkInst is CallInstruction cv))
  360. return false;
  361. target = cv.Arguments.FirstOrDefault();
  362. if (target == null)
  363. return false;
  364. if (target.MatchBox(out var newTarget, out var type) && type.Equals(objVar.Type))
  365. {
  366. boxedValue = type.IsReferenceType != true;
  367. target = newTarget;
  368. }
  369. disposeCall = cv;
  370. }
  371. if (disposeCall.Method.IsStatic)
  372. return false;
  373. if (disposeTypeCode == KnownTypeCode.IAsyncDisposable)
  374. {
  375. if (disposeCall.Method.Name != "DisposeAsync")
  376. return false;
  377. }
  378. else
  379. {
  380. if (disposeCall.Method.FullName != disposeMethodFullName)
  381. return false;
  382. }
  383. if (disposeCall.Method.Parameters.Count > 0)
  384. return false;
  385. if (disposeCall.Arguments.Count != 1)
  386. return false;
  387. return target.MatchLdLocRef(objVar)
  388. || (boxedValue && target.MatchLdLoc(objVar))
  389. || (usingNull && disposeCall.Arguments[0].MatchLdNull())
  390. || (isReference && checkInst is NullableRewrap
  391. && target.MatchIsInst(out var arg, out var type2)
  392. && arg.MatchLdLoc(objVar) && type2.IsKnownType(disposeTypeCode));
  393. }
  394. }
  395. bool MatchNullCheckOrTypeCheck(ILInstruction condition, ref ILVariable objVar, KnownTypeCode disposeType, out bool isInlinedIsInst)
  396. {
  397. isInlinedIsInst = false;
  398. if (condition.MatchCompNotEquals(out var left, out var right))
  399. {
  400. if (left.MatchStLoc(out var inlineAssignVar, out var inlineAssignVal))
  401. {
  402. if (!inlineAssignVal.MatchIsInst(out var arg, out var type) || !type.IsKnownType(disposeType))
  403. return false;
  404. if (!inlineAssignVar.IsSingleDefinition || inlineAssignVar.LoadCount != 1)
  405. return false;
  406. if (!inlineAssignVar.Type.IsKnownType(disposeType))
  407. return false;
  408. isInlinedIsInst = true;
  409. left = arg;
  410. if (!left.MatchLdLoc(objVar) || !right.MatchLdNull())
  411. return false;
  412. objVar = inlineAssignVar;
  413. return true;
  414. }
  415. else if (left.MatchIsInst(out var arg, out var type) && type.IsKnownType(disposeType))
  416. {
  417. isInlinedIsInst = true;
  418. left = arg;
  419. }
  420. if (!left.MatchLdLoc(objVar) || !right.MatchLdNull())
  421. return false;
  422. return true;
  423. }
  424. if (condition is MatchInstruction {
  425. CheckNotNull: true,
  426. CheckType: true,
  427. TestedOperand: LdLoc { Variable: var v },
  428. Variable: var newObjVar
  429. })
  430. {
  431. if (v != objVar)
  432. return false;
  433. if (!newObjVar.Type.IsKnownType(disposeType))
  434. return false;
  435. objVar = newObjVar;
  436. return true;
  437. }
  438. return false;
  439. }
  440. /// <summary>
  441. /// stloc test(resourceExpression)
  442. /// .try BlockContainer {
  443. /// Block IL_002b (incoming: 1) {
  444. /// call Use(ldloc test)
  445. /// leave IL_002b (nop)
  446. /// }
  447. ///
  448. /// } finally BlockContainer {
  449. /// Block IL_0045 (incoming: 1) {
  450. /// if (comp.o(ldloc test == ldnull)) leave IL_0045 (nop)
  451. /// br IL_00ae
  452. /// }
  453. ///
  454. /// Block IL_00ae (incoming: 1) {
  455. /// await(addressof System.Threading.Tasks.ValueTask(callvirt DisposeAsync(ldloc test)))
  456. /// leave IL_0045 (nop)
  457. /// }
  458. ///
  459. /// }
  460. /// </summary>
  461. private bool TransformAsyncUsing(Block block, int i)
  462. {
  463. if (!context.Settings.AsyncUsingAndForEachStatement)
  464. return false;
  465. if (i < 1 || i >= block.Instructions.Count)
  466. return false;
  467. if (!(block.Instructions[i] is TryFinally tryFinally) || !(block.Instructions[i - 1] is StLoc storeInst))
  468. return false;
  469. if (!CheckAsyncResourceType(storeInst.Variable.Type, out string disposeMethodFullName))
  470. return false;
  471. if (storeInst.Variable.Kind != VariableKind.Local)
  472. return false;
  473. if (storeInst.Variable.LoadInstructions.Any(ld => !ld.IsDescendantOf(tryFinally)))
  474. return false;
  475. if (storeInst.Variable.AddressInstructions.Any(la => !la.IsDescendantOf(tryFinally) || (la.IsDescendantOf(tryFinally.TryBlock) && !ILInlining.IsUsedAsThisPointerInCall(la))))
  476. return false;
  477. if (storeInst.Variable.StoreInstructions.Count > 1)
  478. return false;
  479. if (!(tryFinally.FinallyBlock is BlockContainer container) || !MatchDisposeBlock(container, storeInst.Variable, usingNull: false, disposeMethodFullName, KnownTypeCode.IAsyncDisposable))
  480. return false;
  481. context.Step("AsyncUsingTransform", tryFinally);
  482. storeInst.Variable.Kind = VariableKind.UsingLocal;
  483. block.Instructions.RemoveAt(i);
  484. block.Instructions[i - 1] = new UsingInstruction(storeInst.Variable, storeInst.Value, tryFinally.TryBlock) { IsAsync = true }
  485. .WithILRange(storeInst);
  486. return true;
  487. }
  488. bool CheckAsyncResourceType(IType type, out string disposeMethodFullName)
  489. {
  490. disposeMethodFullName = null;
  491. IType t = NullableType.GetUnderlyingType(type);
  492. if (t.GetAllBaseTypes().Any(b => b.IsKnownType(KnownTypeCode.IAsyncDisposable)))
  493. {
  494. disposeMethodFullName = "System.IAsyncDisposable.DisposeAsync";
  495. return true;
  496. }
  497. IMethod disposeMethod = t
  498. .GetMethods(m => m.Parameters.Count == 0 && m.TypeParameters.Count == 0 && m.Name == "DisposeAsync")
  499. .SingleOrDefault();
  500. if (disposeMethod != null)
  501. {
  502. disposeMethodFullName = disposeMethod.FullName;
  503. return true;
  504. }
  505. return false;
  506. }
  507. bool UnwrapAwait(ref ILInstruction awaitInstruction)
  508. {
  509. if (awaitInstruction == null)
  510. return false;
  511. if (!awaitInstruction.MatchAwait(out var arg))
  512. return false;
  513. if (arg.MatchAddressOf(out var awaitInstructionInAddressOf, out var type))
  514. {
  515. awaitInstruction = awaitInstructionInAddressOf;
  516. }
  517. else
  518. {
  519. awaitInstruction = arg;
  520. }
  521. return true;
  522. }
  523. }
  524. }