Skip to content

Commit e4325ae

Browse files
rolfbjarneCopilot
andauthored
[mono-api-html] Fix placeholder leak and add a test project (#26405)
We've had a few regressions in mono-api-html, so this fixes the latest one and adds a unit test project to guard against future ones. ## The bug The `MultiplexedFormatter` builds descriptions using the placeholder tokens `%LESSERTHANREPLACEMENT%` / `%GREATERTHANREPLACEMENT%`, because a single shared description string must be converted into each sub-formatter's own representation (`&lt;`/`&gt;` for HTML, `<`/`>` for markdown). The structured methods already convert these via `Replace ()`, but the raw `Write`/`WriteLine` string overloads did not. The "New Type" addition path (`ClassComparer.AddedInner`) writes generic interface lists such as `IEnumerable<AuthorizationRight>` directly through `Output.Write`, so the placeholders leaked verbatim into the generated HTML (and markdown), e.g.: IEnumerable%LESSERTHANREPLACEMENT%AuthorizationRight%GREATERTHANREPLACEMENT% Applying `Replace ()` in the six string-based `Write`/`WriteLine` overloads converts the placeholders per sub-formatter on this path too. ## Tests & solution * Added `tools/api-tools/mono-api-html-tests`, an NUnit test project that references `mono-api-html`. The first test drives the public `ApiDiffFormatted.Generate` over in-memory api-info XML (the exact `AuthorizationRights` case), producing both HTML and markdown, and asserts no `*THANREPLACEMENT` tokens leak and that the generic interface renders correctly in both formats. Verified the test fails without the fix. * Added `tools/api-tools/api-tools.slnx` referencing `mono-api-info`, `mono-api-html` and the new test project, so `dotnet test` in `tools/api-tools` builds all three and runs the tests. 🤖 Pull request created by Copilot --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent f7920a5 commit e4325ae

4 files changed

Lines changed: 108 additions & 6 deletions

File tree

tools/api-tools/api-tools.slnx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<Solution>
2+
<Project Path="mono-api-info/mono-api-info.csproj" />
3+
<Project Path="mono-api-html/mono-api-html.csproj" />
4+
<Project Path="mono-api-html-tests/mono-api-html-tests.csproj" />
5+
</Solution>
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.IO;
5+
6+
using NUnit.Framework;
7+
8+
using Mono.ApiTools;
9+
10+
namespace MonoApiHtmlTests {
11+
12+
[TestFixture]
13+
public class ApiDiffTests {
14+
15+
static string CreateApiInfo (string classes)
16+
{
17+
return $@"<assemblies>
18+
<assembly name=""Microsoft.macOS"" version=""0.0.0.0"">
19+
<namespaces>
20+
<namespace name=""Security"">
21+
<classes>{classes}</classes>
22+
</namespace>
23+
</namespaces>
24+
</assembly>
25+
</assemblies>";
26+
}
27+
28+
// A new type that implements generic interfaces used to leak the
29+
// %LESSERTHANREPLACEMENT% / %GREATERTHANREPLACEMENT% placeholders into the
30+
// generated output (see the MultiplexedFormatter raw Write path).
31+
[Test]
32+
public void GenericInterfacesDoNotLeakPlaceholders ()
33+
{
34+
var source = CreateApiInfo ("");
35+
var target = CreateApiInfo (@"
36+
<class name=""AuthorizationRights"" type=""class"" sealed=""true"" base=""ObjCRuntime.DisposableObject"">
37+
<interfaces>
38+
<interface name=""System.Collections.Generic.IEnumerable`1[Security.AuthorizationRight]"" />
39+
<interface name=""System.Collections.Generic.IReadOnlyCollection`1[Security.AuthorizationRight]"" />
40+
</interfaces>
41+
</class>");
42+
43+
var sourceFile = Path.GetTempFileName ();
44+
var targetFile = Path.GetTempFileName ();
45+
var htmlFile = Path.GetTempFileName ();
46+
var markdownFile = Path.GetTempFileName ();
47+
try {
48+
File.WriteAllText (sourceFile, source);
49+
File.WriteAllText (targetFile, target);
50+
51+
var config = new ApiDiffFormattedConfig {
52+
HtmlOutput = htmlFile,
53+
MarkdownOutput = markdownFile,
54+
};
55+
ApiDiffFormatted.Generate (sourceFile, targetFile, config);
56+
57+
var html = File.ReadAllText (htmlFile);
58+
var markdown = File.ReadAllText (markdownFile);
59+
60+
Assert.That (html, Does.Not.Contain ("%LESSERTHANREPLACEMENT%"), "html LesserThan placeholder");
61+
Assert.That (html, Does.Not.Contain ("%GREATERTHANREPLACEMENT%"), "html GreaterThan placeholder");
62+
Assert.That (markdown, Does.Not.Contain ("%LESSERTHANREPLACEMENT%"), "markdown LesserThan placeholder");
63+
Assert.That (markdown, Does.Not.Contain ("%GREATERTHANREPLACEMENT%"), "markdown GreaterThan placeholder");
64+
65+
Assert.That (html, Does.Contain ("IEnumerable&lt;AuthorizationRight&gt;"), "html generic");
66+
Assert.That (markdown, Does.Contain ("IEnumerable<AuthorizationRight>"), "markdown generic");
67+
} finally {
68+
File.Delete (sourceFile);
69+
File.Delete (targetFile);
70+
File.Delete (htmlFile);
71+
File.Delete (markdownFile);
72+
}
73+
}
74+
}
75+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Project Sdk="Microsoft.NET.Sdk">
3+
<PropertyGroup>
4+
<TargetFramework>net$(BundledNETCoreAppTargetFrameworkVersion)</TargetFramework>
5+
<IsPackable>false</IsPackable>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNETTestSdkPackageVersion)" />
10+
<PackageReference Include="NUnit" Version="$(NUnitPackageVersion)" />
11+
<PackageReference Include="NUnit3TestAdapter" Version="$(NUnit3TestAdapterPackageVersion)" />
12+
<PackageReference Include="NUnit.Analyzers" Version="$(NUnitAnalyzersPackageVersion)" />
13+
</ItemGroup>
14+
15+
<ItemGroup>
16+
<ProjectReference Include="..\mono-api-html\mono-api-html.csproj" />
17+
</ItemGroup>
18+
</Project>

tools/api-tools/mono-api-html/MultiplexedFormatter.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -237,19 +237,21 @@ public override void WriteLine ()
237237
public override void WriteLine (string line)
238238
{
239239
foreach (var formatter in formatters)
240-
formatter.WriteLine (line);
240+
formatter.WriteLine (Replace (formatter, line));
241241
}
242242

243243
public override void WriteLine (string format, params object [] arguments)
244244
{
245+
var line = string.Format (format, arguments);
245246
foreach (var formatter in formatters)
246-
formatter.WriteLine (format, arguments);
247+
formatter.WriteLine (Replace (formatter, line));
247248
}
248249

249250
public override void WriteLine (StringBuilder sb)
250251
{
252+
var line = sb.ToString ();
251253
foreach (var formatter in formatters)
252-
formatter.WriteLine (sb);
254+
formatter.WriteLine (Replace (formatter, line));
253255
}
254256

255257
public override void Write (char value)
@@ -261,19 +263,21 @@ public override void Write (char value)
261263
public override void Write (string line)
262264
{
263265
foreach (var formatter in formatters)
264-
formatter.Write (line);
266+
formatter.Write (Replace (formatter, line));
265267
}
266268

267269
public override void Write (string format, params object [] arguments)
268270
{
271+
var line = string.Format (format, arguments);
269272
foreach (var formatter in formatters)
270-
formatter.Write (format, arguments);
273+
formatter.Write (Replace (formatter, line));
271274
}
272275

273276
public override void Write (StringBuilder sb)
274277
{
278+
var line = sb.ToString ();
275279
foreach (var formatter in formatters)
276-
formatter.Write (sb);
280+
formatter.Write (Replace (formatter, line));
277281
}
278282
}
279283
}

0 commit comments

Comments
 (0)