diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index c7c28fd9f4..87650f1f43 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -24,6 +24,12 @@ + + + + PreserveNewest + AnalyzerReleases\$(AssemblyName)\AnalyzerReleases.Unshipped.md + diff --git a/src/Tools/GenerateDocumentationAndConfigFiles/Program.cs b/src/Tools/GenerateDocumentationAndConfigFiles/Program.cs index b9606e17ba..6654d8d338 100644 --- a/src/Tools/GenerateDocumentationAndConfigFiles/Program.cs +++ b/src/Tools/GenerateDocumentationAndConfigFiles/Program.cs @@ -666,7 +666,7 @@ async Task checkHelpLinkAsync(string helpLink) async Task createGlobalConfigFilesAsync() { - using var shippedFilesDataBuilder = ArrayBuilder.GetInstance(); + using var releaseTrackingFilesDataBuilder = ArrayBuilder.GetInstance(); using var versionsBuilder = PooledHashSet.GetInstance(); // Validate all assemblies exist on disk and can be loaded. @@ -705,7 +705,19 @@ async Task createGlobalConfigFilesAsync() var assemblyName = Path.GetFileNameWithoutExtension(assembly); var shippedFile = Path.Combine(assemblyDir, "AnalyzerReleases", assemblyName, ReleaseTrackingHelper.ShippedFileName); - if (File.Exists(shippedFile)) + var unshippedFile = Path.Combine(assemblyDir, "AnalyzerReleases", assemblyName, ReleaseTrackingHelper.UnshippedFileName); + var shippedFileExists = File.Exists(shippedFile); + var unshippedFileExists = File.Exists(unshippedFile); + + if (shippedFileExists ^ unshippedFileExists) + { + var existingFile = shippedFileExists ? shippedFile : unshippedFile; + var nonExistingFile = shippedFileExists ? unshippedFile : shippedFile; + await Console.Error.WriteLineAsync($"Expected both '{shippedFile}' and '{unshippedFile}' to exist or not exist, but '{existingFile}' exists and '{nonExistingFile}' does not exist.").ConfigureAwait(false); + return false; + } + + if (shippedFileExists) { sawShippedFile = true; @@ -717,14 +729,24 @@ async Task createGlobalConfigFilesAsync() try { + // Read shipped file using var fileStream = File.OpenRead(shippedFile); var sourceText = SourceText.From(fileStream); var releaseTrackingData = ReleaseTrackingHelper.ReadReleaseTrackingData(shippedFile, sourceText, - onDuplicateEntryInRelease: (_1, _2, _3, _4, line) => throw new Exception($"Duplicate entry in {shippedFile} at {line.LineNumber}: '{line}'"), - onInvalidEntry: (line, _2, _3, _4) => throw new Exception($"Invalid entry in {shippedFile} at {line.LineNumber}: '{line}'"), + onDuplicateEntryInRelease: (_1, _2, _3, _4, line) => throw new InvalidOperationException($"Duplicate entry in {shippedFile} at {line.LineNumber}: '{line}'"), + onInvalidEntry: (line, _2, _3, _4) => throw new InvalidOperationException($"Invalid entry in {shippedFile} at {line.LineNumber}: '{line}'"), isShippedFile: true); - shippedFilesDataBuilder.Add(releaseTrackingData); + releaseTrackingFilesDataBuilder.Add(releaseTrackingData); versionsBuilder.AddRange(releaseTrackingData.Versions); + + // Read unshipped file + using var fileStreamUnshipped = File.OpenRead(unshippedFile); + var sourceTextUnshipped = SourceText.From(fileStreamUnshipped); + var releaseTrackingDataUnshipped = ReleaseTrackingHelper.ReadReleaseTrackingData(unshippedFile, sourceTextUnshipped, + onDuplicateEntryInRelease: (_1, _2, _3, _4, line) => throw new InvalidOperationException($"Duplicate entry in {unshippedFile} at {line.LineNumber}: '{line}'"), + onInvalidEntry: (line, _2, _3, _4) => throw new InvalidOperationException($"Invalid entry in {unshippedFile} at {line.LineNumber}: '{line}'"), + isShippedFile: false); + releaseTrackingFilesDataBuilder.Add(releaseTrackingDataUnshipped); } #pragma warning disable CA1031 // Do not catch general exception types catch (Exception ex) @@ -744,33 +766,49 @@ async Task createGlobalConfigFilesAsync() if (versionsBuilder.Count > 0) { - var shippedFilesData = shippedFilesDataBuilder.ToImmutable(); + var releaseTrackingData = releaseTrackingFilesDataBuilder.ToImmutableArray(); - // Generate global analyzer config files for each shipped version, if required. + // Generate global analyzer config files for each shipped version. foreach (var version in versionsBuilder) { - var analysisLevelVersionString = GetNormalizedVersionStringForEditorconfigFileNameSuffix(version); - - foreach (var analysisMode in Enum.GetValues(typeof(AnalysisMode))) - { - CreateGlobalConfig(version, analysisLevelVersionString, (AnalysisMode)analysisMode!, shippedFilesData, category: null); - foreach (var category in categories) - { - CreateGlobalConfig(version, analysisLevelVersionString, (AnalysisMode)analysisMode!, shippedFilesData, category); - } - } + CreateGlobalConfigsForVersion(version, isShippedVersion: true, releaseTrackingData); } + + // Generate global analyzer config files for unshipped version. + // See https://github.com/dotnet/roslyn-analyzers/issues/6247 for details. + + // Use 'unshippedVersion = maxShippedVersion + 1' for unshipped data. + var maxShippedVersion = versionsBuilder.Max(); + var unshippedVersion = new Version(maxShippedVersion!.Major + 1, maxShippedVersion.Minor); + CreateGlobalConfigsForVersion(unshippedVersion, isShippedVersion: false, releaseTrackingData); } return true; // Local functions. + void CreateGlobalConfigsForVersion( + Version version, + bool isShippedVersion, + ImmutableArray releaseTrackingData) + { + var analysisLevelVersionString = GetNormalizedVersionStringForEditorconfigFileNameSuffix(version); + + foreach (var analysisMode in Enum.GetValues(typeof(AnalysisMode))) + { + CreateGlobalConfig(version, isShippedVersion, analysisLevelVersionString, (AnalysisMode)analysisMode!, releaseTrackingData, category: null); + foreach (var category in categories!) + { + CreateGlobalConfig(version, isShippedVersion, analysisLevelVersionString, (AnalysisMode)analysisMode!, releaseTrackingData, category); + } + } + } void CreateGlobalConfig( Version version, + bool isShippedVersion, string analysisLevelVersionString, AnalysisMode analysisMode, - ImmutableArray shippedFilesData, + ImmutableArray releaseTrackingData, string? category) { var analysisLevelPropName = "AnalysisLevel"; @@ -793,7 +831,7 @@ void CreateGlobalConfig( analysisMode, category, allRulesById, - (shippedFilesData, version)); + (releaseTrackingData, version, isShippedVersion)); } static string GetNormalizedVersionStringForEditorconfigFileNameSuffix(Version version) @@ -1125,7 +1163,7 @@ private static void CreateGlobalconfig( AnalysisMode analysisMode, string? category, SortedList sortedRulesById, - (ImmutableArray shippedFiles, Version version) shippedReleaseData) + (ImmutableArray releaseTrackingData, Version version, bool isShippedVersion) releaseTrackingDataAndVersion) { Debug.Assert(editorconfigFileName.EndsWith(".editorconfig", StringComparison.Ordinal)); @@ -1135,7 +1173,7 @@ private static void CreateGlobalconfig( analysisMode, category, sortedRulesById, - shippedReleaseData); + releaseTrackingDataAndVersion); var directory = Directory.CreateDirectory(folder); var editorconfigFilePath = Path.Combine(directory.FullName, editorconfigFileName.ToLowerInvariant()); File.WriteAllText(editorconfigFilePath, text); @@ -1148,7 +1186,7 @@ static string GetGlobalconfigText( AnalysisMode analysisMode, string? category, SortedList sortedRulesById, - (ImmutableArray shippedFiles, Version version)? shippedReleaseData) + (ImmutableArray releaseTrackingData, Version version, bool isShippedVersion)? releaseTrackingDataAndVersion) { var result = new StringBuilder(); StartGlobalconfig(); @@ -1257,14 +1295,16 @@ bool AddRule(DiagnosticDescriptor rule, string? category) effectiveSeverity = DiagnosticSeverity.Warning; } - if (shippedReleaseData != null) + if (releaseTrackingDataAndVersion != null) { isEnabledByDefault = isEnabledRuleForNonDefaultAnalysisMode; - var maxVersion = shippedReleaseData.Value.version; + var maxVersion = releaseTrackingDataAndVersion.Value.isShippedVersion ? + releaseTrackingDataAndVersion.Value.version : + ReleaseTrackingHelper.UnshippedVersion; var foundReleaseTrackingEntry = false; - foreach (var shippedFile in shippedReleaseData.Value.shippedFiles) + foreach (var releaseTrackingData in releaseTrackingDataAndVersion.Value.releaseTrackingData) { - if (shippedFile.TryGetLatestReleaseTrackingLine(rule.Id, maxVersion, out _, out var releaseTrackingLine)) + if (releaseTrackingData.TryGetLatestReleaseTrackingLine(rule.Id, maxVersion, out _, out var releaseTrackingLine)) { foundReleaseTrackingEntry = true;