Add markdown format output for code analysis tool#340
Conversation
Co-authored-by: witskeeper <5594094+witskeeper@users.noreply.github.com>
Co-authored-by: witskeeper <5594094+witskeeper@users.noreply.github.com>
Co-authored-by: witskeeper <5594094+witskeeper@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a Markdown output mode to the code analysis visualization tool so the architecture output can be consumed as structured text (including optional Mermaid), alongside the existing HTML output.
Changes:
- Added
VisualizationMarkdownBuilderto generate Markdown reports (stats, elements, relationships, Mermaid, optional history). - Extended
generateCLI with--format(html/markdown/md) and updated default output extension behavior. - Updated tool README and added unit tests for Markdown generation basics/escaping.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
src/NetCorePal.Extensions.CodeAnalysis/VisualizationMarkdownBuilder.cs |
Implements Markdown report generation, including optional Mermaid diagrams and snapshot history/trends. |
src/NetCorePal.Extensions.CodeAnalysis.Tools/Program.cs |
Adds --format option and adjusts default output path extension for Markdown. |
src/NetCorePal.Extensions.CodeAnalysis.Tools/AppCsContentGenerator.cs |
Emits generated app.cs that writes either HTML or Markdown depending on selected format. |
src/NetCorePal.Extensions.CodeAnalysis.Tools/README.md |
Documents the new output format option and usage examples. |
test/NetCorePal.Extensions.CodeAnalysis.UnitTests/VisualizationMarkdownBuilderTests.cs |
Adds unit tests for basic Markdown output behavior and escaping. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| foreach (var (chainName, diagram) in processingFlows) | ||
| { | ||
| sb.AppendLine($"#### {chainName}"); | ||
| sb.AppendLine(); |
There was a problem hiding this comment.
chainName is used directly in a Markdown heading. Since it comes from node names, it may contain Markdown-special characters and break the heading/rendering. Escape chainName (or sanitize it) before writing it into the document.
There was a problem hiding this comment.
Fixed in 05e2a25. chainName is now escaped using EscapeMarkdown() before being written to headings.
| foreach (var (aggregateName, diagram) in aggregateRelations) | ||
| { | ||
| sb.AppendLine($"#### {aggregateName}"); | ||
| sb.AppendLine(); |
There was a problem hiding this comment.
aggregateName is used directly in a Markdown heading. Since it comes from node names, it may contain Markdown-special characters and break the heading/rendering. Escape aggregateName (or sanitize it) before writing it into the document.
There was a problem hiding this comment.
Fixed in 05e2a25. aggregateName is now escaped using EscapeMarkdown() before being written to headings.
| var timestampStr = TryParseVersionAsDateTime(snapshot.Metadata.Version, out var timestamp) | ||
| ? timestamp.ToString("yyyy-MM-dd HH:mm") | ||
| : snapshot.Metadata.Version; | ||
| sb.AppendLine($"| {timestampStr} | {EscapeMarkdown(snapshot.Metadata.Description)} | {snapshot.Metadata.NodeCount} | {snapshot.Metadata.RelationshipCount} |"); | ||
| } |
There was a problem hiding this comment.
In the history trends tables, timestampStr falls back to the raw Metadata.Version string when parsing fails, but it is not escaped before being inserted into a Markdown table cell. If a non-timestamp version contains | or other Markdown characters, the table will break. Escape timestampStr before writing it into the table.
There was a problem hiding this comment.
Fixed in 05e2a25. timestampStr fallback now uses EscapeMarkdown() to safely handle malformed version strings containing special characters.
| [Fact] | ||
| public void GenerateVisualizationMarkdown_WithSimpleData_GeneratesValidMarkdown() | ||
| { |
There was a problem hiding this comment.
Tests currently only cover withHistory: false and don't validate output when withHistory: true with snapshots (version history + trend tables). Add tests covering snapshot rendering and table-escaping edge cases (e.g., descriptions containing |, backticks, newlines) to prevent regressions.
There was a problem hiding this comment.
Fixed in 05e2a25. Added 5 comprehensive tests covering:
- History with version snapshots
- Pipe character escaping in descriptions
- Multiple snapshots with trend tables
- Malformed version string handling
- Special markdown characters in descriptions
| // Auto-adjust output file extension if needed | ||
| var expectedExtension = normalizedFormat == "markdown" ? ".md" : ".html"; | ||
| var outputPath = outputFile.FullName; | ||
|
|
There was a problem hiding this comment.
expectedExtension is computed but never used, which creates dead code and can generate compiler warnings. Either remove it or use it to validate/adjust the output filename extension when --format is specified.
| // Auto-adjust output file extension if needed | |
| var expectedExtension = normalizedFormat == "markdown" ? ".md" : ".html"; | |
| var outputPath = outputFile.FullName; | |
| // Determine the expected file extension for the selected format | |
| var expectedExtension = normalizedFormat == "markdown" ? ".md" : ".html"; | |
| var outputPath = outputFile.FullName; | |
| // Warn when the chosen output file extension does not match the selected format | |
| if (!outputPath.EndsWith(expectedExtension, StringComparison.OrdinalIgnoreCase) && verbose) | |
| { | |
| var currentExtension = System.IO.Path.GetExtension(outputPath); | |
| Console.WriteLine( | |
| $"Warning: Output file extension '{currentExtension}' does not match the selected format '{normalizedFormat}'. Expected '{expectedExtension}'."); | |
| } | |
There was a problem hiding this comment.
Fixed in 05e2a25. Removed unused outputPath variable and now use expectedExtension to display extension mismatch warnings when verbose mode is enabled.
| private static string EscapeMarkdown(string text) | ||
| { | ||
| if (string.IsNullOrEmpty(text)) | ||
| return text; | ||
|
|
||
| // Escape special markdown characters | ||
| return text | ||
| .Replace("\\", "\\\\") | ||
| .Replace("`", "\\`") | ||
| .Replace("*", "\\*") |
There was a problem hiding this comment.
EscapeMarkdown does not escape the pipe character (|). Because snapshot descriptions are inserted into Markdown tables, a | in input will break table formatting. Add escaping for | (e.g., \|) or a table-cell specific escaping routine for values written into tables.
There was a problem hiding this comment.
Fixed in 05e2a25. Added pipe character | escaping to EscapeMarkdown() function to prevent table formatting issues.
| foreach (var snapshot in snapshotList) | ||
| { | ||
| var metadata = snapshot.Metadata; | ||
| // Parse version string as DateTime | ||
| var timestampStr = TryParseVersionAsDateTime(metadata.Version, out var timestamp) | ||
| ? timestamp.ToString("yyyy-MM-dd HH:mm:ss") | ||
| : metadata.Version; | ||
| sb.AppendLine($"- **{timestampStr}**: {metadata.Description}"); | ||
| sb.AppendLine($" - 节点数: {metadata.NodeCount}, 关系数: {metadata.RelationshipCount}"); | ||
| sb.AppendLine($" - Hash: `{metadata.Hash}`"); | ||
| } |
There was a problem hiding this comment.
This foreach loop immediately maps its iteration variable to another variable - consider mapping the sequence explicitly using '.Select(...)'.
| foreach (var nodeType in result.Nodes.Select(n => n.Type).Distinct()) | ||
| { | ||
| allNodeTypes.Add(nodeType); | ||
| } |
There was a problem hiding this comment.
This foreach loop immediately maps its iteration variable to another variable - consider mapping the sequence explicitly using '.Select(...)'.
| foreach (var nodeType in result.Nodes.Select(n => n.Type).Distinct()) | |
| { | |
| allNodeTypes.Add(nodeType); | |
| } | |
| allNodeTypes.UnionWith(result.Nodes.Select(n => n.Type)); |
There was a problem hiding this comment.
Fixed in 05e2a25. Replaced nested foreach loop with .UnionWith() for better performance and cleaner code.
| sb.Append($"| {timestampStr} |"); | ||
| foreach (var nodeType in allNodeTypes.OrderBy(t => t.ToString())) | ||
| { | ||
| var count = nodesByType.ContainsKey(nodeType) ? nodesByType[nodeType] : 0; |
There was a problem hiding this comment.
Inefficient use of 'ContainsKey' and indexer.
| var count = nodesByType.ContainsKey(nodeType) ? nodesByType[nodeType] : 0; | |
| var count = nodesByType.TryGetValue(nodeType, out var value) ? value : 0; |
There was a problem hiding this comment.
Fixed in 05e2a25. Replaced ContainsKey + indexer pattern with more efficient .TryGetValue() pattern.
| var outputPath = outputFile.FullName; | ||
|
|
||
| // If output uses default value and format is markdown, change extension | ||
| if (outputFile.FullName.EndsWith("architecture-visualization.html", StringComparison.OrdinalIgnoreCase) | ||
| && normalizedFormat == "markdown") | ||
| { | ||
| outputPath = outputFile.FullName.Replace(".html", ".md"); | ||
| outputFile = new FileInfo(outputPath); |
There was a problem hiding this comment.
This assignment to outputPath is useless, since its value is never read.
| var outputPath = outputFile.FullName; | |
| // If output uses default value and format is markdown, change extension | |
| if (outputFile.FullName.EndsWith("architecture-visualization.html", StringComparison.OrdinalIgnoreCase) | |
| && normalizedFormat == "markdown") | |
| { | |
| outputPath = outputFile.FullName.Replace(".html", ".md"); | |
| outputFile = new FileInfo(outputPath); | |
| // If output uses default value and format is markdown, change extension | |
| if (outputFile.FullName.EndsWith("architecture-visualization.html", StringComparison.OrdinalIgnoreCase) | |
| && normalizedFormat == "markdown") | |
| { | |
| outputFile = new FileInfo(outputFile.FullName.Replace(".html", ".md")); |
There was a problem hiding this comment.
Fixed in 05e2a25. Removed unused outputPath assignment and simplified the code to directly reassign outputFile.
|
@copilot apply changes based on the comments in this thread |
…hensive tests Co-authored-by: witskeeper <5594094+witskeeper@users.noreply.github.com>
All code review feedback has been addressed in commit 05e2a25: Escaping & Security:
Code Quality:
Testing:
The markdown output now safely handles all edge cases and special characters. |
|
@copilot 在code-analysis-tools.md中添加说明 |
|
@copilot 生成md时,应该添加html模版中相同的过滤条件 |
Co-authored-by: witskeeper <5594094+witskeeper@users.noreply.github.com>
已在 1f96ace 中添加说明。更新了以下文档文件: 中文文档 (
英文文档 (
两个文档现在都包含了完整的 Markdown 格式输出功能说明,包括用法示例和注意事项。 |
|
@copilot 生成md时,应该添加html模版中相同的过滤条件 // 定义要显示的节点类型(按定义顺序,排除指定类型) |
…stency with HTML Co-authored-by: witskeeper <5594094+witskeeper@users.noreply.github.com>
已在 0b2b520 中添加。为保持与 HTML 模板的一致性,在 Markdown 生成器中添加了相同的过滤条件参数: 添加的参数:
实现细节:
示例输出: > **注意**: 以下 Mermaid 图表建议配置参数: `maxEdges: 5000, maxTextSize: 1000000`所有测试通过 (10/10)。 |
Co-authored-by: witskeeper <5594094+witskeeper@users.noreply.github.com>
已在 3bfdd21 中添加。为保持与 HTML 模板的一致性,在 Markdown 生成器中添加了相同的节点和关系类型过滤: 添加的过滤规则: DisplayNodeTypes (显示的节点类型):
DisplayRelationshipTypes (显示的关系类型):
过滤掉的内部实现类型:
实现细节:
所有测试通过 (10/10)。 |
|
@copilot 生成md的统计信息部分时,应该基于类似html模版中相同的过滤条件,仅统计下列类型 // 定义要显示的节点类型(按定义顺序,排除指定类型) |
统计信息部分已在 3bfdd21 中实现了相同的过滤条件。 当前实现:
过滤的类型与 HTML 模板完全一致:
统计部分已正确应用过滤条件,与 HTML 模板保持一致。 |
|
@copilot 移除生成md的版本历史里节点数、关系数 的信息 |
…ection Co-authored-by: witskeeper <5594094+witskeeper@users.noreply.github.com>
Implementation Plan for Markdown Output Feature
Adding markdown format output to the code analysis tool to help Large Language Models understand the business model.
--formatoption togeneratecommand (supportinghtmlandmarkdownformats)CodeFlowAnalysisResultto markdown format|escaping inEscapeMarkdownfunctionchainNameandaggregateNamein Mermaid diagram headingstimestampStrand descriptions in history tablesoutputPathvariable in Program.csexpectedExtensionfor extension mismatch warnings.UnionWith().TryGetValue()Changes Summary
Version History Simplification
Type Filtering (Consistent with HTML)
DisplayNodeTypesandDisplayRelationshipTypesarrays to filter outputAPI Consistency
maxEdges(default: 5000) andmaxTextSize(default: 1000000) parameters toGenerateVisualizationMarkdownGenerateVisualizationHtmlfor consistencyDocumentation Updates
--formatparameter to command parameters table in both Chinese and English docs--outputand--titledescriptions to reflect support for both HTML and MarkdownEscaping Improvements
|character escaping to prevent table formatting issuesCode Optimizations
.UnionWith()for better performanceContainsKey+ indexer withTryGetValuepatternTesting
Security Summary
No security vulnerabilities introduced. All user input properly escaped before rendering in markdown.
Original prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.