【问题标题】:Visual Studio code metrics misreporting lines of codeVisual Studio 代码指标误报代码行
【发布时间】:2012-10-16 14:46:56
【问题描述】:

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 计算编译器生成的代码的问题(我不相信它应该这样做)。

【问题讨论】:

    标签: visual-studio out code-metrics lines-of-code


    【解决方案1】:

    “代码行”(LOC) 有几种常见定义。每个人都试图为我认为几乎毫无意义的指标带来一些意义。例如 google 的有效代码行 (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 的,因为我认为无论如何它都是一个无用的指标。​​

    【讨论】:

    • 感谢您的回答。我也在捕捉圈复杂度,但我确实想收集 LOC 指标,并且我相信它具有合法用途,例如没有流控制语句的 1,000 行方法可能是维护的噩梦,即使它的 CC 为 1。我知道 LOC 的计算方式存在一些歧义,但在这种特定情况下,我们谈论的是从我认为最多 3 个 LOC 到 8 个,这是一个很大的区别。我有一个更大的方法,它也被误报为 159 LOC,而实际上它更像是 50。
    • 顺便说一句,从Test方法中删除out参数并更新测试代码会导致LOC报告为2,我相信这是正确的。 out 的添加导致跳转,所以不是因为大括号或属性。
    【解决方案2】:

    使用NDepend 工具,我们得到TestMethod() 的# Lines of Code (LoC) 为2。 (免责声明我是该工具的开发者之一)。我写了一篇关于How do you count your number of Lines Of Code (LOC) ? 的文章,阐明了什么是逻辑 LoC,以及所有 .NET LoC 计数工具如何依赖PDB 序列点 技术。

    我对 VS 度量提供的 LoC 值 8 的猜测是,它包括由 lambda 表达式生成的方法的 LoC + 它包括与打开/结束大括号相关的 PDB 序列点(NDepend 没有) .编译器还进行了很多操作来执行所谓的capturing the local variablestr,但这不应该影响从 PDB 序列点推断的#LoC。

    顺便说一句,我写了另外 2 篇相关的 LoC 文章:

    【讨论】:

    • 感谢您的回答。我可以试试 NDepend,但我更愿意用我现有的工具找到解决方案。我不相信 lambda 会导致问题,因为保留 lambda 但删除 out 参数会导致 LOC 报告为 2。
    【解决方案3】:

    我想知道 Visual Studio 行数以及为什么我看到的不是报告的内容。因此,我编写了一个小型 C# 控制台程序来计算纯代码行数并将结果写入 CSV 文件(见下文)。

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

    Anyhoo,这里是 FWIW 和 YMMV:

    
    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
    
    

    它是这样工作的:

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

    是的,三个循环相互嵌入(O(n-cubed) O_O),但它只是一个简单的独立开发工具,我运行过的最大代码库大约是 350K 行,它花了在 Core i7 上运行 10 秒。

    编辑:刚刚在 Firefox 12 代码库上运行,大约 430 万行(3.3M 代码,1M cmets),大约 21K 文件,使用 AMD Phenom 处理器 - 耗时 7 分钟,观看任务管理器中的性能选项卡,没有压力。供参考。

    我的态度是,如果我将其编写为输入给编译器的指令的一部分,那么它就是一行代码,应该被计算在内。

    它可以轻松自定义为忽略或计算您想要的任何内容(括号、命名空间、文件顶部的包含等)。只需添加正则表达式,使用正则表达式下方的函数对其进行测试,然后使用该正则表达式更新 if 语句。

    【讨论】:

      猜你喜欢
      • 2011-06-02
      • 2011-04-15
      • 2018-05-24
      • 2019-04-14
      • 2011-04-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-04-09
      相关资源
      最近更新 更多