Skip to content

Commit c358a55

Browse files
authored
Merge pull request peopleworks#39 from peopleworks/fix/escape-generic-types
Write a generic type as code, so the reader gets to see it
2 parents 5f214f5 + e8d534a commit c358a55

4 files changed

Lines changed: 60 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- **A generic base type reaches the reader** (found by [@MBrekhof] while running the Word route in
13+
[#36]). `- **Base type:** ViewController<DetailView>` was printed bare, and `<DetailView>` is an
14+
*inline* HTML tag to every CommonMark parser: an export drops it and github.com's sanitizer strips
15+
it, so the page said the base class was `ViewController`. Not a missing answer — a different one.
16+
Backticks at the three sites that print a type name outside code.
17+
The guard from [#28] missed it because it scanned only lines that *opened* with `<`, which was the
18+
shape of the `<details>` block it was written for. It now matches a tag anywhere on a line, with
19+
fenced blocks and inline code spans excluded — the second because it is the remedy, and a guard
20+
that rejected its own fix would be no guard at all. 392 tests.
21+
22+
[#36]: https://github.com/peopleworks/XAFLogicExplainer/pull/36
23+
1024
## [0.15.0] — 2026-08-23
1125

1226
How it works, not only what exists.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ applications. The agent-facing surface is what is landing now, in the open.
390390
|| Pluggable publishing targets (`IDocumentationSink`) |
391391
|| **MCP server** — 11 tools, live against your source |
392392
|| **Installable Claude Code plugin** with skill and MCP server |
393-
|| **391 tests** over synthetic XPO and EF Core fixtures — no DevExpress needed |
393+
|| **392 tests** over synthetic XPO and EF Core fixtures — no DevExpress needed |
394394
|| **DevExpress ground-truth catalog**, generated locally by licensees |
395395

396396
PeopleWorks Copilot, where this tool grew up, is now one sink among several rather than the

src/XafLogicExplainer.Core/Generators/MarkdownDocumentationGenerator.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@ private DocumentSection GenerateEntitiesSection(ExtractedProject project)
496496
sb.AppendLine($"*{entity.Description}*");
497497

498498
sb.AppendLine();
499-
sb.AppendLine($"- **{_l.BaseType}:** {entity.BaseType}");
499+
sb.AppendLine($"- **{_l.BaseType}:** `{entity.BaseType}`");
500500
if (!string.IsNullOrEmpty(entity.ModelCaption))
501501
sb.AppendLine($"- **{_l.CaptionModelEditor}:** {entity.ModelCaption}");
502502
if (!string.IsNullOrEmpty(entity.NavigationGroup))
@@ -599,7 +599,7 @@ private DocumentSection GenerateControllersSection(ExtractedProject project)
599599
{
600600
sb.AppendLine($"## {controller.ClassName}");
601601
sb.AppendLine();
602-
sb.AppendLine($"- **{_l.BaseType}:** {controller.BaseControllerType}");
602+
sb.AppendLine($"- **{_l.BaseType}:** `{controller.BaseControllerType}`");
603603
if (!string.IsNullOrEmpty(controller.TargetObjectType))
604604
sb.AppendLine($"- **{_l.TargetEntity}:** {controller.TargetObjectType}");
605605
if (!string.IsNullOrEmpty(controller.TargetViewType))
@@ -912,7 +912,7 @@ private DocumentSection GenerateNavigationSection(ExtractedProject project)
912912
sb.AppendLine();
913913
foreach (var entity in orphans)
914914
{
915-
sb.AppendLine($"- **{entity.ClassName}** ({entity.BaseType})");
915+
sb.AppendLine($"- **{entity.ClassName}** (`{entity.BaseType}`)");
916916
}
917917
}
918918

tests/XafLogicExplainer.Tests/PortableMarkdownTests.cs

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Text.RegularExpressions;
12
using XafLogicExplainer.Core.Generators;
23
using XafLogicExplainer.Core.Models;
34

@@ -19,6 +20,11 @@ namespace XafLogicExplainer.Tests;
1920
/// pipe tables, fenced code, lists, bold, inline code — maps to a real Word equivalent, so this one
2021
/// call site was the whole difference between an extraction and a document someone can hand over.
2122
/// </para>
23+
/// <para>
24+
/// The second offender was found the same way, by him, one release later: a generic base type was
25+
/// printed bare, and <c>&lt;DetailView&gt;</c> is an <em>inline</em> tag rather than a block. The
26+
/// guard below missed it because it was written to the shape of the first one.
27+
/// </para>
2228
/// </remarks>
2329
public class PortableMarkdownTests
2430
{
@@ -32,6 +38,7 @@ private static readonly (string Name, ExtractedProject Project)[] Samples =
3238
("DeepXpo", SampleProjects.DeepXpo),
3339
("AuditedXpo", SampleProjects.AuditedXpo),
3440
("Demo", SampleProjects.Demo),
41+
("Walkthrough", SampleProjects.Walkthrough),
3542
];
3643

3744
private static string Markdown(ExtractedProject project, string language) =>
@@ -41,10 +48,20 @@ private static string Markdown(ExtractedProject project, string language) =>
4148
.Replace("\r", "");
4249

4350
/// <summary>
44-
/// Lines that begin a CommonMark HTML block: outside a fence, a line whose first character is
45-
/// <c>&lt;</c>. Inside a fence the same line is source code and is left alone, which is why this
46-
/// tracks the fence rather than matching the whole document at once.
51+
/// Every tag CommonMark would treat as HTML, wherever on the line it sits.
4752
/// </summary>
53+
/// <remarks>
54+
/// The first version checked only whether a line <em>opened</em> with <c>&lt;</c>, which was the
55+
/// shape of the <c>&lt;details&gt;</c> block it was written for. A generic type in the middle of
56+
/// a sentence walked straight past it: <c>ViewController&lt;DetailView&gt;</c> is an inline HTML
57+
/// tag to every CommonMark parser, so the reader was shown <c>ViewController</c> and nothing
58+
/// else — on github.com, where the sanitizer strips the unknown tag, as much as in an export.
59+
/// <para>
60+
/// Two things are excluded rather than matched. A fenced block is source code, where
61+
/// <c>CreateObject&lt;Customer&gt;()</c> is exactly right. An inline code span is the remedy
62+
/// itself, so it has to be allowed or the guard would reject its own fix.
63+
/// </para>
64+
/// </remarks>
4865
private static List<string> RawHtmlLines(string markdown)
4966
{
5067
var offenders = new List<string>();
@@ -58,7 +75,12 @@ private static List<string> RawHtmlLines(string markdown)
5875
continue;
5976
}
6077

61-
if (!insideFence && line.TrimStart().StartsWith('<'))
78+
if (insideFence)
79+
continue;
80+
81+
var bare = Regex.Replace(line, "`[^`]*`", "");
82+
83+
if (Regex.IsMatch(bare, "</?[A-Za-z][A-Za-z0-9-]*[^<>]*/?>"))
6284
offenders.Add(line.Trim());
6385
}
6486

@@ -75,11 +97,25 @@ public void NoGeneratedPageOpensALineWithRawHtml(string language)
7597
var offenders = RawHtmlLines(Markdown(project, language));
7698

7799
Assert.True(offenders.Count == 0,
78-
$"{name} ({language}) emits raw HTML outside a code fence, which renders as literal "
79-
+ $"text everywhere but a browser: {string.Join(" | ", offenders)}");
100+
$"{name} ({language}) emits a tag CommonMark reads as HTML. A block renders as "
101+
+ "literal text everywhere but a browser; an inline tag is dropped and takes the "
102+
+ $"type name with it: {string.Join(" | ", offenders)}");
80103
}
81104
}
82105

106+
[Fact]
107+
public void AGenericTypeSurvivesBecauseItIsWrittenAsCode()
108+
{
109+
// Found by @MBrekhof pointing a real Word converter at the output. `ViewController<DetailView>`
110+
// was printed bare, and `<DetailView>` is an inline HTML tag to every CommonMark parser: an
111+
// export drops it and github.com's sanitizer strips it, so the reader is told the base class
112+
// is `ViewController`. That is a different answer rather than a missing one.
113+
var english = Markdown(SampleProjects.Xpo, "en");
114+
115+
Assert.Contains("`ViewController<DetailView>`", english, StringComparison.Ordinal);
116+
Assert.DoesNotContain("** ViewController<DetailView>", english, StringComparison.Ordinal);
117+
}
118+
83119
[Fact]
84120
public void TheSeedSourceIsIntroducedByAHeadingRatherThanAFold()
85121
{

0 commit comments

Comments
 (0)