internal static ReleaseTrackingData ReadReleaseTrackingData( string path, SourceText sourceText, Action <string, Version, string, SourceText, TextLine> onDuplicateEntryInRelease, Action <TextLine, InvalidEntryKind, string, SourceText> onInvalidEntry, bool isShippedFile) { var releaseTrackingDataByRulesBuilder = new Dictionary <string, ReleaseTrackingDataForRuleBuilder>(); var currentVersion = UnshippedVersion; ReleaseTrackingHeaderKind? expectedHeaderKind = isShippedFile ? ReleaseTrackingHeaderKind.ReleaseHeader : ReleaseTrackingHeaderKind.TableHeaderTitle; ReleaseTrackingRuleEntryKind?currentRuleEntryKind = null; using var versionsBuilder = PooledHashSet <Version> .GetInstance(); foreach (TextLine line in sourceText.Lines) { string lineText = line.ToString().Trim(); if (string.IsNullOrWhiteSpace(lineText) || lineText.StartsWith(";", StringComparison.Ordinal)) { // Skip blank and comment lines. continue; } // Parse release header if applicable. switch (expectedHeaderKind) { case ReleaseTrackingHeaderKind.ReleaseHeader: // Parse new release, if any. if (lineText.StartsWith(ReleasePrefix, StringComparison.OrdinalIgnoreCase)) { // Expect new table after this line. expectedHeaderKind = ReleaseTrackingHeaderKind.TableHeaderTitle; // Parse the release version. string versionString = lineText.Substring(ReleasePrefix.Length).Trim(); if (!Version.TryParse(versionString, out var version)) { OnInvalidEntry(line, InvalidEntryKind.Header); return(ReleaseTrackingData.Default); } else { currentVersion = version; versionsBuilder.Add(version); } continue; } OnInvalidEntry(line, InvalidEntryKind.Header); return(ReleaseTrackingData.Default); case ReleaseTrackingHeaderKind.TableHeaderTitle: if (lineText.StartsWith(TableTitleNewRules, StringComparison.OrdinalIgnoreCase)) { expectedHeaderKind = ReleaseTrackingHeaderKind.TableHeaderNewOrRemovedRulesLine1; currentRuleEntryKind = ReleaseTrackingRuleEntryKind.New; } else if (lineText.StartsWith(TableTitleRemovedRules, StringComparison.OrdinalIgnoreCase)) { expectedHeaderKind = ReleaseTrackingHeaderKind.TableHeaderNewOrRemovedRulesLine1; currentRuleEntryKind = ReleaseTrackingRuleEntryKind.Removed; } else if (lineText.StartsWith(TableTitleChangedRules, StringComparison.OrdinalIgnoreCase)) { expectedHeaderKind = ReleaseTrackingHeaderKind.TableHeaderChangedRulesLine1; currentRuleEntryKind = ReleaseTrackingRuleEntryKind.Changed; } else { OnInvalidEntry(line, InvalidEntryKind.Header); return(ReleaseTrackingData.Default); } continue; case ReleaseTrackingHeaderKind.TableHeaderNewOrRemovedRulesLine1: if (lineText.StartsWith(TableHeaderNewOrRemovedRulesLine1, StringComparison.OrdinalIgnoreCase)) { expectedHeaderKind = ReleaseTrackingHeaderKind.TableHeaderNewOrRemovedRulesLine2; continue; } OnInvalidEntry(line, InvalidEntryKind.Header); return(ReleaseTrackingData.Default); case ReleaseTrackingHeaderKind.TableHeaderNewOrRemovedRulesLine2: expectedHeaderKind = null; if (lineText.StartsWith(TableHeaderNewOrRemovedRulesLine2, StringComparison.OrdinalIgnoreCase)) { continue; } OnInvalidEntry(line, InvalidEntryKind.Header); return(ReleaseTrackingData.Default); case ReleaseTrackingHeaderKind.TableHeaderChangedRulesLine1: if (lineText.StartsWith(TableHeaderChangedRulesLine1, StringComparison.OrdinalIgnoreCase)) { expectedHeaderKind = ReleaseTrackingHeaderKind.TableHeaderChangedRulesLine2; continue; } OnInvalidEntry(line, InvalidEntryKind.Header); return(ReleaseTrackingData.Default); case ReleaseTrackingHeaderKind.TableHeaderChangedRulesLine2: expectedHeaderKind = null; if (lineText.StartsWith(TableHeaderChangedRulesLine2, StringComparison.OrdinalIgnoreCase)) { continue; } OnInvalidEntry(line, InvalidEntryKind.Header); return(ReleaseTrackingData.Default); default: // We might be starting a new release or table. if (lineText.StartsWith("## ", StringComparison.OrdinalIgnoreCase)) { goto case ReleaseTrackingHeaderKind.ReleaseHeader; } else if (lineText.StartsWith("### ", StringComparison.OrdinalIgnoreCase)) { goto case ReleaseTrackingHeaderKind.TableHeaderTitle; } break; } RoslynDebug.Assert(currentRuleEntryKind != null); var parts = lineText.Split('|').Select(s => s.Trim()).ToArray(); if (IsInvalidEntry(parts, currentRuleEntryKind.Value)) { // Report invalid entry, but continue parsing remaining entries. OnInvalidEntry(line, InvalidEntryKind.Other); continue; } // New or Removed rule entry: // "Rule ID | Category | Severity | Notes" // " 0 | 1 | 2 | 3 " // // Changed rule entry: // "Rule ID | New Category | New Severity | Old Category | Old Severity | Notes" // " 0 | 1 | 2 | 3 | 4 | 5 " string ruleId = parts[0]; InvalidEntryKind?invalidEntryKind = TryParseFields(parts, categoryIndex: 1, severityIndex: 2, out var category, out var defaultSeverity, out var enabledByDefault); if (invalidEntryKind.HasValue) { OnInvalidEntry(line, invalidEntryKind.Value); } ReleaseTrackingLine releaseTrackingLine; if (currentRuleEntryKind.Value == ReleaseTrackingRuleEntryKind.Changed) { invalidEntryKind = TryParseFields(parts, categoryIndex: 3, severityIndex: 4, out var oldCategory, out var oldDefaultSeverity, out var oldEnabledByDefault); if (invalidEntryKind.HasValue) { OnInvalidEntry(line, invalidEntryKind.Value); } // Verify at least one field is changed for the entry: if (string.Equals(category, oldCategory, StringComparison.OrdinalIgnoreCase) && defaultSeverity == oldDefaultSeverity && enabledByDefault == oldEnabledByDefault) { OnInvalidEntry(line, InvalidEntryKind.Other); return(ReleaseTrackingData.Default); } releaseTrackingLine = new ChangedRuleReleaseTrackingLine(ruleId, category, enabledByDefault, defaultSeverity, oldCategory, oldEnabledByDefault, oldDefaultSeverity, line.Span, sourceText, path, isShippedFile); } else { releaseTrackingLine = new NewOrRemovedRuleReleaseTrackingLine(ruleId, category, enabledByDefault, defaultSeverity, line.Span, sourceText, path, isShippedFile, currentRuleEntryKind.Value); } if (!releaseTrackingDataByRulesBuilder.TryGetValue(ruleId, out var releaseTrackingDataForRuleBuilder)) { releaseTrackingDataForRuleBuilder = new ReleaseTrackingDataForRuleBuilder(); releaseTrackingDataByRulesBuilder.Add(ruleId, releaseTrackingDataForRuleBuilder); } releaseTrackingDataForRuleBuilder.AddEntry(currentVersion, releaseTrackingLine, out var hasExistingEntry); if (hasExistingEntry) { onDuplicateEntryInRelease(ruleId, currentVersion, path, sourceText, line); } } var builder = ImmutableSortedDictionary.CreateBuilder <string, ReleaseTrackingDataForRule>(); foreach (var(ruleId, value) in releaseTrackingDataByRulesBuilder) { var releaseTrackingDataForRule = new ReleaseTrackingDataForRule(ruleId, value); builder.Add(ruleId, releaseTrackingDataForRule); } return(new ReleaseTrackingData(builder.ToImmutable(), versionsBuilder.ToImmutable())); // Local functions void OnInvalidEntry(TextLine line, InvalidEntryKind invalidEntryKind) => onInvalidEntry(line, invalidEntryKind, path, sourceText);
private static ReleaseTrackingData ReadReleaseTrackingData( string path, SourceText sourceText, Action <Diagnostic> addInvalidFileDiagnostic, bool isShippedFile) { var releaseTrackingDataByRulesBuilder = new Dictionary <string, ReleaseTrackingDataForRuleBuilder>(); var currentVersion = s_unshippedVersion; using var reportedInvalidLines = PooledHashSet <TextLine> .GetInstance(); ReleaseTrackingHeaderKind? expectedHeaderKind = isShippedFile ? ReleaseTrackingHeaderKind.ReleaseHeader : ReleaseTrackingHeaderKind.TableHeaderTitle; ReleaseTrackingRuleEntryKind?currentRuleEntryKind = null; foreach (TextLine line in sourceText.Lines) { string lineText = line.ToString().Trim(); if (string.IsNullOrWhiteSpace(lineText) || lineText.StartsWith(";", StringComparison.Ordinal)) { // Skip blank and comment lines. continue; } // Parse release header if applicable. switch (expectedHeaderKind) { case ReleaseTrackingHeaderKind.ReleaseHeader: // Parse new release, if any. if (lineText.StartsWith(ReleasePrefix, StringComparison.OrdinalIgnoreCase)) { // Expect new table after this line. expectedHeaderKind = ReleaseTrackingHeaderKind.TableHeaderTitle; // Parse the release version. string versionString = lineText.Substring(ReleasePrefix.Length).Trim(); if (!Version.TryParse(versionString, out var version)) { ReportInvalidEntryDiagnostic(line, InvalidEntryKind.Header); return(ReleaseTrackingData.Default); } else { currentVersion = version; } continue; } ReportInvalidEntryDiagnostic(line, InvalidEntryKind.Header); return(ReleaseTrackingData.Default); case ReleaseTrackingHeaderKind.TableHeaderTitle: if (lineText.StartsWith(TableTitleNewRules, StringComparison.OrdinalIgnoreCase)) { expectedHeaderKind = ReleaseTrackingHeaderKind.TableHeaderNewOrRemovedRulesLine1; currentRuleEntryKind = ReleaseTrackingRuleEntryKind.New; } else if (lineText.StartsWith(TableTitleRemovedRules, StringComparison.OrdinalIgnoreCase)) { expectedHeaderKind = ReleaseTrackingHeaderKind.TableHeaderNewOrRemovedRulesLine1; currentRuleEntryKind = ReleaseTrackingRuleEntryKind.Removed; } else if (lineText.StartsWith(TableTitleChangedRules, StringComparison.OrdinalIgnoreCase)) { expectedHeaderKind = ReleaseTrackingHeaderKind.TableHeaderChangedRulesLine1; currentRuleEntryKind = ReleaseTrackingRuleEntryKind.Changed; } else { ReportInvalidEntryDiagnostic(line, InvalidEntryKind.Header); return(ReleaseTrackingData.Default); } continue; case ReleaseTrackingHeaderKind.TableHeaderNewOrRemovedRulesLine1: if (lineText.StartsWith(TableHeaderNewOrRemovedRulesLine1, StringComparison.OrdinalIgnoreCase)) { expectedHeaderKind = ReleaseTrackingHeaderKind.TableHeaderNewOrRemovedRulesLine2; continue; } ReportInvalidEntryDiagnostic(line, InvalidEntryKind.Header); return(ReleaseTrackingData.Default); case ReleaseTrackingHeaderKind.TableHeaderNewOrRemovedRulesLine2: expectedHeaderKind = null; if (lineText.StartsWith(TableHeaderNewOrRemovedRulesLine2, StringComparison.OrdinalIgnoreCase)) { continue; } ReportInvalidEntryDiagnostic(line, InvalidEntryKind.Header); return(ReleaseTrackingData.Default); case ReleaseTrackingHeaderKind.TableHeaderChangedRulesLine1: if (lineText.StartsWith(TableHeaderChangedRulesLine1, StringComparison.OrdinalIgnoreCase)) { expectedHeaderKind = ReleaseTrackingHeaderKind.TableHeaderChangedRulesLine2; continue; } ReportInvalidEntryDiagnostic(line, InvalidEntryKind.Header); return(ReleaseTrackingData.Default); case ReleaseTrackingHeaderKind.TableHeaderChangedRulesLine2: expectedHeaderKind = null; if (lineText.StartsWith(TableHeaderChangedRulesLine2, StringComparison.OrdinalIgnoreCase)) { continue; } ReportInvalidEntryDiagnostic(line, InvalidEntryKind.Header); return(ReleaseTrackingData.Default); default: // We might be starting a new release or table. if (lineText.StartsWith("## ", StringComparison.OrdinalIgnoreCase)) { goto case ReleaseTrackingHeaderKind.ReleaseHeader; } else if (lineText.StartsWith("### ", StringComparison.OrdinalIgnoreCase)) { goto case ReleaseTrackingHeaderKind.TableHeaderTitle; } break; } RoslynDebug.Assert(currentRuleEntryKind != null); var parts = lineText.Split('|').Select(s => s.Trim()).ToArray(); if (IsInvalidEntry(parts, currentRuleEntryKind.Value)) { // Report invalid entry, but continue parsing remaining entries. ReportInvalidEntryDiagnostic(line, InvalidEntryKind.Other); continue; } // New or Removed rule entry: // "Rule ID | Category | Severity | Notes" // " 0 | 1 | 2 | 3 " // // Changed rule entry: // "Rule ID | New Category | New Severity | Old Category | Old Severity | Notes" // " 0 | 1 | 2 | 3 | 4 | 5 " string ruleId = parts[0]; InvalidEntryKind?invalidEntryKind = TryParseFields(parts, categoryIndex: 1, severityIndex: 2, out var category, out var defaultSeverity, out var enabledByDefault); if (invalidEntryKind.HasValue) { ReportInvalidEntryDiagnostic(line, invalidEntryKind.Value); } ReleaseTrackingLine releaseTrackingLine; if (currentRuleEntryKind.Value == ReleaseTrackingRuleEntryKind.Changed) { invalidEntryKind = TryParseFields(parts, categoryIndex: 3, severityIndex: 4, out var oldCategory, out var oldDefaultSeverity, out var oldEnabledByDefault); if (invalidEntryKind.HasValue) { ReportInvalidEntryDiagnostic(line, invalidEntryKind.Value); } // Verify at least one field is changed for the entry: if (string.Equals(category, oldCategory, StringComparison.OrdinalIgnoreCase) && defaultSeverity == oldDefaultSeverity && enabledByDefault == oldEnabledByDefault) { ReportInvalidEntryDiagnostic(line, InvalidEntryKind.Other); return(ReleaseTrackingData.Default); } releaseTrackingLine = new ChangedRuleReleaseTrackingLine(ruleId, category, enabledByDefault, defaultSeverity, oldCategory, oldEnabledByDefault, oldDefaultSeverity, line.Span, sourceText, path, isShippedFile); } else { releaseTrackingLine = new NewOrRemovedRuleReleaseTrackingLine(ruleId, category, enabledByDefault, defaultSeverity, line.Span, sourceText, path, isShippedFile, currentRuleEntryKind.Value); } if (!releaseTrackingDataByRulesBuilder.TryGetValue(ruleId, out var releaseTrackingDataForRuleBuilder)) { releaseTrackingDataForRuleBuilder = new ReleaseTrackingDataForRuleBuilder(); releaseTrackingDataByRulesBuilder.Add(ruleId, releaseTrackingDataForRuleBuilder); } releaseTrackingDataForRuleBuilder.AddEntry(currentVersion, releaseTrackingLine, out var hasExistingEntry); if (hasExistingEntry && reportedInvalidLines.Add(line)) { // Rule '{0}' has more then one entry for release '{1}' in analyzer release file '{2}'. string arg1 = ruleId; string arg2 = currentVersion == s_unshippedVersion ? "unshipped" : currentVersion.ToString(); string arg3 = Path.GetFileName(path); LinePositionSpan linePositionSpan = sourceText.Lines.GetLinePositionSpan(line.Span); Location location = Location.Create(path, line.Span, linePositionSpan); var diagnostic = Diagnostic.Create(RemoveDuplicateEntriesForAnalyzerReleaseRule, location, arg1, arg2, arg3); addInvalidFileDiagnostic(diagnostic); } } var builder = ImmutableSortedDictionary.CreateBuilder <string, ReleaseTrackingDataForRule>(); foreach (var(ruleId, value) in releaseTrackingDataByRulesBuilder) { var releaseTrackingDataForRule = new ReleaseTrackingDataForRule(ruleId, value); builder.Add(ruleId, releaseTrackingDataForRule); } return(new ReleaseTrackingData(builder.ToImmutable())); // Local functions void ReportInvalidEntryDiagnostic(TextLine line, InvalidEntryKind invalidEntryKind) { if (!reportedInvalidLines.Add(line)) { // Already reported. return; } var rule = invalidEntryKind switch { // Analyzer release file '{0}' has a missing or invalid release header '{1}'. InvalidEntryKind.Header => InvalidHeaderInAnalyzerReleasesFileRule, // Analyzer release file '{0}' has an entry with one or more 'Undetected' fields that need to be manually filled in '{1}'. InvalidEntryKind.UndetectedField => InvalidUndetectedEntryInAnalyzerReleasesFileRule, // Analyzer release file '{0}' has an invalid entry '{1}'. InvalidEntryKind.Other => InvalidEntryInAnalyzerReleasesFileRule, _ => throw new NotImplementedException(), }; string arg1 = Path.GetFileName(path); string arg2 = line.ToString(); LinePositionSpan linePositionSpan = sourceText.Lines.GetLinePositionSpan(line.Span); Location location = Location.Create(path, line.Span, linePositionSpan); var diagnostic = Diagnostic.Create(rule, location, arg1, arg2); addInvalidFileDiagnostic(diagnostic); }