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

Commit

Permalink
Merge pull request #31 from codiga/codiga-yaml-ignore
Browse files Browse the repository at this point in the history
Add support for `ignore` property in codiga.yml
  • Loading branch information
dastrong-codiga authored Jan 19, 2023
2 parents 48e672a + dec7c0e commit 196abcf
Show file tree
Hide file tree
Showing 16 changed files with 1,197 additions and 200 deletions.
4 changes: 3 additions & 1 deletion src/Extension/Extension.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,10 @@
<Compile Include="Rosie\Annotation\StringUtils.cs" />
<Compile Include="Rosie\CodigaDefaultRulesetsInfoBarHelper.cs" />
<Compile Include="Rosie\CodigaRulesetConfigs.cs" />
<Compile Include="Rosie\Model\Codiga\CodigaCodeAnalysisConfig.cs" />
<Compile Include="Rosie\Model\Codiga\RuleIgnore.cs" />
<Compile Include="Rosie\Model\Codiga\RulesetIgnore.cs" />
<Compile Include="Rosie\RosieClientProvider.cs" />
<Compile Include="Rosie\CodigaCodeAnalysisConfig.cs" />
<Compile Include="Rosie\CodigaConfigFileUtil.cs" />
<Compile Include="Rosie\Annotation\ApplyRosieFixSuggestedAction.cs" />
<Compile Include="Rosie\Annotation\DisableRosieAnalysisSuggestedAction.cs" />
Expand Down
25 changes: 25 additions & 0 deletions src/Extension/Helpers/SolutionHelper.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.IO;
using System.Threading.Tasks;
using Community.VisualStudio.Toolkit;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;

Expand All @@ -9,6 +11,29 @@ namespace Extension.Helpers
/// </summary>
internal static class SolutionHelper
{
/// <summary>
/// Returns the solution's root dir, or in Open Folder mode, the open folder's path.
/// </summary>
/// <returns></returns>
internal static async Task<string?> GetSolutionDir()
{
var serviceProvider = await GetServiceProvider();
return serviceProvider == null ? null : GetSolutionDir(serviceProvider);
}

/// <summary>
/// Returns the <see cref="SVsServiceProvider"/> that is used to retrieve the solution directory.
/// </summary>
/// <returns></returns>
internal static async Task<SVsServiceProvider?> GetServiceProvider()
{
return await ThreadHelper.JoinableTaskFactory.RunAsync(async () =>
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
return await VS.GetMefServiceAsync<SVsServiceProvider>();
});
}

/// <summary>
/// Returns the solution's root dir, or in Open Folder mode, the open folder's path.
/// </summary>
Expand Down
28 changes: 0 additions & 28 deletions src/Extension/Rosie/CodigaCodeAnalysisConfig.cs

This file was deleted.

100 changes: 58 additions & 42 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.Rosie.Model.Codiga;
using Extension.SnippetFormats;
using Microsoft.VisualStudio.Shell;
using YamlDotNet.Serialization;
Expand Down Expand Up @@ -34,6 +35,8 @@ public static class CodigaConfigFileUtil
private static readonly Regex CodigaRulesetNamePattern = new Regex("^[a-z0-9][a-z0-9-]{4,31}$");

private const string CodigaConfigFileName = "codiga.yml";
private const string RULESETS = "rulesets";
private const string IGNORE = "ignore";

/// <summary>
/// Looks up the Codiga config file in the provided Solution's root directory,
Expand Down Expand Up @@ -76,64 +79,77 @@ public static void CreateCodigaConfigFile(LanguageUtils.LanguageEnumeration lang
}

/// <summary>
/// Collects the list of valid ruleset names from the provided configuration object.
/// Deserializes the provided raw YAML string (the content of the Codiga config file) to a <see cref="CodigaCodeAnalysisConfig"/>
/// object, so that ruleset names, ignore prefixes, etc. can be accessed later.
/// <br/>
/// It deserializes it using dynamic typing.
/// </summary>
/// <param name="config">The configuration containing the rulesets.</param>
/// <returns>The list of ruleset names or empty list if there is no config or no ruleset.</returns>
public static List<string> CollectRulesetNames(CodigaCodeAnalysisConfig? config)
/// <param name="rawYamlConfig">The content of the Codiga config file</param>
/// <returns>The deserialized config file, or <see cref="CodigaCodeAnalysisConfig.EMPTY"/> in case deserialization failed.</returns>
public static CodigaCodeAnalysisConfig DeserializeConfig(string rawYamlConfig)
{
return config?.Rulesets == null
? new List<string>()
: config.Rulesets
if (string.IsNullOrWhiteSpace(rawYamlConfig))
return CodigaCodeAnalysisConfig.EMPTY;

try
{
var codigaConfig = new CodigaCodeAnalysisConfig();
var semiRawConfig = ConfigDeserializer.Deserialize<dynamic>(rawYamlConfig);
if (semiRawConfig is Dictionary<object, object> properties)
{
SetRulesets(properties, codigaConfig);
SetIgnore(properties, codigaConfig);
}

return codigaConfig;
}
catch
{
return CodigaCodeAnalysisConfig.EMPTY;
}
}

/// <summary>
/// Configures the rulesets in the argument <see cref="CodigaCodeAnalysisConfig"/> based on the deserialized config data.
/// </summary>
/// <param name="semiRawConfig">The codiga.yml file as a dictionary of string-object entries</param>
/// <param name="codigaConfig">The Codiga config in which rulesets are being configured</param>
private static void SetRulesets(Dictionary<object, object> semiRawConfig, CodigaCodeAnalysisConfig codigaConfig)
{
if (semiRawConfig.ContainsKey(RULESETS) && semiRawConfig[RULESETS] is List<object> rulesetNames)
{
codigaConfig.Rulesets = rulesetNames
.OfType<string>()
//Filter out non-string value, and null and empty ruleset names
.Where(name => !string.IsNullOrEmpty(name))
//Filter out invalid ruleset names
.Where(IsRulesetNameValid)
.ToList();
}
}

/// <summary>
/// Deserializes the provided raw YAML string (the content of the Codiga config file) to a <see cref="CodigaCodeAnalysisConfig"/>
/// object, so that ruleset names can be accessed later.
///
/// First, it tries to deserialize directly into a <c>CodigaCodeAnalysisConfig</c> instance, and if that fails,
/// it tries to deserialize it using dynamic typing. This is necessary because when the config file is configured e.g. like this:
/// <code>
/// rulesets:
/// - my-csharp-ruleset
/// - rules:
/// - some-rule
/// - my-other-ruleset
/// </code>
/// it would fail with an exception on the <c>rules</c> property, and would not return the rest of the ruleset names.
/// Configures the ignores in the argument <see cref="CodigaCodeAnalysisConfig"/> based on the deserialized config data.
/// </summary>
/// <param name="rawYamlConfig">The content of the Codiga config file</param>
/// <returns>The deserialized config file, or null in case deserialization couldn't happen.</returns>
public static CodigaCodeAnalysisConfig? DeserializeConfig(string rawYamlConfig)
/// <param name="semiRawConfig">The codiga.yml file as a dictionary of string-object entries</param>
/// <param name="codigaConfig">The Codiga config in which ignore config is being configured</param>
private static void SetIgnore(Dictionary<object, object> semiRawConfig, CodigaCodeAnalysisConfig codigaConfig)
{
if (string.IsNullOrWhiteSpace(rawYamlConfig))
return null;

try
{
return ConfigDeserializer.Deserialize<CodigaCodeAnalysisConfig>(rawYamlConfig);
}
catch
//List of [ruleset name -> rule ignore config] mappings
if (semiRawConfig.ContainsKey(IGNORE) && semiRawConfig[IGNORE] is List<object> rulesetIgnoreConfigs)
{
var semiRawConfigFile = ConfigDeserializer.Deserialize<dynamic>(rawYamlConfig);
if (semiRawConfigFile is Dictionary<object, object> properties
//If there is one property, and it is called 'rulesets'
&& properties.Keys.Count == 1 && properties.ContainsKey("rulesets")
//If the value of 'rulesets' is a non-empty list
&& properties["rulesets"] is List<object> rulesetNames && rulesetNames.Count > 0)
foreach (var rulesetIgnoreConfig in rulesetIgnoreConfigs)
{
return new CodigaCodeAnalysisConfig
//[ruleset name -> rule ignore config] mappings
if (rulesetIgnoreConfig is Dictionary<object, object> rulesetIgnoreDict)
{
Rulesets = rulesetNames.OfType<string>().ToList()
};
}
var rulesetIgnore = new RulesetIgnore(
rulesetIgnoreDict.Keys.FirstOrDefault() as string,
rulesetIgnoreDict.Values.FirstOrDefault());

return null;
codigaConfig.Ignore.Add(rulesetIgnore.RulesetName, rulesetIgnore);
}
}
}
}

Expand Down
6 changes: 1 addition & 5 deletions src/Extension/Rosie/CodigaDefaultRulesetsInfoBarHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,7 @@ internal class InfoBarHolder
/// </summary>
internal static async void ShowDefaultRulesetCreationInfoBarAsync(InfoBarHolder infoBarHolder)
{
var serviceProvider = await ThreadHelper.JoinableTaskFactory.RunAsync(async () =>
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
return await VS.GetMefServiceAsync<SVsServiceProvider>();
});
var serviceProvider = await SolutionHelper.GetServiceProvider();

if (SolutionSettings.IsShouldNotifyUserToCreateCodigaConfig(serviceProvider)
&& CodigaConfigFileUtil.FindCodigaConfigFile(serviceProvider) == null)
Expand Down
29 changes: 29 additions & 0 deletions src/Extension/Rosie/Model/Codiga/CodigaCodeAnalysisConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Collections.Generic;

namespace Extension.Rosie.Model.Codiga
{
/// <summary>
/// Represents a codiga.yml configuration file.
/// </summary>
/// <see cref="CodigaConfigFileUtil.DeserializeConfig"/>
public class CodigaCodeAnalysisConfig
{
public static readonly CodigaCodeAnalysisConfig EMPTY = new CodigaCodeAnalysisConfig();

private List<string>? _rulesets;

public List<string> Rulesets
{
get => _rulesets ?? new List<string>();
set => _rulesets = value;
}

/// <summary>
/// Stores [ruleset name -> ruleset ignore configuration] mappings.
/// <br/>
/// Using a map instead of a <c>List&lt;RulesetIgnore></c>, so that we can query the ruleset
/// configs by name, without having to filter the list by the ruleset name.
/// </summary>
public IDictionary<string, RulesetIgnore> Ignore { get; } = new Dictionary<string, RulesetIgnore>();
}
}
97 changes: 97 additions & 0 deletions src/Extension/Rosie/Model/Codiga/RuleIgnore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System.Collections.Generic;
using System.Linq;

namespace Extension.Rosie.Model.Codiga
{
/// <summary>
/// Represents a rule ignore configuration element in the codiga.yml file.
/// <br/>
/// This is the element right under a ruleset name property, e.g.:
/// <code>
/// - rule1:
/// - prefix: /path/to/file/to/ignore
/// </code>
/// or
/// <code>
/// - rule2:
/// - prefix:
/// - /path1
/// - /path2
/// </code>
/// </summary>
public class RuleIgnore
{
public string RuleName { get; }

/// <summary>
/// The list of prefix values under the <c>prefix</c> property.
/// <br/>
/// In case multiple <c>prefix</c> properties are defined under the same rule config,
/// they are all added to this list.
/// <br/>
/// For example, in case of:
/// <code>
/// ignore:
/// - my-python-ruleset:
/// - rule1:
/// - prefix:
/// - /path1
/// - /path2
/// - prefix: /path3
/// </code>
/// all of <c>/path1</c>, <c>/path2</c> and <c>/path3</c> are stored here.
/// <br/>
/// In case a <c>prefix</c> property contains the same value multiple times,
/// they are deduplicated and only once instance is stored, for example:
/// <code>
/// ignore:
/// - my-python-ruleset:
/// - rule1:
/// - prefix:
/// - /path1
/// - /path1
/// </code>
/// </summary>
public List<string> Prefixes { get; } = new List<string>();

public RuleIgnore(string ruleName)
{
RuleName = ruleName;
}

public RuleIgnore(string ruleName, object ruleIgnore)
{
RuleName = ruleName;
if (ruleIgnore is List<object> prefixIgnores)
{
foreach (var prefixIgnore in prefixIgnores)
{
if (prefixIgnore is Dictionary<object, object> prefixIgnoreDict)
{
var prefixIgnoreValue = prefixIgnoreDict.Values.FirstOrDefault();
/*
A 'prefix' property can have a single String value:
- prefix: /path/to/file/to/ignore
*/
if (prefixIgnoreValue is string value)
Prefixes = new List<string> { value };

/*
A 'prefix' property can also have multiple String values as a list:
- prefix:
- /path1
- /path2
*/
else if (prefixIgnoreValue is List<object> prefixes)
//It filters out null and non-String prefix values
Prefixes = prefixes
.Where(prefix => prefix != null)
.OfType<string>()
.Distinct()
.ToList();
}
}
}
}
}
}
Loading

0 comments on commit 196abcf

Please sign in to comment.