Skip to content
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

Merged
merged 9 commits into from
Dec 19, 2020

Conversation

chsienki
Copy link
Member

@chsienki chsienki commented Dec 7, 2020

  • 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

Fixes #48634

- 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
@chsienki
Copy link
Member Author

chsienki commented Dec 7, 2020

@dotnet/roslyn-compiler for review please.

//cc @mavasani

@mavasani
Copy link
Contributor

mavasani commented Dec 7, 2020

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.

@chsienki chsienki force-pushed the global_config_levels branch from 0283414 to 1dce77f Compare December 8, 2020 01:50
mavasani added a commit to mavasani/roslyn-analyzers that referenced this pull request Dec 11, 2020
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
@mavasani
Copy link
Contributor

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))
Copy link
Member

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?

Copy link
Member Author

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>;
Copy link
Member

@cston cston Dec 16, 2020

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

Copy link
Member Author

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;
Copy link
Member

@cston cston Dec 16, 2020

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);
Copy link
Member

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?

Copy link
Member Author

@chsienki chsienki Dec 17, 2020

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

Copy link
Member

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)

Copy link
Member Author

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;
Copy link
Member

@cston cston Dec 17, 2020

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();
Copy link
Member

@cston cston Dec 17, 2020

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

Copy link
Member Author

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)]
Copy link
Member

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
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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);
Copy link
Member

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;
Copy link
Member

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?

@chsienki chsienki changed the base branch from master to release/dev16.9 December 19, 2020 00:21
@chsienki chsienki merged commit cf42e6b into dotnet:release/dev16.9 Dec 19, 2020
alexrp added a commit to vezel-dev/zig-sdk that referenced this pull request Mar 29, 2022
alexrp added a commit to vezel-dev/cathode that referenced this pull request Mar 29, 2022
gewarren pushed a commit to dotnet/docs that referenced this pull request May 16, 2022
The note regarding the necessity of `is_global = true` in files named `.globalconfig` is incorrect since dotnet/roslyn#49834.
baronfel pushed a commit to baronfel/dotnet-docs that referenced this pull request Jun 21, 2022
The note regarding the necessity of `is_global = true` in files named `.globalconfig` is incorrect since dotnet/roslyn#49834.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Global analyzer config precedence
4 participants