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);
 internal ReleaseTrackingDataForRule(string ruleId, ReleaseTrackingDataForRuleBuilder builder)
 {
     RuleId = ruleId;
     ReleasesByVersionMap = builder.ToImmutable();
 }
Exemple #3
0
        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);
            }