使用 [ReadOnly] 属性标记参数并让分析器断言方法主体中没有重新分配
Mark parameters with a [ReadOnly] attribute and let an analyzer assert, that no reassignment is in the method body
无法将方法的参数标记为只读,因此无法在方法中重新分配,我开始考虑为此创建一个分析器。该参数将归因于
[AttributeUsage(AttributeTargets.Parameter)]
public class ReadOnlyAttribute: Attribute
{
// ...
}
方法的签名是
public class Foo
{
public void Bar([ReadOnly] MyObject o)
{
o.DoSomething() // this is OK
o = new MyObject() // this would be flagged by the analyzer
}
}
到目前为止,我拥有的是 DiagnosticAnalyzer class,其中包含以下成员:
[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
}
}
}
}
}
如何在方法体内找到参数的所有引用?我怎么知道其中哪一项是作业?
我建议您获取当前 IMethodSymbol 的 SyntaxNode(如果它具有 "ReadOnlyAttribute" 参数),然后从 SyntaxNode 获取所有属于 AssignmentExpressionSyntax 的后代节点。所以,你只需要比较语句的标识符和你的参数。如果您有任何问题,请告诉我。
...
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(location.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()));
}
}
}
无法将方法的参数标记为只读,因此无法在方法中重新分配,我开始考虑为此创建一个分析器。该参数将归因于
[AttributeUsage(AttributeTargets.Parameter)]
public class ReadOnlyAttribute: Attribute
{
// ...
}
方法的签名是
public class Foo
{
public void Bar([ReadOnly] MyObject o)
{
o.DoSomething() // this is OK
o = new MyObject() // this would be flagged by the analyzer
}
}
到目前为止,我拥有的是 DiagnosticAnalyzer class,其中包含以下成员:
[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
}
}
}
}
}
如何在方法体内找到参数的所有引用?我怎么知道其中哪一项是作业?
我建议您获取当前 IMethodSymbol 的 SyntaxNode(如果它具有 "ReadOnlyAttribute" 参数),然后从 SyntaxNode 获取所有属于 AssignmentExpressionSyntax 的后代节点。所以,你只需要比较语句的标识符和你的参数。如果您有任何问题,请告诉我。
...
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(location.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()));
}
}
}