示例#1
0
        /// <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 &amp;&amp; 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&lt;CommandLine&gt;(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;
        }
示例#2
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 &amp;&amp; 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);
        }