I have a toolkit that has many methods often taking Expression<Func<T,TProperty>>
as parameters. Some can be single-level only (o=>o.Name
), while some can be multi-level (o=>o.EmployeeData.Address.Street
).
I want to develop something (MSBuild Task? Visual Studio Plugin? hopefully the first) that reads all the user's .cs files, and gives build errors if the given parameter is not a property-expression (but something like o=>o.Contains("foo")
), or if a multi-level expression is given where only a single-level is allowed.
I tried looking at compiled IL code first but since the expression trees are a C# compiler "trick", in IL all I see is creating expression instances and such, and while I could check each if only MemberExpressions (and the correct number of them) are created, it is not so great.
Then Roslyn came to my mind. Is it possible to write something like this with Roslyn?
Roslyn is a collection of open-source compilers, code analysis and refactoring tools which work with C# and Visual Basic source codes. This set of compilers and tools can be used to create full-fledged compilers, including, first and foremost, source code analysis tools.
NET Compiler Platform (Roslyn) Analyzers inspect your C# or Visual Basic code for style, quality, maintainability, design, and other issues. This inspection or analysis happens during design time in all open files. Analyzers are divided into the following groups: Code style analyzers are built into Visual Studio.
NET Code Analyser . NET Compiler as a service. While it is relatively easy to write code, it is not so easy to write high quality maintainable code.
The simplest way is File -> Preferences -> Settings, from there search for "roslyn" and tick the setting "Enable Roslyn Analyzers". Be sure to save your settings.
Yes, I think Roslyn and its code issues are exactly the right tool for this. With them, you can analyze the code while you type and create errors (or warnings) that are shown as other errors in Visual Studio.
I have tried to create such code issue:
[ExportSyntaxNodeCodeIssueProvider("PropertyExpressionCodeIssue", LanguageNames.CSharp, typeof(InvocationExpressionSyntax))]
class PropertyExpressionCodeIssueProvider : ICodeIssueProvider
{
[ImportingConstructor]
public PropertyExpressionCodeIssueProvider()
{}
public IEnumerable<CodeIssue> GetIssues(IDocument document, CommonSyntaxNode node, CancellationToken cancellationToken)
{
var invocation = (InvocationExpressionSyntax)node;
var semanticModel = document.GetSemanticModel(cancellationToken);
var semanticInfo = semanticModel.GetSemanticInfo(invocation, cancellationToken);
var methodSymbol = (MethodSymbol)semanticInfo.Symbol;
if (methodSymbol == null)
yield break;
var attributes = methodSymbol.GetAttributes();
if (!attributes.Any(a => a.AttributeClass.Name == "PropertyExpressionAttribute"))
yield break;
var arguments = invocation.ArgumentList.Arguments;
foreach (var argument in arguments)
{
var lambdaExpression = argument.Expression as SimpleLambdaExpressionSyntax;
if (lambdaExpression == null)
continue;
var parameter = lambdaExpression.Parameter;
var memberAccess = lambdaExpression.Body as MemberAccessExpressionSyntax;
if (memberAccess != null)
{
var objectIdentifierSyntax = memberAccess.Expression as IdentifierNameSyntax;
if (objectIdentifierSyntax != null
&& objectIdentifierSyntax.PlainName == parameter.Identifier.ValueText
&& semanticModel.GetSemanticInfo(memberAccess, cancellationToken).Symbol is PropertySymbol)
continue;
}
yield return
new CodeIssue(
CodeIssue.Severity.Error, argument.Span,
string.Format("Has to be simple property access of '{0}'", parameter.Identifier.ValueText));
}
}
#region Unimplemented ICodeIssueProvider members
public IEnumerable<CodeIssue> GetIssues(IDocument document, CommonSyntaxToken token, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public IEnumerable<CodeIssue> GetIssues(IDocument document, CommonSyntaxTrivia trivia, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
#endregion
}
The usage would be like this:
[AttributeUsage(AttributeTargets.Method)]
class PropertyExpressionAttribute : Attribute
{ }
…
[PropertyExpression]
static void Foo<T>(Expression<Func<SomeType, T>> expr)
{ }
…
Foo(x => x.P); // OK
Foo(x => x.M()); // error
Foo(x => 42); // error
The code above has several issues:
semanticModel.GetSemanticInfo(memberAccess, cancellationToken).Symbol
near the end always returns null
. This is because semantics of expressions trees is among the currently unimplemented features.Yes, it's totally possible. The problem is Roslyn doesn't yet support all language constructs yet, so you might run into some unsupported stuff. Expression trees are unsupported in that Roslyn cannot compile code that generates expressions, but you should be able to get far enough to make some things work.
At a high level, if you wanted to implement this as an MSBuild task, in your build task you could call Roslyn.Services.Workspace.LoadSolution
or Roslyn.Services.Workspace.LoadStandaloneProject
. You'd then walk through the syntax trees looking for mentions of your various methods, and then bind them to make sure it's actually the method you think you're calling. From there, you could find the lambda syntax nodes, and perform whatever syntax/semantic analysis you want from there.
There are a few sample projects in the CTP you might find useful, such as the RFxCopConsoleCS
project, which implements a simple FxCop-style rule in Roslyn.
I should also mention that the parser is complete for Roslyn, so the more you can do without semantic information, the better.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With