diff --git a/MSBuildLocator.sln b/MSBuildLocator.sln index 4540e62d..e20b3d07 100644 --- a/MSBuildLocator.sln +++ b/MSBuildLocator.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26730.10 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30807.9 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Build.Locator", "src\MSBuildLocator\Microsoft.Build.Locator.csproj", "{2F4700C6-A6F6-49DF-ABF1-6213AAC4BCFD}" EndProject @@ -9,6 +9,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Build.Locator.Tes EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BuilderApp", "samples\BuilderApp\BuilderApp.csproj", "{70F35A21-43E2-47CB-8B96-B160C6758EAA}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MSBuildLocatorAnalyzer", "src\MSBuildLocatorAnalyzer\MSBuildLocatorAnalyzer.csproj", "{BFF38CC3-651E-4BCF-BD06-792DE348EA2C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MSBuildLocatorAnalyzer.CodeFixes", "src\MSBuildLocatorAnalyzer.CodeFixes\MSBuildLocatorAnalyzer.CodeFixes.csproj", "{78A91E45-74F2-438A-8B4B-8DEDE153E1E5}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{872EA464-8296-4ECA-932F-FC33841252EF}" ProjectSection(SolutionItems) = preProject Directory.Build.props = Directory.Build.props @@ -33,6 +37,14 @@ Global {70F35A21-43E2-47CB-8B96-B160C6758EAA}.Debug|Any CPU.Build.0 = Debug|Any CPU {70F35A21-43E2-47CB-8B96-B160C6758EAA}.Release|Any CPU.ActiveCfg = Release|Any CPU {70F35A21-43E2-47CB-8B96-B160C6758EAA}.Release|Any CPU.Build.0 = Release|Any CPU + {BFF38CC3-651E-4BCF-BD06-792DE348EA2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BFF38CC3-651E-4BCF-BD06-792DE348EA2C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BFF38CC3-651E-4BCF-BD06-792DE348EA2C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BFF38CC3-651E-4BCF-BD06-792DE348EA2C}.Release|Any CPU.Build.0 = Release|Any CPU + {78A91E45-74F2-438A-8B4B-8DEDE153E1E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {78A91E45-74F2-438A-8B4B-8DEDE153E1E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {78A91E45-74F2-438A-8B4B-8DEDE153E1E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {78A91E45-74F2-438A-8B4B-8DEDE153E1E5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/samples/BuilderApp/Program.cs b/samples/BuilderApp/Program.cs index 0ec2f0bc..8caac6f0 100644 --- a/samples/BuilderApp/Program.cs +++ b/samples/BuilderApp/Program.cs @@ -42,8 +42,18 @@ private static void Main(string[] args) MSBuildLocator.RegisterMSBuildPath(msbuildDeploymentToUse.MSBuildPath); } + var assembly = typeof(Project).Assembly; + FileVersionInfo fvi = FileVersionInfo.GetVersionInfo(assembly.Location); + + Console.WriteLine(); + Console.WriteLine($"BuildApp running using MSBuild version {fvi.FileVersion}"); + Console.WriteLine(Path.GetDirectoryName(assembly.Location)); + Console.WriteLine(); - var result = new Builder().Build(projectFilePath); + var pre = ProjectRootElement.Open(projectFilePath); + var project = new Project(pre); + var result = project.Build(new Builder.Logger()); + //var result = new Builder().Build(projectFilePath); Console.WriteLine(); Console.ForegroundColor = result ? ConsoleColor.Green : ConsoleColor.Red; @@ -154,7 +164,7 @@ public bool Build(string projectFile) return project.Build(new Logger()); } - private class Logger : ILogger + public class Logger : ILogger { public void Initialize(IEventSource eventSource) { diff --git a/src/MSBuildLocatorAnalyzer.CodeFixes/CodeFixResources.Designer.cs b/src/MSBuildLocatorAnalyzer.CodeFixes/CodeFixResources.Designer.cs new file mode 100644 index 00000000..749477d4 --- /dev/null +++ b/src/MSBuildLocatorAnalyzer.CodeFixes/CodeFixResources.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace MSBuildLocatorAnalyzer { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class CodeFixResources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal CodeFixResources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MSBuildLocatorAnalyzer.CodeFixResources", typeof(CodeFixResources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Make uppercase. + /// + internal static string CodeFixTitle { + get { + return ResourceManager.GetString("CodeFixTitle", resourceCulture); + } + } + } +} diff --git a/src/MSBuildLocatorAnalyzer.CodeFixes/CodeFixResources.resx b/src/MSBuildLocatorAnalyzer.CodeFixes/CodeFixResources.resx new file mode 100644 index 00000000..97abe689 --- /dev/null +++ b/src/MSBuildLocatorAnalyzer.CodeFixes/CodeFixResources.resx @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Make uppercase + The title of the code fix. + + \ No newline at end of file diff --git a/src/MSBuildLocatorAnalyzer.CodeFixes/MSBuildLocatorAnalyzer.CodeFixes.csproj b/src/MSBuildLocatorAnalyzer.CodeFixes/MSBuildLocatorAnalyzer.CodeFixes.csproj new file mode 100644 index 00000000..559d53de --- /dev/null +++ b/src/MSBuildLocatorAnalyzer.CodeFixes/MSBuildLocatorAnalyzer.CodeFixes.csproj @@ -0,0 +1,37 @@ + + + + net48;netcoreapp3.1 + true + MSBuildLocatorAnalyzer + false + True + + + + Microsoft.Build.Locator.Analyzer.CodeFixes + 1.0.0 + Microsoft + https://github.com/microsoft/MSBuildLocator + false + A code fix for the MSBuildLocator analyzer. + Code fix for the analyzer. + Copyright + MSBuild, MSBuildLocator, Microsoft Build, code fix, analyzers + true + + + + + + + + + + + + + + + + diff --git a/src/MSBuildLocatorAnalyzer.CodeFixes/MSBuildLocatorAnalyzerCodeFixProvider.cs b/src/MSBuildLocatorAnalyzer.CodeFixes/MSBuildLocatorAnalyzerCodeFixProvider.cs new file mode 100644 index 00000000..60ba7cfc --- /dev/null +++ b/src/MSBuildLocatorAnalyzer.CodeFixes/MSBuildLocatorAnalyzerCodeFixProvider.cs @@ -0,0 +1,62 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Formatting; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading.Tasks; + +namespace MSBuildLocatorAnalyzer +{ + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(MSBuildLocatorAnalyzerCodeFixProvider)), Shared] + public class MSBuildLocatorAnalyzerCodeFixProvider : CodeFixProvider + { + private const string title = "Move MSBuild use to separate method"; + + public sealed override ImmutableArray FixableDiagnosticIds + { + get { return ImmutableArray.Create(MSBuildLocatorAnalyzerAnalyzer.DiagnosticId); } + } + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var diagnostic = context.Diagnostics.First(); + + // Register a code action that will invoke the fix. + context.RegisterCodeFix( + CodeAction.Create( + title: title, + createChangedDocument: c => MoveMSBuildUseToSeparateMethod(context.Document, diagnostic, root), + equivalenceKey: title), + diagnostic); + } + + Task MoveMSBuildUseToSeparateMethod(Document document, Diagnostic diagnostic, SyntaxNode root) + { + StatementSyntax statement = root.FindNode(diagnostic.Location.SourceSpan).FirstAncestorOrSelf(); + SyntaxNode method; + for (method = root.FindNode(diagnostic.Location.SourceSpan); !(method is MethodDeclarationSyntax); method = method.Parent); + SyntaxTokenList modifiers = (method as MethodDeclarationSyntax).Modifiers.Any(mod => mod.ValueText.Equals("static")) ? + SyntaxFactory.TokenList(SyntaxFactory.Identifier("private"), SyntaxFactory.Identifier("static")) : + SyntaxFactory.TokenList(SyntaxFactory.Identifier("private")); + MethodDeclarationSyntax toInsert = SyntaxFactory.MethodDeclaration(SyntaxFactory.ParseTypeName("void"), "UseMSBuild") + .WithModifiers(modifiers) + .WithBody(SyntaxFactory.Block(statement)) + .WithAdditionalAnnotations(Formatter.Annotation); + var newRoot = root.InsertNodesAfter(method, new List() { toInsert }); + + statement = newRoot.FindNode(diagnostic.Location.SourceSpan).FirstAncestorOrSelf(); + ExpressionStatementSyntax expression = SyntaxFactory.ExpressionStatement(SyntaxFactory.InvocationExpression(SyntaxFactory.IdentifierName(toInsert.Identifier.ValueText)) + .WithArgumentList(SyntaxFactory.ArgumentList() + .WithOpenParenToken(SyntaxFactory.Token(SyntaxKind.OpenParenToken)) + .WithCloseParenToken(SyntaxFactory.Token(SyntaxKind.CloseParenToken)))); + newRoot = newRoot.ReplaceNode(statement, expression); + return Task.FromResult(document.WithSyntaxRoot(newRoot)); + } + } +} diff --git a/src/MSBuildLocatorAnalyzer/MSBuildLocatorAnalyzer.csproj b/src/MSBuildLocatorAnalyzer/MSBuildLocatorAnalyzer.csproj new file mode 100644 index 00000000..536dcc53 --- /dev/null +++ b/src/MSBuildLocatorAnalyzer/MSBuildLocatorAnalyzer.csproj @@ -0,0 +1,31 @@ + + + + net48;netcoreapp3.1 + true + false + True + + + Microsoft.Build.Locator.Analyzer + 1.0.0 + Microsoft + https://github.com/microsoft/MSBuildLocator + false + Analyzers for MSBuildLocator + An analyzer that identifies the common problem in MSBuildLocator of registering MSBuild in the same method that uses it. + Copyright + MSBuildLocator, Build, MSBuild, Analyzers + + + + + + + + + + + + + diff --git a/src/MSBuildLocatorAnalyzer/MSBuildLocatorAnalyzerAnalyzer.cs b/src/MSBuildLocatorAnalyzer/MSBuildLocatorAnalyzerAnalyzer.cs new file mode 100644 index 00000000..539b2bb8 --- /dev/null +++ b/src/MSBuildLocatorAnalyzer/MSBuildLocatorAnalyzerAnalyzer.cs @@ -0,0 +1,75 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace MSBuildLocatorAnalyzer +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class MSBuildLocatorAnalyzerAnalyzer : DiagnosticAnalyzer + { + public const string DiagnosticId = "MSBuildLocatorAnalyzer"; + + // You can change these strings in the Resources.resx file. If you do not want your analyzer to be localize-able, you can use regular strings for Title and MessageFormat. + // See https://github.com/dotnet/roslyn/blob/master/docs/analyzers/Localizing%20Analyzers.md for more on localization + private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.AnalyzerTitle), Resources.ResourceManager, typeof(Resources)); + private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.AnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources)); + private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.AnalyzerDescription), Resources.ResourceManager, typeof(Resources)); + private const string Category = "Naming"; + + private readonly string[] msBuildAssemblies = + { + "Microsoft.Build", + "Microsoft.Build.Engine", + "Microsoft.Build.Framework", + "Microsoft.Build.Tasks.Core", + "Microsoft.Build.Utilities.Core" + }; + + private readonly string[] msbuildLocatorRegisterMethods = + { + "RegisterDefaults", + "RegisterInstance", + "RegisterMSBuildPath" + }; + + private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description); + + public override ImmutableArray SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } } + + public override void Initialize(AnalysisContext context) + { + context.RegisterOperationAction(opact => + { + var invocation = (IInvocationOperation)opact.Operation; + var targetMethod = invocation.TargetMethod; + if (targetMethod?.ContainingAssembly != null) + { + if (targetMethod.ContainingAssembly.Name.Equals("Microsoft.Build.Locator") && msbuildLocatorRegisterMethods.Contains(targetMethod.Name)) + { + MethodDeclarationSyntax method = (MethodDeclarationSyntax)invocation.Syntax.FirstAncestorOrSelf((Func)(a => a is MethodDeclarationSyntax)); + if (method != null) + { + IOperation symbolParent; + for (symbolParent = invocation; symbolParent.Parent != null && symbolParent.Kind != OperationKind.MethodBody; symbolParent = symbolParent.Parent); + foreach (var child in symbolParent.Descendants()) + { + if ((child is IInvocationOperation op && msBuildAssemblies.Contains(op.TargetMethod?.ContainingAssembly?.Name)) || + (child is ITypeOfOperation toOp && msBuildAssemblies.Contains(toOp.TypeOperand.ContainingAssembly?.Name)) || + (msBuildAssemblies.Contains(child.Type?.ContainingAssembly?.Name))) + { + opact.ReportDiagnostic(Diagnostic.Create(Rule, child.Syntax.GetLocation())); + } + } + } + } + } + }, OperationKind.Invocation); + } + } +} diff --git a/src/MSBuildLocatorAnalyzer/NuGet.config b/src/MSBuildLocatorAnalyzer/NuGet.config new file mode 100644 index 00000000..cba8a3e3 --- /dev/null +++ b/src/MSBuildLocatorAnalyzer/NuGet.config @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/MSBuildLocatorAnalyzer/Resources.Designer.cs b/src/MSBuildLocatorAnalyzer/Resources.Designer.cs new file mode 100644 index 00000000..2443d5ad --- /dev/null +++ b/src/MSBuildLocatorAnalyzer/Resources.Designer.cs @@ -0,0 +1,90 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace MSBuildLocatorAnalyzer { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MSBuildLocatorAnalyzer.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to References to MSBuild types and methods should be in a separate method from the call to MSBuildLocator.RegisterMSBuildPath, MSBuildLocator.RegisterInstance, or MSBuildLocator.RegisterDefaults.. + /// + internal static string AnalyzerDescription { + get { + return ResourceManager.GetString("AnalyzerDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use of MSBuild type or method is in the same method as call to register MSBuild.. + /// + internal static string AnalyzerMessageFormat { + get { + return ResourceManager.GetString("AnalyzerMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MSBuild type or method used before being registered.. + /// + internal static string AnalyzerTitle { + get { + return ResourceManager.GetString("AnalyzerTitle", resourceCulture); + } + } + } +} diff --git a/src/MSBuildLocatorAnalyzer/Resources.resx b/src/MSBuildLocatorAnalyzer/Resources.resx new file mode 100644 index 00000000..6714893e --- /dev/null +++ b/src/MSBuildLocatorAnalyzer/Resources.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + References to MSBuild types and methods should be in a separate method from the call to MSBuildLocator.RegisterMSBuildPath, MSBuildLocator.RegisterInstance, or MSBuildLocator.RegisterDefaults. + An optional longer localizable description of the diagnostic. + + + Use of MSBuild type or method is in the same method as call to register MSBuild. + The format-able message the diagnostic displays. + + + MSBuild type or method used before being registered. + The title of the diagnostic. + + \ No newline at end of file