8

This question is not asking about hard coded strings only, but magic numbers etc. as well.

Is there a way to find all the hard coded values i.e. string , magic numbers and what not in C# project/solution in VS?

What prompted this question is a project that I am looking at, I just found 174 times a string value was hardcodely repeated!

jimjim
  • 2,414
  • 2
  • 26
  • 46
  • 5
    Find All with `Ctrl + Shift + F` with `RegEx` – VMAtm Apr 09 '15 at 08:52
  • @Arjang:use resharper – santosh singh Apr 09 '15 at 08:52
  • 4
    @Arjang Quite complex... probably very complex... Try resharper. Otherwise I don't even know where I would begin... Perhaps a custom rule with codeanalysis – xanatos Apr 09 '15 at 08:56
  • @xanatos : would it be down the NDepend's ally? – jimjim Apr 09 '15 at 08:57
  • 1
    @Arjang Probably... There is a big problem: if you reference a `const string Foo = "foo"` from another assembly, the "foo" value is compiled directly and not referenced, so tools that look at the generated dll/exe can't distinguish between an hardcoded "foo" and a correctly referenced `Foo` (see [const](https://msdn.microsoft.com/en-us/library/e6w8fe1b.aspx?f=255&MSPPError=-2147217396): *and because compilers propagate constants, other code compiled with your libraries will have to be recompiled to see the changes*) – xanatos Apr 09 '15 at 09:02
  • 3
    @xanatos Yes, Resharper seems to do it. _[When ReSharper finds a localizable string, it helps you move it to a resource file with only a couple of clicks. You can **optionally search** for identical strings and refactor them to use the new resource item](https://www.jetbrains.com/resharper/features/internationalization.html)_. –  Apr 09 '15 at 09:06
  • possible duplicate of [Find all source hardcoded strings](http://stackoverflow.com/questions/2490974/find-all-source-hardcoded-strings) – Ebad Masood Apr 09 '15 at 09:14
  • @ebad86 : nop, I saw that question, I also want to know about magic numbers etc. – jimjim Apr 09 '15 at 09:15
  • 5
    Theorically you can use: `"(\\.|[^"])*"|'(\\.|.)'|\b0[Xx][A-Za-z0-9]+\b|\b[0-9]{2,}\b` to find numbers (of at least 2 digits), hex digits, strings, chars. Use it in Visual Studio with the Regular Expression option checked. Problem: can't distinguish between comments/xml comments and code. – xanatos Apr 09 '15 at 09:46
  • Resharper via http://stackoverflow.com/questions/2490974/find-all-source-hardcoded-strings. Shows as errors all hardcoded values. – Tamas Ionut Oct 26 '15 at 12:54
  • Related: http://stackoverflow.com/questions/337432/looking-for-a-regular-expression-to-identify-hard-coded-magic-numbers-in-source – nekketsuuu Feb 09 '17 at 08:03

2 Answers2

6

What you could do is program Roslyn, the (not so) new cool kid in town. It allows you to parse C# (or VB.NET) projects quite easily. Then you can visit the detected nodes and check what you really want to check. Detecting magic literals for a machine is not always as easy as it seems for a human. For example, is 1 really a magic number? I personally consider it's not, but 2 is more suspect...

Anyway, here is a small sample that does a good part of the job I believe, but it could/should be improved, maybe to tailor your exact business needs or rules (which is very interesting).

Note Roslyn can also be used directly in the context of Visual Studio, so you could turn this sample into what's called a diagnostic (an extension to Visual Studio) that can help you directly live from within the IDE. There are samples for this: Samples and Walkthroughs

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;
    }
}

If you run this little program (I've also provided helper methods to use it on solution or projects), it will output this:

 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"
Simon Mourier
  • 132,049
  • 21
  • 248
  • 298
0

I have some code which can find magic numbers and hard coded non-constant strings. May be that can help someone -

/// <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;
    }
}

Your txt file that contains file names to ignore would contain values like -

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

Give name of your solution in solution path and run this. This will generate txt file for you with all hard coded strings and magic numbers.

Edit:

To compile the project, you'll need to install Microsoft.CodeAnalysis NuGet package into your console app project:

Install-Package Microsoft.CodeAnalysis -Pre

Here is a complete list of references you should have in your 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...
}
Dan
  • 3
  • 4
darkenergy
  • 103
  • 11