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

/Blog/App_Data/Posts/102,2016,6,28,7,49,0,1,Roslyn.txt

https://bitbucket.org/DanRoberts/blog
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
  2
  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).
  4
  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.
  6
  7Most of the analysers I've been writing have been for libraries that work with [Bridge.NET](http://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.
  8
  9### Creating a fresh Analyser project
 10
 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).
 12
 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,
 14
 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.
 16
 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) -
 18
 19	using System.Collections.Immutable;
 20	using System.Linq;
 21	using Microsoft.CodeAnalysis;
 22	using Microsoft.CodeAnalysis.Diagnostics;
 23
 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.";
 37
 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			);
 47
 48			public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
 49			{
 50				get { return ImmutableArray.Create(Rule); }
 51			}
 52
 53			public override void Initialize(AnalysisContext context)
 54			{
 55				context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);
 56			}
 57
 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	}
 72
 73To summarise what's in the above code:
 74
 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
 78
 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.
 80
 81To illustrate how this should work, the test project includes the following test method -
 82
 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;
 93
 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		};
109
110		VerifyCSharpDiagnostic(test, expected);
111	}
112
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".
114
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.
116
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").*
118
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.
120
121![The Syntax Visualizer](/Content/Images/Posts/SyntaxVisualizer.png)
122
123### Writing a real analyser
124
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.
126
127The analyser is part of my [Bridge.React](https://www.nuget.org/packages/Bridge.React) library but you won't need to know anything about React or Bridge to follow along.
128
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.
130
131I wanted an analyser that handled these simple cases -
132
133    // This is fine
134	return DOM.Select(new SelectAttributes { Multiple = false, Value = "x" };
135    
136    // This is fine
137	return DOM.Select(new SelectAttributes { Multiple = true, Values = new [] { "x", "y" } };
138    
139	// Wrong (shouldn't use "Value" when "Multiple" is true)
140	return DOM.Select(new SelectAttributes { Multiple = true, Value = "x" };
141    
142	// Wrong (shouldn't use "Values" when "Multiple" is false)
143    return DOM.Select(new SelectAttributes { Multiple = false, Values = new [] { "x", "y" } };
144    
145	// Wrong (shouldn't use "Values" when "Multiple" defaults to false)
146    return DOM.Select(new SelectAttributes { Values = new [] { "x", "y" } };
147
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.
149
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:
151
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	}
161
162This will be enough to start writing the analyser.
163
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.
165
166So, replace the contents of "UnitTest.cs" with the following:
167
168	using Microsoft.CodeAnalysis;
169	using Microsoft.CodeAnalysis.Diagnostics;
170	using Microsoft.VisualStudio.TestTools.UnitTesting;
171	using TestHelper;
172
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;
183
184					namespace TestCase
185					{
186						public class Example
187						{
188							public void Go()
189							{
190								new SelectAttributes { Multiple = true, Value = ""1"" };
191							}
192						}
193					}";
194
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				};
205
206				VerifyCSharpDiagnostic(testContent, expected);
207			}
208
209			[TestMethod]
210			public void DoNotUseValuesWhenMultipleIsFalse()
211			{
212				var testContent = @"
213					using Bridge.React;
214
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					}";
225
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				};
236
237				VerifyCSharpDiagnostic(testContent, expected);
238			}
239
240			[TestMethod]
241			public void DoNotUseValueWhenMultipleDefaultsToFalse()
242			{
243				var testContent = @"
244					using Bridge.React;
245
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					}";
257
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				};
268
269				VerifyCSharpDiagnostic(testContent, expected);
270			}
271
272			protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
273			{
274				return new ExampleAnalyserAnalyzer();
275			}
276		}
277	}
278
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.
280
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.
282
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:
285
286        private static readonly MetadataReference CSharpBridgeReactReference
287			= MetadataReference.CreateFromFile(typeof(Bridge.React.SelectAttributes).Assembly.Location);
288
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:
290
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        }
316
317### *Really* writing the analyser
318
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.
320
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:
322
323    var x = new Bridge.React.SelectAttributes { Multiple = true, Value = "x" };
324
325Ensuring that the Syntax Visualizer is visible (View / Other Windows / Syntax Visualizer), clicking on "Multiple" shows the following:
326
327![ObjectInitializerExpression](/Content/Images/Posts/ObjectInitializerExpression.png)
328
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.
330
331So, how do we alter the analyser class that we currently have in order to identify object initialisers such as this?
332
333Currently the example analyser class has an "Initialize" method that looks like this:
334
335	public override void Initialize(AnalysisContext context)
336	{
337		context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);
338	}
339
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.
341
342Googling for ["AnalysisContext ObjectInitializerExpression"](https://www.google.co.uk/search?q=AnalysisContext+ObjectInitializerExpression) doesn't actually return that many results. However, the second one [RoslynClrHeapAllocationAnalyzer/ExplicitAllocationAnalyzer.cs](https://github.com/mjsabby/RoslynClrHeapAllocationAnalyzer/blob/master/ClrHeapAllocationsAnalyzer/ExplicitAllocationAnalyzer.cs) has some code that looks promising:
343
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	}
360
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.
362
363Actually, by starting to change the "Initialize" method to
364
365	public override void Initialize(AnalysisContext context)
366	{
367		context.RegisterSyntaxNodeAction(AnalyzeSymbol,
368
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:
370
371	public override void Initialize(AnalysisContext context)
372	{
373		context.RegisterSyntaxNodeAction(
374			AnalyzeSymbol,
375			SyntaxKind.ObjectInitializerExpression
376		);
377	}
378	
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;**).
380
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" -
382
383	public override void Initialize(AnalysisContext context)
384	{
385		context.RegisterSyntaxNodeAction(
386			LookForInvalidSelectAttributeProperties,
387			SyntaxKind.ObjectInitializerExpression
388		);
389	}
390
391	private static void LookForInvalidSelectAttributeProperties(SyntaxNodeAnalysisContext context)
392	{
393	}
394
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".
396
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.*
398
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.
400
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:
402
403    // This doesn't work,SimpleAssignmentExpressionSyntax isn't a real class :(
404	var propertyInitialisers = context.Node.ChildNodes()
405		.OfType<SimpleAssignmentExpressionSyntax>();
406
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.
408
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] -
410
411	context.Node.ChildNodes().First().GetType().Name
412	
413This displays "AssignmentExpressionSyntax".
414
415This clue is enough to stop the debugger and go back to trying to populate the "LookForInvalidSelectAttributeProperties". It may now start with:
416
417	var propertyInitialisers = context.Node.ChildNodes()
418		.OfType<AssignmentExpressionSyntax>();
419
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.
421
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.
423
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:
425
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		});
433
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:
435
436	var x = new MyClass { Name.Value = "Ted };
437
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.
439
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:
441
442	[TestMethod]
443	public void IgnoreInvalidPropertySetting()
444	{
445		var testContent = @"
446			using Bridge.React;
447
448			namespace TestCase
449			{
450				public class Example
451				{
452					public void Go()
453					{
454						new SelectAttributes { Nested.Multiple = true };
455					}
456				}
457			}";
458
459		VerifyCSharpDiagnostic(testContent);
460	}
461
462*Note: Calling the "VerifyCSharpDiagnostic" with no "expected" value means that the test expects that the analyser will not report any violated rules.*
463
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:
465
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		});
473
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	}
496
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.
498
499From the above code, it should be fairly clear that the way to do this is the following:
500
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;
509
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.
511
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).
513
514"GetTypeInfo" returns a **TypeInfo** instance which has two properties; "Type" and "ConvertedType". "ConvertedType" is (taken from the xml summary documentation):
515
516> The type of the expression after it has undergone an implicit conversion
517
518which shouldn't apply here, so we'll just look at "Type". Note, though, that the documentation for "Type" says that:
519
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.
521
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.
523
524This is the code that needs adding to ensure that the properties that we're looking at are for a Bridge.React.SelectAttributes -
525
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;
532
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".
534
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				}
552
553				public class SelectAttributes
554				{
555					public bool Multiple { get; set; }
556					public string Value { get; set; }
557				}
558			}";
559
560		VerifyCSharpDiagnostic(testContent);
561	}
562	
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.
564
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:
566
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";
575
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	);
592
593	public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
594	{
595		get { return ImmutableArray.Create(MultipleWithValueRule, NoMultipleWithValuesRule); }
596	}
597
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:
599
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	}
614
615### Localisation support
616
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:
618
619> **Name:** SelectAttributesAnalyserTitle
620
621> **Value:** Be careful to use the appropriate 'Value' or 'Values' property for the 'Multiple' setting
622 
623> **Name:** SelectAttributesAnalyserMultipleWithValueMessage
624
625> **Value:** If 'Multiple' is true then the 'Values' property should be used instead of 'Value'
626
627> **Name:** SelectAttributesAnalyserNoMultipleWithValuesTitle
628
629> **Value:** If 'Multiple' is false then the 'Value' property should be used instead of 'Values'
630
631To make accessing these resources a little easier, add the following method to the bottom of the analyser class:
632
633	private static LocalizableString GetLocalizableString(string nameOfLocalizableResource)
634	{
635		return new LocalizableResourceString(
636			nameOfLocalizableResource,
637			Resources.ResourceManager,
638			typeof(Resources)
639		);
640	}
641
642Finally, replace the three hard-coded string property initialisers with the following:
643
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		);
653
654### Summary
655
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.
657
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.
659
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](http://bridge.net/) libraries.
661
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:
663
664    DOM.Select(
665		new SelectAttributes { Value = selectedId },
666		options
667	)
668	
669.. and changed it to:
670
671    DOM.Select(
672		new SelectAttributes { Multiple = true, Value = selectedId },
673		options
674	)
675
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*:
677
678    DOM.Select(
679		new SelectAttributes { Multiple = true, Values = new[] { selectedId } },
680		options
681	)
682
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.
684
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.
686
687### The complete analyser
688
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;
696
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";
713
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			);
730
731			public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
732			{
733				get { return ImmutableArray.Create(MultipleWithValueRule, NoMultipleWithValuesRule); }
734			}
735
736			public override void Initialize(AnalysisContext context)
737			{
738				context.RegisterSyntaxNodeAction(
739					LookForInvalidSelectAttributeProperties,
740					SyntaxKind.ObjectInitializerExpression
741				);
742			}
743
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					});
753
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				}
776
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;
785
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;
792
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			}
808
809			private static LocalizableString GetLocalizableString(string nameOfLocalizableResource)
810			{
811				return new LocalizableResourceString(
812					nameOfLocalizableResource,
813					Resources.ResourceManager,
814					typeof(Resources)
815				);
816			}
817		}
818	}