-
Notifications
You must be signed in to change notification settings - Fork 4.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support global analyzer config levels: #49834
Support global analyzer config levels: #49834
Conversation
- Read the 'global_level' special key - Allow conflicting keys in global configs to be resolved by level - Only warn about conflicts that have the same level - Automatically treat as global and assign a level of 100 for configs called '.globalconfig' - Add tests
@dotnet/roslyn-compiler for review please. //cc @mavasani |
Thanks @chsienki. I am not familiar with the code being changed here, so will let the compiler team review the PR. I can vouch that this is a very valuable feature add. We ship some globalconfigs with the .NET SDK along with SDK analyzers, and we have seen lot of users complain that they are seeing conflicts in user provided globalconfigs with the ones shipping in the SDK. This PR should address all these user requests. |
0283414
to
1dce77f
Compare
Implements category specific AnalysisLevel properties as per dotnet/roslyn#49250 (comment). Users would now be able to specify the core AnalysisLevel and override category specific AnalysisLevel for each category. ```xml <AnalysisLevel>5-minimum</AnalysisLevel> <AnalysisLevelSecurity>latest-all</ AnalysisLevelSecurity > <AnalysisLevelPerformance>preview-recommended</AnalysisLevelPerformance> <AnalysisLevelGlobalization>5-none</AnalysisLevelGlobalization> ``` 1. The post-build tool now generates category-specific global configs for each supported analysis mode 2. The tool also generates category-specific `AddGlobalAnalyzerConfigForPackage` targets so that if user overrides a category-specific AnalysisLevel property NOTE: This PR depends on feature dotnet/roslyn#48634, which is currently in code review: dotnet/roslyn#49834
Note for @chsienki - once this PR is merged, kindly open an issue or PR on dotnet/docs repo to update the documentation for global config precedence: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/configuration-files#precedence |
{ | ||
get | ||
{ | ||
if (GlobalSection.Properties.TryGetValue(GlobalLevelKey, out string? val) && int.TryParse(val, out int level)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
int.TryParse(val, out int level) [](start = 93, length = 32)
If this fails, shouldn't we return 100 rather than potentially 0?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we can't read an explicit value, we fall back to the else case below and make a decision based on filename. So a user config with an error will get 100, but a nuget one with an error gets 0. See AnalyzerConfigTests.InvalidGlobalLevelIsIgnored
/// </description></item> | ||
/// </list> | ||
/// | ||
/// This value is unsued when <see cref="IsGlobal"/> is <c>false</c>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This value is unsued when is false [](start = 12, length = 64)
Is this correct that the value is unused when IsGlobal
is false
? It looks like MergeSection()
uses this value whenever there is a conflict. #Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, MergeSection()
is part of the global config builder, so we'll only get there when IsGlobal
is true
_values.Add(section.Name, sectionDict); | ||
} | ||
|
||
_duplicates.TryGetValue(section.Name, out var duplicateDict); | ||
foreach ((var key, var value) in section.Properties) | ||
{ | ||
if (isGlobalSection && Section.PropertiesKeyComparer.Equals(key, GlobalKey)) | ||
if (isGlobalSection && (Section.PropertiesKeyComparer.Equals(key, GlobalKey) || Section.PropertiesKeyComparer.Equals(key, GlobalLevelKey))) | ||
{ | ||
continue; | ||
} | ||
|
||
bool keyInSection = sectionDict.ContainsKey(key); | ||
bool keyDuplicated = duplicateDict?.ContainsKey(key) ?? false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider getting the dictionary values, to simplify the code below:
bool keyInSection = sectionDict.TryGetValue(key, out var sectionValue);
ImmutableDictionary<string, (int, ArrayBuilder<string>)>.Builder duplicateValue = null;
bool keyDuplicated = duplicateDict?.TryGetValue(key, out duplicateValue) == true;
if ((!keyInSection && !keyDuplicated)
|| (keyInSection && sectionValue.globalLevel < globalLevel)
|| (keyDuplicated && duplicateValue.globalLevel < globalLevel))
{
...
``` #Closed
{ | ||
sectionDict.Add(key, (value, configPath)); | ||
sectionDict[key] = (value, configPath, globalLevel); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sectionDict[key] = (value, configPath, globalLevel); [](start = 24, length = 52)
Is this correct if sectionValue.globalLevel >= globalLevel
and dictionaryValue.globalLevel < globalLevel
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can't get into that situation: If we have one in the section, then it must be the highest precedence, and thus there isn't anything in the duplicate dictionary. If we have duplicates then they must be of the same precedence, and we would have removed the item from the section previously (which we assert at line 616) #Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't quite follow. But does that mean if we find the key in the sectionDict
, then we won't find it in duplicateDict
? If so, would it help to make the assumption clear by only checking for duplicate if not found in the section?
bool keyInSection = sectionDict.TryGetValue(key, out var sectionValue);
bool keyDuplicated = !keyInSection && duplicateDict?.TryGetValue(key, out duplicateValue) == true;
if (!keyInSection || !keyDuplicated)
{
sectionDict.Add(key, (value, configPath, globalLevel));
}
else
{
int existingGlobalLevel = keyInSection ? sectionValue.globalLevel : duplicateValue.GlobalLevel;
if (existingGlobalLevel < globalLevel)
{
...
}
else if (existingGlobalLevel == globalLevel)
{
...
}
}
In reply to: 544743061 [](ancestors = 544743061)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, that is a far nicer way of writing the same logic, thanks!
{ | ||
continue; | ||
} | ||
|
||
bool keyInSection = sectionDict.ContainsKey(key); | ||
bool keyDuplicated = duplicateDict?.ContainsKey(key) ?? false; | ||
bool keyInSection = sectionDict.TryGetValue(key, out var sectionValue) == true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
== true [](start = 90, length = 8)
Minor: == true
is not necessary. #Closed
_duplicates.Add(section.Name, duplicateDict); | ||
} | ||
|
||
// record that this key is now a duplicate | ||
ArrayBuilder<string> configList = keyDuplicated ? duplicateDict[key] : ArrayBuilder<string>.GetInstance(); | ||
ArrayBuilder<string> configList = duplicateValue.configPaths ?? ArrayBuilder<string>.GetInstance(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
. [](start = 72, length = 1)
?.
#Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
duplicateValue
is a tuple, so it's never null
, just the configPaths
it contains when we didn't find it in the dictionary.
@@ -2175,6 +2175,257 @@ public void GlobalConfigIssuesWarningWithInvalidSectionNames(string sectionName, | |||
public void GlobalConfigIssuesWarningWithInvalidSectionNames_PlatformSpecific(string sectionName, bool isValidWindows, bool isValidOther) | |||
=> GlobalConfigIssuesWarningWithInvalidSectionNames(sectionName, ExecutionConditionUtil.IsWindows ? isValidWindows : isValidOther); | |||
|
|||
[Theory] | |||
[InlineData("/.globalconfig", true)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider adding a case where we use a Turkish i (İ
) to make sure we use an invariant comparer here.
[/file.cs] | ||
option1 = abc"); | ||
|
||
analyzerConfig2 = analyzerConfigFile2.WriteAllText(@" | ||
is_global = true | ||
global_level = 100 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did we need to add these entries here? Didn't see any change to the tests verifying the text here so not sure why we needed to add this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These tests are checking the error messages that occur when keys collide, so I made it explicit that they would continue to collide. I could rename the files and let the heuristic pick up the 0 value, but I figured it was better to be explicit in the test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah okay, makes sense.
@@ -218,6 +264,8 @@ private static bool IsComment(string line) | |||
return false; | |||
} | |||
|
|||
private static bool IsGlobalFileName(string fileName) => Path.GetFileName(fileName).Equals(UserGlobalConfigName, StringComparison.OrdinalIgnoreCase); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method is called a number of times and every time is an allocation. There also doesn't appear to be a case where we call it zero times. Consider just calculating this once and storing it in a field.
} | ||
} | ||
|
||
internal readonly bool _hasGlobalFileName; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
internal [](start = 8, length = 8)
private
?
The note regarding the necessity of `is_global = true` in files named `.globalconfig` is incorrect since dotnet/roslyn#49834.
The note regarding the necessity of `is_global = true` in files named `.globalconfig` is incorrect since dotnet/roslyn#49834.
Fixes #48634