/// <summary>In DEBUG mode, runs all post-build checks defined in the specified assemblies. This is intended to be run as a post-build event. See remarks for details.</summary> /// <remarks><para>In non-DEBUG mode, does nothing and returns 0.</para> /// <para>Intended use is as follows:</para> /// <list type="bullet"> /// <item><description><para>Add the following line to your project's post-build event:</para> /// <code>"$(TargetPath)" --post-build-check "$(SolutionDir)."</code></description></item> /// <item><description><para>Add the following code at the beginning of your project's Main() method:</para> /// <code> /// if (args.Length == 2 && args[0] == "--post-build-check") /// return Ut.RunPostBuildChecks(args[1], Assembly.GetExecutingAssembly()); /// </code> /// <para>If your project entails several assemblies, you can specify additional assemblies in the call to <see cref="Ut.RunPostBuildChecks"/>. /// For example, you could specify <c>typeof(SomeTypeInMyLibrary).Assembly</c>.</para> /// </description></item> /// <item><description> /// <para>Add post-build check methods to any type where they may be relevant. For example, for a command-line program that uses /// <see cref="RT.Util.CommandLine.CommandLineParser"/>, you might use code similar to the following:</para> /// <code> /// #if DEBUG /// private static void PostBuildCheck(IPostBuildReporter rep) /// { /// // Replace “CommandLine” with the name of your command-line type, and “Translation” /// // with the name of your translation type (<see cref="RT.Util.Lingo.TranslationBase"/>) /// CommandLineParser.PostBuildStep<CommandLine>(rep, typeof(Translation)); /// } /// #endif /// </code> /// <para>The method is expected to have one parameter of type <see cref="IPostBuildReporter"/>, a return type of void, and it is expected /// to be static and non-public. Errors and warnings can be reported by calling methods on said <see cref="IPostBuildReporter"/> object. /// Alternatively, throwing an exception will also report an error.</para> /// </description></item> /// </list></remarks> /// <param name="sourcePath">Specifies the path to the folder containing the C# source files.</param> /// <param name="assemblies">Specifies the compiled assemblies from which to run post-build checks.</param> /// <returns>1 if any errors occurred, otherwise 0.</returns> public static int RunPostBuildChecks(string sourcePath, params Assembly[] assemblies) { int countMethods = 0; var rep = new postBuildReporter(sourcePath); var attempt = Ut.Lambda((Action action) => { try { action(); } catch (Exception e) { rep.AnyErrors = true; string indent = ""; while (e != null) { var st = new StackTrace(e, true); string fileLine = null; for (int i = 0; i < st.FrameCount; i++) { var frame = st.GetFrame(i); if (frame.GetFileName() != null) { fileLine = frame.GetFileName() + "(" + frame.GetFileLineNumber() + "," + frame.GetFileColumnNumber() + "): "; break; } } Console.Error.WriteLine("{0}Error: {1}{2} ({3})".Fmt( fileLine, indent, e.Message.Replace("\n", " ").Replace("\r", ""), e.GetType().FullName)); Console.Error.WriteLine(e.StackTrace); e = e.InnerException; indent += "---- "; } } }); // Check 1: Custom-defined PostBuildCheck methods foreach (var ty in assemblies.SelectMany(asm => asm.GetTypes())) { attempt(() => { var meth = ty.GetMethod("PostBuildCheck", BindingFlags.NonPublic | BindingFlags.Static); if (meth != null) { if (meth.GetParameters().Select(p => p.ParameterType).SequenceEqual(new Type[] { typeof(IPostBuildReporter) }) && meth.ReturnType == typeof(void)) { countMethods++; meth.Invoke(null, new object[] { rep }); } else rep.Error( "The type {0} has a method called PostBuildCheck() that is not of the expected signature. There should be one parameter of type {1}, and the return type should be void.".Fmt(ty.FullName, typeof(IPostBuildReporter).FullName), (ty.IsValueType ? "struct " : "class ") + ty.Name, "PostBuildCheck"); } }); } // Check 2: All “throw new ArgumentNullException(...)” statements should refer to an actual parameter foreach (var asm in assemblies) foreach (var type in asm.GetTypes()) foreach (var meth in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly)) attempt(() => { var instructions = ILReader.ReadIL(meth, type).ToArray(); for (int i = 0; i < instructions.Length; i++) if (instructions[i].OpCode.Value == OpCodes.Newobj.Value) { var constructor = (ConstructorInfo) instructions[i].Operand; string wrong = null; string wrongException = "ArgumentNullException"; if (constructor.DeclaringType == typeof(ArgumentNullException) && constructor.GetParameters().Select(p => p.ParameterType).SequenceEqual(typeof(string))) if (instructions[i - 1].OpCode.Value == OpCodes.Ldstr.Value) if (!meth.GetParameters().Any(p => p.Name == (string) instructions[i - 1].Operand)) wrong = (string) instructions[i - 1].Operand; if (constructor.DeclaringType == typeof(ArgumentNullException) && constructor.GetParameters().Select(p => p.ParameterType).SequenceEqual(typeof(string), typeof(string))) if (instructions[i - 1].OpCode.Value == OpCodes.Ldstr.Value && instructions[i - 2].OpCode.Value == OpCodes.Ldstr.Value) if (!meth.GetParameters().Any(p => p.Name == (string) instructions[i - 2].Operand)) wrong = (string) instructions[i - 2].Operand; if (constructor.DeclaringType == typeof(ArgumentException) && constructor.GetParameters().Select(p => p.ParameterType).SequenceEqual(typeof(string), typeof(string))) if (instructions[i - 1].OpCode.Value == OpCodes.Ldstr.Value) if (!meth.GetParameters().Any(p => p.Name == (string) instructions[i - 1].Operand)) { wrong = (string) instructions[i - 1].Operand; wrongException = "ArgumentException"; } if (wrong != null) { rep.Error( Regex.IsMatch(meth.DeclaringType.Name, @"<.*>d__\d") ? @"The iterator method ""{0}.{1}"" constructs a {2}. Move this argument check outside the iterator.".Fmt(type.FullName, meth.Name, wrongException, wrong) : @"The method ""{0}.{1}"" constructs an {2} with a parameter name ""{3}"" which doesn't appear to be a parameter in that method.".Fmt(type.FullName, meth.Name, wrongException, wrong), getDebugClassName(meth), getDebugMethodName(meth), wrongException, wrong ); } if (constructor.DeclaringType == typeof(ArgumentException) && constructor.GetParameters().Select(p => p.ParameterType).SequenceEqual(typeof(string))) rep.Error( Regex.IsMatch(meth.DeclaringType.Name, @"<.*>d__\d") ? @"The iterator method ""{0}.{1}"" constructs an ArgumentException. Move this argument check outside the iterator.".Fmt(type.FullName, meth.Name) : @"The method ""{0}.{1}"" uses the single-argument constructor to ArgumentException. Please use the two-argument constructor and specify the parameter name. If there is no parameter involved, use InvalidOperationException.".Fmt(type.FullName, meth.Name), getDebugClassName(meth), getDebugMethodName(meth), "ArgumentException"); } }); Console.WriteLine("Post-build checks ran on {0} assemblies, {1} methods and completed {2}.".Fmt(assemblies.Length, countMethods, rep.AnyErrors ? "with ERRORS" : "SUCCESSFULLY")); return rep.AnyErrors ? 1 : 0; }
/// <summary> /// Runs all post-build checks defined in the specified assemblies. This is intended to be run as a post-build /// event. See remarks for details.</summary> /// <remarks> /// <para> /// In non-DEBUG mode, does nothing and returns 0.</para> /// <para> /// Intended use is as follows:</para> /// <list type="bullet"> /// <item><description> /// <para> /// Add the following line to your project's post-build event:</para> /// <code> /// "$(TargetPath)" --post-build-check "$(SolutionDir)."</code></description></item> /// <item><description> /// <para> /// Add the following code at the beginning of your project's Main() method:</para> /// <code> /// if (args.Length == 2 && args[0] == "--post-build-check") /// return PostBuildChecker.RunPostBuildChecks(args[1], Assembly.GetExecutingAssembly());</code> /// <para> /// If your project entails several assemblies, you can specify additional assemblies in the call to /// <see cref="PostBuildChecker.RunPostBuildChecks"/>. For example, you could specify /// <c>typeof(SomeTypeInMyLibrary).Assembly</c>.</para></description></item> /// <item><description> /// <para> /// Add post-build check methods to any type where they may be relevant. For example, you might use /// code similar to the following:</para> /// <code> /// #if DEBUG /// private static void PostBuildCheck(IPostBuildReporter rep) /// { /// if (somethingWrong()) /// rep.Error("Error XYZ occurred.", "class", "Gizmo"); /// } /// #endif</code> /// <para> /// The method is expected to have one parameter of type <see cref="IPostBuildReporter"/>, a return /// type of void, and it is expected to be static and non-public. Errors and warnings can be reported /// by calling methods on said <see cref="IPostBuildReporter"/> object. (In the above example, /// PostBuildChecker will attempt to find a class called Gizmo to link the error message to a location /// in the source.) Throwing an exception will also report an error.</para></description></item></list></remarks> /// <param name="sourcePath"> /// Specifies the path to the folder containing the C# source files.</param> /// <param name="assemblies"> /// Specifies the compiled assemblies from which to run post-build checks.</param> /// <returns> /// 1 if any errors occurred, otherwise 0.</returns> public static int RunPostBuildChecks(string sourcePath, params Assembly[] assemblies) { int countMethods = 0; var rep = new postBuildReporter(sourcePath); var attempt = Ut.Lambda((Action action) => { try { action(); } catch (Exception e) { rep.AnyErrors = true; string indent = ""; while (e != null) { var st = new StackTrace(e, true); string fileLine = null; for (int i = 0; i < st.FrameCount; i++) { var frame = st.GetFrame(i); if (frame.GetFileName() != null) { fileLine = frame.GetFileName() + "(" + frame.GetFileLineNumber() + "," + frame.GetFileColumnNumber() + "): "; break; } } Console.Error.WriteLine($"{fileLine}Error: {indent}{e.Message.Replace("\n", " ").Replace("\r", "")} ({e.GetType().FullName})"); Console.Error.WriteLine(e.StackTrace); e = e.InnerException; indent += "---- "; } } }); // Step 1: Run all the custom-defined PostBuildCheck methods foreach (var ty in assemblies.SelectMany(asm => asm.GetTypes())) { attempt(() => { var meth = ty.GetMethod("PostBuildCheck", BindingFlags.NonPublic | BindingFlags.Static); if (meth != null) { if (meth.GetParameters().Select(p => p.ParameterType).SequenceEqual(new Type[] { typeof(IPostBuildReporter) }) && meth.ReturnType == typeof(void)) { countMethods++; meth.Invoke(null, new object[] { rep }); } else { rep.Error( $"The type {ty.FullName} has a method called PostBuildCheck() that is not of the expected signature. There should be one parameter of type {typeof(IPostBuildReporter).FullName}, and the return type should be void.", (ty.IsValueType ? "struct " : "class ") + ty.Name, "PostBuildCheck"); } } }); } // Step 2: Run all the built-in checks on IL code foreach (var asm in assemblies) { foreach (var type in asm.GetTypes()) { foreach (var meth in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly)) { attempt(() => { var instructions = ILReader.ReadIL(meth, type).ToArray(); for (int i = 0; i < instructions.Length; i++) { // Check that “throw new ArgumentNullException(...)” statements refer to an actual parameter if (instructions[i].OpCode.Value == OpCodes.Newobj.Value) { var constructor = (ConstructorInfo)instructions[i].Operand; string wrong = null; string wrongException = "ArgumentNullException"; if (constructor.DeclaringType == typeof(ArgumentNullException) && constructor.GetParameters().Select(p => p.ParameterType).SequenceEqual(typeof(string))) { if (instructions[i - 1].OpCode.Value == OpCodes.Ldstr.Value) { if (!meth.GetParameters().Any(p => p.Name == (string)instructions[i - 1].Operand)) { wrong = (string)instructions[i - 1].Operand; } } } if (constructor.DeclaringType == typeof(ArgumentNullException) && constructor.GetParameters().Select(p => p.ParameterType).SequenceEqual(typeof(string), typeof(string))) { if (instructions[i - 1].OpCode.Value == OpCodes.Ldstr.Value && instructions[i - 2].OpCode.Value == OpCodes.Ldstr.Value) { if (!meth.GetParameters().Any(p => p.Name == (string)instructions[i - 2].Operand)) { wrong = (string)instructions[i - 2].Operand; } } } if (constructor.DeclaringType == typeof(ArgumentException) && constructor.GetParameters().Select(p => p.ParameterType).SequenceEqual(typeof(string), typeof(string))) { if (instructions[i - 1].OpCode.Value == OpCodes.Ldstr.Value) { if (!meth.GetParameters().Any(p => p.Name == (string)instructions[i - 1].Operand)) { wrong = (string)instructions[i - 1].Operand; wrongException = "ArgumentException"; } } } if (wrong != null) { rep.Error( Regex.IsMatch(meth.DeclaringType.Name, @"<.*>d__\d") ? $@"The iterator method ""{type.FullName}.{meth.Name}"" constructs a {wrongException}. Move this argument check outside the iterator." : $@"The method ""{type.FullName}.{meth.Name}"" constructs an {wrongException} with a parameter name ""{wrong}"" which doesn't appear to be a parameter in that method.", getDebugClassName(meth), getDebugMethodName(meth), wrongException, wrong ); } if (constructor.DeclaringType == typeof(ArgumentException) && constructor.GetParameters().Select(p => p.ParameterType).SequenceEqual(typeof(string))) { rep.Error( Regex.IsMatch(meth.DeclaringType.Name, @"<.*>d__\d") ? $@"The iterator method ""{type.FullName}.{meth.Name}"" constructs an ArgumentException. Move this argument check outside the iterator." : $@"The method ""{type.FullName}.{meth.Name}"" uses the single-argument constructor to ArgumentException. Please use the two-argument constructor and specify the parameter name. If there is no parameter involved, use InvalidOperationException.", getDebugClassName(meth), getDebugMethodName(meth), "ArgumentException"); } } else if (i < instructions.Length - 1 && (instructions[i].OpCode.Value == OpCodes.Call.Value || instructions[i].OpCode.Value == OpCodes.Callvirt.Value) && instructions[i + 1].OpCode.Value == OpCodes.Pop.Value) { var method = (MethodInfo)instructions[i].Operand; var mType = method.DeclaringType; if (postBuildGetNoPopMethods().Contains(method)) { rep.Error( $@"Useless call to ""{mType.FullName}.{method.Name}"" (the return value is discarded).", getDebugClassName(meth), getDebugMethodName(meth), method.Name ); } } } }); } } } Console.WriteLine($"Post-build checks ran on {assemblies.Length} assemblies, {countMethods} methods and completed {(rep.AnyErrors ? "with ERRORS" : "SUCCESSFULLY")}."); return(rep.AnyErrors ? 1 : 0); }