diff --git a/tools/StandardAnchorTags.Tests/ReferenceUpdateProcessorTests.cs b/tools/StandardAnchorTags.Tests/ReferenceUpdateProcessorTests.cs
index dd08c0ec2..7f6957473 100644
--- a/tools/StandardAnchorTags.Tests/ReferenceUpdateProcessorTests.cs
+++ b/tools/StandardAnchorTags.Tests/ReferenceUpdateProcessorTests.cs
@@ -19,4 +19,14 @@ public void DoesNotTreatFenceMentionsAsDelimiters(string line)
{
Assert.That(ReferenceUpdateProcessor.IsCodeFenceDelimiter(line), Is.False);
}
+
+ [Test]
+ public void RejectsSectionLinkWithoutClosingParenthesis()
+ {
+ const string line = "[ยง15.17](classes.md#1517-record-class-and-non-record-class-differences.";
+
+ var result = ReferenceUpdateProcessor.ExpandToIncludeExistingLink(line, new Range(1, 7));
+
+ Assert.That(result, Is.Null);
+ }
}
diff --git a/tools/StandardAnchorTags/DiagnosticIDs.cs b/tools/StandardAnchorTags/DiagnosticIDs.cs
index e89613386..83eff87f0 100644
--- a/tools/StandardAnchorTags/DiagnosticIDs.cs
+++ b/tools/StandardAnchorTags/DiagnosticIDs.cs
@@ -3,4 +3,5 @@ internal class DiagnosticIDs
{
public const string TOC001 = nameof(TOC001);
public const string TOC002 = nameof(TOC002);
+ public const string TOC003 = nameof(TOC003);
}
diff --git a/tools/StandardAnchorTags/ReferenceUpdateProcessor.cs b/tools/StandardAnchorTags/ReferenceUpdateProcessor.cs
index 95bcc826d..7d5ec7648 100644
--- a/tools/StandardAnchorTags/ReferenceUpdateProcessor.cs
+++ b/tools/StandardAnchorTags/ReferenceUpdateProcessor.cs
@@ -83,7 +83,19 @@ private string ProcessSectionLinks(string line, int lineNumber, string path)
linkText = linkMap[referenceText].FormattedMarkdownLink;
}
// expand the range for any existing link:
- sectionReferenceRange = ExpandToIncludeExistingLink(line, sectionReferenceRange);
+ if (ExpandToIncludeExistingLink(line, sectionReferenceRange) is not Range expandedRange)
+ {
+ var diagnostic = new StatusCheckMessage(
+ path,
+ lineNumber,
+ lineNumber,
+ $"Malformed section link for `{referenceText}`; expected `[section](target)`",
+ DiagnosticIDs.TOC003);
+ logger.LogFailure(diagnostic);
+ returnedLine.Append(line[index..]);
+ return returnedLine.ToString();
+ }
+ sectionReferenceRange = expandedRange;
var textRangeToCopyUnchanged = new Range(index, sectionReferenceRange.Start);
// Copy text up to replacement:
@@ -128,7 +140,7 @@ private string ProcessSectionLinks(string line, int lineNumber, string path)
return new Range(startIndex, endIndex);
}
- private static Range ExpandToIncludeExistingLink(string line, Range range)
+ internal static Range? ExpandToIncludeExistingLink(string line, Range range)
{
// If the character before the start of the range isn't the '[' character,
// return => no existing link.
@@ -138,10 +150,19 @@ private static Range ExpandToIncludeExistingLink(string line, Range range)
// Start and the end of the range, look for "](", then ']'.
int endIndex = range.End.Value;
- if (line.Substring(endIndex, 2) != "](") throw new InvalidOperationException($"Unexpected link text >{line.Substring(endIndex, 2)}< in line >{line}<");
+ if ((endIndex + 1 >= line.Length) ||
+ (line[endIndex] != ']') ||
+ (line[endIndex + 1] != '('))
+ {
+ return null;
+ }
endIndex += 2;
- while (line[endIndex] != ')') endIndex++;
+ endIndex = line.IndexOf(')', endIndex);
+ if (endIndex == -1)
+ {
+ return null;
+ }
return new Range(previous, endIndex + 1);
}
diff --git a/tools/Utilities.Tests/StatusCheckLoggerTests.cs b/tools/Utilities.Tests/StatusCheckLoggerTests.cs
new file mode 100644
index 000000000..18efa12f8
--- /dev/null
+++ b/tools/Utilities.Tests/StatusCheckLoggerTests.cs
@@ -0,0 +1,28 @@
+using Utilities;
+
+namespace Utilities.Tests;
+
+public class StatusCheckLoggerTests
+{
+ [Test]
+ public void CheckRunLimitsAnnotationsAndSummarizesOmittedDiagnostics()
+ {
+ var output = new StringWriter();
+ var logger = new StatusCheckLogger(output, ".", "Test tool");
+
+ for (int i = 1; i <= 55; i++)
+ {
+ logger.LogWarning(new StatusCheckMessage("file.md", i, i, $"Warning {i}", "TEST001"));
+ }
+
+ var checkRun = logger.CreateCheckRun("head-sha");
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(checkRun.Output.Annotations, Has.Count.EqualTo(StatusCheckLogger.MaximumAnnotations));
+ Assert.That(checkRun.Output.Summary, Does.Contain("55 diagnostics"));
+ Assert.That(checkRun.Output.Summary, Does.Contain("5 additional diagnostics"));
+ Assert.That(output.ToString(), Does.Contain("Warning 55"));
+ });
+ }
+}
diff --git a/tools/Utilities.Tests/Utilities.Tests.csproj b/tools/Utilities.Tests/Utilities.Tests.csproj
new file mode 100644
index 000000000..694a55b03
--- /dev/null
+++ b/tools/Utilities.Tests/Utilities.Tests.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/Utilities/Properties/AssemblyInfo.cs b/tools/Utilities/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..0f0027416
--- /dev/null
+++ b/tools/Utilities/Properties/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Utilities.Tests")]
diff --git a/tools/Utilities/StatusCheckLogger.cs b/tools/Utilities/StatusCheckLogger.cs
index dc8e83c4f..073a1ea44 100644
--- a/tools/Utilities/StatusCheckLogger.cs
+++ b/tools/Utilities/StatusCheckLogger.cs
@@ -24,6 +24,8 @@ public record StatusCheckMessage(string file, int StartLine, int EndLine, string
/// The name of the tool that is running the check
public class StatusCheckLogger(TextWriter writer, string pathToRoot, string toolName)
{
+ internal const int MaximumAnnotations = 50;
+
private List annotations = [];
public bool Success { get; private set; } = true;
@@ -155,15 +157,7 @@ public void ExitOnFailure(StatusCheckMessage d)
/// The full check run result object
public async Task BuildCheckRunResult(string token, string owner, string repo, string sha)
{
- NewCheckRun result = new(toolName, sha)
- {
- Status = CheckStatus.Completed,
- Conclusion = Success ? CheckConclusion.Success : CheckConclusion.Failure,
- Output = new($"{toolName} Check Run results", $"{toolName} result is {(Success ? "success" : "failure")} with {annotations.Count} diagnostics.")
- {
- Annotations = annotations
- }
- };
+ NewCheckRun result = CreateCheckRun(sha);
var prodInformation = new ProductHeaderValue("TC49-TG2", "1.0.0");
var tokenAuth = new Credentials(token);
@@ -183,4 +177,25 @@ public async Task BuildCheckRunResult(string token, string owner, string repo, s
writer.WriteLine(e);
}
}
+
+ internal NewCheckRun CreateCheckRun(string sha)
+ {
+ var annotationsToSubmit = annotations.Take(MaximumAnnotations).ToList();
+ var omittedAnnotations = annotations.Count - annotationsToSubmit.Count;
+ var summary = $"{toolName} result is {(Success ? "success" : "failure")} with {annotations.Count} diagnostics.";
+ if (omittedAnnotations > 0)
+ {
+ summary += $" The first {MaximumAnnotations} diagnostics are annotated; {omittedAnnotations} additional diagnostics are available in the workflow log.";
+ }
+
+ return new(toolName, sha)
+ {
+ Status = CheckStatus.Completed,
+ Conclusion = Success ? CheckConclusion.Success : CheckConclusion.Failure,
+ Output = new($"{toolName} Check Run results", summary)
+ {
+ Annotations = annotationsToSubmit
+ }
+ };
+ }
}
diff --git a/tools/tools.sln b/tools/tools.sln
index c259de718..4997f8271 100644
--- a/tools/tools.sln
+++ b/tools/tools.sln
@@ -24,6 +24,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExampleTester.Tests", "Exam
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StandardAnchorTags.Tests", "StandardAnchorTags.Tests\StandardAnchorTags.Tests.csproj", "{DD0FB88E-0E16-45CD-A833-8BDD8EE197F0}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Utilities.Tests", "Utilities.Tests\Utilities.Tests.csproj", "{284AC372-152E-4268-B435-9A122913BA0A}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -142,6 +144,18 @@ Global
{DD0FB88E-0E16-45CD-A833-8BDD8EE197F0}.Release|x64.Build.0 = Release|Any CPU
{DD0FB88E-0E16-45CD-A833-8BDD8EE197F0}.Release|x86.ActiveCfg = Release|Any CPU
{DD0FB88E-0E16-45CD-A833-8BDD8EE197F0}.Release|x86.Build.0 = Release|Any CPU
+ {284AC372-152E-4268-B435-9A122913BA0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {284AC372-152E-4268-B435-9A122913BA0A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {284AC372-152E-4268-B435-9A122913BA0A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {284AC372-152E-4268-B435-9A122913BA0A}.Debug|x64.Build.0 = Debug|Any CPU
+ {284AC372-152E-4268-B435-9A122913BA0A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {284AC372-152E-4268-B435-9A122913BA0A}.Debug|x86.Build.0 = Debug|Any CPU
+ {284AC372-152E-4268-B435-9A122913BA0A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {284AC372-152E-4268-B435-9A122913BA0A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {284AC372-152E-4268-B435-9A122913BA0A}.Release|x64.ActiveCfg = Release|Any CPU
+ {284AC372-152E-4268-B435-9A122913BA0A}.Release|x64.Build.0 = Release|Any CPU
+ {284AC372-152E-4268-B435-9A122913BA0A}.Release|x86.ActiveCfg = Release|Any CPU
+ {284AC372-152E-4268-B435-9A122913BA0A}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE