From d34380fcdc8ea64e915ed73be7caf30c4d468056 Mon Sep 17 00:00:00 2001 From: duncanp-sonar Date: Mon, 6 Aug 2018 13:33:58 +0100 Subject: [PATCH] Fixed #89: allow user to customize rules xml (#90) --- .../DataModel/DebtRemediationFunctionType.cs | 30 ++++ PluginGenerator/DataModel/IssueType.cs | 29 ++++ PluginGenerator/DataModel/Rule.cs | 33 +++++ .../SonarQube.Plugins.PluginGenerator.csproj | 2 + .../AnalyzerPluginGenerator.cs | 76 +++++++++- .../CommandLine/ArgumentProcessor.cs | 58 ++++++-- .../CommandLine/CmdLineResources.Designer.cs | 31 ++++- .../CommandLine/CmdLineResources.resx | 11 +- .../CommandLine/ProcessedArgs.cs | 6 +- RoslynPluginGenerator/UIResources.Designer.cs | 47 ++++++- RoslynPluginGenerator/UIResources.resx | 21 ++- Tests/CommonTests/SerializationTests.cs | 10 +- .../IntegrationTests/Roslyn/RoslynGenTests.cs | 6 +- .../AnalyzerPluginGeneratorTests.cs | 131 +++++++++++++++++- .../ArchiveUpdaterTests.cs | 6 +- .../ArgumentProcessorTests.cs | 37 ++++- .../ProcessedArgsBuilder.cs | 93 +++++++++++++ ...s.Roslyn.RoslynPluginGeneratorTests.csproj | 8 ++ 18 files changed, 594 insertions(+), 41 deletions(-) create mode 100644 PluginGenerator/DataModel/DebtRemediationFunctionType.cs create mode 100644 PluginGenerator/DataModel/IssueType.cs create mode 100644 Tests/RoslynPluginGeneratorTests/ProcessedArgsBuilder.cs diff --git a/PluginGenerator/DataModel/DebtRemediationFunctionType.cs b/PluginGenerator/DataModel/DebtRemediationFunctionType.cs new file mode 100644 index 0000000..a30ae32 --- /dev/null +++ b/PluginGenerator/DataModel/DebtRemediationFunctionType.cs @@ -0,0 +1,30 @@ +/* + * SonarQube Roslyn SDK + * Copyright (C) 2015-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarQube.Plugins.Roslyn +{ + public enum DebtRemediationFunctionType + { + Unspecified, + LINEAR, // Currently not supported by the C# plugin (v7.3) + LINEAR_OFFSET, // Currently not supported by the C# plugin (v7.3) + CONSTANT_ISSUE + } +} diff --git a/PluginGenerator/DataModel/IssueType.cs b/PluginGenerator/DataModel/IssueType.cs new file mode 100644 index 0000000..a92a217 --- /dev/null +++ b/PluginGenerator/DataModel/IssueType.cs @@ -0,0 +1,29 @@ +/* + * SonarQube Roslyn SDK + * Copyright (C) 2015-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + + namespace SonarQube.Plugins.Roslyn +{ + public enum IssueType + { + CODE_SMELL, + BUG, + VULNERABILITY + } +} diff --git a/PluginGenerator/DataModel/Rule.cs b/PluginGenerator/DataModel/Rule.cs index 2fb4d67..df5df34 100644 --- a/PluginGenerator/DataModel/Rule.cs +++ b/PluginGenerator/DataModel/Rule.cs @@ -68,9 +68,42 @@ public XmlCDataSection DescriptionAsCDATA [XmlElement(ElementName = "status")] public string Status { get; set; } + [XmlElement(ElementName = "type")] + public IssueType Type { get; set; } + [XmlElement(ElementName = "tag")] public string[] Tags { get; set; } + [XmlIgnore] + public DebtRemediationFunctionType DebtRemediationFunction { get; set; } + + /// + /// Do not read/write this property directly - use instead. + /// + /// This property is a hack that allows us to use an enum to specify + /// debt remediation function, but to have nothing written to the XML if the + /// the value is not specified. There are other ways to do this, but they required + /// much more code. + [XmlElement(ElementName = "debtRemediationFunction")] + public string DebtRemediationFunction_DoNotUse_ForSerializationOnly + { + get + { + if (DebtRemediationFunction == DebtRemediationFunctionType.Unspecified) + { + return null; + } + return DebtRemediationFunction.ToString(); + } + set + { + this.DebtRemediationFunction = (DebtRemediationFunctionType)Enum.Parse(typeof(DebtRemediationFunctionType), value); + } + } + + [XmlElement(ElementName = "debtRemediationFunctionOffset")] + public string DebtRemediationFunctionOffset { get; set; } + /// /// Specified the culture and case when comparing rule keys /// diff --git a/PluginGenerator/SonarQube.Plugins.PluginGenerator.csproj b/PluginGenerator/SonarQube.Plugins.PluginGenerator.csproj index c4fa667..4dfcddb 100644 --- a/PluginGenerator/SonarQube.Plugins.PluginGenerator.csproj +++ b/PluginGenerator/SonarQube.Plugins.PluginGenerator.csproj @@ -52,6 +52,8 @@ + + diff --git a/RoslynPluginGenerator/AnalyzerPluginGenerator.cs b/RoslynPluginGenerator/AnalyzerPluginGenerator.cs index 26480c6..e6e00fb 100644 --- a/RoslynPluginGenerator/AnalyzerPluginGenerator.cs +++ b/RoslynPluginGenerator/AnalyzerPluginGenerator.cs @@ -45,6 +45,11 @@ public class AnalyzerPluginGenerator /// private static readonly string[] excludedFileExtensions = { ".nupkg", ".nuspec" }; + /// + /// Specifies the format for the name of the template rules xml file + /// + public const string RulesTemplateFileNameFormat = "{0}.{1}.rules.template.xml"; + /// /// Specifies the format for the name of the placeholder SQALE file /// @@ -134,7 +139,7 @@ public bool Generate(ProcessedArgs args) // Initial run with the user-targeted package and arguments if (analyzersByPackage.ContainsKey(targetPackage)) { - string generatedJarPath = GeneratePluginForPackage(args.OutputDirectory, args.Language, args.SqaleFilePath, targetPackage, analyzersByPackage[targetPackage]); + string generatedJarPath = GeneratePluginForPackage(args.OutputDirectory, args.Language, args.SqaleFilePath, args.RuleFilePath, targetPackage, analyzersByPackage[targetPackage]); if (generatedJarPath == null) { return false; @@ -147,12 +152,12 @@ public bool Generate(ProcessedArgs args) // Dependent package generation changes the arguments if (args.RecurseDependencies) { - logger.LogWarning(UIResources.APG_RecurseEnabled_SQALENotEnabled); + logger.LogWarning(UIResources.APG_RecurseEnabled_SQALEandRuleCustomisationNotEnabled); foreach (IPackage currentPackage in analyzersByPackage.Keys) { - // No way to specify the SQALE file for any but the user-targeted package at this time - string generatedJarPath = GeneratePluginForPackage(args.OutputDirectory, args.Language, null, currentPackage, analyzersByPackage[currentPackage]); + // No way to specify the SQALE or rules xml files for any but the user-targeted package at this time + string generatedJarPath = GeneratePluginForPackage(args.OutputDirectory, args.Language, null, null, currentPackage, analyzersByPackage[currentPackage]); if (generatedJarPath == null) { return false; @@ -172,7 +177,7 @@ public bool Generate(ProcessedArgs args) return true; } - private string GeneratePluginForPackage(string outputDir, string language, string sqaleFilePath, IPackage package, IEnumerable analyzers) + private string GeneratePluginForPackage(string outputDir, string language, string sqaleFilePath, string rulesFilePath, IPackage package, IEnumerable analyzers) { Debug.Assert(analyzers.Any(), "The method must be called with a populated list of DiagnosticAnalyzers."); @@ -187,6 +192,7 @@ private string GeneratePluginForPackage(string outputDir, string language, strin { Language = language, SqaleFilePath = sqaleFilePath, + RulesFilePath = rulesFilePath, PackageId = package.Id, PackageVersion = package.Version.ToString(), Manifest = CreatePluginManifest(package) @@ -197,10 +203,23 @@ private string GeneratePluginForPackage(string outputDir, string language, strin definition.SourceZipFilePath = CreateAnalyzerStaticPayloadFile(packageDir, baseDirectory); definition.StaticResourceName = Path.GetFileName(definition.SourceZipFilePath); - definition.RulesFilePath = GenerateRulesFile(analyzers, baseDirectory); + bool generate = true; + + string generatedRulesTemplateFile = null; + if (definition.RulesFilePath == null) + { + definition.RulesFilePath = GenerateRulesFile(analyzers, baseDirectory); + generatedRulesTemplateFile = CalculateRulesTemplateFileName(package, outputDir); + File.Copy(definition.RulesFilePath, generatedRulesTemplateFile, overwrite: true); + } + else + { + this.logger.LogInfo(UIResources.APG_UsingSuppliedRulesFile, definition.RulesFilePath); + generate = IsValidRulesFile(definition.RulesFilePath); + } string generatedSqaleFile = null; - bool generate = true; + if (definition.SqaleFilePath == null) { generatedSqaleFile = CalculateSqaleFileName(package, outputDir); @@ -209,6 +228,7 @@ private string GeneratePluginForPackage(string outputDir, string language, strin } else { + this.logger.LogInfo(UIResources.APG_UsingSuppliedSqaleFile, definition.SqaleFilePath); generate = IsValidSqaleFile(definition.SqaleFilePath); } @@ -217,11 +237,21 @@ private string GeneratePluginForPackage(string outputDir, string language, strin createdJarFilePath = BuildPlugin(definition, outputDir); } + LogMessageForGeneratedRules(generatedRulesTemplateFile); LogMessageForGeneratedSqale(generatedSqaleFile); return createdJarFilePath; } + private void LogMessageForGeneratedRules(string generatedFile) + { + if (generatedFile != null) + { + // Log a message about the generated rules xml file for every plugin generated + this.logger.LogInfo(UIResources.APG_TemplateRuleFileGenerated, generatedFile); + } + } + private void LogMessageForGeneratedSqale(string generatedSqaleFile) { if (generatedSqaleFile != null) @@ -347,6 +377,15 @@ private string GenerateRulesFile(IEnumerable analyzers, stri return rulesFilePath; } + private static string CalculateRulesTemplateFileName(IPackage package, string directory) + { + string filePath = string.Format(System.Globalization.CultureInfo.CurrentCulture, + RulesTemplateFileNameFormat, package.Id, package.Version); + + filePath = Path.Combine(directory, filePath); + return filePath; + } + private static string CalculateSqaleFileName(IPackage package, string directory) { string filePath = string.Format(System.Globalization.CultureInfo.CurrentCulture, @@ -371,6 +410,29 @@ private void GenerateFixedSqaleFile(IEnumerable analyzers, s logger.LogDebug(UIResources.APG_SqaleGeneratedToFile, outputFilePath); } + /// + /// Checks that the supplied rule file has valid content + /// + private bool IsValidRulesFile(string filePath) + { + Debug.Assert(!string.IsNullOrWhiteSpace(filePath)); + // Existence is checked when parsing the arguments + Debug.Assert(File.Exists(filePath), "Expecting the rule file to exist: " + filePath); + + try + { + // TODO: consider adding further checks + Serializer.LoadModel(filePath); + + } + catch (InvalidOperationException) // will be thrown for invalid xml + { + this.logger.LogError(UIResources.APG_InvalidRulesFile, filePath); + return false; + } + return true; + } + /// /// Checks that the supplied sqale file has valid content /// diff --git a/RoslynPluginGenerator/CommandLine/ArgumentProcessor.cs b/RoslynPluginGenerator/CommandLine/ArgumentProcessor.cs index 70367e8..6718f51 100644 --- a/RoslynPluginGenerator/CommandLine/ArgumentProcessor.cs +++ b/RoslynPluginGenerator/CommandLine/ArgumentProcessor.cs @@ -39,6 +39,7 @@ private static class KeywordIds { public const string AnalyzerRef = "analyzer.ref"; public const string SqaleXmlFile = "sqale.xml"; + public const string RuleXmlFile = "rules.xml"; public const string AcceptLicenses = "accept.licenses"; public const string RecurseDependencies = "recurse.dependencies"; } @@ -55,6 +56,8 @@ static ArgumentProcessor() id: KeywordIds.AnalyzerRef, prefixes: new string[] { "/analyzer:", "/a:" }, required: true, allowMultiple: false, description: CmdLineResources.ArgDescription_AnalzyerRef), new ArgumentDescriptor( id: KeywordIds.SqaleXmlFile, prefixes: new string[] { "/sqale:" }, required: false, allowMultiple: false, description: CmdLineResources.ArgDescription_SqaleXmlFile), + new ArgumentDescriptor( + id: KeywordIds.RuleXmlFile, prefixes: new string[] { "/rules:" }, required: false, allowMultiple: false, description: CmdLineResources.ArgDescription_RuleXmlFile), new ArgumentDescriptor( id: KeywordIds.AcceptLicenses, prefixes: new string[] { "/acceptLicenses" }, required: false, allowMultiple: false, description: CmdLineResources.ArgDescription_AcceptLicenses, isVerb: true), new ArgumentDescriptor( @@ -113,6 +116,8 @@ public ProcessedArgs Process(string[] commandLineArgs) parsedOk &= TryParseSqaleFile(arguments, out string sqaleFilePath); + parsedOk &= TryParseRuleFile(arguments, out string ruleFilePath); + bool acceptLicense = GetLicenseAcceptance(arguments); bool recurseDependencies = GetRecursion(arguments); @@ -124,6 +129,7 @@ public ProcessedArgs Process(string[] commandLineArgs) analyzerRef.Version, SupportedLanguages.CSharp, /* TODO: support multiple languages */ sqaleFilePath, + ruleFilePath, acceptLicense, recurseDependencies, System.IO.Directory.GetCurrentDirectory()); @@ -182,24 +188,50 @@ private NuGetReference TryParseNuGetReference(string argumentValue) private bool TryParseSqaleFile(IEnumerable arguments, out string sqaleFilePath) { - bool sucess = true; sqaleFilePath = null; ArgumentInstance arg = arguments.SingleOrDefault(a => ArgumentDescriptor.IdComparer.Equals(KeywordIds.SqaleXmlFile, a.Descriptor.Id)); - if (arg != null) + if (arg == null) { - if (File.Exists(arg.Value)) - { - sqaleFilePath = arg.Value; - logger.LogDebug(CmdLineResources.DEBUG_UsingSqaleFile, sqaleFilePath); - } - else - { - sucess = false; - logger.LogError(CmdLineResources.ERROR_SqaleFileNotFound, arg.Value); - } + return true; + } + + bool success = true; + if (File.Exists(arg.Value)) + { + sqaleFilePath = arg.Value; + logger.LogDebug(CmdLineResources.DEBUG_UsingSqaleFile, sqaleFilePath); + } + else + { + success = false; + logger.LogError(CmdLineResources.ERROR_SqaleFileNotFound, arg.Value); + } + return success; + } + + private bool TryParseRuleFile(IEnumerable arguments, out string ruleFilePath) + { + ruleFilePath = null; + ArgumentInstance arg = arguments.SingleOrDefault(a => ArgumentDescriptor.IdComparer.Equals(KeywordIds.RuleXmlFile, a.Descriptor.Id)); + + if (arg == null) + { + return true; + } + + bool success = true; + if (File.Exists(arg.Value)) + { + ruleFilePath = arg.Value; + this.logger.LogDebug(CmdLineResources.DEBUG_UsingRuleFile, ruleFilePath); + } + else + { + success = false; + this.logger.LogError(CmdLineResources.ERROR_RuleFileNotFound, arg.Value); } - return sucess; + return success; } private static bool GetLicenseAcceptance(IEnumerable arguments) diff --git a/RoslynPluginGenerator/CommandLine/CmdLineResources.Designer.cs b/RoslynPluginGenerator/CommandLine/CmdLineResources.Designer.cs index b1dbd27..84dc204 100644 --- a/RoslynPluginGenerator/CommandLine/CmdLineResources.Designer.cs +++ b/RoslynPluginGenerator/CommandLine/CmdLineResources.Designer.cs @@ -19,7 +19,7 @@ namespace SonarQube.Plugins.Roslyn.CommandLine { // 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", "4.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class CmdLineResources { @@ -88,7 +88,16 @@ internal static string ArgDescription_RecurseDependencies { } /// - /// Looks up a localized string similar to /s:[path to sqale xml file]. + /// Looks up a localized string similar to /rules:[path to rules xml file]. + /// + internal static string ArgDescription_RuleXmlFile { + get { + return ResourceManager.GetString("ArgDescription_RuleXmlFile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to /sqale:[path to sqale xml file]. /// internal static string ArgDescription_SqaleXmlFile { get { @@ -105,6 +114,15 @@ internal static string DEBUG_ParsedReference { } } + /// + /// Looks up a localized string similar to Using rules xml file '{0}'. + /// + internal static string DEBUG_UsingRuleFile { + get { + return ResourceManager.GetString("DEBUG_UsingRuleFile", resourceCulture); + } + } + /// /// Looks up a localized string similar to Using SQALE file '{0}'. /// @@ -132,6 +150,15 @@ internal static string ERROR_MissingPackageId { } } + /// + /// Looks up a localized string similar to The specified rules xml file could not found: {0}. + /// + internal static string ERROR_RuleFileNotFound { + get { + return ResourceManager.GetString("ERROR_RuleFileNotFound", resourceCulture); + } + } + /// /// Looks up a localized string similar to The specified SQALE file could not found: {0}. /// diff --git a/RoslynPluginGenerator/CommandLine/CmdLineResources.resx b/RoslynPluginGenerator/CommandLine/CmdLineResources.resx index 608611e..2e6d2ae 100644 --- a/RoslynPluginGenerator/CommandLine/CmdLineResources.resx +++ b/RoslynPluginGenerator/CommandLine/CmdLineResources.resx @@ -126,12 +126,18 @@ /recurse - search for analyzers in target package and any dependencies + + /rules:[path to rules xml file] + - /s:[path to sqale xml file] + /sqale:[path to sqale xml file] Parsed NuGet reference. Id: {0}, version: {1} + + Using rules xml file '{0}' + Using SQALE file '{0}' @@ -141,6 +147,9 @@ NuGet package id must be specified + + The specified rules xml file could not found: {0} + The specified SQALE file could not found: {0} diff --git a/RoslynPluginGenerator/CommandLine/ProcessedArgs.cs b/RoslynPluginGenerator/CommandLine/ProcessedArgs.cs index 84db0a6..65011c8 100644 --- a/RoslynPluginGenerator/CommandLine/ProcessedArgs.cs +++ b/RoslynPluginGenerator/CommandLine/ProcessedArgs.cs @@ -25,7 +25,8 @@ namespace SonarQube.Plugins.Roslyn.CommandLine { public class ProcessedArgs { - public ProcessedArgs(string packageId, SemanticVersion packageVersion, string language, string sqaleFilePath, bool acceptLicenses, bool recurseDependencies, string outputDirectory) + public ProcessedArgs(string packageId, SemanticVersion packageVersion, string language, string sqaleFilePath, string ruleFilePath, + bool acceptLicenses, bool recurseDependencies, string outputDirectory) { if (string.IsNullOrWhiteSpace(packageId)) { @@ -41,6 +42,7 @@ public ProcessedArgs(string packageId, SemanticVersion packageVersion, string la PackageId = packageId; PackageVersion = packageVersion; SqaleFilePath = sqaleFilePath; // can be null + RuleFilePath = ruleFilePath; Language = language; AcceptLicenses = acceptLicenses; RecurseDependencies = recurseDependencies; @@ -53,6 +55,8 @@ public ProcessedArgs(string packageId, SemanticVersion packageVersion, string la public string SqaleFilePath { get; } + public string RuleFilePath { get; } + public string Language { get; } public bool AcceptLicenses { get; } diff --git a/RoslynPluginGenerator/UIResources.Designer.cs b/RoslynPluginGenerator/UIResources.Designer.cs index 6f2ffb0..7f48976 100644 --- a/RoslynPluginGenerator/UIResources.Designer.cs +++ b/RoslynPluginGenerator/UIResources.Designer.cs @@ -105,6 +105,15 @@ public static string APG_GeneratingRules { } } + /// + /// Looks up a localized string similar to The specified rules xml file is invalid: {0}. + /// + public static string APG_InvalidRulesFile { + get { + return ResourceManager.GetString("APG_InvalidRulesFile", resourceCulture); + } + } + /// /// Looks up a localized string similar to The specified SQALE file is invalid: {0}. /// @@ -196,11 +205,11 @@ public static string APG_PluginGenerated { } /// - /// Looks up a localized string similar to SQALE information cannot currently be embedded into plugins generated from package dependencies.. + /// Looks up a localized string similar to SQALE and customised rule xml information cannot currently be embedded into plugins generated from package dependencies.. /// - public static string APG_RecurseEnabled_SQALENotEnabled { + public static string APG_RecurseEnabled_SQALEandRuleCustomisationNotEnabled { get { - return ResourceManager.GetString("APG_RecurseEnabled_SQALENotEnabled", resourceCulture); + return ResourceManager.GetString("APG_RecurseEnabled_SQALEandRuleCustomisationNotEnabled", resourceCulture); } } @@ -222,6 +231,20 @@ public static string APG_SqaleGeneratedToFile { } } + /// + /// Looks up a localized string similar to + ///Rules definitions: a template rules xml file for the analyzer was saved to {0}. To customise the rules definitions for the analyzer: + /// * rename the file + /// * edit the rules definitions in the file + /// * re-run this generator specifying the rules xml file to use with the /rules:[filename] argument. + ///. + /// + public static string APG_TemplateRuleFileGenerated { + get { + return ResourceManager.GetString("APG_TemplateRuleFileGenerated", resourceCulture); + } + } + /// /// Looks up a localized string similar to ///SQALE: an empty SQALE file for the analyzer was saved to {0}. To provide SQALE remediation information for the analyzer: @@ -245,6 +268,24 @@ public static string APG_UnsupportedLanguage { } } + /// + /// Looks up a localized string similar to Using the supplied rules xml file: {0}. + /// + public static string APG_UsingSuppliedRulesFile { + get { + return ResourceManager.GetString("APG_UsingSuppliedRulesFile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Using the SQALE file: {0}. + /// + public static string APG_UsingSuppliedSqaleFile { + get { + return ResourceManager.GetString("APG_UsingSuppliedSqaleFile", resourceCulture); + } + } + /// /// Looks up a localized string similar to Roslyn Analyzer Plugin Generator for SonarQube. /// diff --git a/RoslynPluginGenerator/UIResources.resx b/RoslynPluginGenerator/UIResources.resx index 02ba1c5..678ad7a 100644 --- a/RoslynPluginGenerator/UIResources.resx +++ b/RoslynPluginGenerator/UIResources.resx @@ -132,6 +132,9 @@ Generating rules... + + The specified rules xml file is invalid: {0} + The specified SQALE file is invalid: {0} @@ -162,8 +165,8 @@ Plugin generated: {0} - - SQALE information cannot currently be embedded into plugins generated from package dependencies. + + SQALE and customised rule xml information cannot currently be embedded into plugins generated from package dependencies. {0} rules generated to {1} @@ -171,6 +174,14 @@ SQALE generated to file {0} + + +Rules definitions: a template rules xml file for the analyzer was saved to {0}. To customise the rules definitions for the analyzer: + * rename the file + * edit the rules definitions in the file + * re-run this generator specifying the rules xml file to use with the /rules:[filename] argument. + + SQALE: an empty SQALE file for the analyzer was saved to {0}. To provide SQALE remediation information for the analyzer: @@ -182,6 +193,12 @@ SQALE: an empty SQALE file for the analyzer was saved to {0}. To provide SQALE r The language '{0}' is not supported. Valid options are 'cs' or 'vb'. + + Using the supplied rules xml file: {0} + + + Using the SQALE file: {0} + Roslyn Analyzer Plugin Generator for SonarQube diff --git a/Tests/CommonTests/SerializationTests.cs b/Tests/CommonTests/SerializationTests.cs index acb8a83..67915c7 100644 --- a/Tests/CommonTests/SerializationTests.cs +++ b/Tests/CommonTests/SerializationTests.cs @@ -44,7 +44,10 @@ public void SerializeRules() Severity = "CRITICAL", Cardinality = "SINGLE", Status = "READY", - Tags = new[] { "t1", "t2" } + Type = IssueType.CODE_SMELL, + Tags = new[] { "t1", "t2" }, + DebtRemediationFunction = DebtRemediationFunctionType.CONSTANT_ISSUE, + DebtRemediationFunctionOffset = "15min" }, new Rule() @@ -55,6 +58,7 @@ public void SerializeRules() Description = @"

An Html Description", Severity = "MAJOR", Cardinality = "SINGLE", + Type = IssueType.BUG, Status = "READY", } }; @@ -78,8 +82,11 @@ public void SerializeRules() CRITICAL SINGLE READY + CODE_SMELL t1 t2 + CONSTANT_ISSUE + 15min key2 @@ -89,6 +96,7 @@ public void SerializeRules() MAJOR SINGLE READY + BUG "; diff --git a/Tests/IntegrationTests/Roslyn/RoslynGenTests.cs b/Tests/IntegrationTests/Roslyn/RoslynGenTests.cs index f8474a9..1c0760c 100644 --- a/Tests/IntegrationTests/Roslyn/RoslynGenTests.cs +++ b/Tests/IntegrationTests/Roslyn/RoslynGenTests.cs @@ -58,7 +58,7 @@ public void RoslynPlugin_GenerateForValidAnalyzer_Succeeds() // Act NuGetPackageHandler nuGetHandler = new NuGetPackageHandler(fakeRemotePkgMgr.LocalRepository, localPackageDestination, logger); AnalyzerPluginGenerator apg = new AnalyzerPluginGenerator(nuGetHandler, logger); - ProcessedArgs args = new ProcessedArgs(packageId, new SemanticVersion("1.0.2"), "cs", null, false, false, outputDir); + ProcessedArgs args = new ProcessedArgs(packageId, new SemanticVersion("1.0.2"), "cs", null, null, false, false, outputDir); bool result = apg.Generate(args); // Assert @@ -92,7 +92,7 @@ public void RoslynPlugin_GenerateForDependencyAnalyzers_Succeeds() // Act NuGetPackageHandler nuGetHandler = new NuGetPackageHandler(fakeRemotePkgMgr.LocalRepository, localPackageDestination, logger); AnalyzerPluginGenerator apg = new AnalyzerPluginGenerator(nuGetHandler, logger); - ProcessedArgs args = new ProcessedArgs(targetPkg.Id, targetPkg.Version, "cs", null, false, + ProcessedArgs args = new ProcessedArgs(targetPkg.Id, targetPkg.Version, "cs", null, null, false, true /* generate plugins for dependencies with analyzers*/, outputDir); bool result = apg.Generate(args); @@ -127,7 +127,7 @@ public void RoslynPlugin_GenerateForMultiLevelAnalyzers_Succeeds() // Act NuGetPackageHandler nuGetHandler = new NuGetPackageHandler(fakeRemotePkgMgr.LocalRepository, localPackageDestination, logger); AnalyzerPluginGenerator apg = new AnalyzerPluginGenerator(nuGetHandler, logger); - ProcessedArgs args = new ProcessedArgs(targetPkg.Id, targetPkg.Version, "cs", null, false, + ProcessedArgs args = new ProcessedArgs(targetPkg.Id, targetPkg.Version, "cs", null, null, false, true /* generate plugins for dependencies with analyzers*/, outputDir); bool result = apg.Generate(args); diff --git a/Tests/RoslynPluginGeneratorTests/AnalyzerPluginGeneratorTests.cs b/Tests/RoslynPluginGeneratorTests/AnalyzerPluginGeneratorTests.cs index d230ad6..05dc418 100644 --- a/Tests/RoslynPluginGeneratorTests/AnalyzerPluginGeneratorTests.cs +++ b/Tests/RoslynPluginGeneratorTests/AnalyzerPluginGeneratorTests.cs @@ -18,14 +18,14 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System; -using System.IO; using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; using NuGet; using SonarLint.XmlDescriptor; using SonarQube.Plugins.Roslyn.CommandLine; using SonarQube.Plugins.Test.Common; +using System; +using System.IO; using static SonarQube.Plugins.Roslyn.RoslynPluginGeneratorTests.RemoteRepoBuilder; namespace SonarQube.Plugins.Roslyn.RoslynPluginGeneratorTests @@ -421,7 +421,7 @@ public void Generate_SqaleFileNotSpecified_TemplateFileCreated() result = apg.Generate(args); result.Should().BeTrue("Expecting generation to have succeeded"); - logger.AssertSingleWarningExists(UIResources.APG_RecurseEnabled_SQALENotEnabled); + logger.AssertSingleWarningExists(UIResources.APG_RecurseEnabled_SQALEandRuleCustomisationNotEnabled); AssertSqaleFileExistsForPackage(logger, outputDir, parent); AssertSqaleFileExistsForPackage(logger, outputDir, child1); AssertSqaleFileExistsForPackage(logger, outputDir, child2); @@ -483,6 +483,108 @@ public void Generate_InvalidSqaleFileSpecified_GeneratorError() logger.AssertSingleErrorExists("invalidSqale.xml"); // expecting an error containing the invalid sqale file name } + [TestMethod] + public void Generate_RulesFileNotSpecified_TemplateFileCreated() + { + // Arrange + string outputDir = TestUtils.CreateTestDirectory(this.TestContext, ".out"); + + var logger = new TestLogger(); + var remoteRepoBuilder = new RemoteRepoBuilder(this.TestContext); + var child1 = CreatePackageWithAnalyzer(remoteRepoBuilder, "child1.requiredAccept.id", "2.1", License.NotRequired); + var child2 = CreatePackageWithAnalyzer(remoteRepoBuilder, "child2.id", "2.2", License.NotRequired); + var parent = CreatePackageWithAnalyzer(remoteRepoBuilder, "parent.id", "1.0", License.NotRequired, child1, child2); + + var nuGetHandler = new NuGetPackageHandler(remoteRepoBuilder.FakeRemoteRepo, GetLocalNuGetDownloadDir(), logger); + + var testSubject = new AnalyzerPluginGenerator(nuGetHandler, logger); + + // 1. Generate a plugin for the target package only. Expecting a plugin and a template rule file. + var args = new ProcessedArgsBuilder("parent.id", outputDir) + .SetLanguage("cs") + .SetPackageVersion("1.0") + .SetRecurseDependencies(true) + .Build(); + bool result = testSubject.Generate(args); + result.Should().BeTrue(); + + AssertRuleTemplateFileExistsForPackage(logger, outputDir, parent); + + // 2. Generate a plugin for target package and all dependencies. Expecting three plugins and associated rule files. + logger.Reset(); + args = CreateArgs("parent.id", "1.0", "cs", null, false, true /* /recurse = true */, outputDir); + result = testSubject.Generate(args); + result.Should().BeTrue(); + + logger.AssertSingleWarningExists(UIResources.APG_RecurseEnabled_SQALEandRuleCustomisationNotEnabled); + AssertRuleTemplateFileExistsForPackage(logger, outputDir, parent); + AssertRuleTemplateFileExistsForPackage(logger, outputDir, child1); + AssertRuleTemplateFileExistsForPackage(logger, outputDir, child2); + } + + [TestMethod] + public void Generate_ValidRuleFileSpecified_TemplateFileNotCreated() + { + // Arrange + var outputDir = TestUtils.CreateTestDirectory(this.TestContext, ".out"); + + var remoteRepoBuilder = new RemoteRepoBuilder(this.TestContext); + var apg = CreateTestSubjectWithFakeRemoteRepo(remoteRepoBuilder); + + CreatePackageInFakeRemoteRepo(remoteRepoBuilder, "dummy.id", "1.1"); + + // Create a dummy rule file + var dummyRuleFilePath = Path.Combine(outputDir, "inputRule.xml"); + Serializer.SaveModel(new Rules(), dummyRuleFilePath); + + var args = new ProcessedArgsBuilder("dummy.id", outputDir) + .SetLanguage("cs") + .SetPackageVersion("1.1") + .SetRuleFilePath(dummyRuleFilePath) + .Build(); + + // Act + bool result = apg.Generate(args); + + // Assert + result.Should().BeTrue(); + AssertRuleTemplateDoesNotExist(outputDir); + } + + [TestMethod] + public void Generate_InvalidRuleFileSpecified_GeneratorError() + { + // Arrange + var outputDir = TestUtils.CreateTestDirectory(this.TestContext, ".out"); + + var logger = new TestLogger(); + + var remoteRepoBuilder = new RemoteRepoBuilder(this.TestContext); + CreatePackageInFakeRemoteRepo(remoteRepoBuilder, "dummy.id", "1.1"); + + var nuGetHandler = new NuGetPackageHandler(remoteRepoBuilder.FakeRemoteRepo, GetLocalNuGetDownloadDir(), logger); + + // Create an invalid rule file + var dummyRuleFilePath = Path.Combine(outputDir, "invalidRule.xml"); + File.WriteAllText(dummyRuleFilePath, "not valid xml"); + + AnalyzerPluginGenerator apg = new AnalyzerPluginGenerator(nuGetHandler, logger); + + var args = new ProcessedArgsBuilder("dummy.id", outputDir) + .SetLanguage("cs") + .SetPackageVersion("1.1") + .SetRuleFilePath(dummyRuleFilePath) + .Build(); + + // Act + bool result = apg.Generate(args); + + // Assert + result.Should().BeFalse(); + AssertRuleTemplateDoesNotExist(outputDir); + logger.AssertSingleErrorExists("invalidRule.xml"); // expecting an error containing the invalid rule file name + } + [TestMethod] public void CreatePluginManifest_AllProperties() { @@ -623,6 +725,7 @@ private static ProcessedArgs CreateArgs(string packageId, string packageVersion, new SemanticVersion(packageVersion), language, sqaleFilePath, + null /* rule xml path */, acceptLicenses, recurseDependencies, outputDirectory); @@ -684,7 +787,27 @@ private void AssertSqaleFileExistsForPackage(TestLogger logger, string outputDir private static void AssertSqaleTemplateDoesNotExist(string outputDir) { string[] matches = Directory.GetFiles(outputDir, "*sqale*template*", SearchOption.AllDirectories); - matches.Length.Should().Be(0, "Not expecting any squale template files to exist"); + matches.Length.Should().Be(0, "Not expecting any sqale template files to exist"); + } + + private static string GetExpectedRuleTemplateFilePath(string outputDir, IPackage package) + { + return Path.Combine(outputDir, String.Format("{0}.{1}.rules.template.xml", package.Id, package.Version.ToString())); + } + + private void AssertRuleTemplateFileExistsForPackage(TestLogger logger, string outputDir, IPackage package) + { + string expectedFilePath = GetExpectedRuleTemplateFilePath(outputDir, package); + + File.Exists(expectedFilePath).Should().BeTrue(); + this.TestContext.AddResultFile(expectedFilePath); + logger.AssertSingleInfoMessageExists(expectedFilePath); // should be a message about the generated file + } + + private static void AssertRuleTemplateDoesNotExist(string outputDir) + { + string[] matches = Directory.GetFiles(outputDir, "*rules*template*", SearchOption.AllDirectories); + matches.Length.Should().Be(0, "Not expecting any rules template files to exist"); } private static void AssertJarsGenerated(string rootDir, int expectedCount) diff --git a/Tests/RoslynPluginGeneratorTests/ArchiveUpdaterTests.cs b/Tests/RoslynPluginGeneratorTests/ArchiveUpdaterTests.cs index ebff4c5..9623f46 100644 --- a/Tests/RoslynPluginGeneratorTests/ArchiveUpdaterTests.cs +++ b/Tests/RoslynPluginGeneratorTests/ArchiveUpdaterTests.cs @@ -18,11 +18,11 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.IO; -using System.IO.Compression; using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; using SonarQube.Plugins.Test.Common; +using System.IO; +using System.IO.Compression; namespace SonarQube.Plugins.Roslyn.RoslynPluginGeneratorTests { @@ -97,7 +97,7 @@ public void ExistingEntryUpdated_And_NewEntryAdded() AddEntry(archive, "sub/changed", "original data in file that is going to be changed to something shorted"); } - Assert.IsTrue(File.Exists(originalZipFile), "Test setup error: original zip file not created"); + File.Exists(originalZipFile).Should().BeTrue("Test setup error: original zip file not created"); string changedFile = TestUtils.CreateTextFile("changed.txt", rootTestDir, "new data in changed file"); string newFile = TestUtils.CreateTextFile("newfile.txt", rootTestDir, "new file"); diff --git a/Tests/RoslynPluginGeneratorTests/ArgumentProcessorTests.cs b/Tests/RoslynPluginGeneratorTests/ArgumentProcessorTests.cs index 45602f5..add52f9 100644 --- a/Tests/RoslynPluginGeneratorTests/ArgumentProcessorTests.cs +++ b/Tests/RoslynPluginGeneratorTests/ArgumentProcessorTests.cs @@ -18,11 +18,11 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.IO; using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; using SonarQube.Plugins.Roslyn.CommandLine; using SonarQube.Plugins.Test.Common; +using System.IO; namespace SonarQube.Plugins.Roslyn.PluginGeneratorTests { @@ -148,6 +148,41 @@ public void ArgProc_SqaleFile() AssertArgumentsProcessed(actualArgs, logger, "valid", "1.0", filePath, false); } + [TestMethod] + public void ArgProc_RuleFile() + { + // 0. Setup + TestLogger logger; + string[] rawArgs; + ProcessedArgs actualArgs; + + // 1. No rule file value -> valid + logger = new TestLogger(); + rawArgs = new string[] { "/a:validId" }; + actualArgs = ArgumentProcessor.TryProcessArguments(rawArgs, logger); + + actualArgs.RuleFilePath.Should().BeNull(); + + // 2. Missing rule file + logger = new TestLogger(); + rawArgs = new string[] { "/rules:missingFile.txt", "/a:validId" }; + actualArgs = ArgumentProcessor.TryProcessArguments(rawArgs, logger); + + AssertArgumentsNotProcessed(actualArgs, logger); + logger.AssertSingleErrorExists("missingFile.txt"); // should be an error containing the missing file name + + // 3. Existing rule file + string testDir = TestUtils.CreateTestDirectory(this.TestContext); + string filePath = TestUtils.CreateTextFile("valid.rules.txt", testDir, "rule file contents"); + + logger = new TestLogger(); + rawArgs = new string[] { $"/rules:{filePath}", "/a:valid:1.0" }; + actualArgs = ArgumentProcessor.TryProcessArguments(rawArgs, logger); + + actualArgs.Should().NotBeNull(); + actualArgs.RuleFilePath.Should().Be(filePath); + } + [TestMethod] public void ArgProc_AcceptLicenses_Valid() { diff --git a/Tests/RoslynPluginGeneratorTests/ProcessedArgsBuilder.cs b/Tests/RoslynPluginGeneratorTests/ProcessedArgsBuilder.cs new file mode 100644 index 0000000..9c39633 --- /dev/null +++ b/Tests/RoslynPluginGeneratorTests/ProcessedArgsBuilder.cs @@ -0,0 +1,93 @@ +/* + * SonarQube Roslyn SDK + * Copyright (C) 2015-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using NuGet; +using SonarQube.Plugins.Roslyn.CommandLine; + +namespace SonarQube.Plugins.Roslyn.RoslynPluginGeneratorTests +{ + public class ProcessedArgsBuilder + { + private readonly string packageId; + private string packageVersion; + private string language; + private string sqaleFilePath; + private string ruleFilePath; + private bool acceptLicenses; + private bool recurseDependencies; + private readonly string outputDirectory; + + public ProcessedArgsBuilder(string packageId, string outputDir) + { + this.packageId = packageId; + this.outputDirectory = outputDir; + } + + public ProcessedArgsBuilder SetPackageVersion(string packageVersion) + { + this.packageVersion = packageVersion; + return this; + } + + public ProcessedArgsBuilder SetLanguage(string language) + { + this.language = language; + return this; + } + + public ProcessedArgsBuilder SetSqaleFilePath(string filePath) + { + this.sqaleFilePath = filePath; + return this; + } + + public ProcessedArgsBuilder SetRuleFilePath(string filePath) + { + this.ruleFilePath = filePath; + return this; + } + + public ProcessedArgsBuilder SetAcceptLicenses(bool acceptLicenses) + { + this.acceptLicenses = acceptLicenses; + return this; + } + + public ProcessedArgsBuilder SetRecurseDependencies(bool recurseDependencies) + { + this.recurseDependencies = recurseDependencies; + return this; + } + + public ProcessedArgs Build() + { + ProcessedArgs args = new ProcessedArgs( + packageId, + new SemanticVersion(packageVersion), + language, + sqaleFilePath, + ruleFilePath, + acceptLicenses, + recurseDependencies, + outputDirectory); + return args; + } + } +} diff --git a/Tests/RoslynPluginGeneratorTests/SonarQube.Plugins.Roslyn.RoslynPluginGeneratorTests.csproj b/Tests/RoslynPluginGeneratorTests/SonarQube.Plugins.Roslyn.RoslynPluginGeneratorTests.csproj index 22052fe..5989929 100644 --- a/Tests/RoslynPluginGeneratorTests/SonarQube.Plugins.Roslyn.RoslynPluginGeneratorTests.csproj +++ b/Tests/RoslynPluginGeneratorTests/SonarQube.Plugins.Roslyn.RoslynPluginGeneratorTests.csproj @@ -38,6 +38,9 @@ ..\..\packages\FluentAssertions.5.4.1\lib\net45\FluentAssertions.dll + + ..\..\packages\Castle.Core.4.2.1\lib\net45\Castle.Core.dll + ..\..\packages\Microsoft.CodeAnalysis.Common.1.1.0\lib\net45\Microsoft.CodeAnalysis.dll True @@ -94,6 +97,7 @@ ..\..\packages\Microsoft.Composition.1.0.27\lib\portable-net45+win8+wp8+wpa81\System.Composition.TypedParts.dll True + @@ -103,6 +107,9 @@ ..\..\packages\System.ValueTuple.4.4.0\lib\netstandard1.0\System.ValueTuple.dll + + ..\..\packages\System.Threading.Tasks.Extensions.4.3.0\lib\portable-net45+win8+wp8+wpa81\System.Threading.Tasks.Extensions.dll + @@ -126,6 +133,7 @@ + Properties\AssemblyInfo.Shared.cs