diff options
author | Jesse Jones <jesjones@mono-cvs.ximian.com> | 2008-12-31 02:41:02 +0300 |
---|---|---|
committer | Jesse Jones <jesjones@mono-cvs.ximian.com> | 2008-12-31 02:41:02 +0300 |
commit | 6a683c0a372962d02d17533ec967b7a9ea37e019 (patch) | |
tree | 7d45a441eea5086d57776feb3c7a6c108cc04d10 | |
parent | 79d15b228d7aecef374b5a8336869ad263e6c253 (diff) |
Added DoNotThrowInUnexpectedLocationRule.
svn path=/trunk/mono-tools/; revision=122277
5 files changed, 908 insertions, 0 deletions
diff --git a/gendarme/rules/Gendarme.Rules.Exceptions/ChangeLog b/gendarme/rules/Gendarme.Rules.Exceptions/ChangeLog index ce17bb45..56b7c4ce 100644 --- a/gendarme/rules/Gendarme.Rules.Exceptions/ChangeLog +++ b/gendarme/rules/Gendarme.Rules.Exceptions/ChangeLog @@ -1,3 +1,7 @@ +2008-12-30 Jesse Jones <jesjones@mindspring.com> + + * DoNotThrowInUnexpectedLocationRule.cs: Added. + 2008-12-30 Sebastien Pouliot <sebastien@ximian.com> * InstantiateArgumentExceptionCorrectlyRule.cs: Make sure the diff --git a/gendarme/rules/Gendarme.Rules.Exceptions/DoNotThrowInUnexpectedLocationRule.cs b/gendarme/rules/Gendarme.Rules.Exceptions/DoNotThrowInUnexpectedLocationRule.cs new file mode 100644 index 00000000..020e80e4 --- /dev/null +++ b/gendarme/rules/Gendarme.Rules.Exceptions/DoNotThrowInUnexpectedLocationRule.cs @@ -0,0 +1,498 @@ +// +// Gendarme.Rules.Exceptions.DoNotThrowInUnexpectedLocationRule +// +// Authors: +// Jesse Jones <jesjones@mindspring.com> +// +// Copyright (C) 2008 Jesse Jones +// +// 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. +// + +using Mono.Cecil; +using Mono.Cecil.Cil; + +using Gendarme.Framework; +using Gendarme.Framework.Engines; +using Gendarme.Framework.Helpers; +using Gendarme.Framework.Rocks; + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Gendarme.Rules.Exceptions { + + /// <summary> + /// There are a number of methods which have constraints on the exceptions + /// which they may throw. This rule checks the following methods: + /// <list type="bullet"> + /// <item> + /// <description>Property getters - properties should work very much + /// like fields: they should execute very quickly and, in general, should + /// not throw exceptions. However they may throw System.InvalidOperationException, + /// System.NotSupportedException, or an exception derived from these. + /// Indexed getters may also throw System.ArgumentException or + /// System.Collections.Generic.KeyNotFoundException.</description> + /// </item> + /// <item> + /// <description>Event accessors - in general events should not throw + /// when adding or removing a handler. However they may throw + /// System.InvalidOperationException, System.NotSupportedException, + /// System.ArgumentException, or an exception derived from these.</description> + /// </item> + /// <item> + /// <description>Object.Equals and IEqualityComparer<T>.Equals - should + /// not throw. In particular they should do something sensible when passed + /// null arguments or unexpected types.</description> + /// </item> + /// <item> + /// <description>Object.GetHashCode - should not throw or the object + /// will not work properly with dictionaries and hash sets.</description> + /// </item> + /// <item> + /// <description>IEqualityComparer<T>.GetHashCode - may throw + /// System.ArgumentException.</description> + /// </item> + /// <item> + /// <description>Object.ToString - these are called by the debugger to display + /// objects and are also often used with printf style debugging so they should + /// not change the object's state and should not throw.</description> + /// </item> + /// <item> + /// <description>static constructors - should very rarely throw. If they + /// do throw then the type will not be useable within that application + /// domain.</description> + /// </item> + /// <item> + /// <description>finalizers - should not throw. If they do (as of .NET 2.0) + /// the process will be torn down.</description> + /// </item> + /// <item> + /// <description>IDisposable.Dispose - should not throw. If they do + /// it's much harder to guarantee that objects clean up properly.</description> + /// </item> + /// <item> + /// <description>Dispose (bool) - should not throw because that makes + /// it very difficult to clean up objects and because they are often + /// called from a finalizer.</description> + /// </item> + /// <item> + /// <description>operator== and operator!= - should not throw. In particular + /// they should do something sensible when passed null arguments or + /// unexpected types.</description> + /// </item> + /// <item> + /// <description>implicit cast operators - should not throw. These methods + /// are called implicitly so it tends to be quite surprising if they throw + /// exceptions.</description> + /// </item> + /// </list> + /// Note that the rule does not complain if a method throws + /// System.NotImplementedException because + /// DoNotForgetNotImplementedMethodsRule will flag them. Also the rule + /// may fire with anonymous types with gmcs versions prior to 2.2, see + /// [https://bugzilla.novell.com/show_bug.cgi?id=462622] for more details. + /// </summary> + /// <example> + /// Bad example: + /// <code> + /// public override bool Equals (object obj) + /// { + /// if (obj == null) { + /// return false; + /// } + /// + /// Customer rhs = (Customer) obj; // throws if obj is not a Customer + /// return name == rhs.name; + /// } + /// </code> + /// </example> + /// <example> + /// Good example: + /// <code> + /// public override bool Equals (object obj) + /// { + /// Customer rhs = obj as Customer; + /// if (rhs == null) { + /// return false; + /// } + /// + /// return name == rhs.name; + /// } + /// </code> + /// </example> + + // http://msdn.microsoft.com/en-us/library/bb386039.aspx + [Problem ("A method throws an exception it should not.")] + [Solution ("Change the code so that it does not throw, throw a correct exception, or trap exceptions.")] + [FxCopCompatibility ("Microsoft.Design", "CA1065:DoNotRaiseExceptionsInUnexpectedLocations")] + [EngineDependency (typeof (OpCodeEngine))] + public sealed class DoNotThrowInUnexpectedLocationRule : Rule, IMethodRule { + + private static readonly OpCodeBitmask Throwers = new OpCodeBitmask (0x0, 0x80C8000000000000, 0x3FC17F8000001FF, 0x0); + private static readonly OpCodeBitmask AlwaysBadThrowers = new OpCodeBitmask (0x0, 0x0, 0x100000000000, 0x0); + private static readonly OpCodeBitmask OverflowThrowers = new OpCodeBitmask (0x0, 0x8000000000000000, 0x3FC17F8000001FF, 0x0); + + private static readonly string [] GetterExceptions = new string [] {"System.InvalidOperationException", "System.NotSupportedException"}; + private static readonly string [] IndexerExceptions = new string [] {"System.InvalidOperationException", "System.NotSupportedException", "System.ArgumentException", "System.Collections.Generic.KeyNotFoundException"}; + private static readonly string [] EventExceptions = new string [] {"System.InvalidOperationException", "System.NotSupportedException", "System.ArgumentException"}; + private static readonly string [] HashCodeExceptions = new string [] {"System.ArgumentException"}; + + private TypeReference type; + private MethodSignature equalityComparerEquals; + private MethodSignature equalityComparerHashCode; + private string [] allowedExceptions; + private string methodLabel; + + private bool HasCatchBlock (MethodDefinition method) + { + foreach (ExceptionHandler handler in method.Body.ExceptionHandlers) { + if (handler.Type == ExceptionHandlerType.Catch) + return true; + } + + return false; + } + + private static readonly string [] AnySingleArg = new string [1]; + private static readonly string [] AnyTwoArgs = new string [2]; + + private void InitType (TypeReference type) + { + if (type.Implements ("System.Collections.Generic.IEqualityComparer`1")) { + equalityComparerEquals = new MethodSignature ("Equals", "System.Boolean", AnyTwoArgs, MethodAttributes.Public); + equalityComparerHashCode = new MethodSignature ("GetHashCode", "System.Int32", AnySingleArg, MethodAttributes.Public); + } else { + equalityComparerEquals = null; + equalityComparerHashCode = null; + } + } + + private bool PreflightMethod (MethodDefinition method) + { + allowedExceptions = null; + bool valid = false; + + if (MethodSignatures.ToString.Matches (method)) { + methodLabel = "Object.ToString"; // these names should match those used within the rule description + valid = true; + + } else if (MethodSignatures.Equals.Matches (method)) { + methodLabel = "Object.Equals"; + valid = true; + + } else if (MethodSignatures.GetHashCode.Matches (method)) { + methodLabel = "Object.GetHashCode"; + valid = true; + + } else if (MethodSignatures.Finalize.Matches (method)) { + methodLabel = "Finalizers"; + valid = true; + + } else if (MethodSignatures.Dispose.Matches (method) && method.DeclaringType.Implements ("System.IDisposable")) { + methodLabel = "IDisposable.Dispose"; + valid = true; + + } else if (MethodSignatures.DisposeExplicit.Matches (method) && method.DeclaringType.Implements ("System.IDisposable")) { + methodLabel = "IDisposable.Dispose"; + valid = true; + + } else if (MethodSignatures.op_Equality.Matches (method)) { + methodLabel = "operator=="; + valid = true; + + } else if (MethodSignatures.op_Inequality.Matches (method)) { + methodLabel = "operator!="; + valid = true; + + } else if (equalityComparerEquals != null && equalityComparerEquals.Matches (method)) { + methodLabel = "IEqualityComparer<T>.Equals"; + valid = true; + + } else if (equalityComparerHashCode != null && equalityComparerHashCode.Matches (method)) { + methodLabel = "IEqualityComparer<T>.GetHashCode"; + allowedExceptions = HashCodeExceptions; + valid = true; + + } else if (method.Name == "Dispose" && method.Parameters.Count == 1 && method.Parameters [0].ParameterType.FullName == "System.Boolean") { + methodLabel = "Dispose (bool)"; + valid = true; + + } else if (method.IsConstructor && method.IsStatic) { + methodLabel = "Static constructors"; + valid = true; + + } else if (method.Name == "op_Implicit") { + methodLabel = "Implicit cast operators"; + valid = true; + + } else if (method.IsSpecialName && method.Name == "get_Item") { + methodLabel = "Indexed getters"; + allowedExceptions = IndexerExceptions; + valid = true; + + } else if (method.IsGetter) { + methodLabel = "Property getters"; + allowedExceptions = GetterExceptions; + valid = true; + + } else if (method.IsAddOn || method.IsRemoveOn) { + methodLabel = "Event accessors"; + allowedExceptions = EventExceptions; + valid = true; + } + + return valid; + } + + // It's not always apparent why the code throws so we'll try to explain + // the reason here (for example foreach can generate castclass or unbox + // instructions and assemblies compiled with checked arithmetic can + // throw even if the code doesn't explicitly use an arithmetic operator). + private string ExplainThrow (Instruction ins) + { + switch (ins.OpCode.Code) { + case Code.Castclass: + return string.Format (" (cast to {0})", ((TypeReference) ins.Operand).Name); + + case Code.Throw: // this one is obvious + return string.Empty; + + case Code.Unbox: + return string.Format (" (unbox from {0})", ((TypeReference) ins.Operand).Name); + + case Code.Ckfinite: + return " (the expression will throw if the value is a NAN or an infinity)"; + + default: + Debug.Assert (ins.OpCode.Name.Contains ("_Ovf"), "expected an overflow opcode, not " + ins.OpCode.Code); + return " (checked arithmetic is being used)"; + } + } + + private List<Instruction> instructions = new List<Instruction> (); + private List<Instruction> bad = new List<Instruction> (); + private List<Instruction> casts = new List<Instruction> (); + public static readonly MethodSignature GetTypeSig = new MethodSignature ("GetType", "System.Type", new string [0], MethodAttributes.Public); + + private void ProcessMethod (MethodDefinition method) + { + bad.Clear (); + casts.Clear (); + bool casts_are_ok = false; + bool is_equals = MethodSignatures.Equals.Matches (method); + + foreach (Instruction ins in method.Body.Instructions) { + Code code = ins.OpCode.Code; + + if (is_equals && !casts_are_ok) { + if (code == Code.Isinst) + casts_are_ok = true; + + else if (code == Code.Call || code == Code.Callvirt) { + MethodReference mr = (MethodReference) ins.Operand; + if (GetTypeSig.Matches (mr)) + casts_are_ok = true; + } + } + + if (Throwers.Get (code)) { + + // A few instructions are bad to the bone. + if (AlwaysBadThrowers.Get (code)) { + bad.Add (ins); + + // If the instruction is castclass or unbox then we may have a + // problem, but only within Object.Equals (casts occur way too + // often to flag them everywhere, but it's common mistake to + // cast the Equals argument without an is or GetType check). + } else if (code == Code.Castclass || code == Code.Unbox) { + if (is_equals && !casts_are_ok) + casts.Add (ins); + + // If the instruction is a checked math instruction then we have + // a problem, but only in GetHashCode methods (they are + // potential problems elsewhere but the likelihood of an actual + // problem is much higher in hash code methods and there are + // too many defects if we flag them everywhere). + } else if (OverflowThrowers.Get (code)) { + if (method.Name == "GetHashCode") + bad.Add (ins); + + // If the instruction is a throw, + } else if (code == Code.Throw) { + + // and is throwing NotImplementedException then it is OK (this + // is a fairly common case and we'll let DoNotForgetNotImplementedMethodsRule + // handle it). + if (ins.Previous != null && ins.Previous.OpCode.Code == Code.Newobj) { + MethodReference mr = (MethodReference) ins.Previous.Operand; + string name = mr.DeclaringType.FullName; + if (name == "System.NotImplementedException" || mr.DeclaringType.Inherits ("System.NotImplementedException")) + continue; + } + + // If the method doesn't allow any exceptions then we have a + // problem. + if (allowedExceptions == null) + bad.Add (ins); + + // If the throw does not one of the enumerated exceptions (or + // a subclass) then we have a problem. + else if (ins.Previous != null && ins.Previous.OpCode.Code == Code.Newobj) { + MethodReference mr = (MethodReference) ins.Previous.Operand; + string name = mr.DeclaringType.FullName; + if (Array.IndexOf (allowedExceptions, name) < 0) { + if (!allowedExceptions.Any (e => mr.DeclaringType.Inherits (e))) { + bad.Add (ins); + } + } + } + } + } + } + + instructions.Clear (); + instructions.AddRange (bad); + if (is_equals && !casts_are_ok) + instructions.AddRange (casts); + } + + private void ReportErrors (MethodDefinition method) + { + foreach (Instruction ins in instructions) { + string mesg; + if (allowedExceptions == null) + mesg = string.Format ("{0} should not throw{1}.", methodLabel, ExplainThrow (ins)); + else + mesg = string.Format ("{0} should only throw {1} or a subclass{2}.", methodLabel, string.Join (", ", allowedExceptions), ExplainThrow (ins)); + Log.WriteLine (this, "{0:X4}: {1}", ins.Offset, mesg); + + // We reduce the severity of getters and event accessors because + // it's not quite as bad for a method which allows some exceptions + // to throw the wrong exception. + Severity severity = method.IsGetter || method.IsAddOn || method.IsRemoveOn ? + Severity.Medium : Severity.High; + Runner.Report (method, ins, severity, Confidence.High, mesg); + } + } + + public RuleResult CheckMethod (MethodDefinition method) + { + if (!method.HasBody) + return RuleResult.DoesNotApply; + + if (!Throwers.Intersect (OpCodeEngine.GetBitmask (method))) + return RuleResult.DoesNotApply; + + if (method.DeclaringType != type) { // note that we cannot use the AnalyzeType event because it is not called with the unit tests + InitType (method.DeclaringType); + type = method.DeclaringType; + } + + if (!HasCatchBlock (method)) { + if (PreflightMethod (method)) { + Log.WriteLine (this); + Log.WriteLine (this, "-------------------------------------------"); + Log.WriteLine (this, method); + + ProcessMethod (method); + if (instructions.Count > 0) + ReportErrors (method); + } + } + + return Runner.CurrentRuleResult; + } + +#if false + // Note that none of these instructions throw an exception that is + // ever allowed. + private static readonly Code [] AlwaysBad = new Code [] + { + Code.Ckfinite, // throws ArithmeticException + }; + + private static readonly Code [] SometimesBad = new Code [] + { + Code.Castclass, // throws InvalidCastException + Code.Throw, + Code.Unbox, // throws InvalidCastException or NullReferenceException + }; + + private static readonly Code [] Overflow = new Code [] + { + Code.Add_Ovf, // throws OverflowException + Code.Mul_Ovf, + Code.Sub_Ovf, + Code.Add_Ovf_Un, + Code.Mul_Ovf_Un, + Code.Sub_Ovf_Un, + Code.Conv_Ovf_I1_Un, + Code.Conv_Ovf_I2_Un, + Code.Conv_Ovf_I4_Un, + Code.Conv_Ovf_I8_Un, + Code.Conv_Ovf_U1_Un, + Code.Conv_Ovf_U2_Un, + Code.Conv_Ovf_U4_Un, + Code.Conv_Ovf_U8_Un, + Code.Conv_Ovf_I_Un, + Code.Conv_Ovf_U_Un, + Code.Conv_Ovf_I1, + Code.Conv_Ovf_U1, + Code.Conv_Ovf_I2, + Code.Conv_Ovf_U2, + Code.Conv_Ovf_I4, + Code.Conv_Ovf_U4, + Code.Conv_Ovf_I8, + Code.Conv_Ovf_U8, + Code.Conv_Ovf_I, + Code.Conv_Ovf_U, + }; + + public void GenerateBitmask () + { + OpCodeBitmask throwers = new OpCodeBitmask (); + OpCodeBitmask alwaysBad = new OpCodeBitmask (); + OpCodeBitmask overflow = new OpCodeBitmask (); + + foreach (Code code in AlwaysBad) { + throwers.Set (code); + alwaysBad.Set (code); + } + + foreach (Code code in SometimesBad) { + throwers.Set (code); + } + + foreach (Code code in Overflow) { + throwers.Set (code); + overflow.Set (code); + } + + Console.WriteLine ("throwers: {0}", throwers); + Console.WriteLine ("alwaysBad: {0}", alwaysBad); + Console.WriteLine ("overflow: {0}", overflow); + } +#endif + } +} diff --git a/gendarme/rules/Gendarme.Rules.Exceptions/Makefile.am b/gendarme/rules/Gendarme.Rules.Exceptions/Makefile.am index 9ba0730d..5ee8a1ed 100644 --- a/gendarme/rules/Gendarme.Rules.Exceptions/Makefile.am +++ b/gendarme/rules/Gendarme.Rules.Exceptions/Makefile.am @@ -4,6 +4,7 @@ rules_sources = \ AvoidArgumentExceptionDefaultConstructorRule.cs \ AvoidThrowingBasicExceptionsRule.cs \ DoNotDestroyStackTraceRule.cs \ + DoNotThrowInUnexpectedLocationRule.cs \ DoNotThrowReservedExceptionRule.cs \ DontSwallowErrorsCatchingNonspecificExceptionsRule.cs \ ExceptionShouldBeVisibleRule.cs \ @@ -22,6 +23,7 @@ rules_sources = \ tests_sources = \ AvoidArgumentExceptionDefaultConstructorTest.cs \ AvoidThrowingBasicExceptionsTest.cs \ + DoNotThrowInUnexpectedLocationTest.cs \ DontDestroyStackTraceTest.cs \ DontSwallowErrorsCatchingNonspecificExceptionsTest.cs \ DoNotThrowReservedExceptionTest.cs \ diff --git a/gendarme/rules/Gendarme.Rules.Exceptions/Test/ChangeLog b/gendarme/rules/Gendarme.Rules.Exceptions/Test/ChangeLog index 68217a5a..25beeb63 100644 --- a/gendarme/rules/Gendarme.Rules.Exceptions/Test/ChangeLog +++ b/gendarme/rules/Gendarme.Rules.Exceptions/Test/ChangeLog @@ -1,3 +1,7 @@ +2008-12-30 Jesse Jones <jesjones@mindspring.com> + + * DoNotThrowInUnexpectedLocationTest.cs: Added. + 2008-12-30 Sebastien Pouliot <sebastien@ximian.com> * DontSwallowErrorsCatchingNonspecificExceptionsTest.cs: Add diff --git a/gendarme/rules/Gendarme.Rules.Exceptions/Test/DoNotThrowInUnexpectedLocationTest.cs b/gendarme/rules/Gendarme.Rules.Exceptions/Test/DoNotThrowInUnexpectedLocationTest.cs new file mode 100644 index 00000000..e0dc0c33 --- /dev/null +++ b/gendarme/rules/Gendarme.Rules.Exceptions/Test/DoNotThrowInUnexpectedLocationTest.cs @@ -0,0 +1,400 @@ +// +// Unit test for DoNotThrowInUnexpectedLocationRule +// +// Authors: +// Jesse Jones <jesjones@mindspring.com> +// +// Copyright (C) 2008 Jesse Jones +// +// 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. +// + +using System; +using System.Collections.Generic; +using System.Reflection; + +using Mono.Cecil; +using Gendarme.Rules.Exceptions; + +using NUnit.Framework; +using Test.Rules.Definitions; +using Test.Rules.Fixtures; +using Test.Rules.Helpers; + +namespace Test.Rules.Exceptions { + + [TestFixture] + internal sealed class DoNotThrowInUnexpectedLocationTest : MethodRuleTestFixture<DoNotThrowInUnexpectedLocationRule> { + + private sealed class Inapplicable { + public override string ToString () + { + return "Inapplicable"; // doesn't have an instruction which can throw + } + } + + private sealed class Good1 { + public int State { get; set; } + + public override string ToString () + { + string result; + + try + { + if (State < 0) + throw new Exception ("state is negative"); + + result = "Good1"; + } catch (Exception e) { // has a catch block so the throw is OK + result = e.Message; + } + + return result; + } + } + + private sealed class Good2 : IEqualityComparer<Good1> { + public bool Equals (Good1 x, Good1 y) + { + return x.State == y.State; + } + + public int GetHashCode (Good1 x) + { + if (x.State < 0) + throw new ArgumentException ("state must be non-negative", "x"); // ok to throw ArgumentException here + + return x.State; + } + } + + private sealed class Good3 : IEqualityComparer<Good1> { + public bool Equals (Good1 x, Good1 y) + { + return x.State == y.State; + } + + public int GetHashCode (Good1 x) + { + if (x.State < 0) + throw new ArgumentOutOfRangeException ("x", "state must be non-negative"); // subclass of ArgumentException so OK + + return x.State; + } + } + + private sealed class Good4 { + public int State { get; set; } + + public override bool Equals (object obj) + { + if (!(obj is Good4)) + return false; + + Good4 rhs = (Good4) obj; // cast is OK when paired with isinst + + return State == rhs.State; + } + + public override int GetHashCode () + { + if (State < 0) + throw new ArgumentException ("oops"); // this is only allowed in IEqualityComparer + + return State; + } + } + + private sealed class Good5 { + public int State { get; set; } + + public override string ToString () + { + int value = checked (State + 1000); // checked aithmetic is OK outside GetHashCode + return value >= 0 ? "Good5" : "empty"; + } + } + + private sealed class Good6 { + public override string ToString () + { + throw new NotImplementedException (); // NotImplementedException is OK everywhere + } + } + + private struct Good7 { + public Good7 (int x, int y) + { + this.x = x; + this.y = y; + } + + public override bool Equals (object rhsObj) + { + if (rhsObj == null) + return false; + + if (GetType () != rhsObj.GetType ()) + return false; + + Good7 rhs = (Good7) rhsObj; // OK because of the GetType call + return x == rhs.x && y == rhs.y; + } + + public override int GetHashCode () + { + int hash; + + unchecked + { + hash = 3*x.GetHashCode () + 7*y.GetHashCode (); + } + + return hash; + } + + private int x, y; + } + + private sealed class Good8 { + public bool this [int index] { + get { + if (index < 0) + throw new KeyNotFoundException (); // this is OK from indexers + + return index > 0; + } + } + } + + private sealed class Bad1 { + public int State { get; set; } + + public override string ToString () + { + if (State < 0) + throw new Exception ("state is negative"); + + return "Bad1"; + } + } + + private sealed class Bad2 { + public int State { get; set; } + + public override string ToString () + { + string result; + + try + { + if (State < 0) + throw new Exception ("state is negative"); + + result = "Bad2"; + } finally { // finally blocks allow the exception to escape so they are no good + result = "oops"; + } + + return result; + } + } + + private sealed class Bad3 { + public string State { get; set; } + + public override string ToString () + { + if (State == null) + throw new Exception ("oops"); // throws + + return State.Length >= 0 ? "Bad3" : "empty"; + } + } + + // no Bad4 + + private sealed class Bad5 { + public int State { get; set; } + + public override bool Equals (object obj) + { + Bad5 rhs = obj as Bad5; + if (rhs == null) + return false; + + return State == rhs.State; + } + + public override int GetHashCode () + { + return checked (State + 1000); // checked airthmetic is not OK within GetHashCode + } + } + + private sealed class Bad6 { + public int State { get; set; } + + public override bool Equals (object obj) + { + if (obj == null) + return false; + + Bad6 rhs = (Bad6) obj; // cast + return State == rhs.State; + } + + public override int GetHashCode () + { + return checked (State + 1000); + } + } + + private sealed class Bad7 : IEqualityComparer<Good1> { + public bool Equals (Good1 x, Good1 y) + { + return x.State == y.State; + } + + public int GetHashCode (Good1 x) + { + if (x.State < 0) + throw new NotSupportedException ("state must be non-negative"); // only ArgumentException is OK + + return x.State; + } + } + + private sealed class Bad8 : IEqualityComparer<Good1> { + public bool Equals (Good1 x, Good1 y) + { + if (x.State < 0) + throw new Exception ("oops"); + + return x.State == y.State; + } + + public int GetHashCode (Good1 x) + { + return x.State; + } + } + + private sealed class Bad9 { + public int State { get; set; } + + public override bool Equals (object obj) + { + Bad9 rhs = obj as Bad9; + if (rhs == null) + return false; + + return State == rhs.State; + } + + public override int GetHashCode () + { + if (State < 0) + throw new ArgumentException ("oops"); // this is only allowed in IEqualityComparer + + return State; + } + } + + private sealed class Bad10 : IDisposable { + public void Dispose () + { + if (disposed) + throw new ObjectDisposedException (GetType ().Name); // throws + + Dispose (true); + GC.SuppressFinalize (this); + } + + private void Dispose (bool disposing) + { + if (!disposing) + throw new Exception ("how did we get here?"); // throws + + disposed = true; + } + + private bool disposed; + } + + private sealed class Bad11 { + public int State { get; set; } + + public static implicit operator int (Bad11 value) + { + if (value.State < 0) + throw new ArgumentException ("oops"); // throws + + return value.State; + } + } + + private sealed class Bad12 { + public bool this [int index] { + get { + if (index < 0) + throw new ApplicationException (); // this is not OK + + return index > 0; + } + } + } + + [Test] + public void DoesNotApply () + { + AssertRuleDoesNotApply (SimpleMethods.ExternalMethod); + AssertRuleDoesNotApply<Inapplicable> ("ToString"); + } + + [Test] + public void Check () + { + AssertRuleSuccess<Good1> ("ToString"); + AssertRuleSuccess<Good2> ("GetHashCode"); + AssertRuleSuccess<Good3> ("GetHashCode"); + AssertRuleSuccess<Good4> ("Equals"); + AssertRuleSuccess<Good5> ("ToString"); + AssertRuleSuccess<Good6> ("ToString"); + AssertRuleSuccess<Good7> ("Equals"); + AssertRuleSuccess<Good8> ("get_Item"); + + AssertRuleFailure<Bad1> ("ToString", 1); + AssertRuleFailure<Bad2> ("ToString", 1); + AssertRuleFailure<Bad3> ("ToString", 1); + AssertRuleFailure<Bad5> ("GetHashCode", 1); + AssertRuleFailure<Bad6> ("Equals", 1); + AssertRuleFailure<Bad6> ("GetHashCode", 1); + AssertRuleFailure<Bad7> ("GetHashCode", 1); + AssertRuleFailure<Bad8> ("Equals", 1); + AssertRuleFailure<Bad9> ("GetHashCode", 1); + AssertRuleFailure<Bad10> ("Dispose", new Type [0], 1); + AssertRuleFailure<Bad10> ("Dispose", new Type [] {typeof (bool)}, 1); + AssertRuleFailure<Bad11> ("op_Implicit", 1); + AssertRuleFailure<Bad12> ("get_Item", 1); + } + } +} |