PageRenderTime 15ms CodeModel.GetById 2ms app.highlight 3ms RepoModel.GetById 1ms app.codeStats 0ms

Plain Text | 818 lines | 649 code | 169 blank | 0 comment | 0 complexity | fca56f12dceab6a27b458996a96a2194 MD5 | raw file
  1## Creating a C# ("Roslyn") Analyser - For beginners by a beginner
  3I've been meaning to try writing a post about creating analysers for a little while now - they're a technology that I think has huge promise for improving code quality and they're something that I've successfully played around with recently.. but I'm still very much in the early phases of being proficient and they're not something that I can just sit down and bang out easily (ie. not without a lot of googling).
  5So this won't be the post of an expert - but I'm hoping to use that to my advantage since I hopefully remember the pain points all too well and can go through the sort of things that I try when I'm hashing these out.
  7Most of the analysers I've been writing have been for libraries that work with [Bridge.NET](, which introduces some of its own complications. I'm hoping to talk about those problems and how to overcome them in a later post - this one will be a more general introduction.
  9### Creating a fresh Analyser project
 11The easiest way to get started is to use a Microsoft template. To do this, first you need to install the Visual Studio 2016 SDK and to do *this* you go to File / New / Project and then choose C# in the left navigation pane, click on Extensibility and then select "Install the Visual Studio Extensibility Tools" (you may already have it installed, it's an optional component of VS2015 - if you see no link to "Install the Visual Studio Extensibility Tools" then hopefully that's why). Next, from the same Extensibility section, you need to select "Download the .NET Compiler Platform SDK". This will ensure that you have the project template installed that we're going to use and it installs some other helpful tools, such as the Syntax Visualizer (which we'll see in a moment).
 13Now that you have the template and since you're already in File / New / Project / C# / Extensibility, select "Analyzer with Code Fix (NuGet + VSIX)" to create an example analyser solution. This will be a fully operational analyser, split into three projects - the analyser itself, a unit test library and a "Vsix" project. This last one would be used if you wanted to create an analyser that would be installed and applied to *all* projects that you would ever open and *not* apply to any specific library. What I'll be talking about here will be creating an analyser to work with a particular library (that would be distributed *with* the library) - so that everyone consuming the library can benefit from it. As such, to keep things simple, delete the "Vsix" project now,
 15The example analyser that this template installs does something very simple - it looks for class names that are not upper case and it warns about them. In terms of functionality, this is not particularly useful.. but in terms of education and illustrating how to get started it's a good jumping off point. In fact, the project includes not just an analyser but also a "code fix" - once a non-all-upper-case class name is identified and warned about, a quick fix will be offered in the IDE to change the name to match the upper case regime that it's pushing. Code fixes can be really helpful but I'll talk about them another day, I think that there already will be plenty to deal with in this post.
 17The analyser class looks basically like this (I've removed comments and replaced localisable strings with hard-coded strings, just to make it a little less to absorb all at once) -
 19	using System.Collections.Immutable;
 20	using System.Linq;
 21	using Microsoft.CodeAnalysis;
 22	using Microsoft.CodeAnalysis.Diagnostics;
 24	namespace ExampleAnalyser
 25	{
 26		[DiagnosticAnalyzer(LanguageNames.CSharp)]
 27		public class ExampleAnalyserAnalyzer : DiagnosticAnalyzer
 28		{
 29			public const string DiagnosticId = "ExampleAnalyser";
 30			private const string Category = "Naming";
 31			private static readonly LocalizableString Title
 32				= "Type name contains lowercase letters";
 33			private static readonly LocalizableString MessageFormat
 34				= "Type name '{0}' contains lowercase letters";
 35			private static readonly LocalizableString Description
 36				= "Type names should be all uppercase.";
 38			private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(
 39				DiagnosticId,
 40				Title,
 41				MessageFormat,
 42				Category,
 43				DiagnosticSeverity.Warning,
 44				isEnabledByDefault: true,
 45				description: Description
 46			);
 48			public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
 49			{
 50				get { return ImmutableArray.Create(Rule); }
 51			}
 53			public override void Initialize(AnalysisContext context)
 54			{
 55				context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);
 56			}
 58			private static void AnalyzeSymbol(SymbolAnalysisContext context)
 59			{
 60				var namedTypeSymbol = (INamedTypeSymbol)context.Symbol;
 61				if (namedTypeSymbol.Name.ToCharArray().Any(char.IsLower))
 62				{
 63					context.ReportDiagnostic(Diagnostic.Create(
 64						Rule,
 65						namedTypeSymbol.Locations[0],
 66						namedTypeSymbol.Name
 67					));
 68				}
 69			}
 70		}
 71	}
 73To summarise what's in the above code:
 751. **Every analyser needs at least one rule that it will declare**, where a rule has various properties such as a Diagnostic Id, Category, Title, MessageFormat, Description and Severity. The two that are most immediately interesting are Severity (make it a Warning to point out a potential mistake or make it an Error to indicate a critical problem that will prevent a build from being completed) and MessageFormat, since MessageFormat is responsible for the text that will be displayed to the user in their Error List. MessageFormat supports string replacement; in the above example, you can see that there is a "{0}" placeholder in the MessageFormat - when "Diagnostic.Create" is called, the argument "namedTypeSymbol.Name" is injected into that "{0}" placeholder.
 761. **Every analyser needs to declare a "SupportedDiagnostics" value that lists all of the types of rule** that it is possible for the analyser to raise. This is vital in order for the analyser to work correctly at runtime. (If you create an analyser that has three different types of rule that it can report but you forget to declare one of the types in the "SupportedDiagnostics" property, there is actually an analyser that is installed with the template that points out the mistake to you - which is a great example of how analysers can protect you at compile time from potential runtime problems!)
 771. **Every analyser needs an "Initialize" method that registers what type of symbol (more on what this actually means in a moment) it's interested in** and provides a reference to a method that will perform the actual analysis
 79The simple task of the class above is to look at any "named type" (ie. classes and structs) and inspect their name to ensure that they consist entirely of capital letters (remember, this example included in the "Analyzer with Code Fix (NuGet + VSIX)" template is simply for educational purposes and *not* because it's believed that all class names should be SHOUTING_FORMAT! :) Any class that doesn't have an all-caps name will result in a warning in the Error List.
 81To illustrate how this should work, the test project includes the following test method -
 83	[TestMethod]
 84	public void TestMethod2()
 85	{
 86		var test = @"
 87	using System;
 88	using System.Collections.Generic;
 89	using System.Linq;
 90	using System.Text;
 91	using System.Threading.Tasks;
 92	using System.Diagnostics;
 94	namespace ConsoleApplication1
 95	{
 96		class TypeName
 97		{   
 98		}
 99	}";
100		var expected = new DiagnosticResult
101		{
102			Id = "ExampleAnalyser",
103			Message = String.Format("Type name '{0}' contains lowercase letters", "TypeName"),
104			Severity = DiagnosticSeverity.Warning,
105			Locations = new[] {
106				new DiagnosticResultLocation("Test0.cs", 11, 15)
107			}
108		};
110		VerifyCSharpDiagnostic(test, expected);
111	}
113This makes it clear to see precisely what sort of thing the analyser is looking for but it also gives us another immediate benefit - we can actually execute the analyser and step through it in the debugger if we want to have a poke around with exactly what is in the **SymbolAnalysisContext** reference or if we want to look at the properties of a particular **INamedTypeSymbol** instance. This is as easy as putting a breakpoint into the "AnalyzeSymbol" method in the example analyser and then going back into the test class, right-clicking within "TestMethod2" and selecting "Debug Tests".
115I want to introduce one other useful technique before moving on - the use of the "Syntax Visualizer". An analyser works on an in-memory tree of nodes that represent the source code of the file that you're looking at\*. In the unit test above, the named symbol "TypeName" is a child node of the "TypeName" class declaration, which is a child node of the "ConsoleApplication1" namespace, which is a child of a top-level construct called the "CompilationUnit". Understanding the various types of node will be key to writing analysers and the Syntax Visualizer makes this a little bit easier.
117\* *(Although an analyser starts by examining source code in a particular file, it's also possible to look up types and values that are referenced in that code that live elsewhere - to find out what namespace a class that is referenced exists in, for example, or to determine what arguments a method that is called that exists in a different library. These lookups are more expensive than looking solely at the content in the current file, however, and so should only be done if strictly necessary. We will see how to do this shortly. When looking only at content parsed from the current file, we are looking at the "syntax tree". When looking up references elsewhere in the solution we accessing the "semantic model").*
119Having installed the ".NET Compiler Platform SDK" earlier, you will now have access to this tool - go to View / Other Windows / Syntax Visualizer. This shows the syntax tree for any code within your project. So, if you click on the name "TestMethod2" then you will see that it is an **IdentifierToken** (which is the name "TestMethod2") that is a child node of a **MethodDeclaration** which is a child node of a **ClassDeclaration** which is a child node of a **NamespaceDeclaration**, which is a child node of a **CompilationUnit**. You can click on any of these nodes in the Syntax Visualiser to inspect some of the properties of the node and you can open further branches to inspect more - for example, there is a "Block" node that will appear shortly after the **IdentifierToken** that you may click to reveal the nodes that represent the statements within the method.
121![The Syntax Visualizer](/Content/Images/Posts/SyntaxVisualizer.png)
123### Writing a real analyser
125I'm going to walk through an analyser that I created recently - starting from scratch and, hopefully, encountering the same problems that I did last time so that I can illustrate how to find out how to solve them.
127The analyser is part of my [Bridge.React]( library but you won't need to know anything about React or Bridge to follow along.
129The root of the problem relates to the rendering of html "select" elements. There are three related properties to consider when rendering a "select" element; "Multiple", "Value" and "Values". Multiple is a boolean that indicates whether the elements supports only single selections (false) or zero, one or more selections (true). If rendering an element with pre-selected items then the "Value" or "Values" properties must be used. "Value" is a string while "Values" is a string array. If "Multiple" is false and "Values" is set then React will display a warning at runtime and ignore the value, similarly if "Multiple" is true and "Value" is set.
131I wanted an analyser that handled these simple cases -
133    // This is fine
134	return DOM.Select(new SelectAttributes { Multiple = false, Value = "x" };
136    // This is fine
137	return DOM.Select(new SelectAttributes { Multiple = true, Values = new [] { "x", "y" } };
139	// Wrong (shouldn't use "Value" when "Multiple" is true)
140	return DOM.Select(new SelectAttributes { Multiple = true, Value = "x" };
142	// Wrong (shouldn't use "Values" when "Multiple" is false)
143    return DOM.Select(new SelectAttributes { Multiple = false, Values = new [] { "x", "y" } };
145	// Wrong (shouldn't use "Values" when "Multiple" defaults to false)
146    return DOM.Select(new SelectAttributes { Values = new [] { "x", "y" } };
148It's worth mentioning that I'm *only* considering these simple cases so this analyser won't be "perfect". If "Multiple" is set according to a variable then I'm not going to try to follow all possible code paths to ensure that it is never true/false if Values/Value is set. I'm also not going to cater for the technically-valid case where someone instantiates a **SelectAttributes** and sets "Values" on it initially (but leaves "Multiple" as false) and then sets "Multiple" to true on a later line of code. While this would be valid (there would be no runtime warning), I think that it would be clearer to set "Multiple" *and* "Values" together. In this case, I'm imposing what I believe to be a best practice on the consumer of my library - some analysers do this, some don't.
150To keep things as simple as possible for now, instead of trying to pull in the real Bridge.React library, we'll just create another class library project in the solution to work against - call it "Bridge.React" and rename the "Class1.cs" file that is automatically created as part of a class library project to "SelectAttributes.cs". Change its contents to the following:
152	namespace Bridge.React
153	{
154		public sealed class SelectAttributes
155		{
156			public bool Multiple { private get; set; }
157			public string Value { private get; set; }
158			public string[] Values { private get; set; }
159		}
160	}
162This will be enough to start writing the analyser.
164What I want to do is to take the example analyser from the "Analyzer with Code Fix (NuGet + VSIX)" and change it to ensure that **SelectAttributes** properties are always configured according to the rule outlined above. Before getting started on that, though, it seems like a good time to formalise the rules by decribing them with unit tests. We get many bonuses here - writing individual tests may help guide us through fixing them up one at a time and so help us focus on individual problems that the analyser has to solve. It will also provide us with a way to exercise the analyser and step through it with the debugger (which I find invaluable when I'm not very familiar with a library or object model - when I *do* have a good grasp on code then stepping through a debugger can feel very time-consuming but it can be helpful in cases like this, as I'll demonstrate shortly). Finally, the tests will help avoid regressions creeping in if I decide to refactor the analyser or extend its functionality in the future.
166So, replace the contents of "UnitTest.cs" with the following:
168	using Microsoft.CodeAnalysis;
169	using Microsoft.CodeAnalysis.Diagnostics;
170	using Microsoft.VisualStudio.TestTools.UnitTesting;
171	using TestHelper;
173	namespace ExampleAnalyser.Test
174	{
175		[TestClass]
176		public class UnitTest : DiagnosticVerifier
177		{
178			[TestMethod]
179			public void DoNotUseValueWhenMultipleIsTrue()
180			{
181				var testContent = @"
182					using Bridge.React;
184					namespace TestCase
185					{
186						public class Example
187						{
188							public void Go()
189							{
190								new SelectAttributes { Multiple = true, Value = ""1"" };
191							}
192						}
193					}";
195				var expected = new DiagnosticResult
196				{
197					Id = ExampleAnalyserAnalyzer.DiagnosticId,
198					Message = "If 'Multiple' is true then the 'Values' property should be used instead of 'Value'",
199					Severity = DiagnosticSeverity.Warning,
200					Locations = new[]
201					{
202						new DiagnosticResultLocation("Test0.cs", 10, 29)
203					}
204				};
206				VerifyCSharpDiagnostic(testContent, expected);
207			}
209			[TestMethod]
210			public void DoNotUseValuesWhenMultipleIsFalse()
211			{
212				var testContent = @"
213					using Bridge.React;
215					namespace TestCase
216					{
217						public class Example
218						{
219							public void Go()
220							{
221								new SelectAttributes { Multiple = false, Values = new[] { ""1"" } };
222							}
223						}
224					}";
226				var expected = new DiagnosticResult
227				{
228					Id = ExampleAnalyserAnalyzer.DiagnosticId,
229					Message = "If 'Multiple' is false then the 'Value' property should be used instead of 'Values'",
230					Severity = DiagnosticSeverity.Warning,
231					Locations = new[]
232					{
233						new DiagnosticResultLocation("Test0.cs", 10, 29)
234					}
235				};
237				VerifyCSharpDiagnostic(testContent, expected);
238			}
240			[TestMethod]
241			public void DoNotUseValueWhenMultipleDefaultsToFalse()
242			{
243				var testContent = @"
244					using Bridge.React;
246					namespace TestCase
247					{
248						public class Example
249						{
250							public void Go()
251							{
252								var x = new SelectAttributes { Values = new[] { ""1"" } };
253								x.Multiple = True;
254							}
255						}
256					}";
258				var expected = new DiagnosticResult
259				{
260					Id = ExampleAnalyserAnalyzer.DiagnosticId,
261					Message = "If 'Multiple' is false then the 'Value' property should be used instead of 'Values'",
262					Severity = DiagnosticSeverity.Warning,
263					Locations = new[]
264					{
265						new DiagnosticResultLocation("Test0.cs", 10, 37)
266					}
267				};
269				VerifyCSharpDiagnostic(testContent, expected);
270			}
272			protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
273			{
274				return new ExampleAnalyserAnalyzer();
275			}
276		}
277	}
279Now there's one more important thing to do before actually writing the analyser. When those unit tests run, the ".NET Compiler Platform" (referred to as "Roslyn") will parse and compile those code snippets in memory. This means that the code snippets need to actually be able to compile! Currently they won't because Roslyn won't know how to resolve the "Bridge.React" namespace that is referenced.
281This is quite easily fixed - the **DiagnosticVerifier** class (which is part of the template that we started with) configures some environment options. That's why each test checks a file called "Test0.cs" - because Roslyn wants a filename to work with and that's what the **DiagnosticVerifier** tells it to use. It also specifies what assemblies to include when building the project. So, if the code snippets referenced "System" or "Sytem.Collections.Generic" then those references will work fine. However, it doesn't initially know about the "Bridge.React" project, so we need to tell it to support it.
2831. Add a reference to the "Bridge.React" project to the "ExampleAnalayser.Test" project
2841. Edit the file "Helpers/DiagnosticVerifier.Helper.cs" in the "ExampleAnalayser.Test" project and add the following near the top, where other **MetadataReference** instances are created:
286        private static readonly MetadataReference CSharpBridgeReactReference
287			= MetadataReference.CreateFromFile(typeof(Bridge.React.SelectAttributes).Assembly.Location);
2893. Open all of the code regions in that file and add pass "CSharpBridgeReactReference" into the solution by adding an additional "AddMetadataReference" call. The "CreateProject" method should now look like this:
291        private static Project CreateProject(string[] sources, string language = LanguageNames.CSharp)
292        {
293            string fileNamePrefix = DefaultFilePathPrefix;
294            string fileExt = language == LanguageNames.CSharp
295			    ? CSharpDefaultFileExt
296				: VisualBasicDefaultExt;
297            var projectId = ProjectId.CreateNewId(debugName: TestProjectName);
298			var solution = new AdhocWorkspace()
299			.CurrentSolution
300				.AddProject(projectId, TestProjectName, TestProjectName, language)
301				.AddMetadataReference(projectId, CorlibReference)
302				.AddMetadataReference(projectId, SystemCoreReference)
303				.AddMetadataReference(projectId, CSharpSymbolsReference)
304				.AddMetadataReference(projectId, CodeAnalysisReference)
305				.AddMetadataReference(projectId, CSharpBridgeReactReference);
306			int count = 0;
307			foreach (var source in sources)
308			{
309				var newFileName = fileNamePrefix + count + "." + fileExt;
310				var documentId = DocumentId.CreateNewId(projectId, debugName: newFileName);
311				solution = solution.AddDocument(documentId, newFileName, SourceText.From(source));
312				count++;
313			}
314			return solution.GetProject(projectId);
315        }
317### *Really* writing the analyser
319Now that the groundwork is done and we've decided what precisely needs doing (and documented it with tests), we need to write the actual code.
321Although I can use the debugger to inspect the syntax tree for the code snippets in the unit tests, at this point I think even that would be information overload. To begin with, just add the following line to one of the unit test methods - it doesn't matter which one because it will be deleted very shortly, it's just to have a bit of a poke around with the Syntax Visualizer:
323    var x = new Bridge.React.SelectAttributes { Multiple = true, Value = "x" };
325Ensuring that the Syntax Visualizer is visible (View / Other Windows / Syntax Visualizer), clicking on "Multiple" shows the following:
329The **IdentifierToken** is the "Multiple" property, which is part of a **SimpleAssignment** (ie. "Multiple = 1") which is a child of an **ObjectInitializerExpression** (which is the curly brackets around the two properties being set) which is a child of an **ObjectCreationExpression** (which is the entire statement that includes "new Bridge.React.SelectAttributes" *and* the setting of the two properties) and that itself is part of a **VariableDeclaration** (which sets "x" to be the result of the object creation). With the Syntax Visualizer, we could go all the way up to the top of the method and then to the class and then to the namespace and then to the top-level CompilationUnit. However, what we're most interested in is the **ObjectInitializerExpression**, since that contains the properties that we want to verify.
331So, how do we alter the analyser class that we currently have in order to identify object initialisers such as this?
333Currently the example analyser class has an "Initialize" method that looks like this:
335	public override void Initialize(AnalysisContext context)
336	{
337		context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);
338	}
340The first thing to try would be to see what other options are in the "SymbolKind" enum. However, this contains things like "Alias", "Event", "Method", "NamedType", "Property" which don't bear much resemblance to **ObjectInitializerExpression**. Without any better plan, I recommend turning to Google. If "SymbolKind" doesn't seem to have what we want, maybe there's something else that we can extract from the **AnalysisContext** instance that the "Initialize" method has.
342Googling for ["AnalysisContext ObjectInitializerExpression"]( doesn't actually return that many results. However, the second one [RoslynClrHeapAllocationAnalyzer/ExplicitAllocationAnalyzer.cs]( has some code that looks promising:
344	public override void Initialize(AnalysisContext context)
345	{
346		var kinds = new[]
347		{
348			SyntaxKind.ObjectCreationExpression,
349			SyntaxKind.AnonymousObjectCreationExpression,
350			SyntaxKind.ArrayInitializerExpression,
351			SyntaxKind.CollectionInitializerExpression,
352			SyntaxKind.ComplexElementInitializerExpression,
353			SyntaxKind.ObjectInitializerExpression,
354			SyntaxKind.ArrayCreationExpression,
355			SyntaxKind.ImplicitArrayCreationExpression,
356			SyntaxKind.LetClause
357		};
358		context.RegisterSyntaxNodeAction(AnalyzeNode, kinds);
359	}
361Instead of calling "RegisterSymbolAction" and passing a "SymbolKind" value, we can call "RegisterSyntaxNodeAction" and pass it an array of "SyntaxKind" values - where "SyntaxKind" is an enum that has an "ObjectInitializerExpression" value.
363Actually, by starting to change the "Initialize" method to
365	public override void Initialize(AnalysisContext context)
366	{
367		context.RegisterSyntaxNodeAction(AnalyzeSymbol,
369.. it becomes clear that the method actually takes a params array and so it will be perfectly happy for us to specify only a single "SyntaxKind" value. "Initialize" now becomes:
371	public override void Initialize(AnalysisContext context)
372	{
373		context.RegisterSyntaxNodeAction(
374			AnalyzeSymbol,
375			SyntaxKind.ObjectInitializerExpression
376		);
377	}
379But the analyser project doesn't compile now - it complains about the type of one of the arguments of the call to "SymbolAnalysisContext". It definitely takes a "SyntaxKind" enum as its second argument so it must be the first that is wrong. Intellisense indicates that it wants the first argument to be of type **Action&lt;SymbolAnalysisContext&gt;** but the "AnalyzeSymbol" method currently takes a **SyntaxNodeAnalysisContext** (and so is an **Action&lt;SymbolAnalysisContext&gt;**, rather than an **Action&lt;SyntaxNodeAnalysisContext&gt;**).
381This is easily fixed by changing the argument of the "AnalyzeSymbol" method. Doing so, however, will mean that *it* causes a compile error because the example code was expecting a **SymbolAnalysisContext** and we want to give it a **SyntaxNodeAnalysisContext**. No matter, that code doesn't do what we want anyway! So change the method argument, delete its body and - while we're making changes - rename it to something better than "AnalyzeSymbol", such as "LookForInvalidSelectAttributeProperties" -
383	public override void Initialize(AnalysisContext context)
384	{
385		context.RegisterSyntaxNodeAction(
386			LookForInvalidSelectAttributeProperties,
387			SyntaxKind.ObjectInitializerExpression
388		);
389	}
391	private static void LookForInvalidSelectAttributeProperties(SyntaxNodeAnalysisContext context)
392	{
393	}
395Now that the basic structure is there, we can start work on the new "LookForInvalidSelectAttributeProperties" implementation. The "context" reference that is passed in has a "Node" property and this will match the SyntaxKind value that we passed to "RegisterSyntaxNodeAction". So "context.Node" will be a reference to a node that represents an "object initialisation".
397*Sanity check: The **SyntaxNode** class (which is the base node class) has a "Kind()" method that will return the "SyntaxKind" enum value that applies to the current node - so calling "Kind()" on "context.Node" here will return the "ObjectInitializerExpression" option from the "SyntaxKind" enum.*
399Now that we have a reference to an object initialisation node, we can go one of two ways. We want to ensure that the type being initialised is the **SelectAttributes** class from the "Bridge.React" assembly and we want to check whether any invalid property combinations are being specified. The first task will involve getting the type name and then doing a lookup in the rest of the solution to work out where that type name comes from (to ensure that it is actually the "Bridge.React" **SelectAttributes** class and not another class that exists somewhere with the same name). The second task only requires us to look at what properties are set by code in the syntax tree that we already have. This means that the first task is more expensive to perform than the second task and so we should try to deal with "step two" first since we will be able to avoid "step one" altogether if no invalid property combinations appear.
401So, to look for invalid property combinations first.. The Syntax Visualizer (as seen in the last image) shows that each individual property-setting is represented by a "SimpleAssignmentExpression" and that each of these is a direct child of the object initialisation. The **SyntaxNode** class has a ChildNodes() method that will return all of the children, which seems like a good place to start. So, we might be able to do something like this:
403    // This doesn't work,SimpleAssignmentExpressionSyntax isn't a real class :(
404	var propertyInitialisers = context.Node.ChildNodes()
405		.OfType<SimpleAssignmentExpressionSyntax>();
407.. however, "SimpleAssignmentExpressionSyntax" is not a real type. I tried starting to type  out "Simple" to see if intellisense would pick up what the correct name was - but that didn't get me anywhere.
409Next, I resorted to deleting those last few lines (since they don't compile) and to just putting a breakpoint at the top of "LookForInvalidSelectAttributeProperties". I then used Debug Tests on "DoNotUseValueWhenMultipleIsTrue". The breakpoint is hit.. but I can't see the child nodes with QuickWatch because "ChildNodes()" is a method, not a property, and QuickWatch only shows you property values (it doesn't offter to execute methods and show you what is returned). So I go to the Immediate Window (Debug / Windows / Immediate), type the following and hit [Enter] -
411	context.Node.ChildNodes().First().GetType().Name
413This displays "AssignmentExpressionSyntax".
415This clue is enough to stop the debugger and go back to trying to populate the "LookForInvalidSelectAttributeProperties". It may now start with:
417	var propertyInitialisers = context.Node.ChildNodes()
418		.OfType<AssignmentExpressionSyntax>();
420Using Go To Definition on **AssignmentExpressionSyntax** shows that it has a "Left" and a "Right" property. These are the expressions that come either side of the operator, which is always an Equals sign when considering object property initialisations.
422The Syntax Visualizer shows that each "SimpleAssignmentExpression" has an "IdentifierName" on the left, so we should be able to get the property name from that.
424To try to work out what type "IdentifierName" relates to, I start typing "Identifier" and intellisense suggests **IdentifierNameSyntax** (if it hadn't suggested anything helpful then I would have resorted to using Debug Tests again and inspecting types in the debugger). Having a poke around the **IdentifierNameSyntax** class, I see that it has a property "Identifier" and that has a string property "ValueText". This looks like the name of the property being set. Things are coming together. The start of the "LookForInvalidSelectAttributeProperties" can now look like this:
426	var propertyInitialisers = context.Node.ChildNodes()
427		.OfType<AssignmentExpressionSyntax>()
428		.Select(propertyInitialiser => new
429		{
430			PropertyName = ((IdentifierNameSyntax)propertyInitialiser.Left).Identifier.ValueText,
431			ValueExpression = propertyInitialiser.Right
432		});
434It's worth noting that we don't have to worry about the "Left" property ever being anything other than a simple identifier because assignments in object initialisers are only ever allow to be simple assignments. For example, the following would not compile:
436	var x = new MyClass { Name.Value = "Ted };
438.. because attempting to set nested properties in object initialisers does not compile in C#. Because it's not valid C#, we don't have to worry about it being passed through the analyser.
440Maybe it's worth adding another unit test around this - to ensure that invalid C# can't result in a load of edge cases that we need to be concerned about:
442	[TestMethod]
443	public void IgnoreInvalidPropertySetting()
444	{
445		var testContent = @"
446			using Bridge.React;
448			namespace TestCase
449			{
450				public class Example
451				{
452					public void Go()
453					{
454						new SelectAttributes { Nested.Multiple = true };
455					}
456				}
457			}";
459		VerifyCSharpDiagnostic(testContent);
460	}
462*Note: Calling the "VerifyCSharpDiagnostic" with no "expected" value means that the test expects that the analyser will not report any violated rules.*
464Now we can really move things along. We're interested in property initialisers where "Multiple" is clearly true or false (meaning it is set specifically to true or false *or* it's not specified at all, leaving it with its default value of false). So, again using the Syntax Visualizer to work out how to tell whether an expression means a "true" constant or a "false" constant, I've come up with this:
466	var propertyInitialisers = context.Node.ChildNodes()
467		.OfType<AssignmentExpressionSyntax>()
468		.Select(propertyInitialiser => new
469		{
470			PropertyName = ((IdentifierNameSyntax)propertyInitialiser.Left).Identifier.ValueText,
471			ValueExpression = propertyInitialiser.Right
472		});
474	var multiplePropertyInitialiser = propertyInitialisers.FirstOrDefault(
475		propertyInitialiser => propertyInitialiser.PropertyName == "Multiple"
476	);
477	bool multiplePropertyValue;
478	if (multiplePropertyInitialiser == null)
479		multiplePropertyValue = false; // Defaults to false if not explicitlt set
480	else
481	{
482        var multiplePropertyValueKind = multiplePropertyInitialiser.ValueExpression.Kind();
483        if (multiplePropertyValueKind == SyntaxKind.TrueLiteralExpression)
484            multiplePropertyValue = true;
485        else if (multiplePropertyValueKind == SyntaxKind.FalseLiteralExpression)
486            multiplePropertyValue = false;	
487		else
488		{
489			// Only looking for very simple cases - where explicitly set to true or to false or not set at
490			// all (defaulting to false). If it's set according to a method return value or a variable then
491			// give up (this is just intended to catch obvious mistakes, not to perform deep and complex
492			// analysis)
493			return;
494		}
495	}
497The next thing to do is to look for a "Value" or "Values" property being specified that is not appropriate for the "Multiple" value that we've found.
499From the above code, it should be fairly clear that the way to do this is the following:
501	var valuePropertyIsSpecified = propertyInitialisers.Any(
502		propertyInitialiser => propertyInitialiser.PropertyName == "Value"
503	);
504	var valuesPropertyIsSpecified = propertyInitialisers.Any(
505		propertyInitialiser => propertyInitialiser.PropertyName == "Values"
506	);
507	if (!valuePropertyIsSpecified && !valuesPropertyIsSpecified)
508		return;
510The final step is to ensure that the object initialisation that we're looking at is indeed for a **SelectAttributes** instance. This is the bit that requires a lookup into the "SemanticModel" and which is more expensive than just looking at the current syntax tree because it needs the project to compile and to then work out what references to external code there may be.
512Knowing that I'm going to be dealing with the full semantic model, I'll start by looking through the methods available on "context.SemanticModel" to see what might help me. Using the intellisense / documentation, it doesn't take long to find a "GetTypeInfo" method that takes an **ObjectCreationExpression** instance - this is ideal because we have an **ObjectInitializerExpressionSyntax** and we know that an **ObjectInitializerExpressionSyntax** is a child of an **ObjectCreationExpressionSyntax**, so it's easy for us to get an **ObjectCreationExpression** (it's just the parent of **ObjectInitializerExpressionSyntax** that we have).
514"GetTypeInfo" returns a **TypeInfo** instance which has two properties; "Type" and "ConvertedType". "ConvertedType" is (taken from the xml summary documentation):
516> The type of the expression after it has undergone an implicit conversion
518which shouldn't apply here, so we'll just look at "Type". Note, though, that the documentation for "Type" says that:
520> For expressions that do not have a type, null is returned. If the type could not be determined due to an error, than an IErrorTypeSymbol is returned.
522Since this is an object creation expression, there should *always* be a type returned (the type of the object being instantiated) but we do need to be careful about the error response. Here, it's fine to stop processing if there's an error - it might mean that there is a "new SelectAttributes" statements in the code being analysed but no "Using Bridge.React;" at the top of the file. We'll ignore these error cases and plan to only analyse valid code.
524This is the code that needs adding to ensure that the properties that we're looking at are for a Bridge.React.SelectAttributes -
526	var objectCreation = (ObjectCreationExpressionSyntax)context.Node.Parent;
527	var objectCreationTypeInfo = context.SemanticModel.GetTypeInfo(objectCreation);
528	if ((objectCreationTypeInfo.Type is IErrorTypeSymbol)
529	|| (objectCreationTypeInfo.Type.ContainingAssembly.Identity.Name != "Bridge.React")
530	|| (objectCreationTypeInfo.Type.Name != "SelectAttributes"))
531		return;
533Having written this code, it strikes me as a good idea to add another test - one that ensures that we don't raise false positives about "Multiple" and "Value" / "Values" in cases where it's a different **SelectAttributes** class, that is declared somewhere other than in "Bridge.React".
535	/// <summary>
536	/// Don't analyse a SelectAttributes initialisation that is for a different SelectAttributes class
537	/// (only target the SelectAttributes class that is part of the Bridge.React library)
538	/// </summary>
539	[TestMethod]
540	public void OnlyTargetBridgeReactSelectAttributes()
541	{
542		var testContent = @"
543			namespace TestCase
544			{
545				public class Example
546				{
547					public void Go()
548					{
549						new SelectAttributes { Multiple = true, Value = ""x"" };
550					}
551				}
553				public class SelectAttributes
554				{
555					public bool Multiple { get; set; }
556					public string Value { get; set; }
557				}
558			}";
560		VerifyCSharpDiagnostic(testContent);
561	}
563Now we have all of the required information to display a warning for invalid "Multiple" / "Value" / "Values" combinations. What we *don't* have is appropriate message content to display - we've only got the warning content from the example analyser in the project template.
565So delete all of the code at the top of the analyser - the const and static strings, the "Rule" reference and the "SupportedDiagnostics" property and replace them with this:
567	public const string DiagnosticId = "Bridge.React";
568	private static readonly LocalizableString Title
569		= "Be careful to use the appropriate 'Value' or 'Values' property for the 'Multiple' setting";
570	private static readonly LocalizableString MultipleWithValueMessage
571		= "If 'Multiple' is true then the 'Values' property should be used instead of 'Value'";
572	private static readonly LocalizableString NoMultipleWithValuesMessage
573		= "If 'Multiple' is false then the 'Value' property should be used instead of 'Values'";
574	private const string Category = "Configuration";
576	private static DiagnosticDescriptor MultipleWithValueRule = new DiagnosticDescriptor(
577		DiagnosticId,
578		Title,
579		MultipleWithValueMessage,
580		Category,
581		DiagnosticSeverity.Warning,
582		isEnabledByDefault: true
583	);
584	private static DiagnosticDescriptor NoMultipleWithValuesRule = new DiagnosticDescriptor(
585		DiagnosticId,
586		Title,
587		NoMultipleWithValuesMessage,
588		Category,
589		DiagnosticSeverity.Warning,
590		isEnabledByDefault: true
591	);
593	public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
594	{
595		get { return ImmutableArray.Create(MultipleWithValueRule, NoMultipleWithValuesRule); }
596	}
598The final step, then, is to report rules when they are broken. The following needs adding to the end of the "LookForInvalidSelectAttributeProperties" method in order to complete it:
600	if ((multiplePropertyValue == true) && valuePropertyIsSpecified)
601	{
602		context.ReportDiagnostic(Diagnostic.Create(
603			MultipleWithValueRule,
604			context.Node.GetLocation()
605		));
606	}
607	else if ((multiplePropertyValue == false) && valuesPropertyIsSpecified)
608	{
609		context.ReportDiagnostic(Diagnostic.Create(
610			NoMultipleWithValuesRule,
611			context.Node.GetLocation()
612		));
613	}
615### Localisation support
617There's just one final thing to do now, which is more of a good practice than an essential - that is to replace the hard-coded strings in the analyser class with resources (that may potentially be translated into different languages one day). The project template includes a "Resources.resx" file, which is where we should move these strings to. Edit that file in Visual Studio and delete the existing entries and then add the following Name and Value pairs:
619> **Name:** SelectAttributesAnalyserTitle
621> **Value:** Be careful to use the appropriate 'Value' or 'Values' property for the 'Multiple' setting
623> **Name:** SelectAttributesAnalyserMultipleWithValueMessage
625> **Value:** If 'Multiple' is true then the 'Values' property should be used instead of 'Value'
627> **Name:** SelectAttributesAnalyserNoMultipleWithValuesTitle
629> **Value:** If 'Multiple' is false then the 'Value' property should be used instead of 'Values'
631To make accessing these resources a little easier, add the following method to the bottom of the analyser class:
633	private static LocalizableString GetLocalizableString(string nameOfLocalizableResource)
634	{
635		return new LocalizableResourceString(
636			nameOfLocalizableResource,
637			Resources.ResourceManager,
638			typeof(Resources)
639		);
640	}
642Finally, replace the three hard-coded string property initialisers with the following:
644		private static readonly LocalizableString Title = GetLocalizableString(
645			nameof(Resources.SelectAttributesAnalyserTitle)
646		);
647		private static readonly LocalizableString MultipleWithValueTitle = GetLocalizableString(
648			nameof(Resources.SelectAttributesAnalyserMultipleWithValueMessage)
649		);
650		private static readonly LocalizableString NoMultipleWithValuesTitle = GetLocalizableString(
651			nameof(Resources.SelectAttributesAnalyserNoMultipleWithValuesTitle)
652		);
654### Summary
656That completes the analyser. I've included the complete source code for the final implementation below - now that it's written it doesn't look like much, which hopefully illustrates how powerful and complete the Roslyn library is. And, hopefully, it's shown that this powerful library doesn't need to be daunting because there's many resources out there for helping you understand how to use it; people have written a lot about it and so Googling for terms relating to what you want to do often yields helpful results, people have answered a lot of questions about it on Stack Overflow and so you will often find example and sample code there.
658If you're not sure what terms to use to try to search for help then using the Syntax Visualizer to explore your code can set you on the right path, as can writing a test or two and then examining the "context.Node" reference in the debugger (if you do this then ensure that you are building your project in Debug mode since Release builds may prevent your breakpoints from being hit and may optimise some of the variable references away, which will mean that you won't be able to use QuickWatch on them). Finally, don't forget that there is a lot of helpful information in the xml summary documentation that's available in Visual Studio when you examine the Roslyn classes and their methods - often the names of methods are descriptive enough to help you choose the appropriate one or, at least, give you a clue as to what direction to go in.
660This has really only scraped the surface of what analysers are capable of, it's a technology with huge capability and potential. I might talk about other uses for analysers (or talk about how particular analysers may be implemented) another day but two topics that I definitely *will* talk about soon are "code fixes" and how to get analysers to work with [Bridge.NET]( libraries.
662Code fixes are interesting because they allow you to go beyond just saying "this is wrong" to saying "this is how it may be fixed (automatically, by the IDE)". For example, if someone changed a **SelectAttributes** instantiation to enable multiple selections - eg. started with:
664    DOM.Select(
665		new SelectAttributes { Value = selectedId },
666		options
667	)
669.. and changed it to:
671    DOM.Select(
672		new SelectAttributes { Multiple = true, Value = selectedId },
673		options
674	)
676.. then the analyser could point out that the "Value" property should not be used now that "Multiple" is true but it could also offer to fix it up to the following *automatically*:
678    DOM.Select(
679		new SelectAttributes { Multiple = true, Values = new[] { selectedId } },
680		options
681	)
683There will be times that the warning from an analyser will require manual intervention to correct but there will also be times where the computer could easily correct it, so it's great having the ability to explain to the computer *how* to do so and thus make life that bit easier for the person consuming your library.
685The reason that I also want to spend a little bit of time talking about making analysers work with Bridge.NET libraries soon is that it's something of a special case since Bridge projects don't have references to the standard .net System, System.Collections, etc.. assemblies because they are replaced by special versions of those libraries that have JavaScript translations. This means that you can't reference a Bridge library from a project that relies on the standard .net assemblies, which is a bit of a problem when you want to write a Roslyn analyser for types in a Bridge library (since the analyser project will rely on standard .net assemblies and the analyser will want to reference the Bridge library whose rules are to be applied by the analyser). But there are ways to get around it and I'll go through that another time.
687### The complete analyser
689	using System.Collections.Immutable;
690	using System.Linq;
691	using System.Reflection;
692	using Microsoft.CodeAnalysis;
693	using Microsoft.CodeAnalysis.CSharp;
694	using Microsoft.CodeAnalysis.CSharp.Syntax;
695	using Microsoft.CodeAnalysis.Diagnostics;
697	namespace ExampleAnalyser
698	{
699		[DiagnosticAnalyzer(LanguageNames.CSharp)]
700		public sealed class ExampleAnalyserAnalyzer : DiagnosticAnalyzer
701		{
702			public const string DiagnosticId = "Bridge.React";
703			private static readonly LocalizableString Title = GetLocalizableString(
704				nameof(Resources.SelectAttributesAnalyserTitle)
705			);
706			private static readonly LocalizableString MultipleWithValueTitle = GetLocalizableString(
707				nameof(Resources.SelectAttributesAnalyserMultipleWithValueMessage)
708			);
709			private static readonly LocalizableString NoMultipleWithValuesTitle = GetLocalizableString(
710				nameof(Resources.SelectAttributesAnalyserNoMultipleWithValuesTitle)
711			);
712			private const string Category = "Configuration";
714			private static DiagnosticDescriptor MultipleWithValueRule = new DiagnosticDescriptor(
715				DiagnosticId,
716				Title,
717				MultipleWithValueTitle,
718				Category,
719				DiagnosticSeverity.Warning,
720				isEnabledByDefault: true
721			);
722			private static DiagnosticDescriptor NoMultipleWithValuesRule = new DiagnosticDescriptor(
723				DiagnosticId,
724				Title,
725				NoMultipleWithValuesTitle,
726				Category,
727				DiagnosticSeverity.Warning,
728				isEnabledByDefault: true
729			);
731			public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
732			{
733				get { return ImmutableArray.Create(MultipleWithValueRule, NoMultipleWithValuesRule); }
734			}
736			public override void Initialize(AnalysisContext context)
737			{
738				context.RegisterSyntaxNodeAction(
739					LookForInvalidSelectAttributeProperties,
740					SyntaxKind.ObjectInitializerExpression
741				);
742			}
744			private static void LookForInvalidSelectAttributeProperties(SyntaxNodeAnalysisContext context)
745			{
746				var propertyInitialisers = context.Node.ChildNodes()
747					.OfType<AssignmentExpressionSyntax>()
748					.Select(propertyInitialiser => new
749					{
750						PropertyName = ((IdentifierNameSyntax)propertyInitialiser.Left).Identifier.ValueText,
751						ValueExpression = propertyInitialiser.Right
752					});
754				var multiplePropertyInitialiser = propertyInitialisers.FirstOrDefault(
755					propertyInitialiser => propertyInitialiser.PropertyName == "Multiple"
756				);
757				bool multiplePropertyValue;
758				if (multiplePropertyInitialiser == null)
759					multiplePropertyValue = false; // Defaults to false if not explicitlt set
760				else
761				{
762					var multiplePropertyValueKind = multiplePropertyInitialiser.ValueExpression.Kind();
763					if (multiplePropertyValueKind == SyntaxKind.TrueLiteralExpression)
764						multiplePropertyValue = true;
765					else if (multiplePropertyValueKind == SyntaxKind.FalseLiteralExpression)
766						multiplePropertyValue = false;
767					else
768					{
769						// Only looking for very simple cases - where explicitly set to true or to false or
770						// not set at all (defaulting to false). If it's set according to a method return
771						// value or a variable then give up (this is just intended to catch obvious
772						// mistakes, not to perform deep and complex analysis)
773						return;
774					}
775				}
777				var valuePropertyIsSpecified = propertyInitialisers.Any(
778					propertyInitialiser => propertyInitialiser.PropertyName == "Value"
779				);
780				var valuesPropertyIsSpecified = propertyInitialisers.Any(
781					propertyInitialiser => propertyInitialiser.PropertyName == "Values"
782				);
783				if (!valuePropertyIsSpecified && !valuesPropertyIsSpecified)
784					return;
786				var objectCreation = (ObjectCreationExpressionSyntax)context.Node.Parent;
787				var objectCreationTypeInfo = context.SemanticModel.GetTypeInfo(objectCreation);
788				if ((objectCreationTypeInfo.Type is IErrorTypeSymbol)
789				|| (objectCreationTypeInfo.Type.ContainingAssembly.Identity.Name != "Bridge.React")
790				|| (objectCreationTypeInfo.Type.Name != "SelectAttributes"))
791					return;
793				if ((multiplePropertyValue == true) && valuePropertyIsSpecified)
794				{
795					context.ReportDiagnostic(Diagnostic.Create(
796						MultipleWithValueRule,
797						context.Node.GetLocation()
798					));
799				}
800				else if ((multiplePropertyValue == false) && valuesPropertyIsSpecified)
801				{
802					context.ReportDiagnostic(Diagnostic.Create(
803						NoMultipleWithValuesRule,
804						context.Node.GetLocation()
805					));
806				}
807			}
809			private static LocalizableString GetLocalizableString(string nameOfLocalizableResource)
810			{
811				return new LocalizableResourceString(
812					nameOfLocalizableResource,
813					Resources.ResourceManager,
814					typeof(Resources)
815				);
816			}
817		}
818	}