12

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#.

Community
  • 1
  • 1
user3657661
  • 306
  • 3
  • 13
  • its not possible. byte is a type with range of 255. i dont think you can limit this in compile time or to create custom type. – M.kazem Akhgary Sep 30 '15 at 18:26
  • @M.kazemAkhgary It could be possible by modifying Roslyn, though I am not sure how hard or reasonable that would be – Dmytro Shevchenko Sep 30 '15 at 18:28
  • Interesting question! In Visual Studio 2013, if I put in a literal value that is too large, the Intellisense knows. I wonder if there's a way to define a class with similar Intellisense support or if that's baked in. – Doug Dawson Sep 30 '15 at 18:29
  • You could use a custom Enum, it would be a pain in the butt though :) – Derek Sep 30 '15 at 19:06
  • @Derek unfortunately enum accepts any value in range of int. – M.kazem Akhgary Sep 30 '15 at 19:07
  • Why not analysing the input values in the constructor by accounting for as many conditions as required (value > 0 && value < 150 and so on) and throwing an exception if they are not met? – varocarbas Sep 30 '15 at 19:08
  • @varocarbas that would be run time error. And happens when the code is executed. OP wants compile time check and exception before the code is executed. – M.kazem Akhgary Sep 30 '15 at 19:11
  • 1
    @M.kazemAkhgary Yes, I know. But I am wondering why. What would the problem with that? – varocarbas Sep 30 '15 at 19:12
  • Ye. I agree. Maybe in a teamwork would be useful. Not to let others make mistakes. However it can be done with custom warnings using directives #. Also resharper supports custom error messages. But im not sure if its possible to generate error for number ranges. Also you can write some documentation on struct using /// @varocarbas – M.kazem Akhgary Sep 30 '15 at 19:16
  • 2
    I've done a bunch of research and I believe this may be possible using a visual studio plugin that meddles around with compiler directives. This is ultimately, way too much effort when I can just clamp the number or throw a runtime exception. I see that Microsoft allows you to impose narrowing constraints on generic types, i.e. I can demand a generic T where T must be something specific, but you can't do this for actual data, just types. Would be nice if I could define an implicit operator with something like (int x.Where(x < 100)) Might be something worth requesting. – user3657661 Oct 02 '15 at 20:30

1 Answers1

1

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.

Kolja
  • 2,307
  • 15
  • 23