Skip to content
This repository has been archived by the owner on May 4, 2023. It is now read-only.

Add default rulesets config for JS and TS files #25

Merged
merged 1 commit into from
Dec 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions src/Extension/Rosie/CodigaConfigFileUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -59,11 +60,19 @@ public static class CodigaConfigFileUtil
/// Creates the Codiga config file in the solution's root directory with default Python rulesets.
/// </summary>
/// <param name="serviceProvider">The service provider to retrieve information about the solution from.</param>
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);
}
}

/// <summary>
Expand Down
178 changes: 109 additions & 69 deletions src/Extension/Rosie/CodigaDefaultRulesetsInfoBarHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -18,13 +18,16 @@ namespace Extension.Rosie
/// <seealso cref="https://learn.microsoft.com/en-us/visualstudio/extensibility/ux-guidelines/notifications-and-progress-for-visual-studio?view=vs-2022#BKMK_EmbeddedInfobar"/>
/// <seealso cref="https://learn.microsoft.com/en-us/visualstudio/extensibility/vsix/recipes/notifications?view=vs-2022"/>
/// <seealso cref="https://stackoverflow.com/questions/49278306/how-do-i-find-the-open-folder-in-a-vsix-extension"/>
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" };

/// <summary>
/// Holds and instance of <see cref="InfoBar"/> that is saved in <see cref="ShowDefaultRulesetCreationInfoBarAsync"/>.
/// <br/>
Expand Down Expand Up @@ -56,7 +59,8 @@ internal class InfoBarHolder
/// <ol>
/// <li>The user hasn't clicked the <strong>Never for this solution</strong> option.</li>
/// <li>There is no Codiga config file in the solution root/open folder.</li>
/// <li>At least one of the projects in the solution is a Python project.</li>
/// <li>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.</li>
/// </ol>
/// </summary>
internal static async void ShowDefaultRulesetCreationInfoBarAsync(InfoBarHolder infoBarHolder)
Expand All @@ -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),
Expand All @@ -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<SVsServiceProvider>());
break;
case NeverForThisSolutionActionText:
SolutionSettings.SaveNeverNotifyUserToCreateCodigaConfigFile(
VS.GetMefService<SVsServiceProvider>());
break;
}

infoBarHolder.InfoBar = null;
}
//Whichever action is clicked, close the info bar
args.InfoBarUIElement.Close();
};

/// <summary>
/// Handles when the user clicks one of the items in the info bar.
/// </summary>
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<SVsServiceProvider>());
break;
case NeverForThisSolutionActionText:
SolutionSettings.SaveNeverNotifyUserToCreateCodigaConfigFile(
VS.GetMefService<SVsServiceProvider>());
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
}
});
}
}

/// <summary>
Expand All @@ -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
/// </summary>
/// <param name="serviceProvider">The service provider to retrieve information about the solution from.</param>
private static async Task<bool> IsSolutionContainPythonAsync(SVsServiceProvider serviceProvider)
private static async Task<LanguageEnumeration> GetLanguageOfSupportedProjectOrFileAsync(
SVsServiceProvider serviceProvider)
{
var solution = await ThreadHelper.JoinableTaskFactory.RunAsync(async () =>
{
Expand All @@ -150,46 +158,78 @@ private static async Task<bool> 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")));
}

/// <summary>
/// Searches the argument <c>directory</c> and all its sub-directories for the presence of files with <c>.py</c>
/// extension.
/// Searches the argument <c>directory</c> and all its sub-directories for the presence of files with any
/// of the supported languages.
/// <br/>
/// It currently excludes lookup in IDE solution and project specific folders, <c>.vs</c> and <c>.idea</c>.
/// It excludes lookup in some folders listed in <see cref="FolderNamesToExclude"/>.
/// </summary>
/// <param name="directory">The root directory to search in</param>
/// <returns>The language of the found file if there is a file found, or null if no file was found.</returns>
private static LanguageUtils.LanguageEnumeration? IsContainPythonFile(string directory)
/// <returns>The language of the found file if there is a file found, or 'Unknown' if no file with supported language was found.</returns>
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;
}
}
}
Expand All @@ -200,7 +240,7 @@ private static async Task<bool> IsSolutionContainPythonAsync(SVsServiceProvider
// with no specific file found
}

return null;
return LanguageEnumeration.Unknown;
}
}
}
6 changes: 6 additions & 0 deletions src/Extension/Rosie/CodigaRulesetConfigs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
}
52 changes: 51 additions & 1 deletion src/Tests/Rosie/CodigaConfigFileUtilTest.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.IO;
using Extension.Rosie;
using Extension.SnippetFormats;
using NUnit.Framework;
using static Tests.ServiceProviderMockSupport;

Expand Down Expand Up @@ -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
Expand Down
Loading