From f6a88c280e6cf4fa4a4059004dad319eaed09229 Mon Sep 17 00:00:00 2001 From: Tamas Balog Date: Tue, 27 Dec 2022 11:47:15 +0100 Subject: [PATCH] Add default rulesets config for JS and TS files --- src/Extension/Rosie/CodigaConfigFileUtil.cs | 15 +- .../CodigaDefaultRulesetsInfoBarHelper.cs | 178 +++++++++++------- src/Extension/Rosie/CodigaRulesetConfigs.cs | 6 + src/Tests/Rosie/CodigaConfigFileUtilTest.cs | 52 ++++- .../CodigaDefaultRulesetInfoBarHelperTest.cs | 144 ++++++++++++++ src/Tests/Rosie/RosieLanguageSupportTest.cs | 24 +++ 6 files changed, 346 insertions(+), 73 deletions(-) create mode 100644 src/Tests/Rosie/CodigaDefaultRulesetInfoBarHelperTest.cs diff --git a/src/Extension/Rosie/CodigaConfigFileUtil.cs b/src/Extension/Rosie/CodigaConfigFileUtil.cs index 7e0cecd..730e5aa 100644 --- a/src/Extension/Rosie/CodigaConfigFileUtil.cs +++ b/src/Extension/Rosie/CodigaConfigFileUtil.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text.RegularExpressions; using Extension.Helpers; +using Extension.SnippetFormats; using Microsoft.VisualStudio.Shell; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; @@ -59,11 +60,19 @@ public static class CodigaConfigFileUtil /// Creates the Codiga config file in the solution's root directory with default Python rulesets. /// /// The service provider to retrieve information about the solution from. - public static void CreateCodigaConfigFile(SVsServiceProvider serviceProvider) + public static void CreateCodigaConfigFile(LanguageUtils.LanguageEnumeration language, SVsServiceProvider serviceProvider) { - var solutionRoot = SolutionHelper.GetSolutionDir(serviceProvider);; + var solutionRoot = SolutionHelper.GetSolutionDir(serviceProvider); if (solutionRoot != null) - File.WriteAllText($"{solutionRoot}\\codiga.yml", CodigaRulesetConfigs.DefaultPythonRulesetConfig); + { + var rulesetConfig = language switch + { + LanguageUtils.LanguageEnumeration.Python => CodigaRulesetConfigs.DefaultPythonRulesetConfig, + LanguageUtils.LanguageEnumeration.Javascript => CodigaRulesetConfigs.DefaultJavascriptRulesetConfig, + LanguageUtils.LanguageEnumeration.Typescript => CodigaRulesetConfigs.DefaultJavascriptRulesetConfig, + }; + File.WriteAllText($"{solutionRoot}\\codiga.yml", rulesetConfig); + } } /// diff --git a/src/Extension/Rosie/CodigaDefaultRulesetsInfoBarHelper.cs b/src/Extension/Rosie/CodigaDefaultRulesetsInfoBarHelper.cs index 74fe702..485f2ac 100644 --- a/src/Extension/Rosie/CodigaDefaultRulesetsInfoBarHelper.cs +++ b/src/Extension/Rosie/CodigaDefaultRulesetsInfoBarHelper.cs @@ -5,10 +5,10 @@ using Extension.Caching; using Extension.Helpers; using Extension.Settings; -using Extension.SnippetFormats; using Microsoft.VisualStudio.Imaging; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; +using LanguageEnumeration = Extension.SnippetFormats.LanguageUtils.LanguageEnumeration; namespace Extension.Rosie { @@ -18,13 +18,16 @@ namespace Extension.Rosie /// /// /// - internal static class CodigaDefaultRulesetsInfoBarHelper + public static class CodigaDefaultRulesetsInfoBarHelper { //\n characters are for better formatting of the entire content of the info bar private const string InfoBarText = "Check for security, code style in your Python code with Codiga.\n"; private const string CreateCodigaYmlActionText = "Create codiga.yml\n"; private const string NeverForThisSolutionActionText = "Never for this solution"; + private static readonly string[] FileSearchPatterns = { "*.py", "*.js", "*.jsx", "*.ts", "*.tsx" }; + private static readonly string[] FolderNamesToExclude = { ".vs", ".vscode", ".idea", ".git", "node_modules" }; + /// /// Holds and instance of that is saved in . ///
@@ -56,7 +59,8 @@ internal class InfoBarHolder ///
    ///
  1. The user hasn't clicked the Never for this solution option.
  2. ///
  3. There is no Codiga config file in the solution root/open folder.
  4. - ///
  5. At least one of the projects in the solution is a Python project.
  6. + ///
  7. At least one of the projects in the solution is a Python project, or the folder contains at least + /// one file whose language is supported by Rosie.
  8. ///
///
internal static async void ShowDefaultRulesetCreationInfoBarAsync(InfoBarHolder infoBarHolder) @@ -68,9 +72,12 @@ internal static async void ShowDefaultRulesetCreationInfoBarAsync(InfoBarHolder }); if (SolutionSettings.IsShouldNotifyUserToCreateCodigaConfig(serviceProvider) - && CodigaConfigFileUtil.FindCodigaConfigFile(serviceProvider) == null - && await IsSolutionContainPythonAsync(serviceProvider)) + && CodigaConfigFileUtil.FindCodigaConfigFile(serviceProvider) == null) { + var supportedLanguage = await GetLanguageOfSupportedProjectOrFileAsync(serviceProvider); + if (supportedLanguage == LanguageEnumeration.Unknown) + return; + var model = new InfoBarModel(new[] { new InfoBarTextSpan(InfoBarText), @@ -79,59 +86,59 @@ internal static async void ShowDefaultRulesetCreationInfoBarAsync(InfoBarHolder }, KnownMonikers.PlayStepGroup); var infoBar = await VS.InfoBar.CreateAsync(ToolWindowGuids80.SolutionExplorer, model); - if (infoBar != null) + if (infoBar == null) + return; + + //Handles when the user clicks one of the items in the info bar. + // Implemented as an inline event handler, so that the supported language can be passed to determine + // the default ruleset config. + infoBar.ActionItemClicked += (_, args) => { - infoBar.ActionItemClicked += InfoBar_ActionItemClicked; - await infoBar.TryShowInfoBarUIAsync(); - } + ThreadHelper.ThrowIfNotOnUIThread(); - infoBarHolder.InfoBar = infoBar; - return; - } + switch (args.ActionItem.Text) + { + case CreateCodigaYmlActionText: + RecordCreateCodigaYaml(); + CodigaConfigFileUtil.CreateCodigaConfigFile( + supportedLanguage, + VS.GetMefService()); + break; + case NeverForThisSolutionActionText: + SolutionSettings.SaveNeverNotifyUserToCreateCodigaConfigFile( + VS.GetMefService()); + break; + } - infoBarHolder.InfoBar = null; - } + //Whichever action is clicked, close the info bar + args.InfoBarUIElement.Close(); + }; - /// - /// Handles when the user clicks one of the items in the info bar. - /// - private static void InfoBar_ActionItemClicked(object sender, InfoBarActionItemEventArgs e) - { - ThreadHelper.ThrowIfNotOnUIThread(); + await infoBar.TryShowInfoBarUIAsync(); - switch (e.ActionItem.Text) - { - case CreateCodigaYmlActionText: - RecordCreateCodigaYaml(); - CodigaConfigFileUtil.CreateCodigaConfigFile(VS.GetMefService()); - break; - case NeverForThisSolutionActionText: - SolutionSettings.SaveNeverNotifyUserToCreateCodigaConfigFile( - VS.GetMefService()); - break; + infoBarHolder.InfoBar = infoBar; + return; } - //Whichever action is clicked, close the info bar - e.InfoBarUIElement.Close(); + infoBarHolder.InfoBar = null; } private static void RecordCreateCodigaYaml() { - var clientProvider = new DefaultCodigaClientProvider(); - if (!clientProvider.TryGetClient(out var client)) - return; - - ThreadHelper.JoinableTaskFactory.RunAsync(async () => + if (new DefaultCodigaClientProvider().TryGetClient(out var client)) { - try - { - await client.RecordCreateCodigaYaml(); - } - catch + ThreadHelper.JoinableTaskFactory.RunAsync(async () => { - //Even if recording this metric fails, the Codiga config file must be created - } - }); + try + { + await client.RecordCreateCodigaYaml(); + } + catch + { + //Even if recording this metric fails, the Codiga config file must be created + } + }); + } } /// @@ -141,7 +148,8 @@ private static void RecordCreateCodigaYaml() /// Python project guid comes from https://github.com/microsoft/PTVS/blob/main/Python/Product/VSCommon/CommonGuidList.cs /// /// The service provider to retrieve information about the solution from. - private static async Task IsSolutionContainPythonAsync(SVsServiceProvider serviceProvider) + private static async Task GetLanguageOfSupportedProjectOrFileAsync( + SVsServiceProvider serviceProvider) { var solution = await ThreadHelper.JoinableTaskFactory.RunAsync(async () => { @@ -150,46 +158,78 @@ private static async Task IsSolutionContainPythonAsync(SVsServiceProvider }); //If we have a proper VS solution open, check if at least one of the projects in it is a Python project - if (!SolutionHelper.IsInOpenFolderMode(solution)) - { - var projectsInSolution = - ThreadHelper.JoinableTaskFactory.Run(async () => await VS.Solutions.GetAllProjectsAsync()); - return projectsInSolution.Any(project => - ThreadHelper.JoinableTaskFactory.Run(async () => - await project.IsKindAsync("888888A0-9F3D-457C-B088-3A5042F75D52"))); - } + return !SolutionHelper.IsInOpenFolderMode(solution) && IsContainPythonProject() + ? LanguageEnumeration.Python + //Running the lookup in the background, so it doesn't block the UI + : await Task.Run(() => FindSupportedFile(SolutionHelper.GetSolutionDir(serviceProvider))); + } - //Running the lookup in the background, so it doesn't block the UI - return await Task.Run(() => IsContainPythonFile(SolutionHelper.GetSolutionDir(serviceProvider)) != null); + private static bool IsContainPythonProject() + { + var projectsInSolution = + ThreadHelper.JoinableTaskFactory.Run(async () => await VS.Solutions.GetAllProjectsAsync()); + return projectsInSolution.Any(project => + ThreadHelper.JoinableTaskFactory.Run(async () => + await project.IsKindAsync("888888A0-9F3D-457C-B088-3A5042F75D52"))); } /// - /// Searches the argument directory and all its sub-directories for the presence of files with .py - /// extension. + /// Searches the argument directory and all its sub-directories for the presence of files with any + /// of the supported languages. ///
- /// It currently excludes lookup in IDE solution and project specific folders, .vs and .idea. + /// It excludes lookup in some folders listed in . ///
/// The root directory to search in - /// The language of the found file if there is a file found, or null if no file was found. - private static LanguageUtils.LanguageEnumeration? IsContainPythonFile(string directory) + /// The language of the found file if there is a file found, or 'Unknown' if no file with supported language was found. + public static LanguageEnumeration FindSupportedFile(string directory) { try { - //Using a foreach instead of a call to '.Any()' because 'Any()' creates an extra enumerator each time it is called. - foreach (var _ in Directory.EnumerateFiles(directory, "*.py")) - return LanguageUtils.LanguageEnumeration.Python; + //Composes a parallel query that goes through the file extension search patterns of the currently supported languages, + //and returns the language of the file that it finds in the current directory, or 'Unknown' if there is no supported file in there. + var foundLanguage = FileSearchPatterns.AsParallel().Select(searchPattern => + { + //Using foreach instead of a call to '.Any()' because 'Any()' creates an extra enumerator each time it is called. + foreach (var _ in Directory.EnumerateFiles(directory, searchPattern, SearchOption.TopDirectoryOnly)) + { + switch (searchPattern) + { + case "*.py": + return LanguageEnumeration.Python; + case "*.js": + case "*.jsx": + return LanguageEnumeration.Javascript; + case "*.ts": + case "*.tsx": + return LanguageEnumeration.Typescript; + } + } + + return LanguageEnumeration.Unknown; + }); + + //Evaluates the previous query and if it contains any of the currently supported languages, + //this method returns that language. + using (var enumerator = foundLanguage.GetEnumerator()) + { + while (enumerator.MoveNext()) + if (LanguageEnumeration.Unknown != enumerator.Current) + return enumerator.Current; + } + //If there is no file with supported language is found in the current directory, + //start to iterate through the files in the subdirectories. foreach (var subDir in Directory.EnumerateDirectories(directory)) { //Exclude non-existent folders, and ones whose name starts with a dot if (subDir != null) { string? directoryName = Path.GetDirectoryName(subDir); - if (directoryName != ".vs" && directoryName != ".idea") + if (!FolderNamesToExclude.Contains(directoryName)) { - var isContainPythonFile = IsContainPythonFile(subDir); - if (isContainPythonFile != null) - return isContainPythonFile; + var supportedLanguage = FindSupportedFile(subDir); + if (supportedLanguage != LanguageEnumeration.Unknown) + return supportedLanguage; } } } @@ -200,7 +240,7 @@ private static async Task IsSolutionContainPythonAsync(SVsServiceProvider // with no specific file found } - return null; + return LanguageEnumeration.Unknown; } } } \ No newline at end of file diff --git a/src/Extension/Rosie/CodigaRulesetConfigs.cs b/src/Extension/Rosie/CodigaRulesetConfigs.cs index 0b92072..00c0a13 100644 --- a/src/Extension/Rosie/CodigaRulesetConfigs.cs +++ b/src/Extension/Rosie/CodigaRulesetConfigs.cs @@ -10,5 +10,11 @@ public static class CodigaRulesetConfigs " - python-security\n" + " - python-best-practices\n" + " - python-code-style"; + + public const string DefaultJavascriptRulesetConfig = + "rulesets:\n" + + " - jsx-a11y\n" + + " - jsx-react\n" + + " - react-best-practices"; } } \ No newline at end of file diff --git a/src/Tests/Rosie/CodigaConfigFileUtilTest.cs b/src/Tests/Rosie/CodigaConfigFileUtilTest.cs index 314fcf9..735350d 100644 --- a/src/Tests/Rosie/CodigaConfigFileUtilTest.cs +++ b/src/Tests/Rosie/CodigaConfigFileUtilTest.cs @@ -1,5 +1,6 @@ using System.IO; using Extension.Rosie; +using Extension.SnippetFormats; using NUnit.Framework; using static Tests.ServiceProviderMockSupport; @@ -187,7 +188,56 @@ public void FindCodigaConfigFile_should_return_path_of_codiga_config_file() Assert.That(configFile, Is.EqualTo(_codigaConfigFile)); } - + + #endregion + + #region CreateCodigaConfigFile + + [Test] + public void CreateCodigaConfigFile_should_create_codiga_yml_with_python_rulesets() + { + var serviceProvider = MockServiceProvider(Path.GetTempPath()); + _codigaConfigFile = $"{Path.GetTempPath()}codiga.yml"; + + CodigaConfigFileUtil.CreateCodigaConfigFile(LanguageUtils.LanguageEnumeration.Python, serviceProvider); + + Assert.That(File.Exists(_codigaConfigFile), Is.True); + Assert.That(File.ReadAllText(_codigaConfigFile), Is.EqualTo("rulesets:\n" + + " - python-security\n" + + " - python-best-practices\n" + + " - python-code-style")); + } + + [Test] + public void CreateCodigaConfigFile_should_create_codiga_yml_with_js_rulesets_for_js() + { + var serviceProvider = MockServiceProvider(Path.GetTempPath()); + _codigaConfigFile = $"{Path.GetTempPath()}codiga.yml"; + + CodigaConfigFileUtil.CreateCodigaConfigFile(LanguageUtils.LanguageEnumeration.Javascript, serviceProvider); + + Assert.That(File.Exists(_codigaConfigFile), Is.True); + Assert.That(File.ReadAllText(_codigaConfigFile), Is.EqualTo("rulesets:\n" + + " - jsx-a11y\n" + + " - jsx-react\n" + + " - react-best-practices")); + } + + [Test] + public void CreateCodigaConfigFile_should_create_codiga_yml_with_js_rulesets_for_ts() + { + var serviceProvider = MockServiceProvider(Path.GetTempPath()); + _codigaConfigFile = $"{Path.GetTempPath()}codiga.yml"; + + CodigaConfigFileUtil.CreateCodigaConfigFile(LanguageUtils.LanguageEnumeration.Typescript, serviceProvider); + + Assert.That(File.Exists(_codigaConfigFile), Is.True); + Assert.That(File.ReadAllText(_codigaConfigFile), Is.EqualTo("rulesets:\n" + + " - jsx-a11y\n" + + " - jsx-react\n" + + " - react-best-practices")); + } + #endregion #region Invalid ruleset names diff --git a/src/Tests/Rosie/CodigaDefaultRulesetInfoBarHelperTest.cs b/src/Tests/Rosie/CodigaDefaultRulesetInfoBarHelperTest.cs new file mode 100644 index 0000000..671c1ca --- /dev/null +++ b/src/Tests/Rosie/CodigaDefaultRulesetInfoBarHelperTest.cs @@ -0,0 +1,144 @@ +using System.IO; +using Extension.Rosie; +using Extension.SnippetFormats; +using NUnit.Framework; + +namespace Tests.Rosie +{ + /// + /// Unit test for . + /// + [TestFixture] + public class CodigaDefaultRulesetInfoBarHelperTest + { + private string _solutionDir; + + [SetUp] + public void Setup() + { + _solutionDir = $"{Path.GetTempPath()}solDir"; + Directory.CreateDirectory(_solutionDir); + } + + [Test] + public void FindSupportedFile_should_find_not_find_supported_file_in_root_only() + { + File.WriteAllText($"{_solutionDir}\\text_file.txt", ""); + + var language = CodigaDefaultRulesetsInfoBarHelper.FindSupportedFile(_solutionDir); + + Assert.That(language, Is.EqualTo(LanguageUtils.LanguageEnumeration.Unknown)); + } + + [Test] + public void FindSupportedFile_should_find_not_find_supported_file_in_subdirectory() + { + Directory.CreateDirectory($"{_solutionDir}\\subDir"); + File.WriteAllText($"{_solutionDir}\\text_file.txt", ""); + File.WriteAllText($"{_solutionDir}\\subDir\\text_file.txt", ""); + + var language = CodigaDefaultRulesetsInfoBarHelper.FindSupportedFile(_solutionDir); + + Assert.That(language, Is.EqualTo(LanguageUtils.LanguageEnumeration.Unknown)); + } + + [Test] + public void FindSupportedFile_should_find_python_file_in_root() + { + File.WriteAllText($"{_solutionDir}\\text_file.txt", ""); + File.WriteAllText($"{_solutionDir}\\python_file.py", ""); + + var language = CodigaDefaultRulesetsInfoBarHelper.FindSupportedFile(_solutionDir); + + Assert.That(language, Is.EqualTo(LanguageUtils.LanguageEnumeration.Python)); + } + + [Test] + public void FindSupportedFile_should_find_typescript_file_in_root() + { + File.WriteAllText($"{_solutionDir}\\text_file.txt", ""); + File.WriteAllText($"{_solutionDir}\\ts_file.ts", ""); + + var language = CodigaDefaultRulesetsInfoBarHelper.FindSupportedFile(_solutionDir); + + Assert.That(language, Is.EqualTo(LanguageUtils.LanguageEnumeration.Typescript)); + } + + [Test] + public void FindSupportedFile_should_find_python_file_in_subdirectory() + { + var subDir = $"{Path.GetTempPath()}solDir\\subDir"; + Directory.CreateDirectory(subDir); + File.WriteAllText($"{_solutionDir}\\text_file.txt", ""); + File.WriteAllText($"{subDir}\\text_file.txt", ""); + File.WriteAllText($"{subDir}\\python_file.py", ""); + + var language = CodigaDefaultRulesetsInfoBarHelper.FindSupportedFile(_solutionDir); + + Assert.That(language, Is.EqualTo(LanguageUtils.LanguageEnumeration.Python)); + } + + [Test] + public void FindSupportedFile_should_find_js_file_in_subdirectory() + { + var subDir = $"{Path.GetTempPath()}solDir\\subDir"; + Directory.CreateDirectory(subDir); + File.WriteAllText($"{_solutionDir}\\text_file.txt", ""); + File.WriteAllText($"{subDir}\\text_file.txt", ""); + File.WriteAllText($"{subDir}\\js_file.js", ""); + + var language = CodigaDefaultRulesetsInfoBarHelper.FindSupportedFile(_solutionDir); + + Assert.That(language, Is.EqualTo(LanguageUtils.LanguageEnumeration.Javascript)); + } + + [Test] + public void FindSupportedFile_should_find_jsx_file_in_subdirectory() + { + var subDir = $"{Path.GetTempPath()}solDir\\subDir"; + Directory.CreateDirectory(subDir); + File.WriteAllText($"{_solutionDir}\\text_file.txt", ""); + File.WriteAllText($"{subDir}\\text_file.txt", ""); + File.WriteAllText($"{subDir}\\jsx_file.jsx", ""); + + var language = CodigaDefaultRulesetsInfoBarHelper.FindSupportedFile(_solutionDir); + + Assert.That(language, Is.EqualTo(LanguageUtils.LanguageEnumeration.Javascript)); + } + + [Test] + public void FindSupportedFile_should_find_ts_file_in_subdirectory() + { + var subDir = $"{Path.GetTempPath()}solDir\\subDir"; + Directory.CreateDirectory(subDir); + File.WriteAllText($"{_solutionDir}\\text_file.txt", ""); + File.WriteAllText($"{subDir}\\text_file.txt", ""); + File.WriteAllText($"{subDir}\\ts_file.ts", ""); + + var language = CodigaDefaultRulesetsInfoBarHelper.FindSupportedFile(_solutionDir); + + Assert.That(language, Is.EqualTo(LanguageUtils.LanguageEnumeration.Typescript)); + } + + [Test] + public void FindSupportedFile_should_find_tsx_file_in_subdirectory() + { + var subDir = $"{Path.GetTempPath()}solDir\\subDir"; + Directory.CreateDirectory(subDir); + File.WriteAllText($"{_solutionDir}\\text_file.txt", ""); + File.WriteAllText($"{subDir}\\text_file.txt", ""); + File.WriteAllText($"{subDir}\\tsx_file.tsx", ""); + + var language = CodigaDefaultRulesetsInfoBarHelper.FindSupportedFile(_solutionDir); + + Assert.That(language, Is.EqualTo(LanguageUtils.LanguageEnumeration.Typescript)); + } + + [TearDown] + public void TearDown() + { + if (Directory.Exists(_solutionDir)) + Directory.Delete(_solutionDir, true); + } + } +} \ No newline at end of file diff --git a/src/Tests/Rosie/RosieLanguageSupportTest.cs b/src/Tests/Rosie/RosieLanguageSupportTest.cs index 01f6f4f..10eda19 100644 --- a/src/Tests/Rosie/RosieLanguageSupportTest.cs +++ b/src/Tests/Rosie/RosieLanguageSupportTest.cs @@ -26,6 +26,30 @@ public void IsLanguageSupported_should_return_false_for_unsupported_language() Assert.That(isLanguageSupported, Is.False); } + [Test] + public void IsLanguageOfFileSupported_should_return_true_for_supported_language() + { + var isLanguageSupported = RosieLanguageSupport.IsLanguageOfFileSupported("typescript_file.tsx"); + + Assert.That(isLanguageSupported, Is.True); + } + + [Test] + public void IsLanguageOfFileSupported_should_return_false_for_unsupported_language() + { + var isLanguageSupported = RosieLanguageSupport.IsLanguageOfFileSupported("csharp_file.cs"); + + Assert.That(isLanguageSupported, Is.False); + } + + [Test] + public void IsLanguageOfFileSupported_should_return_false_for_null_language() + { + var isLanguageSupported = RosieLanguageSupport.IsLanguageOfFileSupported(null); + + Assert.That(isLanguageSupported, Is.False); + } + [Test] public void GetRosieLanguage_should_return_language_string_for_supported_language() {