Mark parameters with a [ReadOnly] attribute and le

2019-09-20 00:35发布

Not being able to mark a parameter of a method as readonly so that it is impossible to be reassigned in the method, I started thinking abount creating an analyzer for this. The parameter would be attributed with

[AttributeUsage(AttributeTargets.Parameter)]
public class ReadOnlyAttribute: Attribute
{
  // ...
}

and the signature of a method would be

public class Foo
{
  public void Bar([ReadOnly] MyObject o)
  {
    o.DoSomething()    // this is OK
    o = new MyObject() // this would be flagged by the analyzer
  }
}

What I have so far is an DiagnosticAnalyzer class with these members:

[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal class MyDiagnosticAnalyzer : DiagnosticAnalyzer
{
  private static readonly DiagnosticDescriptor Descriptor =
    new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Info, true, Description, HelpLink);

  public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Descriptor);

  public override void Initialize(AnalysisContext context)
  {
    context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.Method);
  }

  private static void AnalyzeSymbol(SymbolAnalysisContext context)
  {
    var methodSymbol = context.Symbol as IMethodSymbol;
    var parameters = methodSymbol.Parameters;

    foreach (var p in parameters)
    {
      var attr = p.GetAttributes();

      foreach (var a in attr)
      {
        if (a.AttributeClass.Name == "ReadOnlyAttribute")
        {
          // now I have to find all references of p in order to check if any of them is a assignment
        }
      }
    }
  }
}

How do I find all reference of a parameter within the method body? And how do I know which one of them are assigments?

1条回答
ゆ 、 Hurt°
2楼-- · 2019-09-20 01:08

I sugges you get SyntaxNode for current IMethodSymbol if it has "ReadOnlyAttribute" parameter then get all descendant nodes from SyntaxNode that are AssignmentExpressionSyntax. So, you only need to compare statement' identifiers and your parameter. Give me know if you have some questions.

...
var methodSymbol = context.Symbol as IMethodSymbol;;
var parameters = methodSymbol.Parameters;
if (!parameters.SelectMany(p => p.GetAttributes()).Any(s => s.AttributeClass.Name == "ReadOnlyAttribute"))
{
    // So you don't have params with ReadOnly attribute
    return;
}

// So you have params with ReadOnly attribute
var location = methodSymbol.Locations.FirstOrDefault();
if (location == null)
{
    // throw or return 
}

// Can cahce CompilationRoot
var methodNode = location.SourceTree.GetCompilationUnitRoot().FindNode(locati‌​on.SourceSpan);
var childNodes = methodNode.ChildNodes().ToList();
// Expression-bodied memeber
var blockNode = childNodes.FirstOrDefault(s => s is BlockSyntax) ?? childNodes.FirstOrDefault(s => s is ArrowExpressionClauseSyntax);
if (blockNode == null)
{
    // throw or return 
}

// You can convert this linq to foreach and improve performance removing a where functions 
var assignmentIndetifiers = blockNode.DescendantNodes()
    .Select(s => s as AssignmentExpressionSyntax)
    .Where(s => s != null)
    .Select(s => s.Left as IdentifierNameSyntax)
    .Where(s => s != null)
    .Select(s => s.Identifier)
    .ToList();

// You retrive all left identifiers in assignment expressions from current block syntax and its descendant nodes
// You only need to check if assignmentIdentifiers contains your readonly parameter
// For example, you can compare by name (you can use custom equality comparer)
var names = assignmentIndetifiers.ToLookup(s => s.ValueText, EqualityComparer<string>.Default);
foreach (var parameter in parameters)
{
    if (names.Contains(parameter.Name))
    {
        foreach (var token in names[parameter.Name])
        {
            // throw that readonly argument is a assignment
            context.ReportDiagnostic(Diagnostic.Create(rule, token.GetLocation()));
        }
    }
}
查看更多
登录 后发表回答