public void IdInComments() { BreakingChange bc = new BreakingChange { Id = "144", Title = "Application.FilterMessage no longer throws for re-entrant implementations of IMessageFilter.PreFilterMessage", VersionBroken = Version.Parse("4.6.1"), ImpactScope = BreakingChangeImpact.Edge, SourceAnalyzerStatus = BreakingChangeAnalyzerStatus.Planned, Details = "Prior to the .NET Framework 4.6.1, calling Application.FilterMessage with an IMessageFilter.PreFilterMessage which called AddMessageFilter or RemoveMessageFilter (while also calling Application.DoEvents) would cause an IndexOutOfRangeException." + "\n\n" + "Beginning with applications targeting the .NET Framework 4.6.1, this exception is no longer thrown, and re-entrant filters as described above may be used.", IsQuirked = true, Suggestion = "Be aware that Application.FilterMessage will no longer throw for the re-entrant IMessageFilter.PreFilterMessage behavior described above. This only affects applications targeting the .NET Framework 4.6.1.", Categories = new List <string> { "Windows Forms" }, Link = "https://msdn.microsoft.com/en-us/library/mt620031%28v=vs.110%29.aspx#WinForms", ApplicableApis = new List <string> { "M:System.Windows.Forms.Application.FilterMessage(System.Windows.Forms.Message@)" }, Notes = "It's unclear if this one will be better analyzed by Application.FilterMessage callers (who would have seen the exception previously)" + "\n" + "or the IMessageFilter.PreFilterMessage implementers (who caused the exception previously). Unfortunately, the analyzer on the caller is probably" + "\n" + "more useful, even though it would be easier to be 'precise' if we analyzed the interface implementer." }; ValidateParse(GetBreakingChangeMarkdown("Application.FilterMessage.md"), bc); }
public static void EqualityTests() { var breakingChangeSame1 = new BreakingChange { Id = "id1" }; var breakingChangeSame2 = new BreakingChange { Id = "id1" }; var breakingChangeDifferent1 = new BreakingChange { Id = "ID1" }; var breakingChangeDifferent2 = new BreakingChange { Id = "id2" }; Assert.Equal(breakingChangeSame1.GetHashCode(), breakingChangeSame2.GetHashCode()); Assert.NotEqual(breakingChangeSame1.GetHashCode(), breakingChangeDifferent1.GetHashCode()); Assert.NotEqual(breakingChangeSame2.GetHashCode(), breakingChangeDifferent1.GetHashCode()); Assert.NotEqual(breakingChangeSame1.GetHashCode(), breakingChangeDifferent2.GetHashCode()); Assert.False(breakingChangeSame1.Equals(null)); Assert.False(breakingChangeSame1.Equals(new BreakingChange())); Assert.False(breakingChangeSame1.Equals("other type")); Assert.True(breakingChangeSame1.Equals(breakingChangeSame1)); Assert.True(breakingChangeSame1.Equals(breakingChangeSame2)); Assert.False(breakingChangeSame1.Equals(breakingChangeDifferent1)); Assert.False(breakingChangeSame2.Equals(breakingChangeDifferent1)); Assert.False(breakingChangeSame1.Equals(breakingChangeDifferent2)); }
public void MissingApis() { BreakingChange bc = UriBC.DeepCopy(); bc.ApplicableApis = null; ValidateParse(GetBreakingChangeMarkdown("MissingApis.md"), bc); }
public void BreakingChangeWithComments() { var expected = new BreakingChange { Title = "ASP.NET Accessibility Improvements in .NET 4.7.3", ImpactScope = BreakingChangeImpact.Minor, VersionBroken = Version.Parse("4.7.3"), SourceAnalyzerStatus = BreakingChangeAnalyzerStatus.NotPlanned, IsQuirked = true, IsBuildTime = false, Details = "Starting with the .NET Framework 4.7.1, ASP.NET has improved how ASP.NET Web Controls work with accessibility technology in Visual Studio to better support ASP.NET customers.", Suggestion = @"In order for the Visual Studio Designer to benefit from these changes - Install Visual Studio 2017 15.3 or later, which supports the new accessibility features with the following AppContext Switch by default. ```xml <?xml version=""1.0"" encoding=""utf-8""?> <configuration> <runtime> ... <!-- AppContextSwitchOverrides value attribute is in the form of 'key1=true|false;key2=true|false --> <AppContextSwitchOverrides value=""...;Switch.UseLegacyAccessibilityFeatures=false"" /> ... </runtime> </configuration> ```".Replace(Environment.NewLine, "\n") }; ValidateParse(GetBreakingChangeMarkdown("CommentsInRecommendedChanges.md"), expected); }
private void TestEquality(BreakingChange expected, BreakingChange actual) { if (expected == null) { Assert.Null(actual); return; } else { Assert.NotNull(actual); } Assert.Equal(expected.Id, actual.Id, StringComparer.Ordinal); Assert.Equal(expected.Title, actual.Title, StringComparer.Ordinal); Assert.Equal(expected.Details, actual.Details, StringComparer.Ordinal); Assert.Equal(expected.Suggestion, actual.Suggestion, StringComparer.Ordinal); Assert.Equal(expected.Link, actual.Link, StringComparer.Ordinal); Assert.Equal(expected.BugLink, actual.BugLink, StringComparer.Ordinal); Assert.Equal(expected.Notes, actual.Notes, StringComparer.Ordinal); Assert.Equal(expected.Markdown, actual.Markdown, StringComparer.Ordinal); Assert.Equal(expected.Related, actual.Related, StringComparer.Ordinal); Assert.Equal(expected.ApplicableApis, actual.ApplicableApis, StringComparer.Ordinal); Assert.Equal(expected.VersionBroken, actual.VersionBroken); Assert.Equal(expected.VersionFixed, actual.VersionFixed); Assert.Equal(expected.IsBuildTime, actual.IsBuildTime); Assert.Equal(expected.IsQuirked, actual.IsQuirked); Assert.Equal(expected.SourceAnalyzerStatus, actual.SourceAnalyzerStatus); Assert.Equal(expected.ImpactScope, actual.ImpactScope); }
public void MissingData() { BreakingChange bc = ListTBC.DeepCopy(); bc.ImpactScope = BreakingChangeImpact.Unknown; bc.VersionBroken = null; bc.ApplicableApis = null; ValidateParse(GetBreakingChangeMarkdown("MissingData.md"), bc); }
public void RandomText() { BreakingChange bc = new BreakingChange { Title = "Chapter 2. The Mail" }; ValidateParse(GetBreakingChangeMarkdown("RandomText.md"), bc); ValidateParse(GetBreakingChangeMarkdown("RandomText2.md"), new BreakingChange[0]); }
public void CorruptData() { BreakingChange bc = UriBC.DeepCopy(); bc.VersionBroken = null; bc.ImpactScope = BreakingChangeImpact.Unknown; bc.IsQuirked = false; bc.ApplicableApis = bc.ApplicableApis.Concat(new[] { "##" }); bc.Suggestion = "\\0\0\0\0\0" + bc.Suggestion + "\u0001\u0002"; ValidateParse(GetBreakingChangeMarkdown("CorruptData.md"), bc); }
public void CategoryWithSpace() { var expected = new BreakingChange { Title = "List<T>.ForEach", ImpactScope = BreakingChangeImpact.Minor, VersionBroken = Version.Parse("4.6.2"), SourceAnalyzerStatus = BreakingChangeAnalyzerStatus.Available }; ValidateParse(GetBreakingChangeMarkdown("CategoryWithSpaces.md"), expected); }
public void DuplicateSections() { BreakingChange bc = UriBC.DeepCopy(); bc.VersionFixed = new Version(1, 0); bc.Id = ListTBC.Id; bc.Title = ListTBC.Title; bc.Details = ListTBC.Details + "\n\n\n" + UriBC.Details; bc.Suggestion = ListTBC.Suggestion + "\n\n" + UriBC.Suggestion; bc.ApplicableApis = ListTBC.ApplicableApis.Concat(UriBC.ApplicableApis).ToList(); ValidateParse(GetBreakingChangeMarkdown("DupSections.md"), bc); }
public void PartialData() { BreakingChange bc = new BreakingChange { Id = ListTBC.Id, Title = ListTBC.Title, ImpactScope = ListTBC.ImpactScope, VersionBroken = ListTBC.VersionBroken, Details = ListTBC.Details }; ValidateParse(GetBreakingChangeMarkdown("PartialData.md"), bc); }
private static IApiRecommendations GenerateTestRecommendationsWithoutFixedEntry() { var recommendations = Substitute.For <IApiRecommendations>(); var breakingChange1 = new BreakingChange { ApplicableApis = new[] { TestDocId1 }, Id = "5", VersionBroken = Version.Parse("4.5") }; recommendations.GetBreakingChanges(TestDocId1).Returns(new[] { breakingChange1 }); return(recommendations); }
/// <summary> /// This method generates breaking changes recomendations for testing the ShowRetargettingIssues /// </summary> /// <param name="numOfRetargettingIssues">Number of retargetting issues to put in the recommendations result</param> /// <param name="numOfRuntimeIssues">Number of runtime issues to put in the recommendations result</param> /// <returns>API recommendations result containing the number of issues to be included</returns> private static IApiRecommendations GenerateTestRecommendationsForShowRetargetting(int numOfRetargettingIssues = 1, int numOfRuntimeIssues = 1) { int lastIDUsed = 1; var recommendations = Substitute.For <IApiRecommendations>(); List <BreakingChange> breakingChanges = new List <BreakingChange>(); //add requested number of retargetting issues for (int i = 0; i < numOfRetargettingIssues; i++) { //add a new breaking change BreakingChange bc = new BreakingChange { ApplicableApis = new[] { TestDocId1 }, Id = lastIDUsed.ToString(), VersionBroken = Version.Parse("4.5"), IsQuirked = true }; breakingChanges.Add(bc); lastIDUsed++; } //add requested number of runtime issues for (int i = 0; i < numOfRuntimeIssues; i++) { //add a new breaking change BreakingChange bc = new BreakingChange { ApplicableApis = new[] { TestDocId1 }, Id = lastIDUsed.ToString(), VersionBroken = Version.Parse("4.5"), IsQuirked = false, IsBuildTime = false }; breakingChanges.Add(bc); lastIDUsed++; } recommendations.GetBreakingChanges(TestDocId1).Returns(breakingChanges.ToArray()); return(recommendations); }
public void Compare_Detects_MethodParametersBeingAdded_as_removal() { // Arrange var v1ApiListing = CreateApiListingDocument(V1Assembly); var v2ApiListing = CreateApiListingDocument(V2Assembly); var comparer = new ApiListingComparer(v1ApiListing, v2ApiListing); // Act var breakingChanges = comparer.GetDifferences(); // Assert var expected = new BreakingChange( "public class ComparisonScenarios.ClassWithMethods", "public System.Void MethodToAddParameters()", kind: ChangeKind.Removal); Assert.Contains(expected, breakingChanges); }
public void Compare_Detects_GenericTypeConstraintsBeingAdded_as_removal() { // Arrange var v1ApiListing = CreateApiListingDocument(V1Assembly); var v2ApiListing = CreateApiListingDocument(V2Assembly); var comparer = new ApiListingComparer(v1ApiListing, v2ApiListing); // Act var breakingChanges = comparer.GetDifferences(); // Assert var expected = new BreakingChange( "public class ComparisonScenarios.GenericTypeWithConstraintsToBeAdded<T0>", memberId: null, kind: ChangeKind.Removal); Assert.Contains(expected, breakingChanges); }
public void Compare_Detects_ChangesInTypeVisibility_as_removal() { // Arrange var v1ApiListing = CreateApiListingDocument(V1Assembly); var v2ApiListing = CreateApiListingDocument(V2Assembly); var comparer = new ApiListingComparer(v1ApiListing, v2ApiListing); // Act var breakingChanges = comparer.GetDifferences(); // Assert var expected = new BreakingChange( "public class ComparisonScenarios.PublicToInternalClass", memberId: null, kind: ChangeKind.Removal); Assert.Contains(expected, breakingChanges); }
public void Compare_Detects_TypeRenames_as_removal() { // Arrange var v1ApiListing = CreateApiListingDocument(V1Assembly); var v2ApiListing = CreateApiListingDocument(V2Assembly); var comparer = new ApiListingComparer(v1ApiListing, v2ApiListing); // Act var breakingChanges = comparer.GetDifferences(); // Assert var expected = new BreakingChange( "public interface ComparisonScenarios.TypeToRename", memberId: null, kind: ChangeKind.Removal); Assert.Contains(expected, breakingChanges); }
public void Compare_Detects_ClassBeingUnnested_as_removal() { // Arrange var v1ApiListing = CreateApiListingDocument(V1Assembly); var v2ApiListing = CreateApiListingDocument(V2Assembly); var comparer = new ApiListingComparer(v1ApiListing, v2ApiListing); // Act var breakingChanges = comparer.GetDifferences(); // Assert var expected = new BreakingChange( "public class ComparisonScenarios.ClassToUnnestContainer+ClassToUnnest", memberId: null, kind: ChangeKind.Removal); Assert.Contains(expected, breakingChanges); }
public void EqualityTests() { var breakingChangeSame1 = new BreakingChange { Id = "id1" }; var breakingChangeSame2 = new BreakingChange { Id = "id1" }; var breakingChangeDifferent1 = new BreakingChange { Id = "ID1" }; var breakingChangeDifferent2 = new BreakingChange { Id = "id2" }; Assert.Equal(breakingChangeSame1.GetHashCode(), breakingChangeSame2.GetHashCode()); Assert.NotEqual(breakingChangeSame1.GetHashCode(), breakingChangeDifferent1.GetHashCode()); Assert.NotEqual(breakingChangeSame2.GetHashCode(), breakingChangeDifferent1.GetHashCode()); Assert.NotEqual(breakingChangeSame1.GetHashCode(), breakingChangeDifferent2.GetHashCode()); Assert.False(breakingChangeSame1.Equals(null)); Assert.False(breakingChangeSame1.Equals(new BreakingChange())); Assert.False(breakingChangeSame1.Equals("other type")); Assert.True(breakingChangeSame1.Equals(breakingChangeSame1)); Assert.True(breakingChangeSame1.Equals(breakingChangeSame2)); Assert.False(breakingChangeSame1.Equals(breakingChangeDifferent1)); Assert.False(breakingChangeSame2.Equals(breakingChangeDifferent1)); Assert.False(breakingChangeSame1.Equals(breakingChangeDifferent2)); }
public void Compare_DetectsChangesInForwardedType() { // Arrange var v1ApiListing = CreateApiListingDocument(V1Assembly); var v2ApiListing = CreateApiListingDocument(V2Assembly); var comparer = new ApiListingComparer(v1ApiListing, v2ApiListing); var typeToCheck = "public class ComparisonScenarios.TypeToBeForwardedAndChanged"; var getterRemoval = new BreakingChange( typeToCheck, "public System.String get_PropertyToBeRemoved()", ChangeKind.Removal); var setterRemoval = new BreakingChange( typeToCheck, "public System.Void set_PropertyToBeRemoved(System.String value)", ChangeKind.Removal); // Act var breakingChanges = comparer.GetDifferences(); // Assert Assert.Equal(2, breakingChanges.Count(bc => bc.TypeId == typeToCheck)); Assert.Contains(getterRemoval, breakingChanges); Assert.Contains(setterRemoval, breakingChanges); }
private static void CleanAndAddBreak(List<BreakingChange> breakingChanges, BreakingChange currentBreak) { // Clean up trailing white-space, etc. from long-form text entries if (currentBreak.Details != null) currentBreak.Details = currentBreak.Details.Trim(); if (currentBreak.Suggestion != null) currentBreak.Suggestion = currentBreak.Suggestion.Trim(); if (currentBreak.Notes != null) currentBreak.Notes = currentBreak.Notes.Trim(); breakingChanges.Add(currentBreak); }
private static void ParseNonStateChange(BreakingChange currentBreak, ParseState state, string currentLine) { switch (state) { case ParseState.None: return; case ParseState.OriginalBug: currentBreak.BugLink = currentLine.Trim(); break; case ParseState.Scope: BreakingChangeImpact scope; if (Enum.TryParse<BreakingChangeImpact>(currentLine.Trim(), out scope)) { currentBreak.ImpactScope = scope; } break; case ParseState.VersionBroken: Version verBroken; if (Version.TryParse(currentLine.Trim(), out verBroken)) { currentBreak.VersionBroken = verBroken; } break; case ParseState.VersionFixed: Version verFixed; if (Version.TryParse(currentLine.Trim(), out verFixed)) { currentBreak.VersionFixed = verFixed; } break; case ParseState.AffectedAPIs: // Trim md list markers, as well as comment tags (in case the affected APIs section is followed by a comment) string api = currentLine.Trim().TrimStart('*', '-', ' ', '\t', '<', '!', '-'); if (string.IsNullOrWhiteSpace(api)) return; if (currentBreak.ApplicableApis == null) { currentBreak.ApplicableApis = new List<string>(); } ((List<string>)currentBreak.ApplicableApis).Add(api); break; case ParseState.Details: if (currentBreak.Details == null) { currentBreak.Details = currentLine; } else { currentBreak.Details += ("\n" + currentLine); } break; case ParseState.Suggestion: if (currentBreak.Suggestion == null) { currentBreak.Suggestion = currentLine; } else { currentBreak.Suggestion += ("\n" + currentLine); } break; case ParseState.Notes: // Special-case the fact that 'notes' will often come at the end of a comment section and we don't need the closing --> in the note. if (currentLine.Trim().Equals("-->")) return; if (currentBreak.Notes == null) { currentBreak.Notes = currentLine; } else { currentBreak.Notes += ("\n" + currentLine); } break; default: throw new InvalidOperationException("Unhandled breaking change parse state: " + state.ToString()); } }
private void TestEquality(BreakingChange expected, BreakingChange actual) { if (expected == null) { Assert.Null(actual); return; } else { Assert.NotNull(actual); } Assert.Equal(expected.Id, actual.Id, StringComparer.Ordinal); Assert.Equal(expected.Title, actual.Title, StringComparer.Ordinal); Assert.Equal(expected.Details, actual.Details, StringComparer.Ordinal); Assert.Equal(expected.Suggestion, actual.Suggestion, StringComparer.Ordinal); Assert.Equal(expected.Link, actual.Link, StringComparer.Ordinal); Assert.Equal(expected.BugLink, actual.BugLink, StringComparer.Ordinal); Assert.Equal(expected.Notes, actual.Notes, StringComparer.Ordinal); Assert.Equal(expected.Markdown, actual.Markdown, StringComparer.Ordinal); Assert.Equal(expected.Related, actual.Related, StringComparer.Ordinal); Assert.Equal(expected.ApplicableApis, actual.ApplicableApis, StringComparer.Ordinal); Assert.Equal(expected.VersionBroken, actual.VersionBroken); Assert.Equal(expected.VersionFixed, actual.VersionFixed); Assert.Equal(expected.IsBuildTime, actual.IsBuildTime); Assert.Equal(expected.IsQuirked, actual.IsQuirked); Assert.Equal(expected.IsSourceAnalyzerAvailable, actual.IsSourceAnalyzerAvailable); Assert.Equal(expected.ImpactScope, actual.ImpactScope); }
static void Main(string[] args) { if (args == null || args.Length == 0) { Console.WriteLine("Bad argument."); Console.WriteLine("You need to specify location of breaking change documents."); } string bcpath = args[0]; var bcList = new Dictionary <string, List <BreakingChange> >(); var template = "README-template.md"; string templateText = null; string bcpathREADME = Path.Combine(bcpath, "README.md"); var bcdir = new DirectoryInfo(bcpath); const string versionIntroduced = "### Version Introduced"; foreach (var changeFile in bcdir.GetFiles("*.md")) { if (changeFile.Name == "! Template.md" || changeFile.Name == "README.md" || changeFile.Name == "!categories.md") { continue; } var change = new BreakingChange(); using (var reader = changeFile.OpenText()) { var titleLine = reader.ReadLine(); var title = titleLine.Substring(3); change.Title = title; change.Path = changeFile.Name; string versionLine = null; while ((versionLine = reader.ReadLine()) != null) { if (versionLine != versionIntroduced) { continue; } var version = reader.ReadLine(); change.Version = version; break; } if (!bcList.ContainsKey(change.Version)) { bcList.Add(change.Version, new List <BreakingChange>()); } var versionChanges = bcList[change.Version]; versionChanges.Add(change); } } using (var templateReader = File.OpenText(template)) { templateText = templateReader.ReadToEnd(); } var keysArrayLength = bcList.Keys.Count; var keysArray = new string[keysArrayLength]; bcList.Keys.CopyTo(keysArray, 0); Array.Sort(keysArray); Array.Reverse(keysArray); using (var writer = File.CreateText(bcpathREADME)) { writer.Write(templateText); writer.WriteLine(); foreach (var ver in keysArray) { var hashVersion = new StringBuilder(); foreach (var c in ver) { if (c != '.') { hashVersion.Append(c); } } var hashLink = $"net-framework-{hashVersion.ToString()}"; writer.WriteLine($"- [.NET Framework {ver}](#{hashLink})"); } foreach (var ver in keysArray) { writer.WriteLine(); writer.WriteLine($"## .NET Framework {ver}"); writer.WriteLine(); var breaks = bcList[ver]; breaks.Sort((break1, break2) => break1.Title.CompareTo(break2.Title)); foreach (var b in breaks) { writer.WriteLine($"- [{b.Title}]({b.Path})"); } } writer.WriteLine(); writer.WriteLine("This file was generated by [Breaking Change Readme Generator](https://github.com/Microsoft/dotnet/blob/master/src/bc-readme-gen)."); } }
/// <summary> /// This method generates breaking changes recomendations for testing the ShowRetargettingIssues /// </summary> /// <param name="numOfRetargettingIssues">Number of retargetting issues to put in the recommendations result</param> /// <param name="numOfRuntimeIssues">Number of runtime issues to put in the recommendations result</param> /// <returns>API recommendations result containing the number of issues to be included</returns> private static IApiRecommendations GenerateTestRecommendationsForShowRetargetting(int numOfRetargettingIssues = 1, int numOfRuntimeIssues = 1) { int lastIDUsed = 1; var recommendations = Substitute.For<IApiRecommendations>(); List<BreakingChange> breakingChanges = new List<BreakingChange>(); //add requested number of retargetting issues for (int i = 0; i < numOfRetargettingIssues; i++) { //add a new breaking change BreakingChange bc = new BreakingChange { ApplicableApis = new[] { TestDocId1 }, Id = lastIDUsed.ToString(), VersionBroken = Version.Parse("4.5"), IsQuirked = true }; breakingChanges.Add(bc); lastIDUsed++; } //add requested number of runtime issues for (int i = 0; i < numOfRuntimeIssues; i++) { //add a new breaking change BreakingChange bc = new BreakingChange { ApplicableApis = new[] { TestDocId1 }, Id = lastIDUsed.ToString(), VersionBroken = Version.Parse("4.5"), IsQuirked = false, IsBuildTime = false }; breakingChanges.Add(bc); lastIDUsed++; } recommendations.GetBreakingChanges(TestDocId1).Returns(breakingChanges.ToArray()); return recommendations; }
private static IApiRecommendations GenerateTestRecommendationsWithoutFixedEntry() { var recommendations = Substitute.For<IApiRecommendations>(); var breakingChange1 = new BreakingChange { ApplicableApis = new[] { TestDocId1 }, Id = "5", VersionBroken = Version.Parse("4.5") }; recommendations.GetBreakingChanges(TestDocId1).Returns(new[] { breakingChange1 }); return recommendations; }
private static void ParseNonStateChange(BreakingChange currentBreak, ParseState state, string currentLine, IEnumerable<string> allowedCategories) { switch (state) { case ParseState.None: return; case ParseState.OriginalBug: currentBreak.BugLink = currentLine.Trim(); break; case ParseState.Scope: BreakingChangeImpact scope; if (Enum.TryParse<BreakingChangeImpact>(currentLine.Trim(), out scope)) { currentBreak.ImpactScope = scope; } break; case ParseState.VersionBroken: Version verBroken; if (Version.TryParse(currentLine.Trim(), out verBroken)) { currentBreak.VersionBroken = verBroken; } break; case ParseState.VersionFixed: Version verFixed; if (Version.TryParse(currentLine.Trim(), out verFixed)) { currentBreak.VersionFixed = verFixed; } break; case ParseState.AffectedAPIs: // Trim md list and code markers, as well as comment tags (in case the affected APIs section is followed by a comment) string api = currentLine.Trim().TrimStart('*', '-', '`', ' ', '\t', '<', '!', '-').TrimEnd('`'); if (string.IsNullOrWhiteSpace(api)) return; if (currentBreak.ApplicableApis == null) { currentBreak.ApplicableApis = new List<string>(); } currentBreak.ApplicableApis.Add(api); break; case ParseState.Details: if (currentBreak.Details == null) { currentBreak.Details = currentLine; } else { currentBreak.Details += ("\n" + currentLine); } break; case ParseState.Suggestion: if (currentBreak.Suggestion == null) { currentBreak.Suggestion = currentLine; } else { currentBreak.Suggestion += ("\n" + currentLine); } break; case ParseState.Notes: // Special-case the fact that 'notes' will often come at the end of a comment section and we don't need the closing --> in the note. if (currentLine.Trim().Equals("-->")) return; if (currentBreak.Notes == null) { currentBreak.Notes = currentLine; } else { currentBreak.Notes += ("\n" + currentLine); } break; case ParseState.SourceAnalyzerStatus: BreakingChangeAnalyzerStatus status; if (Enum.TryParse<BreakingChangeAnalyzerStatus>(currentLine.Trim().Replace(" ", ""), true, out status)) { currentBreak.SourceAnalyzerStatus = status; } break; case ParseState.Categories: // Trim md list and code markers, as well as comment tags (in case the categories section is followed by a comment) var category = currentLine.Trim().TrimStart('*', '-', '!', '<', '>'); if (string.IsNullOrWhiteSpace(category)) break; if (currentBreak.Categories == null) { currentBreak.Categories = new List<string>(); } // If a list of allowed categories was provided, make sure that the category found is on the list if (!allowedCategories?.Contains(category, StringComparer.OrdinalIgnoreCase) ?? false) { throw new InvalidOperationException($"Invalid category detected: {category}"); } currentBreak.Categories.Add(category); break; default: throw new InvalidOperationException("Unhandled breaking change parse state: " + state.ToString()); } }
private static bool BreakingChangeIsInVersionRange(IEnumerable <Version> targetVersions, BreakingChange breakingChange) { foreach (var targetVersion in targetVersions) { // Include breaking changes that were broken before a target version and fixed after it, // and also breaking changes that were introduced in a targeted version, _even if they were fixed in that same version_ // // Some breaking changes have VersionBroken==VersionFixed if the break was corrected in GDR-level servicing. We want to report those to // users who are targeting that version so that they understand the importance of updating their NetFX via WU (or whatever // enterprise-specific patch rollout system they have). if (targetVersion == breakingChange.VersionBroken || (targetVersion > breakingChange.VersionBroken && (breakingChange.VersionFixed == null || targetVersion < breakingChange.VersionFixed))) { return(true); } } return(false); }
/// <summary> /// Parses markdown files into BrekaingChange objects /// </summary> /// <param name="stream">The markdown to parse</param> /// <param name="allowedCategories">Valid category strings. Pass null to allow any category. A breaking change using an invalid category will throw an exception while parsing the breaking change.</param> /// <returns>BreakingChanges parsed from the markdown</returns> public static IEnumerable<BreakingChange> FromMarkdown(Stream stream, IEnumerable<string> allowedCategories) { var breakingChanges = new List<BreakingChange>(); var state = ParseState.None; using (var sr = new StreamReader(stream)) { BreakingChange currentBreak = null; string currentLine; while (null != (currentLine = sr.ReadLine())) { currentLine = currentLine.Trim(); // New breaking change if (currentLine.StartsWith("## ", StringComparison.Ordinal)) { // Save previous breaking change and reset currentBreak if (currentBreak != null) { CleanAndAddBreak(breakingChanges, currentBreak); } currentBreak = new BreakingChange(); // Separate ID and title var splitTitle = currentLine.Substring("## ".Length).Split(new[] { ':' }, 2); if (splitTitle.Length == 1) { // Breaking changes are keyed on title, not ID, so if ':' is missing, just take the line as a title. // Note that this will make it impossible to suppress the breaking change, though. currentBreak.Title = splitTitle[0].Trim(); } else if (splitTitle.Length == 2) { currentBreak.Id = splitTitle[0].Trim(); currentBreak.Title = splitTitle[1].Trim(); } // Clear state state = ParseState.None; } else if (currentBreak != null) // Only parse breaking change if we've seen a breaking change header ("## ...") { // State changes if (currentLine.StartsWith("###", StringComparison.Ordinal)) { switch (currentLine.Substring("###".Length).Trim().ToLowerInvariant()) { case "scope": state = ParseState.Scope; break; case "version introduced": case "version broken": state = ParseState.VersionBroken; break; case "version reverted": case "version fixed": state = ParseState.VersionFixed; break; case "change description": case "details": state = ParseState.Details; break; case "recommended action": case "suggestion": state = ParseState.Suggestion; break; case "affected apis": case "applicableapis": state = ParseState.AffectedAPIs; break; case "original bug": case "buglink": case "bug": state = ParseState.OriginalBug; break; case "notes": state = ParseState.Notes; break; case "source analyzer status": state = ParseState.SourceAnalyzerStatus; break; case "category": case "categories": state = ParseState.Categories; break; default: ParseNonStateChange(currentBreak, state, currentLine, allowedCategories); break; } } // Bool properties else if (currentLine.StartsWith("- [ ]", StringComparison.Ordinal) || currentLine.StartsWith("- [x]", StringComparison.OrdinalIgnoreCase)) { bool isChecked = currentLine.StartsWith("- [x]", StringComparison.OrdinalIgnoreCase); switch (currentLine.Substring("- [x]".Length).Trim().ToLowerInvariant()) { case "quirked": case "isquirked": currentBreak.IsQuirked = isChecked; state = ParseState.None; break; case "build-time break": case "isbuildtime": currentBreak.IsBuildTime = isChecked; state = ParseState.None; break; default: ParseNonStateChange(currentBreak, state, currentLine, allowedCategories); break; } } // More info link else if (currentLine.StartsWith("[More information]", StringComparison.OrdinalIgnoreCase)) { currentBreak.Link = currentLine.Substring("[More information]".Length) .Trim(' ', '(', ')', '[', ']', '\t', '\n', '\r') // Remove markdown link enclosures .Replace("\\(", "(").Replace("\\)", ")"); // Unescape parens in link state = ParseState.None; } // Otherwise, process according to our current state else { ParseNonStateChange(currentBreak, state, currentLine, allowedCategories); } } } // Add the final break from the file if (currentBreak != null) { CleanAndAddBreak(breakingChanges, currentBreak); } } return breakingChanges; }