diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs index 5f98e53ab..db7b7ff30 100644 --- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs @@ -675,7 +675,7 @@ namespace ICSharpCode.Decompiler.Tests [Test] public async Task YieldReturn([ValueSource(nameof(defaultOptionsWithMcs))] CompilerOptions cscOptions) { - await RunForLibrary(cscOptions: cscOptions); + await RunForLibrary(cscOptions: cscOptions | CompilerOptions.Preview); } [Test] diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/YieldReturn.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/YieldReturn.cs index d97228ff7..81b528e9b 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/YieldReturn.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/YieldReturn.cs @@ -428,6 +428,13 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty Console.WriteLine("normal exit"); } +#if CS110 + public IEnumerable YieldBangBang(object x!!) + { + yield return x; + } +#endif + internal IEnumerable ForLoopWithYieldReturn(int end, int evil) { // This loop needs to pick the implicit "yield break;" as exit point diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs index e8f6767aa..c8c9d60ff 100644 --- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs @@ -91,6 +91,7 @@ namespace ICSharpCode.Decompiler.CSharp new InlineReturnTransform(), // must run before DetectPinnedRegions new RemoveInfeasiblePathTransform(), new DetectPinnedRegions(), // must run after inlining but before non-critical control flow transforms + new ParameterNullCheckTransform(), // must run after inlining but before yield/async new YieldReturnDecompiler(), // must run after inlining but before loop detection new AsyncAwaitDecompiler(), // must run after inlining but before loop detection new DetectCatchWhenConditionBlocks(), // must run after inlining but before loop detection @@ -1687,6 +1688,14 @@ namespace ICSharpCode.Decompiler.CSharp RemoveAttribute(entityDecl, KnownAttribute.AsyncStateMachine); RemoveAttribute(entityDecl, KnownAttribute.DebuggerStepThrough); } + foreach (var parameter in entityDecl.GetChildrenByRole(Roles.Parameter)) + { + var variable = parameter.Annotation()?.Variable; + if (variable != null && variable.HasNullCheck) + { + parameter.HasNullCheck = true; + } + } } internal static bool RemoveAttribute(EntityDeclaration entityDecl, KnownAttribute attributeType) diff --git a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs index 1e0deaff1..74e5506c4 100644 --- a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs +++ b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs @@ -2574,6 +2574,10 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor { WriteIdentifier(parameterDeclaration.NameToken); } + if (parameterDeclaration.HasNullCheck) + { + WriteToken(Roles.DoubleExclamation); + } if (!parameterDeclaration.DefaultExpression.IsNull) { Space(policy.SpaceAroundAssignment); diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/Roles.cs b/ICSharpCode.Decompiler/CSharp/Syntax/Roles.cs index f55c63542..076cf9411 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/Roles.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/Roles.cs @@ -68,6 +68,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax public static readonly TokenRole Colon = new TokenRole(":"); public static readonly TokenRole DoubleColon = new TokenRole("::"); public static readonly TokenRole Arrow = new TokenRole("=>"); + public static readonly TokenRole DoubleExclamation = new TokenRole("!!"); public static readonly Role Comment = new Role("Comment", null); public static readonly Role PreProcessorDirective = new Role("PreProcessorDirective", null); diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/TypeMembers/ParameterDeclaration.cs b/ICSharpCode.Decompiler/CSharp/Syntax/TypeMembers/ParameterDeclaration.cs index e2c4e9ffc..98ae6bd75 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/TypeMembers/ParameterDeclaration.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/TypeMembers/ParameterDeclaration.cs @@ -24,6 +24,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. +#nullable enable namespace ICSharpCode.Decompiler.CSharp.Syntax { @@ -46,7 +47,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax public static readonly TokenRole InModifierRole = new TokenRole("in"); #region PatternPlaceholder - public static implicit operator ParameterDeclaration(PatternMatching.Pattern pattern) + public static implicit operator ParameterDeclaration?(PatternMatching.Pattern pattern) { return pattern != null ? new PatternPlaceholder(pattern) : null; } @@ -79,7 +80,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax return visitor.VisitPatternPlaceholder(this, child, data); } - protected internal override bool DoMatch(AstNode other, PatternMatching.Match match) + protected internal override bool DoMatch(AstNode? other, PatternMatching.Match match) { return child.DoMatch(other, match); } @@ -154,6 +155,26 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax } } + bool hasNullCheck; + + public CSharpTokenNode DoubleExclamationToken { + get { + if (hasNullCheck) + { + return GetChildByRole(Roles.DoubleExclamation); + } + return CSharpTokenNode.Null; + } + } + + public bool HasNullCheck { + get { return hasNullCheck; } + set { + ThrowIfFrozen(); + hasNullCheck = value; + } + } + public CSharpTokenNode AssignToken { get { return GetChildByRole(Roles.Assign); } } @@ -178,11 +199,12 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax return visitor.VisitParameterDeclaration(this, data); } - protected internal override bool DoMatch(AstNode other, PatternMatching.Match match) + protected internal override bool DoMatch(AstNode? other, PatternMatching.Match match) { - ParameterDeclaration o = other as ParameterDeclaration; + var o = other as ParameterDeclaration; return o != null && this.Attributes.DoMatch(o.Attributes, match) && this.ParameterModifier == o.ParameterModifier && this.Type.DoMatch(o.Type, match) && MatchString(this.Name, o.Name) + && this.HasNullCheck == o.HasNullCheck && this.DefaultExpression.DoMatch(o.DefaultExpression, match); } diff --git a/ICSharpCode.Decompiler/DecompilerSettings.cs b/ICSharpCode.Decompiler/DecompilerSettings.cs index b7888ad30..144837f8e 100644 --- a/ICSharpCode.Decompiler/DecompilerSettings.cs +++ b/ICSharpCode.Decompiler/DecompilerSettings.cs @@ -146,10 +146,16 @@ namespace ICSharpCode.Decompiler { fileScopedNamespaces = false; } + if (languageVersion < CSharp.LanguageVersion.CSharp11_0) + { + parameterNullCheck = false; + } } public CSharp.LanguageVersion GetMinimumRequiredVersion() { + if (parameterNullCheck) + return CSharp.LanguageVersion.CSharp11_0; if (fileScopedNamespaces) return CSharp.LanguageVersion.CSharp10_0; if (nativeIntegers || initAccessors || functionPointers || forEachWithGetEnumeratorExtension @@ -347,6 +353,24 @@ namespace ICSharpCode.Decompiler } } + bool parameterNullCheck = true; + + /// + /// Use C# 11 parameter null-checking. + /// + [Category("C# 11.0 / VS 2022.1")] + [Description("DecompilerSettings.ParameterNullCheck")] + public bool ParameterNullCheck { + get { return parameterNullCheck; } + set { + if (parameterNullCheck != value) + { + parameterNullCheck = value; + OnPropertyChanged(); + } + } + } + bool anonymousMethods = true; /// diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index 7f96f40d2..2f1d823bc 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -117,6 +117,7 @@ + diff --git a/ICSharpCode.Decompiler/IL/ILVariable.cs b/ICSharpCode.Decompiler/IL/ILVariable.cs index f014a3b68..bbe6dfaa6 100644 --- a/ICSharpCode.Decompiler/IL/ILVariable.cs +++ b/ICSharpCode.Decompiler/IL/ILVariable.cs @@ -438,6 +438,21 @@ namespace ICSharpCode.Decompiler.IL /// internal bool RemoveIfRedundant; + private bool hasNullCheck; + + /// + /// Gets/sets whether a parameter has an auto-generated null check, i.e., the !! modifier. + /// Returns false for all variables except parameters. + /// + public bool HasNullCheck { + get => hasNullCheck; + set { + if (Kind != VariableKind.Parameter && value) + throw new InvalidOperationException("Cannot set HasNullCheck on local variables!"); + hasNullCheck = value; + } + } + public ILVariable(VariableKind kind, IType type, int? index = null) { if (type == null) diff --git a/ICSharpCode.Decompiler/IL/Transforms/ParameterNullCheckTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/ParameterNullCheckTransform.cs new file mode 100644 index 000000000..8e0b26fb9 --- /dev/null +++ b/ICSharpCode.Decompiler/IL/Transforms/ParameterNullCheckTransform.cs @@ -0,0 +1,88 @@ +// Copyright (c) 2017 Siegfried Pammer +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +#nullable enable + +using System.Diagnostics.CodeAnalysis; + +using ICSharpCode.Decompiler.TypeSystem; + +namespace ICSharpCode.Decompiler.IL.Transforms +{ + /// + /// Implements transforming <PrivateImplementationDetails>.ThrowIfNull(name, "name"); + /// + class ParameterNullCheckTransform : IILTransform + { + void IILTransform.Run(ILFunction function, ILTransformContext context) + { + if (!context.Settings.ParameterNullCheck) + return; + // we only need to look at the entry-point as parameter null-checks + // do not produce any IL control-flow instructions + Block entryPoint = ((BlockContainer)function.Body).EntryPoint; + int index = 0; + // Early versions of this pattern produced call ThrowIfNull instructions after + // state-machine initialization instead of right at the start of the method. + // In order to support both patterns, we scan all instructions, + // if the current function is decorated with a state-machine attribute. + bool scanFullBlock = function.Method != null + && (function.Method.HasAttribute(KnownAttribute.IteratorStateMachine) + || function.Method.HasAttribute(KnownAttribute.AsyncIteratorStateMachine) + || function.Method.HasAttribute(KnownAttribute.AsyncStateMachine)); + // loop over all instructions + while (index < entryPoint.Instructions.Count) + { + // The pattern does not match for the current instruction + if (!MatchThrowIfNullCall(entryPoint.Instructions[index], out ILVariable? parameterVariable)) + { + if (scanFullBlock) + { + // continue scanning + index++; + continue; + } + else + { + // abort + break; + } + } + // remove the call to ThrowIfNull + entryPoint.Instructions.RemoveAt(index); + // remember to generate !! when producing the final output. + parameterVariable.HasNullCheck = true; + } + } + + // call .ThrowIfNull(ldloc parameterVariable, ldstr "parameterVariable") + private bool MatchThrowIfNullCall(ILInstruction instruction, [NotNullWhen(true)] out ILVariable? parameterVariable) + { + parameterVariable = null; + if (instruction is not Call call) + return false; + if (call.Arguments.Count != 2) + return false; + if (!call.Method.IsStatic || !call.Method.FullNameIs("", "ThrowIfNull")) + return false; + if (!(call.Arguments[0].MatchLdLoc(out parameterVariable) && parameterVariable.Kind == VariableKind.Parameter)) + return false; + return true; + } + } +}