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

Modify global analyzer config precedence: #45871

Merged
merged 13 commits into from
Aug 29, 2020

Conversation

chsienki
Copy link
Member

@chsienki chsienki commented Jul 10, 2020

Fixes #44804

  • Pre-parse the global options into their own collection
  • Don't put global diagnostics on the tree level options
  • Add global diagnostics onto the tree options provider
  • Pass through the global diagnostics during compilation
  • Update the analyzer supression code
  • Add tests

Precedence level is now:

  1. Warning level
  2. Syntax tree level
  3. Compilation level
  4. Global analyzer level
  5. Global warning level

/// <summary>
/// Get diagnostic severity set globally for a given diagnostic identifier
/// </summary>
public abstract bool TryGetGlobalDiagnosticValue(string diagnosticId, out ReportDiagnostic severity);
}

internal sealed class CompilerSyntaxTreeOptionsProvider : SyntaxTreeOptionsProvider
Copy link
Member Author

@chsienki chsienki Jul 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ This type is a new public api. Should we rename it to better reflect that it's not really about TreeOptions?

Perhaps DiagnosticSeverityProvider? Although its based on top of analyzer options it really only returns ReportDiagnostics #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'm not sure if there is time to rename APIs. Isn't the API locked with .NET 5 RC1 shipping?


In reply to: 452976584 [](ancestors = 452976584)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This better not be locked given we just had our review of APIs today and we have other things to change here too. 😄

@chsienki Just to make sure we understood, this needs to be a different API than TryGetGlobalDiagnosticValue because other options have to be slotted in the middle in terms of the precedence? And that one is just the compilation level diagnostic settings?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TryGetGlobalDiagnosticValue has to exist as the user has to have a way of asking for diagnostics that still apply when not in the context of a syntax tree.

My comment is that I think we should rename CompilerSyntaxTreeOptionsProvider as we only use it to get diagnostics. Though i'm now wondering if keeping as TreeOptions is better as as it allows us to add things onto it in the future


In reply to: 473402105 [](ancestors = 473402105)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are not locked yet, we can still change them if needed.

}
break;
}
}

// 5. Global analyzer options
Copy link
Contributor

@mavasani mavasani Jul 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this should go immediately after // 3. Compilation level check. Any rule ID specific configuration should still trump over generic diagnostic option handled in 4. Global options and be done before pragma related checks. Basically, if global config options suppress a specific diagnostic ID, then it would need a rule ID specific configuration from user to override it. #Resolved

Copy link
Member Author

@chsienki chsienki Jul 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok, that makes sense I think. I wasn't sure how we wanted to handle the precedence here.

So, if a NuGet package ups a specific diagnostic to a warning, but the user specifies the global -warn:0 then we would still expect to warn about that particular diagnostic?
#Resolved

Copy link
Member Author

@chsienki chsienki Jul 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right yes thinking about it, the user would say -warn:0, but then still have to explicity add a -nowarn for the one the global config upped. That makes sense I think. #Resolved

Copy link
Contributor

@mavasani mavasani Jul 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, any diagnostic ID specific setting should override diagnostic ID agnostic setting. That has been the common design for diagnostic ID configuration from all sources. #Resolved

@@ -335,6 +335,12 @@ internal bool IsSupportedDiagnostic(DiagnosticAnalyzer analyzer, Diagnostic diag
isSuppressed = true;
}

// Global editorconfig settings only apply if it wasn't supressed via commandline already
if (!isSuppressed && options.SyntaxTreeOptionsProvider is object && options.SyntaxTreeOptionsProvider.TryGetGlobalDiagnosticValue(diag.Id, out severity))
Copy link
Contributor

@mavasani mavasani Jul 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is better to instead move this check upfront based on precedence order:

                // Is this diagnostic suppressed by default (as written by the rule author)
                var isSuppressed = !diag.IsEnabledByDefault;

                // Global editorconfig settings overrides the analyzer author
                // Compilation wide user settings (diagnosticOptions) from ruleset/nowarn/warnaserror overrides the analyzer author and global editorconfig settings.
                if (diagnosticOptions.TryGetValue(diag.Id, out var severity) ||
                    options.SyntaxTreeOptionsProvider is object && options.SyntaxTreeOptionsProvider.TryGetGlobalDiagnosticValue(diag.Id, out severity))
                {
                    isSuppressed = severity == ReportDiagnostic.Suppress;
                }
                else
                {
                    severity = isSuppressed ? ReportDiagnostic.Suppress : DiagnosticDescriptor.MapSeverityToReport(diag.DefaultSeverity);
                }
``` #Resolved

@@ -176,6 +176,11 @@ Namespace Microsoft.CodeAnalysis.VisualBasic
End If
End If

Dim globalReport As ReportDiagnostic
If (Not isSpecified) AndAlso syntaxTreeOptions IsNot Nothing AndAlso syntaxTreeOptions.TryGetGlobalDiagnosticValue(id, globalReport) Then
Copy link
Contributor

@mavasani mavasani Jul 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same suggestion here. Rule ID specific global config options should override Rule ID agnostic options. #Resolved

@chsienki chsienki force-pushed the analyer_config_global_precedence2 branch 2 times, most recently from 10ea50a to 26bf2ee Compare August 10, 2020 21:58
@chsienki chsienki marked this pull request as ready for review August 11, 2020 18:01
@chsienki chsienki requested review from a team as code owners August 11, 2020 18:01
@chsienki
Copy link
Member Author

@dotnet/roslyn-compiler for review please.

//cc @mavasani @jasonmalinowski

@jcouv
Copy link
Member

jcouv commented Aug 11, 2020

Looking
Sorry for the delay (got distracted), will get back to this PR in the morning. #Closed

@jaredpar
Copy link
Member

jaredpar commented Aug 12, 2020

Could we add a quick summary of what the precedence is now to the description of the PR? #Resolved

@chsienki
Copy link
Member Author

Ping @dotnet/roslyn-compiler for reviews please :)

@@ -145,8 +145,35 @@ private AnalyzerConfigSet(ImmutableArray<AnalyzerConfig> analyzerConfigs, Global
Debug.Assert(allMatchers.Count == _analyzerConfigs.Length);

_analyzerMatchers = allMatchers.ToImmutableAndFree();

// parse the global options upfront
Copy link
Member

@jcouv jcouv Aug 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the motivation to move this parsing of global options up? (why not keep it in GetOptionsForSourcePath?) #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.

Because we're returning them both as a property and inline in GetOptionsForSourcePath we should probably just make it lazy though, rather than doing it unconditionally.


In reply to: 471683120 [](ancestors = 471683120)

@@ -87,7 +87,8 @@ internal abstract partial class CommonCompiler
TextWriter consoleOutput,
TouchedFileLogger touchedFilesLogger,
ErrorLogger errorLoggerOpt,
ImmutableArray<AnalyzerConfigOptionsResult> analyzerConfigOptions);
ImmutableArray<AnalyzerConfigOptionsResult> analyzerConfigOptions,
AnalyzerConfigOptionsResult globalConfigOptions);
Copy link
Member

@jcouv jcouv Aug 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: since global options now take precedence, consider moving them up in the parameter list (before analyzerConfigOptions). #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.

Global options are lower precedence than analyzer options.


In reply to: 471708791 [](ancestors = 471708791)

/// <summary>
/// Get diagnostic severity set globally for a given diagnostic identifier
/// </summary>
public abstract bool TryGetGlobalDiagnosticValue(string diagnosticId, out ReportDiagnostic severity);
Copy link
Member

@jcouv jcouv Aug 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TryGetGlobalDiagnosticValue [](start = 29, length = 27)

Sorry if this was already discussed with feature crew offline, but what consumer of our public API needs this new API?
I would have thought the existing TryGetDiagnosticValue would be sufficient. #Closed

Copy link
Member Author

@chsienki chsienki Aug 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because global options can exist 'ambiently' without applying to any specific tree. One option would be to call TryGetDiagnosticValue with a null tree but that seems icky.


In reply to: 471710188 [](ancestors = 471710188)

@jcouv
Copy link
Member

jcouv commented Aug 17, 2020

    public void GlobalConfigInvalidSeverity()

Do we have a test for GlobalConfigOptions when there is no global config? #Resolved


Refers to: src/Compilers/Core/CodeAnalysisTest/Analyzers/AnalyzerConfigTests.cs:1920 in 89ba3bf. [](commit_id = 89ba3bf2955022f11a1d0ab2b1c3b8b96df22f8e, deletion_comment = False)

@@ -10020,6 +10020,58 @@ End Class")
End If
End Sub

<Fact>
<WorkItem(44087, "https://github.com/dotnet/roslyn/issues/44804")>
Public Sub GlobalAnalyzerConfigDiagnosticOptionsCanBeOverridenByCommandLine()
Copy link
Member

@jcouv jcouv Aug 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GlobalAnalyzerConfigDiagnosticOptionsCanBeOverridenByCommandLine [](start = 19, length = 64)

nit: thanks for naming tests to make intent clear. Makes the review easier :-)

Copy link
Member

@jcouv jcouv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM Thanks (iteration 10)

@jcouv jcouv self-assigned this Aug 17, 2020
@chsienki
Copy link
Member Author

    public void GlobalConfigInvalidSeverity()

Added


In reply to: 675076806 [](ancestors = 675076806)


Refers to: src/Compilers/Core/CodeAnalysisTest/Analyzers/AnalyzerConfigTests.cs:1920 in 89ba3bf. [](commit_id = 89ba3bf2955022f11a1d0ab2b1c3b8b96df22f8e, deletion_comment = False)

@chsienki
Copy link
Member Author

Ping @dotnet/roslyn-compiler for a second review please :)

@@ -119,7 +119,8 @@ internal static class CSharpDiagnosticFilter
/// 1. Warning level
/// 2. Syntax tree level
/// 3. Compilation level
/// 4. Global warning level
/// 4. Global analyzer config
/// 5. Global warning level
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is 'global warning level' different than 'warning level'? I'm trying to understand this list and keep coming up confused.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could probably make the wording clearer. Warning level is the /warn:3 that enables/disables big swathes of warnings.

Global warning level is /warnaserror:+/- that bumps all warnings up to errors


In reply to: 473388737 [](ancestors = 473388737)

[CombinatorialData]
public void TestAddingAndRemovingGlobalEditorConfigFileWithDiagnosticSeverity([CombinatorialValues(LanguageNames.CSharp, LanguageNames.VisualBasic)] string languageName, bool useRecoverableTrees)
{
using var workspace = useRecoverableTrees ? CreateWorkspaceWithRecoverableSyntaxTrees() : CreateWorkspace();
Copy link
Member

@jasonmalinowski jasonmalinowski Aug 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can remove the 'useRecoverableTrees' and make this just a regular [Fact]. The distinction mattered when diagnostic values were held on the trees since we could have bugs where we didn't handle the trees correctly in both cases. It's now moot. I'm guessing this test predates merging the other PR so you were following the correct pattern, at the time. #Resolved

@@ -26,17 +27,27 @@ public sealed class TestSyntaxTreeOptionsProvider : SyntaxTreeOptionsProvider
x => x.Item2,
comparer)
);
if (globalOption.Item1 is object)
Copy link
Member

@jasonmalinowski jasonmalinowski Aug 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we instead make globalOption a (string, ReportDiagnostic) to make the null handling clearer? This file is #nullable enabled so presumably you shouldn't be able to get a null first parameter; the fact you can pass default in line 50 is a compiler bug: #38957 #Resolved

/// <summary>
/// Get diagnostic severity set globally for a given diagnostic identifier
/// </summary>
public abstract bool TryGetGlobalDiagnosticValue(string diagnosticId, out ReportDiagnostic severity);
}

internal sealed class CompilerSyntaxTreeOptionsProvider : SyntaxTreeOptionsProvider
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This better not be locked given we just had our review of APIs today and we have other things to change here too. 😄

@chsienki Just to make sure we understood, this needs to be a different API than TryGetGlobalDiagnosticValue because other options have to be slotted in the middle in terms of the precedence? And that one is just the compilation level diagnostic settings?

@jaredpar
Copy link
Member

jaredpar commented Aug 24, 2020

Do we have a document describing the precedence levels? If not we should create one and if we do we should update it with this PR.

Assuming that "Syntax Tree Level" includes non-global analyzer configurations correct? #Resolved

{
if (_lazyConfigOptions is null)
{
Interlocked.CompareExchange(
Copy link
Member

@jaredpar jaredpar Aug 24, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the value in using this lazy pattern given that AnalyzerConfigOptionsResult is a struct? This property will always return a copy of the value, the ParseGlobalConfigOptions can run multiple times even in this config. Can't see the value StrongBox<AnalyzerConfigOptionResult> provides over AnalyzerConfigOptionResult? (nullable value).

What am I missing here? #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the struct is bigger than an IntPtr, assignment isn't guaranteed to be atomic (AFAIK). Means one thread could read from it and get a half initialized struct if we don't box + interlock exchange.


In reply to: 475294138 [](ancestors = 475294138)

private static void ParseSectionOptions(Section section, TreeOptions.Builder treeBuilder, AnalyzerOptions.Builder analyzerBuilder, ArrayBuilder<Diagnostic> diagnosticBuilder, string analyzerConfigPath, ConcurrentDictionary<ReadOnlyMemory<char>, string> diagIdCache)
{
const string DiagnosticOptionPrefix = "dotnet_diagnostic.";
const string DiagnosticOptionSuffix = ".severity";
Copy link
Member

@jaredpar jaredpar Aug 24, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Believe const locals should be camel cased #Resolved

/// <summary>
/// Get diagnostic severity set globally for a given diagnostic identifier
/// </summary>
public abstract bool TryGetGlobalDiagnosticValue(string diagnosticId, out ReportDiagnostic severity);
}

internal sealed class CompilerSyntaxTreeOptionsProvider : SyntaxTreeOptionsProvider
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are not locked yet, we can still change them if needed.

@@ -1255,8 +1255,13 @@ private ImmutableHashSet<DiagnosticAnalyzer> ComputeSuppressedAnalyzersForTree(S
var hasUnsuppressedDiagnostic = false;
foreach (var descriptor in descriptors)
{
if (!options.TryGetDiagnosticValue(tree, descriptor.Id, out var configuredSeverity) ||
configuredSeverity != ReportDiagnostic.Suppress)
options.TryGetGlobalDiagnosticValue(descriptor.Id, out var configuredSeverity);
Copy link
Member

@jaredpar jaredpar Aug 24, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider explicitly discarding to acknowledge you're deliberately ignoring this result rather than accidentally doing so.

Suggested change
options.TryGetGlobalDiagnosticValue(descriptor.Id, out var configuredSeverity);
_ = options.TryGetGlobalDiagnosticValue(descriptor.Id, out var configuredSeverity);
``` #Resolved

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way you are using configuredSeverity here implies that on a false return the value is guaranteed to be default. What happens if we pass a non-defaulted value into the API. Does it default the value?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It explicitly sets it to Default (see https://github.com/dotnet/roslyn/pull/45871/files/55ac7e5acdd2a1f91625ce31caa99af5cc43b5b9#diff-ffb0437c61e5f1029ae0c11cb59507d9R89). It's an out param though, so I don't understand how we'd pass in an explicit value?


In reply to: 475295255 [](ancestors = 475295255)

@@ -26,17 +27,27 @@ public sealed class TestSyntaxTreeOptionsProvider : SyntaxTreeOptionsProvider
x => x.Item2,
comparer)
);
if (globalOption.key is object)
{
_globalOptions = new Dictionary<string, ReportDiagnostic>() { { globalOption.key, globalOption.diagnostic } };
Copy link
Member

@jaredpar jaredpar Aug 24, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use an explicit comparer. #Resolved

@@ -186,6 +187,11 @@ internal static class CSharpDiagnosticFilter
// 3. Compilation level
isSpecified = true;
}
else if (syntaxTreeOptions is object && syntaxTreeOptions.TryGetGlobalDiagnosticValue(id, out report))
{
// 4. Global analyzer config options
Copy link
Member

@jaredpar jaredpar Aug 24, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Crazy specific NIT: the rest of the comments match up with the comment verbiage at the top of the method.

Suggested change
// 4. Global analyzer config options
// 4. Global analyzer config level
``` #Resolved

@chsienki
Copy link
Member Author

Hmm, I'm not sure we do? We should write one, unless @mavasani knows of such a thing already?


In reply to: 678853235 [](ancestors = 678853235)

@mavasani
Copy link
Contributor

Hmm, I'm not sure we do? We should write one, unless @mavasani knows of such a thing already?

In reply to: 678853235 [](ancestors = 678853235)

I don't believe have such a document, might be good to add something here: https://github.com/dotnet/roslyn/tree/master/docs/analyzers

@chsienki
Copy link
Member Author

Ping @dotnet/roslyn-compiler for second review please :)

@chsienki
Copy link
Member Author

Filed #47154 to track creating a doc


In reply to: 679233670 [](ancestors = 679233670,678853235)

- Pre-parse the global options into their own collection
- Don't put global diagnostics on the tree level options
- Add global diagnostics onto the tree options provider
- Pass through the global diagnostics during compilation
- Update the analyzer supression code
- Add tests
add a test for no global config
@chsienki chsienki force-pushed the analyer_config_global_precedence2 branch from 7413077 to daadd45 Compare August 28, 2020 19:15
@chsienki chsienki merged commit c75bc71 into dotnet:master Aug 29, 2020
@ghost ghost added this to the Next milestone Aug 29, 2020
333fred added a commit to 333fred/roslyn that referenced this pull request Aug 31, 2020
* upstream/master: (43 commits)
  Fix typos (dotnet#47264)
  Disable CS0660 where Full Solution Analysis produces a different result
  Pass in correct arguments to TypeScript handler
  Add skipped failing test for IDE0044 (dotnet#45288)
  Avoid first chance exception
  Code review feedback part 3
  Update stale URLs in the readme
  Update IntegrationTestSourceGenerator.cs
  Fix comment
  Update IntegrationTestSourceGenerator.cs
  Remove unused reference to obsolete class
  Update nullable annotations in Classification folder
  Update generator public API names: (dotnet#47222)
  NullableContextAttribute for property parameters is from accessor (dotnet#47223)
  Modify global analyzer config precedence: (dotnet#45871)
  Skip the C# source generator integration test running for now
  Additional XAML LSP handlers (dotnet#47217)
  Remap diagnostics using IWorkspaceVenusSpanMappingService in-proc
  Warn on type named "record" (dotnet#47094)
  Bail out early before getting SyntaxTree
  ...
@allisonchou allisonchou modified the milestones: Next, 16.8.P3 Aug 31, 2020
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.

Precedence of diagnostic severity settings from compilation options, editorconfig and global analyzerconfig
6 participants