Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Force a narrow implicit coercion at compile time

I'm trying to define a struct which uses a variable with a restricted range of numbers, and implicit coercion from ints. I'd like to be able to force build errors if any constants or other hardcoded values are used with this struct.

Here is an example of what I'm trying to accomplish.

    byte a = 123; // Allowed
    byte b = 123123; // Not allowed
    const int x = 123;
    const int y = 123123;
    byte c = x; // Allowed
    byte d = y; // Not allowed

I would ideally like to be able to, for example, restrict a number from 1 to 99, so that MyStruct s = 50; works but MyStruct s = 150; causes a compile time error like the bytes b and d above do.

I found something similar for a different language, but not for C#.

like image 590
user3657661 Avatar asked Sep 30 '15 18:09

user3657661


1 Answers

I think you can do this by using custom attributes and roslyn code analyses. Let me sketch a solution. This should at least solve the first usecase where you initialize with a literal.

First you would need a custom attribute that applies to your struct to allow the code analyses to be able to know the valid range:

[AttributeUsage(System.AttributeTargets.Struct)]
public class MinMaxSizeAttribute : Attribute
{
    public int MinVal { get; set;}
    public int MaxVal { get; set;}
    public MinMaxSizeAttribute()
    {
    }
}

What you do here is you store the min and max value in an attribute. That way you can use this later in the source code analyses.

Now apply this attribute to the struct declaration:

[MinMaxSize(MinVal = 0, MaxVal = 100)]
public struct Foo
{
    //members and implicit conversion operators go here
}

Now the type information for the struct Foo contains the value range. The next thing you need is a DiagnosticAnalyzer to analyze your code.

public class MyAnalyzer : DiagnosticAnalyzer
{
    internal static DiagnosticDescriptor Rule = new DiagnosticDescriptor("CS00042", 
        "Value not allowed here",
        @"Type {0} does not allow Values in this range", 
        "type checker", 
        DiagnosticSeverity.Error,
        isEnabledByDefault: true, description: "Value to big");
    public MyAnalyzer()
    {
    }

    #region implemented abstract members of DiagnosticAnalyzer

    public override void Initialize(AnalysisContext context)
    {
        context.RegisterSyntaxNodeAction(AnalyzeSyntaxTree, SyntaxKind.SimpleAssignmentExpression);
    }

    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

    #endregion

    private static void AnalyzeSyntaxTree(SyntaxNodeAnalysisContext context)
    {

    }
}

This is the bare bone skeleton to participate in code analyzes. The analyzer registers to analyze assignments:

context.RegisterSyntaxNodeAction(AnalyzeSyntaxTree, SyntaxKind.SimpleAssignmentExpression);

For variable declarations you would need to register for a different SyntaxKind but for simplicity I will stick to one here.

Lets have a look at the analyses logic:

private static void AnalyzeSyntaxTree(SyntaxNodeAnalysisContext context)
        {
            if (context.Node.IsKind(SyntaxKind.SimpleAssignmentExpression))
            {
                var assign = (AssignmentExpressionSyntax)context.Node;
                var leftType = context.SemanticModel.GetTypeInfo(assign.Left).GetType();
                var attr = leftType.GetCustomAttributes(typeof(MinMaxSizeAttribute), false).OfType<MinMaxSizeAttribute>().FirstOrDefault();
                if (attr != null && assign.Right.IsKind(SyntaxKind.NumericLiteralExpression))
                {
                    var numLitteral = (LiteralExpressionSyntax)assign.Right;
                    var t = numLitteral.Token;
                    if (t.Value.GetType().Equals(typeof(int)))
                    {
                        var intVal = (int)t.Value;
                        if (intVal > attr.MaxVal || intVal < attr.MaxVal)
                        {
                            Diagnostic.Create(Rule, assign.GetLocation(), leftType.Name);
                        }
                    }
                }
            }
        }

What the analyzer does is, is checking if the type on the left side has a MinMaxSize associated with it and if so it checks if the right side is a literal. When it is a literal it tries to get the integer value and compares it to the MinVal and MaxVal associated with the type. If the values exceeds that range it will report a diagnostics error.

Please note that all this code is mostly untested. It compiles and passed some basic tests. But it is only meant to illustrate a possible solution. For further information have a look at the Rsolyn Docs

The second case you want to covers is more complex because you will need to apply dataflow analyzes to get the value of x.

like image 185
Kolja Avatar answered Oct 19 '22 04:10

Kolja