Visual Studio代码度量错误报告代码行数

7

Visual Studio 中的代码度量分析器以及代码度量工具会报告以下代码中 TestMethod 方法的代码行数为 8。

我最多只期望它将代码行数报告为 3。

[TestClass]
public class UnitTest1
{
    private void Test(out string str)
    {
        str = null;
    }

    [TestMethod]
    public void TestMethod()
    {
        var mock = new Mock<UnitTest1>();

        string str;
        mock.Verify(m => m.Test(out str));
    }
}

有人能解释一下这个情况吗?

更多信息

经过进一步的调查,我发现从Test方法中删除out参数并更新测试代码会导致LOC报告为2,我认为这是正确的。添加out会导致跳转,因此不是因为大括号或属性。

使用dotPeek反编译DLL会显示由于out参数而生成了相当多的附加代码,可以认为是8 LOC,但是删除参数并反编译也会显示生成的代码,可以认为是5 LOC,因此问题不仅仅是VS计算编译器生成的代码(我认为它本来就不应该这样做)。

3个回答

2

'代码行数'(LOC)有几种常见定义。每个定义都试图为我认为几乎没有意义的指标带来一些意义。例如,谷歌的有效代码行数(eLOC)。

我认为VS将属性包括在方法声明中,并尝试通过计算语句甚至花括号来给出eLOC。一个可能性是'm => m.Test(out str)'被计算为一个语句。

考虑这个:

if (a > 1 &&
    b > 2)
{
   var result;
   result = GetAValue();
   return result;
}

并且还有这个:

if (a> 1 && b >2)
   return GetAValue();

LOC的一个定义是计算任何代码行数,这甚至可能包括花括号。在这种极其简单化的定义中,代码风格对计数的影响非常大。

eLOC试图减少或消除代码风格的影响。例如,在某些情况下,声明可能被计算为“行”。这并不是为之辩解,仅仅是在说明问题。

考虑以下内容:

int varA = 0;
varA = GetAValue();

还有这个:

var varA = GetAValue();

两行还是一行?

这取决于意图。如果是为了测量需要多高的显示器,则可以使用简单的LOC。如果意图是为了测量复杂性,那么计算代码语句的数量会更好,例如eLOC。

如果想要测量复杂性,可以使用复杂度指标,如圆形复杂度。别担心VS如何测量LOC,因为我认为这是一个无用的指标。


谢谢你的回答。我也在捕捉圈复杂度,但我也想收集行数指标,因为我相信它有合理的用途,比如一个没有流程控制语句的1,000行方法可能是一个维护噩梦,即使它的圈复杂度是1。我知道计算行数存在一些歧义,但在这个特定情况下,我们从我认为最多只有3行代码跳到8行,这是一个很大的差异。我还有一个更大的方法被错误地报告为有159行代码,而实际上只有50行左右。 - Ian Newson
顺便提一下,从Test方法中删除out参数并更新测试代码会导致LOC报告为2,我认为这是正确的。添加out会导致跳转,因此不是由于大括号或属性引起的。 - Ian Newson

2
使用工具 NDepend,我们得到了 TestMethod() 的 2 行代码(LoC)。(免责声明:我是该工具的开发人员之一)。我撰写了一篇关于 如何计算代码行数 (LOC)? 的文章,阐明了什么是“逻辑” LoC,以及所有 .NET LoC 计数工具都依赖于“PDB 序列点”技术。
关于由VS度量提供的LoC值为8的猜测是,它包括由lambda表达式生成的方法的LoC + 包括与打开/结束大括号相关的PDB序列点(NDepend没有)。编译器做了很多花式操作,称为捕获局部变量str,但这不应影响从PDB序列点推断出的#LoC。
顺便说一下,我写了另外两篇相关的LoC文章:

谢谢你的回答。我可能会尝试使用NDepend,但我更愿意找到一个使用现有工具的解决方案。我不认为Lambda表达式是导致问题的原因,因为保留Lambda表达式但删除“out”参数会导致LOC报告为2。 - Ian Newson

0

我对Visual Studio的行数计算方式感到困惑,因为我看到的结果与报告的结果不一致。所以我写了一个小的C#控制台程序来计算纯代码行数,并将结果写入CSV文件中(见下文)。

打开一个新的解决方案,将其复制并粘贴到Program.cs文件中,构建可执行文件,然后你就可以开始使用了。这是一个.Net 3.5应用程序。将其复制到代码库的最顶层目录中。打开命令窗口并运行可执行文件。你会得到两个提示,第一个是程序/子系统的名称,第二个是你想要分析的任何额外文件类型。然后它会将结果写入当前目录下的CSV文件中。这是一个非常简单实用的工具,适合你的需求或交给管理层使用。

总之,这就是它,供参考,但效果因人而异:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.IO;

namespace CodeMetricsConsole
{
    class Program
    {
        // Concept here is that the program has a list of file extensions to do line counts on; it
        // gets any extra extensions at startup from the user. Then it gets a list of files based on
        // each extension in the current directory and all subdirectories. Then it walks through 
        // each file line by line and will display counts for that file and for that file extension.
        // It writes that information to a CSV file in the current directory. It uses regular expressions
        // on each line of each file to figure out what it's looking at, and how to count it (i.e. is it
        // a line of code, a single or multi line comment, a multi-line string, or a whitespace line).
        // 
        static void Main(string[] args)
        {
            try
            {
                Console.WriteLine(); // spacing

                // prompt user for subsystem or application name
                String userInput_subSystemName;
                Console.Write("Enter the name of this application or subsystem (required): ");
                userInput_subSystemName = Console.ReadLine();

                if (userInput_subSystemName.Length == 0)
                {
                    Console.WriteLine("Application or subsystem name required, exiting.");
                    return;
                }

                Console.WriteLine(); // spacing

                // prompt user for additional types
                String userInput_additionalFileTypes;
                Console.WriteLine("Default extensions are asax, css, cs, js, aspx, ascx, master, txt, jsp, java, php, bas");
                Console.WriteLine("Enter a comma-separated list of additional file extensions (if any) you wish to analyze");
                Console.Write(" --> ");
                userInput_additionalFileTypes = Console.ReadLine();

                // tell user processing is starting
                Console.WriteLine();
                Console.WriteLine("Getting LOC counts...");
                Console.WriteLine();

                // the default file types to analyze - hashset to avoid duplicates if the user supplies extensions
                HashSet allowedExtensions = new HashSet { "asax", "css", "cs", "js", "aspx", "ascx", "master", "txt", "jsp", "java", "php", "bas" };

                // Add user-supplied types to allowedExtensions if any
                String[] additionalFileTypes;
                String[] separator = { "," };
                if (userInput_additionalFileTypes.Length > 0)
                {
                    // split string into array of additional file types
                    additionalFileTypes = userInput_additionalFileTypes.Split(separator, StringSplitOptions.RemoveEmptyEntries);

                    // walk through user-provided file types and append to default file types
                    foreach (String ext in additionalFileTypes)
                    {
                        try
                        {
                            allowedExtensions.Add(ext.Trim()); // remove spaces
                        }
                        catch (Exception e)
                        {
                            Console.WriteLine("Exception: " + e.Message);
                        }
                    }
                }

                // summary file to write to
                String summaryFile = userInput_subSystemName + "_Summary.csv";
                String path = Directory.GetCurrentDirectory();
                String pathAndFile = path + Path.DirectorySeparatorChar + summaryFile;

                // regexes for the different line possibilities
                Regex oneLineComment = new Regex(@"^\s*//"); // match whitespace to two slashes
                Regex startBlockComment = new Regex(@"^\s*/\*.*"); // match whitespace to /*
                Regex whiteSpaceOnly = new Regex(@"^\s*$"); // match whitespace only
                Regex code = new Regex(@"\S*"); // match anything but whitespace
                Regex endBlockComment = new Regex(@".*\*/"); // match anything and */ - only used after block comment detected
                Regex oneLineBlockComment = new Regex(@"^\s*/\*.*\*/.*"); // match whitespace to /* ... */
                Regex multiLineStringStart = new Regex("^[^\"]*@\".*"); // match @" - don't match "@"
                Regex multiLineStringEnd = new Regex("^.*\".*"); // match double quotes - only used after multi line string start detected
                Regex oneLineMLString = new Regex("^.*@\".*\""); // match @"..."
                Regex vbaComment = new Regex(@"^\s*'"); // match whitespace to single quote

                // Uncomment these two lines to test your regex with the function testRegex() below
                //new Program().testRegex(oneLineMLString);
                //return;

                FileStream fs = null;
                String line = null;
                int codeLineCount = 0;
                int commentLineCount = 0;
                int wsLineCount = 0;
                int multiLineStringCount = 0;
                int fileCodeLineCount = 0;
                int fileCommentLineCount = 0;
                int fileWsLineCount = 0;
                int fileMultiLineStringCount = 0;
                Boolean inBlockComment = false;
                Boolean inMultiLineString = false;

                try
                {
                    // write to summary CSV file, overwrite if exists, don't append
                    using (StreamWriter outFile = new StreamWriter(pathAndFile, false))
                    {
                        // outFile header
                        outFile.WriteLine("filename, codeLineCount, commentLineCount, wsLineCount, mlsLineCount");

                        // walk through files with specified extensions
                        foreach (String allowed_extension in allowedExtensions)
                        {
                            String extension = "*." + allowed_extension;

                            // reset accumulating values for extension
                            codeLineCount = 0;
                            commentLineCount = 0;
                            wsLineCount = 0;
                            multiLineStringCount = 0;

                            // Get all files in current directory and subdirectories with specified extension
                            String[] fileList = Directory.GetFiles(Directory.GetCurrentDirectory(), extension, SearchOption.AllDirectories);

                            // walk through all files of this type
                            for (int i = 0; i < fileList.Length; i++)
                            {
                                // reset values for this file
                                fileCodeLineCount = 0;
                                fileCommentLineCount = 0;
                                fileWsLineCount = 0;
                                fileMultiLineStringCount = 0;
                                inBlockComment = false;
                                inMultiLineString = false;

                                try
                                {
                                    // open file
                                    fs = new FileStream(fileList[i], FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
                                    using (TextReader tr = new StreamReader(fs))
                                    {
                                        // walk through lines in file
                                        while ((line = tr.ReadLine()) != null)
                                        {
                                            if (inBlockComment)
                                            {
                                                if (whiteSpaceOnly.IsMatch(line))
                                                {
                                                    fileWsLineCount++;
                                                }
                                                else
                                                {
                                                    fileCommentLineCount++;
                                                }

                                                if (endBlockComment.IsMatch(line)) inBlockComment = false;
                                            }
                                            else if (inMultiLineString)
                                            {
                                                fileMultiLineStringCount++;

                                                if (multiLineStringEnd.IsMatch(line)) inMultiLineString = false;
                                            }
                                            else
                                            {
                                                // not in a block comment or multi-line string
                                                if (oneLineComment.IsMatch(line))
                                                {
                                                    fileCommentLineCount++;
                                                }
                                                else if (oneLineBlockComment.IsMatch(line))
                                                {
                                                    fileCommentLineCount++;
                                                }
                                                else if ((startBlockComment.IsMatch(line)) && (!(oneLineBlockComment.IsMatch(line))))
                                                {
                                                    fileCommentLineCount++;
                                                    inBlockComment = true;
                                                }
                                                else if (whiteSpaceOnly.IsMatch(line))
                                                {
                                                    fileWsLineCount++;
                                                }
                                                else if (oneLineMLString.IsMatch(line))
                                                {
                                                    fileCodeLineCount++;
                                                }
                                                else if ((multiLineStringStart.IsMatch(line)) && (!(oneLineMLString.IsMatch(line))))
                                                {
                                                    fileCodeLineCount++;
                                                    inMultiLineString = true;
                                                }
                                                else if ((vbaComment.IsMatch(line)) && (allowed_extension.Equals("txt") || allowed_extension.Equals("bas"))
                                                {
                                                    fileCommentLineCount++;
                                                }
                                                else
                                                {
                                                    // none of the above, thus it is a code line
                                                    fileCodeLineCount++;
                                                }
                                            }
                                        } // while

                                        outFile.WriteLine(fileList[i] + ", " + fileCodeLineCount + ", " + fileCommentLineCount + ", " + fileWsLineCount + ", " + fileMultiLineStringCount);

                                        fs.Close();
                                        fs = null;

                                    } // using
                                }
                                finally
                                {
                                    if (fs != null) fs.Dispose();
                                }

                                // update accumulating values
                                codeLineCount = codeLineCount + fileCodeLineCount;
                                commentLineCount = commentLineCount + fileCommentLineCount;
                                wsLineCount = wsLineCount + fileWsLineCount;
                                multiLineStringCount = multiLineStringCount + fileMultiLineStringCount;

                            } // for (specific file)

                            outFile.WriteLine("Summary for: " + extension + ", " + codeLineCount + ", " + commentLineCount + ", " + wsLineCount + ", " + multiLineStringCount);

                        } // foreach (all files with specified extension)

                    } // using summary file streamwriter

                    Console.WriteLine("Analysis complete, file is: " + pathAndFile);

                } // try block
                catch (Exception e)
                {
                    Console.WriteLine("Error: " + e.Message);
                }
            }
            catch (Exception e2)
            {
                Console.WriteLine("Error: " + e2.Message);
            }

        } // main


        // local testing function for debugging purposes
        private void testRegex(Regex rx)
        {
            String test = "        asdfasd asdf @\"     adf ++--// /*\" ";

            if (rx.IsMatch(test))
            {
                Console.WriteLine(" -->| " + rx.ToString() + " | matched: " + test);
            }
            else
            {
                Console.WriteLine("No match");
            }
        }

    } // class
} // namespace

这是它的工作原理:

  • 程序有一组您想要分析的文件扩展名。
  • 它遍历集合中的每个扩展名,在当前目录和所有子目录中获取该类型的所有文件。
  • 它选择每个文件,遍历该文件的每一行,将每一行与正则表达式进行比较以确定它正在查看什么,并在确定它正在查看什么后递增行计数。
  • 如果一行不是空格、单行或多行注释或多行字符串,则将其视为代码行。它报告每种类型行(代码、注释、空格、多行字符串)的所有计数,并将它们写入CSV文件中。无需解释为什么Visual Studio会或不会将某些内容视为代码行。

是的,其中嵌套了三个循环(O(n-cubed) O_O),但它只是一个简单的独立开发者工具,我运行它的最大代码库约为350K行,它在Core i7上运行大约需要10秒钟。

编辑:刚在Firefox 12代码库上运行了一下,大约有430万行(330万代码,100万注释),大约21K个文件,使用AMD Phenom处理器-花费了7分钟,在任务管理器的性能选项卡中观察,没有压力。供参考。

我的态度是,如果我写的是编译器指令的一部分,那么它就是一行代码,应该计入其中。

它可以很容易地定制忽略或计算任何你想要的内容(括号、命名空间、文件顶部的包含等)。只需添加正则表达式,使用正则表达式下面的函数进行测试,然后更新if语句中的正则表达式即可。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接