如何在C#项目(解决方案)中找到所有硬编码的值?

8

这个问题不仅仅是询问硬编码的字符串,还包括魔数等内容。

有没有一种方法可以在VS中查找C#项目/解决方案中所有的硬编码数值,例如字符串、魔数等等?

引发这个问题的原因是我正在查看一个项目,我刚刚发现174次重复出现的字符串值是硬编码的!


5
使用 Ctrl + Shift + FRegEx 查找所有匹配的结果。 - VMAtm
4
@Arjang 这个问题非常复杂,也许非常非常复杂。尝试使用resharper。否则我甚至不知道该从哪里开始...也许可以使用自定义规则和代码分析。 - xanatos
1
@Arjang 可能会有一个大问题:如果你从另一个程序集中引用了一个 const string Foo = "foo",那么 "foo" 的值将被直接编译而不是被引用,因此查看生成的 dll/exe 的工具无法区分硬编码的 "foo" 和正确引用的 Foo(参见 const由于编译器会传播常量,所以使用你的库编译的其他代码将需要重新编译才能看到更改)。 - xanatos
3
@xanatos 是的,Resharper 可以做到。当 Resharper 发现可本地化的字符串时,只需轻轻点击几下即可帮助您将其移动到资源文件中。您可以选择搜索相同的字符串并重构它们以使用新的资源项。 - user585968
5
理论上,您可以使用以下正则表达式来查找数字(至少为两位数)、十六进制数字、字符串和字符:"(\\.|[^"])*"|'(\\.|.)'|\b0[Xx][A-Za-z0-9]+\b|\b[0-9]{2,}\b。在 Visual Studio 中使用正则表达式选项进行搜索。问题是无法区分注释/XML 注释和代码。 - xanatos
显示剩余6条评论
2个回答

6
你可以编写 Roslyn,这是一个新的流行工具。它可以轻松解析C#(或VB.NET)项目。然后,您可以访问检测到的节点并检查您真正想要检查的内容。对于机器来说,检测魔法数字并不总是像人类那样容易。例如,1真的是一个魔法数字吗?我个人认为不是,但2就更可疑了...

无论如何,下面是一个小样本,可以很好地完成部分工作,但它可能/应该得到改进,以适应您的业务需求或规则(这非常有趣)。

请注意,Roslyn也可以直接在Visual Studio上下文中使用,因此您可以将此示例转换为所谓的诊断(Visual Studio的扩展),从而可以直接在IDE中实时帮助您。这方面有一些示例:示例和演练

class Program
{
    static void Main(string[] args)
    {
        var text = @" 
public class MyClass 
{ 
public void MyMethod() 
{ 
    const int i = 0; // this is ok
    decimal d = 11; // this is not ok
    string s = ""magic"";
    if (i == 29) // another magic
    {
    }
    else if (s != ""again another magic"")
    {
    }
}
}";
        ScanHardcodedFromText("test.cs", text, (n, s) =>
        {
            Console.WriteLine(" " + n.SyntaxTree.GetLineSpan(n.FullSpan) + ": " + s);
        }).Wait();
    }

    public static async Task ScanHardcodedFromText(string documentName, string text, Action<SyntaxNodeOrToken, string> scannedFunction)
    {
        if (text == null)
            throw new ArgumentNullException("text");

        AdhocWorkspace ws = new AdhocWorkspace();
        var project = ws.AddProject(documentName + "Project", LanguageNames.CSharp);
        ws.AddDocument(project.Id, documentName, SourceText.From(text));
        await ScanHardcoded(ws, scannedFunction);
    }

    public static async Task ScanHardcodedFromSolution(string solutionFilePath, Action<SyntaxNodeOrToken, string> scannedFunction)
    {
        if (solutionFilePath == null)
            throw new ArgumentNullException("solutionFilePath");

        var ws = MSBuildWorkspace.Create();
        await ws.OpenSolutionAsync(solutionFilePath);
        await ScanHardcoded(ws, scannedFunction);
    }

    public static async Task ScanHardcodedFromProject(string solutionFilePath, Action<SyntaxNodeOrToken, string> scannedFunction)
    {
        if (solutionFilePath == null)
            throw new ArgumentNullException("solutionFilePath");

        var ws = MSBuildWorkspace.Create();
        await ws.OpenProjectAsync(solutionFilePath);
        await ScanHardcoded(ws, scannedFunction);
    }

    public static async Task ScanHardcoded(Workspace workspace, Action<SyntaxNodeOrToken, string> scannedFunction)
    {
        if (workspace == null)
            throw new ArgumentNullException("workspace");

        if (scannedFunction == null)
            throw new ArgumentNullException("scannedFunction");

        foreach (var project in workspace.CurrentSolution.Projects)
        {
            foreach (var document in project.Documents)
            {
                var tree = await document.GetSyntaxTreeAsync();
                var root = await tree.GetRootAsync();
                foreach (var n in root.DescendantNodesAndTokens())
                {
                    if (!CanBeMagic(n.Kind()))
                        continue;

                    if (IsWellKnownConstant(n))
                        continue;

                    string suggestion;
                    if (IsMagic(n, out suggestion))
                    {
                        scannedFunction(n, suggestion);
                    }
                }
            }
        }
    }

    public static bool IsMagic(SyntaxNodeOrToken kind, out string suggestion)
    {
        var vdec = kind.Parent.Ancestors().OfType<VariableDeclarationSyntax>().FirstOrDefault();
        if (vdec != null)
        {
            var dec = vdec.Parent as MemberDeclarationSyntax;
            if (dec != null)
            {
                if (!HasConstOrEquivalent(dec))
                {
                    suggestion = "member declaration could be const: " + dec.ToFullString();
                    return true;
                }
            }
            else
            {
                var ldec = vdec.Parent as LocalDeclarationStatementSyntax;
                if (ldec != null)
                {
                    if (!HasConstOrEquivalent(ldec))
                    {
                        suggestion = "local declaration contains at least one non const value: " + ldec.ToFullString();
                        return true;
                    }
                }
            }
        }
        else
        {
            var expr = kind.Parent.Ancestors().OfType<ExpressionSyntax>().FirstOrDefault();
            if (expr != null)
            {
                suggestion = "expression uses a non const value: " + expr.ToFullString();
                return true;
            }
        }

        // TODO: add other cases?

        suggestion = null;
        return false;
    }

    private static bool IsWellKnownConstant(SyntaxNodeOrToken node)
    {
        if (!node.IsToken)
            return false;

        string text = node.AsToken().Text;
        if (text == null)
            return false;

        // note: this is naïve. we also should add 0d, 0f, 0m, etc.
        if (text == "1" || text == "-1" || text == "0")
            return true;

        // ok for '\0' or '\r', etc.
        if (text.Length == 4 && text.StartsWith("'\\") && text.EndsWith("'"))
            return true;

        if (text == "' '")
            return true;

        // TODO add more of these? or make it configurable...

        return false;
    }

    private static bool HasConstOrEquivalent(SyntaxNode node)
    {
        bool hasStatic = false;
        bool hasReadOnly = false;
        foreach (var tok in node.ChildTokens())
        {
            switch (tok.Kind())
            {
                case SyntaxKind.ReadOnlyKeyword:
                    hasReadOnly = true;
                    if (hasStatic)
                        return true;
                    break;

                case SyntaxKind.StaticKeyword:
                    hasStatic = true;
                    if (hasReadOnly)
                        return true;
                    break;

                case SyntaxKind.ConstKeyword:
                    return true;
            }
        }
        return false;
    }

    private static bool CanBeMagic(SyntaxKind kind)
    {
        return kind == SyntaxKind.CharacterLiteralToken ||
            kind == SyntaxKind.NumericLiteralToken ||
            kind == SyntaxKind.StringLiteralToken;
    }
}

如果您运行这个小程序(我还提供了一些帮助方法来使用它在解决方案或项目上),它会输出以下内容:
 test.cs: (6,20)-(6,22): local declaration contains at least one non const value:         decimal d = 11; // this is not ok

 test.cs: (7,19)-(7,26): local declaration contains at least one non const value:         string s = "magic";

 test.cs: (8,17)-(8,19): expression uses a non const value: i == 29
 test.cs: (11,22)-(11,43): expression uses a non const value: s != "again another magic"

0
我有一些可以找到魔数和硬编码的非常量字符串的代码。也许这能对某人有所帮助 -
/// <summary>
/// Scans all cs files in the solutions for magic strings and numbers using the Roslyn 
/// compiler and analyzer tools.
/// Based upon a Roslyn code sample.
/// </summary>
class MagicStringAnalyzer
{
    protected static Filter filter;

    static void Main(string[] args)
    {

        string outputPath = @"E:\output.txt";
        string solutionPath = @"E:\Solution.sln";

        filter = new Filter(@"E:\IgnorePatterns.txt");

        if (File.Exists(outputPath))
        {
            OverWriteFile(outputPath);
        }

        analyzeSolution(outputPath, solutionPath);

    }

    protected static void loadFilters()
    {

    }

    private static void OverWriteFile(string path)
    {
        Console.WriteLine("Do you want to overwrite existing output file? (y/n)");

        if (Console.ReadKey().Key == ConsoleKey.Y)
        {
            File.Delete(path);
            Console.WriteLine("");

        }
        else
        {
            Environment.Exit(-1);
        }
    }

    public static void analyzeSolution(string outputPath, string solutionPath)
    {
        Console.WriteLine("Analyzing file...");


        System.IO.StreamWriter writer = new System.IO.StreamWriter(outputPath);

        ScanHardcodedFromSolution(solutionPath, (n, s) =>
        {
            string syntaxLineSpan = n.SyntaxTree.GetLineSpan(n.FullSpan).ToString();

            if (!filter.IsMatch(syntaxLineSpan))
            {
                writer.WriteLine(" " + syntaxLineSpan + ": \r\n" + s + "\r\n\r\n");
            }
        }).Wait();


        writer.Close();
    }

    public static async Task ScanHardcodedFromText(string documentName, string text, Action<SyntaxNodeOrToken, string> scannedFunction)
    {
        if (text == null)
            throw new ArgumentNullException("text");

        AdhocWorkspace ws = new AdhocWorkspace();
        var project = ws.AddProject(documentName + "Project", LanguageNames.CSharp);
        ws.AddDocument(project.Id, documentName, SourceText.From(text));
        await ScanHardcoded(ws, scannedFunction);
    }

    public static async Task ScanHardcodedFromSolution(string solutionFilePath, Action<SyntaxNodeOrToken, string> scannedFunction)
    {
        if (solutionFilePath == null)
            throw new ArgumentNullException("solutionFilePath");

        var ws = MSBuildWorkspace.Create();
        await ws.OpenSolutionAsync(solutionFilePath);
        await ScanHardcoded(ws, scannedFunction);
    }

    public static async Task ScanHardcodedFromProject(string solutionFilePath, Action<SyntaxNodeOrToken, string> scannedFunction)
    {
        if (solutionFilePath == null)
            throw new ArgumentNullException("solutionFilePath");

        var ws = MSBuildWorkspace.Create();
        await ws.OpenProjectAsync(solutionFilePath);
        await ScanHardcoded(ws, scannedFunction);
    }

    public static async Task ScanHardcoded(Workspace workspace, Action<SyntaxNodeOrToken, string> scannedFunction)
    {
        if (workspace == null)
            throw new ArgumentNullException("workspace");

        if (scannedFunction == null)
            throw new ArgumentNullException("scannedFunction");

        foreach (var project in workspace.CurrentSolution.Projects)
        {
            foreach (var document in project.Documents)
            {
                var tree = await document.GetSyntaxTreeAsync();
                var root = await tree.GetRootAsync();
                foreach (var n in root.DescendantNodesAndTokens())
                {
                    if (!CanBeMagic(n.Kind()))
                        continue;

                    if (IsWellKnownConstant(n))
                        continue;

                    string suggestion;
                    if (IsMagic(n, out suggestion))
                    {
                        scannedFunction(n, suggestion);
                    }
                }
            }
        }
    }

    public static bool IsMagic(SyntaxNodeOrToken kind, out string suggestion)
    {
        var vdec = kind.Parent.Ancestors().OfType<VariableDeclarationSyntax>().FirstOrDefault();
        if (vdec != null)
        {
            var dec = vdec.Parent as MemberDeclarationSyntax;
            if (dec != null)
            {
                if (!HasConstOrEquivalent(dec))
                {
                    suggestion = "member declaration could be const: " + dec.ToFullString();
                    return true;
                }
            }
            else
            {
                var ldec = vdec.Parent as LocalDeclarationStatementSyntax;
                if (ldec != null)
                {
                    if (!HasConstOrEquivalent(ldec))
                    {
                        suggestion = "local declaration contains at least one non const value: " + ldec.ToFullString();
                        return true;
                    }
                }
            }
        }
        else
        {
            var expr = kind.Parent.Ancestors().OfType<ExpressionSyntax>().FirstOrDefault();
            if (expr != null)
            {
                suggestion = "expression uses a non const value: " + expr.ToFullString();
                return true;
            }
        }

        // TODO: add other cases?

        suggestion = null;
        return false;
    }

    private static bool IsWellKnownConstant(SyntaxNodeOrToken node)
    {
        if (!node.IsToken)
            return false;

        string text = node.AsToken().Text;
        if (text == null)
            return false;

        // note: this is naïve. we also should add 0d, 0f, 0m, etc.
        if (text == "1" || text == "-1" || text == "0")
            return true;

        // ok for '\0' or '\r', etc.
        if (text.Length == 4 && text.StartsWith("'\\") && text.EndsWith("'"))
            return true;

        if (text == "' '")
            return true;

        if (text == "")
            return true;

        return false;
    }

    private static bool HasConstOrEquivalent(SyntaxNode node)
    {
        bool hasStatic = false;
        bool hasReadOnly = false;
        foreach (var tok in node.ChildTokens())
        {
            switch (tok.Kind())
            {
                case SyntaxKind.ReadOnlyKeyword:
                    hasReadOnly = true;
                    if (hasStatic)
                        return true;
                    break;

                case SyntaxKind.StaticKeyword:
                    hasStatic = true;
                    if (hasReadOnly)
                        return true;
                    break;

                case SyntaxKind.ConstKeyword:
                    return true;
            }
        }
        return false;
    }

    private static bool CanBeMagic(SyntaxKind kind)
    {
        return kind == SyntaxKind.CharacterLiteralToken ||
            kind == SyntaxKind.NumericLiteralToken ||
            kind == SyntaxKind.StringLiteralToken;
    }
}


public class Filter
{

    protected string[] patterns;

    public Filter(string path)
    {
        loadFilters(path);
    }

    protected void loadFilters(string path)
    {
        patterns = File.ReadAllLines(path);
    }

    public bool IsMatch(string input)
    {
        foreach (string pattern in patterns)
        {
            if(Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase))
            {
                return true;
            }
        }

        return false;
    }
}

你的文本文件包含需要忽略的文件名,其值类似于 -

Constant.cs
Resoures.Designer.cs
Configuration.cs
Reference.cs
Test

在解决方案路径中给出您的解决方案名称并运行此命令。这将为您生成包含所有硬编码字符串和魔术数字的txt文件。

编辑:

要编译项目,您需要将Microsoft.CodeAnalysis NuGet软件包安装到控制台应用程序项目中:

Install-Package Microsoft.CodeAnalysis -Pre

这里是你的 Program.cs 文件中应该包含的完整引用列表:

using System;
using System.Linq;
using System.Threading.Tasks;
using System.IO;
using System.Text.RegularExpressions;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.MSBuild;
using Microsoft.CodeAnalysis.Text;

namespace MagicStringAnalyzer
{
   // the rest of the code goes here...
}

有一些类缺失,比如SyntaxKind、ExpressionSyntax等,因此它无法使用。 - Latrova

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