From 4320443a0626f45c5fdd897e85e593650959c9ac Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 15:28:53 +0000 Subject: [PATCH 01/36] Add method-level change detection for ILMismatch assemblies Use System.Reflection.Metadata to detect type/method/property/field additions and removals, and method body IL byte changes between old and new assemblies. Results appear as a collapsible section in both MD (## Method-Level Changes) and HTML (inline expandable row above IL diff) reports. Controlled by ShouldIncludeMethodLevelChangesInReport config setting (default: true). https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- CHANGELOG.md | 8 + .../Models/MethodLevelChangesSummaryTests.cs | 66 ++++ .../Services/AssemblyMethodAnalyzerTests.cs | 69 +++++ .../HtmlReportGenerateServiceTests.cs | 90 ++++++ .../Services/ReportGenerateServiceTests.cs | 114 +++++++ Models/ConfigSettings.cs | 10 + Models/FileDiffResultLists.cs | 8 + Models/MethodLevelChangesSummary.cs | 66 ++++ README.md | 10 + Services/AssemblyMethodAnalyzer.cs | 288 ++++++++++++++++++ Services/FileDiffService.cs | 33 ++ .../HtmlReportGenerateService.Css.cs | 7 +- .../HtmlReportGenerateService.Sections.cs | 74 +++++ .../ReportGenerateService.SectionWriters.cs | 85 ++++++ Services/ReportGenerateService.cs | 2 + doc/DEVELOPER_GUIDE.md | 2 + doc/samples/diff_report.html | 19 ++ doc/samples/diff_report.md | 30 ++ 18 files changed, 980 insertions(+), 1 deletion(-) create mode 100644 FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs create mode 100644 FolderDiffIL4DotNet.Tests/Services/AssemblyMethodAnalyzerTests.cs create mode 100644 Models/MethodLevelChangesSummary.cs create mode 100644 Services/AssemblyMethodAnalyzer.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index b8f800de..6b1b679e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### [Unreleased] +#### Added + +- Added method-level change detection for `ILMismatch` assemblies using `System.Reflection.Metadata`. For each modified .NET assembly, the report now shows type/method/property/field additions, removals, and method body changes. This new **Method-Level Changes** section appears between **Summary** and **IL Cache Stats** in the Markdown report, and as an expandable inline row above the IL diff in the HTML report. Controlled by the new `ShouldIncludeMethodLevelChangesInReport` config setting (default: `true`). Added [`AssemblyMethodAnalyzer`](Services/AssemblyMethodAnalyzer.cs), [`MethodLevelChangesSummary`](Models/MethodLevelChangesSummary.cs), and corresponding tests. + ### [1.4.1] - 2026-03-20 #### Added @@ -365,6 +369,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### [Unreleased] +#### 追加 + +- `System.Reflection.Metadata` を使用した `ILMismatch` アセンブリのメソッドレベル変更検出を追加。変更のあった各 .NET アセンブリについて、型・メソッド・プロパティ・フィールドの増減およびメソッドボディの変更をレポートに出力します。Markdown レポートでは **Summary** と **IL Cache Stats** の間に **Method-Level Changes** セクションとして表示され、HTML レポートでは IL diff の上に展開可能なインライン行として表示されます。新しい設定項目 `ShouldIncludeMethodLevelChangesInReport`(既定: `true`)で制御可能。[`AssemblyMethodAnalyzer`](Services/AssemblyMethodAnalyzer.cs)、[`MethodLevelChangesSummary`](Models/MethodLevelChangesSummary.cs)、および対応するテストを追加。 + ### [1.4.1] - 2026-03-20 #### 追加 diff --git a/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs b/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs new file mode 100644 index 00000000..d85d3166 --- /dev/null +++ b/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using FolderDiffIL4DotNet.Models; +using Xunit; + +namespace FolderDiffIL4DotNet.Tests.Models +{ + public sealed class MethodLevelChangesSummaryTests + { + [Fact] + public void HasChanges_DefaultInstance_ReturnsFalse() + { + var summary = new MethodLevelChangesSummary(); + Assert.False(summary.HasChanges); + } + + [Fact] + public void HasChanges_WithAddedMethods_ReturnsTrue() + { + var summary = new MethodLevelChangesSummary + { + AddedMethods = new List { "[public] Foo::Bar() : void" }, + }; + Assert.True(summary.HasChanges); + } + + [Fact] + public void HasChanges_WithRemovedTypes_ReturnsTrue() + { + var summary = new MethodLevelChangesSummary + { + RemovedTypes = new List { "MyApp.OldService" }, + }; + Assert.True(summary.HasChanges); + } + + [Fact] + public void HasChanges_WithBodyChangedMethods_ReturnsTrue() + { + var summary = new MethodLevelChangesSummary + { + BodyChangedMethods = new List { "[public] Foo::Run() : void" }, + }; + Assert.True(summary.HasChanges); + } + + [Fact] + public void HasChanges_WithAddedProperties_ReturnsTrue() + { + var summary = new MethodLevelChangesSummary + { + AddedProperties = new List { "Foo::Name" }, + }; + Assert.True(summary.HasChanges); + } + + [Fact] + public void HasChanges_WithRemovedFields_ReturnsTrue() + { + var summary = new MethodLevelChangesSummary + { + RemovedFields = new List { "Foo::_bar" }, + }; + Assert.True(summary.HasChanges); + } + } +} diff --git a/FolderDiffIL4DotNet.Tests/Services/AssemblyMethodAnalyzerTests.cs b/FolderDiffIL4DotNet.Tests/Services/AssemblyMethodAnalyzerTests.cs new file mode 100644 index 00000000..60a2462d --- /dev/null +++ b/FolderDiffIL4DotNet.Tests/Services/AssemblyMethodAnalyzerTests.cs @@ -0,0 +1,69 @@ +using FolderDiffIL4DotNet.Services; +using Xunit; + +namespace FolderDiffIL4DotNet.Tests.Services +{ + public sealed class AssemblyMethodAnalyzerTests + { + [Fact] + public void Analyze_SameAssembly_NoChanges() + { + // Compare a real assembly to itself — should report no changes + // 実アセンブリを自分自身と比較 — 変更なしが期待される + var assemblyPath = typeof(AssemblyMethodAnalyzerTests).Assembly.Location; + var result = AssemblyMethodAnalyzer.Analyze(assemblyPath, assemblyPath); + + Assert.NotNull(result); + Assert.False(result.HasChanges); + Assert.Empty(result.AddedTypes); + Assert.Empty(result.RemovedTypes); + Assert.Empty(result.AddedMethods); + Assert.Empty(result.RemovedMethods); + Assert.Empty(result.BodyChangedMethods); + Assert.Empty(result.AddedProperties); + Assert.Empty(result.RemovedProperties); + Assert.Empty(result.AddedFields); + Assert.Empty(result.RemovedFields); + Assert.True(result.OldMethodCount > 0); + Assert.Equal(result.OldMethodCount, result.NewMethodCount); + } + + [Fact] + public void Analyze_NonExistentFile_ReturnsNull() + { + // Attempting to analyse a missing file should gracefully return null + // 存在しないファイルの解析は null を返すべき + var result = AssemblyMethodAnalyzer.Analyze("/nonexistent/old.dll", "/nonexistent/new.dll"); + Assert.Null(result); + } + + [Fact] + public void Analyze_InvalidFile_ReturnsNull() + { + // Attempting to analyse a non-PE file should gracefully return null + // PE でないファイルの解析は null を返すべき + var textFile = typeof(AssemblyMethodAnalyzerTests).Assembly.Location + ".runtimeconfig.json"; + if (!System.IO.File.Exists(textFile)) return; // skip if runtime config not available + var result = AssemblyMethodAnalyzer.Analyze(textFile, textFile); + Assert.Null(result); + } + + [Fact] + public void Analyze_DifferentAssemblies_DetectsChanges() + { + // Compare test assembly to main assembly — should detect differences + // テストアセンブリとメインアセンブリを比較 — 差異が検出されるべき + var testAssembly = typeof(AssemblyMethodAnalyzerTests).Assembly.Location; + var mainAssembly = typeof(FolderDiffIL4DotNet.Models.ConfigSettings).Assembly.Location; + + var result = AssemblyMethodAnalyzer.Analyze(testAssembly, mainAssembly); + + Assert.NotNull(result); + Assert.True(result.HasChanges); + // These are completely different assemblies, so there should be type/method differences + // 完全に異なるアセンブリなので、型やメソッドの差異があるはず + Assert.True(result.AddedTypes.Count > 0 || result.RemovedTypes.Count > 0 || + result.AddedMethods.Count > 0 || result.RemovedMethods.Count > 0); + } + } +} diff --git a/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs b/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs index 7c804234..2bb00895 100644 --- a/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs @@ -716,6 +716,96 @@ public void GenerateDiffReportHtml_LazyRender_JsSetupFunctionPresent() Assert.Contains("data-diff-html", html); // JS references the attribute name } + // ── Method-Level Changes / メソッドレベル変更 ───────────────────── + + [Fact] + public void GenerateDiffReportHtml_MethodLevelChanges_ShowsInlineAboveILDiff() + { + var (oldDir, newDir, reportDir) = MakeDirs("method-changes"); + + _resultLists.AddModifiedFileRelativePath("lib.dll"); + _resultLists.RecordDiffDetail("lib.dll", FileDiffResultLists.DiffDetailResult.ILMismatch, "dotnet-ildasm (version: 0.12.0)"); + + _resultLists.FileRelativePathToMethodLevelChanges["lib.dll"] = new MethodLevelChangesSummary + { + OldMethodCount = 10, + NewMethodCount = 12, + AddedMethods = new List { "[public] MyApp.Service::NewMethod(string) : void" }, + RemovedMethods = new List(), + BodyChangedMethods = new List { "[public] MyApp.Service::ExistingMethod(int) : bool" }, + AddedProperties = new List { "MyApp.Service::NewProp" }, + RemovedProperties = new List(), + AddedFields = new List(), + RemovedFields = new List { "MyApp.Service::_oldField" }, + AddedTypes = new List(), + RemovedTypes = new List(), + }; + + var config = CreateConfig(enableInlineDiff: true); + config.ShouldIncludeMethodLevelChangesInReport = true; + _service.GenerateDiffReportHtml(oldDir, newDir, reportDir, + appVersion: "1.0", elapsedTimeString: null, + computerName: "test-host", config); + + var html = File.ReadAllText(Path.Combine(reportDir, HtmlReportGenerateService.DIFF_REPORT_HTML_FILE_NAME)); + Assert.Contains("Show member changes", html); + Assert.Contains("methods_mod_0", html); + } + + [Fact] + public void GenerateDiffReportHtml_MethodLevelChanges_NotShownWhenDisabled() + { + var (oldDir, newDir, reportDir) = MakeDirs("method-changes-off"); + + _resultLists.AddModifiedFileRelativePath("lib.dll"); + _resultLists.RecordDiffDetail("lib.dll", FileDiffResultLists.DiffDetailResult.ILMismatch, "dotnet-ildasm (version: 0.12.0)"); + + _resultLists.FileRelativePathToMethodLevelChanges["lib.dll"] = new MethodLevelChangesSummary + { + OldMethodCount = 10, + NewMethodCount = 12, + AddedMethods = new List { "[public] MyApp.Service::NewMethod(string) : void" }, + }; + + var config = CreateConfig(enableInlineDiff: true); + config.ShouldIncludeMethodLevelChangesInReport = false; + _service.GenerateDiffReportHtml(oldDir, newDir, reportDir, + appVersion: "1.0", elapsedTimeString: null, + computerName: "test-host", config); + + var html = File.ReadAllText(Path.Combine(reportDir, HtmlReportGenerateService.DIFF_REPORT_HTML_FILE_NAME)); + Assert.DoesNotContain("Show member changes", html); + } + + [Fact] + public void GenerateDiffReportHtml_MethodLevelChanges_LazyRender_EncodesAsBase64() + { + var (oldDir, newDir, reportDir) = MakeDirs("method-changes-lazy"); + + _resultLists.AddModifiedFileRelativePath("lib.dll"); + _resultLists.RecordDiffDetail("lib.dll", FileDiffResultLists.DiffDetailResult.ILMismatch, "dotnet-ildasm (version: 0.12.0)"); + + _resultLists.FileRelativePathToMethodLevelChanges["lib.dll"] = new MethodLevelChangesSummary + { + OldMethodCount = 5, + NewMethodCount = 6, + AddedMethods = new List { "[public] Foo::Bar() : void" }, + }; + + var config = CreateConfig(enableInlineDiff: true, lazyRender: true); + config.ShouldIncludeMethodLevelChangesInReport = true; + _service.GenerateDiffReportHtml(oldDir, newDir, reportDir, + appVersion: "1.0", elapsedTimeString: null, + computerName: "test-host", config); + + var html = File.ReadAllText(Path.Combine(reportDir, HtmlReportGenerateService.DIFF_REPORT_HTML_FILE_NAME)); + // Should contain a data-diff-html attribute for the method changes row + Assert.Contains("methods_mod_0", html); + Assert.Contains("Show member changes", html); + // Content should NOT be inline (lazy rendered) + Assert.DoesNotContain("Foo::Bar()", html); + } + private static ConfigSettings CreateConfig(bool enableInlineDiff = true, bool lazyRender = false) => new() { IgnoredExtensions = new List(), diff --git a/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs b/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs index afe9997c..9c6d6d73 100644 --- a/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs @@ -754,6 +754,120 @@ public void GenerateDiffReport_WithIgnoredFilesNoneLocation_DoesNotBreakReport() Assert.True(File.Exists(Path.Combine(reportDir, "diff_report.md"))); } + // ── Method-Level Changes / メソッドレベル変更 ───────────────────── + + [Fact] + public void GenerateDiffReport_MethodLevelChanges_IncludedBetweenSummaryAndILCacheStats() + { + var oldDir = Path.Combine(_rootDir, "old-mlc"); + var newDir = Path.Combine(_rootDir, "new-mlc"); + var reportDir = Path.Combine(_rootDir, "report-mlc"); + Directory.CreateDirectory(oldDir); + Directory.CreateDirectory(newDir); + Directory.CreateDirectory(reportDir); + + _resultLists.AddModifiedFileRelativePath("src/App.dll"); + _resultLists.RecordDiffDetail("src/App.dll", FileDiffResultLists.DiffDetailResult.ILMismatch, "dotnet-ildasm (version: 0.12.0)"); + + var summary = new MethodLevelChangesSummary + { + OldMethodCount = 42, + NewMethodCount = 44, + AddedTypes = new List { "MyApp.NewService" }, + RemovedTypes = new List(), + AddedMethods = new List { "[public] MyApp.UserService::ValidateToken(string) : bool", "[internal] MyApp.UserService::RefreshSession(int) : void" }, + RemovedMethods = new List { "[public] MyApp.UserService::LegacyAuth(string) : void" }, + BodyChangedMethods = new List { "[public] MyApp.UserService::Login(string, string) : bool" }, + AddedProperties = new List { "MyApp.UserService::IsActive" }, + RemovedProperties = new List(), + AddedFields = new List { "MyApp.UserService::_cache" }, + RemovedFields = new List(), + }; + _resultLists.FileRelativePathToMethodLevelChanges["src/App.dll"] = summary; + + // Also add IL Cache Stats to verify ordering + var config = CreateConfig(); + config.ShouldIncludeILCacheStatsInReport = true; + var ilCache = new ILCache(ilCacheDirectoryAbsolutePath: string.Empty); + + _service.GenerateDiffReport( + oldDir, newDir, reportDir, + appVersion: "test", elapsedTimeString: null, computerName: "test-host", + config, ilCache); + + var reportText = File.ReadAllText(Path.Combine(reportDir, "diff_report.md")); + + // Content checks + Assert.Contains("## Method-Level Changes", reportText); + Assert.Contains("### src/App.dll", reportText); + Assert.Contains("- Types added (1):", reportText); + Assert.Contains("`MyApp.NewService`", reportText); + Assert.Contains("- Methods added (2):", reportText); + Assert.Contains("`[public] MyApp.UserService::ValidateToken(string) : bool`", reportText); + Assert.Contains("- Methods removed (1):", reportText); + Assert.Contains("- Methods with body changes (1):", reportText); + Assert.Contains("- Properties added (1):", reportText); + Assert.Contains("- Fields added (1):", reportText); + Assert.Contains("- Method count: 42 (old) → 44 (new)", reportText); + + // Ordering: Summary < Method-Level Changes < IL Cache Stats + int summaryIdx = reportText.IndexOf("## Summary", StringComparison.Ordinal); + int methodIdx = reportText.IndexOf("## Method-Level Changes", StringComparison.Ordinal); + int ilCacheIdx = reportText.IndexOf("## IL Cache Stats", StringComparison.Ordinal); + Assert.True(summaryIdx < methodIdx, "Method-Level Changes should appear after Summary"); + Assert.True(methodIdx < ilCacheIdx, "Method-Level Changes should appear before IL Cache Stats"); + } + + [Fact] + public void GenerateDiffReport_MethodLevelChanges_NotIncludedWhenDisabled() + { + var oldDir = Path.Combine(_rootDir, "old-mlc-off"); + var newDir = Path.Combine(_rootDir, "new-mlc-off"); + var reportDir = Path.Combine(_rootDir, "report-mlc-off"); + Directory.CreateDirectory(oldDir); + Directory.CreateDirectory(newDir); + Directory.CreateDirectory(reportDir); + + _resultLists.FileRelativePathToMethodLevelChanges["src/App.dll"] = new MethodLevelChangesSummary + { + OldMethodCount = 10, + NewMethodCount = 12, + AddedMethods = new List { "[public] Foo::Bar() : void" }, + }; + + var config = CreateConfig(); + config.ShouldIncludeMethodLevelChangesInReport = false; + _service.GenerateDiffReport( + oldDir, newDir, reportDir, + appVersion: "test", elapsedTimeString: null, computerName: "test-host", + config); + + var reportText = File.ReadAllText(Path.Combine(reportDir, "diff_report.md")); + Assert.DoesNotContain("## Method-Level Changes", reportText); + } + + [Fact] + public void GenerateDiffReport_MethodLevelChanges_NotIncludedWhenNoChanges() + { + var oldDir = Path.Combine(_rootDir, "old-mlc-empty"); + var newDir = Path.Combine(_rootDir, "new-mlc-empty"); + var reportDir = Path.Combine(_rootDir, "report-mlc-empty"); + Directory.CreateDirectory(oldDir); + Directory.CreateDirectory(newDir); + Directory.CreateDirectory(reportDir); + + // No method-level changes recorded + var config = CreateConfig(); + config.ShouldIncludeMethodLevelChangesInReport = true; + _service.GenerateDiffReport( + oldDir, newDir, reportDir, + appVersion: "test", elapsedTimeString: null, computerName: "test-host", + config); + + var reportText = File.ReadAllText(Path.Combine(reportDir, "diff_report.md")); + Assert.DoesNotContain("## Method-Level Changes", reportText); + } + private static ConfigSettings CreateConfig() => new() { IgnoredExtensions = new List(), diff --git a/Models/ConfigSettings.cs b/Models/ConfigSettings.cs index cfc5021a..90bb9eb7 100644 --- a/Models/ConfigSettings.cs +++ b/Models/ConfigSettings.cs @@ -97,6 +97,16 @@ public List TextFileExtensions /// public bool ShouldIncludeIgnoredFiles { get; set; } = true; + /// + /// Whether to include method-level change details (type/method/property/field additions, removals, + /// and method body changes) for ILMismatch assemblies in the diff report. + /// When true, a Method-Level Changes section is inserted between Summary and IL Cache Stats. + /// ILMismatch と判定された .NET アセンブリについて、メンバーレベルの変更詳細 + /// (型・メソッド・プロパティ・フィールドの増減およびメソッドボディの変更)をレポートに出力するかどうか。 + /// true の場合、Summary セクションと IL Cache Stats セクションの間に Method-Level Changes セクションを追加します。 + /// + public bool ShouldIncludeMethodLevelChangesInReport { get; set; } = true; + /// /// Whether to include IL cache statistics (hits, misses, hit rate, etc.) in the diff report. /// When true, an IL Cache Stats section is inserted between the Summary and Warnings sections. diff --git a/Models/FileDiffResultLists.cs b/Models/FileDiffResultLists.cs index 55c5575e..7f5bb8c7 100644 --- a/Models/FileDiffResultLists.cs +++ b/Models/FileDiffResultLists.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; namespace FolderDiffIL4DotNet.Models @@ -106,6 +107,12 @@ public sealed record DiffSummaryStatistics( /// public ConcurrentDictionary NewFileTimestampOlderThanOldWarnings { get; } = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + /// + /// Method-level change summaries for ILMismatch files, keyed by file relative path. + /// ILMismatch ファイルに対するメソッドレベル変更要約。キーはファイルの相対パス。 + /// + public ConcurrentDictionary FileRelativePathToMethodLevelChanges { get; } = new ConcurrentDictionary(StringComparer.Ordinal); + public bool HasAnyNewFileTimestampOlderThanOldWarning => !NewFileTimestampOlderThanOldWarnings.IsEmpty; /// @@ -167,6 +174,7 @@ public void ResetAll() DisassemblerToolVersions.Clear(); DisassemblerToolVersionsFromCache.Clear(); NewFileTimestampOlderThanOldWarnings.Clear(); + FileRelativePathToMethodLevelChanges.Clear(); } /// diff --git a/Models/MethodLevelChangesSummary.cs b/Models/MethodLevelChangesSummary.cs new file mode 100644 index 00000000..d7a9001d --- /dev/null +++ b/Models/MethodLevelChangesSummary.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; + +namespace FolderDiffIL4DotNet.Models +{ + /// + /// Summarises member-level changes detected between two builds of a .NET assembly, + /// including types, methods, properties, and fields. + /// .NET アセンブリの新旧ビルド間で検出されたメンバーレベルの変更要約を保持します。 + /// 型・メソッド・プロパティ・フィールドを含みます。 + /// + public sealed class MethodLevelChangesSummary + { + // ── Types / 型 ────────────────────────────────────────────────────── + + /// Types added in the new assembly. / 新アセンブリで追加された型。 + public IReadOnlyList AddedTypes { get; init; } = []; + + /// Types removed from the old assembly. / 旧アセンブリから削除された型。 + public IReadOnlyList RemovedTypes { get; init; } = []; + + // ── Methods / メソッド ─────────────────────────────────────────────── + + /// Total method count in the old assembly. / 旧アセンブリのメソッド総数。 + public int OldMethodCount { get; init; } + + /// Total method count in the new assembly. / 新アセンブリのメソッド総数。 + public int NewMethodCount { get; init; } + + /// Methods added in the new assembly (all access modifiers). / 新アセンブリで追加されたメソッド(全アクセス修飾子)。 + public IReadOnlyList AddedMethods { get; init; } = []; + + /// Methods removed from the old assembly (all access modifiers). / 旧アセンブリから削除されたメソッド(全アクセス修飾子)。 + public IReadOnlyList RemovedMethods { get; init; } = []; + + /// Methods whose IL body bytes differ between old and new. / IL ボディバイト列が新旧で異なるメソッド。 + public IReadOnlyList BodyChangedMethods { get; init; } = []; + + // ── Properties / プロパティ ────────────────────────────────────────── + + /// Properties added in the new assembly. / 新アセンブリで追加されたプロパティ。 + public IReadOnlyList AddedProperties { get; init; } = []; + + /// Properties removed from the old assembly. / 旧アセンブリから削除されたプロパティ。 + public IReadOnlyList RemovedProperties { get; init; } = []; + + // ── Fields / フィールド ────────────────────────────────────────────── + + /// Fields added in the new assembly. / 新アセンブリで追加されたフィールド。 + public IReadOnlyList AddedFields { get; init; } = []; + + /// Fields removed from the old assembly. / 旧アセンブリから削除されたフィールド。 + public IReadOnlyList RemovedFields { get; init; } = []; + + /// Whether any changes were detected. / 何らかの変更が検出されたかどうか。 + public bool HasChanges => + AddedTypes.Count > 0 || + RemovedTypes.Count > 0 || + AddedMethods.Count > 0 || + RemovedMethods.Count > 0 || + BodyChangedMethods.Count > 0 || + AddedProperties.Count > 0 || + RemovedProperties.Count > 0 || + AddedFields.Count > 0 || + RemovedFields.Count > 0; + } +} diff --git a/README.md b/README.md index ae9638c4..ad700840 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,11 @@ Override only the settings you want to change. For example: true Includes Ignored Files section before Unchanged. + + ShouldIncludeMethodLevelChangesInReport + true + When true, includes a Method-Level Changes section for ILMismatch assemblies between Summary and IL Cache Stats. Uses System.Reflection.Metadata to detect type/method/property/field additions, removals, and method body changes. In the HTML report, this appears as an expandable inline row above the IL diff. + ShouldIncludeILCacheStatsInReport false @@ -682,6 +687,11 @@ flowchart TD true レポートに Ignored Files セクションを出力するか。 + + ShouldIncludeMethodLevelChangesInReport + true + true の場合、ILMismatch と判定された .NET アセンブリについて、SummaryIL Cache Stats の間に Method-Level Changes セクションを出力します。System.Reflection.Metadata を使用して型・メソッド・プロパティ・フィールドの増減およびメソッドボディの変更を検出します。HTML レポートでは IL diff の上に展開可能なインライン行として表示されます。 + ShouldIncludeILCacheStatsInReport false diff --git a/Services/AssemblyMethodAnalyzer.cs b/Services/AssemblyMethodAnalyzer.cs new file mode 100644 index 00000000..da40c8d0 --- /dev/null +++ b/Services/AssemblyMethodAnalyzer.cs @@ -0,0 +1,288 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using FolderDiffIL4DotNet.Models; + +namespace FolderDiffIL4DotNet.Services +{ + /// + /// Compares two .NET assemblies at the metadata level using + /// to detect type, method, property, and field additions/removals and method body changes. + /// を使用して 2 つの .NET アセンブリのメタデータを比較し、 + /// 型・メソッド・プロパティ・フィールドの増減およびメソッドボディの変更を検出します。 + /// + internal static class AssemblyMethodAnalyzer + { + /// + /// Analyses two assembly files and returns a summary of member-level changes. + /// Returns if analysis fails (best-effort). + /// 2 つのアセンブリファイルを解析し、メンバーレベルの変更要約を返します。 + /// 解析に失敗した場合は を返します(ベストエフォート)。 + /// + public static MethodLevelChangesSummary? Analyze(string oldAssemblyPath, string newAssemblyPath) + { + try + { + var oldSnapshot = ReadAssemblySnapshot(oldAssemblyPath); + var newSnapshot = ReadAssemblySnapshot(newAssemblyPath); + + // Types + var addedTypes = newSnapshot.TypeNames.Except(oldSnapshot.TypeNames, StringComparer.Ordinal).OrderBy(t => t, StringComparer.Ordinal).ToList(); + var removedTypes = oldSnapshot.TypeNames.Except(newSnapshot.TypeNames, StringComparer.Ordinal).OrderBy(t => t, StringComparer.Ordinal).ToList(); + + // Methods + var addedMethods = newSnapshot.Methods.Keys.Except(oldSnapshot.Methods.Keys, StringComparer.Ordinal).OrderBy(m => m, StringComparer.Ordinal).ToList(); + var removedMethods = oldSnapshot.Methods.Keys.Except(newSnapshot.Methods.Keys, StringComparer.Ordinal).OrderBy(m => m, StringComparer.Ordinal).ToList(); + + var bodyChanged = new List(); + foreach (var key in oldSnapshot.Methods.Keys.Intersect(newSnapshot.Methods.Keys, StringComparer.Ordinal).OrderBy(m => m, StringComparer.Ordinal)) + { + if (!oldSnapshot.Methods[key].AsSpan().SequenceEqual(newSnapshot.Methods[key].AsSpan())) + { + bodyChanged.Add(key); + } + } + + // Properties + var addedProperties = newSnapshot.PropertyNames.Except(oldSnapshot.PropertyNames, StringComparer.Ordinal).OrderBy(p => p, StringComparer.Ordinal).ToList(); + var removedProperties = oldSnapshot.PropertyNames.Except(newSnapshot.PropertyNames, StringComparer.Ordinal).OrderBy(p => p, StringComparer.Ordinal).ToList(); + + // Fields + var addedFields = newSnapshot.FieldNames.Except(oldSnapshot.FieldNames, StringComparer.Ordinal).OrderBy(f => f, StringComparer.Ordinal).ToList(); + var removedFields = oldSnapshot.FieldNames.Except(newSnapshot.FieldNames, StringComparer.Ordinal).OrderBy(f => f, StringComparer.Ordinal).ToList(); + + return new MethodLevelChangesSummary + { + AddedTypes = addedTypes, + RemovedTypes = removedTypes, + OldMethodCount = oldSnapshot.Methods.Count, + NewMethodCount = newSnapshot.Methods.Count, + AddedMethods = addedMethods, + RemovedMethods = removedMethods, + BodyChangedMethods = bodyChanged, + AddedProperties = addedProperties, + RemovedProperties = removedProperties, + AddedFields = addedFields, + RemovedFields = removedFields, + }; + } +#pragma warning disable CA1031 // ベストエフォート解析のため全例外をキャッチ / Catch-all for best-effort analysis + catch + { + return null; + } +#pragma warning restore CA1031 + } + + // ── Internal snapshot ──────────────────────────────────────────────── + + private sealed class AssemblySnapshot + { + public HashSet TypeNames { get; } = new(StringComparer.Ordinal); + public Dictionary Methods { get; } = new(StringComparer.Ordinal); + public HashSet PropertyNames { get; } = new(StringComparer.Ordinal); + public HashSet FieldNames { get; } = new(StringComparer.Ordinal); + } + + private static AssemblySnapshot ReadAssemblySnapshot(string assemblyPath) + { + var snapshot = new AssemblySnapshot(); + using var stream = new FileStream(assemblyPath, FileMode.Open, FileAccess.Read, FileShare.Read); + using var peReader = new PEReader(stream); + var reader = peReader.GetMetadataReader(); + var typeProvider = new SimpleSignatureTypeProvider(reader); + + foreach (var typeHandle in reader.TypeDefinitions) + { + var typeDef = reader.GetTypeDefinition(typeHandle); + string typeName = GetFullTypeName(reader, typeDef); + + // Skip the special type + if (typeName == "") continue; + + snapshot.TypeNames.Add(typeName); + + // Methods + foreach (var methodHandle in typeDef.GetMethods()) + { + var methodDef = reader.GetMethodDefinition(methodHandle); + string methodKey = BuildMethodKey(reader, typeName, methodDef, typeProvider); + + byte[] ilBytes = ReadIlBytes(peReader, methodDef); + snapshot.Methods[methodKey] = ilBytes; + } + + // Properties + foreach (var propHandle in typeDef.GetProperties()) + { + var propDef = reader.GetPropertyDefinition(propHandle); + string propName = reader.GetString(propDef.Name); + string propKey = $"{typeName}::{propName}"; + snapshot.PropertyNames.Add(propKey); + } + + // Fields + foreach (var fieldHandle in typeDef.GetFields()) + { + var fieldDef = reader.GetFieldDefinition(fieldHandle); + string fieldName = reader.GetString(fieldDef.Name); + string fieldKey = $"{typeName}::{fieldName}"; + snapshot.FieldNames.Add(fieldKey); + } + } + + return snapshot; + } + + // ── Helpers ────────────────────────────────────────────────────────── + + private static string GetFullTypeName(MetadataReader reader, TypeDefinition typeDef) + { + string name = reader.GetString(typeDef.Name); + string ns = reader.GetString(typeDef.Namespace); + + // Handle nested types + if (typeDef.IsNested) + { + var declaringType = reader.GetTypeDefinition(typeDef.GetDeclaringType()); + string parentName = GetFullTypeName(reader, declaringType); + return $"{parentName}/{name}"; + } + + return string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}"; + } + + private static string BuildMethodKey(MetadataReader reader, string typeName, MethodDefinition methodDef, SimpleSignatureTypeProvider typeProvider) + { + string methodName = reader.GetString(methodDef.Name); + string accessModifier = GetAccessModifier(methodDef.Attributes); + + try + { + var sigBlobReader = reader.GetBlobReader(methodDef.Signature); + var decoder = new SignatureDecoder(typeProvider, reader, genericContext: null); + var signature = decoder.DecodeMethodSignature(ref sigBlobReader); + string parameters = string.Join(", ", signature.ParameterTypes); + string returnType = signature.ReturnType; + return $"[{accessModifier}] {typeName}::{methodName}({parameters}) : {returnType}"; + } +#pragma warning disable CA1031 // シグネチャデコード失敗時のフォールバック / Fallback when signature decoding fails + catch + { + // Fallback: use raw signature blob hex for uniqueness + var sigBytes = reader.GetBlobBytes(methodDef.Signature); + string sigHex = Convert.ToHexString(sigBytes); + return $"[{accessModifier}] {typeName}::{methodName}(#{sigHex})"; + } +#pragma warning restore CA1031 + } + + private static string GetAccessModifier(MethodAttributes attributes) + { + var access = attributes & MethodAttributes.MemberAccessMask; + return access switch + { + MethodAttributes.Public => "public", + MethodAttributes.Family => "protected", + MethodAttributes.FamORAssem => "protected internal", + MethodAttributes.Assembly => "internal", + MethodAttributes.FamANDAssem => "private protected", + MethodAttributes.Private => "private", + _ => "private" + }; + } + + private static byte[] ReadIlBytes(PEReader peReader, MethodDefinition methodDef) + { + if (methodDef.RelativeVirtualAddress == 0) return []; + + try + { + var body = peReader.GetMethodBody(methodDef.RelativeVirtualAddress); + return body.GetILBytes() ?? []; + } +#pragma warning disable CA1031 // ベストエフォートの IL バイト読み取り / Best-effort IL body read + catch + { + return []; + } +#pragma warning restore CA1031 + } + + // ── Signature type provider ────────────────────────────────────────── + + /// + /// Minimal that decodes + /// method parameter and return types into human-readable strings. + /// メソッドパラメータおよび戻り値の型を可読文字列にデコードする最小限の実装。 + /// + private sealed class SimpleSignatureTypeProvider : ISignatureTypeProvider + { + private readonly MetadataReader _reader; + + public SimpleSignatureTypeProvider(MetadataReader reader) => _reader = reader; + + public string GetPrimitiveType(PrimitiveTypeCode typeCode) + => typeCode switch + { + PrimitiveTypeCode.Void => "void", + PrimitiveTypeCode.Boolean => "bool", + PrimitiveTypeCode.Char => "char", + PrimitiveTypeCode.SByte => "sbyte", + PrimitiveTypeCode.Byte => "byte", + PrimitiveTypeCode.Int16 => "short", + PrimitiveTypeCode.UInt16 => "ushort", + PrimitiveTypeCode.Int32 => "int", + PrimitiveTypeCode.UInt32 => "uint", + PrimitiveTypeCode.Int64 => "long", + PrimitiveTypeCode.UInt64 => "ulong", + PrimitiveTypeCode.Single => "float", + PrimitiveTypeCode.Double => "double", + PrimitiveTypeCode.String => "string", + PrimitiveTypeCode.Object => "object", + PrimitiveTypeCode.IntPtr => "nint", + PrimitiveTypeCode.UIntPtr => "nuint", + PrimitiveTypeCode.TypedReference => "TypedReference", + _ => typeCode.ToString() + }; + + public string GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) + { + var typeDef = reader.GetTypeDefinition(handle); + return GetFullTypeName(reader, typeDef); + } + + public string GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) + { + var typeRef = reader.GetTypeReference(handle); + string ns = reader.GetString(typeRef.Namespace); + string name = reader.GetString(typeRef.Name); + return string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}"; + } + + public string GetTypeFromSpecification(MetadataReader reader, object? genericContext, TypeSpecificationHandle handle, byte rawTypeKind) + { + var sigReader = reader.GetBlobReader(reader.GetTypeSpecification(handle).Signature); + return new SignatureDecoder(this, reader, genericContext).DecodeType(ref sigReader); + } + + public string GetSZArrayType(string elementType) => $"{elementType}[]"; + public string GetPointerType(string elementType) => $"{elementType}*"; + public string GetByReferenceType(string elementType) => $"{elementType}&"; + public string GetGenericInstantiation(string genericType, ImmutableArray typeArguments) + => $"{genericType}<{string.Join(", ", typeArguments)}>"; + public string GetGenericMethodParameter(object? genericContext, int index) => $"!!{index}"; + public string GetGenericTypeParameter(object? genericContext, int index) => $"!{index}"; + public string GetPinnedType(string elementType) => elementType; + public string GetModifiedType(string modifier, string unmodifiedType, bool isRequired) => unmodifiedType; + public string GetArrayType(string elementType, ArrayShape shape) + => $"{elementType}[{new string(',', shape.Rank - 1)}]"; + public string GetFunctionPointerType(MethodSignature signature) => "delegate*"; + } + } +} diff --git a/Services/FileDiffService.cs b/Services/FileDiffService.cs index 04d45466..50f7f84e 100644 --- a/Services/FileDiffService.cs +++ b/Services/FileDiffService.cs @@ -125,6 +125,13 @@ public async Task FilesAreEqualAsync(string fileRelativePath, int maxParal fileRelativePath, areDotNetAssembliesEqual ? FileDiffResultLists.DiffDetailResult.ILMatch : FileDiffResultLists.DiffDetailResult.ILMismatch, disassemblerLabel); + + // Best-effort method-level analysis for ILMismatch assemblies + if (!areDotNetAssembliesEqual && _config.ShouldIncludeMethodLevelChangesInReport) + { + TryAnalyzeMethodLevelChanges(fileRelativePath, file1AbsolutePath, file2AbsolutePath); + } + return areDotNetAssembliesEqual; } catch (InvalidOperationException ex) @@ -234,6 +241,32 @@ public async Task FilesAreEqualAsync(string fileRelativePath, int maxParal } } + /// + /// Best-effort method-level analysis using System.Reflection.Metadata. + /// Failures are logged but do not affect the comparison result. + /// System.Reflection.Metadata を使用したベストエフォートのメソッドレベル解析。 + /// 失敗してもファイル比較結果には影響しません。 + /// + private void TryAnalyzeMethodLevelChanges(string fileRelativePath, string oldPath, string newPath) + { + try + { + var summary = AssemblyMethodAnalyzer.Analyze(oldPath, newPath); + if (summary?.HasChanges == true) + { + _fileDiffResultLists.FileRelativePathToMethodLevelChanges[fileRelativePath] = summary; + } + } +#pragma warning disable CA1031 // ベストエフォート解析のため全例外をキャッチ / Catch-all for best-effort analysis + catch (Exception ex) + { + _logger.LogMessage(AppLogLevel.Warning, + $"Method-level analysis failed for '{fileRelativePath}': {ex.Message}", + shouldOutputMessageToConsole: false, ex); + } +#pragma warning restore CA1031 + } + private void LogExpectedFileDiffFailure(string file1AbsolutePath, string file2AbsolutePath, Exception exception) { _logger.LogMessage( diff --git a/Services/HtmlReport/HtmlReportGenerateService.Css.cs b/Services/HtmlReport/HtmlReportGenerateService.Css.cs index 9fcc3517..7edd03d6 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Css.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Css.cs @@ -132,7 +132,12 @@ private static string GetCss() tr.diff-trunc-tr { background: #fffbdd; } td.diff-trunc-td { color: #735c0f; padding: 2px 8px; font-style: italic; } p.diff-skipped { color: #735c0f; font-size: 12px; padding: 4px 8px; - background: #fffbdd; margin: 0; }"; + background: #fffbdd; margin: 0; } + /* ── Method-level changes ──────────────────────────────────────────── */ + .method-changes { padding: 6px 12px; font-size: 12px; } + .method-changes p { margin: 4px 0 2px; } + .method-changes ul { margin: 0 0 4px 1.4rem; } + .method-changes li { margin-bottom: 1px; }"; } } } diff --git a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs index 1e2291a2..6ef721d0 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs @@ -208,6 +208,15 @@ private void AppendModifiedSection( string col6 = BuildDiffDetailDisplay(diffDetail); AppendFileRow(sb, "mod", idx, path, ts, col6, asm ?? ""); + // Method-level changes row (above IL diff) + if (config.ShouldIncludeMethodLevelChangesInReport && + diffDetail == FileDiffResultLists.DiffDetailResult.ILMismatch && + _fileDiffResultLists.FileRelativePathToMethodLevelChanges.TryGetValue(path, out var methodChanges) && + methodChanges.HasChanges) + { + AppendMethodLevelChangesRow(sb, idx, methodChanges, config); + } + if (config.EnableInlineDiff && (diffDetail == FileDiffResultLists.DiffDetailResult.TextMismatch || diffDetail == FileDiffResultLists.DiffDetailResult.ILMismatch)) @@ -337,6 +346,71 @@ private void AppendInlineDiffRow( sb.AppendLine(""); } + private void AppendMethodLevelChangesRow( + StringBuilder sb, + int idx, + MethodLevelChangesSummary summary, + ConfigSettings config, + string sectionPrefix = "mod") + { + int recordNo = idx + 1; + + int totalChanges = summary.AddedTypes.Count + summary.RemovedTypes.Count + + summary.AddedMethods.Count + summary.RemovedMethods.Count + + summary.BodyChangedMethods.Count + + summary.AddedProperties.Count + summary.RemovedProperties.Count + + summary.AddedFields.Count + summary.RemovedFields.Count; + + var contentBuilder = new StringBuilder(); + contentBuilder.AppendLine("
"); + + AppendMemberList(contentBuilder, "Types added", summary.AddedTypes); + AppendMemberList(contentBuilder, "Types removed", summary.RemovedTypes); + AppendMemberList(contentBuilder, "Methods added", summary.AddedMethods); + AppendMemberList(contentBuilder, "Methods removed", summary.RemovedMethods); + AppendMemberList(contentBuilder, "Methods with body changes", summary.BodyChangedMethods); + AppendMemberList(contentBuilder, "Properties added", summary.AddedProperties); + AppendMemberList(contentBuilder, "Properties removed", summary.RemovedProperties); + AppendMemberList(contentBuilder, "Fields added", summary.AddedFields); + AppendMemberList(contentBuilder, "Fields removed", summary.RemovedFields); + + contentBuilder.AppendLine($"

Method count: {summary.OldMethodCount} (old) → {summary.NewMethodCount} (new)

"); + contentBuilder.AppendLine("
"); + + string detailsId = $"methods_{sectionPrefix}_{idx}"; + string summaryLabel = $" #{recordNo} Show member changes ({totalChanges} change{(totalChanges == 1 ? "" : "s")})"; + string contentHtml = contentBuilder.ToString(); + + sb.AppendLine(""); + sb.AppendLine(" "); + if (config.InlineDiffLazyRender) + { + string b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(contentHtml)); + sb.AppendLine($"
"); + sb.AppendLine(summaryLabel); + sb.AppendLine("
"); + } + else + { + sb.AppendLine($"
"); + sb.AppendLine(summaryLabel); + sb.Append(contentHtml); + sb.AppendLine("
"); + } + sb.AppendLine(" "); + sb.AppendLine(""); + } + + private static void AppendMemberList(StringBuilder sb, string label, IReadOnlyList members) + { + if (members.Count == 0) return; + sb.AppendLine($"

{HtmlEncode(label)} ({members.Count}):

"); + sb.AppendLine("
    "); + foreach (var member in members) + sb.AppendLine($"
  • {HtmlEncode(member)}
  • "); + sb.AppendLine("
"); + } + private void AppendSummarySection(StringBuilder sb, ConfigSettings config) { sb.AppendLine("

Summary

"); diff --git a/Services/ReportGenerateService.SectionWriters.cs b/Services/ReportGenerateService.SectionWriters.cs index 1994ad63..04b602d0 100644 --- a/Services/ReportGenerateService.SectionWriters.cs +++ b/Services/ReportGenerateService.SectionWriters.cs @@ -191,6 +191,91 @@ public void Write(StreamWriter writer, ReportWriteContext ctx) } } + /// Writes the Method-Level Changes section for ILMismatch assemblies. / ILMismatch アセンブリのメソッドレベル変更セクションを書き込みます。 + private sealed class MethodLevelChangesSectionWriter : IReportSectionWriter + { + public void Write(StreamWriter writer, ReportWriteContext ctx) + { + if (!ctx.Config.ShouldIncludeMethodLevelChangesInReport) return; + var changes = ctx.FileDiffResultLists.FileRelativePathToMethodLevelChanges; + if (changes.IsEmpty) return; + + writer.WriteLine(REPORT_SECTION_METHOD_LEVEL_CHANGES); + + foreach (var (filePath, summary) in changes.OrderBy(kv => kv.Key, StringComparer.OrdinalIgnoreCase)) + { + writer.WriteLine($"\n### {filePath}"); + + if (summary.AddedTypes.Count > 0) + { + writer.WriteLine($"- Types added ({summary.AddedTypes.Count}):"); + foreach (var t in summary.AddedTypes) + writer.WriteLine($" - `{t}`"); + } + + if (summary.RemovedTypes.Count > 0) + { + writer.WriteLine($"- Types removed ({summary.RemovedTypes.Count}):"); + foreach (var t in summary.RemovedTypes) + writer.WriteLine($" - `{t}`"); + } + + if (summary.AddedMethods.Count > 0) + { + writer.WriteLine($"- Methods added ({summary.AddedMethods.Count}):"); + foreach (var m in summary.AddedMethods) + writer.WriteLine($" - `{m}`"); + } + + if (summary.RemovedMethods.Count > 0) + { + writer.WriteLine($"- Methods removed ({summary.RemovedMethods.Count}):"); + foreach (var m in summary.RemovedMethods) + writer.WriteLine($" - `{m}`"); + } + + if (summary.BodyChangedMethods.Count > 0) + { + writer.WriteLine($"- Methods with body changes ({summary.BodyChangedMethods.Count}):"); + foreach (var m in summary.BodyChangedMethods) + writer.WriteLine($" - `{m}`"); + } + + if (summary.AddedProperties.Count > 0) + { + writer.WriteLine($"- Properties added ({summary.AddedProperties.Count}):"); + foreach (var p in summary.AddedProperties) + writer.WriteLine($" - `{p}`"); + } + + if (summary.RemovedProperties.Count > 0) + { + writer.WriteLine($"- Properties removed ({summary.RemovedProperties.Count}):"); + foreach (var p in summary.RemovedProperties) + writer.WriteLine($" - `{p}`"); + } + + if (summary.AddedFields.Count > 0) + { + writer.WriteLine($"- Fields added ({summary.AddedFields.Count}):"); + foreach (var f in summary.AddedFields) + writer.WriteLine($" - `{f}`"); + } + + if (summary.RemovedFields.Count > 0) + { + writer.WriteLine($"- Fields removed ({summary.RemovedFields.Count}):"); + foreach (var f in summary.RemovedFields) + writer.WriteLine($" - `{f}`"); + } + + writer.WriteLine($"- Method count: {summary.OldMethodCount} (old) → {summary.NewMethodCount} (new)"); + } + + writer.WriteLine(); + } + } + /// Writes the IL Cache Stats section (only when enabled and ilCache is non-null). / IL Cache Stats セクションを書き込みます。 private sealed class ILCacheStatsSectionWriter : IReportSectionWriter { diff --git a/Services/ReportGenerateService.cs b/Services/ReportGenerateService.cs index a3d218cd..dd54e981 100644 --- a/Services/ReportGenerateService.cs +++ b/Services/ReportGenerateService.cs @@ -61,6 +61,7 @@ public ReportGenerateService(FileDiffResultLists fileDiffResultLists, ILoggerSer private const string REPORT_LOCATION_BOTH = "(old/new)"; private const string REPORT_TIMESTAMP_ARROW = " → "; private const string REPORT_SECTION_SUMMARY = REPORT_SECTION_PREFIX + "Summary"; + private const string REPORT_SECTION_METHOD_LEVEL_CHANGES = REPORT_SECTION_PREFIX + "Method-Level Changes"; private const string REPORT_SECTION_IL_CACHE_STATS = REPORT_SECTION_PREFIX + "IL Cache Stats"; private const string WARNING_NEW_FILE_TIMESTAMP_OLDER_THAN_OLD = "One or more **modified** files in `new` have older last-modified timestamps than the corresponding files in `old`."; private const string REPORT_SECTION_WARNINGS = REPORT_SECTION_PREFIX + "Warnings"; @@ -173,6 +174,7 @@ private void WriteDiffReport( new RemovedFilesSectionWriter(), new ModifiedFilesSectionWriter(), new SummarySectionWriter(), + new MethodLevelChangesSectionWriter(), new ILCacheStatsSectionWriter(), new WarningsSectionWriter(), }; diff --git a/doc/DEVELOPER_GUIDE.md b/doc/DEVELOPER_GUIDE.md index e97bfa4d..2cdc79de 100644 --- a/doc/DEVELOPER_GUIDE.md +++ b/doc/DEVELOPER_GUIDE.md @@ -283,6 +283,7 @@ Why this matters: | [`Services/FileDiffService.cs`](../Services/FileDiffService.cs) | Per-file decision tree | MD5 -> IL -> text -> fallback | | [`Services/IFileComparisonService.cs`](../Services/IFileComparisonService.cs) + [`Services/FileComparisonService.cs`](../Services/FileComparisonService.cs) | Per-file compare/detect I/O abstraction | Enables file-level unit tests | | [`Services/ILOutputService.cs`](../Services/ILOutputService.cs) | IL compare flow, line filtering, optional IL dump writing | Enforces same disassembler identity | +| [`Services/AssemblyMethodAnalyzer.cs`](../Services/AssemblyMethodAnalyzer.cs) | Method-level change detection via `System.Reflection.Metadata` | Best-effort; returns `null` on failure. Detects type/method/property/field additions and removals, and method body IL changes | | [`Services/DotNetDisassembleService.cs`](../Services/DotNetDisassembleService.cs) | Tool probing, disassembly execution, cache hit/store tracking, blacklist handling | Central tool boundary; delegates prefetch to [`ILCachePrefetcher`](../Services/ILCachePrefetcher.cs) | | [`Services/ILCachePrefetcher.cs`](../Services/ILCachePrefetcher.cs) | IL-cache prefetch (pre-hit verification for all candidate command/arg patterns) | Extracted from [`DotNetDisassembleService`](../Services/DotNetDisassembleService.cs); owns its own hit counter | | [`Services/DisassemblerHelper.cs`](../Services/DisassemblerHelper.cs) | Shared static helpers: command identification, candidate enumeration, executable path resolution | Used by both [`DotNetDisassembleService`](../Services/DotNetDisassembleService.cs) and [`ILCachePrefetcher`](../Services/ILCachePrefetcher.cs); no instance state | @@ -910,6 +911,7 @@ sequenceDiagram | [`Services/FileDiffService.cs`](../Services/FileDiffService.cs) | ファイル単位の判定木 | `MD5 -> IL -> text -> fallback` | | [`Services/IFileComparisonService.cs`](../Services/IFileComparisonService.cs) + [`Services/FileComparisonService.cs`](../Services/FileComparisonService.cs) | ファイル単位の比較/判定 I/O 抽象 | ファイル単位ユニットテスト向け | | [`Services/ILOutputService.cs`](../Services/ILOutputService.cs) | IL 比較、行除外、任意 IL 出力 | 同一逆アセンブラ制約を保証 | +| [`Services/AssemblyMethodAnalyzer.cs`](../Services/AssemblyMethodAnalyzer.cs) | `System.Reflection.Metadata` によるメソッドレベル変更検出 | ベストエフォート;失敗時は `null` を返す。型・メソッド・プロパティ・フィールドの増減およびメソッド本体の IL 変更を検出 | | [`Services/DotNetDisassembleService.cs`](../Services/DotNetDisassembleService.cs) | ツール探索、逆アセンブル実行、キャッシュヒット/ストア追跡、ブラックリスト | 外部ツール境界;プリフェッチは [`ILCachePrefetcher`](../Services/ILCachePrefetcher.cs) へ委譲 | | [`Services/ILCachePrefetcher.cs`](../Services/ILCachePrefetcher.cs) | IL キャッシュのプリフェッチ(全候補コマンド×引数パターンの事前ヒット確認) | [`DotNetDisassembleService`](../Services/DotNetDisassembleService.cs) から分離;独自のヒットカウンタを保持 | | [`Services/DisassemblerHelper.cs`](../Services/DisassemblerHelper.cs) | 共有静的ヘルパー:コマンド判定・候補列挙・実行ファイルパス解決 | [`DotNetDisassembleService`](../Services/DotNetDisassembleService.cs) と [`ILCachePrefetcher`](../Services/ILCachePrefetcher.cs) の両方が使用;インスタンス状態なし | diff --git a/doc/samples/diff_report.html b/doc/samples/diff_report.html index 32f76c2a..52ad5323 100644 --- a/doc/samples/diff_report.html +++ b/doc/samples/diff_report.html @@ -131,6 +131,11 @@ td.diff-trunc-td { color: #735c0f; padding: 2px 8px; font-style: italic; } p.diff-skipped { color: #735c0f; font-size: 12px; padding: 4px 8px; background: #fffbdd; margin: 0; } + /* ── Method-level changes ──────────────────────────────────────────── */ + .method-changes { padding: 6px 12px; font-size: 12px; } + .method-changes p { margin: 4px 0 2px; } + .method-changes ul { margin: 0 0 4px 1.4rem; } + .method-changes li { margin-bottom: 1px; } @@ -429,6 +434,13 @@

[ * ] Modified Files (8)

ILMismatch dotnet-ildasm (version: 0.12.2) + + +
+ #3 Show member changes (5 changes) +
+ +
@@ -463,6 +475,13 @@

[ * ] Modified Files (8)

ILMismatch dotnet-ildasm (version: 0.12.2) + + +
+ #5 Show member changes (9 changes) +
+ +
diff --git a/doc/samples/diff_report.md b/doc/samples/diff_report.md index 60768cf2..1bc5652d 100644 --- a/doc/samples/diff_report.md +++ b/doc/samples/diff_report.md @@ -51,6 +51,36 @@ - Modified : 8 - Compared : 17 (Old) vs 17 (New) +## Method-Level Changes + +### src/App.dll +- Methods added (1): + - `[public] MyApp.Controllers.ApiController::HealthCheck() : string` +- Methods with body changes (3): + - `[public] MyApp.Controllers.ApiController::GetUsers(int) : System.Collections.Generic.IList` + - `[internal] MyApp.Services.DataService::RefreshCache() : void` + - `[private] MyApp.Services.DataService::ValidateConnection(string) : bool` +- Properties added (1): + - `MyApp.Services.DataService::CacheTimeout` +- Method count: 28 (old) → 29 (new) + +### src/Service.dll +- Types added (1): + - `MyApp.Services.NewValidator` +- Methods added (4): + - `[public] MyApp.Services.NewValidator::.ctor() : void` + - `[public] MyApp.Services.NewValidator::Validate(string) : bool` + - `[private] MyApp.Services.NewValidator::ParseInput(string) : string` + - `[public] MyApp.Services.OrderService::ValidateWithNewValidator(string) : bool` +- Methods removed (1): + - `[public] MyApp.Services.OrderService::LegacyValidate(string) : bool` +- Methods with body changes (2): + - `[public] MyApp.Services.OrderService::ProcessOrder(int) : void` + - `[internal] MyApp.Services.OrderService::CalculateTotal(int, int) : decimal` +- Fields added (1): + - `MyApp.Services.NewValidator::_pattern` +- Method count: 15 (old) → 18 (new) + ## IL Cache Stats - Hits : 42 - Misses : 8 From 759e65e14e53eab6d5afac19461b9e0ed69f942a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 16:26:59 +0000 Subject: [PATCH 02/36] Refactor method-level changes to structured table format with MemberChangeEntry - Replace flat string lists in MethodLevelChangesSummary with structured MemberChangeEntry records (Change, TypeName, Access, MemberKind, MemberName, Details) - Rewrite AssemblyMethodAnalyzer to extract parameter names, default values, property accessors, field types, and access modifiers from PE metadata - Render method-level changes as tables (Assembly|Change|Class|Access|Kind|Name|Details) in both Markdown and HTML reports - Add "Other changes only" fallback when ILMismatch has no structural member changes - Update sample reports with table format, add util/Legacy.dll "Other" example - Remove unused System.Reflection.Metadata.Ecma335 import - Update all related tests for new MemberChangeEntry-based data model https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- .../Models/MethodLevelChangesSummaryTests.cs | 49 +-- .../Services/AssemblyMethodAnalyzerTests.cs | 35 +- .../HtmlReportGenerateServiceTests.cs | 32 +- .../Services/ReportGenerateServiceTests.cs | 40 +-- Models/MemberChangeEntry.cs | 24 ++ Models/MethodLevelChangesSummary.cs | 53 +-- Services/AssemblyMethodAnalyzer.cs | 321 +++++++++++++++--- .../HtmlReportGenerateService.Css.cs | 5 +- .../HtmlReportGenerateService.Sections.cs | 56 ++- .../ReportGenerateService.SectionWriters.cs | 71 +--- doc/samples/diff_report.html | 39 ++- doc/samples/diff_report.md | 53 +-- 12 files changed, 492 insertions(+), 286 deletions(-) create mode 100644 Models/MemberChangeEntry.cs diff --git a/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs b/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs index d85d3166..68ac5dd9 100644 --- a/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs +++ b/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs @@ -14,53 +14,40 @@ public void HasChanges_DefaultInstance_ReturnsFalse() } [Fact] - public void HasChanges_WithAddedMethods_ReturnsTrue() + public void HasChanges_WithEntries_ReturnsTrue() { var summary = new MethodLevelChangesSummary { - AddedMethods = new List { "[public] Foo::Bar() : void" }, + Entries = new List + { + new("+", "MyApp.Service", "public", "Method", "DoWork", "(int count) : void"), + }, }; Assert.True(summary.HasChanges); } [Fact] - public void HasChanges_WithRemovedTypes_ReturnsTrue() + public void HasChanges_EmptyEntries_ReturnsFalse() { var summary = new MethodLevelChangesSummary { - RemovedTypes = new List { "MyApp.OldService" }, + Entries = new List(), + OldMethodCount = 10, + NewMethodCount = 10, }; - Assert.True(summary.HasChanges); - } - - [Fact] - public void HasChanges_WithBodyChangedMethods_ReturnsTrue() - { - var summary = new MethodLevelChangesSummary - { - BodyChangedMethods = new List { "[public] Foo::Run() : void" }, - }; - Assert.True(summary.HasChanges); - } - - [Fact] - public void HasChanges_WithAddedProperties_ReturnsTrue() - { - var summary = new MethodLevelChangesSummary - { - AddedProperties = new List { "Foo::Name" }, - }; - Assert.True(summary.HasChanges); + Assert.False(summary.HasChanges); } [Fact] - public void HasChanges_WithRemovedFields_ReturnsTrue() + public void Entries_ContainStructuredData() { - var summary = new MethodLevelChangesSummary - { - RemovedFields = new List { "Foo::_bar" }, - }; - Assert.True(summary.HasChanges); + var entry = new MemberChangeEntry("+", "MyApp.Service", "public", "Method", "GetName", "(string id) : string"); + Assert.Equal("+", entry.Change); + Assert.Equal("MyApp.Service", entry.TypeName); + Assert.Equal("public", entry.Access); + Assert.Equal("Method", entry.MemberKind); + Assert.Equal("GetName", entry.MemberName); + Assert.Equal("(string id) : string", entry.Details); } } } diff --git a/FolderDiffIL4DotNet.Tests/Services/AssemblyMethodAnalyzerTests.cs b/FolderDiffIL4DotNet.Tests/Services/AssemblyMethodAnalyzerTests.cs index 60a2462d..8bafe2e9 100644 --- a/FolderDiffIL4DotNet.Tests/Services/AssemblyMethodAnalyzerTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/AssemblyMethodAnalyzerTests.cs @@ -1,3 +1,4 @@ +using System.Linq; using FolderDiffIL4DotNet.Services; using Xunit; @@ -15,15 +16,7 @@ public void Analyze_SameAssembly_NoChanges() Assert.NotNull(result); Assert.False(result.HasChanges); - Assert.Empty(result.AddedTypes); - Assert.Empty(result.RemovedTypes); - Assert.Empty(result.AddedMethods); - Assert.Empty(result.RemovedMethods); - Assert.Empty(result.BodyChangedMethods); - Assert.Empty(result.AddedProperties); - Assert.Empty(result.RemovedProperties); - Assert.Empty(result.AddedFields); - Assert.Empty(result.RemovedFields); + Assert.Empty(result.Entries); Assert.True(result.OldMethodCount > 0); Assert.Equal(result.OldMethodCount, result.NewMethodCount); } @@ -60,10 +53,26 @@ public void Analyze_DifferentAssemblies_DetectsChanges() Assert.NotNull(result); Assert.True(result.HasChanges); - // These are completely different assemblies, so there should be type/method differences - // 完全に異なるアセンブリなので、型やメソッドの差異があるはず - Assert.True(result.AddedTypes.Count > 0 || result.RemovedTypes.Count > 0 || - result.AddedMethods.Count > 0 || result.RemovedMethods.Count > 0); + Assert.True(result.Entries.Count > 0); + } + + [Fact] + public void Analyze_DifferentAssemblies_EntriesHaveStructuredData() + { + // Entries should contain structured MemberChangeEntry data + // エントリには構造化された MemberChangeEntry データが含まれるべき + var testAssembly = typeof(AssemblyMethodAnalyzerTests).Assembly.Location; + var mainAssembly = typeof(FolderDiffIL4DotNet.Models.ConfigSettings).Assembly.Location; + + var result = AssemblyMethodAnalyzer.Analyze(testAssembly, mainAssembly); + + Assert.NotNull(result); + var firstEntry = result.Entries.First(); + Assert.False(string.IsNullOrEmpty(firstEntry.Change)); + Assert.False(string.IsNullOrEmpty(firstEntry.TypeName)); + Assert.False(string.IsNullOrEmpty(firstEntry.MemberKind)); + Assert.Contains(firstEntry.Change, new[] { "+", "-", "~" }); + Assert.Contains(firstEntry.MemberKind, new[] { "Type", "Method", "Property", "Field" }); } } } diff --git a/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs b/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs index 2bb00895..b190611e 100644 --- a/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs @@ -730,15 +730,13 @@ public void GenerateDiffReportHtml_MethodLevelChanges_ShowsInlineAboveILDiff() { OldMethodCount = 10, NewMethodCount = 12, - AddedMethods = new List { "[public] MyApp.Service::NewMethod(string) : void" }, - RemovedMethods = new List(), - BodyChangedMethods = new List { "[public] MyApp.Service::ExistingMethod(int) : bool" }, - AddedProperties = new List { "MyApp.Service::NewProp" }, - RemovedProperties = new List(), - AddedFields = new List(), - RemovedFields = new List { "MyApp.Service::_oldField" }, - AddedTypes = new List(), - RemovedTypes = new List(), + Entries = new List + { + new("+", "MyApp.Service", "public", "Method", "NewMethod", "(string name) : void"), + new("~", "MyApp.Service", "public", "Method", "ExistingMethod", "(int id) : bool"), + new("+", "MyApp.Service", "public", "Property", "NewProp", ": string { get; set; }"), + new("-", "MyApp.Service", "private", "Field", "_oldField", ": int"), + }, }; var config = CreateConfig(enableInlineDiff: true); @@ -750,6 +748,7 @@ public void GenerateDiffReportHtml_MethodLevelChanges_ShowsInlineAboveILDiff() var html = File.ReadAllText(Path.Combine(reportDir, HtmlReportGenerateService.DIFF_REPORT_HTML_FILE_NAME)); Assert.Contains("Show member changes", html); Assert.Contains("methods_mod_0", html); + Assert.Contains("method-changes-table", html); } [Fact] @@ -764,7 +763,10 @@ public void GenerateDiffReportHtml_MethodLevelChanges_NotShownWhenDisabled() { OldMethodCount = 10, NewMethodCount = 12, - AddedMethods = new List { "[public] MyApp.Service::NewMethod(string) : void" }, + Entries = new List + { + new("+", "MyApp.Service", "public", "Method", "NewMethod", "(string name) : void"), + }, }; var config = CreateConfig(enableInlineDiff: true); @@ -789,7 +791,10 @@ public void GenerateDiffReportHtml_MethodLevelChanges_LazyRender_EncodesAsBase64 { OldMethodCount = 5, NewMethodCount = 6, - AddedMethods = new List { "[public] Foo::Bar() : void" }, + Entries = new List + { + new("+", "Foo", "public", "Method", "Bar", "() : void"), + }, }; var config = CreateConfig(enableInlineDiff: true, lazyRender: true); @@ -802,8 +807,9 @@ public void GenerateDiffReportHtml_MethodLevelChanges_LazyRender_EncodesAsBase64 // Should contain a data-diff-html attribute for the method changes row Assert.Contains("methods_mod_0", html); Assert.Contains("Show member changes", html); - // Content should NOT be inline (lazy rendered) - Assert.DoesNotContain("Foo::Bar()", html); + // Content should NOT be inline (lazy rendered) — table markup is base64-encoded + Assert.DoesNotContain("method-changes-table", html); + Assert.Contains("data-diff-html", html); } private static ConfigSettings CreateConfig(bool enableInlineDiff = true, bool lazyRender = false) => new() diff --git a/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs b/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs index 9c6d6d73..347cd937 100644 --- a/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs @@ -773,15 +773,16 @@ public void GenerateDiffReport_MethodLevelChanges_IncludedBetweenSummaryAndILCac { OldMethodCount = 42, NewMethodCount = 44, - AddedTypes = new List { "MyApp.NewService" }, - RemovedTypes = new List(), - AddedMethods = new List { "[public] MyApp.UserService::ValidateToken(string) : bool", "[internal] MyApp.UserService::RefreshSession(int) : void" }, - RemovedMethods = new List { "[public] MyApp.UserService::LegacyAuth(string) : void" }, - BodyChangedMethods = new List { "[public] MyApp.UserService::Login(string, string) : bool" }, - AddedProperties = new List { "MyApp.UserService::IsActive" }, - RemovedProperties = new List(), - AddedFields = new List { "MyApp.UserService::_cache" }, - RemovedFields = new List(), + Entries = new List + { + new("+", "MyApp.NewService", "", "Type", "", ""), + new("+", "MyApp.UserService", "public", "Method", "ValidateToken", "(string token) : bool"), + new("+", "MyApp.UserService", "internal", "Method", "RefreshSession", "(int userId) : void"), + new("-", "MyApp.UserService", "public", "Method", "LegacyAuth", "(string key) : void"), + new("~", "MyApp.UserService", "public", "Method", "Login", "(string user, string pass) : bool"), + new("+", "MyApp.UserService", "public", "Property", "IsActive", ": bool { get; set; }"), + new("+", "MyApp.UserService", "private", "Field", "_cache", ": object"), + }, }; _resultLists.FileRelativePathToMethodLevelChanges["src/App.dll"] = summary; @@ -797,17 +798,15 @@ public void GenerateDiffReport_MethodLevelChanges_IncludedBetweenSummaryAndILCac var reportText = File.ReadAllText(Path.Combine(reportDir, "diff_report.md")); - // Content checks + // Content checks — table format Assert.Contains("## Method-Level Changes", reportText); Assert.Contains("### src/App.dll", reportText); - Assert.Contains("- Types added (1):", reportText); - Assert.Contains("`MyApp.NewService`", reportText); - Assert.Contains("- Methods added (2):", reportText); - Assert.Contains("`[public] MyApp.UserService::ValidateToken(string) : bool`", reportText); - Assert.Contains("- Methods removed (1):", reportText); - Assert.Contains("- Methods with body changes (1):", reportText); - Assert.Contains("- Properties added (1):", reportText); - Assert.Contains("- Fields added (1):", reportText); + Assert.Contains("| Assembly | Change | Class | Access | Kind | Name | Details |", reportText); + Assert.Contains("| src/App.dll | + | MyApp.NewService | | Type | | |", reportText); + Assert.Contains("| src/App.dll | + | MyApp.UserService | public | Method | ValidateToken | (string token) : bool |", reportText); + Assert.Contains("| src/App.dll | ~ | MyApp.UserService | public | Method | Login |", reportText); + Assert.Contains("| src/App.dll | + | MyApp.UserService | public | Property | IsActive |", reportText); + Assert.Contains("| src/App.dll | + | MyApp.UserService | private | Field | _cache |", reportText); Assert.Contains("- Method count: 42 (old) → 44 (new)", reportText); // Ordering: Summary < Method-Level Changes < IL Cache Stats @@ -832,7 +831,10 @@ public void GenerateDiffReport_MethodLevelChanges_NotIncludedWhenDisabled() { OldMethodCount = 10, NewMethodCount = 12, - AddedMethods = new List { "[public] Foo::Bar() : void" }, + Entries = new List + { + new("+", "Foo", "public", "Method", "Bar", "() : void"), + }, }; var config = CreateConfig(); diff --git a/Models/MemberChangeEntry.cs b/Models/MemberChangeEntry.cs new file mode 100644 index 00000000..2f2cc79e --- /dev/null +++ b/Models/MemberChangeEntry.cs @@ -0,0 +1,24 @@ +namespace FolderDiffIL4DotNet.Models +{ + /// + /// Represents a single member-level change detected between two assembly builds. + /// 2 つのアセンブリビルド間で検出された単一のメンバーレベル変更を表します。 + /// + /// Change kind: "+" (added), "-" (removed), "~" (body changed). / 変更種別。 + /// Owning type name (or the type itself for Type entries). / 所属型名(Type エントリの場合は型名そのもの)。 + /// Access modifier. Empty for Type entries. / アクセス修飾子。Type の場合は空。 + /// Member kind: "Type", "Method", "Property", "Field". / メンバー種別。 + /// Member name. Empty for Type entries. / メンバー名。Type の場合は空。 + /// + /// Additional details: method signature with parameter defaults for methods, + /// type and default value for fields, type and accessor info for properties. + /// 追加詳細:メソッドならパラメータ既定値を含むシグネチャ、フィールドなら型と既定値、プロパティなら型とアクセサ情報。 + /// + public sealed record MemberChangeEntry( + string Change, + string TypeName, + string Access, + string MemberKind, + string MemberName, + string Details); +} diff --git a/Models/MethodLevelChangesSummary.cs b/Models/MethodLevelChangesSummary.cs index d7a9001d..bf3cb504 100644 --- a/Models/MethodLevelChangesSummary.cs +++ b/Models/MethodLevelChangesSummary.cs @@ -3,22 +3,15 @@ namespace FolderDiffIL4DotNet.Models { /// - /// Summarises member-level changes detected between two builds of a .NET assembly, - /// including types, methods, properties, and fields. + /// Summarises member-level changes detected between two builds of a .NET assembly. + /// Each change is represented as a structured . /// .NET アセンブリの新旧ビルド間で検出されたメンバーレベルの変更要約を保持します。 - /// 型・メソッド・プロパティ・フィールドを含みます。 + /// 各変更は構造化された として表現されます。 /// public sealed class MethodLevelChangesSummary { - // ── Types / 型 ────────────────────────────────────────────────────── - - /// Types added in the new assembly. / 新アセンブリで追加された型。 - public IReadOnlyList AddedTypes { get; init; } = []; - - /// Types removed from the old assembly. / 旧アセンブリから削除された型。 - public IReadOnlyList RemovedTypes { get; init; } = []; - - // ── Methods / メソッド ─────────────────────────────────────────────── + /// All detected member-level changes. / 検出されたすべてのメンバーレベル変更。 + public IReadOnlyList Entries { get; init; } = []; /// Total method count in the old assembly. / 旧アセンブリのメソッド総数。 public int OldMethodCount { get; init; } @@ -26,41 +19,7 @@ public sealed class MethodLevelChangesSummary /// Total method count in the new assembly. / 新アセンブリのメソッド総数。 public int NewMethodCount { get; init; } - /// Methods added in the new assembly (all access modifiers). / 新アセンブリで追加されたメソッド(全アクセス修飾子)。 - public IReadOnlyList AddedMethods { get; init; } = []; - - /// Methods removed from the old assembly (all access modifiers). / 旧アセンブリから削除されたメソッド(全アクセス修飾子)。 - public IReadOnlyList RemovedMethods { get; init; } = []; - - /// Methods whose IL body bytes differ between old and new. / IL ボディバイト列が新旧で異なるメソッド。 - public IReadOnlyList BodyChangedMethods { get; init; } = []; - - // ── Properties / プロパティ ────────────────────────────────────────── - - /// Properties added in the new assembly. / 新アセンブリで追加されたプロパティ。 - public IReadOnlyList AddedProperties { get; init; } = []; - - /// Properties removed from the old assembly. / 旧アセンブリから削除されたプロパティ。 - public IReadOnlyList RemovedProperties { get; init; } = []; - - // ── Fields / フィールド ────────────────────────────────────────────── - - /// Fields added in the new assembly. / 新アセンブリで追加されたフィールド。 - public IReadOnlyList AddedFields { get; init; } = []; - - /// Fields removed from the old assembly. / 旧アセンブリから削除されたフィールド。 - public IReadOnlyList RemovedFields { get; init; } = []; - /// Whether any changes were detected. / 何らかの変更が検出されたかどうか。 - public bool HasChanges => - AddedTypes.Count > 0 || - RemovedTypes.Count > 0 || - AddedMethods.Count > 0 || - RemovedMethods.Count > 0 || - BodyChangedMethods.Count > 0 || - AddedProperties.Count > 0 || - RemovedProperties.Count > 0 || - AddedFields.Count > 0 || - RemovedFields.Count > 0; + public bool HasChanges => Entries.Count > 0; } } diff --git a/Services/AssemblyMethodAnalyzer.cs b/Services/AssemblyMethodAnalyzer.cs index da40c8d0..42810ccd 100644 --- a/Services/AssemblyMethodAnalyzer.cs +++ b/Services/AssemblyMethodAnalyzer.cs @@ -13,8 +13,10 @@ namespace FolderDiffIL4DotNet.Services /// /// Compares two .NET assemblies at the metadata level using /// to detect type, method, property, and field additions/removals and method body changes. + /// Returns structured records for table-style rendering. /// を使用して 2 つの .NET アセンブリのメタデータを比較し、 /// 型・メソッド・プロパティ・フィールドの増減およびメソッドボディの変更を検出します。 + /// 表形式レンダリング向けの構造化 レコードを返します。 /// internal static class AssemblyMethodAnalyzer { @@ -31,44 +33,63 @@ internal static class AssemblyMethodAnalyzer var oldSnapshot = ReadAssemblySnapshot(oldAssemblyPath); var newSnapshot = ReadAssemblySnapshot(newAssemblyPath); + var entries = new List(); + // Types - var addedTypes = newSnapshot.TypeNames.Except(oldSnapshot.TypeNames, StringComparer.Ordinal).OrderBy(t => t, StringComparer.Ordinal).ToList(); - var removedTypes = oldSnapshot.TypeNames.Except(newSnapshot.TypeNames, StringComparer.Ordinal).OrderBy(t => t, StringComparer.Ordinal).ToList(); + foreach (var t in newSnapshot.TypeNames.Except(oldSnapshot.TypeNames, StringComparer.Ordinal).OrderBy(t => t, StringComparer.Ordinal)) + entries.Add(new MemberChangeEntry("+", t, "", "Type", "", "")); + foreach (var t in oldSnapshot.TypeNames.Except(newSnapshot.TypeNames, StringComparer.Ordinal).OrderBy(t => t, StringComparer.Ordinal)) + entries.Add(new MemberChangeEntry("-", t, "", "Type", "", "")); // Methods - var addedMethods = newSnapshot.Methods.Keys.Except(oldSnapshot.Methods.Keys, StringComparer.Ordinal).OrderBy(m => m, StringComparer.Ordinal).ToList(); - var removedMethods = oldSnapshot.Methods.Keys.Except(newSnapshot.Methods.Keys, StringComparer.Ordinal).OrderBy(m => m, StringComparer.Ordinal).ToList(); - - var bodyChanged = new List(); - foreach (var key in oldSnapshot.Methods.Keys.Intersect(newSnapshot.Methods.Keys, StringComparer.Ordinal).OrderBy(m => m, StringComparer.Ordinal)) + foreach (var key in newSnapshot.Methods.Keys.Except(oldSnapshot.Methods.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) + { + var m = newSnapshot.Methods[key]; + entries.Add(new MemberChangeEntry("+", m.TypeName, m.Access, "Method", m.MethodName, m.Details)); + } + foreach (var key in oldSnapshot.Methods.Keys.Except(newSnapshot.Methods.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { - if (!oldSnapshot.Methods[key].AsSpan().SequenceEqual(newSnapshot.Methods[key].AsSpan())) + var m = oldSnapshot.Methods[key]; + entries.Add(new MemberChangeEntry("-", m.TypeName, m.Access, "Method", m.MethodName, m.Details)); + } + foreach (var key in oldSnapshot.Methods.Keys.Intersect(newSnapshot.Methods.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) + { + if (!oldSnapshot.Methods[key].IlBytes.AsSpan().SequenceEqual(newSnapshot.Methods[key].IlBytes.AsSpan())) { - bodyChanged.Add(key); + var m = newSnapshot.Methods[key]; + entries.Add(new MemberChangeEntry("~", m.TypeName, m.Access, "Method", m.MethodName, m.Details)); } } // Properties - var addedProperties = newSnapshot.PropertyNames.Except(oldSnapshot.PropertyNames, StringComparer.Ordinal).OrderBy(p => p, StringComparer.Ordinal).ToList(); - var removedProperties = oldSnapshot.PropertyNames.Except(newSnapshot.PropertyNames, StringComparer.Ordinal).OrderBy(p => p, StringComparer.Ordinal).ToList(); + foreach (var key in newSnapshot.Properties.Keys.Except(oldSnapshot.Properties.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) + { + var p = newSnapshot.Properties[key]; + entries.Add(new MemberChangeEntry("+", p.TypeName, p.Access, "Property", p.PropertyName, p.Details)); + } + foreach (var key in oldSnapshot.Properties.Keys.Except(newSnapshot.Properties.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) + { + var p = oldSnapshot.Properties[key]; + entries.Add(new MemberChangeEntry("-", p.TypeName, p.Access, "Property", p.PropertyName, p.Details)); + } // Fields - var addedFields = newSnapshot.FieldNames.Except(oldSnapshot.FieldNames, StringComparer.Ordinal).OrderBy(f => f, StringComparer.Ordinal).ToList(); - var removedFields = oldSnapshot.FieldNames.Except(newSnapshot.FieldNames, StringComparer.Ordinal).OrderBy(f => f, StringComparer.Ordinal).ToList(); + foreach (var key in newSnapshot.Fields.Keys.Except(oldSnapshot.Fields.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) + { + var f = newSnapshot.Fields[key]; + entries.Add(new MemberChangeEntry("+", f.TypeName, f.Access, "Field", f.FieldName, f.Details)); + } + foreach (var key in oldSnapshot.Fields.Keys.Except(newSnapshot.Fields.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) + { + var f = oldSnapshot.Fields[key]; + entries.Add(new MemberChangeEntry("-", f.TypeName, f.Access, "Field", f.FieldName, f.Details)); + } return new MethodLevelChangesSummary { - AddedTypes = addedTypes, - RemovedTypes = removedTypes, + Entries = entries, OldMethodCount = oldSnapshot.Methods.Count, NewMethodCount = newSnapshot.Methods.Count, - AddedMethods = addedMethods, - RemovedMethods = removedMethods, - BodyChangedMethods = bodyChanged, - AddedProperties = addedProperties, - RemovedProperties = removedProperties, - AddedFields = addedFields, - RemovedFields = removedFields, }; } #pragma warning disable CA1031 // ベストエフォート解析のため全例外をキャッチ / Catch-all for best-effort analysis @@ -79,16 +100,43 @@ internal static class AssemblyMethodAnalyzer #pragma warning restore CA1031 } - // ── Internal snapshot ──────────────────────────────────────────────── + // ── Internal snapshot data ────────────────────────────────────────── + + private sealed class MethodDetail + { + public required string TypeName { get; init; } + public required string Access { get; init; } + public required string MethodName { get; init; } + public required string Details { get; init; } + public required byte[] IlBytes { get; init; } + } + + private sealed class PropertyDetail + { + public required string TypeName { get; init; } + public required string Access { get; init; } + public required string PropertyName { get; init; } + public required string Details { get; init; } + } + + private sealed class FieldDetail + { + public required string TypeName { get; init; } + public required string Access { get; init; } + public required string FieldName { get; init; } + public required string Details { get; init; } + } private sealed class AssemblySnapshot { public HashSet TypeNames { get; } = new(StringComparer.Ordinal); - public Dictionary Methods { get; } = new(StringComparer.Ordinal); - public HashSet PropertyNames { get; } = new(StringComparer.Ordinal); - public HashSet FieldNames { get; } = new(StringComparer.Ordinal); + public Dictionary Methods { get; } = new(StringComparer.Ordinal); + public Dictionary Properties { get; } = new(StringComparer.Ordinal); + public Dictionary Fields { get; } = new(StringComparer.Ordinal); } + // ── Snapshot construction ─────────────────────────────────────────── + private static AssemblySnapshot ReadAssemblySnapshot(string assemblyPath) { var snapshot = new AssemblySnapshot(); @@ -111,10 +159,20 @@ private static AssemblySnapshot ReadAssemblySnapshot(string assemblyPath) foreach (var methodHandle in typeDef.GetMethods()) { var methodDef = reader.GetMethodDefinition(methodHandle); - string methodKey = BuildMethodKey(reader, typeName, methodDef, typeProvider); - + string access = GetAccessModifier(methodDef.Attributes); + string methodName = reader.GetString(methodDef.Name); + string matchKey = BuildMethodMatchKey(reader, typeName, methodDef, typeProvider); + string details = BuildMethodDetails(reader, methodDef, typeProvider); byte[] ilBytes = ReadIlBytes(peReader, methodDef); - snapshot.Methods[methodKey] = ilBytes; + + snapshot.Methods[matchKey] = new MethodDetail + { + TypeName = typeName, + Access = access, + MethodName = methodName, + Details = details, + IlBytes = ilBytes, + }; } // Properties @@ -123,7 +181,16 @@ private static AssemblySnapshot ReadAssemblySnapshot(string assemblyPath) var propDef = reader.GetPropertyDefinition(propHandle); string propName = reader.GetString(propDef.Name); string propKey = $"{typeName}::{propName}"; - snapshot.PropertyNames.Add(propKey); + string propAccess = GetPropertyAccess(reader, propDef); + string propDetails = BuildPropertyDetails(reader, propDef, typeProvider); + + snapshot.Properties[propKey] = new PropertyDetail + { + TypeName = typeName, + Access = propAccess, + PropertyName = propName, + Details = propDetails, + }; } // Fields @@ -132,7 +199,16 @@ private static AssemblySnapshot ReadAssemblySnapshot(string assemblyPath) var fieldDef = reader.GetFieldDefinition(fieldHandle); string fieldName = reader.GetString(fieldDef.Name); string fieldKey = $"{typeName}::{fieldName}"; - snapshot.FieldNames.Add(fieldKey); + string fieldAccess = GetFieldAccessModifier(fieldDef.Attributes); + string fieldDetails = BuildFieldDetails(reader, fieldDef, typeProvider); + + snapshot.Fields[fieldKey] = new FieldDetail + { + TypeName = typeName, + Access = fieldAccess, + FieldName = fieldName, + Details = fieldDetails, + }; } } @@ -146,7 +222,6 @@ private static string GetFullTypeName(MetadataReader reader, TypeDefinition type string name = reader.GetString(typeDef.Name); string ns = reader.GetString(typeDef.Namespace); - // Handle nested types if (typeDef.IsNested) { var declaringType = reader.GetTypeDefinition(typeDef.GetDeclaringType()); @@ -157,10 +232,10 @@ private static string GetFullTypeName(MetadataReader reader, TypeDefinition type return string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}"; } - private static string BuildMethodKey(MetadataReader reader, string typeName, MethodDefinition methodDef, SimpleSignatureTypeProvider typeProvider) + /// Build a key for method matching (without access modifier so access changes don't cause false add/remove pairs). + private static string BuildMethodMatchKey(MetadataReader reader, string typeName, MethodDefinition methodDef, SimpleSignatureTypeProvider typeProvider) { string methodName = reader.GetString(methodDef.Name); - string accessModifier = GetAccessModifier(methodDef.Attributes); try { @@ -168,20 +243,136 @@ private static string BuildMethodKey(MetadataReader reader, string typeName, Met var decoder = new SignatureDecoder(typeProvider, reader, genericContext: null); var signature = decoder.DecodeMethodSignature(ref sigBlobReader); string parameters = string.Join(", ", signature.ParameterTypes); - string returnType = signature.ReturnType; - return $"[{accessModifier}] {typeName}::{methodName}({parameters}) : {returnType}"; + return $"{typeName}::{methodName}({parameters}) : {signature.ReturnType}"; } #pragma warning disable CA1031 // シグネチャデコード失敗時のフォールバック / Fallback when signature decoding fails catch { - // Fallback: use raw signature blob hex for uniqueness var sigBytes = reader.GetBlobBytes(methodDef.Signature); - string sigHex = Convert.ToHexString(sigBytes); - return $"[{accessModifier}] {typeName}::{methodName}(#{sigHex})"; + return $"{typeName}::{methodName}(#{Convert.ToHexString(sigBytes)})"; } #pragma warning restore CA1031 } + /// Build human-readable details for a method: "(Type paramName, Type paramName = defaultValue) : ReturnType". + private static string BuildMethodDetails(MetadataReader reader, MethodDefinition methodDef, SimpleSignatureTypeProvider typeProvider) + { + try + { + var sigBlobReader = reader.GetBlobReader(methodDef.Signature); + var decoder = new SignatureDecoder(typeProvider, reader, genericContext: null); + var signature = decoder.DecodeMethodSignature(ref sigBlobReader); + + // Collect parameter metadata (names and default values) + var paramsBySeq = new Dictionary(); + foreach (var paramHandle in methodDef.GetParameters()) + { + var param = reader.GetParameter(paramHandle); + if (param.SequenceNumber > 0) + paramsBySeq[param.SequenceNumber] = param; + } + + var parts = new List(); + for (int i = 0; i < signature.ParameterTypes.Length; i++) + { + string paramType = signature.ParameterTypes[i]; + string part; + + if (paramsBySeq.TryGetValue(i + 1, out var param)) + { + string paramName = reader.GetString(param.Name); + part = string.IsNullOrEmpty(paramName) ? paramType : $"{paramType} {paramName}"; + + var defaultHandle = param.GetDefaultValue(); + if (!defaultHandle.IsNil) + { + string defaultVal = ReadConstantValue(reader, defaultHandle); + if (!string.IsNullOrEmpty(defaultVal)) + part += $" = {defaultVal}"; + } + } + else + { + part = paramType; + } + parts.Add(part); + } + + return $"({string.Join(", ", parts)}) : {signature.ReturnType}"; + } +#pragma warning disable CA1031 + catch + { + return ""; + } +#pragma warning restore CA1031 + } + + /// Build property details: ": Type { get; set; }". + private static string BuildPropertyDetails(MetadataReader reader, PropertyDefinition propDef, SimpleSignatureTypeProvider typeProvider) + { + try + { + var sigBlobReader = reader.GetBlobReader(propDef.Signature); + var decoder = new SignatureDecoder(typeProvider, reader, genericContext: null); + var signature = decoder.DecodeMethodSignature(ref sigBlobReader); + string propType = signature.ReturnType; + + var accessors = propDef.GetAccessors(); + string accessorInfo = (!accessors.Getter.IsNil, !accessors.Setter.IsNil) switch + { + (true, true) => " { get; set; }", + (true, false) => " { get; }", + (false, true) => " { set; }", + _ => "" + }; + + return $": {propType}{accessorInfo}"; + } +#pragma warning disable CA1031 + catch + { + return ""; + } +#pragma warning restore CA1031 + } + + /// Build field details: ": Type" or ": Type = defaultValue". + private static string BuildFieldDetails(MetadataReader reader, FieldDefinition fieldDef, SimpleSignatureTypeProvider typeProvider) + { + try + { + string fieldType = fieldDef.DecodeSignature(typeProvider, null); + string result = $": {fieldType}"; + + var defaultHandle = fieldDef.GetDefaultValue(); + if (!defaultHandle.IsNil) + { + string defaultVal = ReadConstantValue(reader, defaultHandle); + if (!string.IsNullOrEmpty(defaultVal)) + result += $" = {defaultVal}"; + } + + return result; + } +#pragma warning disable CA1031 + catch + { + return ""; + } +#pragma warning restore CA1031 + } + + private static string GetPropertyAccess(MetadataReader reader, PropertyDefinition propDef) + { + var accessors = propDef.GetAccessors(); + if (!accessors.Getter.IsNil) + return GetAccessModifier(reader.GetMethodDefinition(accessors.Getter).Attributes); + if (!accessors.Setter.IsNil) + return GetAccessModifier(reader.GetMethodDefinition(accessors.Setter).Attributes); + return ""; + } + private static string GetAccessModifier(MethodAttributes attributes) { var access = attributes & MethodAttributes.MemberAccessMask; @@ -197,6 +388,21 @@ private static string GetAccessModifier(MethodAttributes attributes) }; } + private static string GetFieldAccessModifier(FieldAttributes attributes) + { + var access = attributes & FieldAttributes.FieldAccessMask; + return access switch + { + FieldAttributes.Public => "public", + FieldAttributes.Family => "protected", + FieldAttributes.FamORAssem => "protected internal", + FieldAttributes.Assembly => "internal", + FieldAttributes.FamANDAssem => "private protected", + FieldAttributes.Private => "private", + _ => "private" + }; + } + private static byte[] ReadIlBytes(PEReader peReader, MethodDefinition methodDef) { if (methodDef.RelativeVirtualAddress == 0) return []; @@ -214,6 +420,41 @@ private static byte[] ReadIlBytes(PEReader peReader, MethodDefinition methodDef) #pragma warning restore CA1031 } + private static string ReadConstantValue(MetadataReader reader, ConstantHandle handle) + { + if (handle.IsNil) return ""; + var constant = reader.GetConstant(handle); + var blobReader = reader.GetBlobReader(constant.Value); + + try + { + return constant.TypeCode switch + { + ConstantTypeCode.Boolean => blobReader.ReadBoolean() ? "true" : "false", + ConstantTypeCode.Char => $"'{(char)blobReader.ReadUInt16()}'", + ConstantTypeCode.SByte => blobReader.ReadSByte().ToString(), + ConstantTypeCode.Byte => blobReader.ReadByte().ToString(), + ConstantTypeCode.Int16 => blobReader.ReadInt16().ToString(), + ConstantTypeCode.UInt16 => blobReader.ReadUInt16().ToString(), + ConstantTypeCode.Int32 => blobReader.ReadInt32().ToString(), + ConstantTypeCode.UInt32 => blobReader.ReadUInt32().ToString(), + ConstantTypeCode.Int64 => blobReader.ReadInt64().ToString(), + ConstantTypeCode.UInt64 => blobReader.ReadUInt64().ToString(), + ConstantTypeCode.Single => blobReader.ReadSingle().ToString(), + ConstantTypeCode.Double => blobReader.ReadDouble().ToString(), + ConstantTypeCode.String => $"\"{blobReader.ReadSerializedString() ?? ""}\"", + ConstantTypeCode.NullReference => "null", + _ => "" + }; + } +#pragma warning disable CA1031 + catch + { + return ""; + } +#pragma warning restore CA1031 + } + // ── Signature type provider ────────────────────────────────────────── /// @@ -221,7 +462,7 @@ private static byte[] ReadIlBytes(PEReader peReader, MethodDefinition methodDef) /// method parameter and return types into human-readable strings. /// メソッドパラメータおよび戻り値の型を可読文字列にデコードする最小限の実装。 /// - private sealed class SimpleSignatureTypeProvider : ISignatureTypeProvider + internal sealed class SimpleSignatureTypeProvider : ISignatureTypeProvider { private readonly MetadataReader _reader; diff --git a/Services/HtmlReport/HtmlReportGenerateService.Css.cs b/Services/HtmlReport/HtmlReportGenerateService.Css.cs index 7edd03d6..dbda87c8 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Css.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Css.cs @@ -136,8 +136,9 @@ private static string GetCss() /* ── Method-level changes ──────────────────────────────────────────── */ .method-changes { padding: 6px 12px; font-size: 12px; } .method-changes p { margin: 4px 0 2px; } - .method-changes ul { margin: 0 0 4px 1.4rem; } - .method-changes li { margin-bottom: 1px; }"; + table.method-changes-table { width: auto; border-collapse: collapse; margin: 4px 0; font-size: 12px; } + table.method-changes-table th { padding: 3px 8px; border: 1px solid #ccc; background: #f6f8fa; font-size: 11px; text-align: left; white-space: nowrap; } + table.method-changes-table td { padding: 2px 8px; border: 1px solid #e0e0e0; font-size: 12px; white-space: nowrap; }"; } } } diff --git a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs index 6ef721d0..33855c59 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs @@ -211,10 +211,9 @@ private void AppendModifiedSection( // Method-level changes row (above IL diff) if (config.ShouldIncludeMethodLevelChangesInReport && diffDetail == FileDiffResultLists.DiffDetailResult.ILMismatch && - _fileDiffResultLists.FileRelativePathToMethodLevelChanges.TryGetValue(path, out var methodChanges) && - methodChanges.HasChanges) + _fileDiffResultLists.FileRelativePathToMethodLevelChanges.TryGetValue(path, out var methodChanges)) { - AppendMethodLevelChangesRow(sb, idx, methodChanges, config); + AppendMethodLevelChangesRow(sb, idx, path, methodChanges, config); } if (config.EnableInlineDiff && @@ -349,36 +348,41 @@ private void AppendInlineDiffRow( private void AppendMethodLevelChangesRow( StringBuilder sb, int idx, + string assemblyPath, MethodLevelChangesSummary summary, ConfigSettings config, string sectionPrefix = "mod") { int recordNo = idx + 1; - - int totalChanges = summary.AddedTypes.Count + summary.RemovedTypes.Count + - summary.AddedMethods.Count + summary.RemovedMethods.Count + - summary.BodyChangedMethods.Count + - summary.AddedProperties.Count + summary.RemovedProperties.Count + - summary.AddedFields.Count + summary.RemovedFields.Count; + int totalChanges = summary.Entries.Count; var contentBuilder = new StringBuilder(); contentBuilder.AppendLine("
"); - AppendMemberList(contentBuilder, "Types added", summary.AddedTypes); - AppendMemberList(contentBuilder, "Types removed", summary.RemovedTypes); - AppendMemberList(contentBuilder, "Methods added", summary.AddedMethods); - AppendMemberList(contentBuilder, "Methods removed", summary.RemovedMethods); - AppendMemberList(contentBuilder, "Methods with body changes", summary.BodyChangedMethods); - AppendMemberList(contentBuilder, "Properties added", summary.AddedProperties); - AppendMemberList(contentBuilder, "Properties removed", summary.RemovedProperties); - AppendMemberList(contentBuilder, "Fields added", summary.AddedFields); - AppendMemberList(contentBuilder, "Fields removed", summary.RemovedFields); - - contentBuilder.AppendLine($"

Method count: {summary.OldMethodCount} (old) → {summary.NewMethodCount} (new)

"); + if (summary.Entries.Count > 0) + { + contentBuilder.AppendLine(""); + contentBuilder.AppendLine(""); + contentBuilder.AppendLine(""); + foreach (var e in summary.Entries) + { + contentBuilder.AppendLine($""); + } + contentBuilder.AppendLine("
AssemblyChangeClassAccessKindNameDetails
{HtmlEncode(assemblyPath)}{HtmlEncode(e.Change)}{HtmlEncode(e.TypeName)}{HtmlEncode(e.Access)}{HtmlEncode(e.MemberKind)}{HtmlEncode(e.MemberName)}{HtmlEncode(e.Details)}
"); + } + else + { + contentBuilder.AppendLine("

Other changes only. See IL diff for details.

"); + } + + contentBuilder.AppendLine($"

Method count: {summary.OldMethodCount} (old) → {summary.NewMethodCount} (new)

"); contentBuilder.AppendLine("
"); string detailsId = $"methods_{sectionPrefix}_{idx}"; - string summaryLabel = $" #{recordNo} Show member changes ({totalChanges} change{(totalChanges == 1 ? "" : "s")})"; + string summaryText = totalChanges > 0 + ? $"#{recordNo} Show member changes ({totalChanges} change{(totalChanges == 1 ? "" : "s")})" + : $"#{recordNo} Show member changes (other changes only)"; + string summaryLabel = $" {HtmlEncode(summaryText)}"; string contentHtml = contentBuilder.ToString(); sb.AppendLine(""); @@ -401,16 +405,6 @@ private void AppendMethodLevelChangesRow( sb.AppendLine(""); } - private static void AppendMemberList(StringBuilder sb, string label, IReadOnlyList members) - { - if (members.Count == 0) return; - sb.AppendLine($"

{HtmlEncode(label)} ({members.Count}):

"); - sb.AppendLine("
    "); - foreach (var member in members) - sb.AppendLine($"
  • {HtmlEncode(member)}
  • "); - sb.AppendLine("
"); - } - private void AppendSummarySection(StringBuilder sb, ConfigSettings config) { sb.AppendLine("

Summary

"); diff --git a/Services/ReportGenerateService.SectionWriters.cs b/Services/ReportGenerateService.SectionWriters.cs index 04b602d0..c098adc6 100644 --- a/Services/ReportGenerateService.SectionWriters.cs +++ b/Services/ReportGenerateService.SectionWriters.cs @@ -206,67 +206,19 @@ public void Write(StreamWriter writer, ReportWriteContext ctx) { writer.WriteLine($"\n### {filePath}"); - if (summary.AddedTypes.Count > 0) + if (summary.Entries.Count > 0) { - writer.WriteLine($"- Types added ({summary.AddedTypes.Count}):"); - foreach (var t in summary.AddedTypes) - writer.WriteLine($" - `{t}`"); + writer.WriteLine(); + writer.WriteLine("| Assembly | Change | Class | Access | Kind | Name | Details |"); + writer.WriteLine("|----------|--------|-------|--------|------|------|---------|"); + foreach (var e in summary.Entries) + { + writer.WriteLine($"| {EscapeMdTable(filePath)} | {e.Change} | {EscapeMdTable(e.TypeName)} | {EscapeMdTable(e.Access)} | {e.MemberKind} | {EscapeMdTable(e.MemberName)} | {EscapeMdTable(e.Details)} |"); + } } - - if (summary.RemovedTypes.Count > 0) - { - writer.WriteLine($"- Types removed ({summary.RemovedTypes.Count}):"); - foreach (var t in summary.RemovedTypes) - writer.WriteLine($" - `{t}`"); - } - - if (summary.AddedMethods.Count > 0) - { - writer.WriteLine($"- Methods added ({summary.AddedMethods.Count}):"); - foreach (var m in summary.AddedMethods) - writer.WriteLine($" - `{m}`"); - } - - if (summary.RemovedMethods.Count > 0) - { - writer.WriteLine($"- Methods removed ({summary.RemovedMethods.Count}):"); - foreach (var m in summary.RemovedMethods) - writer.WriteLine($" - `{m}`"); - } - - if (summary.BodyChangedMethods.Count > 0) - { - writer.WriteLine($"- Methods with body changes ({summary.BodyChangedMethods.Count}):"); - foreach (var m in summary.BodyChangedMethods) - writer.WriteLine($" - `{m}`"); - } - - if (summary.AddedProperties.Count > 0) - { - writer.WriteLine($"- Properties added ({summary.AddedProperties.Count}):"); - foreach (var p in summary.AddedProperties) - writer.WriteLine($" - `{p}`"); - } - - if (summary.RemovedProperties.Count > 0) - { - writer.WriteLine($"- Properties removed ({summary.RemovedProperties.Count}):"); - foreach (var p in summary.RemovedProperties) - writer.WriteLine($" - `{p}`"); - } - - if (summary.AddedFields.Count > 0) - { - writer.WriteLine($"- Fields added ({summary.AddedFields.Count}):"); - foreach (var f in summary.AddedFields) - writer.WriteLine($" - `{f}`"); - } - - if (summary.RemovedFields.Count > 0) + else { - writer.WriteLine($"- Fields removed ({summary.RemovedFields.Count}):"); - foreach (var f in summary.RemovedFields) - writer.WriteLine($" - `{f}`"); + writer.WriteLine("- Other changes only. See IL diff for details."); } writer.WriteLine($"- Method count: {summary.OldMethodCount} (old) → {summary.NewMethodCount} (new)"); @@ -274,6 +226,9 @@ public void Write(StreamWriter writer, ReportWriteContext ctx) writer.WriteLine(); } + + /// Escape pipe characters for Markdown table cells. / Markdown テーブルセル用にパイプ文字をエスケープ。 + private static string EscapeMdTable(string value) => value.Replace("|", "\\|"); } /// Writes the IL Cache Stats section (only when enabled and ilCache is non-null). / IL Cache Stats セクションを書き込みます。 diff --git a/doc/samples/diff_report.html b/doc/samples/diff_report.html index 52ad5323..cb86c56f 100644 --- a/doc/samples/diff_report.html +++ b/doc/samples/diff_report.html @@ -134,8 +134,9 @@ /* ── Method-level changes ──────────────────────────────────────────── */ .method-changes { padding: 6px 12px; font-size: 12px; } .method-changes p { margin: 4px 0 2px; } - .method-changes ul { margin: 0 0 4px 1.4rem; } - .method-changes li { margin-bottom: 1px; } + table.method-changes-table { width: auto; border-collapse: collapse; margin: 4px 0; font-size: 12px; } + table.method-changes-table th { padding: 3px 8px; border: 1px solid #ccc; background: #f6f8fa; font-size: 11px; text-align: left; white-space: nowrap; } + table.method-changes-table td { padding: 2px 8px; border: 1px solid #e0e0e0; font-size: 12px; white-space: nowrap; } @@ -373,7 +374,7 @@

[ - ] Removed Files (1)

-

[ * ] Modified Files (8)

+

[ * ] Modified Files (9)

@@ -436,7 +437,7 @@

[ * ] Modified Files (8)

@@ -477,7 +478,7 @@

[ * ] Modified Files (8)

@@ -532,6 +533,30 @@

[ * ] Modified Files (8)

+ + + + + + + + + + + + + + + +
-
+
#3 Show member changes (5 changes)
-
+
#5 Show member changes (9 changes)

#8 Inline diff skipped: diff too large (12500 diff lines; limit is 10000). Increase InlineDiffMaxDiffLines in config to enable.

9util/Legacy.dll[2026-03-15 08:50:00 → 2026-03-15 09:01:00]ILMismatchdotnet-ildasm (version: 0.12.2)
+
+ #9 Show member changes (other changes only) +
+
+
+ #9 Show IL diff (+1 / -1) +
+

Summary

@@ -541,8 +566,8 @@

Summary

Unchanged5 Added1 Removed1 - Modified8 - Compared17 (Old) vs 17 (New) + Modified9 + Compared18 (Old) vs 18 (New)

IL Cache Stats

diff --git a/doc/samples/diff_report.md b/doc/samples/diff_report.md index 1bc5652d..f8f4e8b7 100644 --- a/doc/samples/diff_report.md +++ b/doc/samples/diff_report.md @@ -33,7 +33,7 @@ ## [ - ] Removed Files (1) - [ - ] /Users/UserA/workspace/old/legacy/old-tool.txt [2026-03-15 08:55:00] -## [ * ] Modified Files (8) +## [ * ] Modified Files (9) - [ * ] config/app.config [2026-03-15 08:56:00 → 2026-03-15 09:01:00] `TextMismatch` - [ * ] payload.bin [2026-03-15 08:59:00 → 2026-03-15 08:54:00] `MD5Mismatch` - [ * ] src/App.dll [2026-03-15 08:58:00 → 2026-03-15 09:02:00] `ILMismatch` `dotnet-ildasm (version: 0.12.2)` @@ -42,45 +42,48 @@ - [ * ] src/Utils.cs [2026-03-15 08:57:00 → 2026-03-15 09:03:00] `TextMismatch` - [ * ] src/BigSchema.cs [2026-03-15 08:55:00 → 2026-03-15 09:04:00] `TextMismatch` - [ * ] src/LargeConfig.xml [2026-03-15 08:54:00 → 2026-03-15 09:05:00] `TextMismatch` +- [ * ] util/Legacy.dll [2026-03-15 08:50:00 → 2026-03-15 09:01:00] `ILMismatch` `dotnet-ildasm (version: 0.12.2)` ## Summary - Ignored : 3 - Unchanged : 5 - Added : 1 - Removed : 1 -- Modified : 8 -- Compared : 17 (Old) vs 17 (New) +- Modified : 9 +- Compared : 18 (Old) vs 18 (New) ## Method-Level Changes ### src/App.dll -- Methods added (1): - - `[public] MyApp.Controllers.ApiController::HealthCheck() : string` -- Methods with body changes (3): - - `[public] MyApp.Controllers.ApiController::GetUsers(int) : System.Collections.Generic.IList` - - `[internal] MyApp.Services.DataService::RefreshCache() : void` - - `[private] MyApp.Services.DataService::ValidateConnection(string) : bool` -- Properties added (1): - - `MyApp.Services.DataService::CacheTimeout` + +| Assembly | Change | Class | Access | Kind | Name | Details | +|----------|--------|-------|--------|------|------|---------| +| src/App.dll | + | MyApp.Controllers.ApiController | public | Method | HealthCheck | () : string | +| src/App.dll | ~ | MyApp.Controllers.ApiController | public | Method | GetUsers | (int page) : System.Collections.Generic.IList\ | +| src/App.dll | ~ | MyApp.Services.DataService | internal | Method | RefreshCache | () : void | +| src/App.dll | ~ | MyApp.Services.DataService | private | Method | ValidateConnection | (string connStr) : bool | +| src/App.dll | + | MyApp.Services.DataService | public | Property | CacheTimeout | : int { get; set; } | - Method count: 28 (old) → 29 (new) ### src/Service.dll -- Types added (1): - - `MyApp.Services.NewValidator` -- Methods added (4): - - `[public] MyApp.Services.NewValidator::.ctor() : void` - - `[public] MyApp.Services.NewValidator::Validate(string) : bool` - - `[private] MyApp.Services.NewValidator::ParseInput(string) : string` - - `[public] MyApp.Services.OrderService::ValidateWithNewValidator(string) : bool` -- Methods removed (1): - - `[public] MyApp.Services.OrderService::LegacyValidate(string) : bool` -- Methods with body changes (2): - - `[public] MyApp.Services.OrderService::ProcessOrder(int) : void` - - `[internal] MyApp.Services.OrderService::CalculateTotal(int, int) : decimal` -- Fields added (1): - - `MyApp.Services.NewValidator::_pattern` + +| Assembly | Change | Class | Access | Kind | Name | Details | +|----------|--------|-------|--------|------|------|---------| +| src/Service.dll | + | MyApp.Services.NewValidator | | Type | | | +| src/Service.dll | + | MyApp.Services.NewValidator | public | Method | .ctor | () : void | +| src/Service.dll | + | MyApp.Services.NewValidator | public | Method | Validate | (string input) : bool | +| src/Service.dll | + | MyApp.Services.NewValidator | private | Method | ParseInput | (string raw) : string | +| src/Service.dll | + | MyApp.Services.OrderService | public | Method | ValidateWithNewValidator | (string data) : bool | +| src/Service.dll | - | MyApp.Services.OrderService | public | Method | LegacyValidate | (string data) : bool | +| src/Service.dll | ~ | MyApp.Services.OrderService | public | Method | ProcessOrder | (int orderId) : void | +| src/Service.dll | ~ | MyApp.Services.OrderService | internal | Method | CalculateTotal | (int qty, int price) : decimal | +| src/Service.dll | + | MyApp.Services.NewValidator | private | Field | _pattern | : string | - Method count: 15 (old) → 18 (new) +### util/Legacy.dll +- Other changes only. See IL diff for details. +- Method count: 8 (old) → 8 (new) + ## IL Cache Stats - Hits : 42 - Misses : 8 From 17ac9669d915cb127c3988bdaffffe1b51be9ea7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 16:38:50 +0000 Subject: [PATCH 03/36] Change symbols to words: Added/Removed/Modified, method count uses vs format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace +/-/~ change indicators with Added/Removed/Modified - Change method count format from "X (old) → Y (new)" to "X (Old) vs Y (New)" - Add backtick emphasis on Change column in Markdown output - Update HTML sample base64 data and MD sample for new format - Update all related tests https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- .../Models/MethodLevelChangesSummaryTests.cs | 6 ++-- .../HtmlReportGenerateServiceTests.cs | 12 +++---- .../Services/ReportGenerateServiceTests.cs | 28 +++++++-------- Services/AssemblyMethodAnalyzer.cs | 18 +++++----- .../HtmlReportGenerateService.Sections.cs | 2 +- .../ReportGenerateService.SectionWriters.cs | 4 +-- doc/samples/diff_report.html | 6 ++-- doc/samples/diff_report.md | 34 +++++++++---------- 8 files changed, 55 insertions(+), 55 deletions(-) diff --git a/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs b/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs index 68ac5dd9..734fa9bf 100644 --- a/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs +++ b/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs @@ -20,7 +20,7 @@ public void HasChanges_WithEntries_ReturnsTrue() { Entries = new List { - new("+", "MyApp.Service", "public", "Method", "DoWork", "(int count) : void"), + new("Added", "MyApp.Service", "public", "Method", "DoWork", "(int count) : void"), }, }; Assert.True(summary.HasChanges); @@ -41,8 +41,8 @@ public void HasChanges_EmptyEntries_ReturnsFalse() [Fact] public void Entries_ContainStructuredData() { - var entry = new MemberChangeEntry("+", "MyApp.Service", "public", "Method", "GetName", "(string id) : string"); - Assert.Equal("+", entry.Change); + var entry = new MemberChangeEntry("Added", "MyApp.Service", "public", "Method", "GetName", "(string id) : string"); + Assert.Equal("Added", entry.Change); Assert.Equal("MyApp.Service", entry.TypeName); Assert.Equal("public", entry.Access); Assert.Equal("Method", entry.MemberKind); diff --git a/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs b/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs index b190611e..0e82894c 100644 --- a/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs @@ -732,10 +732,10 @@ public void GenerateDiffReportHtml_MethodLevelChanges_ShowsInlineAboveILDiff() NewMethodCount = 12, Entries = new List { - new("+", "MyApp.Service", "public", "Method", "NewMethod", "(string name) : void"), - new("~", "MyApp.Service", "public", "Method", "ExistingMethod", "(int id) : bool"), - new("+", "MyApp.Service", "public", "Property", "NewProp", ": string { get; set; }"), - new("-", "MyApp.Service", "private", "Field", "_oldField", ": int"), + new("Added", "MyApp.Service", "public", "Method", "NewMethod", "(string name) : void"), + new("Modified", "MyApp.Service", "public", "Method", "ExistingMethod", "(int id) : bool"), + new("Added", "MyApp.Service", "public", "Property", "NewProp", ": string { get; set; }"), + new("Removed", "MyApp.Service", "private", "Field", "_oldField", ": int"), }, }; @@ -765,7 +765,7 @@ public void GenerateDiffReportHtml_MethodLevelChanges_NotShownWhenDisabled() NewMethodCount = 12, Entries = new List { - new("+", "MyApp.Service", "public", "Method", "NewMethod", "(string name) : void"), + new("Added", "MyApp.Service", "public", "Method", "NewMethod", "(string name) : void"), }, }; @@ -793,7 +793,7 @@ public void GenerateDiffReportHtml_MethodLevelChanges_LazyRender_EncodesAsBase64 NewMethodCount = 6, Entries = new List { - new("+", "Foo", "public", "Method", "Bar", "() : void"), + new("Added", "Foo", "public", "Method", "Bar", "() : void"), }, }; diff --git a/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs b/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs index 347cd937..9e1a6f90 100644 --- a/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs @@ -775,13 +775,13 @@ public void GenerateDiffReport_MethodLevelChanges_IncludedBetweenSummaryAndILCac NewMethodCount = 44, Entries = new List { - new("+", "MyApp.NewService", "", "Type", "", ""), - new("+", "MyApp.UserService", "public", "Method", "ValidateToken", "(string token) : bool"), - new("+", "MyApp.UserService", "internal", "Method", "RefreshSession", "(int userId) : void"), - new("-", "MyApp.UserService", "public", "Method", "LegacyAuth", "(string key) : void"), - new("~", "MyApp.UserService", "public", "Method", "Login", "(string user, string pass) : bool"), - new("+", "MyApp.UserService", "public", "Property", "IsActive", ": bool { get; set; }"), - new("+", "MyApp.UserService", "private", "Field", "_cache", ": object"), + new("Added", "MyApp.NewService", "", "Type", "", ""), + new("Added", "MyApp.UserService", "public", "Method", "ValidateToken", "(string token) : bool"), + new("Added", "MyApp.UserService", "internal", "Method", "RefreshSession", "(int userId) : void"), + new("Removed", "MyApp.UserService", "public", "Method", "LegacyAuth", "(string key) : void"), + new("Modified", "MyApp.UserService", "public", "Method", "Login", "(string user, string pass) : bool"), + new("Added", "MyApp.UserService", "public", "Property", "IsActive", ": bool { get; set; }"), + new("Added", "MyApp.UserService", "private", "Field", "_cache", ": object"), }, }; _resultLists.FileRelativePathToMethodLevelChanges["src/App.dll"] = summary; @@ -802,12 +802,12 @@ public void GenerateDiffReport_MethodLevelChanges_IncludedBetweenSummaryAndILCac Assert.Contains("## Method-Level Changes", reportText); Assert.Contains("### src/App.dll", reportText); Assert.Contains("| Assembly | Change | Class | Access | Kind | Name | Details |", reportText); - Assert.Contains("| src/App.dll | + | MyApp.NewService | | Type | | |", reportText); - Assert.Contains("| src/App.dll | + | MyApp.UserService | public | Method | ValidateToken | (string token) : bool |", reportText); - Assert.Contains("| src/App.dll | ~ | MyApp.UserService | public | Method | Login |", reportText); - Assert.Contains("| src/App.dll | + | MyApp.UserService | public | Property | IsActive |", reportText); - Assert.Contains("| src/App.dll | + | MyApp.UserService | private | Field | _cache |", reportText); - Assert.Contains("- Method count: 42 (old) → 44 (new)", reportText); + Assert.Contains("| src/App.dll | `Added` | MyApp.NewService | | Type | | |", reportText); + Assert.Contains("| src/App.dll | `Added` | MyApp.UserService | public | Method | ValidateToken | (string token) : bool |", reportText); + Assert.Contains("| src/App.dll | `Modified` | MyApp.UserService | public | Method | Login |", reportText); + Assert.Contains("| src/App.dll | `Added` | MyApp.UserService | public | Property | IsActive |", reportText); + Assert.Contains("| src/App.dll | `Added` | MyApp.UserService | private | Field | _cache |", reportText); + Assert.Contains("- Method count: 42 (Old) vs 44 (New)", reportText); // Ordering: Summary < Method-Level Changes < IL Cache Stats int summaryIdx = reportText.IndexOf("## Summary", StringComparison.Ordinal); @@ -833,7 +833,7 @@ public void GenerateDiffReport_MethodLevelChanges_NotIncludedWhenDisabled() NewMethodCount = 12, Entries = new List { - new("+", "Foo", "public", "Method", "Bar", "() : void"), + new("Added", "Foo", "public", "Method", "Bar", "() : void"), }, }; diff --git a/Services/AssemblyMethodAnalyzer.cs b/Services/AssemblyMethodAnalyzer.cs index 42810ccd..42f2d411 100644 --- a/Services/AssemblyMethodAnalyzer.cs +++ b/Services/AssemblyMethodAnalyzer.cs @@ -37,27 +37,27 @@ internal static class AssemblyMethodAnalyzer // Types foreach (var t in newSnapshot.TypeNames.Except(oldSnapshot.TypeNames, StringComparer.Ordinal).OrderBy(t => t, StringComparer.Ordinal)) - entries.Add(new MemberChangeEntry("+", t, "", "Type", "", "")); + entries.Add(new MemberChangeEntry("Added", t, "", "Type", "", "")); foreach (var t in oldSnapshot.TypeNames.Except(newSnapshot.TypeNames, StringComparer.Ordinal).OrderBy(t => t, StringComparer.Ordinal)) - entries.Add(new MemberChangeEntry("-", t, "", "Type", "", "")); + entries.Add(new MemberChangeEntry("Removed", t, "", "Type", "", "")); // Methods foreach (var key in newSnapshot.Methods.Keys.Except(oldSnapshot.Methods.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var m = newSnapshot.Methods[key]; - entries.Add(new MemberChangeEntry("+", m.TypeName, m.Access, "Method", m.MethodName, m.Details)); + entries.Add(new MemberChangeEntry("Added", m.TypeName, m.Access, "Method", m.MethodName, m.Details)); } foreach (var key in oldSnapshot.Methods.Keys.Except(newSnapshot.Methods.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var m = oldSnapshot.Methods[key]; - entries.Add(new MemberChangeEntry("-", m.TypeName, m.Access, "Method", m.MethodName, m.Details)); + entries.Add(new MemberChangeEntry("Removed", m.TypeName, m.Access, "Method", m.MethodName, m.Details)); } foreach (var key in oldSnapshot.Methods.Keys.Intersect(newSnapshot.Methods.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { if (!oldSnapshot.Methods[key].IlBytes.AsSpan().SequenceEqual(newSnapshot.Methods[key].IlBytes.AsSpan())) { var m = newSnapshot.Methods[key]; - entries.Add(new MemberChangeEntry("~", m.TypeName, m.Access, "Method", m.MethodName, m.Details)); + entries.Add(new MemberChangeEntry("Modified", m.TypeName, m.Access, "Method", m.MethodName, m.Details)); } } @@ -65,24 +65,24 @@ internal static class AssemblyMethodAnalyzer foreach (var key in newSnapshot.Properties.Keys.Except(oldSnapshot.Properties.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var p = newSnapshot.Properties[key]; - entries.Add(new MemberChangeEntry("+", p.TypeName, p.Access, "Property", p.PropertyName, p.Details)); + entries.Add(new MemberChangeEntry("Added", p.TypeName, p.Access, "Property", p.PropertyName, p.Details)); } foreach (var key in oldSnapshot.Properties.Keys.Except(newSnapshot.Properties.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var p = oldSnapshot.Properties[key]; - entries.Add(new MemberChangeEntry("-", p.TypeName, p.Access, "Property", p.PropertyName, p.Details)); + entries.Add(new MemberChangeEntry("Removed", p.TypeName, p.Access, "Property", p.PropertyName, p.Details)); } // Fields foreach (var key in newSnapshot.Fields.Keys.Except(oldSnapshot.Fields.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var f = newSnapshot.Fields[key]; - entries.Add(new MemberChangeEntry("+", f.TypeName, f.Access, "Field", f.FieldName, f.Details)); + entries.Add(new MemberChangeEntry("Added", f.TypeName, f.Access, "Field", f.FieldName, f.Details)); } foreach (var key in oldSnapshot.Fields.Keys.Except(newSnapshot.Fields.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var f = oldSnapshot.Fields[key]; - entries.Add(new MemberChangeEntry("-", f.TypeName, f.Access, "Field", f.FieldName, f.Details)); + entries.Add(new MemberChangeEntry("Removed", f.TypeName, f.Access, "Field", f.FieldName, f.Details)); } return new MethodLevelChangesSummary diff --git a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs index 33855c59..1328e174 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs @@ -375,7 +375,7 @@ private void AppendMethodLevelChangesRow( contentBuilder.AppendLine("

Other changes only. See IL diff for details.

"); } - contentBuilder.AppendLine($"

Method count: {summary.OldMethodCount} (old) → {summary.NewMethodCount} (new)

"); + contentBuilder.AppendLine($"

Method count: {summary.OldMethodCount} (Old) vs {summary.NewMethodCount} (New)

"); contentBuilder.AppendLine(""); string detailsId = $"methods_{sectionPrefix}_{idx}"; diff --git a/Services/ReportGenerateService.SectionWriters.cs b/Services/ReportGenerateService.SectionWriters.cs index c098adc6..1ca6f0ce 100644 --- a/Services/ReportGenerateService.SectionWriters.cs +++ b/Services/ReportGenerateService.SectionWriters.cs @@ -213,7 +213,7 @@ public void Write(StreamWriter writer, ReportWriteContext ctx) writer.WriteLine("|----------|--------|-------|--------|------|------|---------|"); foreach (var e in summary.Entries) { - writer.WriteLine($"| {EscapeMdTable(filePath)} | {e.Change} | {EscapeMdTable(e.TypeName)} | {EscapeMdTable(e.Access)} | {e.MemberKind} | {EscapeMdTable(e.MemberName)} | {EscapeMdTable(e.Details)} |"); + writer.WriteLine($"| {EscapeMdTable(filePath)} | `{e.Change}` | {EscapeMdTable(e.TypeName)} | {EscapeMdTable(e.Access)} | {e.MemberKind} | {EscapeMdTable(e.MemberName)} | {EscapeMdTable(e.Details)} |"); } } else @@ -221,7 +221,7 @@ public void Write(StreamWriter writer, ReportWriteContext ctx) writer.WriteLine("- Other changes only. See IL diff for details."); } - writer.WriteLine($"- Method count: {summary.OldMethodCount} (old) → {summary.NewMethodCount} (new)"); + writer.WriteLine($"- Method count: {summary.OldMethodCount} (Old) vs {summary.NewMethodCount} (New)"); } writer.WriteLine(); diff --git a/doc/samples/diff_report.html b/doc/samples/diff_report.html index cb86c56f..208a29f6 100644 --- a/doc/samples/diff_report.html +++ b/doc/samples/diff_report.html @@ -437,7 +437,7 @@

[ * ] Modified Files (9)

-
+
#3 Show member changes (5 changes)
@@ -478,7 +478,7 @@

[ * ] Modified Files (9)

-
+
#5 Show member changes (9 changes)
@@ -545,7 +545,7 @@

[ * ] Modified Files (9)

-
+
#9 Show member changes (other changes only)
diff --git a/doc/samples/diff_report.md b/doc/samples/diff_report.md index f8f4e8b7..5e79ba29 100644 --- a/doc/samples/diff_report.md +++ b/doc/samples/diff_report.md @@ -58,31 +58,31 @@ | Assembly | Change | Class | Access | Kind | Name | Details | |----------|--------|-------|--------|------|------|---------| -| src/App.dll | + | MyApp.Controllers.ApiController | public | Method | HealthCheck | () : string | -| src/App.dll | ~ | MyApp.Controllers.ApiController | public | Method | GetUsers | (int page) : System.Collections.Generic.IList\ | -| src/App.dll | ~ | MyApp.Services.DataService | internal | Method | RefreshCache | () : void | -| src/App.dll | ~ | MyApp.Services.DataService | private | Method | ValidateConnection | (string connStr) : bool | -| src/App.dll | + | MyApp.Services.DataService | public | Property | CacheTimeout | : int { get; set; } | -- Method count: 28 (old) → 29 (new) +| src/App.dll | `Added` | MyApp.Controllers.ApiController | public | Method | HealthCheck | () : string | +| src/App.dll | `Modified` | MyApp.Controllers.ApiController | public | Method | GetUsers | (int page) : System.Collections.Generic.IList\ | +| src/App.dll | `Modified` | MyApp.Services.DataService | internal | Method | RefreshCache | () : void | +| src/App.dll | `Modified` | MyApp.Services.DataService | private | Method | ValidateConnection | (string connStr) : bool | +| src/App.dll | `Added` | MyApp.Services.DataService | public | Property | CacheTimeout | : int { get; set; } | +- Method count: 28 (Old) vs 29 (New) ### src/Service.dll | Assembly | Change | Class | Access | Kind | Name | Details | |----------|--------|-------|--------|------|------|---------| -| src/Service.dll | + | MyApp.Services.NewValidator | | Type | | | -| src/Service.dll | + | MyApp.Services.NewValidator | public | Method | .ctor | () : void | -| src/Service.dll | + | MyApp.Services.NewValidator | public | Method | Validate | (string input) : bool | -| src/Service.dll | + | MyApp.Services.NewValidator | private | Method | ParseInput | (string raw) : string | -| src/Service.dll | + | MyApp.Services.OrderService | public | Method | ValidateWithNewValidator | (string data) : bool | -| src/Service.dll | - | MyApp.Services.OrderService | public | Method | LegacyValidate | (string data) : bool | -| src/Service.dll | ~ | MyApp.Services.OrderService | public | Method | ProcessOrder | (int orderId) : void | -| src/Service.dll | ~ | MyApp.Services.OrderService | internal | Method | CalculateTotal | (int qty, int price) : decimal | -| src/Service.dll | + | MyApp.Services.NewValidator | private | Field | _pattern | : string | -- Method count: 15 (old) → 18 (new) +| src/Service.dll | `Added` | MyApp.Services.NewValidator | | Type | | | +| src/Service.dll | `Added` | MyApp.Services.NewValidator | public | Method | .ctor | () : void | +| src/Service.dll | `Added` | MyApp.Services.NewValidator | public | Method | Validate | (string input) : bool | +| src/Service.dll | `Added` | MyApp.Services.NewValidator | private | Method | ParseInput | (string raw) : string | +| src/Service.dll | `Added` | MyApp.Services.OrderService | public | Method | ValidateWithNewValidator | (string data) : bool | +| src/Service.dll | `Removed` | MyApp.Services.OrderService | public | Method | LegacyValidate | (string data) : bool | +| src/Service.dll | `Modified` | MyApp.Services.OrderService | public | Method | ProcessOrder | (int orderId) : void | +| src/Service.dll | `Modified` | MyApp.Services.OrderService | internal | Method | CalculateTotal | (int qty, int price) : decimal | +| src/Service.dll | `Added` | MyApp.Services.NewValidator | private | Field | _pattern | : string | +- Method count: 15 (Old) vs 18 (New) ### util/Legacy.dll - Other changes only. See IL diff for details. -- Method count: 8 (old) → 8 (new) +- Method count: 8 (Old) vs 8 (New) ## IL Cache Stats - Hits : 42 From 49e736a65cf03c8b847371c24778246de3529887 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 17:01:45 +0000 Subject: [PATCH 04/36] Add Modifiers column, C# detail format, backtick emphasis, and README semantic changes section - Add Modifiers column to MemberChangeEntry and AssemblyMethodAnalyzer extracting static/abstract/virtual/override/sealed override/const/readonly from metadata - Change Detail format from (Type paramName) : ReturnType to ReturnType (Type paramName) matching C# declaration order - Rename Detail column header to Detail (ReturnType (Type paramName)) - Apply backtick emphasis to Change, Access, Modifiers, Kind columns in Markdown/HTML - Add bilingual Assembly Semantic Changes section to README with detection table and column legend - Fix AssemblyMethodAnalyzerTests to use Added/Removed/Modified instead of +/-/~ - Update all test data for new Modifiers parameter and detail format https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- CHANGELOG.md | 4 + .../Models/MethodLevelChangesSummaryTests.cs | 7 +- .../Services/AssemblyMethodAnalyzerTests.cs | 2 +- .../HtmlReportGenerateServiceTests.cs | 12 +-- .../Services/ReportGenerateServiceTests.cs | 28 +++---- Models/MemberChangeEntry.cs | 10 ++- README.md | 64 ++++++++++++++++ Services/AssemblyMethodAnalyzer.cs | 73 ++++++++++++++++--- .../HtmlReportGenerateService.Sections.cs | 4 +- .../ReportGenerateService.SectionWriters.cs | 6 +- 10 files changed, 166 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b1b679e..f5c6275a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - Added method-level change detection for `ILMismatch` assemblies using `System.Reflection.Metadata`. For each modified .NET assembly, the report now shows type/method/property/field additions, removals, and method body changes. This new **Method-Level Changes** section appears between **Summary** and **IL Cache Stats** in the Markdown report, and as an expandable inline row above the IL diff in the HTML report. Controlled by the new `ShouldIncludeMethodLevelChangesInReport` config setting (default: `true`). Added [`AssemblyMethodAnalyzer`](Services/AssemblyMethodAnalyzer.cs), [`MethodLevelChangesSummary`](Models/MethodLevelChangesSummary.cs), and corresponding tests. +- Added **Modifiers** column to the Method-Level Changes table, extracting `static`, `abstract`, `virtual`, `override`, `sealed override`, `const`, and `readonly` modifiers from assembly metadata. Changed the **Detail** column format from `(Type paramName) : ReturnType` to `ReturnType (Type paramName)` to match C# declaration order. Renamed the column header to `Detail (ReturnType (Type paramName))`. Applied backtick emphasis to Change, Access, Modifiers, and Kind columns. Added bilingual **Assembly Semantic Changes** section to [README.md](README.md). + ### [1.4.1] - 2026-03-20 #### Added @@ -373,6 +375,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - `System.Reflection.Metadata` を使用した `ILMismatch` アセンブリのメソッドレベル変更検出を追加。変更のあった各 .NET アセンブリについて、型・メソッド・プロパティ・フィールドの増減およびメソッドボディの変更をレポートに出力します。Markdown レポートでは **Summary** と **IL Cache Stats** の間に **Method-Level Changes** セクションとして表示され、HTML レポートでは IL diff の上に展開可能なインライン行として表示されます。新しい設定項目 `ShouldIncludeMethodLevelChangesInReport`(既定: `true`)で制御可能。[`AssemblyMethodAnalyzer`](Services/AssemblyMethodAnalyzer.cs)、[`MethodLevelChangesSummary`](Models/MethodLevelChangesSummary.cs)、および対応するテストを追加。 +- Method-Level Changes テーブルに **Modifiers** 列を追加。アセンブリメタデータから `static`、`abstract`、`virtual`、`override`、`sealed override`、`const`、`readonly` 修飾子を抽出します。**Detail** 列の形式を `(Type paramName) : ReturnType` から `ReturnType (Type paramName)` へ C# 宣言順に変更。列ヘッダを `Detail (ReturnType (Type paramName))` に改名。Change・Access・Modifiers・Kind 列にバッククォート強調を適用。[README.md](README.md) にバイリンガルの **アセンブリ セマンティック変更** セクションを追加。 + ### [1.4.1] - 2026-03-20 #### 追加 diff --git a/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs b/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs index 734fa9bf..ccbaa409 100644 --- a/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs +++ b/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs @@ -20,7 +20,7 @@ public void HasChanges_WithEntries_ReturnsTrue() { Entries = new List { - new("Added", "MyApp.Service", "public", "Method", "DoWork", "(int count) : void"), + new("Added", "MyApp.Service", "public", "", "Method", "DoWork", "void (int count)"), }, }; Assert.True(summary.HasChanges); @@ -41,13 +41,14 @@ public void HasChanges_EmptyEntries_ReturnsFalse() [Fact] public void Entries_ContainStructuredData() { - var entry = new MemberChangeEntry("Added", "MyApp.Service", "public", "Method", "GetName", "(string id) : string"); + var entry = new MemberChangeEntry("Added", "MyApp.Service", "public", "static", "Method", "GetName", "string (string id)"); Assert.Equal("Added", entry.Change); Assert.Equal("MyApp.Service", entry.TypeName); Assert.Equal("public", entry.Access); + Assert.Equal("static", entry.Modifiers); Assert.Equal("Method", entry.MemberKind); Assert.Equal("GetName", entry.MemberName); - Assert.Equal("(string id) : string", entry.Details); + Assert.Equal("string (string id)", entry.Details); } } } diff --git a/FolderDiffIL4DotNet.Tests/Services/AssemblyMethodAnalyzerTests.cs b/FolderDiffIL4DotNet.Tests/Services/AssemblyMethodAnalyzerTests.cs index 8bafe2e9..4d356e0d 100644 --- a/FolderDiffIL4DotNet.Tests/Services/AssemblyMethodAnalyzerTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/AssemblyMethodAnalyzerTests.cs @@ -71,7 +71,7 @@ public void Analyze_DifferentAssemblies_EntriesHaveStructuredData() Assert.False(string.IsNullOrEmpty(firstEntry.Change)); Assert.False(string.IsNullOrEmpty(firstEntry.TypeName)); Assert.False(string.IsNullOrEmpty(firstEntry.MemberKind)); - Assert.Contains(firstEntry.Change, new[] { "+", "-", "~" }); + Assert.Contains(firstEntry.Change, new[] { "Added", "Removed", "Modified" }); Assert.Contains(firstEntry.MemberKind, new[] { "Type", "Method", "Property", "Field" }); } } diff --git a/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs b/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs index 0e82894c..a86e1e1c 100644 --- a/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs @@ -732,10 +732,10 @@ public void GenerateDiffReportHtml_MethodLevelChanges_ShowsInlineAboveILDiff() NewMethodCount = 12, Entries = new List { - new("Added", "MyApp.Service", "public", "Method", "NewMethod", "(string name) : void"), - new("Modified", "MyApp.Service", "public", "Method", "ExistingMethod", "(int id) : bool"), - new("Added", "MyApp.Service", "public", "Property", "NewProp", ": string { get; set; }"), - new("Removed", "MyApp.Service", "private", "Field", "_oldField", ": int"), + new("Added", "MyApp.Service", "public", "", "Method", "NewMethod", "void (string name)"), + new("Modified", "MyApp.Service", "public", "virtual", "Method", "ExistingMethod", "bool (int id)"), + new("Added", "MyApp.Service", "public", "", "Property", "NewProp", ": string { get; set; }"), + new("Removed", "MyApp.Service", "private", "readonly", "Field", "_oldField", ": int"), }, }; @@ -765,7 +765,7 @@ public void GenerateDiffReportHtml_MethodLevelChanges_NotShownWhenDisabled() NewMethodCount = 12, Entries = new List { - new("Added", "MyApp.Service", "public", "Method", "NewMethod", "(string name) : void"), + new("Added", "MyApp.Service", "public", "", "Method", "NewMethod", "void (string name)"), }, }; @@ -793,7 +793,7 @@ public void GenerateDiffReportHtml_MethodLevelChanges_LazyRender_EncodesAsBase64 NewMethodCount = 6, Entries = new List { - new("Added", "Foo", "public", "Method", "Bar", "() : void"), + new("Added", "Foo", "public", "", "Method", "Bar", "void ()"), }, }; diff --git a/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs b/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs index 9e1a6f90..52dab502 100644 --- a/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs @@ -775,13 +775,13 @@ public void GenerateDiffReport_MethodLevelChanges_IncludedBetweenSummaryAndILCac NewMethodCount = 44, Entries = new List { - new("Added", "MyApp.NewService", "", "Type", "", ""), - new("Added", "MyApp.UserService", "public", "Method", "ValidateToken", "(string token) : bool"), - new("Added", "MyApp.UserService", "internal", "Method", "RefreshSession", "(int userId) : void"), - new("Removed", "MyApp.UserService", "public", "Method", "LegacyAuth", "(string key) : void"), - new("Modified", "MyApp.UserService", "public", "Method", "Login", "(string user, string pass) : bool"), - new("Added", "MyApp.UserService", "public", "Property", "IsActive", ": bool { get; set; }"), - new("Added", "MyApp.UserService", "private", "Field", "_cache", ": object"), + new("Added", "MyApp.NewService", "", "", "Type", "", ""), + new("Added", "MyApp.UserService", "public", "static", "Method", "ValidateToken", "bool (string token)"), + new("Added", "MyApp.UserService", "internal", "", "Method", "RefreshSession", "void (int userId)"), + new("Removed", "MyApp.UserService", "public", "virtual", "Method", "LegacyAuth", "void (string key)"), + new("Modified", "MyApp.UserService", "public", "", "Method", "Login", "bool (string user, string pass)"), + new("Added", "MyApp.UserService", "public", "", "Property", "IsActive", ": bool { get; set; }"), + new("Added", "MyApp.UserService", "private", "readonly", "Field", "_cache", ": object"), }, }; _resultLists.FileRelativePathToMethodLevelChanges["src/App.dll"] = summary; @@ -801,12 +801,12 @@ public void GenerateDiffReport_MethodLevelChanges_IncludedBetweenSummaryAndILCac // Content checks — table format Assert.Contains("## Method-Level Changes", reportText); Assert.Contains("### src/App.dll", reportText); - Assert.Contains("| Assembly | Change | Class | Access | Kind | Name | Details |", reportText); - Assert.Contains("| src/App.dll | `Added` | MyApp.NewService | | Type | | |", reportText); - Assert.Contains("| src/App.dll | `Added` | MyApp.UserService | public | Method | ValidateToken | (string token) : bool |", reportText); - Assert.Contains("| src/App.dll | `Modified` | MyApp.UserService | public | Method | Login |", reportText); - Assert.Contains("| src/App.dll | `Added` | MyApp.UserService | public | Property | IsActive |", reportText); - Assert.Contains("| src/App.dll | `Added` | MyApp.UserService | private | Field | _cache |", reportText); + Assert.Contains("| Assembly | Change | Class | Access | Modifiers | Kind | Name | Detail (ReturnType (Type paramName)) |", reportText); + Assert.Contains("| src/App.dll | `Added` | MyApp.NewService | `` | `` | `Type` | | |", reportText); + Assert.Contains("| src/App.dll | `Added` | MyApp.UserService | `public` | `static` | `Method` | ValidateToken | bool (string token) |", reportText); + Assert.Contains("| src/App.dll | `Modified` | MyApp.UserService | `public` | `` | `Method` | Login |", reportText); + Assert.Contains("| src/App.dll | `Added` | MyApp.UserService | `public` | `` | `Property` | IsActive |", reportText); + Assert.Contains("| src/App.dll | `Added` | MyApp.UserService | `private` | `readonly` | `Field` | _cache |", reportText); Assert.Contains("- Method count: 42 (Old) vs 44 (New)", reportText); // Ordering: Summary < Method-Level Changes < IL Cache Stats @@ -833,7 +833,7 @@ public void GenerateDiffReport_MethodLevelChanges_NotIncludedWhenDisabled() NewMethodCount = 12, Entries = new List { - new("Added", "Foo", "public", "Method", "Bar", "() : void"), + new("Added", "Foo", "public", "", "Method", "Bar", "void ()"), }, }; diff --git a/Models/MemberChangeEntry.cs b/Models/MemberChangeEntry.cs index 2f2cc79e..47044717 100644 --- a/Models/MemberChangeEntry.cs +++ b/Models/MemberChangeEntry.cs @@ -4,20 +4,22 @@ namespace FolderDiffIL4DotNet.Models /// Represents a single member-level change detected between two assembly builds. /// 2 つのアセンブリビルド間で検出された単一のメンバーレベル変更を表します。 ///
- /// Change kind: "+" (added), "-" (removed), "~" (body changed). / 変更種別。 + /// Change kind: "Added", "Removed", "Modified". / 変更種別。 /// Owning type name (or the type itself for Type entries). / 所属型名(Type エントリの場合は型名そのもの)。 /// Access modifier. Empty for Type entries. / アクセス修飾子。Type の場合は空。 + /// Other modifiers (static, abstract, virtual, sealed, override, etc.). / その他の修飾子。 /// Member kind: "Type", "Method", "Property", "Field". / メンバー種別。 /// Member name. Empty for Type entries. / メンバー名。Type の場合は空。 /// - /// Additional details: method signature with parameter defaults for methods, - /// type and default value for fields, type and accessor info for properties. - /// 追加詳細:メソッドならパラメータ既定値を含むシグネチャ、フィールドなら型と既定値、プロパティなら型とアクセサ情報。 + /// Additional details in C# declaration order: ReturnType (Type paramName, ...) for methods, + /// : Type for fields/properties. + /// 追加詳細(C# 宣言順):メソッドなら ReturnType (Type paramName, ...)、フィールド/プロパティなら : Type。 /// public sealed record MemberChangeEntry( string Change, string TypeName, string Access, + string Modifiers, string MemberKind, string MemberName, string Details); diff --git a/README.md b/README.md index ad700840..bb8cb850 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Developer-focused details (architecture, CI, tests, implementation cautions): | Need | Document | | --- | --- | | Product overview, setup, usage, and configuration | [README.md](README.md#readme-en-usage) | +| Assembly semantic change detection | [README.md](README.md#readme-en-assembly-semantic-changes) | | Runtime architecture, execution flow, DI scopes, and implementation guardrails | [doc/DEVELOPER_GUIDE.md](doc/DEVELOPER_GUIDE.md#guide-en-map) | | Test strategy, local test commands, coverage, and isolation rules | [doc/TESTING_GUIDE.md](doc/TESTING_GUIDE.md#testing-en-run-tests) | | Generated API reference from XML documentation comments | [api/index.md](api/index.md) via [docfx.json](docfx.json) | @@ -184,6 +185,37 @@ Important details: - If [`ShouldIgnoreILLinesContainingConfiguredStrings`](#config-en-shouldignoreillinescontainingconfiguredstrings) is `true`, lines containing any configured ignore string are also skipped during IL comparison. - If IL comparison itself fails, the run stops instead of silently falling back to a weaker comparison. + +## Assembly Semantic Changes + +When an assembly is classified as `ILMismatch`, the tool performs an additional **semantic analysis** using [`System.Reflection.Metadata`](https://learn.microsoft.com/dotnet/api/system.reflection.metadata) to identify exactly what changed at the member level. Results appear in the **Method-Level Changes** section of the Markdown report and as an expandable inline row in the HTML report. + +### What is detected + +| Category | Detected changes | +|----------|-----------------| +| **Type** | Additions and removals (including nested types) | +| **Method** | Additions, removals, and IL body modifications | +| **Property** | Additions and removals (with get/set accessor info) | +| **Field** | Additions and removals (with type and default value) | +| **Access** | `public`, `protected`, `internal`, `private`, `protected internal`, `private protected` | +| **Modifiers** | `static`, `abstract`, `virtual`, `override`, `sealed override`, `const`, `readonly` | + +### Report table columns + +| Column | Description | Example | +|--------|-------------|---------| +| Assembly | Relative path of the assembly | `bin/MyLib.dll` | +| Change | `Added`, `Removed`, or `Modified` | `Added` | +| Class | Fully qualified type name | `MyNamespace.MyClass` | +| Access | Access modifier | `public` | +| Modifiers | Other modifiers | `static` | +| Kind | Member kind | `Method` | +| Name | Member name (empty for Type entries) | `DoWork` | +| Detail | Signature in C# declaration order | `void (string name, int count = 0)` | + +Controlled by [`ShouldIncludeMethodLevelChangesInReport`](#config-en-shouldincludemethodlevelchangesinreport) (default: `true`). + ## Configuration ([`config.json`](config.json)) Place [`config.json`](config.json) next to the executable. All keys are optional; omitted keys use the code-defined defaults in [`ConfigSettings`](Models/ConfigSettings.cs). If the defaults are acceptable, this file can be just: @@ -455,6 +487,7 @@ For developer-focused details (architecture, exception handling, test setup, CI/ | 見たい内容 | ドキュメント | | --- | --- | | 製品概要、導入、使い方、設定 | [README.md](README.md#readme-ja-usage) | +| アセンブリ セマンティック変更の検出 | [README.md](README.md#readme-ja-assembly-semantic-changes) | | 実行時アーキテクチャ、実行フロー、DI スコープ、実装上の注意点 | [doc/DEVELOPER_GUIDE.md](doc/DEVELOPER_GUIDE.md#guide-ja-map) | | テスト戦略、ローカル実行コマンド、カバレッジ、分離ルール | [doc/TESTING_GUIDE.md](doc/TESTING_GUIDE.md#testing-ja-run-tests) | | XML ドキュメントコメントから生成する API リファレンス | [docfx.json](docfx.json) 経由 [api/index.md](api/index.md) | @@ -630,6 +663,37 @@ flowchart TD - [`ShouldIgnoreILLinesContainingConfiguredStrings`](#config-ja-shouldignoreillinescontainingconfiguredstrings) が `true` の場合は、設定した文字列を含む行も IL 比較から除外します。 - IL 比較そのものに失敗した場合は、弱い比較へ黙って落とさず、その実行全体を停止します。 + +## アセンブリ セマンティック変更 + +アセンブリが `ILMismatch` に分類された場合、[`System.Reflection.Metadata`](https://learn.microsoft.com/dotnet/api/system.reflection.metadata) を使用してメンバーレベルの**セマンティック解析**を追加実行します。結果は Markdown レポートの **Method-Level Changes** セクション、および HTML レポートの展開可能なインライン行に表示されます。 + +### 検出対象 + +| カテゴリ | 検出内容 | +|---------|---------| +| **Type** | 型の追加・削除(ネスト型を含む) | +| **Method** | メソッドの追加・削除・IL ボディの変更 | +| **Property** | プロパティの追加・削除(get/set アクセサ情報付き) | +| **Field** | フィールドの追加・削除(型と既定値付き) | +| **Access** | `public`, `protected`, `internal`, `private`, `protected internal`, `private protected` | +| **Modifiers** | `static`, `abstract`, `virtual`, `override`, `sealed override`, `const`, `readonly` | + +### レポートテーブル列 + +| 列 | 説明 | 例 | +|----|------|-----| +| Assembly | アセンブリの相対パス | `bin/MyLib.dll` | +| Change | `Added`、`Removed`、`Modified` | `Added` | +| Class | 完全修飾型名 | `MyNamespace.MyClass` | +| Access | アクセス修飾子 | `public` | +| Modifiers | その他の修飾子 | `static` | +| Kind | メンバー種別 | `Method` | +| Name | メンバー名(Type エントリの場合は空) | `DoWork` | +| Detail | C# 宣言順のシグネチャ | `void (string name, int count = 0)` | + +[`ShouldIncludeMethodLevelChangesInReport`](#config-ja-shouldincludemethodlevelchangesinreport)(既定値: `true`)で制御します。 + ## 設定([`config.json`](config.json)) 実行ファイルと同じディレクトリに配置します。全項目省略可能で、未指定の項目は [`ConfigSettings`](Models/ConfigSettings.cs) に定義されたコード既定値を使います。既定値のままでよければ、次のように空オブジェクトだけで構いません。 diff --git a/Services/AssemblyMethodAnalyzer.cs b/Services/AssemblyMethodAnalyzer.cs index 42f2d411..74339793 100644 --- a/Services/AssemblyMethodAnalyzer.cs +++ b/Services/AssemblyMethodAnalyzer.cs @@ -37,27 +37,27 @@ internal static class AssemblyMethodAnalyzer // Types foreach (var t in newSnapshot.TypeNames.Except(oldSnapshot.TypeNames, StringComparer.Ordinal).OrderBy(t => t, StringComparer.Ordinal)) - entries.Add(new MemberChangeEntry("Added", t, "", "Type", "", "")); + entries.Add(new MemberChangeEntry("Added", t, "", "", "Type", "", "")); foreach (var t in oldSnapshot.TypeNames.Except(newSnapshot.TypeNames, StringComparer.Ordinal).OrderBy(t => t, StringComparer.Ordinal)) - entries.Add(new MemberChangeEntry("Removed", t, "", "Type", "", "")); + entries.Add(new MemberChangeEntry("Removed", t, "", "", "Type", "", "")); // Methods foreach (var key in newSnapshot.Methods.Keys.Except(oldSnapshot.Methods.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var m = newSnapshot.Methods[key]; - entries.Add(new MemberChangeEntry("Added", m.TypeName, m.Access, "Method", m.MethodName, m.Details)); + entries.Add(new MemberChangeEntry("Added", m.TypeName, m.Access, m.Modifiers, "Method", m.MethodName, m.Details)); } foreach (var key in oldSnapshot.Methods.Keys.Except(newSnapshot.Methods.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var m = oldSnapshot.Methods[key]; - entries.Add(new MemberChangeEntry("Removed", m.TypeName, m.Access, "Method", m.MethodName, m.Details)); + entries.Add(new MemberChangeEntry("Removed", m.TypeName, m.Access, m.Modifiers, "Method", m.MethodName, m.Details)); } foreach (var key in oldSnapshot.Methods.Keys.Intersect(newSnapshot.Methods.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { if (!oldSnapshot.Methods[key].IlBytes.AsSpan().SequenceEqual(newSnapshot.Methods[key].IlBytes.AsSpan())) { var m = newSnapshot.Methods[key]; - entries.Add(new MemberChangeEntry("Modified", m.TypeName, m.Access, "Method", m.MethodName, m.Details)); + entries.Add(new MemberChangeEntry("Modified", m.TypeName, m.Access, m.Modifiers, "Method", m.MethodName, m.Details)); } } @@ -65,24 +65,24 @@ internal static class AssemblyMethodAnalyzer foreach (var key in newSnapshot.Properties.Keys.Except(oldSnapshot.Properties.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var p = newSnapshot.Properties[key]; - entries.Add(new MemberChangeEntry("Added", p.TypeName, p.Access, "Property", p.PropertyName, p.Details)); + entries.Add(new MemberChangeEntry("Added", p.TypeName, p.Access, p.Modifiers, "Property", p.PropertyName, p.Details)); } foreach (var key in oldSnapshot.Properties.Keys.Except(newSnapshot.Properties.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var p = oldSnapshot.Properties[key]; - entries.Add(new MemberChangeEntry("Removed", p.TypeName, p.Access, "Property", p.PropertyName, p.Details)); + entries.Add(new MemberChangeEntry("Removed", p.TypeName, p.Access, p.Modifiers, "Property", p.PropertyName, p.Details)); } // Fields foreach (var key in newSnapshot.Fields.Keys.Except(oldSnapshot.Fields.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var f = newSnapshot.Fields[key]; - entries.Add(new MemberChangeEntry("Added", f.TypeName, f.Access, "Field", f.FieldName, f.Details)); + entries.Add(new MemberChangeEntry("Added", f.TypeName, f.Access, f.Modifiers, "Field", f.FieldName, f.Details)); } foreach (var key in oldSnapshot.Fields.Keys.Except(newSnapshot.Fields.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var f = oldSnapshot.Fields[key]; - entries.Add(new MemberChangeEntry("Removed", f.TypeName, f.Access, "Field", f.FieldName, f.Details)); + entries.Add(new MemberChangeEntry("Removed", f.TypeName, f.Access, f.Modifiers, "Field", f.FieldName, f.Details)); } return new MethodLevelChangesSummary @@ -106,6 +106,7 @@ private sealed class MethodDetail { public required string TypeName { get; init; } public required string Access { get; init; } + public required string Modifiers { get; init; } public required string MethodName { get; init; } public required string Details { get; init; } public required byte[] IlBytes { get; init; } @@ -115,6 +116,7 @@ private sealed class PropertyDetail { public required string TypeName { get; init; } public required string Access { get; init; } + public required string Modifiers { get; init; } public required string PropertyName { get; init; } public required string Details { get; init; } } @@ -123,6 +125,7 @@ private sealed class FieldDetail { public required string TypeName { get; init; } public required string Access { get; init; } + public required string Modifiers { get; init; } public required string FieldName { get; init; } public required string Details { get; init; } } @@ -160,6 +163,7 @@ private static AssemblySnapshot ReadAssemblySnapshot(string assemblyPath) { var methodDef = reader.GetMethodDefinition(methodHandle); string access = GetAccessModifier(methodDef.Attributes); + string modifiers = GetMethodModifiers(methodDef.Attributes); string methodName = reader.GetString(methodDef.Name); string matchKey = BuildMethodMatchKey(reader, typeName, methodDef, typeProvider); string details = BuildMethodDetails(reader, methodDef, typeProvider); @@ -169,6 +173,7 @@ private static AssemblySnapshot ReadAssemblySnapshot(string assemblyPath) { TypeName = typeName, Access = access, + Modifiers = modifiers, MethodName = methodName, Details = details, IlBytes = ilBytes, @@ -182,12 +187,14 @@ private static AssemblySnapshot ReadAssemblySnapshot(string assemblyPath) string propName = reader.GetString(propDef.Name); string propKey = $"{typeName}::{propName}"; string propAccess = GetPropertyAccess(reader, propDef); + string propModifiers = GetPropertyModifiers(reader, propDef); string propDetails = BuildPropertyDetails(reader, propDef, typeProvider); snapshot.Properties[propKey] = new PropertyDetail { TypeName = typeName, Access = propAccess, + Modifiers = propModifiers, PropertyName = propName, Details = propDetails, }; @@ -200,12 +207,14 @@ private static AssemblySnapshot ReadAssemblySnapshot(string assemblyPath) string fieldName = reader.GetString(fieldDef.Name); string fieldKey = $"{typeName}::{fieldName}"; string fieldAccess = GetFieldAccessModifier(fieldDef.Attributes); + string fieldModifiers = GetFieldModifiers(fieldDef.Attributes); string fieldDetails = BuildFieldDetails(reader, fieldDef, typeProvider); snapshot.Fields[fieldKey] = new FieldDetail { TypeName = typeName, Access = fieldAccess, + Modifiers = fieldModifiers, FieldName = fieldName, Details = fieldDetails, }; @@ -254,7 +263,7 @@ private static string BuildMethodMatchKey(MetadataReader reader, string typeName #pragma warning restore CA1031 } - /// Build human-readable details for a method: "(Type paramName, Type paramName = defaultValue) : ReturnType". + /// Build human-readable details for a method: "ReturnType (Type paramName, Type paramName = defaultValue)". private static string BuildMethodDetails(MetadataReader reader, MethodDefinition methodDef, SimpleSignatureTypeProvider typeProvider) { try @@ -298,7 +307,7 @@ private static string BuildMethodDetails(MetadataReader reader, MethodDefinition parts.Add(part); } - return $"({string.Join(", ", parts)}) : {signature.ReturnType}"; + return $"{signature.ReturnType} ({string.Join(", ", parts)})"; } #pragma warning disable CA1031 catch @@ -403,6 +412,48 @@ private static string GetFieldAccessModifier(FieldAttributes attributes) }; } + /// Extract non-access modifiers from method attributes (static, abstract, virtual, sealed, override, etc.). + private static string GetMethodModifiers(MethodAttributes attributes) + { + var parts = new List(); + if ((attributes & MethodAttributes.Static) != 0) parts.Add("static"); + if ((attributes & MethodAttributes.Abstract) != 0) + parts.Add("abstract"); + else if ((attributes & MethodAttributes.Final) != 0 && (attributes & MethodAttributes.Virtual) != 0 && (attributes & MethodAttributes.NewSlot) == 0) + parts.Add("sealed override"); + else if ((attributes & MethodAttributes.Virtual) != 0 && (attributes & MethodAttributes.NewSlot) != 0) + parts.Add("virtual"); + else if ((attributes & MethodAttributes.Virtual) != 0) + parts.Add("override"); + return string.Join(" ", parts); + } + + /// Extract modifiers for a property by inspecting its getter/setter method attributes. + private static string GetPropertyModifiers(MetadataReader reader, PropertyDefinition propDef) + { + var accessors = propDef.GetAccessors(); + MethodDefinition? accessor = !accessors.Getter.IsNil + ? reader.GetMethodDefinition(accessors.Getter) + : !accessors.Setter.IsNil ? reader.GetMethodDefinition(accessors.Setter) : null; + return accessor.HasValue ? GetMethodModifiers(accessor.Value.Attributes) : ""; + } + + /// Extract modifiers from field attributes (static, readonly, const, volatile). + private static string GetFieldModifiers(FieldAttributes attributes) + { + var parts = new List(); + if ((attributes & FieldAttributes.Static) != 0 && (attributes & FieldAttributes.Literal) != 0) + { + parts.Add("const"); + } + else + { + if ((attributes & FieldAttributes.Static) != 0) parts.Add("static"); + if ((attributes & FieldAttributes.InitOnly) != 0) parts.Add("readonly"); + } + return string.Join(" ", parts); + } + private static byte[] ReadIlBytes(PEReader peReader, MethodDefinition methodDef) { if (methodDef.RelativeVirtualAddress == 0) return []; diff --git a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs index 1328e174..3754a271 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs @@ -362,11 +362,11 @@ private void AppendMethodLevelChangesRow( if (summary.Entries.Count > 0) { contentBuilder.AppendLine(""); - contentBuilder.AppendLine(""); + contentBuilder.AppendLine(""); contentBuilder.AppendLine(""); foreach (var e in summary.Entries) { - contentBuilder.AppendLine($""); + contentBuilder.AppendLine($""); } contentBuilder.AppendLine("
AssemblyChangeClassAccessKindNameDetails
AssemblyChangeClassAccessModifiersKindNameDetail (ReturnType (Type paramName))
{HtmlEncode(assemblyPath)}{HtmlEncode(e.Change)}{HtmlEncode(e.TypeName)}{HtmlEncode(e.Access)}{HtmlEncode(e.MemberKind)}{HtmlEncode(e.MemberName)}{HtmlEncode(e.Details)}
{HtmlEncode(assemblyPath)}{HtmlEncode(e.Change)}{HtmlEncode(e.TypeName)}{HtmlEncode(e.Access)}{HtmlEncode(e.Modifiers)}{HtmlEncode(e.MemberKind)}{HtmlEncode(e.MemberName)}{HtmlEncode(e.Details)}
"); } diff --git a/Services/ReportGenerateService.SectionWriters.cs b/Services/ReportGenerateService.SectionWriters.cs index 1ca6f0ce..cd105ff4 100644 --- a/Services/ReportGenerateService.SectionWriters.cs +++ b/Services/ReportGenerateService.SectionWriters.cs @@ -209,11 +209,11 @@ public void Write(StreamWriter writer, ReportWriteContext ctx) if (summary.Entries.Count > 0) { writer.WriteLine(); - writer.WriteLine("| Assembly | Change | Class | Access | Kind | Name | Details |"); - writer.WriteLine("|----------|--------|-------|--------|------|------|---------|"); + writer.WriteLine("| Assembly | Change | Class | Access | Modifiers | Kind | Name | Detail (ReturnType (Type paramName)) |"); + writer.WriteLine("|----------|--------|-------|--------|-----------|------|------|--------------------------------------|"); foreach (var e in summary.Entries) { - writer.WriteLine($"| {EscapeMdTable(filePath)} | `{e.Change}` | {EscapeMdTable(e.TypeName)} | {EscapeMdTable(e.Access)} | {e.MemberKind} | {EscapeMdTable(e.MemberName)} | {EscapeMdTable(e.Details)} |"); + writer.WriteLine($"| {EscapeMdTable(filePath)} | `{EscapeMdTable(e.Change)}` | {EscapeMdTable(e.TypeName)} | `{EscapeMdTable(e.Access)}` | `{EscapeMdTable(e.Modifiers)}` | `{EscapeMdTable(e.MemberKind)}` | {EscapeMdTable(e.MemberName)} | {EscapeMdTable(e.Details)} |"); } } else From e80a52319ecd018044c142ebe565fadfa219324f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 17:07:34 +0000 Subject: [PATCH 05/36] Sync doc/samples with new Modifiers column, C# detail format, and backtick emphasis Update both diff_report.md and diff_report.html samples to match the current report format: Modifiers column, ReturnType (Type paramName) detail order, backtick emphasis on Change/Access/Modifiers/Kind, and renamed Detail header. https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- doc/samples/diff_report.html | 4 ++-- doc/samples/diff_report.md | 36 ++++++++++++++++++------------------ 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/doc/samples/diff_report.html b/doc/samples/diff_report.html index 208a29f6..e0f3dfb7 100644 --- a/doc/samples/diff_report.html +++ b/doc/samples/diff_report.html @@ -437,7 +437,7 @@

[ * ] Modified Files (9)

-
+
#3 Show member changes (5 changes)
@@ -478,7 +478,7 @@

[ * ] Modified Files (9)

-
+
#5 Show member changes (9 changes)
diff --git a/doc/samples/diff_report.md b/doc/samples/diff_report.md index 5e79ba29..8dc36290 100644 --- a/doc/samples/diff_report.md +++ b/doc/samples/diff_report.md @@ -56,28 +56,28 @@ ### src/App.dll -| Assembly | Change | Class | Access | Kind | Name | Details | -|----------|--------|-------|--------|------|------|---------| -| src/App.dll | `Added` | MyApp.Controllers.ApiController | public | Method | HealthCheck | () : string | -| src/App.dll | `Modified` | MyApp.Controllers.ApiController | public | Method | GetUsers | (int page) : System.Collections.Generic.IList\ | -| src/App.dll | `Modified` | MyApp.Services.DataService | internal | Method | RefreshCache | () : void | -| src/App.dll | `Modified` | MyApp.Services.DataService | private | Method | ValidateConnection | (string connStr) : bool | -| src/App.dll | `Added` | MyApp.Services.DataService | public | Property | CacheTimeout | : int { get; set; } | +| Assembly | Change | Class | Access | Modifiers | Kind | Name | Detail (ReturnType (Type paramName)) | +|----------|--------|-------|--------|-----------|------|------|--------------------------------------| +| src/App.dll | `Added` | MyApp.Controllers.ApiController | `public` | `` | `Method` | HealthCheck | string () | +| src/App.dll | `Modified` | MyApp.Controllers.ApiController | `public` | `virtual` | `Method` | GetUsers | System.Collections.Generic.IList\ (int page) | +| src/App.dll | `Modified` | MyApp.Services.DataService | `internal` | `` | `Method` | RefreshCache | void () | +| src/App.dll | `Modified` | MyApp.Services.DataService | `private` | `` | `Method` | ValidateConnection | bool (string connStr) | +| src/App.dll | `Added` | MyApp.Services.DataService | `public` | `` | `Property` | CacheTimeout | : int { get; set; } | - Method count: 28 (Old) vs 29 (New) ### src/Service.dll -| Assembly | Change | Class | Access | Kind | Name | Details | -|----------|--------|-------|--------|------|------|---------| -| src/Service.dll | `Added` | MyApp.Services.NewValidator | | Type | | | -| src/Service.dll | `Added` | MyApp.Services.NewValidator | public | Method | .ctor | () : void | -| src/Service.dll | `Added` | MyApp.Services.NewValidator | public | Method | Validate | (string input) : bool | -| src/Service.dll | `Added` | MyApp.Services.NewValidator | private | Method | ParseInput | (string raw) : string | -| src/Service.dll | `Added` | MyApp.Services.OrderService | public | Method | ValidateWithNewValidator | (string data) : bool | -| src/Service.dll | `Removed` | MyApp.Services.OrderService | public | Method | LegacyValidate | (string data) : bool | -| src/Service.dll | `Modified` | MyApp.Services.OrderService | public | Method | ProcessOrder | (int orderId) : void | -| src/Service.dll | `Modified` | MyApp.Services.OrderService | internal | Method | CalculateTotal | (int qty, int price) : decimal | -| src/Service.dll | `Added` | MyApp.Services.NewValidator | private | Field | _pattern | : string | +| Assembly | Change | Class | Access | Modifiers | Kind | Name | Detail (ReturnType (Type paramName)) | +|----------|--------|-------|--------|-----------|------|------|--------------------------------------| +| src/Service.dll | `Added` | MyApp.Services.NewValidator | `` | `` | `Type` | | | +| src/Service.dll | `Added` | MyApp.Services.NewValidator | `public` | `` | `Method` | .ctor | void () | +| src/Service.dll | `Added` | MyApp.Services.NewValidator | `public` | `` | `Method` | Validate | bool (string input) | +| src/Service.dll | `Added` | MyApp.Services.NewValidator | `private` | `` | `Method` | ParseInput | string (string raw) | +| src/Service.dll | `Added` | MyApp.Services.OrderService | `public` | `` | `Method` | ValidateWithNewValidator | bool (string data) | +| src/Service.dll | `Removed` | MyApp.Services.OrderService | `public` | `virtual` | `Method` | LegacyValidate | bool (string data) | +| src/Service.dll | `Modified` | MyApp.Services.OrderService | `public` | `` | `Method` | ProcessOrder | void (int orderId) | +| src/Service.dll | `Modified` | MyApp.Services.OrderService | `internal` | `static` | `Method` | CalculateTotal | decimal (int qty, int price) | +| src/Service.dll | `Added` | MyApp.Services.NewValidator | `private` | `readonly` | `Field` | _pattern | : string | - Method count: 15 (Old) vs 18 (New) ### util/Legacy.dll From ce20649867d7531819be22859df2b449a193f89c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 17:33:01 +0000 Subject: [PATCH 06/36] Revise sample report: Assembly Semantic Changes, Type column, record example - Rename section ## Method-Level Changes -> ## Assembly Semantic Changes - Add Type column (for Field/Property types), Method rows leave it blank - Rename .ctor -> class name (e.g. NewValidator), static ctor uses Modifiers=static - Remove empty backticks `` from blank cells - Rename Detail -> split into Type + ReturnType (Type paramName) - Add record type sample (UserRecord with init properties, Equals, etc.) - Change "Method count" -> "Member count" - Update HTML Base64, colspan 8->9, summary counts https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- doc/samples/diff_report.html | 32 +++++++++++----------- doc/samples/diff_report.md | 51 ++++++++++++++++++++---------------- 2 files changed, 45 insertions(+), 38 deletions(-) diff --git a/doc/samples/diff_report.html b/doc/samples/diff_report.html index e0f3dfb7..15b67916 100644 --- a/doc/samples/diff_report.html +++ b/doc/samples/diff_report.html @@ -409,7 +409,7 @@

[ * ] Modified Files (9)

- +
#1 Show diff (+2 / -1)
@@ -436,14 +436,14 @@

[ * ] Modified Files (9)

dotnet-ildasm (version: 0.12.2) - -
+ +
#3 Show member changes (5 changes)
- +
#3 Show IL diff (+3 / -2)
@@ -460,7 +460,7 @@

[ * ] Modified Files (9)

- +
#4 Show diff (+2 / -2)
@@ -477,14 +477,14 @@

[ * ] Modified Files (9)

dotnet-ildasm (version: 0.12.2) - -
- #5 Show member changes (9 changes) + +
+ #5 Show member changes (16 changes)
- +
#5 Show IL diff (+2 / -2)
@@ -501,7 +501,7 @@

[ * ] Modified Files (9)

- +
#6 Show diff (+1 / -1)
@@ -518,7 +518,7 @@

[ * ] Modified Files (9)

-

#7 Inline diff skipped: edit distance too large (>4000 insertions/deletions in 2001 vs 2001 lines). Increase InlineDiffMaxEditDistance in config to raise the limit.

+

#7 Inline diff skipped: edit distance too large (>4000 insertions/deletions in 2001 vs 2001 lines). Increase InlineDiffMaxEditDistance in config to raise the limit.

8 @@ -531,7 +531,7 @@

[ * ] Modified Files (9)

-

#8 Inline diff skipped: diff too large (12500 diff lines; limit is 10000). Increase InlineDiffMaxDiffLines in config to enable.

+

#8 Inline diff skipped: diff too large (12500 diff lines; limit is 10000). Increase InlineDiffMaxDiffLines in config to enable.

9 @@ -544,14 +544,14 @@

[ * ] Modified Files (9)

dotnet-ildasm (version: 0.12.2) - -
+ +
#9 Show member changes (other changes only)
- +
#9 Show IL diff (+1 / -1)
@@ -631,7 +631,7 @@

[ ! ] Modified Files — Timestamps Regressed (2) dotnet-ildasm (version: 0.12.2) - +
#2 Show IL diff (+2 / -2)
diff --git a/doc/samples/diff_report.md b/doc/samples/diff_report.md index 8dc36290..5e010233 100644 --- a/doc/samples/diff_report.md +++ b/doc/samples/diff_report.md @@ -52,37 +52,44 @@ - Modified : 9 - Compared : 18 (Old) vs 18 (New) -## Method-Level Changes +## Assembly Semantic Changes ### src/App.dll -| Assembly | Change | Class | Access | Modifiers | Kind | Name | Detail (ReturnType (Type paramName)) | -|----------|--------|-------|--------|-----------|------|------|--------------------------------------| -| src/App.dll | `Added` | MyApp.Controllers.ApiController | `public` | `` | `Method` | HealthCheck | string () | -| src/App.dll | `Modified` | MyApp.Controllers.ApiController | `public` | `virtual` | `Method` | GetUsers | System.Collections.Generic.IList\ (int page) | -| src/App.dll | `Modified` | MyApp.Services.DataService | `internal` | `` | `Method` | RefreshCache | void () | -| src/App.dll | `Modified` | MyApp.Services.DataService | `private` | `` | `Method` | ValidateConnection | bool (string connStr) | -| src/App.dll | `Added` | MyApp.Services.DataService | `public` | `` | `Property` | CacheTimeout | : int { get; set; } | -- Method count: 28 (Old) vs 29 (New) +| Assembly | Change | Class | Access | Modifiers | Kind | Type | Name | ReturnType (Type paramName) | +|----------|--------|-------|--------|-----------|------|------|------|-----------------------------| +| src/App.dll | `Added` | MyApp.Controllers.ApiController | `public` | | `Method` | | HealthCheck | string () | +| src/App.dll | `Modified` | MyApp.Controllers.ApiController | `public` | `virtual` | `Method` | | GetUsers | System.Collections.Generic.IList\ (int page) | +| src/App.dll | `Modified` | MyApp.Services.DataService | `internal` | | `Method` | | RefreshCache | void () | +| src/App.dll | `Modified` | MyApp.Services.DataService | `private` | | `Method` | | ValidateConnection | bool (string connStr) | +| src/App.dll | `Added` | MyApp.Services.DataService | `public` | | `Property` | int { get; set; } | CacheTimeout | | +- Member count: 28 (Old) vs 29 (New) ### src/Service.dll -| Assembly | Change | Class | Access | Modifiers | Kind | Name | Detail (ReturnType (Type paramName)) | -|----------|--------|-------|--------|-----------|------|------|--------------------------------------| -| src/Service.dll | `Added` | MyApp.Services.NewValidator | `` | `` | `Type` | | | -| src/Service.dll | `Added` | MyApp.Services.NewValidator | `public` | `` | `Method` | .ctor | void () | -| src/Service.dll | `Added` | MyApp.Services.NewValidator | `public` | `` | `Method` | Validate | bool (string input) | -| src/Service.dll | `Added` | MyApp.Services.NewValidator | `private` | `` | `Method` | ParseInput | string (string raw) | -| src/Service.dll | `Added` | MyApp.Services.OrderService | `public` | `` | `Method` | ValidateWithNewValidator | bool (string data) | -| src/Service.dll | `Removed` | MyApp.Services.OrderService | `public` | `virtual` | `Method` | LegacyValidate | bool (string data) | -| src/Service.dll | `Modified` | MyApp.Services.OrderService | `public` | `` | `Method` | ProcessOrder | void (int orderId) | -| src/Service.dll | `Modified` | MyApp.Services.OrderService | `internal` | `static` | `Method` | CalculateTotal | decimal (int qty, int price) | -| src/Service.dll | `Added` | MyApp.Services.NewValidator | `private` | `readonly` | `Field` | _pattern | : string | -- Method count: 15 (Old) vs 18 (New) +| Assembly | Change | Class | Access | Modifiers | Kind | Type | Name | ReturnType (Type paramName) | +|----------|--------|-------|--------|-----------|------|------|------|-----------------------------| +| src/Service.dll | `Added` | MyApp.Services.NewValidator | | | `Type` | | | | +| src/Service.dll | `Added` | MyApp.Services.NewValidator | `public` | | `Method` | | NewValidator | void () | +| src/Service.dll | `Added` | MyApp.Services.NewValidator | `public` | | `Method` | | Validate | bool (string input) | +| src/Service.dll | `Added` | MyApp.Services.NewValidator | `private` | | `Method` | | ParseInput | string (string raw) | +| src/Service.dll | `Added` | MyApp.Services.OrderService | `public` | | `Method` | | ValidateWithNewValidator | bool (string data) | +| src/Service.dll | `Removed` | MyApp.Services.OrderService | `public` | `virtual` | `Method` | | LegacyValidate | bool (string data) | +| src/Service.dll | `Modified` | MyApp.Services.OrderService | `public` | | `Method` | | ProcessOrder | void (int orderId) | +| src/Service.dll | `Modified` | MyApp.Services.OrderService | `internal` | `static` | `Method` | | CalculateTotal | decimal (int qty, int price) | +| src/Service.dll | `Added` | MyApp.Services.NewValidator | `private` | `readonly` | `Field` | string | _pattern | | +| src/Service.dll | `Added` | MyApp.Models.UserRecord | | | `Type` | | | | +| src/Service.dll | `Added` | MyApp.Models.UserRecord | `public` | | `Method` | UserRecord | void (string Name, int Age) | +| src/Service.dll | `Added` | MyApp.Models.UserRecord | `public` | | `Property` | string { get; init; } | Name | | +| src/Service.dll | `Added` | MyApp.Models.UserRecord | `public` | | `Property` | int { get; init; } | Age | | +| src/Service.dll | `Added` | MyApp.Models.UserRecord | `public` | `override` | `Method` | | ToString | string () | +| src/Service.dll | `Added` | MyApp.Models.UserRecord | `public` | `virtual` | `Method` | | Equals | bool (object obj) | +| src/Service.dll | `Added` | MyApp.Models.UserRecord | `public` | `override` | `Method` | | GetHashCode | int () | +- Member count: 15 (Old) vs 25 (New) ### util/Legacy.dll - Other changes only. See IL diff for details. -- Method count: 8 (Old) vs 8 (New) +- Member count: 8 (Old) vs 8 (New) ## IL Cache Stats - Hits : 42 From 0c5158b3c3c1f734a18ad6063e42e9b163de407c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 17:45:08 +0000 Subject: [PATCH 07/36] =?UTF-8?q?Rename=20section=20to=20Assembly=20Semant?= =?UTF-8?q?ic=20Changes,=20add=20Type=20column,=20.ctor=E2=86=92class=20na?= =?UTF-8?q?me?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename report section "Method-Level Changes" → "Assembly Semantic Changes" - Add MemberType property to MemberChangeEntry for the new Type column - Field/Property: Type column shows declared type, ReturnType column empty - Method: Type column empty, ReturnType column shows signature - Convert .ctor/.cctor → C# class name in AssemblyMethodAnalyzer - Strip ": " prefix from field/property details into MemberType - Remove empty backticks for blank Access/Modifiers cells in MD/HTML - Rename "Method count" → "Member count" in report output - Update report generators, tests, README, CHANGELOG, ConfigSettings https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- CHANGELOG.md | 8 +-- .../Models/MethodLevelChangesSummaryTests.cs | 5 +- .../HtmlReportGenerateServiceTests.cs | 14 +++--- .../Services/ReportGenerateServiceTests.cs | 50 +++++++++---------- Models/ConfigSettings.cs | 6 +-- Models/MemberChangeEntry.cs | 9 ++-- Models/MethodLevelChangesSummary.cs | 4 +- README.md | 18 ++++--- Services/AssemblyMethodAnalyzer.cs | 36 +++++++++---- .../HtmlReportGenerateService.Sections.cs | 8 +-- .../ReportGenerateService.SectionWriters.cs | 14 +++--- Services/ReportGenerateService.cs | 2 +- 12 files changed, 100 insertions(+), 74 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5c6275a..d538811a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), #### Added -- Added method-level change detection for `ILMismatch` assemblies using `System.Reflection.Metadata`. For each modified .NET assembly, the report now shows type/method/property/field additions, removals, and method body changes. This new **Method-Level Changes** section appears between **Summary** and **IL Cache Stats** in the Markdown report, and as an expandable inline row above the IL diff in the HTML report. Controlled by the new `ShouldIncludeMethodLevelChangesInReport` config setting (default: `true`). Added [`AssemblyMethodAnalyzer`](Services/AssemblyMethodAnalyzer.cs), [`MethodLevelChangesSummary`](Models/MethodLevelChangesSummary.cs), and corresponding tests. +- Added member-level change detection for `ILMismatch` assemblies using `System.Reflection.Metadata`. For each modified .NET assembly, the report now shows type/method/property/field additions, removals, and method body changes. This new **Assembly Semantic Changes** section appears between **Summary** and **IL Cache Stats** in the Markdown report, and as an expandable inline row above the IL diff in the HTML report. Controlled by the new `ShouldIncludeMethodLevelChangesInReport` config setting (default: `true`). Added [`AssemblyMethodAnalyzer`](Services/AssemblyMethodAnalyzer.cs), [`MethodLevelChangesSummary`](Models/MethodLevelChangesSummary.cs), and corresponding tests. -- Added **Modifiers** column to the Method-Level Changes table, extracting `static`, `abstract`, `virtual`, `override`, `sealed override`, `const`, and `readonly` modifiers from assembly metadata. Changed the **Detail** column format from `(Type paramName) : ReturnType` to `ReturnType (Type paramName)` to match C# declaration order. Renamed the column header to `Detail (ReturnType (Type paramName))`. Applied backtick emphasis to Change, Access, Modifiers, and Kind columns. Added bilingual **Assembly Semantic Changes** section to [README.md](README.md). +- Added **Modifiers** column and **Type** column to the Assembly Semantic Changes table. The **Type** column shows the declared type for Field/Property entries (e.g. `int { get; set; }`, `string`), while the **ReturnType (Type paramName)** column shows method signatures. Constructors now display the C# class name instead of `.ctor`. Empty Access/Modifiers cells no longer render as empty backticks. Renamed **Detail** column to split **Type** + **ReturnType (Type paramName)**. Changed `Method count` label to `Member count`. Renamed section from `Method-Level Changes` to `Assembly Semantic Changes`. Added record type sample to [`doc/samples/diff_report.md`](doc/samples/diff_report.md). Added bilingual **Assembly Semantic Changes** section to [README.md](README.md). ### [1.4.1] - 2026-03-20 @@ -373,9 +373,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), #### 追加 -- `System.Reflection.Metadata` を使用した `ILMismatch` アセンブリのメソッドレベル変更検出を追加。変更のあった各 .NET アセンブリについて、型・メソッド・プロパティ・フィールドの増減およびメソッドボディの変更をレポートに出力します。Markdown レポートでは **Summary** と **IL Cache Stats** の間に **Method-Level Changes** セクションとして表示され、HTML レポートでは IL diff の上に展開可能なインライン行として表示されます。新しい設定項目 `ShouldIncludeMethodLevelChangesInReport`(既定: `true`)で制御可能。[`AssemblyMethodAnalyzer`](Services/AssemblyMethodAnalyzer.cs)、[`MethodLevelChangesSummary`](Models/MethodLevelChangesSummary.cs)、および対応するテストを追加。 +- `System.Reflection.Metadata` を使用した `ILMismatch` アセンブリのメンバーレベル変更検出を追加。変更のあった各 .NET アセンブリについて、型・メソッド・プロパティ・フィールドの増減およびメソッドボディの変更をレポートに出力します。Markdown レポートでは **Summary** と **IL Cache Stats** の間に **Assembly Semantic Changes** セクションとして表示され、HTML レポートでは IL diff の上に展開可能なインライン行として表示されます。新しい設定項目 `ShouldIncludeMethodLevelChangesInReport`(既定: `true`)で制御可能。[`AssemblyMethodAnalyzer`](Services/AssemblyMethodAnalyzer.cs)、[`MethodLevelChangesSummary`](Models/MethodLevelChangesSummary.cs)、および対応するテストを追加。 -- Method-Level Changes テーブルに **Modifiers** 列を追加。アセンブリメタデータから `static`、`abstract`、`virtual`、`override`、`sealed override`、`const`、`readonly` 修飾子を抽出します。**Detail** 列の形式を `(Type paramName) : ReturnType` から `ReturnType (Type paramName)` へ C# 宣言順に変更。列ヘッダを `Detail (ReturnType (Type paramName))` に改名。Change・Access・Modifiers・Kind 列にバッククォート強調を適用。[README.md](README.md) にバイリンガルの **アセンブリ セマンティック変更** セクションを追加。 +- Assembly Semantic Changes テーブルに **Modifiers** 列と **Type** 列を追加。**Type** 列は Field/Property の宣言型(例: `int { get; set; }`、`string`)を表示し、**ReturnType (Type paramName)** 列はメソッドシグネチャを表示します。コンストラクタは `.ctor` ではなく C# のクラス名で表示。空の Access/Modifiers セルは空バッククォートではなく空欄に。**Detail** 列を **Type** + **ReturnType (Type paramName)** に分割。`Method count` ラベルを `Member count` に変更。セクション名を `Method-Level Changes` から `Assembly Semantic Changes` に改名。[`doc/samples/diff_report.md`](doc/samples/diff_report.md) に record 型のサンプルを追加。[README.md](README.md) にバイリンガルの **アセンブリ セマンティック変更** セクションを追加。 ### [1.4.1] - 2026-03-20 diff --git a/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs b/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs index ccbaa409..464f8354 100644 --- a/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs +++ b/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs @@ -20,7 +20,7 @@ public void HasChanges_WithEntries_ReturnsTrue() { Entries = new List { - new("Added", "MyApp.Service", "public", "", "Method", "DoWork", "void (int count)"), + new("Added", "MyApp.Service", "public", "", "Method", "DoWork", "", "void (int count)"), }, }; Assert.True(summary.HasChanges); @@ -41,13 +41,14 @@ public void HasChanges_EmptyEntries_ReturnsFalse() [Fact] public void Entries_ContainStructuredData() { - var entry = new MemberChangeEntry("Added", "MyApp.Service", "public", "static", "Method", "GetName", "string (string id)"); + var entry = new MemberChangeEntry("Added", "MyApp.Service", "public", "static", "Method", "GetName", "", "string (string id)"); Assert.Equal("Added", entry.Change); Assert.Equal("MyApp.Service", entry.TypeName); Assert.Equal("public", entry.Access); Assert.Equal("static", entry.Modifiers); Assert.Equal("Method", entry.MemberKind); Assert.Equal("GetName", entry.MemberName); + Assert.Equal("", entry.MemberType); Assert.Equal("string (string id)", entry.Details); } } diff --git a/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs b/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs index a86e1e1c..9f1819fe 100644 --- a/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs @@ -716,7 +716,7 @@ public void GenerateDiffReportHtml_LazyRender_JsSetupFunctionPresent() Assert.Contains("data-diff-html", html); // JS references the attribute name } - // ── Method-Level Changes / メソッドレベル変更 ───────────────────── + // ── Assembly Semantic Changes / アセンブリ意味変更 ───────────────────── [Fact] public void GenerateDiffReportHtml_MethodLevelChanges_ShowsInlineAboveILDiff() @@ -732,10 +732,10 @@ public void GenerateDiffReportHtml_MethodLevelChanges_ShowsInlineAboveILDiff() NewMethodCount = 12, Entries = new List { - new("Added", "MyApp.Service", "public", "", "Method", "NewMethod", "void (string name)"), - new("Modified", "MyApp.Service", "public", "virtual", "Method", "ExistingMethod", "bool (int id)"), - new("Added", "MyApp.Service", "public", "", "Property", "NewProp", ": string { get; set; }"), - new("Removed", "MyApp.Service", "private", "readonly", "Field", "_oldField", ": int"), + new("Added", "MyApp.Service", "public", "", "Method", "NewMethod", "", "void (string name)"), + new("Modified", "MyApp.Service", "public", "virtual", "Method", "ExistingMethod", "", "bool (int id)"), + new("Added", "MyApp.Service", "public", "", "Property", "NewProp", "string { get; set; }", ""), + new("Removed", "MyApp.Service", "private", "readonly", "Field", "_oldField", "int", ""), }, }; @@ -765,7 +765,7 @@ public void GenerateDiffReportHtml_MethodLevelChanges_NotShownWhenDisabled() NewMethodCount = 12, Entries = new List { - new("Added", "MyApp.Service", "public", "", "Method", "NewMethod", "void (string name)"), + new("Added", "MyApp.Service", "public", "", "Method", "NewMethod", "", "void (string name)"), }, }; @@ -793,7 +793,7 @@ public void GenerateDiffReportHtml_MethodLevelChanges_LazyRender_EncodesAsBase64 NewMethodCount = 6, Entries = new List { - new("Added", "Foo", "public", "", "Method", "Bar", "void ()"), + new("Added", "Foo", "public", "", "Method", "Bar", "", "void ()"), }, }; diff --git a/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs b/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs index 52dab502..a68229f6 100644 --- a/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs @@ -754,10 +754,10 @@ public void GenerateDiffReport_WithIgnoredFilesNoneLocation_DoesNotBreakReport() Assert.True(File.Exists(Path.Combine(reportDir, "diff_report.md"))); } - // ── Method-Level Changes / メソッドレベル変更 ───────────────────── + // ── Assembly Semantic Changes / アセンブリ意味変更 ───────────────────── [Fact] - public void GenerateDiffReport_MethodLevelChanges_IncludedBetweenSummaryAndILCacheStats() + public void GenerateDiffReport_AssemblySemanticChanges_IncludedBetweenSummaryAndILCacheStats() { var oldDir = Path.Combine(_rootDir, "old-mlc"); var newDir = Path.Combine(_rootDir, "new-mlc"); @@ -775,13 +775,13 @@ public void GenerateDiffReport_MethodLevelChanges_IncludedBetweenSummaryAndILCac NewMethodCount = 44, Entries = new List { - new("Added", "MyApp.NewService", "", "", "Type", "", ""), - new("Added", "MyApp.UserService", "public", "static", "Method", "ValidateToken", "bool (string token)"), - new("Added", "MyApp.UserService", "internal", "", "Method", "RefreshSession", "void (int userId)"), - new("Removed", "MyApp.UserService", "public", "virtual", "Method", "LegacyAuth", "void (string key)"), - new("Modified", "MyApp.UserService", "public", "", "Method", "Login", "bool (string user, string pass)"), - new("Added", "MyApp.UserService", "public", "", "Property", "IsActive", ": bool { get; set; }"), - new("Added", "MyApp.UserService", "private", "readonly", "Field", "_cache", ": object"), + new("Added", "MyApp.NewService", "", "", "Type", "", "", ""), + new("Added", "MyApp.UserService", "public", "static", "Method", "ValidateToken", "", "bool (string token)"), + new("Added", "MyApp.UserService", "internal", "", "Method", "RefreshSession", "", "void (int userId)"), + new("Removed", "MyApp.UserService", "public", "virtual", "Method", "LegacyAuth", "", "void (string key)"), + new("Modified", "MyApp.UserService", "public", "", "Method", "Login", "", "bool (string user, string pass)"), + new("Added", "MyApp.UserService", "public", "", "Property", "IsActive", "bool { get; set; }", ""), + new("Added", "MyApp.UserService", "private", "readonly", "Field", "_cache", "object", ""), }, }; _resultLists.FileRelativePathToMethodLevelChanges["src/App.dll"] = summary; @@ -799,22 +799,22 @@ public void GenerateDiffReport_MethodLevelChanges_IncludedBetweenSummaryAndILCac var reportText = File.ReadAllText(Path.Combine(reportDir, "diff_report.md")); // Content checks — table format - Assert.Contains("## Method-Level Changes", reportText); + Assert.Contains("## Assembly Semantic Changes", reportText); Assert.Contains("### src/App.dll", reportText); - Assert.Contains("| Assembly | Change | Class | Access | Modifiers | Kind | Name | Detail (ReturnType (Type paramName)) |", reportText); - Assert.Contains("| src/App.dll | `Added` | MyApp.NewService | `` | `` | `Type` | | |", reportText); - Assert.Contains("| src/App.dll | `Added` | MyApp.UserService | `public` | `static` | `Method` | ValidateToken | bool (string token) |", reportText); - Assert.Contains("| src/App.dll | `Modified` | MyApp.UserService | `public` | `` | `Method` | Login |", reportText); - Assert.Contains("| src/App.dll | `Added` | MyApp.UserService | `public` | `` | `Property` | IsActive |", reportText); - Assert.Contains("| src/App.dll | `Added` | MyApp.UserService | `private` | `readonly` | `Field` | _cache |", reportText); - Assert.Contains("- Method count: 42 (Old) vs 44 (New)", reportText); - - // Ordering: Summary < Method-Level Changes < IL Cache Stats + Assert.Contains("| Assembly | Change | Class | Access | Modifiers | Kind | Type | Name | ReturnType (Type paramName) |", reportText); + Assert.Contains("| src/App.dll | `Added` | MyApp.NewService | | | `Type` | | | |", reportText); + Assert.Contains("| src/App.dll | `Added` | MyApp.UserService | `public` | `static` | `Method` | | ValidateToken | bool (string token) |", reportText); + Assert.Contains("| src/App.dll | `Modified` | MyApp.UserService | `public` | | `Method` | | Login |", reportText); + Assert.Contains("| src/App.dll | `Added` | MyApp.UserService | `public` | | `Property` | bool { get; set; } | IsActive |", reportText); + Assert.Contains("| src/App.dll | `Added` | MyApp.UserService | `private` | `readonly` | `Field` | object | _cache |", reportText); + Assert.Contains("- Member count: 42 (Old) vs 44 (New)", reportText); + + // Ordering: Summary < Assembly Semantic Changes < IL Cache Stats int summaryIdx = reportText.IndexOf("## Summary", StringComparison.Ordinal); - int methodIdx = reportText.IndexOf("## Method-Level Changes", StringComparison.Ordinal); + int semanticIdx = reportText.IndexOf("## Assembly Semantic Changes", StringComparison.Ordinal); int ilCacheIdx = reportText.IndexOf("## IL Cache Stats", StringComparison.Ordinal); - Assert.True(summaryIdx < methodIdx, "Method-Level Changes should appear after Summary"); - Assert.True(methodIdx < ilCacheIdx, "Method-Level Changes should appear before IL Cache Stats"); + Assert.True(summaryIdx < semanticIdx, "Assembly Semantic Changes should appear after Summary"); + Assert.True(semanticIdx < ilCacheIdx, "Assembly Semantic Changes should appear before IL Cache Stats"); } [Fact] @@ -833,7 +833,7 @@ public void GenerateDiffReport_MethodLevelChanges_NotIncludedWhenDisabled() NewMethodCount = 12, Entries = new List { - new("Added", "Foo", "public", "", "Method", "Bar", "void ()"), + new("Added", "Foo", "public", "", "Method", "Bar", "", "void ()"), }, }; @@ -845,7 +845,7 @@ public void GenerateDiffReport_MethodLevelChanges_NotIncludedWhenDisabled() config); var reportText = File.ReadAllText(Path.Combine(reportDir, "diff_report.md")); - Assert.DoesNotContain("## Method-Level Changes", reportText); + Assert.DoesNotContain("## Assembly Semantic Changes", reportText); } [Fact] @@ -867,7 +867,7 @@ public void GenerateDiffReport_MethodLevelChanges_NotIncludedWhenNoChanges() config); var reportText = File.ReadAllText(Path.Combine(reportDir, "diff_report.md")); - Assert.DoesNotContain("## Method-Level Changes", reportText); + Assert.DoesNotContain("## Assembly Semantic Changes", reportText); } private static ConfigSettings CreateConfig() => new() diff --git a/Models/ConfigSettings.cs b/Models/ConfigSettings.cs index 90bb9eb7..fc5fa4f2 100644 --- a/Models/ConfigSettings.cs +++ b/Models/ConfigSettings.cs @@ -98,12 +98,12 @@ public List TextFileExtensions public bool ShouldIncludeIgnoredFiles { get; set; } = true; /// - /// Whether to include method-level change details (type/method/property/field additions, removals, + /// Whether to include member-level change details (type/method/property/field additions, removals, /// and method body changes) for ILMismatch assemblies in the diff report. - /// When true, a Method-Level Changes section is inserted between Summary and IL Cache Stats. + /// When true, an Assembly Semantic Changes section is inserted between Summary and IL Cache Stats. /// ILMismatch と判定された .NET アセンブリについて、メンバーレベルの変更詳細 /// (型・メソッド・プロパティ・フィールドの増減およびメソッドボディの変更)をレポートに出力するかどうか。 - /// true の場合、Summary セクションと IL Cache Stats セクションの間に Method-Level Changes セクションを追加します。 + /// true の場合、Summary セクションと IL Cache Stats セクションの間に Assembly Semantic Changes セクションを追加します。 /// public bool ShouldIncludeMethodLevelChangesInReport { get; set; } = true; diff --git a/Models/MemberChangeEntry.cs b/Models/MemberChangeEntry.cs index 47044717..4382b88e 100644 --- a/Models/MemberChangeEntry.cs +++ b/Models/MemberChangeEntry.cs @@ -9,11 +9,11 @@ namespace FolderDiffIL4DotNet.Models /// Access modifier. Empty for Type entries. / アクセス修飾子。Type の場合は空。 /// Other modifiers (static, abstract, virtual, sealed, override, etc.). / その他の修飾子。 /// Member kind: "Type", "Method", "Property", "Field". / メンバー種別。 - /// Member name. Empty for Type entries. / メンバー名。Type の場合は空。 + /// Member name (C# name; constructors use the class name, not .ctor). Empty for Type entries. / メンバー名(C# 名、コンストラクタは .ctor ではなくクラス名)。Type の場合は空。 + /// For Field/Property: the declared type (e.g. "string", "int { get; set; }"). Empty for Method/Type entries. / フィールド・プロパティの宣言型。メソッド・Type の場合は空。 /// - /// Additional details in C# declaration order: ReturnType (Type paramName, ...) for methods, - /// : Type for fields/properties. - /// 追加詳細(C# 宣言順):メソッドなら ReturnType (Type paramName, ...)、フィールド/プロパティなら : Type。 + /// For methods: ReturnType (Type paramName, ...). Empty for Type/Field/Property entries. + /// メソッドなら ReturnType (Type paramName, ...)。Type/Field/Property の場合は空。 /// public sealed record MemberChangeEntry( string Change, @@ -22,5 +22,6 @@ public sealed record MemberChangeEntry( string Modifiers, string MemberKind, string MemberName, + string MemberType, string Details); } diff --git a/Models/MethodLevelChangesSummary.cs b/Models/MethodLevelChangesSummary.cs index bf3cb504..c7c48a3f 100644 --- a/Models/MethodLevelChangesSummary.cs +++ b/Models/MethodLevelChangesSummary.cs @@ -13,10 +13,10 @@ public sealed class MethodLevelChangesSummary /// All detected member-level changes. / 検出されたすべてのメンバーレベル変更。 public IReadOnlyList Entries { get; init; } = []; - /// Total method count in the old assembly. / 旧アセンブリのメソッド総数。 + /// Total member (method) count in the old assembly. / 旧アセンブリのメンバー(メソッド)総数。 public int OldMethodCount { get; init; } - /// Total method count in the new assembly. / 新アセンブリのメソッド総数。 + /// Total member (method) count in the new assembly. / 新アセンブリのメンバー(メソッド)総数。 public int NewMethodCount { get; init; } /// Whether any changes were detected. / 何らかの変更が検出されたかどうか。 diff --git a/README.md b/README.md index bb8cb850..b21071a9 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ Important details: ## Assembly Semantic Changes -When an assembly is classified as `ILMismatch`, the tool performs an additional **semantic analysis** using [`System.Reflection.Metadata`](https://learn.microsoft.com/dotnet/api/system.reflection.metadata) to identify exactly what changed at the member level. Results appear in the **Method-Level Changes** section of the Markdown report and as an expandable inline row in the HTML report. +When an assembly is classified as `ILMismatch`, the tool performs an additional **semantic analysis** using [`System.Reflection.Metadata`](https://learn.microsoft.com/dotnet/api/system.reflection.metadata) to identify exactly what changed at the member level. Results appear in the **Assembly Semantic Changes** section of the Markdown report and as an expandable inline row in the HTML report. ### What is detected @@ -211,8 +211,9 @@ When an assembly is classified as `ILMismatch`, the tool performs an additional | Access | Access modifier | `public` | | Modifiers | Other modifiers | `static` | | Kind | Member kind | `Method` | -| Name | Member name (empty for Type entries) | `DoWork` | -| Detail | Signature in C# declaration order | `void (string name, int count = 0)` | +| Type | Declared type for Field/Property (empty for Method/Type) | `int { get; set; }` | +| Name | Member name (constructors use the class name; empty for Type entries) | `DoWork` | +| ReturnType (Type paramName) | Method signature (empty for Field/Property/Type) | `void (string name, int count = 0)` | Controlled by [`ShouldIncludeMethodLevelChangesInReport`](#config-en-shouldincludemethodlevelchangesinreport) (default: `true`). @@ -276,7 +277,7 @@ Override only the settings you want to change. For example: ShouldIncludeMethodLevelChangesInReport true - When true, includes a Method-Level Changes section for ILMismatch assemblies between Summary and IL Cache Stats. Uses System.Reflection.Metadata to detect type/method/property/field additions, removals, and method body changes. In the HTML report, this appears as an expandable inline row above the IL diff. + When true, includes an Assembly Semantic Changes section for ILMismatch assemblies between Summary and IL Cache Stats. Uses System.Reflection.Metadata to detect type/method/property/field additions, removals, and method body changes. In the HTML report, this appears as an expandable inline row above the IL diff. ShouldIncludeILCacheStatsInReport @@ -666,7 +667,7 @@ flowchart TD ## アセンブリ セマンティック変更 -アセンブリが `ILMismatch` に分類された場合、[`System.Reflection.Metadata`](https://learn.microsoft.com/dotnet/api/system.reflection.metadata) を使用してメンバーレベルの**セマンティック解析**を追加実行します。結果は Markdown レポートの **Method-Level Changes** セクション、および HTML レポートの展開可能なインライン行に表示されます。 +アセンブリが `ILMismatch` に分類された場合、[`System.Reflection.Metadata`](https://learn.microsoft.com/dotnet/api/system.reflection.metadata) を使用してメンバーレベルの**セマンティック解析**を追加実行します。結果は Markdown レポートの **Assembly Semantic Changes** セクション、および HTML レポートの展開可能なインライン行に表示されます。 ### 検出対象 @@ -689,8 +690,9 @@ flowchart TD | Access | アクセス修飾子 | `public` | | Modifiers | その他の修飾子 | `static` | | Kind | メンバー種別 | `Method` | -| Name | メンバー名(Type エントリの場合は空) | `DoWork` | -| Detail | C# 宣言順のシグネチャ | `void (string name, int count = 0)` | +| Type | Field/Property の宣言型(Method/Type の場合は空) | `int { get; set; }` | +| Name | メンバー名(コンストラクタはクラス名、Type エントリの場合は空) | `DoWork` | +| ReturnType (Type paramName) | メソッドシグネチャ(Field/Property/Type の場合は空) | `void (string name, int count = 0)` | [`ShouldIncludeMethodLevelChangesInReport`](#config-ja-shouldincludemethodlevelchangesinreport)(既定値: `true`)で制御します。 @@ -754,7 +756,7 @@ flowchart TD ShouldIncludeMethodLevelChangesInReport true - true の場合、ILMismatch と判定された .NET アセンブリについて、SummaryIL Cache Stats の間に Method-Level Changes セクションを出力します。System.Reflection.Metadata を使用して型・メソッド・プロパティ・フィールドの増減およびメソッドボディの変更を検出します。HTML レポートでは IL diff の上に展開可能なインライン行として表示されます。 + true の場合、ILMismatch と判定された .NET アセンブリについて、SummaryIL Cache Stats の間に Assembly Semantic Changes セクションを出力します。System.Reflection.Metadata を使用して型・メソッド・プロパティ・フィールドの増減およびメソッドボディの変更を検出します。HTML レポートでは IL diff の上に展開可能なインライン行として表示されます。 ShouldIncludeILCacheStatsInReport diff --git a/Services/AssemblyMethodAnalyzer.cs b/Services/AssemblyMethodAnalyzer.cs index 74339793..1ab9c9be 100644 --- a/Services/AssemblyMethodAnalyzer.cs +++ b/Services/AssemblyMethodAnalyzer.cs @@ -37,27 +37,27 @@ internal static class AssemblyMethodAnalyzer // Types foreach (var t in newSnapshot.TypeNames.Except(oldSnapshot.TypeNames, StringComparer.Ordinal).OrderBy(t => t, StringComparer.Ordinal)) - entries.Add(new MemberChangeEntry("Added", t, "", "", "Type", "", "")); + entries.Add(new MemberChangeEntry("Added", t, "", "", "Type", "", "", "")); foreach (var t in oldSnapshot.TypeNames.Except(newSnapshot.TypeNames, StringComparer.Ordinal).OrderBy(t => t, StringComparer.Ordinal)) - entries.Add(new MemberChangeEntry("Removed", t, "", "", "Type", "", "")); + entries.Add(new MemberChangeEntry("Removed", t, "", "", "Type", "", "", "")); // Methods foreach (var key in newSnapshot.Methods.Keys.Except(oldSnapshot.Methods.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var m = newSnapshot.Methods[key]; - entries.Add(new MemberChangeEntry("Added", m.TypeName, m.Access, m.Modifiers, "Method", m.MethodName, m.Details)); + entries.Add(new MemberChangeEntry("Added", m.TypeName, m.Access, m.Modifiers, "Method", ToCSharpMethodName(m.MethodName, m.TypeName), "", m.Details)); } foreach (var key in oldSnapshot.Methods.Keys.Except(newSnapshot.Methods.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var m = oldSnapshot.Methods[key]; - entries.Add(new MemberChangeEntry("Removed", m.TypeName, m.Access, m.Modifiers, "Method", m.MethodName, m.Details)); + entries.Add(new MemberChangeEntry("Removed", m.TypeName, m.Access, m.Modifiers, "Method", ToCSharpMethodName(m.MethodName, m.TypeName), "", m.Details)); } foreach (var key in oldSnapshot.Methods.Keys.Intersect(newSnapshot.Methods.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { if (!oldSnapshot.Methods[key].IlBytes.AsSpan().SequenceEqual(newSnapshot.Methods[key].IlBytes.AsSpan())) { var m = newSnapshot.Methods[key]; - entries.Add(new MemberChangeEntry("Modified", m.TypeName, m.Access, m.Modifiers, "Method", m.MethodName, m.Details)); + entries.Add(new MemberChangeEntry("Modified", m.TypeName, m.Access, m.Modifiers, "Method", ToCSharpMethodName(m.MethodName, m.TypeName), "", m.Details)); } } @@ -65,24 +65,24 @@ internal static class AssemblyMethodAnalyzer foreach (var key in newSnapshot.Properties.Keys.Except(oldSnapshot.Properties.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var p = newSnapshot.Properties[key]; - entries.Add(new MemberChangeEntry("Added", p.TypeName, p.Access, p.Modifiers, "Property", p.PropertyName, p.Details)); + entries.Add(new MemberChangeEntry("Added", p.TypeName, p.Access, p.Modifiers, "Property", p.PropertyName, StripColonPrefix(p.Details), "")); } foreach (var key in oldSnapshot.Properties.Keys.Except(newSnapshot.Properties.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var p = oldSnapshot.Properties[key]; - entries.Add(new MemberChangeEntry("Removed", p.TypeName, p.Access, p.Modifiers, "Property", p.PropertyName, p.Details)); + entries.Add(new MemberChangeEntry("Removed", p.TypeName, p.Access, p.Modifiers, "Property", p.PropertyName, StripColonPrefix(p.Details), "")); } // Fields foreach (var key in newSnapshot.Fields.Keys.Except(oldSnapshot.Fields.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var f = newSnapshot.Fields[key]; - entries.Add(new MemberChangeEntry("Added", f.TypeName, f.Access, f.Modifiers, "Field", f.FieldName, f.Details)); + entries.Add(new MemberChangeEntry("Added", f.TypeName, f.Access, f.Modifiers, "Field", f.FieldName, StripColonPrefix(f.Details), "")); } foreach (var key in oldSnapshot.Fields.Keys.Except(newSnapshot.Fields.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var f = oldSnapshot.Fields[key]; - entries.Add(new MemberChangeEntry("Removed", f.TypeName, f.Access, f.Modifiers, "Field", f.FieldName, f.Details)); + entries.Add(new MemberChangeEntry("Removed", f.TypeName, f.Access, f.Modifiers, "Field", f.FieldName, StripColonPrefix(f.Details), "")); } return new MethodLevelChangesSummary @@ -241,6 +241,24 @@ private static string GetFullTypeName(MetadataReader reader, TypeDefinition type return string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}"; } + /// Convert IL method name to C# name: .ctor/.cctor → simple class name. / IL メソッド名を C# 名に変換。 + private static string ToCSharpMethodName(string ilMethodName, string typeName) + { + if (ilMethodName is ".ctor" or ".cctor") + { + // Extract simple class name from potentially nested or namespaced type name + int slashIdx = typeName.LastIndexOf('/'); + string leaf = slashIdx >= 0 ? typeName[(slashIdx + 1)..] : typeName; + int dotIdx = leaf.LastIndexOf('.'); + return dotIdx >= 0 ? leaf[(dotIdx + 1)..] : leaf; + } + return ilMethodName; + } + + /// Strip leading ": " prefix from field/property details to extract the type portion. / フィールド・プロパティの詳細から先頭 ": " を除去して型部分を抽出。 + private static string StripColonPrefix(string details) + => details.StartsWith(": ", StringComparison.Ordinal) ? details[2..] : details; + /// Build a key for method matching (without access modifier so access changes don't cause false add/remove pairs). private static string BuildMethodMatchKey(MetadataReader reader, string typeName, MethodDefinition methodDef, SimpleSignatureTypeProvider typeProvider) { diff --git a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs index 3754a271..04478455 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs @@ -362,11 +362,13 @@ private void AppendMethodLevelChangesRow( if (summary.Entries.Count > 0) { contentBuilder.AppendLine(""); - contentBuilder.AppendLine(""); + contentBuilder.AppendLine(""); contentBuilder.AppendLine(""); foreach (var e in summary.Entries) { - contentBuilder.AppendLine($""); + string accessTd = e.Access.Length > 0 ? $"{HtmlEncode(e.Access)}" : ""; + string modifiersTd = e.Modifiers.Length > 0 ? $"{HtmlEncode(e.Modifiers)}" : ""; + contentBuilder.AppendLine($""); } contentBuilder.AppendLine("
AssemblyChangeClassAccessModifiersKindNameDetail (ReturnType (Type paramName))
AssemblyChangeClassAccessModifiersKindTypeNameReturnType (Type paramName)
{HtmlEncode(assemblyPath)}{HtmlEncode(e.Change)}{HtmlEncode(e.TypeName)}{HtmlEncode(e.Access)}{HtmlEncode(e.Modifiers)}{HtmlEncode(e.MemberKind)}{HtmlEncode(e.MemberName)}{HtmlEncode(e.Details)}
{HtmlEncode(assemblyPath)}{HtmlEncode(e.Change)}{HtmlEncode(e.TypeName)}{accessTd}{modifiersTd}{HtmlEncode(e.MemberKind)}{HtmlEncode(e.MemberType)}{HtmlEncode(e.MemberName)}{HtmlEncode(e.Details)}
"); } @@ -375,7 +377,7 @@ private void AppendMethodLevelChangesRow( contentBuilder.AppendLine("

Other changes only. See IL diff for details.

"); } - contentBuilder.AppendLine($"

Method count: {summary.OldMethodCount} (Old) vs {summary.NewMethodCount} (New)

"); + contentBuilder.AppendLine($"

Member count: {summary.OldMethodCount} (Old) vs {summary.NewMethodCount} (New)

"); contentBuilder.AppendLine(""); string detailsId = $"methods_{sectionPrefix}_{idx}"; diff --git a/Services/ReportGenerateService.SectionWriters.cs b/Services/ReportGenerateService.SectionWriters.cs index cd105ff4..d5795e85 100644 --- a/Services/ReportGenerateService.SectionWriters.cs +++ b/Services/ReportGenerateService.SectionWriters.cs @@ -191,7 +191,7 @@ public void Write(StreamWriter writer, ReportWriteContext ctx) } } - /// Writes the Method-Level Changes section for ILMismatch assemblies. / ILMismatch アセンブリのメソッドレベル変更セクションを書き込みます。 + /// Writes the Assembly Semantic Changes section for ILMismatch assemblies. / ILMismatch アセンブリのアセンブリ意味変更セクションを書き込みます。 private sealed class MethodLevelChangesSectionWriter : IReportSectionWriter { public void Write(StreamWriter writer, ReportWriteContext ctx) @@ -200,7 +200,7 @@ public void Write(StreamWriter writer, ReportWriteContext ctx) var changes = ctx.FileDiffResultLists.FileRelativePathToMethodLevelChanges; if (changes.IsEmpty) return; - writer.WriteLine(REPORT_SECTION_METHOD_LEVEL_CHANGES); + writer.WriteLine(REPORT_SECTION_ASSEMBLY_SEMANTIC_CHANGES); foreach (var (filePath, summary) in changes.OrderBy(kv => kv.Key, StringComparer.OrdinalIgnoreCase)) { @@ -209,11 +209,13 @@ public void Write(StreamWriter writer, ReportWriteContext ctx) if (summary.Entries.Count > 0) { writer.WriteLine(); - writer.WriteLine("| Assembly | Change | Class | Access | Modifiers | Kind | Name | Detail (ReturnType (Type paramName)) |"); - writer.WriteLine("|----------|--------|-------|--------|-----------|------|------|--------------------------------------|"); + writer.WriteLine("| Assembly | Change | Class | Access | Modifiers | Kind | Type | Name | ReturnType (Type paramName) |"); + writer.WriteLine("|----------|--------|-------|--------|-----------|------|------|------|-----------------------------|"); foreach (var e in summary.Entries) { - writer.WriteLine($"| {EscapeMdTable(filePath)} | `{EscapeMdTable(e.Change)}` | {EscapeMdTable(e.TypeName)} | `{EscapeMdTable(e.Access)}` | `{EscapeMdTable(e.Modifiers)}` | `{EscapeMdTable(e.MemberKind)}` | {EscapeMdTable(e.MemberName)} | {EscapeMdTable(e.Details)} |"); + string access = e.Access.Length > 0 ? $"`{EscapeMdTable(e.Access)}`" : ""; + string modifiers = e.Modifiers.Length > 0 ? $"`{EscapeMdTable(e.Modifiers)}`" : ""; + writer.WriteLine($"| {EscapeMdTable(filePath)} | `{EscapeMdTable(e.Change)}` | {EscapeMdTable(e.TypeName)} | {access} | {modifiers} | `{EscapeMdTable(e.MemberKind)}` | {EscapeMdTable(e.MemberType)} | {EscapeMdTable(e.MemberName)} | {EscapeMdTable(e.Details)} |"); } } else @@ -221,7 +223,7 @@ public void Write(StreamWriter writer, ReportWriteContext ctx) writer.WriteLine("- Other changes only. See IL diff for details."); } - writer.WriteLine($"- Method count: {summary.OldMethodCount} (Old) vs {summary.NewMethodCount} (New)"); + writer.WriteLine($"- Member count: {summary.OldMethodCount} (Old) vs {summary.NewMethodCount} (New)"); } writer.WriteLine(); diff --git a/Services/ReportGenerateService.cs b/Services/ReportGenerateService.cs index dd54e981..730d75ec 100644 --- a/Services/ReportGenerateService.cs +++ b/Services/ReportGenerateService.cs @@ -61,7 +61,7 @@ public ReportGenerateService(FileDiffResultLists fileDiffResultLists, ILoggerSer private const string REPORT_LOCATION_BOTH = "(old/new)"; private const string REPORT_TIMESTAMP_ARROW = " → "; private const string REPORT_SECTION_SUMMARY = REPORT_SECTION_PREFIX + "Summary"; - private const string REPORT_SECTION_METHOD_LEVEL_CHANGES = REPORT_SECTION_PREFIX + "Method-Level Changes"; + private const string REPORT_SECTION_ASSEMBLY_SEMANTIC_CHANGES = REPORT_SECTION_PREFIX + "Assembly Semantic Changes"; private const string REPORT_SECTION_IL_CACHE_STATS = REPORT_SECTION_PREFIX + "IL Cache Stats"; private const string WARNING_NEW_FILE_TIMESTAMP_OLDER_THAN_OLD = "One or more **modified** files in `new` have older last-modified timestamps than the corresponding files in `old`."; private const string REPORT_SECTION_WARNINGS = REPORT_SECTION_PREFIX + "Warnings"; From 27cd6eb2f0d480b04eefe8a7628ee5ea5cf578fb Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 18:01:23 +0000 Subject: [PATCH 08/36] Fix Property Type column, HTML table CSS, summary label, UserRecord row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Property Type column: show type only (e.g. "int"), not "int { get; set; }" - Add BuildPropertyType() and PropertyType field to PropertyDetail - Use PropertyType instead of StripColonPrefix(Details) for MemberType - Fix HTML table layout: exclude .method-changes-table from table-layout:fixed/width:1px CSS rule that was collapsing all columns - Rename "Show member changes" → "Show assembly semantic changes" - Fix UserRecord constructor row in diff_report.md: add missing empty Type column cell so columns align correctly - Regenerate base64-encoded HTML in diff_report.html sample - Update tests, README examples https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- .../HtmlReportGenerateServiceTests.cs | 8 +++--- .../Services/ReportGenerateServiceTests.cs | 4 +-- Models/MemberChangeEntry.cs | 2 +- README.md | 4 +-- Services/AssemblyMethodAnalyzer.cs | 25 +++++++++++++++++-- .../HtmlReportGenerateService.Css.cs | 2 +- .../HtmlReportGenerateService.Sections.cs | 4 +-- doc/samples/diff_report.html | 14 +++++------ doc/samples/diff_report.md | 8 +++--- 9 files changed, 46 insertions(+), 25 deletions(-) diff --git a/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs b/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs index 9f1819fe..d7b54bd5 100644 --- a/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs @@ -734,7 +734,7 @@ public void GenerateDiffReportHtml_MethodLevelChanges_ShowsInlineAboveILDiff() { new("Added", "MyApp.Service", "public", "", "Method", "NewMethod", "", "void (string name)"), new("Modified", "MyApp.Service", "public", "virtual", "Method", "ExistingMethod", "", "bool (int id)"), - new("Added", "MyApp.Service", "public", "", "Property", "NewProp", "string { get; set; }", ""), + new("Added", "MyApp.Service", "public", "", "Property", "NewProp", "string", ""), new("Removed", "MyApp.Service", "private", "readonly", "Field", "_oldField", "int", ""), }, }; @@ -746,7 +746,7 @@ public void GenerateDiffReportHtml_MethodLevelChanges_ShowsInlineAboveILDiff() computerName: "test-host", config); var html = File.ReadAllText(Path.Combine(reportDir, HtmlReportGenerateService.DIFF_REPORT_HTML_FILE_NAME)); - Assert.Contains("Show member changes", html); + Assert.Contains("Show assembly semantic changes", html); Assert.Contains("methods_mod_0", html); Assert.Contains("method-changes-table", html); } @@ -776,7 +776,7 @@ public void GenerateDiffReportHtml_MethodLevelChanges_NotShownWhenDisabled() computerName: "test-host", config); var html = File.ReadAllText(Path.Combine(reportDir, HtmlReportGenerateService.DIFF_REPORT_HTML_FILE_NAME)); - Assert.DoesNotContain("Show member changes", html); + Assert.DoesNotContain("Show assembly semantic changes", html); } [Fact] @@ -806,7 +806,7 @@ public void GenerateDiffReportHtml_MethodLevelChanges_LazyRender_EncodesAsBase64 var html = File.ReadAllText(Path.Combine(reportDir, HtmlReportGenerateService.DIFF_REPORT_HTML_FILE_NAME)); // Should contain a data-diff-html attribute for the method changes row Assert.Contains("methods_mod_0", html); - Assert.Contains("Show member changes", html); + Assert.Contains("Show assembly semantic changes", html); // Content should NOT be inline (lazy rendered) — table markup is base64-encoded Assert.DoesNotContain("method-changes-table", html); Assert.Contains("data-diff-html", html); diff --git a/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs b/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs index a68229f6..f11d865e 100644 --- a/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs @@ -780,7 +780,7 @@ public void GenerateDiffReport_AssemblySemanticChanges_IncludedBetweenSummaryAnd new("Added", "MyApp.UserService", "internal", "", "Method", "RefreshSession", "", "void (int userId)"), new("Removed", "MyApp.UserService", "public", "virtual", "Method", "LegacyAuth", "", "void (string key)"), new("Modified", "MyApp.UserService", "public", "", "Method", "Login", "", "bool (string user, string pass)"), - new("Added", "MyApp.UserService", "public", "", "Property", "IsActive", "bool { get; set; }", ""), + new("Added", "MyApp.UserService", "public", "", "Property", "IsActive", "bool", ""), new("Added", "MyApp.UserService", "private", "readonly", "Field", "_cache", "object", ""), }, }; @@ -805,7 +805,7 @@ public void GenerateDiffReport_AssemblySemanticChanges_IncludedBetweenSummaryAnd Assert.Contains("| src/App.dll | `Added` | MyApp.NewService | | | `Type` | | | |", reportText); Assert.Contains("| src/App.dll | `Added` | MyApp.UserService | `public` | `static` | `Method` | | ValidateToken | bool (string token) |", reportText); Assert.Contains("| src/App.dll | `Modified` | MyApp.UserService | `public` | | `Method` | | Login |", reportText); - Assert.Contains("| src/App.dll | `Added` | MyApp.UserService | `public` | | `Property` | bool { get; set; } | IsActive |", reportText); + Assert.Contains("| src/App.dll | `Added` | MyApp.UserService | `public` | | `Property` | bool | IsActive |", reportText); Assert.Contains("| src/App.dll | `Added` | MyApp.UserService | `private` | `readonly` | `Field` | object | _cache |", reportText); Assert.Contains("- Member count: 42 (Old) vs 44 (New)", reportText); diff --git a/Models/MemberChangeEntry.cs b/Models/MemberChangeEntry.cs index 4382b88e..68d5b8b7 100644 --- a/Models/MemberChangeEntry.cs +++ b/Models/MemberChangeEntry.cs @@ -10,7 +10,7 @@ namespace FolderDiffIL4DotNet.Models /// Other modifiers (static, abstract, virtual, sealed, override, etc.). / その他の修飾子。 /// Member kind: "Type", "Method", "Property", "Field". / メンバー種別。 /// Member name (C# name; constructors use the class name, not .ctor). Empty for Type entries. / メンバー名(C# 名、コンストラクタは .ctor ではなくクラス名)。Type の場合は空。 - /// For Field/Property: the declared type (e.g. "string", "int { get; set; }"). Empty for Method/Type entries. / フィールド・プロパティの宣言型。メソッド・Type の場合は空。 + /// For Field/Property: the declared type (e.g. "string", "int"). Empty for Method/Type entries. / フィールド・プロパティの宣言型。メソッド・Type の場合は空。 /// /// For methods: ReturnType (Type paramName, ...). Empty for Type/Field/Property entries. /// メソッドなら ReturnType (Type paramName, ...)。Type/Field/Property の場合は空。 diff --git a/README.md b/README.md index b21071a9..8f193964 100644 --- a/README.md +++ b/README.md @@ -211,7 +211,7 @@ When an assembly is classified as `ILMismatch`, the tool performs an additional | Access | Access modifier | `public` | | Modifiers | Other modifiers | `static` | | Kind | Member kind | `Method` | -| Type | Declared type for Field/Property (empty for Method/Type) | `int { get; set; }` | +| Type | Declared type for Field/Property (empty for Method/Type) | `int` | | Name | Member name (constructors use the class name; empty for Type entries) | `DoWork` | | ReturnType (Type paramName) | Method signature (empty for Field/Property/Type) | `void (string name, int count = 0)` | @@ -690,7 +690,7 @@ flowchart TD | Access | アクセス修飾子 | `public` | | Modifiers | その他の修飾子 | `static` | | Kind | メンバー種別 | `Method` | -| Type | Field/Property の宣言型(Method/Type の場合は空) | `int { get; set; }` | +| Type | Field/Property の宣言型(Method/Type の場合は空) | `int` | | Name | メンバー名(コンストラクタはクラス名、Type エントリの場合は空) | `DoWork` | | ReturnType (Type paramName) | メソッドシグネチャ(Field/Property/Type の場合は空) | `void (string name, int count = 0)` | diff --git a/Services/AssemblyMethodAnalyzer.cs b/Services/AssemblyMethodAnalyzer.cs index 1ab9c9be..0541d239 100644 --- a/Services/AssemblyMethodAnalyzer.cs +++ b/Services/AssemblyMethodAnalyzer.cs @@ -65,12 +65,12 @@ internal static class AssemblyMethodAnalyzer foreach (var key in newSnapshot.Properties.Keys.Except(oldSnapshot.Properties.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var p = newSnapshot.Properties[key]; - entries.Add(new MemberChangeEntry("Added", p.TypeName, p.Access, p.Modifiers, "Property", p.PropertyName, StripColonPrefix(p.Details), "")); + entries.Add(new MemberChangeEntry("Added", p.TypeName, p.Access, p.Modifiers, "Property", p.PropertyName, p.PropertyType, "")); } foreach (var key in oldSnapshot.Properties.Keys.Except(newSnapshot.Properties.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var p = oldSnapshot.Properties[key]; - entries.Add(new MemberChangeEntry("Removed", p.TypeName, p.Access, p.Modifiers, "Property", p.PropertyName, StripColonPrefix(p.Details), "")); + entries.Add(new MemberChangeEntry("Removed", p.TypeName, p.Access, p.Modifiers, "Property", p.PropertyName, p.PropertyType, "")); } // Fields @@ -118,6 +118,7 @@ private sealed class PropertyDetail public required string Access { get; init; } public required string Modifiers { get; init; } public required string PropertyName { get; init; } + public required string PropertyType { get; init; } public required string Details { get; init; } } @@ -188,6 +189,7 @@ private static AssemblySnapshot ReadAssemblySnapshot(string assemblyPath) string propKey = $"{typeName}::{propName}"; string propAccess = GetPropertyAccess(reader, propDef); string propModifiers = GetPropertyModifiers(reader, propDef); + string propType = BuildPropertyType(reader, propDef, typeProvider); string propDetails = BuildPropertyDetails(reader, propDef, typeProvider); snapshot.Properties[propKey] = new PropertyDetail @@ -196,6 +198,7 @@ private static AssemblySnapshot ReadAssemblySnapshot(string assemblyPath) Access = propAccess, Modifiers = propModifiers, PropertyName = propName, + PropertyType = propType, Details = propDetails, }; } @@ -335,6 +338,24 @@ private static string BuildMethodDetails(MetadataReader reader, MethodDefinition #pragma warning restore CA1031 } + /// Extract the declared type of a property (without accessor info). / プロパティの宣言型を抽出(アクセサ情報なし)。 + private static string BuildPropertyType(MetadataReader reader, PropertyDefinition propDef, SimpleSignatureTypeProvider typeProvider) + { + try + { + var sigBlobReader = reader.GetBlobReader(propDef.Signature); + var decoder = new SignatureDecoder(typeProvider, reader, genericContext: null); + var signature = decoder.DecodeMethodSignature(ref sigBlobReader); + return signature.ReturnType; + } +#pragma warning disable CA1031 + catch + { + return ""; + } +#pragma warning restore CA1031 + } + /// Build property details: ": Type { get; set; }". private static string BuildPropertyDetails(MetadataReader reader, PropertyDefinition propDef, SimpleSignatureTypeProvider typeProvider) { diff --git a/Services/HtmlReport/HtmlReportGenerateService.Css.cs b/Services/HtmlReport/HtmlReportGenerateService.Css.cs index dbda87c8..13c88304 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Css.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Css.cs @@ -65,7 +65,7 @@ private static string GetCss() /* ── Data tables ─────────────────────────────────────────────────────── */ .table-scroll { overflow-x: auto; margin-bottom: 1.2rem; } table { border-collapse: collapse; width: 100%; margin-bottom: 1.2rem; } - table:not(.stat-table):not(.diff-table) { table-layout: fixed; width: 1px; margin-bottom: 0; } + table:not(.stat-table):not(.diff-table):not(.method-changes-table) { table-layout: fixed; width: 1px; margin-bottom: 0; } th { padding: 4px 6px; font-size: 12px; white-space: nowrap; overflow: hidden; text-align: left; border: 1px solid #bbb; color: #000; } th.th-resizable { position: relative; } diff --git a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs index 04478455..bcd91782 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs @@ -382,8 +382,8 @@ private void AppendMethodLevelChangesRow( string detailsId = $"methods_{sectionPrefix}_{idx}"; string summaryText = totalChanges > 0 - ? $"#{recordNo} Show member changes ({totalChanges} change{(totalChanges == 1 ? "" : "s")})" - : $"#{recordNo} Show member changes (other changes only)"; + ? $"#{recordNo} Show assembly semantic changes ({totalChanges} change{(totalChanges == 1 ? "" : "s")})" + : $"#{recordNo} Show assembly semantic changes (other changes only)"; string summaryLabel = $" {HtmlEncode(summaryText)}"; string contentHtml = contentBuilder.ToString(); diff --git a/doc/samples/diff_report.html b/doc/samples/diff_report.html index 15b67916..bf163d9e 100644 --- a/doc/samples/diff_report.html +++ b/doc/samples/diff_report.html @@ -63,7 +63,7 @@ /* ── Data tables ─────────────────────────────────────────────────────── */ .table-scroll { overflow-x: auto; margin-bottom: 1.2rem; } table { border-collapse: collapse; width: 100%; margin-bottom: 1.2rem; } - table:not(.stat-table):not(.diff-table) { table-layout: fixed; width: 1px; margin-bottom: 0; } + table:not(.stat-table):not(.diff-table):not(.method-changes-table) { table-layout: fixed; width: 1px; margin-bottom: 0; } th { padding: 4px 6px; font-size: 12px; white-space: nowrap; overflow: hidden; text-align: left; border: 1px solid #bbb; color: #000; } th.th-resizable { position: relative; } @@ -437,8 +437,8 @@

[ * ] Modified Files (9)

-
- #3 Show member changes (5 changes) +
+ #3 Show assembly semantic changes (5 changes)
@@ -478,8 +478,8 @@

[ * ] Modified Files (9)

-
- #5 Show member changes (16 changes) +
+ #5 Show assembly semantic changes (16 changes)
@@ -546,7 +546,7 @@

[ * ] Modified Files (9)

- #9 Show member changes (other changes only) + #9 Show assembly semantic changes (other changes only)
@@ -759,7 +759,7 @@

[ ! ] Modified Files — Timestamps Regressed (2) + px('--col-reason-w', 10) + px('--col-notes-w', 10) + px('--col-path-w', 22) + px('--col-diff-w', 9) + px('--col-disasm-w', 28); - document.querySelectorAll('table:not(.stat-table):not(.diff-table)').forEach(function(t) { + document.querySelectorAll('table:not(.stat-table):not(.diff-table):not(.method-changes-table)').forEach(function(t) { t.style.width = w + 'px'; }); } diff --git a/doc/samples/diff_report.md b/doc/samples/diff_report.md index 5e010233..665ab144 100644 --- a/doc/samples/diff_report.md +++ b/doc/samples/diff_report.md @@ -62,7 +62,7 @@ | src/App.dll | `Modified` | MyApp.Controllers.ApiController | `public` | `virtual` | `Method` | | GetUsers | System.Collections.Generic.IList\ (int page) | | src/App.dll | `Modified` | MyApp.Services.DataService | `internal` | | `Method` | | RefreshCache | void () | | src/App.dll | `Modified` | MyApp.Services.DataService | `private` | | `Method` | | ValidateConnection | bool (string connStr) | -| src/App.dll | `Added` | MyApp.Services.DataService | `public` | | `Property` | int { get; set; } | CacheTimeout | | +| src/App.dll | `Added` | MyApp.Services.DataService | `public` | | `Property` | int | CacheTimeout | | - Member count: 28 (Old) vs 29 (New) ### src/Service.dll @@ -79,9 +79,9 @@ | src/Service.dll | `Modified` | MyApp.Services.OrderService | `internal` | `static` | `Method` | | CalculateTotal | decimal (int qty, int price) | | src/Service.dll | `Added` | MyApp.Services.NewValidator | `private` | `readonly` | `Field` | string | _pattern | | | src/Service.dll | `Added` | MyApp.Models.UserRecord | | | `Type` | | | | -| src/Service.dll | `Added` | MyApp.Models.UserRecord | `public` | | `Method` | UserRecord | void (string Name, int Age) | -| src/Service.dll | `Added` | MyApp.Models.UserRecord | `public` | | `Property` | string { get; init; } | Name | | -| src/Service.dll | `Added` | MyApp.Models.UserRecord | `public` | | `Property` | int { get; init; } | Age | | +| src/Service.dll | `Added` | MyApp.Models.UserRecord | `public` | | `Method` | | UserRecord | void (string Name, int Age) | +| src/Service.dll | `Added` | MyApp.Models.UserRecord | `public` | | `Property` | string | Name | | +| src/Service.dll | `Added` | MyApp.Models.UserRecord | `public` | | `Property` | int | Age | | | src/Service.dll | `Added` | MyApp.Models.UserRecord | `public` | `override` | `Method` | | ToString | string () | | src/Service.dll | `Added` | MyApp.Models.UserRecord | `public` | `virtual` | `Method` | | Equals | bool (object obj) | | src/Service.dll | `Added` | MyApp.Models.UserRecord | `public` | `override` | `Method` | | GetHashCode | int () | From 84acda14a3a6f1bd2b296ecab38e54cc311066d4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 18:12:08 +0000 Subject: [PATCH 09/36] Add _defaultUser field example to Service.dll sample reports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add OrderService._defaultUser (MyApp.Models.UserRecord) field row to both MD and HTML sample reports - Update Service.dll member count from 25→26 (New) - Update HTML change count from 16→17 https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- doc/samples/diff_report.html | 4 ++-- doc/samples/diff_report.md | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/samples/diff_report.html b/doc/samples/diff_report.html index bf163d9e..8c4e2770 100644 --- a/doc/samples/diff_report.html +++ b/doc/samples/diff_report.html @@ -478,8 +478,8 @@

[ * ] Modified Files (9)

-
- #5 Show assembly semantic changes (16 changes) +
+ #5 Show assembly semantic changes (17 changes)
diff --git a/doc/samples/diff_report.md b/doc/samples/diff_report.md index 665ab144..92280b32 100644 --- a/doc/samples/diff_report.md +++ b/doc/samples/diff_report.md @@ -77,6 +77,7 @@ | src/Service.dll | `Removed` | MyApp.Services.OrderService | `public` | `virtual` | `Method` | | LegacyValidate | bool (string data) | | src/Service.dll | `Modified` | MyApp.Services.OrderService | `public` | | `Method` | | ProcessOrder | void (int orderId) | | src/Service.dll | `Modified` | MyApp.Services.OrderService | `internal` | `static` | `Method` | | CalculateTotal | decimal (int qty, int price) | +| src/Service.dll | `Added` | MyApp.Services.OrderService | `private` | `readonly` | `Field` | MyApp.Models.UserRecord | _defaultUser | | | src/Service.dll | `Added` | MyApp.Services.NewValidator | `private` | `readonly` | `Field` | string | _pattern | | | src/Service.dll | `Added` | MyApp.Models.UserRecord | | | `Type` | | | | | src/Service.dll | `Added` | MyApp.Models.UserRecord | `public` | | `Method` | | UserRecord | void (string Name, int Age) | @@ -85,7 +86,7 @@ | src/Service.dll | `Added` | MyApp.Models.UserRecord | `public` | `override` | `Method` | | ToString | string () | | src/Service.dll | `Added` | MyApp.Models.UserRecord | `public` | `virtual` | `Method` | | Equals | bool (object obj) | | src/Service.dll | `Added` | MyApp.Models.UserRecord | `public` | `override` | `Method` | | GetHashCode | int () | -- Member count: 15 (Old) vs 25 (New) +- Member count: 15 (Old) vs 26 (New) ### util/Legacy.dll - Other changes only. See IL diff for details. From 85c8ffa77d5cca4760f3862af6aaed5c6c9494cc Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 18:27:31 +0000 Subject: [PATCH 10/36] Restructure Assembly Semantic Changes table to 10 columns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Split ReturnType (Type paramName) into separate ReturnType and Parameters columns for clarity - Add Constructor and StaticConstructor as Kind values (.ctor/.cctor were previously shown as Method) - Move Kind column before Access/Modifiers for better readability - Update MemberChangeEntry model from 8 to 9 parameters - Refactor BuildMethodDetails into BuildMethodSignatureParts returning (ReturnType, Parameters) tuple - Add ToMemberKind helper for .ctor → Constructor mapping - Update all report generators, tests, samples, README, and CHANGELOG https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- CHANGELOG.md | 4 +- .../Models/MethodLevelChangesSummaryTests.cs | 7 +-- .../Services/AssemblyMethodAnalyzerTests.cs | 2 +- .../HtmlReportGenerateServiceTests.cs | 12 ++--- .../Services/ReportGenerateServiceTests.cs | 28 +++++----- Models/MemberChangeEntry.cs | 13 +++-- README.md | 14 ++--- Services/AssemblyMethodAnalyzer.cs | 51 ++++++++++++------ .../HtmlReportGenerateService.Sections.cs | 4 +- .../ReportGenerateService.SectionWriters.cs | 6 +-- doc/samples/diff_report.html | 4 +- doc/samples/diff_report.md | 52 +++++++++---------- 12 files changed, 108 insertions(+), 89 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d538811a..841781a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - Added member-level change detection for `ILMismatch` assemblies using `System.Reflection.Metadata`. For each modified .NET assembly, the report now shows type/method/property/field additions, removals, and method body changes. This new **Assembly Semantic Changes** section appears between **Summary** and **IL Cache Stats** in the Markdown report, and as an expandable inline row above the IL diff in the HTML report. Controlled by the new `ShouldIncludeMethodLevelChangesInReport` config setting (default: `true`). Added [`AssemblyMethodAnalyzer`](Services/AssemblyMethodAnalyzer.cs), [`MethodLevelChangesSummary`](Models/MethodLevelChangesSummary.cs), and corresponding tests. -- Added **Modifiers** column and **Type** column to the Assembly Semantic Changes table. The **Type** column shows the declared type for Field/Property entries (e.g. `int { get; set; }`, `string`), while the **ReturnType (Type paramName)** column shows method signatures. Constructors now display the C# class name instead of `.ctor`. Empty Access/Modifiers cells no longer render as empty backticks. Renamed **Detail** column to split **Type** + **ReturnType (Type paramName)**. Changed `Method count` label to `Member count`. Renamed section from `Method-Level Changes` to `Assembly Semantic Changes`. Added record type sample to [`doc/samples/diff_report.md`](doc/samples/diff_report.md). Added bilingual **Assembly Semantic Changes** section to [README.md](README.md). +- Restructured the Assembly Semantic Changes table from 9 columns to 10 columns for clarity. Split the former `ReturnType (Type paramName)` column into separate `ReturnType` and `Parameters` columns. Moved `Kind` column before `Access` and `Modifiers` for better readability. Added `Constructor` and `StaticConstructor` as new Kind values (previously `.ctor`/`.cctor` were shown as `Method`). Constructors display the C# class name instead of `.ctor`. The `Type` column shows the declared type for Field/Property entries only. Empty Access/Modifiers cells no longer render as empty backticks. Changed `Method count` label to `Member count`. Renamed section from `Method-Level Changes` to `Assembly Semantic Changes`. Added record type and field variable samples to [`doc/samples/diff_report.md`](doc/samples/diff_report.md). Added bilingual **Assembly Semantic Changes** section to [README.md](README.md). ### [1.4.1] - 2026-03-20 @@ -375,7 +375,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - `System.Reflection.Metadata` を使用した `ILMismatch` アセンブリのメンバーレベル変更検出を追加。変更のあった各 .NET アセンブリについて、型・メソッド・プロパティ・フィールドの増減およびメソッドボディの変更をレポートに出力します。Markdown レポートでは **Summary** と **IL Cache Stats** の間に **Assembly Semantic Changes** セクションとして表示され、HTML レポートでは IL diff の上に展開可能なインライン行として表示されます。新しい設定項目 `ShouldIncludeMethodLevelChangesInReport`(既定: `true`)で制御可能。[`AssemblyMethodAnalyzer`](Services/AssemblyMethodAnalyzer.cs)、[`MethodLevelChangesSummary`](Models/MethodLevelChangesSummary.cs)、および対応するテストを追加。 -- Assembly Semantic Changes テーブルに **Modifiers** 列と **Type** 列を追加。**Type** 列は Field/Property の宣言型(例: `int { get; set; }`、`string`)を表示し、**ReturnType (Type paramName)** 列はメソッドシグネチャを表示します。コンストラクタは `.ctor` ではなく C# のクラス名で表示。空の Access/Modifiers セルは空バッククォートではなく空欄に。**Detail** 列を **Type** + **ReturnType (Type paramName)** に分割。`Method count` ラベルを `Member count` に変更。セクション名を `Method-Level Changes` から `Assembly Semantic Changes` に改名。[`doc/samples/diff_report.md`](doc/samples/diff_report.md) に record 型のサンプルを追加。[README.md](README.md) にバイリンガルの **アセンブリ セマンティック変更** セクションを追加。 +- Assembly Semantic Changes テーブルを 9 列から 10 列に再構成し明確化。旧 `ReturnType (Type paramName)` 列を `ReturnType` 列と `Parameters` 列に分離。`Kind` 列を `Access`・`Modifiers` の前に移動。Kind 値に `Constructor` と `StaticConstructor` を追加(従来 `.ctor`/`.cctor` は `Method` として表示)。コンストラクタは `.ctor` ではなく C# のクラス名で表示。`Type` 列は Field/Property の宣言型のみを表示。空の Access/Modifiers セルは空バッククォートではなく空欄に。`Method count` ラベルを `Member count` に変更。セクション名を `Method-Level Changes` から `Assembly Semantic Changes` に改名。[`doc/samples/diff_report.md`](doc/samples/diff_report.md) に record 型およびフィールド変数のサンプルを追加。[README.md](README.md) にバイリンガルの **アセンブリ セマンティック変更** セクションを追加。 ### [1.4.1] - 2026-03-20 diff --git a/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs b/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs index 464f8354..ce01e36a 100644 --- a/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs +++ b/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs @@ -20,7 +20,7 @@ public void HasChanges_WithEntries_ReturnsTrue() { Entries = new List { - new("Added", "MyApp.Service", "public", "", "Method", "DoWork", "", "void (int count)"), + new("Added", "MyApp.Service", "public", "", "Method", "DoWork", "", "void", "(int count)"), }, }; Assert.True(summary.HasChanges); @@ -41,7 +41,7 @@ public void HasChanges_EmptyEntries_ReturnsFalse() [Fact] public void Entries_ContainStructuredData() { - var entry = new MemberChangeEntry("Added", "MyApp.Service", "public", "static", "Method", "GetName", "", "string (string id)"); + var entry = new MemberChangeEntry("Added", "MyApp.Service", "public", "static", "Method", "GetName", "", "string", "(string id)"); Assert.Equal("Added", entry.Change); Assert.Equal("MyApp.Service", entry.TypeName); Assert.Equal("public", entry.Access); @@ -49,7 +49,8 @@ public void Entries_ContainStructuredData() Assert.Equal("Method", entry.MemberKind); Assert.Equal("GetName", entry.MemberName); Assert.Equal("", entry.MemberType); - Assert.Equal("string (string id)", entry.Details); + Assert.Equal("string", entry.ReturnType); + Assert.Equal("(string id)", entry.Parameters); } } } diff --git a/FolderDiffIL4DotNet.Tests/Services/AssemblyMethodAnalyzerTests.cs b/FolderDiffIL4DotNet.Tests/Services/AssemblyMethodAnalyzerTests.cs index 4d356e0d..782d5267 100644 --- a/FolderDiffIL4DotNet.Tests/Services/AssemblyMethodAnalyzerTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/AssemblyMethodAnalyzerTests.cs @@ -72,7 +72,7 @@ public void Analyze_DifferentAssemblies_EntriesHaveStructuredData() Assert.False(string.IsNullOrEmpty(firstEntry.TypeName)); Assert.False(string.IsNullOrEmpty(firstEntry.MemberKind)); Assert.Contains(firstEntry.Change, new[] { "Added", "Removed", "Modified" }); - Assert.Contains(firstEntry.MemberKind, new[] { "Type", "Method", "Property", "Field" }); + Assert.Contains(firstEntry.MemberKind, new[] { "Type", "Constructor", "StaticConstructor", "Method", "Property", "Field" }); } } } diff --git a/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs b/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs index d7b54bd5..97018b67 100644 --- a/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs @@ -732,10 +732,10 @@ public void GenerateDiffReportHtml_MethodLevelChanges_ShowsInlineAboveILDiff() NewMethodCount = 12, Entries = new List { - new("Added", "MyApp.Service", "public", "", "Method", "NewMethod", "", "void (string name)"), - new("Modified", "MyApp.Service", "public", "virtual", "Method", "ExistingMethod", "", "bool (int id)"), - new("Added", "MyApp.Service", "public", "", "Property", "NewProp", "string", ""), - new("Removed", "MyApp.Service", "private", "readonly", "Field", "_oldField", "int", ""), + new("Added", "MyApp.Service", "public", "", "Method", "NewMethod", "", "void", "(string name)"), + new("Modified", "MyApp.Service", "public", "virtual", "Method", "ExistingMethod", "", "bool", "(int id)"), + new("Added", "MyApp.Service", "public", "", "Property", "NewProp", "string", "", ""), + new("Removed", "MyApp.Service", "private", "readonly", "Field", "_oldField", "int", "", ""), }, }; @@ -765,7 +765,7 @@ public void GenerateDiffReportHtml_MethodLevelChanges_NotShownWhenDisabled() NewMethodCount = 12, Entries = new List { - new("Added", "MyApp.Service", "public", "", "Method", "NewMethod", "", "void (string name)"), + new("Added", "MyApp.Service", "public", "", "Method", "NewMethod", "", "void", "(string name)"), }, }; @@ -793,7 +793,7 @@ public void GenerateDiffReportHtml_MethodLevelChanges_LazyRender_EncodesAsBase64 NewMethodCount = 6, Entries = new List { - new("Added", "Foo", "public", "", "Method", "Bar", "", "void ()"), + new("Added", "Foo", "public", "", "Method", "Bar", "", "void", "()"), }, }; diff --git a/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs b/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs index f11d865e..90b92be8 100644 --- a/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs @@ -775,13 +775,13 @@ public void GenerateDiffReport_AssemblySemanticChanges_IncludedBetweenSummaryAnd NewMethodCount = 44, Entries = new List { - new("Added", "MyApp.NewService", "", "", "Type", "", "", ""), - new("Added", "MyApp.UserService", "public", "static", "Method", "ValidateToken", "", "bool (string token)"), - new("Added", "MyApp.UserService", "internal", "", "Method", "RefreshSession", "", "void (int userId)"), - new("Removed", "MyApp.UserService", "public", "virtual", "Method", "LegacyAuth", "", "void (string key)"), - new("Modified", "MyApp.UserService", "public", "", "Method", "Login", "", "bool (string user, string pass)"), - new("Added", "MyApp.UserService", "public", "", "Property", "IsActive", "bool", ""), - new("Added", "MyApp.UserService", "private", "readonly", "Field", "_cache", "object", ""), + new("Added", "MyApp.NewService", "", "", "Type", "", "", "", ""), + new("Added", "MyApp.UserService", "public", "static", "Method", "ValidateToken", "", "bool", "(string token)"), + new("Added", "MyApp.UserService", "internal", "", "Method", "RefreshSession", "", "void", "(int userId)"), + new("Removed", "MyApp.UserService", "public", "virtual", "Method", "LegacyAuth", "", "void", "(string key)"), + new("Modified", "MyApp.UserService", "public", "", "Method", "Login", "", "bool", "(string user, string pass)"), + new("Added", "MyApp.UserService", "public", "", "Property", "IsActive", "bool", "", ""), + new("Added", "MyApp.UserService", "private", "readonly", "Field", "_cache", "object", "", ""), }, }; _resultLists.FileRelativePathToMethodLevelChanges["src/App.dll"] = summary; @@ -801,12 +801,12 @@ public void GenerateDiffReport_AssemblySemanticChanges_IncludedBetweenSummaryAnd // Content checks — table format Assert.Contains("## Assembly Semantic Changes", reportText); Assert.Contains("### src/App.dll", reportText); - Assert.Contains("| Assembly | Change | Class | Access | Modifiers | Kind | Type | Name | ReturnType (Type paramName) |", reportText); - Assert.Contains("| src/App.dll | `Added` | MyApp.NewService | | | `Type` | | | |", reportText); - Assert.Contains("| src/App.dll | `Added` | MyApp.UserService | `public` | `static` | `Method` | | ValidateToken | bool (string token) |", reportText); - Assert.Contains("| src/App.dll | `Modified` | MyApp.UserService | `public` | | `Method` | | Login |", reportText); - Assert.Contains("| src/App.dll | `Added` | MyApp.UserService | `public` | | `Property` | bool | IsActive |", reportText); - Assert.Contains("| src/App.dll | `Added` | MyApp.UserService | `private` | `readonly` | `Field` | object | _cache |", reportText); + Assert.Contains("| Assembly | Change | Class | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters |", reportText); + Assert.Contains("| src/App.dll | `Added` | MyApp.NewService | `Type` | | | | | | |", reportText); + Assert.Contains("| src/App.dll | `Added` | MyApp.UserService | `Method` | `public` | `static` | | ValidateToken | bool | (string token) |", reportText); + Assert.Contains("| src/App.dll | `Modified` | MyApp.UserService | `Method` | `public` | | | Login | bool | (string user, string pass) |", reportText); + Assert.Contains("| src/App.dll | `Added` | MyApp.UserService | `Property` | `public` | | bool | IsActive | | |", reportText); + Assert.Contains("| src/App.dll | `Added` | MyApp.UserService | `Field` | `private` | `readonly` | object | _cache | | |", reportText); Assert.Contains("- Member count: 42 (Old) vs 44 (New)", reportText); // Ordering: Summary < Assembly Semantic Changes < IL Cache Stats @@ -833,7 +833,7 @@ public void GenerateDiffReport_MethodLevelChanges_NotIncludedWhenDisabled() NewMethodCount = 12, Entries = new List { - new("Added", "Foo", "public", "", "Method", "Bar", "", "void ()"), + new("Added", "Foo", "public", "", "Method", "Bar", "", "void", "()"), }, }; diff --git a/Models/MemberChangeEntry.cs b/Models/MemberChangeEntry.cs index 68d5b8b7..0b0def6c 100644 --- a/Models/MemberChangeEntry.cs +++ b/Models/MemberChangeEntry.cs @@ -8,13 +8,11 @@ namespace FolderDiffIL4DotNet.Models /// Owning type name (or the type itself for Type entries). / 所属型名(Type エントリの場合は型名そのもの)。 /// Access modifier. Empty for Type entries. / アクセス修飾子。Type の場合は空。 /// Other modifiers (static, abstract, virtual, sealed, override, etc.). / その他の修飾子。 - /// Member kind: "Type", "Method", "Property", "Field". / メンバー種別。 + /// Member kind: "Type", "Constructor", "StaticConstructor", "Method", "Property", "Field". / メンバー種別。 /// Member name (C# name; constructors use the class name, not .ctor). Empty for Type entries. / メンバー名(C# 名、コンストラクタは .ctor ではなくクラス名)。Type の場合は空。 - /// For Field/Property: the declared type (e.g. "string", "int"). Empty for Method/Type entries. / フィールド・プロパティの宣言型。メソッド・Type の場合は空。 - /// - /// For methods: ReturnType (Type paramName, ...). Empty for Type/Field/Property entries. - /// メソッドなら ReturnType (Type paramName, ...)。Type/Field/Property の場合は空。 - /// + /// For Field/Property: the declared type (e.g. "string", "int"). Empty for Method/Constructor/Type entries. / フィールド・プロパティの宣言型。メソッド・コンストラクタ・Type の場合は空。 + /// For Method: the return type (e.g. "void", "string"). For Constructor: "void". Empty for Type/Field/Property entries. / メソッドの戻り値型。コンストラクタは "void"。Type/Field/Property の場合は空。 + /// For Method/Constructor: the parameter list including parentheses (e.g. "(int page)", "()"). Empty for Type/Field/Property entries. / メソッド・コンストラクタのパラメータ一覧(括弧含む)。Type/Field/Property の場合は空。 public sealed record MemberChangeEntry( string Change, string TypeName, @@ -23,5 +21,6 @@ public sealed record MemberChangeEntry( string MemberKind, string MemberName, string MemberType, - string Details); + string ReturnType, + string Parameters); } diff --git a/README.md b/README.md index 8f193964..36107042 100644 --- a/README.md +++ b/README.md @@ -208,12 +208,13 @@ When an assembly is classified as `ILMismatch`, the tool performs an additional | Assembly | Relative path of the assembly | `bin/MyLib.dll` | | Change | `Added`, `Removed`, or `Modified` | `Added` | | Class | Fully qualified type name | `MyNamespace.MyClass` | +| Kind | Member kind: `Type`, `Constructor`, `StaticConstructor`, `Method`, `Property`, `Field` | `Method` | | Access | Access modifier | `public` | | Modifiers | Other modifiers | `static` | -| Kind | Member kind | `Method` | -| Type | Declared type for Field/Property (empty for Method/Type) | `int` | +| Type | Declared type for Field/Property (empty for Method/Constructor/Type) | `int` | | Name | Member name (constructors use the class name; empty for Type entries) | `DoWork` | -| ReturnType (Type paramName) | Method signature (empty for Field/Property/Type) | `void (string name, int count = 0)` | +| ReturnType | Return type for Method/Constructor (empty for Field/Property/Type) | `void` | +| Parameters | Parameter list for Method/Constructor (empty for Field/Property/Type) | `(string name, int count = 0)` | Controlled by [`ShouldIncludeMethodLevelChangesInReport`](#config-en-shouldincludemethodlevelchangesinreport) (default: `true`). @@ -687,12 +688,13 @@ flowchart TD | Assembly | アセンブリの相対パス | `bin/MyLib.dll` | | Change | `Added`、`Removed`、`Modified` | `Added` | | Class | 完全修飾型名 | `MyNamespace.MyClass` | +| Kind | メンバー種別: `Type`, `Constructor`, `StaticConstructor`, `Method`, `Property`, `Field` | `Method` | | Access | アクセス修飾子 | `public` | | Modifiers | その他の修飾子 | `static` | -| Kind | メンバー種別 | `Method` | -| Type | Field/Property の宣言型(Method/Type の場合は空) | `int` | +| Type | Field/Property の宣言型(Method/Constructor/Type の場合は空) | `int` | | Name | メンバー名(コンストラクタはクラス名、Type エントリの場合は空) | `DoWork` | -| ReturnType (Type paramName) | メソッドシグネチャ(Field/Property/Type の場合は空) | `void (string name, int count = 0)` | +| ReturnType | Method/Constructor の戻り値型(Field/Property/Type の場合は空) | `void` | +| Parameters | Method/Constructor のパラメータ一覧(Field/Property/Type の場合は空) | `(string name, int count = 0)` | [`ShouldIncludeMethodLevelChangesInReport`](#config-ja-shouldincludemethodlevelchangesinreport)(既定値: `true`)で制御します。 diff --git a/Services/AssemblyMethodAnalyzer.cs b/Services/AssemblyMethodAnalyzer.cs index 0541d239..94e59bca 100644 --- a/Services/AssemblyMethodAnalyzer.cs +++ b/Services/AssemblyMethodAnalyzer.cs @@ -37,27 +37,30 @@ internal static class AssemblyMethodAnalyzer // Types foreach (var t in newSnapshot.TypeNames.Except(oldSnapshot.TypeNames, StringComparer.Ordinal).OrderBy(t => t, StringComparer.Ordinal)) - entries.Add(new MemberChangeEntry("Added", t, "", "", "Type", "", "", "")); + entries.Add(new MemberChangeEntry("Added", t, "", "", "Type", "", "", "", "")); foreach (var t in oldSnapshot.TypeNames.Except(newSnapshot.TypeNames, StringComparer.Ordinal).OrderBy(t => t, StringComparer.Ordinal)) - entries.Add(new MemberChangeEntry("Removed", t, "", "", "Type", "", "", "")); + entries.Add(new MemberChangeEntry("Removed", t, "", "", "Type", "", "", "", "")); - // Methods + // Methods (including constructors) foreach (var key in newSnapshot.Methods.Keys.Except(oldSnapshot.Methods.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var m = newSnapshot.Methods[key]; - entries.Add(new MemberChangeEntry("Added", m.TypeName, m.Access, m.Modifiers, "Method", ToCSharpMethodName(m.MethodName, m.TypeName), "", m.Details)); + string kind = ToMemberKind(m.MethodName); + entries.Add(new MemberChangeEntry("Added", m.TypeName, m.Access, m.Modifiers, kind, ToCSharpMethodName(m.MethodName, m.TypeName), "", m.ReturnType, m.Parameters)); } foreach (var key in oldSnapshot.Methods.Keys.Except(newSnapshot.Methods.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var m = oldSnapshot.Methods[key]; - entries.Add(new MemberChangeEntry("Removed", m.TypeName, m.Access, m.Modifiers, "Method", ToCSharpMethodName(m.MethodName, m.TypeName), "", m.Details)); + string kind = ToMemberKind(m.MethodName); + entries.Add(new MemberChangeEntry("Removed", m.TypeName, m.Access, m.Modifiers, kind, ToCSharpMethodName(m.MethodName, m.TypeName), "", m.ReturnType, m.Parameters)); } foreach (var key in oldSnapshot.Methods.Keys.Intersect(newSnapshot.Methods.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { if (!oldSnapshot.Methods[key].IlBytes.AsSpan().SequenceEqual(newSnapshot.Methods[key].IlBytes.AsSpan())) { var m = newSnapshot.Methods[key]; - entries.Add(new MemberChangeEntry("Modified", m.TypeName, m.Access, m.Modifiers, "Method", ToCSharpMethodName(m.MethodName, m.TypeName), "", m.Details)); + string kind = ToMemberKind(m.MethodName); + entries.Add(new MemberChangeEntry("Modified", m.TypeName, m.Access, m.Modifiers, kind, ToCSharpMethodName(m.MethodName, m.TypeName), "", m.ReturnType, m.Parameters)); } } @@ -65,24 +68,24 @@ internal static class AssemblyMethodAnalyzer foreach (var key in newSnapshot.Properties.Keys.Except(oldSnapshot.Properties.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var p = newSnapshot.Properties[key]; - entries.Add(new MemberChangeEntry("Added", p.TypeName, p.Access, p.Modifiers, "Property", p.PropertyName, p.PropertyType, "")); + entries.Add(new MemberChangeEntry("Added", p.TypeName, p.Access, p.Modifiers, "Property", p.PropertyName, p.PropertyType, "", "")); } foreach (var key in oldSnapshot.Properties.Keys.Except(newSnapshot.Properties.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var p = oldSnapshot.Properties[key]; - entries.Add(new MemberChangeEntry("Removed", p.TypeName, p.Access, p.Modifiers, "Property", p.PropertyName, p.PropertyType, "")); + entries.Add(new MemberChangeEntry("Removed", p.TypeName, p.Access, p.Modifiers, "Property", p.PropertyName, p.PropertyType, "", "")); } // Fields foreach (var key in newSnapshot.Fields.Keys.Except(oldSnapshot.Fields.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var f = newSnapshot.Fields[key]; - entries.Add(new MemberChangeEntry("Added", f.TypeName, f.Access, f.Modifiers, "Field", f.FieldName, StripColonPrefix(f.Details), "")); + entries.Add(new MemberChangeEntry("Added", f.TypeName, f.Access, f.Modifiers, "Field", f.FieldName, StripColonPrefix(f.Details), "", "")); } foreach (var key in oldSnapshot.Fields.Keys.Except(newSnapshot.Fields.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var f = oldSnapshot.Fields[key]; - entries.Add(new MemberChangeEntry("Removed", f.TypeName, f.Access, f.Modifiers, "Field", f.FieldName, StripColonPrefix(f.Details), "")); + entries.Add(new MemberChangeEntry("Removed", f.TypeName, f.Access, f.Modifiers, "Field", f.FieldName, StripColonPrefix(f.Details), "", "")); } return new MethodLevelChangesSummary @@ -108,7 +111,8 @@ private sealed class MethodDetail public required string Access { get; init; } public required string Modifiers { get; init; } public required string MethodName { get; init; } - public required string Details { get; init; } + public required string ReturnType { get; init; } + public required string Parameters { get; init; } public required byte[] IlBytes { get; init; } } @@ -167,7 +171,7 @@ private static AssemblySnapshot ReadAssemblySnapshot(string assemblyPath) string modifiers = GetMethodModifiers(methodDef.Attributes); string methodName = reader.GetString(methodDef.Name); string matchKey = BuildMethodMatchKey(reader, typeName, methodDef, typeProvider); - string details = BuildMethodDetails(reader, methodDef, typeProvider); + var (retType, parameters) = BuildMethodSignatureParts(reader, methodDef, typeProvider); byte[] ilBytes = ReadIlBytes(peReader, methodDef); snapshot.Methods[matchKey] = new MethodDetail @@ -176,7 +180,8 @@ private static AssemblySnapshot ReadAssemblySnapshot(string assemblyPath) Access = access, Modifiers = modifiers, MethodName = methodName, - Details = details, + ReturnType = retType, + Parameters = parameters, IlBytes = ilBytes, }; } @@ -244,6 +249,15 @@ private static string GetFullTypeName(MetadataReader reader, TypeDefinition type return string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}"; } + /// Determine the member kind from the IL method name: .ctor → Constructor, .cctor → StaticConstructor, else → Method. / IL メソッド名からメンバー種別を判定。 + private static string ToMemberKind(string ilMethodName) + => ilMethodName switch + { + ".ctor" => "Constructor", + ".cctor" => "StaticConstructor", + _ => "Method" + }; + /// Convert IL method name to C# name: .ctor/.cctor → simple class name. / IL メソッド名を C# 名に変換。 private static string ToCSharpMethodName(string ilMethodName, string typeName) { @@ -284,8 +298,11 @@ private static string BuildMethodMatchKey(MetadataReader reader, string typeName #pragma warning restore CA1031 } - /// Build human-readable details for a method: "ReturnType (Type paramName, Type paramName = defaultValue)". - private static string BuildMethodDetails(MetadataReader reader, MethodDefinition methodDef, SimpleSignatureTypeProvider typeProvider) + /// + /// Build separate return type and parameter strings for a method. + /// メソッドの戻り値型とパラメータ文字列を個別に構築します。 + /// + private static (string ReturnType, string Parameters) BuildMethodSignatureParts(MetadataReader reader, MethodDefinition methodDef, SimpleSignatureTypeProvider typeProvider) { try { @@ -328,12 +345,12 @@ private static string BuildMethodDetails(MetadataReader reader, MethodDefinition parts.Add(part); } - return $"{signature.ReturnType} ({string.Join(", ", parts)})"; + return (signature.ReturnType, $"({string.Join(", ", parts)})"); } #pragma warning disable CA1031 catch { - return ""; + return ("", ""); } #pragma warning restore CA1031 } diff --git a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs index bcd91782..0b2483b1 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs @@ -362,13 +362,13 @@ private void AppendMethodLevelChangesRow( if (summary.Entries.Count > 0) { contentBuilder.AppendLine(""); - contentBuilder.AppendLine(""); + contentBuilder.AppendLine(""); contentBuilder.AppendLine(""); foreach (var e in summary.Entries) { string accessTd = e.Access.Length > 0 ? $"{HtmlEncode(e.Access)}" : ""; string modifiersTd = e.Modifiers.Length > 0 ? $"{HtmlEncode(e.Modifiers)}" : ""; - contentBuilder.AppendLine($""); + contentBuilder.AppendLine($""); } contentBuilder.AppendLine("
AssemblyChangeClassAccessModifiersKindTypeNameReturnType (Type paramName)
AssemblyChangeClassKindAccessModifiersTypeNameReturnTypeParameters
{HtmlEncode(assemblyPath)}{HtmlEncode(e.Change)}{HtmlEncode(e.TypeName)}{accessTd}{modifiersTd}{HtmlEncode(e.MemberKind)}{HtmlEncode(e.MemberType)}{HtmlEncode(e.MemberName)}{HtmlEncode(e.Details)}
{HtmlEncode(assemblyPath)}{HtmlEncode(e.Change)}{HtmlEncode(e.TypeName)}{HtmlEncode(e.MemberKind)}{accessTd}{modifiersTd}{HtmlEncode(e.MemberType)}{HtmlEncode(e.MemberName)}{HtmlEncode(e.ReturnType)}{HtmlEncode(e.Parameters)}
"); } diff --git a/Services/ReportGenerateService.SectionWriters.cs b/Services/ReportGenerateService.SectionWriters.cs index d5795e85..6ad4e3fa 100644 --- a/Services/ReportGenerateService.SectionWriters.cs +++ b/Services/ReportGenerateService.SectionWriters.cs @@ -209,13 +209,13 @@ public void Write(StreamWriter writer, ReportWriteContext ctx) if (summary.Entries.Count > 0) { writer.WriteLine(); - writer.WriteLine("| Assembly | Change | Class | Access | Modifiers | Kind | Type | Name | ReturnType (Type paramName) |"); - writer.WriteLine("|----------|--------|-------|--------|-----------|------|------|------|-----------------------------|"); + writer.WriteLine("| Assembly | Change | Class | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters |"); + writer.WriteLine("|----------|--------|-------|------|--------|-----------|------|------|------------|------------|"); foreach (var e in summary.Entries) { string access = e.Access.Length > 0 ? $"`{EscapeMdTable(e.Access)}`" : ""; string modifiers = e.Modifiers.Length > 0 ? $"`{EscapeMdTable(e.Modifiers)}`" : ""; - writer.WriteLine($"| {EscapeMdTable(filePath)} | `{EscapeMdTable(e.Change)}` | {EscapeMdTable(e.TypeName)} | {access} | {modifiers} | `{EscapeMdTable(e.MemberKind)}` | {EscapeMdTable(e.MemberType)} | {EscapeMdTable(e.MemberName)} | {EscapeMdTable(e.Details)} |"); + writer.WriteLine($"| {EscapeMdTable(filePath)} | `{EscapeMdTable(e.Change)}` | {EscapeMdTable(e.TypeName)} | `{EscapeMdTable(e.MemberKind)}` | {access} | {modifiers} | {EscapeMdTable(e.MemberType)} | {EscapeMdTable(e.MemberName)} | {EscapeMdTable(e.ReturnType)} | {EscapeMdTable(e.Parameters)} |"); } } else diff --git a/doc/samples/diff_report.html b/doc/samples/diff_report.html index 8c4e2770..2dfbd5f0 100644 --- a/doc/samples/diff_report.html +++ b/doc/samples/diff_report.html @@ -437,7 +437,7 @@

[ * ] Modified Files (9)

-
+
#3 Show assembly semantic changes (5 changes)
@@ -478,7 +478,7 @@

[ * ] Modified Files (9)

-
+
#5 Show assembly semantic changes (17 changes)
diff --git a/doc/samples/diff_report.md b/doc/samples/diff_report.md index 92280b32..aa349d3c 100644 --- a/doc/samples/diff_report.md +++ b/doc/samples/diff_report.md @@ -56,36 +56,36 @@ ### src/App.dll -| Assembly | Change | Class | Access | Modifiers | Kind | Type | Name | ReturnType (Type paramName) | -|----------|--------|-------|--------|-----------|------|------|------|-----------------------------| -| src/App.dll | `Added` | MyApp.Controllers.ApiController | `public` | | `Method` | | HealthCheck | string () | -| src/App.dll | `Modified` | MyApp.Controllers.ApiController | `public` | `virtual` | `Method` | | GetUsers | System.Collections.Generic.IList\ (int page) | -| src/App.dll | `Modified` | MyApp.Services.DataService | `internal` | | `Method` | | RefreshCache | void () | -| src/App.dll | `Modified` | MyApp.Services.DataService | `private` | | `Method` | | ValidateConnection | bool (string connStr) | -| src/App.dll | `Added` | MyApp.Services.DataService | `public` | | `Property` | int | CacheTimeout | | +| Assembly | Change | Class | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | +|----------|--------|-------|------|--------|-----------|------|------|------------|------------| +| src/App.dll | `Added` | MyApp.Controllers.ApiController | `Method` | `public` | | | HealthCheck | string | () | +| src/App.dll | `Modified` | MyApp.Controllers.ApiController | `Method` | `public` | `virtual` | | GetUsers | System.Collections.Generic.IList\ | (int page) | +| src/App.dll | `Modified` | MyApp.Services.DataService | `Method` | `internal` | | | RefreshCache | void | () | +| src/App.dll | `Modified` | MyApp.Services.DataService | `Method` | `private` | | | ValidateConnection | bool | (string connStr) | +| src/App.dll | `Added` | MyApp.Services.DataService | `Property` | `public` | | int | CacheTimeout | | | - Member count: 28 (Old) vs 29 (New) ### src/Service.dll -| Assembly | Change | Class | Access | Modifiers | Kind | Type | Name | ReturnType (Type paramName) | -|----------|--------|-------|--------|-----------|------|------|------|-----------------------------| -| src/Service.dll | `Added` | MyApp.Services.NewValidator | | | `Type` | | | | -| src/Service.dll | `Added` | MyApp.Services.NewValidator | `public` | | `Method` | | NewValidator | void () | -| src/Service.dll | `Added` | MyApp.Services.NewValidator | `public` | | `Method` | | Validate | bool (string input) | -| src/Service.dll | `Added` | MyApp.Services.NewValidator | `private` | | `Method` | | ParseInput | string (string raw) | -| src/Service.dll | `Added` | MyApp.Services.OrderService | `public` | | `Method` | | ValidateWithNewValidator | bool (string data) | -| src/Service.dll | `Removed` | MyApp.Services.OrderService | `public` | `virtual` | `Method` | | LegacyValidate | bool (string data) | -| src/Service.dll | `Modified` | MyApp.Services.OrderService | `public` | | `Method` | | ProcessOrder | void (int orderId) | -| src/Service.dll | `Modified` | MyApp.Services.OrderService | `internal` | `static` | `Method` | | CalculateTotal | decimal (int qty, int price) | -| src/Service.dll | `Added` | MyApp.Services.OrderService | `private` | `readonly` | `Field` | MyApp.Models.UserRecord | _defaultUser | | -| src/Service.dll | `Added` | MyApp.Services.NewValidator | `private` | `readonly` | `Field` | string | _pattern | | -| src/Service.dll | `Added` | MyApp.Models.UserRecord | | | `Type` | | | | -| src/Service.dll | `Added` | MyApp.Models.UserRecord | `public` | | `Method` | | UserRecord | void (string Name, int Age) | -| src/Service.dll | `Added` | MyApp.Models.UserRecord | `public` | | `Property` | string | Name | | -| src/Service.dll | `Added` | MyApp.Models.UserRecord | `public` | | `Property` | int | Age | | -| src/Service.dll | `Added` | MyApp.Models.UserRecord | `public` | `override` | `Method` | | ToString | string () | -| src/Service.dll | `Added` | MyApp.Models.UserRecord | `public` | `virtual` | `Method` | | Equals | bool (object obj) | -| src/Service.dll | `Added` | MyApp.Models.UserRecord | `public` | `override` | `Method` | | GetHashCode | int () | +| Assembly | Change | Class | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | +|----------|--------|-------|------|--------|-----------|------|------|------------|------------| +| src/Service.dll | `Added` | MyApp.Services.NewValidator | `Type` | | | | | | | +| src/Service.dll | `Added` | MyApp.Services.NewValidator | `Constructor` | `public` | | | NewValidator | void | () | +| src/Service.dll | `Added` | MyApp.Services.NewValidator | `Method` | `public` | | | Validate | bool | (string input) | +| src/Service.dll | `Added` | MyApp.Services.NewValidator | `Method` | `private` | | | ParseInput | string | (string raw) | +| src/Service.dll | `Added` | MyApp.Services.OrderService | `Method` | `public` | | | ValidateWithNewValidator | bool | (string data) | +| src/Service.dll | `Removed` | MyApp.Services.OrderService | `Method` | `public` | `virtual` | | LegacyValidate | bool | (string data) | +| src/Service.dll | `Modified` | MyApp.Services.OrderService | `Method` | `public` | | | ProcessOrder | void | (int orderId) | +| src/Service.dll | `Modified` | MyApp.Services.OrderService | `Method` | `internal` | `static` | | CalculateTotal | decimal | (int qty, int price) | +| src/Service.dll | `Added` | MyApp.Services.OrderService | `Field` | `private` | `readonly` | MyApp.Models.UserRecord | _defaultUser | | | +| src/Service.dll | `Added` | MyApp.Services.NewValidator | `Field` | `private` | `readonly` | string | _pattern | | | +| src/Service.dll | `Added` | MyApp.Models.UserRecord | `Type` | | | | | | | +| src/Service.dll | `Added` | MyApp.Models.UserRecord | `Constructor` | `public` | | | UserRecord | void | (string Name, int Age) | +| src/Service.dll | `Added` | MyApp.Models.UserRecord | `Property` | `public` | | string | Name | | | +| src/Service.dll | `Added` | MyApp.Models.UserRecord | `Property` | `public` | | int | Age | | | +| src/Service.dll | `Added` | MyApp.Models.UserRecord | `Method` | `public` | `override` | | ToString | string | () | +| src/Service.dll | `Added` | MyApp.Models.UserRecord | `Method` | `public` | `virtual` | | Equals | bool | (object obj) | +| src/Service.dll | `Added` | MyApp.Models.UserRecord | `Method` | `public` | `override` | | GetHashCode | int | () | - Member count: 15 (Old) vs 26 (New) ### util/Legacy.dll From 994017e7c2b7131097bd10ba60f6bd613da4280e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 18:43:16 +0000 Subject: [PATCH 11/36] Refine Kind values, add type access modifiers, remove Assembly column MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace generic Kind=Type with specific Class/Record/Struct/Interface/Enum - Detect Record types via EqualityContract property heuristic - Add type access modifier detection using TypeAttributes.VisibilityMask - Remove redundant Assembly column from semantic changes table (10→9 cols) - Enrich samples: protected access, Removed types, overloads, move patterns, custom app-defined type properties - Update tests, README, CHANGELOG https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- CHANGELOG.md | 4 + .../Services/AssemblyMethodAnalyzerTests.cs | 2 +- .../Services/ReportGenerateServiceTests.cs | 14 +-- Models/MemberChangeEntry.cs | 4 +- README.md | 22 ++--- Services/AssemblyMethodAnalyzer.cs | 96 +++++++++++++++++-- .../HtmlReportGenerateService.Sections.cs | 4 +- .../ReportGenerateService.SectionWriters.cs | 6 +- doc/samples/diff_report.html | 8 +- doc/samples/diff_report.md | 68 +++++++------ 10 files changed, 164 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 841781a2..076375f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - Restructured the Assembly Semantic Changes table from 9 columns to 10 columns for clarity. Split the former `ReturnType (Type paramName)` column into separate `ReturnType` and `Parameters` columns. Moved `Kind` column before `Access` and `Modifiers` for better readability. Added `Constructor` and `StaticConstructor` as new Kind values (previously `.ctor`/`.cctor` were shown as `Method`). Constructors display the C# class name instead of `.ctor`. The `Type` column shows the declared type for Field/Property entries only. Empty Access/Modifiers cells no longer render as empty backticks. Changed `Method count` label to `Member count`. Renamed section from `Method-Level Changes` to `Assembly Semantic Changes`. Added record type and field variable samples to [`doc/samples/diff_report.md`](doc/samples/diff_report.md). Added bilingual **Assembly Semantic Changes** section to [README.md](README.md). +- Refined Kind values for type entries: replaced generic `Type` with specific `Class`, `Record`, `Struct`, `Interface`, and `Enum` kinds. Record types are detected heuristically by the presence of an `EqualityContract` property. Type entries now show access modifiers (`public`, `internal`, `protected`, etc.) instead of leaving the Access column empty. Removed the redundant `Assembly` column from the semantic changes table (10→9 columns) since the assembly name is already shown as the section header. Enriched sample reports with `protected` access modifier examples, `Removed` type entries, Added+Removed pairs (move/rename pattern), method overload examples, and custom application-defined type properties. + ### [1.4.1] - 2026-03-20 #### Added @@ -377,6 +379,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - Assembly Semantic Changes テーブルを 9 列から 10 列に再構成し明確化。旧 `ReturnType (Type paramName)` 列を `ReturnType` 列と `Parameters` 列に分離。`Kind` 列を `Access`・`Modifiers` の前に移動。Kind 値に `Constructor` と `StaticConstructor` を追加(従来 `.ctor`/`.cctor` は `Method` として表示)。コンストラクタは `.ctor` ではなく C# のクラス名で表示。`Type` 列は Field/Property の宣言型のみを表示。空の Access/Modifiers セルは空バッククォートではなく空欄に。`Method count` ラベルを `Member count` に変更。セクション名を `Method-Level Changes` から `Assembly Semantic Changes` に改名。[`doc/samples/diff_report.md`](doc/samples/diff_report.md) に record 型およびフィールド変数のサンプルを追加。[README.md](README.md) にバイリンガルの **アセンブリ セマンティック変更** セクションを追加。 +- Kind 値を詳細化: 汎用の `Type` を `Class`、`Record`、`Struct`、`Interface`、`Enum` に置き換え。Record 型は `EqualityContract` プロパティの有無で推定。型エントリの Access 列に空欄ではなくアクセス修飾子(`public`、`internal`、`protected` 等)を表示。セクションヘッダに既にアセンブリ名が表示されるため冗長な `Assembly` 列を削除(10→9 列)。サンプルレポートに `protected` アクセス修飾子、`Removed` 型エントリ、Added+Removed ペア(移動/リネームパターン)、メソッドオーバーロード、アプリケーション独自型のプロパティを追加。 + ### [1.4.1] - 2026-03-20 #### 追加 diff --git a/FolderDiffIL4DotNet.Tests/Services/AssemblyMethodAnalyzerTests.cs b/FolderDiffIL4DotNet.Tests/Services/AssemblyMethodAnalyzerTests.cs index 782d5267..1e383953 100644 --- a/FolderDiffIL4DotNet.Tests/Services/AssemblyMethodAnalyzerTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/AssemblyMethodAnalyzerTests.cs @@ -72,7 +72,7 @@ public void Analyze_DifferentAssemblies_EntriesHaveStructuredData() Assert.False(string.IsNullOrEmpty(firstEntry.TypeName)); Assert.False(string.IsNullOrEmpty(firstEntry.MemberKind)); Assert.Contains(firstEntry.Change, new[] { "Added", "Removed", "Modified" }); - Assert.Contains(firstEntry.MemberKind, new[] { "Type", "Constructor", "StaticConstructor", "Method", "Property", "Field" }); + Assert.Contains(firstEntry.MemberKind, new[] { "Class", "Record", "Struct", "Interface", "Enum", "Constructor", "StaticConstructor", "Method", "Property", "Field" }); } } } diff --git a/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs b/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs index 90b92be8..9e3a62be 100644 --- a/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs @@ -775,7 +775,7 @@ public void GenerateDiffReport_AssemblySemanticChanges_IncludedBetweenSummaryAnd NewMethodCount = 44, Entries = new List { - new("Added", "MyApp.NewService", "", "", "Type", "", "", "", ""), + new("Added", "MyApp.NewService", "public", "", "Class", "", "", "", ""), new("Added", "MyApp.UserService", "public", "static", "Method", "ValidateToken", "", "bool", "(string token)"), new("Added", "MyApp.UserService", "internal", "", "Method", "RefreshSession", "", "void", "(int userId)"), new("Removed", "MyApp.UserService", "public", "virtual", "Method", "LegacyAuth", "", "void", "(string key)"), @@ -801,12 +801,12 @@ public void GenerateDiffReport_AssemblySemanticChanges_IncludedBetweenSummaryAnd // Content checks — table format Assert.Contains("## Assembly Semantic Changes", reportText); Assert.Contains("### src/App.dll", reportText); - Assert.Contains("| Assembly | Change | Class | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters |", reportText); - Assert.Contains("| src/App.dll | `Added` | MyApp.NewService | `Type` | | | | | | |", reportText); - Assert.Contains("| src/App.dll | `Added` | MyApp.UserService | `Method` | `public` | `static` | | ValidateToken | bool | (string token) |", reportText); - Assert.Contains("| src/App.dll | `Modified` | MyApp.UserService | `Method` | `public` | | | Login | bool | (string user, string pass) |", reportText); - Assert.Contains("| src/App.dll | `Added` | MyApp.UserService | `Property` | `public` | | bool | IsActive | | |", reportText); - Assert.Contains("| src/App.dll | `Added` | MyApp.UserService | `Field` | `private` | `readonly` | object | _cache | | |", reportText); + Assert.Contains("| Change | Class | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters |", reportText); + Assert.Contains("| `Added` | MyApp.NewService | `Class` | `public` | | | | | |", reportText); + Assert.Contains("| `Added` | MyApp.UserService | `Method` | `public` | `static` | | ValidateToken | bool | (string token) |", reportText); + Assert.Contains("| `Modified` | MyApp.UserService | `Method` | `public` | | | Login | bool | (string user, string pass) |", reportText); + Assert.Contains("| `Added` | MyApp.UserService | `Property` | `public` | | bool | IsActive | | |", reportText); + Assert.Contains("| `Added` | MyApp.UserService | `Field` | `private` | `readonly` | object | _cache | | |", reportText); Assert.Contains("- Member count: 42 (Old) vs 44 (New)", reportText); // Ordering: Summary < Assembly Semantic Changes < IL Cache Stats diff --git a/Models/MemberChangeEntry.cs b/Models/MemberChangeEntry.cs index 0b0def6c..58806c09 100644 --- a/Models/MemberChangeEntry.cs +++ b/Models/MemberChangeEntry.cs @@ -6,9 +6,9 @@ namespace FolderDiffIL4DotNet.Models ///

/// Change kind: "Added", "Removed", "Modified". / 変更種別。 /// Owning type name (or the type itself for Type entries). / 所属型名(Type エントリの場合は型名そのもの)。 - /// Access modifier. Empty for Type entries. / アクセス修飾子。Type の場合は空。 + /// Access modifier: public, internal, protected, private, etc. / アクセス修飾子。 /// Other modifiers (static, abstract, virtual, sealed, override, etc.). / その他の修飾子。 - /// Member kind: "Type", "Constructor", "StaticConstructor", "Method", "Property", "Field". / メンバー種別。 + /// Member kind: "Class", "Record", "Struct", "Interface", "Enum", "Constructor", "StaticConstructor", "Method", "Property", "Field". / メンバー種別。 /// Member name (C# name; constructors use the class name, not .ctor). Empty for Type entries. / メンバー名(C# 名、コンストラクタは .ctor ではなくクラス名)。Type の場合は空。 /// For Field/Property: the declared type (e.g. "string", "int"). Empty for Method/Constructor/Type entries. / フィールド・プロパティの宣言型。メソッド・コンストラクタ・Type の場合は空。 /// For Method: the return type (e.g. "void", "string"). For Constructor: "void". Empty for Type/Field/Property entries. / メソッドの戻り値型。コンストラクタは "void"。Type/Field/Property の場合は空。 diff --git a/README.md b/README.md index 36107042..e61d4f27 100644 --- a/README.md +++ b/README.md @@ -205,16 +205,15 @@ When an assembly is classified as `ILMismatch`, the tool performs an additional | Column | Description | Example | |--------|-------------|---------| -| Assembly | Relative path of the assembly | `bin/MyLib.dll` | | Change | `Added`, `Removed`, or `Modified` | `Added` | | Class | Fully qualified type name | `MyNamespace.MyClass` | -| Kind | Member kind: `Type`, `Constructor`, `StaticConstructor`, `Method`, `Property`, `Field` | `Method` | +| Kind | Member kind: `Class`, `Record`, `Struct`, `Interface`, `Enum`, `Constructor`, `StaticConstructor`, `Method`, `Property`, `Field` | `Method` | | Access | Access modifier | `public` | | Modifiers | Other modifiers | `static` | -| Type | Declared type for Field/Property (empty for Method/Constructor/Type) | `int` | -| Name | Member name (constructors use the class name; empty for Type entries) | `DoWork` | -| ReturnType | Return type for Method/Constructor (empty for Field/Property/Type) | `void` | -| Parameters | Parameter list for Method/Constructor (empty for Field/Property/Type) | `(string name, int count = 0)` | +| Type | Declared type for Field/Property (empty for Method/Constructor/Class/Record) | `int` | +| Name | Member name (constructors use the class name; empty for Class/Record/Struct/Interface/Enum entries) | `DoWork` | +| ReturnType | Return type for Method/Constructor (empty for Field/Property/Class/Record) | `void` | +| Parameters | Parameter list for Method/Constructor (empty for Field/Property/Class/Record) | `(string name, int count = 0)` | Controlled by [`ShouldIncludeMethodLevelChangesInReport`](#config-en-shouldincludemethodlevelchangesinreport) (default: `true`). @@ -685,16 +684,15 @@ flowchart TD | 列 | 説明 | 例 | |----|------|-----| -| Assembly | アセンブリの相対パス | `bin/MyLib.dll` | | Change | `Added`、`Removed`、`Modified` | `Added` | | Class | 完全修飾型名 | `MyNamespace.MyClass` | -| Kind | メンバー種別: `Type`, `Constructor`, `StaticConstructor`, `Method`, `Property`, `Field` | `Method` | +| Kind | メンバー種別: `Class`, `Record`, `Struct`, `Interface`, `Enum`, `Constructor`, `StaticConstructor`, `Method`, `Property`, `Field` | `Method` | | Access | アクセス修飾子 | `public` | | Modifiers | その他の修飾子 | `static` | -| Type | Field/Property の宣言型(Method/Constructor/Type の場合は空) | `int` | -| Name | メンバー名(コンストラクタはクラス名、Type エントリの場合は空) | `DoWork` | -| ReturnType | Method/Constructor の戻り値型(Field/Property/Type の場合は空) | `void` | -| Parameters | Method/Constructor のパラメータ一覧(Field/Property/Type の場合は空) | `(string name, int count = 0)` | +| Type | Field/Property の宣言型(Method/Constructor/Class/Record の場合は空) | `int` | +| Name | メンバー名(コンストラクタはクラス名、Class/Record/Struct/Interface/Enum エントリの場合は空) | `DoWork` | +| ReturnType | Method/Constructor の戻り値型(Field/Property/Class/Record の場合は空) | `void` | +| Parameters | Method/Constructor のパラメータ一覧(Field/Property/Class/Record の場合は空) | `(string name, int count = 0)` | [`ShouldIncludeMethodLevelChangesInReport`](#config-ja-shouldincludemethodlevelchangesinreport)(既定値: `true`)で制御します。 diff --git a/Services/AssemblyMethodAnalyzer.cs b/Services/AssemblyMethodAnalyzer.cs index 94e59bca..fc57eeff 100644 --- a/Services/AssemblyMethodAnalyzer.cs +++ b/Services/AssemblyMethodAnalyzer.cs @@ -36,10 +36,16 @@ internal static class AssemblyMethodAnalyzer var entries = new List(); // Types - foreach (var t in newSnapshot.TypeNames.Except(oldSnapshot.TypeNames, StringComparer.Ordinal).OrderBy(t => t, StringComparer.Ordinal)) - entries.Add(new MemberChangeEntry("Added", t, "", "", "Type", "", "", "", "")); - foreach (var t in oldSnapshot.TypeNames.Except(newSnapshot.TypeNames, StringComparer.Ordinal).OrderBy(t => t, StringComparer.Ordinal)) - entries.Add(new MemberChangeEntry("Removed", t, "", "", "Type", "", "", "", "")); + foreach (var t in newSnapshot.TypeNames.Keys.Except(oldSnapshot.TypeNames.Keys, StringComparer.Ordinal).OrderBy(t => t, StringComparer.Ordinal)) + { + var info = newSnapshot.TypeNames[t]; + entries.Add(new MemberChangeEntry("Added", t, info.Access, "", info.Kind, "", "", "", "")); + } + foreach (var t in oldSnapshot.TypeNames.Keys.Except(newSnapshot.TypeNames.Keys, StringComparer.Ordinal).OrderBy(t => t, StringComparer.Ordinal)) + { + var info = oldSnapshot.TypeNames[t]; + entries.Add(new MemberChangeEntry("Removed", t, info.Access, "", info.Kind, "", "", "", "")); + } // Methods (including constructors) foreach (var key in newSnapshot.Methods.Keys.Except(oldSnapshot.Methods.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) @@ -135,9 +141,15 @@ private sealed class FieldDetail public required string Details { get; init; } } + private sealed class TypeInfo + { + public required string Access { get; init; } + public required string Kind { get; init; } + } + private sealed class AssemblySnapshot { - public HashSet TypeNames { get; } = new(StringComparer.Ordinal); + public Dictionary TypeNames { get; } = new(StringComparer.Ordinal); public Dictionary Methods { get; } = new(StringComparer.Ordinal); public Dictionary Properties { get; } = new(StringComparer.Ordinal); public Dictionary Fields { get; } = new(StringComparer.Ordinal); @@ -161,7 +173,9 @@ private static AssemblySnapshot ReadAssemblySnapshot(string assemblyPath) // Skip the special type if (typeName == "") continue; - snapshot.TypeNames.Add(typeName); + string typeAccess = GetTypeAccessModifier(typeDef.Attributes); + string typeKind = GetTypeKind(reader, typeDef); + snapshot.TypeNames[typeName] = new TypeInfo { Access = typeAccess, Kind = typeKind }; // Methods foreach (var methodHandle in typeDef.GetMethods()) @@ -468,6 +482,76 @@ private static string GetFieldAccessModifier(FieldAttributes attributes) }; } + /// Extract access modifier from type attributes. / TypeAttributes からアクセス修飾子を取得。 + private static string GetTypeAccessModifier(System.Reflection.TypeAttributes attributes) + { + var visibility = attributes & System.Reflection.TypeAttributes.VisibilityMask; + return visibility switch + { + System.Reflection.TypeAttributes.Public => "public", + System.Reflection.TypeAttributes.NotPublic => "internal", + System.Reflection.TypeAttributes.NestedPublic => "public", + System.Reflection.TypeAttributes.NestedFamily => "protected", + System.Reflection.TypeAttributes.NestedFamORAssem => "protected internal", + System.Reflection.TypeAttributes.NestedAssembly => "internal", + System.Reflection.TypeAttributes.NestedFamANDAssem => "private protected", + System.Reflection.TypeAttributes.NestedPrivate => "private", + _ => "internal" + }; + } + + /// + /// Determine the type kind: Class, Record, Struct, Interface, or Enum. + /// Record is detected heuristically by the presence of an EqualityContract property. + /// 型の種別を判定: Class, Record, Struct, Interface, Enum。 + /// Record は EqualityContract プロパティの有無で推定。 + /// + private static string GetTypeKind(MetadataReader reader, TypeDefinition typeDef) + { + var attributes = typeDef.Attributes; + + if ((attributes & System.Reflection.TypeAttributes.Interface) != 0) + return "Interface"; + + // Check base type for enum / struct (value type) + if (!typeDef.BaseType.IsNil) + { + string baseTypeName = GetBaseTypeName(reader, typeDef.BaseType); + if (baseTypeName is "System.Enum") + return "Enum"; + if (baseTypeName is "System.ValueType") + return "Struct"; + } + + // Heuristic: C# records have a compiler-generated EqualityContract property + foreach (var propHandle in typeDef.GetProperties()) + { + var propDef = reader.GetPropertyDefinition(propHandle); + if (reader.GetString(propDef.Name) == "EqualityContract") + return "Record"; + } + + return "Class"; + } + + /// Get the full name of a base type from its EntityHandle. + private static string GetBaseTypeName(MetadataReader reader, EntityHandle baseTypeHandle) + { + if (baseTypeHandle.Kind == HandleKind.TypeReference) + { + var typeRef = reader.GetTypeReference((TypeReferenceHandle)baseTypeHandle); + string ns = reader.GetString(typeRef.Namespace); + string name = reader.GetString(typeRef.Name); + return string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}"; + } + if (baseTypeHandle.Kind == HandleKind.TypeDefinition) + { + var typeDef = reader.GetTypeDefinition((TypeDefinitionHandle)baseTypeHandle); + return GetFullTypeName(reader, typeDef); + } + return ""; + } + /// Extract non-access modifiers from method attributes (static, abstract, virtual, sealed, override, etc.). private static string GetMethodModifiers(MethodAttributes attributes) { diff --git a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs index 0b2483b1..47c555af 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs @@ -362,13 +362,13 @@ private void AppendMethodLevelChangesRow( if (summary.Entries.Count > 0) { contentBuilder.AppendLine(""); - contentBuilder.AppendLine(""); + contentBuilder.AppendLine(""); contentBuilder.AppendLine(""); foreach (var e in summary.Entries) { string accessTd = e.Access.Length > 0 ? $"{HtmlEncode(e.Access)}" : ""; string modifiersTd = e.Modifiers.Length > 0 ? $"{HtmlEncode(e.Modifiers)}" : ""; - contentBuilder.AppendLine($""); + contentBuilder.AppendLine($""); } contentBuilder.AppendLine("
AssemblyChangeClassKindAccessModifiersTypeNameReturnTypeParameters
ChangeClassKindAccessModifiersTypeNameReturnTypeParameters
{HtmlEncode(assemblyPath)}{HtmlEncode(e.Change)}{HtmlEncode(e.TypeName)}{HtmlEncode(e.MemberKind)}{accessTd}{modifiersTd}{HtmlEncode(e.MemberType)}{HtmlEncode(e.MemberName)}{HtmlEncode(e.ReturnType)}{HtmlEncode(e.Parameters)}
{HtmlEncode(e.Change)}{HtmlEncode(e.TypeName)}{HtmlEncode(e.MemberKind)}{accessTd}{modifiersTd}{HtmlEncode(e.MemberType)}{HtmlEncode(e.MemberName)}{HtmlEncode(e.ReturnType)}{HtmlEncode(e.Parameters)}
"); } diff --git a/Services/ReportGenerateService.SectionWriters.cs b/Services/ReportGenerateService.SectionWriters.cs index 6ad4e3fa..c720a4aa 100644 --- a/Services/ReportGenerateService.SectionWriters.cs +++ b/Services/ReportGenerateService.SectionWriters.cs @@ -209,13 +209,13 @@ public void Write(StreamWriter writer, ReportWriteContext ctx) if (summary.Entries.Count > 0) { writer.WriteLine(); - writer.WriteLine("| Assembly | Change | Class | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters |"); - writer.WriteLine("|----------|--------|-------|------|--------|-----------|------|------|------------|------------|"); + writer.WriteLine("| Change | Class | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters |"); + writer.WriteLine("|--------|-------|------|--------|-----------|------|------|------------|------------|"); foreach (var e in summary.Entries) { string access = e.Access.Length > 0 ? $"`{EscapeMdTable(e.Access)}`" : ""; string modifiers = e.Modifiers.Length > 0 ? $"`{EscapeMdTable(e.Modifiers)}`" : ""; - writer.WriteLine($"| {EscapeMdTable(filePath)} | `{EscapeMdTable(e.Change)}` | {EscapeMdTable(e.TypeName)} | `{EscapeMdTable(e.MemberKind)}` | {access} | {modifiers} | {EscapeMdTable(e.MemberType)} | {EscapeMdTable(e.MemberName)} | {EscapeMdTable(e.ReturnType)} | {EscapeMdTable(e.Parameters)} |"); + writer.WriteLine($"| `{EscapeMdTable(e.Change)}` | {EscapeMdTable(e.TypeName)} | `{EscapeMdTable(e.MemberKind)}` | {access} | {modifiers} | {EscapeMdTable(e.MemberType)} | {EscapeMdTable(e.MemberName)} | {EscapeMdTable(e.ReturnType)} | {EscapeMdTable(e.Parameters)} |"); } } else diff --git a/doc/samples/diff_report.html b/doc/samples/diff_report.html index 2dfbd5f0..e59afc80 100644 --- a/doc/samples/diff_report.html +++ b/doc/samples/diff_report.html @@ -437,8 +437,8 @@

[ * ] Modified Files (9)

-
- #3 Show assembly semantic changes (5 changes) +
+ #3 Show assembly semantic changes (10 changes)
@@ -478,8 +478,8 @@

[ * ] Modified Files (9)

-
- #5 Show assembly semantic changes (17 changes) +
+ #5 Show assembly semantic changes (26 changes)
diff --git a/doc/samples/diff_report.md b/doc/samples/diff_report.md index aa349d3c..909431ae 100644 --- a/doc/samples/diff_report.md +++ b/doc/samples/diff_report.md @@ -56,36 +56,50 @@ ### src/App.dll -| Assembly | Change | Class | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | -|----------|--------|-------|------|--------|-----------|------|------|------------|------------| -| src/App.dll | `Added` | MyApp.Controllers.ApiController | `Method` | `public` | | | HealthCheck | string | () | -| src/App.dll | `Modified` | MyApp.Controllers.ApiController | `Method` | `public` | `virtual` | | GetUsers | System.Collections.Generic.IList\ | (int page) | -| src/App.dll | `Modified` | MyApp.Services.DataService | `Method` | `internal` | | | RefreshCache | void | () | -| src/App.dll | `Modified` | MyApp.Services.DataService | `Method` | `private` | | | ValidateConnection | bool | (string connStr) | -| src/App.dll | `Added` | MyApp.Services.DataService | `Property` | `public` | | int | CacheTimeout | | | -- Member count: 28 (Old) vs 29 (New) +| Change | Class | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | +|--------|-------|------|--------|-----------|------|------|------------|------------| +| `Added` | MyApp.Controllers.ApiController | `Method` | `public` | | | HealthCheck | string | () | +| `Added` | MyApp.Controllers.ApiController | `Method` | `public` | | | GetUsers | System.Collections.Generic.IList\ | (int page, int pageSize = 20) | +| `Removed` | MyApp.Controllers.ApiController | `Method` | `public` | | | GetUsers | System.Collections.Generic.IList\ | (int page) | +| `Modified` | MyApp.Controllers.ApiController | `Method` | `public` | `virtual` | | Search | System.Collections.Generic.IList\ | (string query) | +| `Modified` | MyApp.Controllers.ApiController | `Method` | `protected` | | | OnAuthorize | bool | (MyApp.Models.UserContext ctx) | +| `Modified` | MyApp.Services.DataService | `Method` | `internal` | | | RefreshCache | void | () | +| `Modified` | MyApp.Services.DataService | `Method` | `private` | | | ValidateConnection | bool | (string connStr) | +| `Added` | MyApp.Services.DataService | `Property` | `public` | | int | CacheTimeout | | | +| `Added` | MyApp.Services.DataService | `Property` | `public` | | MyApp.Models.CachePolicy | Policy | | | +| `Added` | MyApp.Services.DataService | `Property` | `internal` | | MyApp.Services.IConnectionPool | ConnectionPool | | | +- Member count: 28 (Old) vs 30 (New) ### src/Service.dll -| Assembly | Change | Class | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | -|----------|--------|-------|------|--------|-----------|------|------|------------|------------| -| src/Service.dll | `Added` | MyApp.Services.NewValidator | `Type` | | | | | | | -| src/Service.dll | `Added` | MyApp.Services.NewValidator | `Constructor` | `public` | | | NewValidator | void | () | -| src/Service.dll | `Added` | MyApp.Services.NewValidator | `Method` | `public` | | | Validate | bool | (string input) | -| src/Service.dll | `Added` | MyApp.Services.NewValidator | `Method` | `private` | | | ParseInput | string | (string raw) | -| src/Service.dll | `Added` | MyApp.Services.OrderService | `Method` | `public` | | | ValidateWithNewValidator | bool | (string data) | -| src/Service.dll | `Removed` | MyApp.Services.OrderService | `Method` | `public` | `virtual` | | LegacyValidate | bool | (string data) | -| src/Service.dll | `Modified` | MyApp.Services.OrderService | `Method` | `public` | | | ProcessOrder | void | (int orderId) | -| src/Service.dll | `Modified` | MyApp.Services.OrderService | `Method` | `internal` | `static` | | CalculateTotal | decimal | (int qty, int price) | -| src/Service.dll | `Added` | MyApp.Services.OrderService | `Field` | `private` | `readonly` | MyApp.Models.UserRecord | _defaultUser | | | -| src/Service.dll | `Added` | MyApp.Services.NewValidator | `Field` | `private` | `readonly` | string | _pattern | | | -| src/Service.dll | `Added` | MyApp.Models.UserRecord | `Type` | | | | | | | -| src/Service.dll | `Added` | MyApp.Models.UserRecord | `Constructor` | `public` | | | UserRecord | void | (string Name, int Age) | -| src/Service.dll | `Added` | MyApp.Models.UserRecord | `Property` | `public` | | string | Name | | | -| src/Service.dll | `Added` | MyApp.Models.UserRecord | `Property` | `public` | | int | Age | | | -| src/Service.dll | `Added` | MyApp.Models.UserRecord | `Method` | `public` | `override` | | ToString | string | () | -| src/Service.dll | `Added` | MyApp.Models.UserRecord | `Method` | `public` | `virtual` | | Equals | bool | (object obj) | -| src/Service.dll | `Added` | MyApp.Models.UserRecord | `Method` | `public` | `override` | | GetHashCode | int | () | +| Change | Class | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | +|--------|-------|------|--------|-----------|------|------|------------|------------| +| `Added` | MyApp.Services.NewValidator | `Class` | `public` | | | | | | +| `Added` | MyApp.Services.NewValidator | `Constructor` | `public` | | | NewValidator | void | () | +| `Added` | MyApp.Services.NewValidator | `Method` | `public` | | | Validate | bool | (string input) | +| `Added` | MyApp.Services.NewValidator | `Method` | `public` | | | Validate | bool | (string input, MyApp.Models.ValidationOptions options) | +| `Added` | MyApp.Services.NewValidator | `Method` | `private` | | | ParseInput | string | (string raw) | +| `Added` | MyApp.Services.NewValidator | `Property` | `public` | | MyApp.Models.ValidationResult | LastResult | | | +| `Added` | MyApp.Services.NewValidator | `Field` | `private` | `readonly` | string | _pattern | | | +| `Added` | MyApp.Services.OrderService | `Method` | `public` | | | ValidateWithNewValidator | bool | (string data) | +| `Removed` | MyApp.Services.OrderService | `Method` | `public` | `virtual` | | LegacyValidate | bool | (string data) | +| `Modified` | MyApp.Services.OrderService | `Method` | `public` | | | ProcessOrder | void | (int orderId) | +| `Modified` | MyApp.Services.OrderService | `Method` | `internal` | `static` | | CalculateTotal | decimal | (int qty, int price) | +| `Added` | MyApp.Services.OrderService | `Property` | `protected` | | MyApp.Models.OrderContext | CurrentContext | | | +| `Added` | MyApp.Services.OrderService | `Field` | `private` | `readonly` | MyApp.Models.UserRecord | _defaultUser | | | +| `Removed` | MyApp.Services.LegacyHelper | `Class` | `internal` | | | | | | +| `Removed` | MyApp.Services.LegacyHelper | `Method` | `public` | | | Convert | string | (object value) | +| `Removed` | MyApp.Services.LegacyHelper | `Method` | `public` | `static` | | Format | string | (string template, object[] args) | +| `Added` | MyApp.Models.UserRecord | `Record` | `public` | | | | | | +| `Added` | MyApp.Models.UserRecord | `Constructor` | `public` | | | UserRecord | void | (string Name, int Age) | +| `Added` | MyApp.Models.UserRecord | `Property` | `public` | | string | Name | | | +| `Added` | MyApp.Models.UserRecord | `Property` | `public` | | int | Age | | | +| `Added` | MyApp.Models.UserRecord | `Method` | `public` | `override` | | ToString | string | () | +| `Added` | MyApp.Models.UserRecord | `Method` | `public` | `virtual` | | Equals | bool | (object obj) | +| `Added` | MyApp.Models.UserRecord | `Method` | `public` | `override` | | GetHashCode | int | () | +| `Removed` | MyApp.Models.UserDto | `Class` | `public` | | | | | | +| `Removed` | MyApp.Models.UserDto | `Property` | `public` | | string | Name | | | +| `Removed` | MyApp.Models.UserDto | `Property` | `public` | | int | Age | | | - Member count: 15 (Old) vs 26 (New) ### util/Legacy.dll From 6d1df53908a7d5a86039b7c76c3599ff792fb856 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 18:49:34 +0000 Subject: [PATCH 12/36] Remove parentheses from Parameters column values Since Parameters is now an independent column, wrapping values in parentheses is redundant. Values display as "string name, int count = 0" instead of "(string name, int count = 0)". Empty parameter lists show as blank instead of "()". https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- CHANGELOG.md | 4 ++ .../Models/MethodLevelChangesSummaryTests.cs | 6 +-- .../HtmlReportGenerateServiceTests.cs | 8 ++-- .../Services/ReportGenerateServiceTests.cs | 12 +++--- Models/MemberChangeEntry.cs | 2 +- README.md | 4 +- Services/AssemblyMethodAnalyzer.cs | 2 +- doc/samples/diff_report.html | 4 +- doc/samples/diff_report.md | 42 +++++++++---------- 9 files changed, 44 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 076375f4..60fb1c35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - Refined Kind values for type entries: replaced generic `Type` with specific `Class`, `Record`, `Struct`, `Interface`, and `Enum` kinds. Record types are detected heuristically by the presence of an `EqualityContract` property. Type entries now show access modifiers (`public`, `internal`, `protected`, etc.) instead of leaving the Access column empty. Removed the redundant `Assembly` column from the semantic changes table (10→9 columns) since the assembly name is already shown as the section header. Enriched sample reports with `protected` access modifier examples, `Removed` type entries, Added+Removed pairs (move/rename pattern), method overload examples, and custom application-defined type properties. +- Removed parentheses from the `Parameters` column values. Since Parameters is now an independent column, wrapping values in `(…)` is redundant. Values now display as `string name, int count = 0` instead of `(string name, int count = 0)`. Empty parameter lists display as blank instead of `()`. + ### [1.4.1] - 2026-03-20 #### Added @@ -381,6 +383,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - Kind 値を詳細化: 汎用の `Type` を `Class`、`Record`、`Struct`、`Interface`、`Enum` に置き換え。Record 型は `EqualityContract` プロパティの有無で推定。型エントリの Access 列に空欄ではなくアクセス修飾子(`public`、`internal`、`protected` 等)を表示。セクションヘッダに既にアセンブリ名が表示されるため冗長な `Assembly` 列を削除(10→9 列)。サンプルレポートに `protected` アクセス修飾子、`Removed` 型エントリ、Added+Removed ペア(移動/リネームパターン)、メソッドオーバーロード、アプリケーション独自型のプロパティを追加。 +- `Parameters` 列の値から括弧を削除。Parameters が独立列となったため `(…)` は冗長。値は `(string name, int count = 0)` ではなく `string name, int count = 0` で表示。引数なしは `()` ではなく空欄。 + ### [1.4.1] - 2026-03-20 #### 追加 diff --git a/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs b/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs index ce01e36a..48adb58d 100644 --- a/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs +++ b/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs @@ -20,7 +20,7 @@ public void HasChanges_WithEntries_ReturnsTrue() { Entries = new List { - new("Added", "MyApp.Service", "public", "", "Method", "DoWork", "", "void", "(int count)"), + new("Added", "MyApp.Service", "public", "", "Method", "DoWork", "", "void", "int count"), }, }; Assert.True(summary.HasChanges); @@ -41,7 +41,7 @@ public void HasChanges_EmptyEntries_ReturnsFalse() [Fact] public void Entries_ContainStructuredData() { - var entry = new MemberChangeEntry("Added", "MyApp.Service", "public", "static", "Method", "GetName", "", "string", "(string id)"); + var entry = new MemberChangeEntry("Added", "MyApp.Service", "public", "static", "Method", "GetName", "", "string", "string id"); Assert.Equal("Added", entry.Change); Assert.Equal("MyApp.Service", entry.TypeName); Assert.Equal("public", entry.Access); @@ -50,7 +50,7 @@ public void Entries_ContainStructuredData() Assert.Equal("GetName", entry.MemberName); Assert.Equal("", entry.MemberType); Assert.Equal("string", entry.ReturnType); - Assert.Equal("(string id)", entry.Parameters); + Assert.Equal("string id", entry.Parameters); } } } diff --git a/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs b/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs index 97018b67..e1a5ab86 100644 --- a/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs @@ -732,8 +732,8 @@ public void GenerateDiffReportHtml_MethodLevelChanges_ShowsInlineAboveILDiff() NewMethodCount = 12, Entries = new List { - new("Added", "MyApp.Service", "public", "", "Method", "NewMethod", "", "void", "(string name)"), - new("Modified", "MyApp.Service", "public", "virtual", "Method", "ExistingMethod", "", "bool", "(int id)"), + new("Added", "MyApp.Service", "public", "", "Method", "NewMethod", "", "void", "string name"), + new("Modified", "MyApp.Service", "public", "virtual", "Method", "ExistingMethod", "", "bool", "int id"), new("Added", "MyApp.Service", "public", "", "Property", "NewProp", "string", "", ""), new("Removed", "MyApp.Service", "private", "readonly", "Field", "_oldField", "int", "", ""), }, @@ -765,7 +765,7 @@ public void GenerateDiffReportHtml_MethodLevelChanges_NotShownWhenDisabled() NewMethodCount = 12, Entries = new List { - new("Added", "MyApp.Service", "public", "", "Method", "NewMethod", "", "void", "(string name)"), + new("Added", "MyApp.Service", "public", "", "Method", "NewMethod", "", "void", "string name"), }, }; @@ -793,7 +793,7 @@ public void GenerateDiffReportHtml_MethodLevelChanges_LazyRender_EncodesAsBase64 NewMethodCount = 6, Entries = new List { - new("Added", "Foo", "public", "", "Method", "Bar", "", "void", "()"), + new("Added", "Foo", "public", "", "Method", "Bar", "", "void", ""), }, }; diff --git a/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs b/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs index 9e3a62be..8258f995 100644 --- a/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs @@ -776,10 +776,10 @@ public void GenerateDiffReport_AssemblySemanticChanges_IncludedBetweenSummaryAnd Entries = new List { new("Added", "MyApp.NewService", "public", "", "Class", "", "", "", ""), - new("Added", "MyApp.UserService", "public", "static", "Method", "ValidateToken", "", "bool", "(string token)"), - new("Added", "MyApp.UserService", "internal", "", "Method", "RefreshSession", "", "void", "(int userId)"), - new("Removed", "MyApp.UserService", "public", "virtual", "Method", "LegacyAuth", "", "void", "(string key)"), - new("Modified", "MyApp.UserService", "public", "", "Method", "Login", "", "bool", "(string user, string pass)"), + new("Added", "MyApp.UserService", "public", "static", "Method", "ValidateToken", "", "bool", "string token"), + new("Added", "MyApp.UserService", "internal", "", "Method", "RefreshSession", "", "void", "int userId"), + new("Removed", "MyApp.UserService", "public", "virtual", "Method", "LegacyAuth", "", "void", "string key"), + new("Modified", "MyApp.UserService", "public", "", "Method", "Login", "", "bool", "string user, string pass"), new("Added", "MyApp.UserService", "public", "", "Property", "IsActive", "bool", "", ""), new("Added", "MyApp.UserService", "private", "readonly", "Field", "_cache", "object", "", ""), }, @@ -803,7 +803,7 @@ public void GenerateDiffReport_AssemblySemanticChanges_IncludedBetweenSummaryAnd Assert.Contains("### src/App.dll", reportText); Assert.Contains("| Change | Class | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters |", reportText); Assert.Contains("| `Added` | MyApp.NewService | `Class` | `public` | | | | | |", reportText); - Assert.Contains("| `Added` | MyApp.UserService | `Method` | `public` | `static` | | ValidateToken | bool | (string token) |", reportText); + Assert.Contains("| `Added` | MyApp.UserService | `Method` | `public` | `static` | | ValidateToken | bool | string token |", reportText); Assert.Contains("| `Modified` | MyApp.UserService | `Method` | `public` | | | Login | bool | (string user, string pass) |", reportText); Assert.Contains("| `Added` | MyApp.UserService | `Property` | `public` | | bool | IsActive | | |", reportText); Assert.Contains("| `Added` | MyApp.UserService | `Field` | `private` | `readonly` | object | _cache | | |", reportText); @@ -833,7 +833,7 @@ public void GenerateDiffReport_MethodLevelChanges_NotIncludedWhenDisabled() NewMethodCount = 12, Entries = new List { - new("Added", "Foo", "public", "", "Method", "Bar", "", "void", "()"), + new("Added", "Foo", "public", "", "Method", "Bar", "", "void", ""), }, }; diff --git a/Models/MemberChangeEntry.cs b/Models/MemberChangeEntry.cs index 58806c09..cb8089b0 100644 --- a/Models/MemberChangeEntry.cs +++ b/Models/MemberChangeEntry.cs @@ -12,7 +12,7 @@ namespace FolderDiffIL4DotNet.Models /// Member name (C# name; constructors use the class name, not .ctor). Empty for Type entries. / メンバー名(C# 名、コンストラクタは .ctor ではなくクラス名)。Type の場合は空。 /// For Field/Property: the declared type (e.g. "string", "int"). Empty for Method/Constructor/Type entries. / フィールド・プロパティの宣言型。メソッド・コンストラクタ・Type の場合は空。 /// For Method: the return type (e.g. "void", "string"). For Constructor: "void". Empty for Type/Field/Property entries. / メソッドの戻り値型。コンストラクタは "void"。Type/Field/Property の場合は空。 - /// For Method/Constructor: the parameter list including parentheses (e.g. "(int page)", "()"). Empty for Type/Field/Property entries. / メソッド・コンストラクタのパラメータ一覧(括弧含む)。Type/Field/Property の場合は空。 + /// For Method/Constructor: the parameter list without parentheses (e.g. "int page", "string name, int count = 0"). Empty string for no-arg methods. Empty for Type/Field/Property entries. / メソッド・コンストラクタのパラメータ一覧(括弧なし)。引数なしは空文字列。Type/Field/Property の場合は空。 public sealed record MemberChangeEntry( string Change, string TypeName, diff --git a/README.md b/README.md index e61d4f27..827c954e 100644 --- a/README.md +++ b/README.md @@ -213,7 +213,7 @@ When an assembly is classified as `ILMismatch`, the tool performs an additional | Type | Declared type for Field/Property (empty for Method/Constructor/Class/Record) | `int` | | Name | Member name (constructors use the class name; empty for Class/Record/Struct/Interface/Enum entries) | `DoWork` | | ReturnType | Return type for Method/Constructor (empty for Field/Property/Class/Record) | `void` | -| Parameters | Parameter list for Method/Constructor (empty for Field/Property/Class/Record) | `(string name, int count = 0)` | +| Parameters | Parameter list for Method/Constructor (empty for Field/Property/Class/Record) | `string name, int count = 0` | Controlled by [`ShouldIncludeMethodLevelChangesInReport`](#config-en-shouldincludemethodlevelchangesinreport) (default: `true`). @@ -692,7 +692,7 @@ flowchart TD | Type | Field/Property の宣言型(Method/Constructor/Class/Record の場合は空) | `int` | | Name | メンバー名(コンストラクタはクラス名、Class/Record/Struct/Interface/Enum エントリの場合は空) | `DoWork` | | ReturnType | Method/Constructor の戻り値型(Field/Property/Class/Record の場合は空) | `void` | -| Parameters | Method/Constructor のパラメータ一覧(Field/Property/Class/Record の場合は空) | `(string name, int count = 0)` | +| Parameters | Method/Constructor のパラメータ一覧(Field/Property/Class/Record の場合は空) | `string name, int count = 0` | [`ShouldIncludeMethodLevelChangesInReport`](#config-ja-shouldincludemethodlevelchangesinreport)(既定値: `true`)で制御します。 diff --git a/Services/AssemblyMethodAnalyzer.cs b/Services/AssemblyMethodAnalyzer.cs index fc57eeff..a9dcba33 100644 --- a/Services/AssemblyMethodAnalyzer.cs +++ b/Services/AssemblyMethodAnalyzer.cs @@ -359,7 +359,7 @@ private static (string ReturnType, string Parameters) BuildMethodSignatureParts( parts.Add(part); } - return (signature.ReturnType, $"({string.Join(", ", parts)})"); + return (signature.ReturnType, string.Join(", ", parts)); } #pragma warning disable CA1031 catch diff --git a/doc/samples/diff_report.html b/doc/samples/diff_report.html index e59afc80..c6ad7415 100644 --- a/doc/samples/diff_report.html +++ b/doc/samples/diff_report.html @@ -437,7 +437,7 @@

[ * ] Modified Files (9)

-
+
#3 Show assembly semantic changes (10 changes)
@@ -478,7 +478,7 @@

[ * ] Modified Files (9)

-
+
#5 Show assembly semantic changes (26 changes)
diff --git a/doc/samples/diff_report.md b/doc/samples/diff_report.md index 909431ae..95757650 100644 --- a/doc/samples/diff_report.md +++ b/doc/samples/diff_report.md @@ -58,13 +58,13 @@ | Change | Class | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | |--------|-------|------|--------|-----------|------|------|------------|------------| -| `Added` | MyApp.Controllers.ApiController | `Method` | `public` | | | HealthCheck | string | () | -| `Added` | MyApp.Controllers.ApiController | `Method` | `public` | | | GetUsers | System.Collections.Generic.IList\ | (int page, int pageSize = 20) | -| `Removed` | MyApp.Controllers.ApiController | `Method` | `public` | | | GetUsers | System.Collections.Generic.IList\ | (int page) | -| `Modified` | MyApp.Controllers.ApiController | `Method` | `public` | `virtual` | | Search | System.Collections.Generic.IList\ | (string query) | -| `Modified` | MyApp.Controllers.ApiController | `Method` | `protected` | | | OnAuthorize | bool | (MyApp.Models.UserContext ctx) | -| `Modified` | MyApp.Services.DataService | `Method` | `internal` | | | RefreshCache | void | () | -| `Modified` | MyApp.Services.DataService | `Method` | `private` | | | ValidateConnection | bool | (string connStr) | +| `Added` | MyApp.Controllers.ApiController | `Method` | `public` | | | HealthCheck | string | | +| `Added` | MyApp.Controllers.ApiController | `Method` | `public` | | | GetUsers | System.Collections.Generic.IList\ | int page, int pageSize = 20 | +| `Removed` | MyApp.Controllers.ApiController | `Method` | `public` | | | GetUsers | System.Collections.Generic.IList\ | int page | +| `Modified` | MyApp.Controllers.ApiController | `Method` | `public` | `virtual` | | Search | System.Collections.Generic.IList\ | string query | +| `Modified` | MyApp.Controllers.ApiController | `Method` | `protected` | | | OnAuthorize | bool | MyApp.Models.UserContext ctx | +| `Modified` | MyApp.Services.DataService | `Method` | `internal` | | | RefreshCache | void | | +| `Modified` | MyApp.Services.DataService | `Method` | `private` | | | ValidateConnection | bool | string connStr | | `Added` | MyApp.Services.DataService | `Property` | `public` | | int | CacheTimeout | | | | `Added` | MyApp.Services.DataService | `Property` | `public` | | MyApp.Models.CachePolicy | Policy | | | | `Added` | MyApp.Services.DataService | `Property` | `internal` | | MyApp.Services.IConnectionPool | ConnectionPool | | | @@ -75,28 +75,28 @@ | Change | Class | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | |--------|-------|------|--------|-----------|------|------|------------|------------| | `Added` | MyApp.Services.NewValidator | `Class` | `public` | | | | | | -| `Added` | MyApp.Services.NewValidator | `Constructor` | `public` | | | NewValidator | void | () | -| `Added` | MyApp.Services.NewValidator | `Method` | `public` | | | Validate | bool | (string input) | -| `Added` | MyApp.Services.NewValidator | `Method` | `public` | | | Validate | bool | (string input, MyApp.Models.ValidationOptions options) | -| `Added` | MyApp.Services.NewValidator | `Method` | `private` | | | ParseInput | string | (string raw) | +| `Added` | MyApp.Services.NewValidator | `Constructor` | `public` | | | NewValidator | void | | +| `Added` | MyApp.Services.NewValidator | `Method` | `public` | | | Validate | bool | string input | +| `Added` | MyApp.Services.NewValidator | `Method` | `public` | | | Validate | bool | string input, MyApp.Models.ValidationOptions options | +| `Added` | MyApp.Services.NewValidator | `Method` | `private` | | | ParseInput | string | string raw | | `Added` | MyApp.Services.NewValidator | `Property` | `public` | | MyApp.Models.ValidationResult | LastResult | | | | `Added` | MyApp.Services.NewValidator | `Field` | `private` | `readonly` | string | _pattern | | | -| `Added` | MyApp.Services.OrderService | `Method` | `public` | | | ValidateWithNewValidator | bool | (string data) | -| `Removed` | MyApp.Services.OrderService | `Method` | `public` | `virtual` | | LegacyValidate | bool | (string data) | -| `Modified` | MyApp.Services.OrderService | `Method` | `public` | | | ProcessOrder | void | (int orderId) | -| `Modified` | MyApp.Services.OrderService | `Method` | `internal` | `static` | | CalculateTotal | decimal | (int qty, int price) | +| `Added` | MyApp.Services.OrderService | `Method` | `public` | | | ValidateWithNewValidator | bool | string data | +| `Removed` | MyApp.Services.OrderService | `Method` | `public` | `virtual` | | LegacyValidate | bool | string data | +| `Modified` | MyApp.Services.OrderService | `Method` | `public` | | | ProcessOrder | void | int orderId | +| `Modified` | MyApp.Services.OrderService | `Method` | `internal` | `static` | | CalculateTotal | decimal | int qty, int price | | `Added` | MyApp.Services.OrderService | `Property` | `protected` | | MyApp.Models.OrderContext | CurrentContext | | | | `Added` | MyApp.Services.OrderService | `Field` | `private` | `readonly` | MyApp.Models.UserRecord | _defaultUser | | | | `Removed` | MyApp.Services.LegacyHelper | `Class` | `internal` | | | | | | -| `Removed` | MyApp.Services.LegacyHelper | `Method` | `public` | | | Convert | string | (object value) | -| `Removed` | MyApp.Services.LegacyHelper | `Method` | `public` | `static` | | Format | string | (string template, object[] args) | +| `Removed` | MyApp.Services.LegacyHelper | `Method` | `public` | | | Convert | string | object value | +| `Removed` | MyApp.Services.LegacyHelper | `Method` | `public` | `static` | | Format | string | string template, object[] args | | `Added` | MyApp.Models.UserRecord | `Record` | `public` | | | | | | -| `Added` | MyApp.Models.UserRecord | `Constructor` | `public` | | | UserRecord | void | (string Name, int Age) | +| `Added` | MyApp.Models.UserRecord | `Constructor` | `public` | | | UserRecord | void | string Name, int Age | | `Added` | MyApp.Models.UserRecord | `Property` | `public` | | string | Name | | | | `Added` | MyApp.Models.UserRecord | `Property` | `public` | | int | Age | | | -| `Added` | MyApp.Models.UserRecord | `Method` | `public` | `override` | | ToString | string | () | -| `Added` | MyApp.Models.UserRecord | `Method` | `public` | `virtual` | | Equals | bool | (object obj) | -| `Added` | MyApp.Models.UserRecord | `Method` | `public` | `override` | | GetHashCode | int | () | +| `Added` | MyApp.Models.UserRecord | `Method` | `public` | `override` | | ToString | string | | +| `Added` | MyApp.Models.UserRecord | `Method` | `public` | `virtual` | | Equals | bool | object obj | +| `Added` | MyApp.Models.UserRecord | `Method` | `public` | `override` | | GetHashCode | int | | | `Removed` | MyApp.Models.UserDto | `Class` | `public` | | | | | | | `Removed` | MyApp.Models.UserDto | `Property` | `public` | | string | Name | | | | `Removed` | MyApp.Models.UserDto | `Property` | `public` | | int | Age | | | From c39df79cbc319001192bb189fa9ae21799f686b0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 18:56:34 +0000 Subject: [PATCH 13/36] Add Body column and replace member count with Added/Removed/Modified counts Add a 10th column (Body) to the Assembly Semantic Changes table that shows "Changed" when a method body or field initializer has been modified at the IL level. Replace "Member count: N (Old) vs N (New)" summary with "Added: N, Removed: N, Modified: N" computed from entries. Remove OldMethodCount/NewMethodCount from MethodLevelChangesSummary in favour of computed AddedCount, RemovedCount, ModifiedCount properties. https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- CHANGELOG.md | 4 + .../Models/MethodLevelChangesSummaryTests.cs | 7 +- .../Services/AssemblyMethodAnalyzerTests.cs | 5 +- .../HtmlReportGenerateServiceTests.cs | 18 ++-- .../Services/ReportGenerateServiceTests.cs | 34 ++++---- Models/MemberChangeEntry.cs | 4 +- Models/MethodLevelChangesSummary.cs | 24 ++++-- README.md | 6 ++ Services/AssemblyMethodAnalyzer.cs | 20 ++--- .../HtmlReportGenerateService.Sections.cs | 7 +- .../ReportGenerateService.SectionWriters.cs | 9 +- doc/samples/diff_report.html | 6 +- doc/samples/diff_report.md | 86 +++++++++---------- 13 files changed, 122 insertions(+), 108 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60fb1c35..76082eec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - Removed parentheses from the `Parameters` column values. Since Parameters is now an independent column, wrapping values in `(…)` is redundant. Values now display as `string name, int count = 0` instead of `(string name, int count = 0)`. Empty parameter lists display as blank instead of `()`. +- Added `Body` column (10th column, rightmost) to the Assembly Semantic Changes table. Displays `Changed` when a method body or field initializer has been modified at the IL level; otherwise empty. Entries with body changes use `Modified` in the Change column. Replaced the `Member count: N (Old) vs N (New)` summary line with `Added: N, Removed: N, Modified: N` counts computed from entries. Removed `OldMethodCount`/`NewMethodCount` properties from `MethodLevelChangesSummary` in favour of computed `AddedCount`, `RemovedCount`, and `ModifiedCount` properties. + ### [1.4.1] - 2026-03-20 #### Added @@ -385,6 +387,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - `Parameters` 列の値から括弧を削除。Parameters が独立列となったため `(…)` は冗長。値は `(string name, int count = 0)` ではなく `string name, int count = 0` で表示。引数なしは `()` ではなく空欄。 +- Assembly Semantic Changes テーブルに `Body` 列(10 列目、最右端)を追加。メソッドボディまたはフィールド初期化子が IL レベルで変更された場合に `Changed` を表示、それ以外は空欄。ボディ変更があるエントリの Change 列は `Modified`。集計行を `Member count: N (Old) vs N (New)` から `Added: N, Removed: N, Modified: N`(エントリから算出)に変更。`MethodLevelChangesSummary` の `OldMethodCount`/`NewMethodCount` プロパティを削除し、算出プロパティ `AddedCount`、`RemovedCount`、`ModifiedCount` に置き換え。 + ### [1.4.1] - 2026-03-20 #### 追加 diff --git a/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs b/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs index 48adb58d..eb96738e 100644 --- a/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs +++ b/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs @@ -20,7 +20,7 @@ public void HasChanges_WithEntries_ReturnsTrue() { Entries = new List { - new("Added", "MyApp.Service", "public", "", "Method", "DoWork", "", "void", "int count"), + new("Added", "MyApp.Service", "public", "", "Method", "DoWork", "", "void", "int count", ""), }, }; Assert.True(summary.HasChanges); @@ -32,8 +32,6 @@ public void HasChanges_EmptyEntries_ReturnsFalse() var summary = new MethodLevelChangesSummary { Entries = new List(), - OldMethodCount = 10, - NewMethodCount = 10, }; Assert.False(summary.HasChanges); } @@ -41,7 +39,7 @@ public void HasChanges_EmptyEntries_ReturnsFalse() [Fact] public void Entries_ContainStructuredData() { - var entry = new MemberChangeEntry("Added", "MyApp.Service", "public", "static", "Method", "GetName", "", "string", "string id"); + var entry = new MemberChangeEntry("Added", "MyApp.Service", "public", "static", "Method", "GetName", "", "string", "string id", ""); Assert.Equal("Added", entry.Change); Assert.Equal("MyApp.Service", entry.TypeName); Assert.Equal("public", entry.Access); @@ -51,6 +49,7 @@ public void Entries_ContainStructuredData() Assert.Equal("", entry.MemberType); Assert.Equal("string", entry.ReturnType); Assert.Equal("string id", entry.Parameters); + Assert.Equal("", entry.Body); } } } diff --git a/FolderDiffIL4DotNet.Tests/Services/AssemblyMethodAnalyzerTests.cs b/FolderDiffIL4DotNet.Tests/Services/AssemblyMethodAnalyzerTests.cs index 1e383953..3375b15b 100644 --- a/FolderDiffIL4DotNet.Tests/Services/AssemblyMethodAnalyzerTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/AssemblyMethodAnalyzerTests.cs @@ -17,8 +17,9 @@ public void Analyze_SameAssembly_NoChanges() Assert.NotNull(result); Assert.False(result.HasChanges); Assert.Empty(result.Entries); - Assert.True(result.OldMethodCount > 0); - Assert.Equal(result.OldMethodCount, result.NewMethodCount); + Assert.Equal(0, result.AddedCount); + Assert.Equal(0, result.RemovedCount); + Assert.Equal(0, result.ModifiedCount); } [Fact] diff --git a/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs b/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs index e1a5ab86..d37963c5 100644 --- a/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs @@ -728,14 +728,12 @@ public void GenerateDiffReportHtml_MethodLevelChanges_ShowsInlineAboveILDiff() _resultLists.FileRelativePathToMethodLevelChanges["lib.dll"] = new MethodLevelChangesSummary { - OldMethodCount = 10, - NewMethodCount = 12, Entries = new List { - new("Added", "MyApp.Service", "public", "", "Method", "NewMethod", "", "void", "string name"), - new("Modified", "MyApp.Service", "public", "virtual", "Method", "ExistingMethod", "", "bool", "int id"), - new("Added", "MyApp.Service", "public", "", "Property", "NewProp", "string", "", ""), - new("Removed", "MyApp.Service", "private", "readonly", "Field", "_oldField", "int", "", ""), + new("Added", "MyApp.Service", "public", "", "Method", "NewMethod", "", "void", "string name", ""), + new("Modified", "MyApp.Service", "public", "virtual", "Method", "ExistingMethod", "", "bool", "int id", "Changed"), + new("Added", "MyApp.Service", "public", "", "Property", "NewProp", "string", "", "", ""), + new("Removed", "MyApp.Service", "private", "readonly", "Field", "_oldField", "int", "", "", ""), }, }; @@ -761,11 +759,9 @@ public void GenerateDiffReportHtml_MethodLevelChanges_NotShownWhenDisabled() _resultLists.FileRelativePathToMethodLevelChanges["lib.dll"] = new MethodLevelChangesSummary { - OldMethodCount = 10, - NewMethodCount = 12, Entries = new List { - new("Added", "MyApp.Service", "public", "", "Method", "NewMethod", "", "void", "string name"), + new("Added", "MyApp.Service", "public", "", "Method", "NewMethod", "", "void", "string name", ""), }, }; @@ -789,11 +785,9 @@ public void GenerateDiffReportHtml_MethodLevelChanges_LazyRender_EncodesAsBase64 _resultLists.FileRelativePathToMethodLevelChanges["lib.dll"] = new MethodLevelChangesSummary { - OldMethodCount = 5, - NewMethodCount = 6, Entries = new List { - new("Added", "Foo", "public", "", "Method", "Bar", "", "void", ""), + new("Added", "Foo", "public", "", "Method", "Bar", "", "void", "", ""), }, }; diff --git a/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs b/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs index 8258f995..0b6a39ee 100644 --- a/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs @@ -771,17 +771,15 @@ public void GenerateDiffReport_AssemblySemanticChanges_IncludedBetweenSummaryAnd var summary = new MethodLevelChangesSummary { - OldMethodCount = 42, - NewMethodCount = 44, Entries = new List { - new("Added", "MyApp.NewService", "public", "", "Class", "", "", "", ""), - new("Added", "MyApp.UserService", "public", "static", "Method", "ValidateToken", "", "bool", "string token"), - new("Added", "MyApp.UserService", "internal", "", "Method", "RefreshSession", "", "void", "int userId"), - new("Removed", "MyApp.UserService", "public", "virtual", "Method", "LegacyAuth", "", "void", "string key"), - new("Modified", "MyApp.UserService", "public", "", "Method", "Login", "", "bool", "string user, string pass"), - new("Added", "MyApp.UserService", "public", "", "Property", "IsActive", "bool", "", ""), - new("Added", "MyApp.UserService", "private", "readonly", "Field", "_cache", "object", "", ""), + new("Added", "MyApp.NewService", "public", "", "Class", "", "", "", "", ""), + new("Added", "MyApp.UserService", "public", "static", "Method", "ValidateToken", "", "bool", "string token", ""), + new("Added", "MyApp.UserService", "internal", "", "Method", "RefreshSession", "", "void", "int userId", ""), + new("Removed", "MyApp.UserService", "public", "virtual", "Method", "LegacyAuth", "", "void", "string key", ""), + new("Modified", "MyApp.UserService", "public", "", "Method", "Login", "", "bool", "string user, string pass", "Changed"), + new("Added", "MyApp.UserService", "public", "", "Property", "IsActive", "bool", "", "", ""), + new("Added", "MyApp.UserService", "private", "readonly", "Field", "_cache", "object", "", "", ""), }, }; _resultLists.FileRelativePathToMethodLevelChanges["src/App.dll"] = summary; @@ -801,13 +799,13 @@ public void GenerateDiffReport_AssemblySemanticChanges_IncludedBetweenSummaryAnd // Content checks — table format Assert.Contains("## Assembly Semantic Changes", reportText); Assert.Contains("### src/App.dll", reportText); - Assert.Contains("| Change | Class | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters |", reportText); - Assert.Contains("| `Added` | MyApp.NewService | `Class` | `public` | | | | | |", reportText); - Assert.Contains("| `Added` | MyApp.UserService | `Method` | `public` | `static` | | ValidateToken | bool | string token |", reportText); - Assert.Contains("| `Modified` | MyApp.UserService | `Method` | `public` | | | Login | bool | (string user, string pass) |", reportText); - Assert.Contains("| `Added` | MyApp.UserService | `Property` | `public` | | bool | IsActive | | |", reportText); - Assert.Contains("| `Added` | MyApp.UserService | `Field` | `private` | `readonly` | object | _cache | | |", reportText); - Assert.Contains("- Member count: 42 (Old) vs 44 (New)", reportText); + Assert.Contains("| Change | Class | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | Body |", reportText); + Assert.Contains("| `Added` | MyApp.NewService | `Class` | `public` | | | | | | |", reportText); + Assert.Contains("| `Added` | MyApp.UserService | `Method` | `public` | `static` | | ValidateToken | bool | string token | |", reportText); + Assert.Contains("| `Modified` | MyApp.UserService | `Method` | `public` | | | Login | bool | string user, string pass | `Changed` |", reportText); + Assert.Contains("| `Added` | MyApp.UserService | `Property` | `public` | | bool | IsActive | | | |", reportText); + Assert.Contains("| `Added` | MyApp.UserService | `Field` | `private` | `readonly` | object | _cache | | | |", reportText); + Assert.Contains("- Added: 5, Removed: 1, Modified: 1", reportText); // Ordering: Summary < Assembly Semantic Changes < IL Cache Stats int summaryIdx = reportText.IndexOf("## Summary", StringComparison.Ordinal); @@ -829,11 +827,9 @@ public void GenerateDiffReport_MethodLevelChanges_NotIncludedWhenDisabled() _resultLists.FileRelativePathToMethodLevelChanges["src/App.dll"] = new MethodLevelChangesSummary { - OldMethodCount = 10, - NewMethodCount = 12, Entries = new List { - new("Added", "Foo", "public", "", "Method", "Bar", "", "void", ""), + new("Added", "Foo", "public", "", "Method", "Bar", "", "void", "", ""), }, }; diff --git a/Models/MemberChangeEntry.cs b/Models/MemberChangeEntry.cs index cb8089b0..50b04871 100644 --- a/Models/MemberChangeEntry.cs +++ b/Models/MemberChangeEntry.cs @@ -13,6 +13,7 @@ namespace FolderDiffIL4DotNet.Models /// For Field/Property: the declared type (e.g. "string", "int"). Empty for Method/Constructor/Type entries. / フィールド・プロパティの宣言型。メソッド・コンストラクタ・Type の場合は空。 /// For Method: the return type (e.g. "void", "string"). For Constructor: "void". Empty for Type/Field/Property entries. / メソッドの戻り値型。コンストラクタは "void"。Type/Field/Property の場合は空。 /// For Method/Constructor: the parameter list without parentheses (e.g. "int page", "string name, int count = 0"). Empty string for no-arg methods. Empty for Type/Field/Property entries. / メソッド・コンストラクタのパラメータ一覧(括弧なし)。引数なしは空文字列。Type/Field/Property の場合は空。 + /// "Changed" when the method body or field initializer IL has changed; otherwise empty. / メソッドボディまたはフィールド初期化子の IL が変更された場合 "Changed"、それ以外は空。 public sealed record MemberChangeEntry( string Change, string TypeName, @@ -22,5 +23,6 @@ public sealed record MemberChangeEntry( string MemberName, string MemberType, string ReturnType, - string Parameters); + string Parameters, + string Body); } diff --git a/Models/MethodLevelChangesSummary.cs b/Models/MethodLevelChangesSummary.cs index c7c48a3f..189b902b 100644 --- a/Models/MethodLevelChangesSummary.cs +++ b/Models/MethodLevelChangesSummary.cs @@ -13,13 +13,25 @@ public sealed class MethodLevelChangesSummary /// All detected member-level changes. / 検出されたすべてのメンバーレベル変更。 public IReadOnlyList Entries { get; init; } = []; - /// Total member (method) count in the old assembly. / 旧アセンブリのメンバー(メソッド)総数。 - public int OldMethodCount { get; init; } - - /// Total member (method) count in the new assembly. / 新アセンブリのメンバー(メソッド)総数。 - public int NewMethodCount { get; init; } - /// Whether any changes were detected. / 何らかの変更が検出されたかどうか。 public bool HasChanges => Entries.Count > 0; + + /// Number of entries with Change="Added". / Change="Added" のエントリ数。 + public int AddedCount => CountByChange("Added"); + + /// Number of entries with Change="Removed". / Change="Removed" のエントリ数。 + public int RemovedCount => CountByChange("Removed"); + + /// Number of entries with Change="Modified". / Change="Modified" のエントリ数。 + public int ModifiedCount => CountByChange("Modified"); + + private int CountByChange(string change) + { + int count = 0; + foreach (var e in Entries) + if (string.Equals(e.Change, change, System.StringComparison.Ordinal)) + count++; + return count; + } } } diff --git a/README.md b/README.md index 827c954e..8d1623c1 100644 --- a/README.md +++ b/README.md @@ -214,9 +214,12 @@ When an assembly is classified as `ILMismatch`, the tool performs an additional | Name | Member name (constructors use the class name; empty for Class/Record/Struct/Interface/Enum entries) | `DoWork` | | ReturnType | Return type for Method/Constructor (empty for Field/Property/Class/Record) | `void` | | Parameters | Parameter list for Method/Constructor (empty for Field/Property/Class/Record) | `string name, int count = 0` | +| Body | `Changed` when method body or field initializer IL has changed; otherwise empty | `Changed` | Controlled by [`ShouldIncludeMethodLevelChangesInReport`](#config-en-shouldincludemethodlevelchangesinreport) (default: `true`). +The summary line below the table shows counts: `Added: N, Removed: N, Modified: N`. + ## Configuration ([`config.json`](config.json)) Place [`config.json`](config.json) next to the executable. All keys are optional; omitted keys use the code-defined defaults in [`ConfigSettings`](Models/ConfigSettings.cs). If the defaults are acceptable, this file can be just: @@ -693,9 +696,12 @@ flowchart TD | Name | メンバー名(コンストラクタはクラス名、Class/Record/Struct/Interface/Enum エントリの場合は空) | `DoWork` | | ReturnType | Method/Constructor の戻り値型(Field/Property/Class/Record の場合は空) | `void` | | Parameters | Method/Constructor のパラメータ一覧(Field/Property/Class/Record の場合は空) | `string name, int count = 0` | +| Body | メソッドボディまたはフィールド初期化子の IL が変更された場合 `Changed`、それ以外は空 | `Changed` | [`ShouldIncludeMethodLevelChangesInReport`](#config-ja-shouldincludemethodlevelchangesinreport)(既定値: `true`)で制御します。 +テーブル下の集計行にはカウントが表示されます: `Added: N, Removed: N, Modified: N`。 + ## 設定([`config.json`](config.json)) 実行ファイルと同じディレクトリに配置します。全項目省略可能で、未指定の項目は [`ConfigSettings`](Models/ConfigSettings.cs) に定義されたコード既定値を使います。既定値のままでよければ、次のように空オブジェクトだけで構いません。 diff --git a/Services/AssemblyMethodAnalyzer.cs b/Services/AssemblyMethodAnalyzer.cs index a9dcba33..ac9a8d11 100644 --- a/Services/AssemblyMethodAnalyzer.cs +++ b/Services/AssemblyMethodAnalyzer.cs @@ -39,12 +39,12 @@ internal static class AssemblyMethodAnalyzer foreach (var t in newSnapshot.TypeNames.Keys.Except(oldSnapshot.TypeNames.Keys, StringComparer.Ordinal).OrderBy(t => t, StringComparer.Ordinal)) { var info = newSnapshot.TypeNames[t]; - entries.Add(new MemberChangeEntry("Added", t, info.Access, "", info.Kind, "", "", "", "")); + entries.Add(new MemberChangeEntry("Added", t, info.Access, "", info.Kind, "", "", "", "", "")); } foreach (var t in oldSnapshot.TypeNames.Keys.Except(newSnapshot.TypeNames.Keys, StringComparer.Ordinal).OrderBy(t => t, StringComparer.Ordinal)) { var info = oldSnapshot.TypeNames[t]; - entries.Add(new MemberChangeEntry("Removed", t, info.Access, "", info.Kind, "", "", "", "")); + entries.Add(new MemberChangeEntry("Removed", t, info.Access, "", info.Kind, "", "", "", "", "")); } // Methods (including constructors) @@ -52,13 +52,13 @@ internal static class AssemblyMethodAnalyzer { var m = newSnapshot.Methods[key]; string kind = ToMemberKind(m.MethodName); - entries.Add(new MemberChangeEntry("Added", m.TypeName, m.Access, m.Modifiers, kind, ToCSharpMethodName(m.MethodName, m.TypeName), "", m.ReturnType, m.Parameters)); + entries.Add(new MemberChangeEntry("Added", m.TypeName, m.Access, m.Modifiers, kind, ToCSharpMethodName(m.MethodName, m.TypeName), "", m.ReturnType, m.Parameters, "")); } foreach (var key in oldSnapshot.Methods.Keys.Except(newSnapshot.Methods.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var m = oldSnapshot.Methods[key]; string kind = ToMemberKind(m.MethodName); - entries.Add(new MemberChangeEntry("Removed", m.TypeName, m.Access, m.Modifiers, kind, ToCSharpMethodName(m.MethodName, m.TypeName), "", m.ReturnType, m.Parameters)); + entries.Add(new MemberChangeEntry("Removed", m.TypeName, m.Access, m.Modifiers, kind, ToCSharpMethodName(m.MethodName, m.TypeName), "", m.ReturnType, m.Parameters, "")); } foreach (var key in oldSnapshot.Methods.Keys.Intersect(newSnapshot.Methods.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { @@ -66,7 +66,7 @@ internal static class AssemblyMethodAnalyzer { var m = newSnapshot.Methods[key]; string kind = ToMemberKind(m.MethodName); - entries.Add(new MemberChangeEntry("Modified", m.TypeName, m.Access, m.Modifiers, kind, ToCSharpMethodName(m.MethodName, m.TypeName), "", m.ReturnType, m.Parameters)); + entries.Add(new MemberChangeEntry("Modified", m.TypeName, m.Access, m.Modifiers, kind, ToCSharpMethodName(m.MethodName, m.TypeName), "", m.ReturnType, m.Parameters, "Changed")); } } @@ -74,31 +74,29 @@ internal static class AssemblyMethodAnalyzer foreach (var key in newSnapshot.Properties.Keys.Except(oldSnapshot.Properties.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var p = newSnapshot.Properties[key]; - entries.Add(new MemberChangeEntry("Added", p.TypeName, p.Access, p.Modifiers, "Property", p.PropertyName, p.PropertyType, "", "")); + entries.Add(new MemberChangeEntry("Added", p.TypeName, p.Access, p.Modifiers, "Property", p.PropertyName, p.PropertyType, "", "", "")); } foreach (var key in oldSnapshot.Properties.Keys.Except(newSnapshot.Properties.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var p = oldSnapshot.Properties[key]; - entries.Add(new MemberChangeEntry("Removed", p.TypeName, p.Access, p.Modifiers, "Property", p.PropertyName, p.PropertyType, "", "")); + entries.Add(new MemberChangeEntry("Removed", p.TypeName, p.Access, p.Modifiers, "Property", p.PropertyName, p.PropertyType, "", "", "")); } // Fields foreach (var key in newSnapshot.Fields.Keys.Except(oldSnapshot.Fields.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var f = newSnapshot.Fields[key]; - entries.Add(new MemberChangeEntry("Added", f.TypeName, f.Access, f.Modifiers, "Field", f.FieldName, StripColonPrefix(f.Details), "", "")); + entries.Add(new MemberChangeEntry("Added", f.TypeName, f.Access, f.Modifiers, "Field", f.FieldName, StripColonPrefix(f.Details), "", "", "")); } foreach (var key in oldSnapshot.Fields.Keys.Except(newSnapshot.Fields.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var f = oldSnapshot.Fields[key]; - entries.Add(new MemberChangeEntry("Removed", f.TypeName, f.Access, f.Modifiers, "Field", f.FieldName, StripColonPrefix(f.Details), "", "")); + entries.Add(new MemberChangeEntry("Removed", f.TypeName, f.Access, f.Modifiers, "Field", f.FieldName, StripColonPrefix(f.Details), "", "", "")); } return new MethodLevelChangesSummary { Entries = entries, - OldMethodCount = oldSnapshot.Methods.Count, - NewMethodCount = newSnapshot.Methods.Count, }; } #pragma warning disable CA1031 // ベストエフォート解析のため全例外をキャッチ / Catch-all for best-effort analysis diff --git a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs index 47c555af..d4c4d52b 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs @@ -362,13 +362,14 @@ private void AppendMethodLevelChangesRow( if (summary.Entries.Count > 0) { contentBuilder.AppendLine(""); - contentBuilder.AppendLine(""); + contentBuilder.AppendLine(""); contentBuilder.AppendLine(""); foreach (var e in summary.Entries) { string accessTd = e.Access.Length > 0 ? $"{HtmlEncode(e.Access)}" : ""; string modifiersTd = e.Modifiers.Length > 0 ? $"{HtmlEncode(e.Modifiers)}" : ""; - contentBuilder.AppendLine($""); + string bodyTd = e.Body.Length > 0 ? $"{HtmlEncode(e.Body)}" : ""; + contentBuilder.AppendLine($""); } contentBuilder.AppendLine("
ChangeClassKindAccessModifiersTypeNameReturnTypeParameters
ChangeClassKindAccessModifiersTypeNameReturnTypeParametersBody
{HtmlEncode(e.Change)}{HtmlEncode(e.TypeName)}{HtmlEncode(e.MemberKind)}{accessTd}{modifiersTd}{HtmlEncode(e.MemberType)}{HtmlEncode(e.MemberName)}{HtmlEncode(e.ReturnType)}{HtmlEncode(e.Parameters)}
{HtmlEncode(e.Change)}{HtmlEncode(e.TypeName)}{HtmlEncode(e.MemberKind)}{accessTd}{modifiersTd}{HtmlEncode(e.MemberType)}{HtmlEncode(e.MemberName)}{HtmlEncode(e.ReturnType)}{HtmlEncode(e.Parameters)}{bodyTd}
"); } @@ -377,7 +378,7 @@ private void AppendMethodLevelChangesRow( contentBuilder.AppendLine("

Other changes only. See IL diff for details.

"); } - contentBuilder.AppendLine($"

Member count: {summary.OldMethodCount} (Old) vs {summary.NewMethodCount} (New)

"); + contentBuilder.AppendLine($"

Added: {summary.AddedCount}, Removed: {summary.RemovedCount}, Modified: {summary.ModifiedCount}

"); contentBuilder.AppendLine(""); string detailsId = $"methods_{sectionPrefix}_{idx}"; diff --git a/Services/ReportGenerateService.SectionWriters.cs b/Services/ReportGenerateService.SectionWriters.cs index c720a4aa..98a6f243 100644 --- a/Services/ReportGenerateService.SectionWriters.cs +++ b/Services/ReportGenerateService.SectionWriters.cs @@ -209,13 +209,14 @@ public void Write(StreamWriter writer, ReportWriteContext ctx) if (summary.Entries.Count > 0) { writer.WriteLine(); - writer.WriteLine("| Change | Class | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters |"); - writer.WriteLine("|--------|-------|------|--------|-----------|------|------|------------|------------|"); + writer.WriteLine("| Change | Class | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | Body |"); + writer.WriteLine("|--------|-------|------|--------|-----------|------|------|------------|------------|------|"); foreach (var e in summary.Entries) { string access = e.Access.Length > 0 ? $"`{EscapeMdTable(e.Access)}`" : ""; string modifiers = e.Modifiers.Length > 0 ? $"`{EscapeMdTable(e.Modifiers)}`" : ""; - writer.WriteLine($"| `{EscapeMdTable(e.Change)}` | {EscapeMdTable(e.TypeName)} | `{EscapeMdTable(e.MemberKind)}` | {access} | {modifiers} | {EscapeMdTable(e.MemberType)} | {EscapeMdTable(e.MemberName)} | {EscapeMdTable(e.ReturnType)} | {EscapeMdTable(e.Parameters)} |"); + string body = e.Body.Length > 0 ? $"`{EscapeMdTable(e.Body)}`" : ""; + writer.WriteLine($"| `{EscapeMdTable(e.Change)}` | {EscapeMdTable(e.TypeName)} | `{EscapeMdTable(e.MemberKind)}` | {access} | {modifiers} | {EscapeMdTable(e.MemberType)} | {EscapeMdTable(e.MemberName)} | {EscapeMdTable(e.ReturnType)} | {EscapeMdTable(e.Parameters)} | {body} |"); } } else @@ -223,7 +224,7 @@ public void Write(StreamWriter writer, ReportWriteContext ctx) writer.WriteLine("- Other changes only. See IL diff for details."); } - writer.WriteLine($"- Member count: {summary.OldMethodCount} (Old) vs {summary.NewMethodCount} (New)"); + writer.WriteLine($"- Added: {summary.AddedCount}, Removed: {summary.RemovedCount}, Modified: {summary.ModifiedCount}"); } writer.WriteLine(); diff --git a/doc/samples/diff_report.html b/doc/samples/diff_report.html index c6ad7415..30df9b79 100644 --- a/doc/samples/diff_report.html +++ b/doc/samples/diff_report.html @@ -437,7 +437,7 @@

[ * ] Modified Files (9)

-
+
#3 Show assembly semantic changes (10 changes)
@@ -478,7 +478,7 @@

[ * ] Modified Files (9)

-
+
#5 Show assembly semantic changes (26 changes)
@@ -545,7 +545,7 @@

[ * ] Modified Files (9)

-
+
#9 Show assembly semantic changes (other changes only)
diff --git a/doc/samples/diff_report.md b/doc/samples/diff_report.md index 95757650..2cba9ea8 100644 --- a/doc/samples/diff_report.md +++ b/doc/samples/diff_report.md @@ -56,55 +56,55 @@ ### src/App.dll -| Change | Class | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | -|--------|-------|------|--------|-----------|------|------|------------|------------| -| `Added` | MyApp.Controllers.ApiController | `Method` | `public` | | | HealthCheck | string | | -| `Added` | MyApp.Controllers.ApiController | `Method` | `public` | | | GetUsers | System.Collections.Generic.IList\ | int page, int pageSize = 20 | -| `Removed` | MyApp.Controllers.ApiController | `Method` | `public` | | | GetUsers | System.Collections.Generic.IList\ | int page | -| `Modified` | MyApp.Controllers.ApiController | `Method` | `public` | `virtual` | | Search | System.Collections.Generic.IList\ | string query | -| `Modified` | MyApp.Controllers.ApiController | `Method` | `protected` | | | OnAuthorize | bool | MyApp.Models.UserContext ctx | -| `Modified` | MyApp.Services.DataService | `Method` | `internal` | | | RefreshCache | void | | -| `Modified` | MyApp.Services.DataService | `Method` | `private` | | | ValidateConnection | bool | string connStr | -| `Added` | MyApp.Services.DataService | `Property` | `public` | | int | CacheTimeout | | | -| `Added` | MyApp.Services.DataService | `Property` | `public` | | MyApp.Models.CachePolicy | Policy | | | -| `Added` | MyApp.Services.DataService | `Property` | `internal` | | MyApp.Services.IConnectionPool | ConnectionPool | | | -- Member count: 28 (Old) vs 30 (New) +| Change | Class | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | Body | +|--------|-------|------|--------|-----------|------|------|------------|------------|------| +| `Added` | MyApp.Controllers.ApiController | `Method` | `public` | | | HealthCheck | string | | | +| `Added` | MyApp.Controllers.ApiController | `Method` | `public` | | | GetUsers | System.Collections.Generic.IList\ | int page, int pageSize = 20 | | +| `Removed` | MyApp.Controllers.ApiController | `Method` | `public` | | | GetUsers | System.Collections.Generic.IList\ | int page | | +| `Modified` | MyApp.Controllers.ApiController | `Method` | `public` | `virtual` | | Search | System.Collections.Generic.IList\ | string query | `Changed` | +| `Modified` | MyApp.Controllers.ApiController | `Method` | `protected` | | | OnAuthorize | bool | MyApp.Models.UserContext ctx | `Changed` | +| `Modified` | MyApp.Services.DataService | `Method` | `internal` | | | RefreshCache | void | | `Changed` | +| `Modified` | MyApp.Services.DataService | `Method` | `private` | | | ValidateConnection | bool | string connStr | `Changed` | +| `Added` | MyApp.Services.DataService | `Property` | `public` | | int | CacheTimeout | | | | +| `Added` | MyApp.Services.DataService | `Property` | `public` | | MyApp.Models.CachePolicy | Policy | | | | +| `Added` | MyApp.Services.DataService | `Property` | `internal` | | MyApp.Services.IConnectionPool | ConnectionPool | | | | +- Added: 5, Removed: 1, Modified: 4 ### src/Service.dll -| Change | Class | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | -|--------|-------|------|--------|-----------|------|------|------------|------------| -| `Added` | MyApp.Services.NewValidator | `Class` | `public` | | | | | | -| `Added` | MyApp.Services.NewValidator | `Constructor` | `public` | | | NewValidator | void | | -| `Added` | MyApp.Services.NewValidator | `Method` | `public` | | | Validate | bool | string input | -| `Added` | MyApp.Services.NewValidator | `Method` | `public` | | | Validate | bool | string input, MyApp.Models.ValidationOptions options | -| `Added` | MyApp.Services.NewValidator | `Method` | `private` | | | ParseInput | string | string raw | -| `Added` | MyApp.Services.NewValidator | `Property` | `public` | | MyApp.Models.ValidationResult | LastResult | | | -| `Added` | MyApp.Services.NewValidator | `Field` | `private` | `readonly` | string | _pattern | | | -| `Added` | MyApp.Services.OrderService | `Method` | `public` | | | ValidateWithNewValidator | bool | string data | -| `Removed` | MyApp.Services.OrderService | `Method` | `public` | `virtual` | | LegacyValidate | bool | string data | -| `Modified` | MyApp.Services.OrderService | `Method` | `public` | | | ProcessOrder | void | int orderId | -| `Modified` | MyApp.Services.OrderService | `Method` | `internal` | `static` | | CalculateTotal | decimal | int qty, int price | -| `Added` | MyApp.Services.OrderService | `Property` | `protected` | | MyApp.Models.OrderContext | CurrentContext | | | -| `Added` | MyApp.Services.OrderService | `Field` | `private` | `readonly` | MyApp.Models.UserRecord | _defaultUser | | | -| `Removed` | MyApp.Services.LegacyHelper | `Class` | `internal` | | | | | | -| `Removed` | MyApp.Services.LegacyHelper | `Method` | `public` | | | Convert | string | object value | -| `Removed` | MyApp.Services.LegacyHelper | `Method` | `public` | `static` | | Format | string | string template, object[] args | -| `Added` | MyApp.Models.UserRecord | `Record` | `public` | | | | | | -| `Added` | MyApp.Models.UserRecord | `Constructor` | `public` | | | UserRecord | void | string Name, int Age | -| `Added` | MyApp.Models.UserRecord | `Property` | `public` | | string | Name | | | -| `Added` | MyApp.Models.UserRecord | `Property` | `public` | | int | Age | | | -| `Added` | MyApp.Models.UserRecord | `Method` | `public` | `override` | | ToString | string | | -| `Added` | MyApp.Models.UserRecord | `Method` | `public` | `virtual` | | Equals | bool | object obj | -| `Added` | MyApp.Models.UserRecord | `Method` | `public` | `override` | | GetHashCode | int | | -| `Removed` | MyApp.Models.UserDto | `Class` | `public` | | | | | | -| `Removed` | MyApp.Models.UserDto | `Property` | `public` | | string | Name | | | -| `Removed` | MyApp.Models.UserDto | `Property` | `public` | | int | Age | | | -- Member count: 15 (Old) vs 26 (New) +| Change | Class | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | Body | +|--------|-------|------|--------|-----------|------|------|------------|------------|------| +| `Added` | MyApp.Services.NewValidator | `Class` | `public` | | | | | | | +| `Added` | MyApp.Services.NewValidator | `Constructor` | `public` | | | NewValidator | void | | | +| `Added` | MyApp.Services.NewValidator | `Method` | `public` | | | Validate | bool | string input | | +| `Added` | MyApp.Services.NewValidator | `Method` | `public` | | | Validate | bool | string input, MyApp.Models.ValidationOptions options | | +| `Added` | MyApp.Services.NewValidator | `Method` | `private` | | | ParseInput | string | string raw | | +| `Added` | MyApp.Services.NewValidator | `Property` | `public` | | MyApp.Models.ValidationResult | LastResult | | | | +| `Added` | MyApp.Services.NewValidator | `Field` | `private` | `readonly` | string | _pattern | | | | +| `Added` | MyApp.Services.OrderService | `Method` | `public` | | | ValidateWithNewValidator | bool | string data | | +| `Removed` | MyApp.Services.OrderService | `Method` | `public` | `virtual` | | LegacyValidate | bool | string data | | +| `Modified` | MyApp.Services.OrderService | `Method` | `public` | | | ProcessOrder | void | int orderId | `Changed` | +| `Modified` | MyApp.Services.OrderService | `Method` | `internal` | `static` | | CalculateTotal | decimal | int qty, int price | `Changed` | +| `Added` | MyApp.Services.OrderService | `Property` | `protected` | | MyApp.Models.OrderContext | CurrentContext | | | | +| `Added` | MyApp.Services.OrderService | `Field` | `private` | `readonly` | MyApp.Models.UserRecord | _defaultUser | | | | +| `Removed` | MyApp.Services.LegacyHelper | `Class` | `internal` | | | | | | | +| `Removed` | MyApp.Services.LegacyHelper | `Method` | `public` | | | Convert | string | object value | | +| `Removed` | MyApp.Services.LegacyHelper | `Method` | `public` | `static` | | Format | string | string template, object[] args | | +| `Added` | MyApp.Models.UserRecord | `Record` | `public` | | | | | | | +| `Added` | MyApp.Models.UserRecord | `Constructor` | `public` | | | UserRecord | void | string Name, int Age | | +| `Added` | MyApp.Models.UserRecord | `Property` | `public` | | string | Name | | | | +| `Added` | MyApp.Models.UserRecord | `Property` | `public` | | int | Age | | | | +| `Added` | MyApp.Models.UserRecord | `Method` | `public` | `override` | | ToString | string | | | +| `Added` | MyApp.Models.UserRecord | `Method` | `public` | `virtual` | | Equals | bool | object obj | | +| `Added` | MyApp.Models.UserRecord | `Method` | `public` | `override` | | GetHashCode | int | | | +| `Removed` | MyApp.Models.UserDto | `Class` | `public` | | | | | | | +| `Removed` | MyApp.Models.UserDto | `Property` | `public` | | string | Name | | | | +| `Removed` | MyApp.Models.UserDto | `Property` | `public` | | int | Age | | | | +- Added: 17, Removed: 7, Modified: 2 ### util/Legacy.dll - Other changes only. See IL diff for details. -- Member count: 8 (Old) vs 8 (New) +- Added: 0, Removed: 0, Modified: 0 ## IL Cache Stats - Hits : 42 From d117e2c19045a4caab640d18684bb714aa1e51ea Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 19:00:26 +0000 Subject: [PATCH 14/36] Swap Class and Change columns; use bullet list for change counts Move Class column to the first position for better readability. Change the summary from a single-line "Added: N, Removed: N, Modified: N" to a bullet list matching the Summary section style. https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- .../Services/ReportGenerateServiceTests.cs | 16 ++-- README.md | 8 +- .../HtmlReportGenerateService.Sections.cs | 10 +- .../ReportGenerateService.SectionWriters.cs | 10 +- doc/samples/diff_report.html | 6 +- doc/samples/diff_report.md | 92 ++++++++++--------- 6 files changed, 78 insertions(+), 64 deletions(-) diff --git a/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs b/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs index 0b6a39ee..a133056b 100644 --- a/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs @@ -799,13 +799,15 @@ public void GenerateDiffReport_AssemblySemanticChanges_IncludedBetweenSummaryAnd // Content checks — table format Assert.Contains("## Assembly Semantic Changes", reportText); Assert.Contains("### src/App.dll", reportText); - Assert.Contains("| Change | Class | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | Body |", reportText); - Assert.Contains("| `Added` | MyApp.NewService | `Class` | `public` | | | | | | |", reportText); - Assert.Contains("| `Added` | MyApp.UserService | `Method` | `public` | `static` | | ValidateToken | bool | string token | |", reportText); - Assert.Contains("| `Modified` | MyApp.UserService | `Method` | `public` | | | Login | bool | string user, string pass | `Changed` |", reportText); - Assert.Contains("| `Added` | MyApp.UserService | `Property` | `public` | | bool | IsActive | | | |", reportText); - Assert.Contains("| `Added` | MyApp.UserService | `Field` | `private` | `readonly` | object | _cache | | | |", reportText); - Assert.Contains("- Added: 5, Removed: 1, Modified: 1", reportText); + Assert.Contains("| Class | Change | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | Body |", reportText); + Assert.Contains("| MyApp.NewService | `Added` | `Class` | `public` | | | | | | |", reportText); + Assert.Contains("| MyApp.UserService | `Added` | `Method` | `public` | `static` | | ValidateToken | bool | string token | |", reportText); + Assert.Contains("| MyApp.UserService | `Modified` | `Method` | `public` | | | Login | bool | string user, string pass | `Changed` |", reportText); + Assert.Contains("| MyApp.UserService | `Added` | `Property` | `public` | | bool | IsActive | | | |", reportText); + Assert.Contains("| MyApp.UserService | `Added` | `Field` | `private` | `readonly` | object | _cache | | | |", reportText); + Assert.Contains("- Added : 5", reportText); + Assert.Contains("- Removed : 1", reportText); + Assert.Contains("- Modified : 1", reportText); // Ordering: Summary < Assembly Semantic Changes < IL Cache Stats int summaryIdx = reportText.IndexOf("## Summary", StringComparison.Ordinal); diff --git a/README.md b/README.md index 8d1623c1..a04f555d 100644 --- a/README.md +++ b/README.md @@ -205,8 +205,8 @@ When an assembly is classified as `ILMismatch`, the tool performs an additional | Column | Description | Example | |--------|-------------|---------| -| Change | `Added`, `Removed`, or `Modified` | `Added` | | Class | Fully qualified type name | `MyNamespace.MyClass` | +| Change | `Added`, `Removed`, or `Modified` | `Added` | | Kind | Member kind: `Class`, `Record`, `Struct`, `Interface`, `Enum`, `Constructor`, `StaticConstructor`, `Method`, `Property`, `Field` | `Method` | | Access | Access modifier | `public` | | Modifiers | Other modifiers | `static` | @@ -218,7 +218,7 @@ When an assembly is classified as `ILMismatch`, the tool performs an additional Controlled by [`ShouldIncludeMethodLevelChangesInReport`](#config-en-shouldincludemethodlevelchangesinreport) (default: `true`). -The summary line below the table shows counts: `Added: N, Removed: N, Modified: N`. +The bullet list below the table shows counts per change kind (`Added`, `Removed`, `Modified`). ## Configuration ([`config.json`](config.json)) @@ -687,8 +687,8 @@ flowchart TD | 列 | 説明 | 例 | |----|------|-----| -| Change | `Added`、`Removed`、`Modified` | `Added` | | Class | 完全修飾型名 | `MyNamespace.MyClass` | +| Change | `Added`、`Removed`、`Modified` | `Added` | | Kind | メンバー種別: `Class`, `Record`, `Struct`, `Interface`, `Enum`, `Constructor`, `StaticConstructor`, `Method`, `Property`, `Field` | `Method` | | Access | アクセス修飾子 | `public` | | Modifiers | その他の修飾子 | `static` | @@ -700,7 +700,7 @@ flowchart TD [`ShouldIncludeMethodLevelChangesInReport`](#config-ja-shouldincludemethodlevelchangesinreport)(既定値: `true`)で制御します。 -テーブル下の集計行にはカウントが表示されます: `Added: N, Removed: N, Modified: N`。 +テーブル下の箇条書きに変更種別ごとのカウント(`Added`、`Removed`、`Modified`)が表示されます。 ## 設定([`config.json`](config.json)) diff --git a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs index d4c4d52b..0e9eae5d 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs @@ -362,14 +362,14 @@ private void AppendMethodLevelChangesRow( if (summary.Entries.Count > 0) { contentBuilder.AppendLine(""); - contentBuilder.AppendLine(""); + contentBuilder.AppendLine(""); contentBuilder.AppendLine(""); foreach (var e in summary.Entries) { string accessTd = e.Access.Length > 0 ? $"{HtmlEncode(e.Access)}" : ""; string modifiersTd = e.Modifiers.Length > 0 ? $"{HtmlEncode(e.Modifiers)}" : ""; string bodyTd = e.Body.Length > 0 ? $"{HtmlEncode(e.Body)}" : ""; - contentBuilder.AppendLine($""); + contentBuilder.AppendLine($""); } contentBuilder.AppendLine("
ChangeClassKindAccessModifiersTypeNameReturnTypeParametersBody
ClassChangeKindAccessModifiersTypeNameReturnTypeParametersBody
{HtmlEncode(e.Change)}{HtmlEncode(e.TypeName)}{HtmlEncode(e.MemberKind)}{accessTd}{modifiersTd}{HtmlEncode(e.MemberType)}{HtmlEncode(e.MemberName)}{HtmlEncode(e.ReturnType)}{HtmlEncode(e.Parameters)}{bodyTd}
{HtmlEncode(e.TypeName)}{HtmlEncode(e.Change)}{HtmlEncode(e.MemberKind)}{accessTd}{modifiersTd}{HtmlEncode(e.MemberType)}{HtmlEncode(e.MemberName)}{HtmlEncode(e.ReturnType)}{HtmlEncode(e.Parameters)}{bodyTd}
"); } @@ -378,7 +378,11 @@ private void AppendMethodLevelChangesRow( contentBuilder.AppendLine("

Other changes only. See IL diff for details.

"); } - contentBuilder.AppendLine($"

Added: {summary.AddedCount}, Removed: {summary.RemovedCount}, Modified: {summary.ModifiedCount}

"); + contentBuilder.AppendLine("
    "); + contentBuilder.AppendLine($"
  • Added : {summary.AddedCount}
  • "); + contentBuilder.AppendLine($"
  • Removed : {summary.RemovedCount}
  • "); + contentBuilder.AppendLine($"
  • Modified : {summary.ModifiedCount}
  • "); + contentBuilder.AppendLine("
"); contentBuilder.AppendLine(""); string detailsId = $"methods_{sectionPrefix}_{idx}"; diff --git a/Services/ReportGenerateService.SectionWriters.cs b/Services/ReportGenerateService.SectionWriters.cs index 98a6f243..4efaa557 100644 --- a/Services/ReportGenerateService.SectionWriters.cs +++ b/Services/ReportGenerateService.SectionWriters.cs @@ -209,14 +209,14 @@ public void Write(StreamWriter writer, ReportWriteContext ctx) if (summary.Entries.Count > 0) { writer.WriteLine(); - writer.WriteLine("| Change | Class | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | Body |"); - writer.WriteLine("|--------|-------|------|--------|-----------|------|------|------------|------------|------|"); + writer.WriteLine("| Class | Change | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | Body |"); + writer.WriteLine("|-------|--------|------|--------|-----------|------|------|------------|------------|------|"); foreach (var e in summary.Entries) { string access = e.Access.Length > 0 ? $"`{EscapeMdTable(e.Access)}`" : ""; string modifiers = e.Modifiers.Length > 0 ? $"`{EscapeMdTable(e.Modifiers)}`" : ""; string body = e.Body.Length > 0 ? $"`{EscapeMdTable(e.Body)}`" : ""; - writer.WriteLine($"| `{EscapeMdTable(e.Change)}` | {EscapeMdTable(e.TypeName)} | `{EscapeMdTable(e.MemberKind)}` | {access} | {modifiers} | {EscapeMdTable(e.MemberType)} | {EscapeMdTable(e.MemberName)} | {EscapeMdTable(e.ReturnType)} | {EscapeMdTable(e.Parameters)} | {body} |"); + writer.WriteLine($"| {EscapeMdTable(e.TypeName)} | `{EscapeMdTable(e.Change)}` | `{EscapeMdTable(e.MemberKind)}` | {access} | {modifiers} | {EscapeMdTable(e.MemberType)} | {EscapeMdTable(e.MemberName)} | {EscapeMdTable(e.ReturnType)} | {EscapeMdTable(e.Parameters)} | {body} |"); } } else @@ -224,7 +224,9 @@ public void Write(StreamWriter writer, ReportWriteContext ctx) writer.WriteLine("- Other changes only. See IL diff for details."); } - writer.WriteLine($"- Added: {summary.AddedCount}, Removed: {summary.RemovedCount}, Modified: {summary.ModifiedCount}"); + writer.WriteLine($"- Added : {summary.AddedCount}"); + writer.WriteLine($"- Removed : {summary.RemovedCount}"); + writer.WriteLine($"- Modified : {summary.ModifiedCount}"); } writer.WriteLine(); diff --git a/doc/samples/diff_report.html b/doc/samples/diff_report.html index 30df9b79..e59afc80 100644 --- a/doc/samples/diff_report.html +++ b/doc/samples/diff_report.html @@ -437,7 +437,7 @@

[ * ] Modified Files (9)

-
+
#3 Show assembly semantic changes (10 changes)
@@ -478,7 +478,7 @@

[ * ] Modified Files (9)

-
+
#5 Show assembly semantic changes (26 changes)
@@ -545,7 +545,7 @@

[ * ] Modified Files (9)

-
+
#9 Show assembly semantic changes (other changes only)
diff --git a/doc/samples/diff_report.md b/doc/samples/diff_report.md index 2cba9ea8..ba3b1618 100644 --- a/doc/samples/diff_report.md +++ b/doc/samples/diff_report.md @@ -56,55 +56,61 @@ ### src/App.dll -| Change | Class | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | Body | -|--------|-------|------|--------|-----------|------|------|------------|------------|------| -| `Added` | MyApp.Controllers.ApiController | `Method` | `public` | | | HealthCheck | string | | | -| `Added` | MyApp.Controllers.ApiController | `Method` | `public` | | | GetUsers | System.Collections.Generic.IList\ | int page, int pageSize = 20 | | -| `Removed` | MyApp.Controllers.ApiController | `Method` | `public` | | | GetUsers | System.Collections.Generic.IList\ | int page | | -| `Modified` | MyApp.Controllers.ApiController | `Method` | `public` | `virtual` | | Search | System.Collections.Generic.IList\ | string query | `Changed` | -| `Modified` | MyApp.Controllers.ApiController | `Method` | `protected` | | | OnAuthorize | bool | MyApp.Models.UserContext ctx | `Changed` | -| `Modified` | MyApp.Services.DataService | `Method` | `internal` | | | RefreshCache | void | | `Changed` | -| `Modified` | MyApp.Services.DataService | `Method` | `private` | | | ValidateConnection | bool | string connStr | `Changed` | -| `Added` | MyApp.Services.DataService | `Property` | `public` | | int | CacheTimeout | | | | -| `Added` | MyApp.Services.DataService | `Property` | `public` | | MyApp.Models.CachePolicy | Policy | | | | -| `Added` | MyApp.Services.DataService | `Property` | `internal` | | MyApp.Services.IConnectionPool | ConnectionPool | | | | -- Added: 5, Removed: 1, Modified: 4 +| Class | Change | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | Body | +|-------|--------|------|--------|-----------|------|------|------------|------------|------| +| MyApp.Controllers.ApiController | `Added` | `Method` | `public` | | | HealthCheck | string | | | +| MyApp.Controllers.ApiController | `Added` | `Method` | `public` | | | GetUsers | System.Collections.Generic.IList\ | int page, int pageSize = 20 | | +| MyApp.Controllers.ApiController | `Removed` | `Method` | `public` | | | GetUsers | System.Collections.Generic.IList\ | int page | | +| MyApp.Controllers.ApiController | `Modified` | `Method` | `public` | `virtual` | | Search | System.Collections.Generic.IList\ | string query | `Changed` | +| MyApp.Controllers.ApiController | `Modified` | `Method` | `protected` | | | OnAuthorize | bool | MyApp.Models.UserContext ctx | `Changed` | +| MyApp.Services.DataService | `Modified` | `Method` | `internal` | | | RefreshCache | void | | `Changed` | +| MyApp.Services.DataService | `Modified` | `Method` | `private` | | | ValidateConnection | bool | string connStr | `Changed` | +| MyApp.Services.DataService | `Added` | `Property` | `public` | | int | CacheTimeout | | | | +| MyApp.Services.DataService | `Added` | `Property` | `public` | | MyApp.Models.CachePolicy | Policy | | | | +| MyApp.Services.DataService | `Added` | `Property` | `internal` | | MyApp.Services.IConnectionPool | ConnectionPool | | | | +- Added : 5 +- Removed : 1 +- Modified : 4 ### src/Service.dll -| Change | Class | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | Body | -|--------|-------|------|--------|-----------|------|------|------------|------------|------| -| `Added` | MyApp.Services.NewValidator | `Class` | `public` | | | | | | | -| `Added` | MyApp.Services.NewValidator | `Constructor` | `public` | | | NewValidator | void | | | -| `Added` | MyApp.Services.NewValidator | `Method` | `public` | | | Validate | bool | string input | | -| `Added` | MyApp.Services.NewValidator | `Method` | `public` | | | Validate | bool | string input, MyApp.Models.ValidationOptions options | | -| `Added` | MyApp.Services.NewValidator | `Method` | `private` | | | ParseInput | string | string raw | | -| `Added` | MyApp.Services.NewValidator | `Property` | `public` | | MyApp.Models.ValidationResult | LastResult | | | | -| `Added` | MyApp.Services.NewValidator | `Field` | `private` | `readonly` | string | _pattern | | | | -| `Added` | MyApp.Services.OrderService | `Method` | `public` | | | ValidateWithNewValidator | bool | string data | | -| `Removed` | MyApp.Services.OrderService | `Method` | `public` | `virtual` | | LegacyValidate | bool | string data | | -| `Modified` | MyApp.Services.OrderService | `Method` | `public` | | | ProcessOrder | void | int orderId | `Changed` | -| `Modified` | MyApp.Services.OrderService | `Method` | `internal` | `static` | | CalculateTotal | decimal | int qty, int price | `Changed` | -| `Added` | MyApp.Services.OrderService | `Property` | `protected` | | MyApp.Models.OrderContext | CurrentContext | | | | -| `Added` | MyApp.Services.OrderService | `Field` | `private` | `readonly` | MyApp.Models.UserRecord | _defaultUser | | | | -| `Removed` | MyApp.Services.LegacyHelper | `Class` | `internal` | | | | | | | -| `Removed` | MyApp.Services.LegacyHelper | `Method` | `public` | | | Convert | string | object value | | -| `Removed` | MyApp.Services.LegacyHelper | `Method` | `public` | `static` | | Format | string | string template, object[] args | | -| `Added` | MyApp.Models.UserRecord | `Record` | `public` | | | | | | | -| `Added` | MyApp.Models.UserRecord | `Constructor` | `public` | | | UserRecord | void | string Name, int Age | | -| `Added` | MyApp.Models.UserRecord | `Property` | `public` | | string | Name | | | | -| `Added` | MyApp.Models.UserRecord | `Property` | `public` | | int | Age | | | | -| `Added` | MyApp.Models.UserRecord | `Method` | `public` | `override` | | ToString | string | | | -| `Added` | MyApp.Models.UserRecord | `Method` | `public` | `virtual` | | Equals | bool | object obj | | -| `Added` | MyApp.Models.UserRecord | `Method` | `public` | `override` | | GetHashCode | int | | | -| `Removed` | MyApp.Models.UserDto | `Class` | `public` | | | | | | | -| `Removed` | MyApp.Models.UserDto | `Property` | `public` | | string | Name | | | | -| `Removed` | MyApp.Models.UserDto | `Property` | `public` | | int | Age | | | | -- Added: 17, Removed: 7, Modified: 2 +| Class | Change | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | Body | +|-------|--------|------|--------|-----------|------|------|------------|------------|------| +| MyApp.Services.NewValidator | `Added` | `Class` | `public` | | | | | | | +| MyApp.Services.NewValidator | `Added` | `Constructor` | `public` | | | NewValidator | void | | | +| MyApp.Services.NewValidator | `Added` | `Method` | `public` | | | Validate | bool | string input | | +| MyApp.Services.NewValidator | `Added` | `Method` | `public` | | | Validate | bool | string input, MyApp.Models.ValidationOptions options | | +| MyApp.Services.NewValidator | `Added` | `Method` | `private` | | | ParseInput | string | string raw | | +| MyApp.Services.NewValidator | `Added` | `Property` | `public` | | MyApp.Models.ValidationResult | LastResult | | | | +| MyApp.Services.NewValidator | `Added` | `Field` | `private` | `readonly` | string | _pattern | | | | +| MyApp.Services.OrderService | `Added` | `Method` | `public` | | | ValidateWithNewValidator | bool | string data | | +| MyApp.Services.OrderService | `Removed` | `Method` | `public` | `virtual` | | LegacyValidate | bool | string data | | +| MyApp.Services.OrderService | `Modified` | `Method` | `public` | | | ProcessOrder | void | int orderId | `Changed` | +| MyApp.Services.OrderService | `Modified` | `Method` | `internal` | `static` | | CalculateTotal | decimal | int qty, int price | `Changed` | +| MyApp.Services.OrderService | `Added` | `Property` | `protected` | | MyApp.Models.OrderContext | CurrentContext | | | | +| MyApp.Services.OrderService | `Added` | `Field` | `private` | `readonly` | MyApp.Models.UserRecord | _defaultUser | | | | +| MyApp.Services.LegacyHelper | `Removed` | `Class` | `internal` | | | | | | | +| MyApp.Services.LegacyHelper | `Removed` | `Method` | `public` | | | Convert | string | object value | | +| MyApp.Services.LegacyHelper | `Removed` | `Method` | `public` | `static` | | Format | string | string template, object[] args | | +| MyApp.Models.UserRecord | `Added` | `Record` | `public` | | | | | | | +| MyApp.Models.UserRecord | `Added` | `Constructor` | `public` | | | UserRecord | void | string Name, int Age | | +| MyApp.Models.UserRecord | `Added` | `Property` | `public` | | string | Name | | | | +| MyApp.Models.UserRecord | `Added` | `Property` | `public` | | int | Age | | | | +| MyApp.Models.UserRecord | `Added` | `Method` | `public` | `override` | | ToString | string | | | +| MyApp.Models.UserRecord | `Added` | `Method` | `public` | `virtual` | | Equals | bool | object obj | | +| MyApp.Models.UserRecord | `Added` | `Method` | `public` | `override` | | GetHashCode | int | | | +| MyApp.Models.UserDto | `Removed` | `Class` | `public` | | | | | | | +| MyApp.Models.UserDto | `Removed` | `Property` | `public` | | string | Name | | | | +| MyApp.Models.UserDto | `Removed` | `Property` | `public` | | int | Age | | | | +- Added : 17 +- Removed : 7 +- Modified : 2 ### util/Legacy.dll - Other changes only. See IL diff for details. -- Added: 0, Removed: 0, Modified: 0 +- Added : 0 +- Removed : 0 +- Modified : 0 ## IL Cache Stats - Hits : 42 From 86515eaac812bc3ce75cc262f754adeec42e35bf Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 19:07:06 +0000 Subject: [PATCH 15/36] Rename MethodLevelChanges to AssemblySemanticChanges throughout Rename ShouldIncludeMethodLevelChangesInReport to ShouldIncludeAssemblySemanticChangesInReport. Rename MethodLevelChangesSummary class/file to AssemblySemanticChangesSummary. Rename all related properties, methods, variables, CSS classes, and HTML element IDs to use the AssemblySemanticChanges naming convention. https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- CHANGELOG.md | 8 ++--- ...=> AssemblySemanticChangesSummaryTests.cs} | 8 ++--- .../HtmlReportGenerateServiceTests.cs | 34 +++++++++---------- .../Services/ReportGenerateServiceTests.cs | 32 ++++++++--------- ...y.cs => AssemblySemanticChangesSummary.cs} | 8 ++--- Models/ConfigSettings.cs | 2 +- Models/FileDiffResultLists.cs | 8 ++--- README.md | 12 +++---- Services/AssemblyMethodAnalyzer.cs | 8 ++--- Services/FileDiffService.cs | 14 ++++---- .../HtmlReportGenerateService.Css.cs | 14 ++++---- .../HtmlReportGenerateService.Sections.cs | 16 ++++----- .../ReportGenerateService.SectionWriters.cs | 6 ++-- Services/ReportGenerateService.cs | 2 +- doc/samples/diff_report.html | 20 +++++------ 15 files changed, 96 insertions(+), 96 deletions(-) rename FolderDiffIL4DotNet.Tests/Models/{MethodLevelChangesSummaryTests.cs => AssemblySemanticChangesSummaryTests.cs} (86%) rename Models/{MethodLevelChangesSummary.cs => AssemblySemanticChangesSummary.cs} (80%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76082eec..97f94c73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), #### Added -- Added member-level change detection for `ILMismatch` assemblies using `System.Reflection.Metadata`. For each modified .NET assembly, the report now shows type/method/property/field additions, removals, and method body changes. This new **Assembly Semantic Changes** section appears between **Summary** and **IL Cache Stats** in the Markdown report, and as an expandable inline row above the IL diff in the HTML report. Controlled by the new `ShouldIncludeMethodLevelChangesInReport` config setting (default: `true`). Added [`AssemblyMethodAnalyzer`](Services/AssemblyMethodAnalyzer.cs), [`MethodLevelChangesSummary`](Models/MethodLevelChangesSummary.cs), and corresponding tests. +- Added member-level change detection for `ILMismatch` assemblies using `System.Reflection.Metadata`. For each modified .NET assembly, the report now shows type/method/property/field additions, removals, and method body changes. This new **Assembly Semantic Changes** section appears between **Summary** and **IL Cache Stats** in the Markdown report, and as an expandable inline row above the IL diff in the HTML report. Controlled by the new `ShouldIncludeAssemblySemanticChangesInReport` config setting (default: `true`). Added [`AssemblyMethodAnalyzer`](Services/AssemblyMethodAnalyzer.cs), [`AssemblySemanticChangesSummary`](Models/AssemblySemanticChangesSummary.cs), and corresponding tests. - Restructured the Assembly Semantic Changes table from 9 columns to 10 columns for clarity. Split the former `ReturnType (Type paramName)` column into separate `ReturnType` and `Parameters` columns. Moved `Kind` column before `Access` and `Modifiers` for better readability. Added `Constructor` and `StaticConstructor` as new Kind values (previously `.ctor`/`.cctor` were shown as `Method`). Constructors display the C# class name instead of `.ctor`. The `Type` column shows the declared type for Field/Property entries only. Empty Access/Modifiers cells no longer render as empty backticks. Changed `Method count` label to `Member count`. Renamed section from `Method-Level Changes` to `Assembly Semantic Changes`. Added record type and field variable samples to [`doc/samples/diff_report.md`](doc/samples/diff_report.md). Added bilingual **Assembly Semantic Changes** section to [README.md](README.md). @@ -19,7 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - Removed parentheses from the `Parameters` column values. Since Parameters is now an independent column, wrapping values in `(…)` is redundant. Values now display as `string name, int count = 0` instead of `(string name, int count = 0)`. Empty parameter lists display as blank instead of `()`. -- Added `Body` column (10th column, rightmost) to the Assembly Semantic Changes table. Displays `Changed` when a method body or field initializer has been modified at the IL level; otherwise empty. Entries with body changes use `Modified` in the Change column. Replaced the `Member count: N (Old) vs N (New)` summary line with `Added: N, Removed: N, Modified: N` counts computed from entries. Removed `OldMethodCount`/`NewMethodCount` properties from `MethodLevelChangesSummary` in favour of computed `AddedCount`, `RemovedCount`, and `ModifiedCount` properties. +- Added `Body` column (10th column, rightmost) to the Assembly Semantic Changes table. Displays `Changed` when a method body or field initializer has been modified at the IL level; otherwise empty. Entries with body changes use `Modified` in the Change column. Replaced the `Member count: N (Old) vs N (New)` summary line with `Added: N, Removed: N, Modified: N` counts computed from entries. Removed `OldMethodCount`/`NewMethodCount` properties from `AssemblySemanticChangesSummary` in favour of computed `AddedCount`, `RemovedCount`, and `ModifiedCount` properties. ### [1.4.1] - 2026-03-20 @@ -379,7 +379,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), #### 追加 -- `System.Reflection.Metadata` を使用した `ILMismatch` アセンブリのメンバーレベル変更検出を追加。変更のあった各 .NET アセンブリについて、型・メソッド・プロパティ・フィールドの増減およびメソッドボディの変更をレポートに出力します。Markdown レポートでは **Summary** と **IL Cache Stats** の間に **Assembly Semantic Changes** セクションとして表示され、HTML レポートでは IL diff の上に展開可能なインライン行として表示されます。新しい設定項目 `ShouldIncludeMethodLevelChangesInReport`(既定: `true`)で制御可能。[`AssemblyMethodAnalyzer`](Services/AssemblyMethodAnalyzer.cs)、[`MethodLevelChangesSummary`](Models/MethodLevelChangesSummary.cs)、および対応するテストを追加。 +- `System.Reflection.Metadata` を使用した `ILMismatch` アセンブリのメンバーレベル変更検出を追加。変更のあった各 .NET アセンブリについて、型・メソッド・プロパティ・フィールドの増減およびメソッドボディの変更をレポートに出力します。Markdown レポートでは **Summary** と **IL Cache Stats** の間に **Assembly Semantic Changes** セクションとして表示され、HTML レポートでは IL diff の上に展開可能なインライン行として表示されます。新しい設定項目 `ShouldIncludeAssemblySemanticChangesInReport`(既定: `true`)で制御可能。[`AssemblyMethodAnalyzer`](Services/AssemblyMethodAnalyzer.cs)、[`AssemblySemanticChangesSummary`](Models/AssemblySemanticChangesSummary.cs)、および対応するテストを追加。 - Assembly Semantic Changes テーブルを 9 列から 10 列に再構成し明確化。旧 `ReturnType (Type paramName)` 列を `ReturnType` 列と `Parameters` 列に分離。`Kind` 列を `Access`・`Modifiers` の前に移動。Kind 値に `Constructor` と `StaticConstructor` を追加(従来 `.ctor`/`.cctor` は `Method` として表示)。コンストラクタは `.ctor` ではなく C# のクラス名で表示。`Type` 列は Field/Property の宣言型のみを表示。空の Access/Modifiers セルは空バッククォートではなく空欄に。`Method count` ラベルを `Member count` に変更。セクション名を `Method-Level Changes` から `Assembly Semantic Changes` に改名。[`doc/samples/diff_report.md`](doc/samples/diff_report.md) に record 型およびフィールド変数のサンプルを追加。[README.md](README.md) にバイリンガルの **アセンブリ セマンティック変更** セクションを追加。 @@ -387,7 +387,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - `Parameters` 列の値から括弧を削除。Parameters が独立列となったため `(…)` は冗長。値は `(string name, int count = 0)` ではなく `string name, int count = 0` で表示。引数なしは `()` ではなく空欄。 -- Assembly Semantic Changes テーブルに `Body` 列(10 列目、最右端)を追加。メソッドボディまたはフィールド初期化子が IL レベルで変更された場合に `Changed` を表示、それ以外は空欄。ボディ変更があるエントリの Change 列は `Modified`。集計行を `Member count: N (Old) vs N (New)` から `Added: N, Removed: N, Modified: N`(エントリから算出)に変更。`MethodLevelChangesSummary` の `OldMethodCount`/`NewMethodCount` プロパティを削除し、算出プロパティ `AddedCount`、`RemovedCount`、`ModifiedCount` に置き換え。 +- Assembly Semantic Changes テーブルに `Body` 列(10 列目、最右端)を追加。メソッドボディまたはフィールド初期化子が IL レベルで変更された場合に `Changed` を表示、それ以外は空欄。ボディ変更があるエントリの Change 列は `Modified`。集計行を `Member count: N (Old) vs N (New)` から `Added: N, Removed: N, Modified: N`(エントリから算出)に変更。`AssemblySemanticChangesSummary` の `OldMethodCount`/`NewMethodCount` プロパティを削除し、算出プロパティ `AddedCount`、`RemovedCount`、`ModifiedCount` に置き換え。 ### [1.4.1] - 2026-03-20 diff --git a/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs b/FolderDiffIL4DotNet.Tests/Models/AssemblySemanticChangesSummaryTests.cs similarity index 86% rename from FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs rename to FolderDiffIL4DotNet.Tests/Models/AssemblySemanticChangesSummaryTests.cs index eb96738e..91d481ed 100644 --- a/FolderDiffIL4DotNet.Tests/Models/MethodLevelChangesSummaryTests.cs +++ b/FolderDiffIL4DotNet.Tests/Models/AssemblySemanticChangesSummaryTests.cs @@ -4,19 +4,19 @@ namespace FolderDiffIL4DotNet.Tests.Models { - public sealed class MethodLevelChangesSummaryTests + public sealed class AssemblySemanticChangesSummaryTests { [Fact] public void HasChanges_DefaultInstance_ReturnsFalse() { - var summary = new MethodLevelChangesSummary(); + var summary = new AssemblySemanticChangesSummary(); Assert.False(summary.HasChanges); } [Fact] public void HasChanges_WithEntries_ReturnsTrue() { - var summary = new MethodLevelChangesSummary + var summary = new AssemblySemanticChangesSummary { Entries = new List { @@ -29,7 +29,7 @@ public void HasChanges_WithEntries_ReturnsTrue() [Fact] public void HasChanges_EmptyEntries_ReturnsFalse() { - var summary = new MethodLevelChangesSummary + var summary = new AssemblySemanticChangesSummary { Entries = new List(), }; diff --git a/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs b/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs index d37963c5..c12c78a5 100644 --- a/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs @@ -719,14 +719,14 @@ public void GenerateDiffReportHtml_LazyRender_JsSetupFunctionPresent() // ── Assembly Semantic Changes / アセンブリ意味変更 ───────────────────── [Fact] - public void GenerateDiffReportHtml_MethodLevelChanges_ShowsInlineAboveILDiff() + public void GenerateDiffReportHtml_AssemblySemanticChanges_ShowsInlineAboveILDiff() { - var (oldDir, newDir, reportDir) = MakeDirs("method-changes"); + var (oldDir, newDir, reportDir) = MakeDirs("semantic-changes"); _resultLists.AddModifiedFileRelativePath("lib.dll"); _resultLists.RecordDiffDetail("lib.dll", FileDiffResultLists.DiffDetailResult.ILMismatch, "dotnet-ildasm (version: 0.12.0)"); - _resultLists.FileRelativePathToMethodLevelChanges["lib.dll"] = new MethodLevelChangesSummary + _resultLists.FileRelativePathToAssemblySemanticChanges["lib.dll"] = new AssemblySemanticChangesSummary { Entries = new List { @@ -738,26 +738,26 @@ public void GenerateDiffReportHtml_MethodLevelChanges_ShowsInlineAboveILDiff() }; var config = CreateConfig(enableInlineDiff: true); - config.ShouldIncludeMethodLevelChangesInReport = true; + config.ShouldIncludeAssemblySemanticChangesInReport = true; _service.GenerateDiffReportHtml(oldDir, newDir, reportDir, appVersion: "1.0", elapsedTimeString: null, computerName: "test-host", config); var html = File.ReadAllText(Path.Combine(reportDir, HtmlReportGenerateService.DIFF_REPORT_HTML_FILE_NAME)); Assert.Contains("Show assembly semantic changes", html); - Assert.Contains("methods_mod_0", html); - Assert.Contains("method-changes-table", html); + Assert.Contains("semantic_mod_0", html); + Assert.Contains("semantic-changes-table", html); } [Fact] - public void GenerateDiffReportHtml_MethodLevelChanges_NotShownWhenDisabled() + public void GenerateDiffReportHtml_AssemblySemanticChanges_NotShownWhenDisabled() { - var (oldDir, newDir, reportDir) = MakeDirs("method-changes-off"); + var (oldDir, newDir, reportDir) = MakeDirs("semantic-changes-off"); _resultLists.AddModifiedFileRelativePath("lib.dll"); _resultLists.RecordDiffDetail("lib.dll", FileDiffResultLists.DiffDetailResult.ILMismatch, "dotnet-ildasm (version: 0.12.0)"); - _resultLists.FileRelativePathToMethodLevelChanges["lib.dll"] = new MethodLevelChangesSummary + _resultLists.FileRelativePathToAssemblySemanticChanges["lib.dll"] = new AssemblySemanticChangesSummary { Entries = new List { @@ -766,7 +766,7 @@ public void GenerateDiffReportHtml_MethodLevelChanges_NotShownWhenDisabled() }; var config = CreateConfig(enableInlineDiff: true); - config.ShouldIncludeMethodLevelChangesInReport = false; + config.ShouldIncludeAssemblySemanticChangesInReport = false; _service.GenerateDiffReportHtml(oldDir, newDir, reportDir, appVersion: "1.0", elapsedTimeString: null, computerName: "test-host", config); @@ -776,14 +776,14 @@ public void GenerateDiffReportHtml_MethodLevelChanges_NotShownWhenDisabled() } [Fact] - public void GenerateDiffReportHtml_MethodLevelChanges_LazyRender_EncodesAsBase64() + public void GenerateDiffReportHtml_AssemblySemanticChanges_LazyRender_EncodesAsBase64() { - var (oldDir, newDir, reportDir) = MakeDirs("method-changes-lazy"); + var (oldDir, newDir, reportDir) = MakeDirs("semantic-changes-lazy"); _resultLists.AddModifiedFileRelativePath("lib.dll"); _resultLists.RecordDiffDetail("lib.dll", FileDiffResultLists.DiffDetailResult.ILMismatch, "dotnet-ildasm (version: 0.12.0)"); - _resultLists.FileRelativePathToMethodLevelChanges["lib.dll"] = new MethodLevelChangesSummary + _resultLists.FileRelativePathToAssemblySemanticChanges["lib.dll"] = new AssemblySemanticChangesSummary { Entries = new List { @@ -792,17 +792,17 @@ public void GenerateDiffReportHtml_MethodLevelChanges_LazyRender_EncodesAsBase64 }; var config = CreateConfig(enableInlineDiff: true, lazyRender: true); - config.ShouldIncludeMethodLevelChangesInReport = true; + config.ShouldIncludeAssemblySemanticChangesInReport = true; _service.GenerateDiffReportHtml(oldDir, newDir, reportDir, appVersion: "1.0", elapsedTimeString: null, computerName: "test-host", config); var html = File.ReadAllText(Path.Combine(reportDir, HtmlReportGenerateService.DIFF_REPORT_HTML_FILE_NAME)); - // Should contain a data-diff-html attribute for the method changes row - Assert.Contains("methods_mod_0", html); + // Should contain a data-diff-html attribute for the semantic changes row + Assert.Contains("semantic_mod_0", html); Assert.Contains("Show assembly semantic changes", html); // Content should NOT be inline (lazy rendered) — table markup is base64-encoded - Assert.DoesNotContain("method-changes-table", html); + Assert.DoesNotContain("semantic-changes-table", html); Assert.Contains("data-diff-html", html); } diff --git a/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs b/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs index a133056b..cd422427 100644 --- a/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs @@ -759,9 +759,9 @@ public void GenerateDiffReport_WithIgnoredFilesNoneLocation_DoesNotBreakReport() [Fact] public void GenerateDiffReport_AssemblySemanticChanges_IncludedBetweenSummaryAndILCacheStats() { - var oldDir = Path.Combine(_rootDir, "old-mlc"); - var newDir = Path.Combine(_rootDir, "new-mlc"); - var reportDir = Path.Combine(_rootDir, "report-mlc"); + var oldDir = Path.Combine(_rootDir, "old-asc"); + var newDir = Path.Combine(_rootDir, "new-asc"); + var reportDir = Path.Combine(_rootDir, "report-asc"); Directory.CreateDirectory(oldDir); Directory.CreateDirectory(newDir); Directory.CreateDirectory(reportDir); @@ -769,7 +769,7 @@ public void GenerateDiffReport_AssemblySemanticChanges_IncludedBetweenSummaryAnd _resultLists.AddModifiedFileRelativePath("src/App.dll"); _resultLists.RecordDiffDetail("src/App.dll", FileDiffResultLists.DiffDetailResult.ILMismatch, "dotnet-ildasm (version: 0.12.0)"); - var summary = new MethodLevelChangesSummary + var summary = new AssemblySemanticChangesSummary { Entries = new List { @@ -782,7 +782,7 @@ public void GenerateDiffReport_AssemblySemanticChanges_IncludedBetweenSummaryAnd new("Added", "MyApp.UserService", "private", "readonly", "Field", "_cache", "object", "", "", ""), }, }; - _resultLists.FileRelativePathToMethodLevelChanges["src/App.dll"] = summary; + _resultLists.FileRelativePathToAssemblySemanticChanges["src/App.dll"] = summary; // Also add IL Cache Stats to verify ordering var config = CreateConfig(); @@ -818,16 +818,16 @@ public void GenerateDiffReport_AssemblySemanticChanges_IncludedBetweenSummaryAnd } [Fact] - public void GenerateDiffReport_MethodLevelChanges_NotIncludedWhenDisabled() + public void GenerateDiffReport_AssemblySemanticChanges_NotIncludedWhenDisabled() { - var oldDir = Path.Combine(_rootDir, "old-mlc-off"); - var newDir = Path.Combine(_rootDir, "new-mlc-off"); - var reportDir = Path.Combine(_rootDir, "report-mlc-off"); + var oldDir = Path.Combine(_rootDir, "old-asc-off"); + var newDir = Path.Combine(_rootDir, "new-asc-off"); + var reportDir = Path.Combine(_rootDir, "report-asc-off"); Directory.CreateDirectory(oldDir); Directory.CreateDirectory(newDir); Directory.CreateDirectory(reportDir); - _resultLists.FileRelativePathToMethodLevelChanges["src/App.dll"] = new MethodLevelChangesSummary + _resultLists.FileRelativePathToAssemblySemanticChanges["src/App.dll"] = new AssemblySemanticChangesSummary { Entries = new List { @@ -836,7 +836,7 @@ public void GenerateDiffReport_MethodLevelChanges_NotIncludedWhenDisabled() }; var config = CreateConfig(); - config.ShouldIncludeMethodLevelChangesInReport = false; + config.ShouldIncludeAssemblySemanticChangesInReport = false; _service.GenerateDiffReport( oldDir, newDir, reportDir, appVersion: "test", elapsedTimeString: null, computerName: "test-host", @@ -847,18 +847,18 @@ public void GenerateDiffReport_MethodLevelChanges_NotIncludedWhenDisabled() } [Fact] - public void GenerateDiffReport_MethodLevelChanges_NotIncludedWhenNoChanges() + public void GenerateDiffReport_AssemblySemanticChanges_NotIncludedWhenNoChanges() { - var oldDir = Path.Combine(_rootDir, "old-mlc-empty"); - var newDir = Path.Combine(_rootDir, "new-mlc-empty"); - var reportDir = Path.Combine(_rootDir, "report-mlc-empty"); + var oldDir = Path.Combine(_rootDir, "old-asc-empty"); + var newDir = Path.Combine(_rootDir, "new-asc-empty"); + var reportDir = Path.Combine(_rootDir, "report-asc-empty"); Directory.CreateDirectory(oldDir); Directory.CreateDirectory(newDir); Directory.CreateDirectory(reportDir); // No method-level changes recorded var config = CreateConfig(); - config.ShouldIncludeMethodLevelChangesInReport = true; + config.ShouldIncludeAssemblySemanticChangesInReport = true; _service.GenerateDiffReport( oldDir, newDir, reportDir, appVersion: "test", elapsedTimeString: null, computerName: "test-host", diff --git a/Models/MethodLevelChangesSummary.cs b/Models/AssemblySemanticChangesSummary.cs similarity index 80% rename from Models/MethodLevelChangesSummary.cs rename to Models/AssemblySemanticChangesSummary.cs index 189b902b..9d2fd149 100644 --- a/Models/MethodLevelChangesSummary.cs +++ b/Models/AssemblySemanticChangesSummary.cs @@ -3,14 +3,14 @@ namespace FolderDiffIL4DotNet.Models { /// - /// Summarises member-level changes detected between two builds of a .NET assembly. + /// Summarises assembly semantic changes detected between two builds of a .NET assembly. /// Each change is represented as a structured . - /// .NET アセンブリの新旧ビルド間で検出されたメンバーレベルの変更要約を保持します。 + /// .NET アセンブリの新旧ビルド間で検出されたセマンティック変更要約を保持します。 /// 各変更は構造化された として表現されます。 /// - public sealed class MethodLevelChangesSummary + public sealed class AssemblySemanticChangesSummary { - /// All detected member-level changes. / 検出されたすべてのメンバーレベル変更。 + /// All detected assembly semantic changes. / 検出されたすべてのセマンティック変更。 public IReadOnlyList Entries { get; init; } = []; /// Whether any changes were detected. / 何らかの変更が検出されたかどうか。 diff --git a/Models/ConfigSettings.cs b/Models/ConfigSettings.cs index fc5fa4f2..56e0ddc1 100644 --- a/Models/ConfigSettings.cs +++ b/Models/ConfigSettings.cs @@ -105,7 +105,7 @@ public List TextFileExtensions /// (型・メソッド・プロパティ・フィールドの増減およびメソッドボディの変更)をレポートに出力するかどうか。 /// true の場合、Summary セクションと IL Cache Stats セクションの間に Assembly Semantic Changes セクションを追加します。 /// - public bool ShouldIncludeMethodLevelChangesInReport { get; set; } = true; + public bool ShouldIncludeAssemblySemanticChangesInReport { get; set; } = true; /// /// Whether to include IL cache statistics (hits, misses, hit rate, etc.) in the diff report. diff --git a/Models/FileDiffResultLists.cs b/Models/FileDiffResultLists.cs index 7f5bb8c7..6d729563 100644 --- a/Models/FileDiffResultLists.cs +++ b/Models/FileDiffResultLists.cs @@ -108,10 +108,10 @@ public sealed record DiffSummaryStatistics( public ConcurrentDictionary NewFileTimestampOlderThanOldWarnings { get; } = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); /// - /// Method-level change summaries for ILMismatch files, keyed by file relative path. - /// ILMismatch ファイルに対するメソッドレベル変更要約。キーはファイルの相対パス。 + /// Assembly semantic change summaries for ILMismatch files, keyed by file relative path. + /// ILMismatch ファイルに対するアセンブリセマンティック変更要約。キーはファイルの相対パス。 /// - public ConcurrentDictionary FileRelativePathToMethodLevelChanges { get; } = new ConcurrentDictionary(StringComparer.Ordinal); + public ConcurrentDictionary FileRelativePathToAssemblySemanticChanges { get; } = new ConcurrentDictionary(StringComparer.Ordinal); public bool HasAnyNewFileTimestampOlderThanOldWarning => !NewFileTimestampOlderThanOldWarnings.IsEmpty; @@ -174,7 +174,7 @@ public void ResetAll() DisassemblerToolVersions.Clear(); DisassemblerToolVersionsFromCache.Clear(); NewFileTimestampOlderThanOldWarnings.Clear(); - FileRelativePathToMethodLevelChanges.Clear(); + FileRelativePathToAssemblySemanticChanges.Clear(); } /// diff --git a/README.md b/README.md index a04f555d..3e29bf3c 100644 --- a/README.md +++ b/README.md @@ -216,7 +216,7 @@ When an assembly is classified as `ILMismatch`, the tool performs an additional | Parameters | Parameter list for Method/Constructor (empty for Field/Property/Class/Record) | `string name, int count = 0` | | Body | `Changed` when method body or field initializer IL has changed; otherwise empty | `Changed` | -Controlled by [`ShouldIncludeMethodLevelChangesInReport`](#config-en-shouldincludemethodlevelchangesinreport) (default: `true`). +Controlled by [`ShouldIncludeAssemblySemanticChangesInReport`](#config-en-shouldincludeassemblysemanticchangesinreport) (default: `true`). The bullet list below the table shows counts per change kind (`Added`, `Removed`, `Modified`). @@ -277,8 +277,8 @@ Override only the settings you want to change. For example: true Includes Ignored Files section before Unchanged. - - ShouldIncludeMethodLevelChangesInReport + + ShouldIncludeAssemblySemanticChangesInReport true When true, includes an Assembly Semantic Changes section for ILMismatch assemblies between Summary and IL Cache Stats. Uses System.Reflection.Metadata to detect type/method/property/field additions, removals, and method body changes. In the HTML report, this appears as an expandable inline row above the IL diff. @@ -698,7 +698,7 @@ flowchart TD | Parameters | Method/Constructor のパラメータ一覧(Field/Property/Class/Record の場合は空) | `string name, int count = 0` | | Body | メソッドボディまたはフィールド初期化子の IL が変更された場合 `Changed`、それ以外は空 | `Changed` | -[`ShouldIncludeMethodLevelChangesInReport`](#config-ja-shouldincludemethodlevelchangesinreport)(既定値: `true`)で制御します。 +[`ShouldIncludeAssemblySemanticChangesInReport`](#config-ja-shouldincludeassemblysemanticchangesinreport)(既定値: `true`)で制御します。 テーブル下の箇条書きに変更種別ごとのカウント(`Added`、`Removed`、`Modified`)が表示されます。 @@ -759,8 +759,8 @@ flowchart TD true レポートに Ignored Files セクションを出力するか。 - - ShouldIncludeMethodLevelChangesInReport + + ShouldIncludeAssemblySemanticChangesInReport true true の場合、ILMismatch と判定された .NET アセンブリについて、SummaryIL Cache Stats の間に Assembly Semantic Changes セクションを出力します。System.Reflection.Metadata を使用して型・メソッド・プロパティ・フィールドの増減およびメソッドボディの変更を検出します。HTML レポートでは IL diff の上に展開可能なインライン行として表示されます。 diff --git a/Services/AssemblyMethodAnalyzer.cs b/Services/AssemblyMethodAnalyzer.cs index ac9a8d11..697e389f 100644 --- a/Services/AssemblyMethodAnalyzer.cs +++ b/Services/AssemblyMethodAnalyzer.cs @@ -21,12 +21,12 @@ namespace FolderDiffIL4DotNet.Services internal static class AssemblyMethodAnalyzer { /// - /// Analyses two assembly files and returns a summary of member-level changes. + /// Analyses two assembly files and returns a summary of assembly semantic changes. /// Returns if analysis fails (best-effort). - /// 2 つのアセンブリファイルを解析し、メンバーレベルの変更要約を返します。 + /// 2 つのアセンブリファイルを解析し、アセンブリセマンティック変更要約を返します。 /// 解析に失敗した場合は を返します(ベストエフォート)。 /// - public static MethodLevelChangesSummary? Analyze(string oldAssemblyPath, string newAssemblyPath) + public static AssemblySemanticChangesSummary? Analyze(string oldAssemblyPath, string newAssemblyPath) { try { @@ -94,7 +94,7 @@ internal static class AssemblyMethodAnalyzer entries.Add(new MemberChangeEntry("Removed", f.TypeName, f.Access, f.Modifiers, "Field", f.FieldName, StripColonPrefix(f.Details), "", "", "")); } - return new MethodLevelChangesSummary + return new AssemblySemanticChangesSummary { Entries = entries, }; diff --git a/Services/FileDiffService.cs b/Services/FileDiffService.cs index 50f7f84e..e74590c3 100644 --- a/Services/FileDiffService.cs +++ b/Services/FileDiffService.cs @@ -126,10 +126,10 @@ public async Task FilesAreEqualAsync(string fileRelativePath, int maxParal areDotNetAssembliesEqual ? FileDiffResultLists.DiffDetailResult.ILMatch : FileDiffResultLists.DiffDetailResult.ILMismatch, disassemblerLabel); - // Best-effort method-level analysis for ILMismatch assemblies - if (!areDotNetAssembliesEqual && _config.ShouldIncludeMethodLevelChangesInReport) + // Best-effort assembly semantic analysis for ILMismatch assemblies + if (!areDotNetAssembliesEqual && _config.ShouldIncludeAssemblySemanticChangesInReport) { - TryAnalyzeMethodLevelChanges(fileRelativePath, file1AbsolutePath, file2AbsolutePath); + TryAnalyzeAssemblySemanticChanges(fileRelativePath, file1AbsolutePath, file2AbsolutePath); } return areDotNetAssembliesEqual; @@ -242,19 +242,19 @@ public async Task FilesAreEqualAsync(string fileRelativePath, int maxParal } /// - /// Best-effort method-level analysis using System.Reflection.Metadata. + /// Best-effort assembly semantic analysis using System.Reflection.Metadata. /// Failures are logged but do not affect the comparison result. - /// System.Reflection.Metadata を使用したベストエフォートのメソッドレベル解析。 + /// System.Reflection.Metadata を使用したベストエフォートのアセンブリセマンティック解析。 /// 失敗してもファイル比較結果には影響しません。 /// - private void TryAnalyzeMethodLevelChanges(string fileRelativePath, string oldPath, string newPath) + private void TryAnalyzeAssemblySemanticChanges(string fileRelativePath, string oldPath, string newPath) { try { var summary = AssemblyMethodAnalyzer.Analyze(oldPath, newPath); if (summary?.HasChanges == true) { - _fileDiffResultLists.FileRelativePathToMethodLevelChanges[fileRelativePath] = summary; + _fileDiffResultLists.FileRelativePathToAssemblySemanticChanges[fileRelativePath] = summary; } } #pragma warning disable CA1031 // ベストエフォート解析のため全例外をキャッチ / Catch-all for best-effort analysis diff --git a/Services/HtmlReport/HtmlReportGenerateService.Css.cs b/Services/HtmlReport/HtmlReportGenerateService.Css.cs index 13c88304..78c78c90 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Css.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Css.cs @@ -65,7 +65,7 @@ private static string GetCss() /* ── Data tables ─────────────────────────────────────────────────────── */ .table-scroll { overflow-x: auto; margin-bottom: 1.2rem; } table { border-collapse: collapse; width: 100%; margin-bottom: 1.2rem; } - table:not(.stat-table):not(.diff-table):not(.method-changes-table) { table-layout: fixed; width: 1px; margin-bottom: 0; } + table:not(.stat-table):not(.diff-table):not(.semantic-changes-table) { table-layout: fixed; width: 1px; margin-bottom: 0; } th { padding: 4px 6px; font-size: 12px; white-space: nowrap; overflow: hidden; text-align: left; border: 1px solid #bbb; color: #000; } th.th-resizable { position: relative; } @@ -133,12 +133,12 @@ private static string GetCss() td.diff-trunc-td { color: #735c0f; padding: 2px 8px; font-style: italic; } p.diff-skipped { color: #735c0f; font-size: 12px; padding: 4px 8px; background: #fffbdd; margin: 0; } - /* ── Method-level changes ──────────────────────────────────────────── */ - .method-changes { padding: 6px 12px; font-size: 12px; } - .method-changes p { margin: 4px 0 2px; } - table.method-changes-table { width: auto; border-collapse: collapse; margin: 4px 0; font-size: 12px; } - table.method-changes-table th { padding: 3px 8px; border: 1px solid #ccc; background: #f6f8fa; font-size: 11px; text-align: left; white-space: nowrap; } - table.method-changes-table td { padding: 2px 8px; border: 1px solid #e0e0e0; font-size: 12px; white-space: nowrap; }"; + /* ── Assembly semantic changes ─────────────────────────────────────── */ + .semantic-changes { padding: 6px 12px; font-size: 12px; } + .semantic-changes p { margin: 4px 0 2px; } + table.semantic-changes-table { width: auto; border-collapse: collapse; margin: 4px 0; font-size: 12px; } + table.semantic-changes-table th { padding: 3px 8px; border: 1px solid #ccc; background: #f6f8fa; font-size: 11px; text-align: left; white-space: nowrap; } + table.semantic-changes-table td { padding: 2px 8px; border: 1px solid #e0e0e0; font-size: 12px; white-space: nowrap; }"; } } } diff --git a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs index 0e9eae5d..7d6e15bd 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs @@ -209,11 +209,11 @@ private void AppendModifiedSection( AppendFileRow(sb, "mod", idx, path, ts, col6, asm ?? ""); // Method-level changes row (above IL diff) - if (config.ShouldIncludeMethodLevelChangesInReport && + if (config.ShouldIncludeAssemblySemanticChangesInReport && diffDetail == FileDiffResultLists.DiffDetailResult.ILMismatch && - _fileDiffResultLists.FileRelativePathToMethodLevelChanges.TryGetValue(path, out var methodChanges)) + _fileDiffResultLists.FileRelativePathToAssemblySemanticChanges.TryGetValue(path, out var semanticChanges)) { - AppendMethodLevelChangesRow(sb, idx, path, methodChanges, config); + AppendAssemblySemanticChangesRow(sb, idx, path, semanticChanges, config); } if (config.EnableInlineDiff && @@ -345,11 +345,11 @@ private void AppendInlineDiffRow( sb.AppendLine(""); } - private void AppendMethodLevelChangesRow( + private void AppendAssemblySemanticChangesRow( StringBuilder sb, int idx, string assemblyPath, - MethodLevelChangesSummary summary, + AssemblySemanticChangesSummary summary, ConfigSettings config, string sectionPrefix = "mod") { @@ -357,11 +357,11 @@ private void AppendMethodLevelChangesRow( int totalChanges = summary.Entries.Count; var contentBuilder = new StringBuilder(); - contentBuilder.AppendLine("
"); + contentBuilder.AppendLine("
"); if (summary.Entries.Count > 0) { - contentBuilder.AppendLine(""); + contentBuilder.AppendLine("
"); contentBuilder.AppendLine(""); contentBuilder.AppendLine(""); foreach (var e in summary.Entries) @@ -385,7 +385,7 @@ private void AppendMethodLevelChangesRow( contentBuilder.AppendLine(""); contentBuilder.AppendLine(""); - string detailsId = $"methods_{sectionPrefix}_{idx}"; + string detailsId = $"semantic_{sectionPrefix}_{idx}"; string summaryText = totalChanges > 0 ? $"#{recordNo} Show assembly semantic changes ({totalChanges} change{(totalChanges == 1 ? "" : "s")})" : $"#{recordNo} Show assembly semantic changes (other changes only)"; diff --git a/Services/ReportGenerateService.SectionWriters.cs b/Services/ReportGenerateService.SectionWriters.cs index 4efaa557..27fe7af0 100644 --- a/Services/ReportGenerateService.SectionWriters.cs +++ b/Services/ReportGenerateService.SectionWriters.cs @@ -192,12 +192,12 @@ public void Write(StreamWriter writer, ReportWriteContext ctx) } /// Writes the Assembly Semantic Changes section for ILMismatch assemblies. / ILMismatch アセンブリのアセンブリ意味変更セクションを書き込みます。 - private sealed class MethodLevelChangesSectionWriter : IReportSectionWriter + private sealed class AssemblySemanticChangesSectionWriter : IReportSectionWriter { public void Write(StreamWriter writer, ReportWriteContext ctx) { - if (!ctx.Config.ShouldIncludeMethodLevelChangesInReport) return; - var changes = ctx.FileDiffResultLists.FileRelativePathToMethodLevelChanges; + if (!ctx.Config.ShouldIncludeAssemblySemanticChangesInReport) return; + var changes = ctx.FileDiffResultLists.FileRelativePathToAssemblySemanticChanges; if (changes.IsEmpty) return; writer.WriteLine(REPORT_SECTION_ASSEMBLY_SEMANTIC_CHANGES); diff --git a/Services/ReportGenerateService.cs b/Services/ReportGenerateService.cs index 730d75ec..d9963eaa 100644 --- a/Services/ReportGenerateService.cs +++ b/Services/ReportGenerateService.cs @@ -174,7 +174,7 @@ private void WriteDiffReport( new RemovedFilesSectionWriter(), new ModifiedFilesSectionWriter(), new SummarySectionWriter(), - new MethodLevelChangesSectionWriter(), + new AssemblySemanticChangesSectionWriter(), new ILCacheStatsSectionWriter(), new WarningsSectionWriter(), }; diff --git a/doc/samples/diff_report.html b/doc/samples/diff_report.html index e59afc80..52cafa99 100644 --- a/doc/samples/diff_report.html +++ b/doc/samples/diff_report.html @@ -63,7 +63,7 @@ /* ── Data tables ─────────────────────────────────────────────────────── */ .table-scroll { overflow-x: auto; margin-bottom: 1.2rem; } table { border-collapse: collapse; width: 100%; margin-bottom: 1.2rem; } - table:not(.stat-table):not(.diff-table):not(.method-changes-table) { table-layout: fixed; width: 1px; margin-bottom: 0; } + table:not(.stat-table):not(.diff-table):not(.semantic-changes-table) { table-layout: fixed; width: 1px; margin-bottom: 0; } th { padding: 4px 6px; font-size: 12px; white-space: nowrap; overflow: hidden; text-align: left; border: 1px solid #bbb; color: #000; } th.th-resizable { position: relative; } @@ -132,11 +132,11 @@ p.diff-skipped { color: #735c0f; font-size: 12px; padding: 4px 8px; background: #fffbdd; margin: 0; } /* ── Method-level changes ──────────────────────────────────────────── */ - .method-changes { padding: 6px 12px; font-size: 12px; } - .method-changes p { margin: 4px 0 2px; } - table.method-changes-table { width: auto; border-collapse: collapse; margin: 4px 0; font-size: 12px; } - table.method-changes-table th { padding: 3px 8px; border: 1px solid #ccc; background: #f6f8fa; font-size: 11px; text-align: left; white-space: nowrap; } - table.method-changes-table td { padding: 2px 8px; border: 1px solid #e0e0e0; font-size: 12px; white-space: nowrap; } + .semantic-changes { padding: 6px 12px; font-size: 12px; } + .semantic-changes p { margin: 4px 0 2px; } + table.semantic-changes-table { width: auto; border-collapse: collapse; margin: 4px 0; font-size: 12px; } + table.semantic-changes-table th { padding: 3px 8px; border: 1px solid #ccc; background: #f6f8fa; font-size: 11px; text-align: left; white-space: nowrap; } + table.semantic-changes-table td { padding: 2px 8px; border: 1px solid #e0e0e0; font-size: 12px; white-space: nowrap; } @@ -437,7 +437,7 @@

[ * ] Modified Files (9)

@@ -478,7 +478,7 @@

[ * ] Modified Files (9)

@@ -545,7 +545,7 @@

[ * ] Modified Files (9)

@@ -759,7 +759,7 @@

[ ! ] Modified Files — Timestamps Regressed (2) + px('--col-reason-w', 10) + px('--col-notes-w', 10) + px('--col-path-w', 22) + px('--col-diff-w', 9) + px('--col-disasm-w', 28); - document.querySelectorAll('table:not(.stat-table):not(.diff-table):not(.method-changes-table)').forEach(function(t) { + document.querySelectorAll('table:not(.stat-table):not(.diff-table):not(.semantic-changes-table)').forEach(function(t) { t.style.width = w + 'px'; }); } From 84713ac74cb2ce8de244a7fcd2f754d671b37e50 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 19:10:17 +0000 Subject: [PATCH 16/36] Replace summary bullet list with Class|Change|Count table; merge consecutive Class cells Replace the per-assembly bullet list counts (Added/Removed/Modified) with a summary count table grouped by Class and Change. In both the main semantic changes table and the summary count table, suppress the Class column for consecutive rows with the same class name. https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- .../Services/ReportGenerateServiceTests.cs | 15 ++-- README.md | 4 +- .../HtmlReportGenerateService.Sections.cs | 33 +++++-- .../ReportGenerateService.SectionWriters.cs | 30 ++++++- doc/samples/diff_report.md | 88 +++++++++++-------- 5 files changed, 114 insertions(+), 56 deletions(-) diff --git a/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs b/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs index cd422427..344061da 100644 --- a/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs @@ -800,14 +800,17 @@ public void GenerateDiffReport_AssemblySemanticChanges_IncludedBetweenSummaryAnd Assert.Contains("## Assembly Semantic Changes", reportText); Assert.Contains("### src/App.dll", reportText); Assert.Contains("| Class | Change | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | Body |", reportText); + // First row shows class name; subsequent rows for same class are empty Assert.Contains("| MyApp.NewService | `Added` | `Class` | `public` | | | | | | |", reportText); Assert.Contains("| MyApp.UserService | `Added` | `Method` | `public` | `static` | | ValidateToken | bool | string token | |", reportText); - Assert.Contains("| MyApp.UserService | `Modified` | `Method` | `public` | | | Login | bool | string user, string pass | `Changed` |", reportText); - Assert.Contains("| MyApp.UserService | `Added` | `Property` | `public` | | bool | IsActive | | | |", reportText); - Assert.Contains("| MyApp.UserService | `Added` | `Field` | `private` | `readonly` | object | _cache | | | |", reportText); - Assert.Contains("- Added : 5", reportText); - Assert.Contains("- Removed : 1", reportText); - Assert.Contains("- Modified : 1", reportText); + Assert.Contains("| | `Modified` | `Method` | `public` | | | Login | bool | string user, string pass | `Changed` |", reportText); + Assert.Contains("| | `Added` | `Property` | `public` | | bool | IsActive | | | |", reportText); + // Summary count table + Assert.Contains("| Class | Change | Count |", reportText); + Assert.Contains("| MyApp.NewService | `Added` | 1 |", reportText); + Assert.Contains("| MyApp.UserService | `Added` | 4 |", reportText); + Assert.Contains("| | `Modified` | 1 |", reportText); + Assert.Contains("| | `Removed` | 1 |", reportText); // Ordering: Summary < Assembly Semantic Changes < IL Cache Stats int summaryIdx = reportText.IndexOf("## Summary", StringComparison.Ordinal); diff --git a/README.md b/README.md index 3e29bf3c..f18b0920 100644 --- a/README.md +++ b/README.md @@ -218,7 +218,7 @@ When an assembly is classified as `ILMismatch`, the tool performs an additional Controlled by [`ShouldIncludeAssemblySemanticChangesInReport`](#config-en-shouldincludeassemblysemanticchangesinreport) (default: `true`). -The bullet list below the table shows counts per change kind (`Added`, `Removed`, `Modified`). +A summary count table (`Class | Change | Count`) follows, grouping entries by class and change kind. Consecutive rows with the same class name suppress the class column for readability. ## Configuration ([`config.json`](config.json)) @@ -700,7 +700,7 @@ flowchart TD [`ShouldIncludeAssemblySemanticChangesInReport`](#config-ja-shouldincludeassemblysemanticchangesinreport)(既定値: `true`)で制御します。 -テーブル下の箇条書きに変更種別ごとのカウント(`Added`、`Removed`、`Modified`)が表示されます。 +テーブル下に集計テーブル(`Class | Change | Count`)を表示し、クラスと変更種別ごとのカウントをまとめます。同一クラスが連続する場合、Class 列は先頭行のみに表示されます。 ## 設定([`config.json`](config.json)) diff --git a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs index 7d6e15bd..42593f5f 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs @@ -364,12 +364,15 @@ private void AppendAssemblySemanticChangesRow( contentBuilder.AppendLine("

ClassChangeKindAccessModifiersTypeNameReturnTypeParametersBody
-
+
#3 Show assembly semantic changes (10 changes)
-
+
#5 Show assembly semantic changes (26 changes)
-
+
#9 Show assembly semantic changes (other changes only)
"); contentBuilder.AppendLine(""); contentBuilder.AppendLine(""); + string prevType = ""; foreach (var e in summary.Entries) { + string classTd = e.TypeName != prevType ? HtmlEncode(e.TypeName) : ""; + prevType = e.TypeName; string accessTd = e.Access.Length > 0 ? $"{HtmlEncode(e.Access)}" : ""; string modifiersTd = e.Modifiers.Length > 0 ? $"{HtmlEncode(e.Modifiers)}" : ""; string bodyTd = e.Body.Length > 0 ? $"{HtmlEncode(e.Body)}" : ""; - contentBuilder.AppendLine($""); + contentBuilder.AppendLine($""); } contentBuilder.AppendLine("
ClassChangeKindAccessModifiersTypeNameReturnTypeParametersBody
{HtmlEncode(e.TypeName)}{HtmlEncode(e.Change)}{HtmlEncode(e.MemberKind)}{accessTd}{modifiersTd}{HtmlEncode(e.MemberType)}{HtmlEncode(e.MemberName)}{HtmlEncode(e.ReturnType)}{HtmlEncode(e.Parameters)}{bodyTd}
{classTd}{HtmlEncode(e.Change)}{HtmlEncode(e.MemberKind)}{accessTd}{modifiersTd}{HtmlEncode(e.MemberType)}{HtmlEncode(e.MemberName)}{HtmlEncode(e.ReturnType)}{HtmlEncode(e.Parameters)}{bodyTd}
"); } @@ -378,11 +381,7 @@ private void AppendAssemblySemanticChangesRow( contentBuilder.AppendLine("

Other changes only. See IL diff for details.

"); } - contentBuilder.AppendLine("
    "); - contentBuilder.AppendLine($"
  • Added : {summary.AddedCount}
  • "); - contentBuilder.AppendLine($"
  • Removed : {summary.RemovedCount}
  • "); - contentBuilder.AppendLine($"
  • Modified : {summary.ModifiedCount}
  • "); - contentBuilder.AppendLine("
"); + AppendSummaryCountTable(contentBuilder, summary); contentBuilder.AppendLine("
"); string detailsId = $"semantic_{sectionPrefix}_{idx}"; @@ -412,6 +411,28 @@ private void AppendAssemblySemanticChangesRow( sb.AppendLine(""); } + private static void AppendSummaryCountTable(StringBuilder sb, AssemblySemanticChangesSummary summary) + { + var counts = new Dictionary<(string TypeName, string Change), int>(); + foreach (var e in summary.Entries) + { + var key = (e.TypeName, e.Change); + counts[key] = counts.TryGetValue(key, out int c) ? c + 1 : 1; + } + + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine(""); + string prevType = ""; + foreach (var ((typeName, change), count) in counts.OrderBy(kv => kv.Key.TypeName, StringComparer.Ordinal).ThenBy(kv => kv.Key.Change, StringComparer.Ordinal)) + { + string classTd = typeName != prevType ? HtmlEncode(typeName) : ""; + prevType = typeName; + sb.AppendLine($""); + } + sb.AppendLine("
ClassChangeCount
{classTd}{HtmlEncode(change)}{count}
"); + } + private void AppendSummarySection(StringBuilder sb, ConfigSettings config) { sb.AppendLine("

Summary

"); diff --git a/Services/ReportGenerateService.SectionWriters.cs b/Services/ReportGenerateService.SectionWriters.cs index 27fe7af0..a33d8055 100644 --- a/Services/ReportGenerateService.SectionWriters.cs +++ b/Services/ReportGenerateService.SectionWriters.cs @@ -211,12 +211,15 @@ public void Write(StreamWriter writer, ReportWriteContext ctx) writer.WriteLine(); writer.WriteLine("| Class | Change | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | Body |"); writer.WriteLine("|-------|--------|------|--------|-----------|------|------|------------|------------|------|"); + string prevType = ""; foreach (var e in summary.Entries) { + string classCol = e.TypeName != prevType ? EscapeMdTable(e.TypeName) : ""; + prevType = e.TypeName; string access = e.Access.Length > 0 ? $"`{EscapeMdTable(e.Access)}`" : ""; string modifiers = e.Modifiers.Length > 0 ? $"`{EscapeMdTable(e.Modifiers)}`" : ""; string body = e.Body.Length > 0 ? $"`{EscapeMdTable(e.Body)}`" : ""; - writer.WriteLine($"| {EscapeMdTable(e.TypeName)} | `{EscapeMdTable(e.Change)}` | `{EscapeMdTable(e.MemberKind)}` | {access} | {modifiers} | {EscapeMdTable(e.MemberType)} | {EscapeMdTable(e.MemberName)} | {EscapeMdTable(e.ReturnType)} | {EscapeMdTable(e.Parameters)} | {body} |"); + writer.WriteLine($"| {classCol} | `{EscapeMdTable(e.Change)}` | `{EscapeMdTable(e.MemberKind)}` | {access} | {modifiers} | {EscapeMdTable(e.MemberType)} | {EscapeMdTable(e.MemberName)} | {EscapeMdTable(e.ReturnType)} | {EscapeMdTable(e.Parameters)} | {body} |"); } } else @@ -224,14 +227,33 @@ public void Write(StreamWriter writer, ReportWriteContext ctx) writer.WriteLine("- Other changes only. See IL diff for details."); } - writer.WriteLine($"- Added : {summary.AddedCount}"); - writer.WriteLine($"- Removed : {summary.RemovedCount}"); - writer.WriteLine($"- Modified : {summary.ModifiedCount}"); + writer.WriteLine(); + writer.WriteLine("| Class | Change | Count |"); + writer.WriteLine("|-------|--------|-------|"); + WriteSummaryCountTable(writer, summary); } writer.WriteLine(); } + private static void WriteSummaryCountTable(StreamWriter writer, AssemblySemanticChangesSummary summary) + { + var counts = new Dictionary<(string TypeName, string Change), int>(); + foreach (var e in summary.Entries) + { + var key = (e.TypeName, e.Change); + counts[key] = counts.TryGetValue(key, out int c) ? c + 1 : 1; + } + + string prevType = ""; + foreach (var ((typeName, change), count) in counts.OrderBy(kv => kv.Key.TypeName, StringComparer.Ordinal).ThenBy(kv => kv.Key.Change, StringComparer.Ordinal)) + { + string classCol = typeName != prevType ? EscapeMdTable(typeName) : ""; + prevType = typeName; + writer.WriteLine($"| {classCol} | `{EscapeMdTable(change)}` | {count} |"); + } + } + /// Escape pipe characters for Markdown table cells. / Markdown テーブルセル用にパイプ文字をエスケープ。 private static string EscapeMdTable(string value) => value.Replace("|", "\\|"); } diff --git a/doc/samples/diff_report.md b/doc/samples/diff_report.md index ba3b1618..f3f3e7ad 100644 --- a/doc/samples/diff_report.md +++ b/doc/samples/diff_report.md @@ -59,58 +59,70 @@ | Class | Change | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | Body | |-------|--------|------|--------|-----------|------|------|------------|------------|------| | MyApp.Controllers.ApiController | `Added` | `Method` | `public` | | | HealthCheck | string | | | -| MyApp.Controllers.ApiController | `Added` | `Method` | `public` | | | GetUsers | System.Collections.Generic.IList\ | int page, int pageSize = 20 | | -| MyApp.Controllers.ApiController | `Removed` | `Method` | `public` | | | GetUsers | System.Collections.Generic.IList\ | int page | | -| MyApp.Controllers.ApiController | `Modified` | `Method` | `public` | `virtual` | | Search | System.Collections.Generic.IList\ | string query | `Changed` | -| MyApp.Controllers.ApiController | `Modified` | `Method` | `protected` | | | OnAuthorize | bool | MyApp.Models.UserContext ctx | `Changed` | +| | `Added` | `Method` | `public` | | | GetUsers | System.Collections.Generic.IList\ | int page, int pageSize = 20 | | +| | `Removed` | `Method` | `public` | | | GetUsers | System.Collections.Generic.IList\ | int page | | +| | `Modified` | `Method` | `public` | `virtual` | | Search | System.Collections.Generic.IList\ | string query | `Changed` | +| | `Modified` | `Method` | `protected` | | | OnAuthorize | bool | MyApp.Models.UserContext ctx | `Changed` | | MyApp.Services.DataService | `Modified` | `Method` | `internal` | | | RefreshCache | void | | `Changed` | -| MyApp.Services.DataService | `Modified` | `Method` | `private` | | | ValidateConnection | bool | string connStr | `Changed` | -| MyApp.Services.DataService | `Added` | `Property` | `public` | | int | CacheTimeout | | | | -| MyApp.Services.DataService | `Added` | `Property` | `public` | | MyApp.Models.CachePolicy | Policy | | | | -| MyApp.Services.DataService | `Added` | `Property` | `internal` | | MyApp.Services.IConnectionPool | ConnectionPool | | | | -- Added : 5 -- Removed : 1 -- Modified : 4 +| | `Modified` | `Method` | `private` | | | ValidateConnection | bool | string connStr | `Changed` | +| | `Added` | `Property` | `public` | | int | CacheTimeout | | | | +| | `Added` | `Property` | `public` | | MyApp.Models.CachePolicy | Policy | | | | +| | `Added` | `Property` | `internal` | | MyApp.Services.IConnectionPool | ConnectionPool | | | | + +| Class | Change | Count | +|-------|--------|-------| +| MyApp.Controllers.ApiController | `Added` | 2 | +| | `Modified` | 2 | +| | `Removed` | 1 | +| MyApp.Services.DataService | `Added` | 3 | +| | `Modified` | 2 | ### src/Service.dll | Class | Change | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | Body | |-------|--------|------|--------|-----------|------|------|------------|------------|------| | MyApp.Services.NewValidator | `Added` | `Class` | `public` | | | | | | | -| MyApp.Services.NewValidator | `Added` | `Constructor` | `public` | | | NewValidator | void | | | -| MyApp.Services.NewValidator | `Added` | `Method` | `public` | | | Validate | bool | string input | | -| MyApp.Services.NewValidator | `Added` | `Method` | `public` | | | Validate | bool | string input, MyApp.Models.ValidationOptions options | | -| MyApp.Services.NewValidator | `Added` | `Method` | `private` | | | ParseInput | string | string raw | | -| MyApp.Services.NewValidator | `Added` | `Property` | `public` | | MyApp.Models.ValidationResult | LastResult | | | | -| MyApp.Services.NewValidator | `Added` | `Field` | `private` | `readonly` | string | _pattern | | | | +| | `Added` | `Constructor` | `public` | | | NewValidator | void | | | +| | `Added` | `Method` | `public` | | | Validate | bool | string input | | +| | `Added` | `Method` | `public` | | | Validate | bool | string input, MyApp.Models.ValidationOptions options | | +| | `Added` | `Method` | `private` | | | ParseInput | string | string raw | | +| | `Added` | `Property` | `public` | | MyApp.Models.ValidationResult | LastResult | | | | +| | `Added` | `Field` | `private` | `readonly` | string | _pattern | | | | | MyApp.Services.OrderService | `Added` | `Method` | `public` | | | ValidateWithNewValidator | bool | string data | | -| MyApp.Services.OrderService | `Removed` | `Method` | `public` | `virtual` | | LegacyValidate | bool | string data | | -| MyApp.Services.OrderService | `Modified` | `Method` | `public` | | | ProcessOrder | void | int orderId | `Changed` | -| MyApp.Services.OrderService | `Modified` | `Method` | `internal` | `static` | | CalculateTotal | decimal | int qty, int price | `Changed` | -| MyApp.Services.OrderService | `Added` | `Property` | `protected` | | MyApp.Models.OrderContext | CurrentContext | | | | -| MyApp.Services.OrderService | `Added` | `Field` | `private` | `readonly` | MyApp.Models.UserRecord | _defaultUser | | | | +| | `Removed` | `Method` | `public` | `virtual` | | LegacyValidate | bool | string data | | +| | `Modified` | `Method` | `public` | | | ProcessOrder | void | int orderId | `Changed` | +| | `Modified` | `Method` | `internal` | `static` | | CalculateTotal | decimal | int qty, int price | `Changed` | +| | `Added` | `Property` | `protected` | | MyApp.Models.OrderContext | CurrentContext | | | | +| | `Added` | `Field` | `private` | `readonly` | MyApp.Models.UserRecord | _defaultUser | | | | | MyApp.Services.LegacyHelper | `Removed` | `Class` | `internal` | | | | | | | -| MyApp.Services.LegacyHelper | `Removed` | `Method` | `public` | | | Convert | string | object value | | -| MyApp.Services.LegacyHelper | `Removed` | `Method` | `public` | `static` | | Format | string | string template, object[] args | | +| | `Removed` | `Method` | `public` | | | Convert | string | object value | | +| | `Removed` | `Method` | `public` | `static` | | Format | string | string template, object[] args | | | MyApp.Models.UserRecord | `Added` | `Record` | `public` | | | | | | | -| MyApp.Models.UserRecord | `Added` | `Constructor` | `public` | | | UserRecord | void | string Name, int Age | | -| MyApp.Models.UserRecord | `Added` | `Property` | `public` | | string | Name | | | | -| MyApp.Models.UserRecord | `Added` | `Property` | `public` | | int | Age | | | | -| MyApp.Models.UserRecord | `Added` | `Method` | `public` | `override` | | ToString | string | | | -| MyApp.Models.UserRecord | `Added` | `Method` | `public` | `virtual` | | Equals | bool | object obj | | -| MyApp.Models.UserRecord | `Added` | `Method` | `public` | `override` | | GetHashCode | int | | | +| | `Added` | `Constructor` | `public` | | | UserRecord | void | string Name, int Age | | +| | `Added` | `Property` | `public` | | string | Name | | | | +| | `Added` | `Property` | `public` | | int | Age | | | | +| | `Added` | `Method` | `public` | `override` | | ToString | string | | | +| | `Added` | `Method` | `public` | `virtual` | | Equals | bool | object obj | | +| | `Added` | `Method` | `public` | `override` | | GetHashCode | int | | | | MyApp.Models.UserDto | `Removed` | `Class` | `public` | | | | | | | -| MyApp.Models.UserDto | `Removed` | `Property` | `public` | | string | Name | | | | -| MyApp.Models.UserDto | `Removed` | `Property` | `public` | | int | Age | | | | -- Added : 17 -- Removed : 7 -- Modified : 2 +| | `Removed` | `Property` | `public` | | string | Name | | | | +| | `Removed` | `Property` | `public` | | int | Age | | | | + +| Class | Change | Count | +|-------|--------|-------| +| MyApp.Models.UserDto | `Removed` | 3 | +| MyApp.Models.UserRecord | `Added` | 7 | +| MyApp.Services.LegacyHelper | `Removed` | 3 | +| MyApp.Services.NewValidator | `Added` | 7 | +| MyApp.Services.OrderService | `Added` | 3 | +| | `Modified` | 2 | +| | `Removed` | 1 | ### util/Legacy.dll - Other changes only. See IL diff for details. -- Added : 0 -- Removed : 0 -- Modified : 0 + +| Class | Change | Count | +|-------|--------|-------| ## IL Cache Stats - Hits : 42 From 86c8c3e4803d413782dacf624a03f55bb6755359 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 19:13:25 +0000 Subject: [PATCH 17/36] Remove parenthetical counts from Show assembly semantic changes label https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- Services/HtmlReport/HtmlReportGenerateService.Sections.cs | 6 +----- doc/samples/diff_report.html | 6 +++--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs index 42593f5f..e8c3a476 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs @@ -354,8 +354,6 @@ private void AppendAssemblySemanticChangesRow( string sectionPrefix = "mod") { int recordNo = idx + 1; - int totalChanges = summary.Entries.Count; - var contentBuilder = new StringBuilder(); contentBuilder.AppendLine("
"); @@ -385,9 +383,7 @@ private void AppendAssemblySemanticChangesRow( contentBuilder.AppendLine("
"); string detailsId = $"semantic_{sectionPrefix}_{idx}"; - string summaryText = totalChanges > 0 - ? $"#{recordNo} Show assembly semantic changes ({totalChanges} change{(totalChanges == 1 ? "" : "s")})" - : $"#{recordNo} Show assembly semantic changes (other changes only)"; + string summaryText = $"#{recordNo} Show assembly semantic changes"; string summaryLabel = $" {HtmlEncode(summaryText)}"; string contentHtml = contentBuilder.ToString(); diff --git a/doc/samples/diff_report.html b/doc/samples/diff_report.html index 52cafa99..dd82bd48 100644 --- a/doc/samples/diff_report.html +++ b/doc/samples/diff_report.html @@ -438,7 +438,7 @@

[ * ] Modified Files (9)

- #3 Show assembly semantic changes (10 changes) + #3 Show assembly semantic changes
@@ -479,7 +479,7 @@

[ * ] Modified Files (9)

- #5 Show assembly semantic changes (26 changes) + #5 Show assembly semantic changes
@@ -546,7 +546,7 @@

[ * ] Modified Files (9)

- #9 Show assembly semantic changes (other changes only) + #9 Show assembly semantic changes
From 58f4822fecb59f78109ef83a25dfe6fe9a8d73b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 19:15:47 +0000 Subject: [PATCH 18/36] Hide horizontal borders between rows of the same class group Add CSS class group-cont to continuation rows in both the main semantic changes table and the summary count table. The border-top is removed for these rows so grouped entries appear visually merged. https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- Services/HtmlReport/HtmlReportGenerateService.Css.cs | 3 ++- .../HtmlReport/HtmlReportGenerateService.Sections.cs | 12 ++++++++---- doc/samples/diff_report.html | 1 + 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Services/HtmlReport/HtmlReportGenerateService.Css.cs b/Services/HtmlReport/HtmlReportGenerateService.Css.cs index 78c78c90..3bfbb499 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Css.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Css.cs @@ -138,7 +138,8 @@ private static string GetCss() .semantic-changes p { margin: 4px 0 2px; } table.semantic-changes-table { width: auto; border-collapse: collapse; margin: 4px 0; font-size: 12px; } table.semantic-changes-table th { padding: 3px 8px; border: 1px solid #ccc; background: #f6f8fa; font-size: 11px; text-align: left; white-space: nowrap; } - table.semantic-changes-table td { padding: 2px 8px; border: 1px solid #e0e0e0; font-size: 12px; white-space: nowrap; }"; + table.semantic-changes-table td { padding: 2px 8px; border: 1px solid #e0e0e0; font-size: 12px; white-space: nowrap; } + table.semantic-changes-table tr.group-cont td { border-top: none; }"; } } } diff --git a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs index e8c3a476..b6f8629f 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs @@ -365,12 +365,14 @@ private void AppendAssemblySemanticChangesRow( string prevType = ""; foreach (var e in summary.Entries) { - string classTd = e.TypeName != prevType ? HtmlEncode(e.TypeName) : ""; + bool isCont = e.TypeName == prevType; + string classTd = !isCont ? HtmlEncode(e.TypeName) : ""; prevType = e.TypeName; + string trOpen = isCont ? "" : ""; string accessTd = e.Access.Length > 0 ? $"{HtmlEncode(e.Access)}" : ""; string modifiersTd = e.Modifiers.Length > 0 ? $"{HtmlEncode(e.Modifiers)}" : ""; string bodyTd = e.Body.Length > 0 ? $"{HtmlEncode(e.Body)}" : ""; - contentBuilder.AppendLine($"{classTd}{HtmlEncode(e.Change)}{HtmlEncode(e.MemberKind)}{accessTd}{modifiersTd}{HtmlEncode(e.MemberType)}{HtmlEncode(e.MemberName)}{HtmlEncode(e.ReturnType)}{HtmlEncode(e.Parameters)}{bodyTd}"); + contentBuilder.AppendLine($"{trOpen}{classTd}{HtmlEncode(e.Change)}{HtmlEncode(e.MemberKind)}{accessTd}{modifiersTd}{HtmlEncode(e.MemberType)}{HtmlEncode(e.MemberName)}{HtmlEncode(e.ReturnType)}{HtmlEncode(e.Parameters)}{bodyTd}"); } contentBuilder.AppendLine(""); } @@ -422,9 +424,11 @@ private static void AppendSummaryCountTable(StringBuilder sb, AssemblySemanticCh string prevType = ""; foreach (var ((typeName, change), count) in counts.OrderBy(kv => kv.Key.TypeName, StringComparer.Ordinal).ThenBy(kv => kv.Key.Change, StringComparer.Ordinal)) { - string classTd = typeName != prevType ? HtmlEncode(typeName) : ""; + bool isCont = typeName == prevType; + string classTd = !isCont ? HtmlEncode(typeName) : ""; prevType = typeName; - sb.AppendLine($"{classTd}{HtmlEncode(change)}{count}"); + string trOpen = isCont ? "" : ""; + sb.AppendLine($"{trOpen}{classTd}{HtmlEncode(change)}{count}"); } sb.AppendLine(""); } diff --git a/doc/samples/diff_report.html b/doc/samples/diff_report.html index dd82bd48..784db9c9 100644 --- a/doc/samples/diff_report.html +++ b/doc/samples/diff_report.html @@ -137,6 +137,7 @@ table.semantic-changes-table { width: auto; border-collapse: collapse; margin: 4px 0; font-size: 12px; } table.semantic-changes-table th { padding: 3px 8px; border: 1px solid #ccc; background: #f6f8fa; font-size: 11px; text-align: left; white-space: nowrap; } table.semantic-changes-table td { padding: 2px 8px; border: 1px solid #e0e0e0; font-size: 12px; white-space: nowrap; } + table.semantic-changes-table tr.group-cont td { border-top: none; } From 1dac444229849d96a23ca873cff5970c0a4d1d20 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 19:16:37 +0000 Subject: [PATCH 19/36] Skip summary count table when there are no entries Do not render the empty Class|Change|Count table header for assemblies with no semantic change entries (Other changes only). https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- .../HtmlReport/HtmlReportGenerateService.Sections.cs | 3 ++- Services/ReportGenerateService.SectionWriters.cs | 11 +++++++---- doc/samples/diff_report.md | 3 --- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs index b6f8629f..eef9f037 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs @@ -381,7 +381,8 @@ private void AppendAssemblySemanticChangesRow( contentBuilder.AppendLine("

Other changes only. See IL diff for details.

"); } - AppendSummaryCountTable(contentBuilder, summary); + if (summary.Entries.Count > 0) + AppendSummaryCountTable(contentBuilder, summary); contentBuilder.AppendLine("
"); string detailsId = $"semantic_{sectionPrefix}_{idx}"; diff --git a/Services/ReportGenerateService.SectionWriters.cs b/Services/ReportGenerateService.SectionWriters.cs index a33d8055..c4d06bb5 100644 --- a/Services/ReportGenerateService.SectionWriters.cs +++ b/Services/ReportGenerateService.SectionWriters.cs @@ -227,10 +227,13 @@ public void Write(StreamWriter writer, ReportWriteContext ctx) writer.WriteLine("- Other changes only. See IL diff for details."); } - writer.WriteLine(); - writer.WriteLine("| Class | Change | Count |"); - writer.WriteLine("|-------|--------|-------|"); - WriteSummaryCountTable(writer, summary); + if (summary.Entries.Count > 0) + { + writer.WriteLine(); + writer.WriteLine("| Class | Change | Count |"); + writer.WriteLine("|-------|--------|-------|"); + WriteSummaryCountTable(writer, summary); + } } writer.WriteLine(); diff --git a/doc/samples/diff_report.md b/doc/samples/diff_report.md index f3f3e7ad..7717cfb5 100644 --- a/doc/samples/diff_report.md +++ b/doc/samples/diff_report.md @@ -121,9 +121,6 @@ ### util/Legacy.dll - Other changes only. See IL diff for details. -| Class | Change | Count | -|-------|--------|-------| - ## IL Cache Stats - Hits : 42 - Misses : 8 From 808d8003c2545251faf1337a6189b697abf5cc09 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 19:39:20 +0000 Subject: [PATCH 20/36] Add BaseType column, fully qualified type names, sealed/interface detection, and table alignment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add BaseType column (after Class) showing base class and implemented interfaces (omits trivial bases like System.Object, System.ValueType, System.Enum) - Use fully qualified .NET type names instead of C# aliases (System.String not string, System.Int32 not int, System.Void not void, etc.) in Type, ReturnType, and Parameters - Detect sealed/abstract/static modifiers for type entries - Read implemented interfaces from assembly metadata via GetInterfaceImplementations - Sort entries by Class name, then by Change order (Added → Removed → Modified) - Sort Count summary table by same order - Center-align Change, Kind, Access, Modifiers column bodies in HTML - Right-align Count column body in HTML summary count table - Update sample reports with interface and sealed class examples - Update "No structural changes detected" text for assemblies with no semantic changes https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- .../AssemblySemanticChangesSummaryTests.cs | 5 +- .../HtmlReportGenerateServiceTests.cs | 12 +- .../Services/ReportGenerateServiceTests.cs | 28 ++-- Models/MemberChangeEntry.cs | 2 + README.md | 26 ++-- Services/AssemblyMethodAnalyzer.cs | 146 ++++++++++++++---- .../HtmlReportGenerateService.Css.cs | 2 + .../HtmlReportGenerateService.Sections.cs | 14 +- .../ReportGenerateService.SectionWriters.cs | 18 ++- doc/samples/diff_report.html | 8 +- doc/samples/diff_report.md | 89 +++++------ 11 files changed, 231 insertions(+), 119 deletions(-) diff --git a/FolderDiffIL4DotNet.Tests/Models/AssemblySemanticChangesSummaryTests.cs b/FolderDiffIL4DotNet.Tests/Models/AssemblySemanticChangesSummaryTests.cs index 91d481ed..b1529869 100644 --- a/FolderDiffIL4DotNet.Tests/Models/AssemblySemanticChangesSummaryTests.cs +++ b/FolderDiffIL4DotNet.Tests/Models/AssemblySemanticChangesSummaryTests.cs @@ -20,7 +20,7 @@ public void HasChanges_WithEntries_ReturnsTrue() { Entries = new List { - new("Added", "MyApp.Service", "public", "", "Method", "DoWork", "", "void", "int count", ""), + new("Added", "MyApp.Service", "", "public", "", "Method", "DoWork", "", "void", "int count", ""), }, }; Assert.True(summary.HasChanges); @@ -39,9 +39,10 @@ public void HasChanges_EmptyEntries_ReturnsFalse() [Fact] public void Entries_ContainStructuredData() { - var entry = new MemberChangeEntry("Added", "MyApp.Service", "public", "static", "Method", "GetName", "", "string", "string id", ""); + var entry = new MemberChangeEntry("Added", "MyApp.Service", "SomeBase", "public", "static", "Method", "GetName", "", "string", "string id", ""); Assert.Equal("Added", entry.Change); Assert.Equal("MyApp.Service", entry.TypeName); + Assert.Equal("SomeBase", entry.BaseType); Assert.Equal("public", entry.Access); Assert.Equal("static", entry.Modifiers); Assert.Equal("Method", entry.MemberKind); diff --git a/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs b/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs index c12c78a5..908c62f6 100644 --- a/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs @@ -730,10 +730,10 @@ public void GenerateDiffReportHtml_AssemblySemanticChanges_ShowsInlineAboveILDif { Entries = new List { - new("Added", "MyApp.Service", "public", "", "Method", "NewMethod", "", "void", "string name", ""), - new("Modified", "MyApp.Service", "public", "virtual", "Method", "ExistingMethod", "", "bool", "int id", "Changed"), - new("Added", "MyApp.Service", "public", "", "Property", "NewProp", "string", "", "", ""), - new("Removed", "MyApp.Service", "private", "readonly", "Field", "_oldField", "int", "", "", ""), + new("Added", "MyApp.Service", "", "public", "", "Method", "NewMethod", "", "void", "string name", ""), + new("Modified", "MyApp.Service", "", "public", "virtual", "Method", "ExistingMethod", "", "bool", "int id", "Changed"), + new("Added", "MyApp.Service", "", "public", "", "Property", "NewProp", "string", "", "", ""), + new("Removed", "MyApp.Service", "", "private", "readonly", "Field", "_oldField", "int", "", "", ""), }, }; @@ -761,7 +761,7 @@ public void GenerateDiffReportHtml_AssemblySemanticChanges_NotShownWhenDisabled( { Entries = new List { - new("Added", "MyApp.Service", "public", "", "Method", "NewMethod", "", "void", "string name", ""), + new("Added", "MyApp.Service", "", "public", "", "Method", "NewMethod", "", "void", "string name", ""), }, }; @@ -787,7 +787,7 @@ public void GenerateDiffReportHtml_AssemblySemanticChanges_LazyRender_EncodesAsB { Entries = new List { - new("Added", "Foo", "public", "", "Method", "Bar", "", "void", "", ""), + new("Added", "Foo", "", "public", "", "Method", "Bar", "", "void", "", ""), }, }; diff --git a/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs b/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs index 344061da..51d037fd 100644 --- a/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs @@ -773,13 +773,13 @@ public void GenerateDiffReport_AssemblySemanticChanges_IncludedBetweenSummaryAnd { Entries = new List { - new("Added", "MyApp.NewService", "public", "", "Class", "", "", "", "", ""), - new("Added", "MyApp.UserService", "public", "static", "Method", "ValidateToken", "", "bool", "string token", ""), - new("Added", "MyApp.UserService", "internal", "", "Method", "RefreshSession", "", "void", "int userId", ""), - new("Removed", "MyApp.UserService", "public", "virtual", "Method", "LegacyAuth", "", "void", "string key", ""), - new("Modified", "MyApp.UserService", "public", "", "Method", "Login", "", "bool", "string user, string pass", "Changed"), - new("Added", "MyApp.UserService", "public", "", "Property", "IsActive", "bool", "", "", ""), - new("Added", "MyApp.UserService", "private", "readonly", "Field", "_cache", "object", "", "", ""), + new("Added", "MyApp.NewService", "", "public", "", "Class", "", "", "", "", ""), + new("Added", "MyApp.UserService", "", "public", "static", "Method", "ValidateToken", "", "bool", "string token", ""), + new("Added", "MyApp.UserService", "", "internal", "", "Method", "RefreshSession", "", "void", "int userId", ""), + new("Removed", "MyApp.UserService", "", "public", "virtual", "Method", "LegacyAuth", "", "void", "string key", ""), + new("Modified", "MyApp.UserService", "", "public", "", "Method", "Login", "", "bool", "string user, string pass", "Changed"), + new("Added", "MyApp.UserService", "", "public", "", "Property", "IsActive", "bool", "", "", ""), + new("Added", "MyApp.UserService", "", "private", "readonly", "Field", "_cache", "object", "", "", ""), }, }; _resultLists.FileRelativePathToAssemblySemanticChanges["src/App.dll"] = summary; @@ -799,18 +799,18 @@ public void GenerateDiffReport_AssemblySemanticChanges_IncludedBetweenSummaryAnd // Content checks — table format Assert.Contains("## Assembly Semantic Changes", reportText); Assert.Contains("### src/App.dll", reportText); - Assert.Contains("| Class | Change | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | Body |", reportText); + Assert.Contains("| Class | BaseType | Change | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | Body |", reportText); // First row shows class name; subsequent rows for same class are empty - Assert.Contains("| MyApp.NewService | `Added` | `Class` | `public` | | | | | | |", reportText); - Assert.Contains("| MyApp.UserService | `Added` | `Method` | `public` | `static` | | ValidateToken | bool | string token | |", reportText); - Assert.Contains("| | `Modified` | `Method` | `public` | | | Login | bool | string user, string pass | `Changed` |", reportText); - Assert.Contains("| | `Added` | `Property` | `public` | | bool | IsActive | | | |", reportText); + Assert.Contains("| MyApp.NewService | | `Added` | `Class` | `public` | | | | | | |", reportText); + Assert.Contains("| MyApp.UserService | | `Added` | `Method` | `public` | `static` | | ValidateToken | bool | string token | |", reportText); + Assert.Contains("| | | `Modified` | `Method` | `public` | | | Login | bool | string user, string pass | `Changed` |", reportText); + Assert.Contains("| | | `Added` | `Property` | `public` | | bool | IsActive | | | |", reportText); // Summary count table Assert.Contains("| Class | Change | Count |", reportText); Assert.Contains("| MyApp.NewService | `Added` | 1 |", reportText); Assert.Contains("| MyApp.UserService | `Added` | 4 |", reportText); - Assert.Contains("| | `Modified` | 1 |", reportText); Assert.Contains("| | `Removed` | 1 |", reportText); + Assert.Contains("| | `Modified` | 1 |", reportText); // Ordering: Summary < Assembly Semantic Changes < IL Cache Stats int summaryIdx = reportText.IndexOf("## Summary", StringComparison.Ordinal); @@ -834,7 +834,7 @@ public void GenerateDiffReport_AssemblySemanticChanges_NotIncludedWhenDisabled() { Entries = new List { - new("Added", "Foo", "public", "", "Method", "Bar", "", "void", "", ""), + new("Added", "Foo", "", "public", "", "Method", "Bar", "", "void", "", ""), }, }; diff --git a/Models/MemberChangeEntry.cs b/Models/MemberChangeEntry.cs index 50b04871..d3254382 100644 --- a/Models/MemberChangeEntry.cs +++ b/Models/MemberChangeEntry.cs @@ -13,10 +13,12 @@ namespace FolderDiffIL4DotNet.Models /// For Field/Property: the declared type (e.g. "string", "int"). Empty for Method/Constructor/Type entries. / フィールド・プロパティの宣言型。メソッド・コンストラクタ・Type の場合は空。 /// For Method: the return type (e.g. "void", "string"). For Constructor: "void". Empty for Type/Field/Property entries. / メソッドの戻り値型。コンストラクタは "void"。Type/Field/Property の場合は空。 /// For Method/Constructor: the parameter list without parentheses (e.g. "int page", "string name, int count = 0"). Empty string for no-arg methods. Empty for Type/Field/Property entries. / メソッド・コンストラクタのパラメータ一覧(括弧なし)。引数なしは空文字列。Type/Field/Property の場合は空。 + /// Base type and implemented interfaces of the owning type (e.g. "MyApp.BaseController, System.IDisposable"). Omits trivial bases (System.Object, System.ValueType, System.Enum). / 所属型の基底型および実装インターフェース。自明な基底型は省略。 /// "Changed" when the method body or field initializer IL has changed; otherwise empty. / メソッドボディまたはフィールド初期化子の IL が変更された場合 "Changed"、それ以外は空。 public sealed record MemberChangeEntry( string Change, string TypeName, + string BaseType, string Access, string Modifiers, string MemberKind, diff --git a/README.md b/README.md index f18b0920..8cd803ce 100644 --- a/README.md +++ b/README.md @@ -194,26 +194,27 @@ When an assembly is classified as `ILMismatch`, the tool performs an additional | Category | Detected changes | |----------|-----------------| -| **Type** | Additions and removals (including nested types) | +| **Type** | Additions and removals (including nested types), with base type and implemented interfaces | | **Method** | Additions, removals, and IL body modifications | | **Property** | Additions and removals (with get/set accessor info) | | **Field** | Additions and removals (with type and default value) | | **Access** | `public`, `protected`, `internal`, `private`, `protected internal`, `private protected` | -| **Modifiers** | `static`, `abstract`, `virtual`, `override`, `sealed override`, `const`, `readonly` | +| **Modifiers** | For types: `sealed`, `abstract`, `static`. For members: `static`, `abstract`, `virtual`, `override`, `sealed override`, `const`, `readonly` | ### Report table columns | Column | Description | Example | |--------|-------------|---------| | Class | Fully qualified type name | `MyNamespace.MyClass` | +| BaseType | Base type and implemented interfaces (omits trivial bases like System.Object) | `MyApp.BaseController, System.IDisposable` | | Change | `Added`, `Removed`, or `Modified` | `Added` | | Kind | Member kind: `Class`, `Record`, `Struct`, `Interface`, `Enum`, `Constructor`, `StaticConstructor`, `Method`, `Property`, `Field` | `Method` | | Access | Access modifier | `public` | -| Modifiers | Other modifiers | `static` | -| Type | Declared type for Field/Property (empty for Method/Constructor/Class/Record) | `int` | +| Modifiers | Other modifiers (for types: `sealed`, `abstract`, `static`; for members: `static`, `virtual`, `override`, etc.) | `sealed` | +| Type | Declared type for Field/Property using fully qualified .NET type names (empty for Method/Constructor/Class/Record) | `System.Int32` | | Name | Member name (constructors use the class name; empty for Class/Record/Struct/Interface/Enum entries) | `DoWork` | -| ReturnType | Return type for Method/Constructor (empty for Field/Property/Class/Record) | `void` | -| Parameters | Parameter list for Method/Constructor (empty for Field/Property/Class/Record) | `string name, int count = 0` | +| ReturnType | Return type for Method/Constructor using fully qualified .NET type names (empty for Field/Property/Class/Record) | `System.Void` | +| Parameters | Parameter list for Method/Constructor using fully qualified .NET type names (empty for Field/Property/Class/Record) | `System.String name, System.Int32 count = 0` | | Body | `Changed` when method body or field initializer IL has changed; otherwise empty | `Changed` | Controlled by [`ShouldIncludeAssemblySemanticChangesInReport`](#config-en-shouldincludeassemblysemanticchangesinreport) (default: `true`). @@ -676,26 +677,27 @@ flowchart TD | カテゴリ | 検出内容 | |---------|---------| -| **Type** | 型の追加・削除(ネスト型を含む) | +| **Type** | 型の追加・削除(ネスト型を含む)、基底型および実装インターフェース情報付き | | **Method** | メソッドの追加・削除・IL ボディの変更 | | **Property** | プロパティの追加・削除(get/set アクセサ情報付き) | | **Field** | フィールドの追加・削除(型と既定値付き) | | **Access** | `public`, `protected`, `internal`, `private`, `protected internal`, `private protected` | -| **Modifiers** | `static`, `abstract`, `virtual`, `override`, `sealed override`, `const`, `readonly` | +| **Modifiers** | 型: `sealed`, `abstract`, `static`。メンバー: `static`, `abstract`, `virtual`, `override`, `sealed override`, `const`, `readonly` | ### レポートテーブル列 | 列 | 説明 | 例 | |----|------|-----| | Class | 完全修飾型名 | `MyNamespace.MyClass` | +| BaseType | 基底型および実装インターフェース(System.Object 等の自明な基底型は省略) | `MyApp.BaseController, System.IDisposable` | | Change | `Added`、`Removed`、`Modified` | `Added` | | Kind | メンバー種別: `Class`, `Record`, `Struct`, `Interface`, `Enum`, `Constructor`, `StaticConstructor`, `Method`, `Property`, `Field` | `Method` | | Access | アクセス修飾子 | `public` | -| Modifiers | その他の修飾子 | `static` | -| Type | Field/Property の宣言型(Method/Constructor/Class/Record の場合は空) | `int` | +| Modifiers | その他の修飾子(型: `sealed`, `abstract`, `static`、メンバー: `static`, `virtual` 等) | `sealed` | +| Type | Field/Property の宣言型(完全修飾 .NET 型名、Method/Constructor/Class/Record の場合は空) | `System.Int32` | | Name | メンバー名(コンストラクタはクラス名、Class/Record/Struct/Interface/Enum エントリの場合は空) | `DoWork` | -| ReturnType | Method/Constructor の戻り値型(Field/Property/Class/Record の場合は空) | `void` | -| Parameters | Method/Constructor のパラメータ一覧(Field/Property/Class/Record の場合は空) | `string name, int count = 0` | +| ReturnType | Method/Constructor の戻り値型(完全修飾 .NET 型名、Field/Property/Class/Record の場合は空) | `System.Void` | +| Parameters | Method/Constructor のパラメータ一覧(完全修飾 .NET 型名、Field/Property/Class/Record の場合は空) | `System.String name, System.Int32 count = 0` | | Body | メソッドボディまたはフィールド初期化子の IL が変更された場合 `Changed`、それ以外は空 | `Changed` | [`ShouldIncludeAssemblySemanticChangesInReport`](#config-ja-shouldincludeassemblysemanticchangesinreport)(既定値: `true`)で制御します。 diff --git a/Services/AssemblyMethodAnalyzer.cs b/Services/AssemblyMethodAnalyzer.cs index 697e389f..6472bde9 100644 --- a/Services/AssemblyMethodAnalyzer.cs +++ b/Services/AssemblyMethodAnalyzer.cs @@ -35,16 +35,24 @@ internal static class AssemblyMethodAnalyzer var entries = new List(); + // Helper: look up BaseType string for a given type name from the appropriate snapshot + string LookupBaseType(string typeName, AssemblySnapshot preferred, AssemblySnapshot fallback) + { + if (preferred.TypeNames.TryGetValue(typeName, out var info)) return info.BaseType; + if (fallback.TypeNames.TryGetValue(typeName, out info)) return info.BaseType; + return ""; + } + // Types foreach (var t in newSnapshot.TypeNames.Keys.Except(oldSnapshot.TypeNames.Keys, StringComparer.Ordinal).OrderBy(t => t, StringComparer.Ordinal)) { var info = newSnapshot.TypeNames[t]; - entries.Add(new MemberChangeEntry("Added", t, info.Access, "", info.Kind, "", "", "", "", "")); + entries.Add(new MemberChangeEntry("Added", t, info.BaseType, info.Access, info.Modifiers, info.Kind, "", "", "", "", "")); } foreach (var t in oldSnapshot.TypeNames.Keys.Except(newSnapshot.TypeNames.Keys, StringComparer.Ordinal).OrderBy(t => t, StringComparer.Ordinal)) { var info = oldSnapshot.TypeNames[t]; - entries.Add(new MemberChangeEntry("Removed", t, info.Access, "", info.Kind, "", "", "", "", "")); + entries.Add(new MemberChangeEntry("Removed", t, info.BaseType, info.Access, info.Modifiers, info.Kind, "", "", "", "", "")); } // Methods (including constructors) @@ -52,13 +60,13 @@ internal static class AssemblyMethodAnalyzer { var m = newSnapshot.Methods[key]; string kind = ToMemberKind(m.MethodName); - entries.Add(new MemberChangeEntry("Added", m.TypeName, m.Access, m.Modifiers, kind, ToCSharpMethodName(m.MethodName, m.TypeName), "", m.ReturnType, m.Parameters, "")); + entries.Add(new MemberChangeEntry("Added", m.TypeName, LookupBaseType(m.TypeName, newSnapshot, oldSnapshot), m.Access, m.Modifiers, kind, ToCSharpMethodName(m.MethodName, m.TypeName), "", m.ReturnType, m.Parameters, "")); } foreach (var key in oldSnapshot.Methods.Keys.Except(newSnapshot.Methods.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var m = oldSnapshot.Methods[key]; string kind = ToMemberKind(m.MethodName); - entries.Add(new MemberChangeEntry("Removed", m.TypeName, m.Access, m.Modifiers, kind, ToCSharpMethodName(m.MethodName, m.TypeName), "", m.ReturnType, m.Parameters, "")); + entries.Add(new MemberChangeEntry("Removed", m.TypeName, LookupBaseType(m.TypeName, oldSnapshot, newSnapshot), m.Access, m.Modifiers, kind, ToCSharpMethodName(m.MethodName, m.TypeName), "", m.ReturnType, m.Parameters, "")); } foreach (var key in oldSnapshot.Methods.Keys.Intersect(newSnapshot.Methods.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { @@ -66,7 +74,7 @@ internal static class AssemblyMethodAnalyzer { var m = newSnapshot.Methods[key]; string kind = ToMemberKind(m.MethodName); - entries.Add(new MemberChangeEntry("Modified", m.TypeName, m.Access, m.Modifiers, kind, ToCSharpMethodName(m.MethodName, m.TypeName), "", m.ReturnType, m.Parameters, "Changed")); + entries.Add(new MemberChangeEntry("Modified", m.TypeName, LookupBaseType(m.TypeName, newSnapshot, oldSnapshot), m.Access, m.Modifiers, kind, ToCSharpMethodName(m.MethodName, m.TypeName), "", m.ReturnType, m.Parameters, "Changed")); } } @@ -74,26 +82,34 @@ internal static class AssemblyMethodAnalyzer foreach (var key in newSnapshot.Properties.Keys.Except(oldSnapshot.Properties.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var p = newSnapshot.Properties[key]; - entries.Add(new MemberChangeEntry("Added", p.TypeName, p.Access, p.Modifiers, "Property", p.PropertyName, p.PropertyType, "", "", "")); + entries.Add(new MemberChangeEntry("Added", p.TypeName, LookupBaseType(p.TypeName, newSnapshot, oldSnapshot), p.Access, p.Modifiers, "Property", p.PropertyName, p.PropertyType, "", "", "")); } foreach (var key in oldSnapshot.Properties.Keys.Except(newSnapshot.Properties.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var p = oldSnapshot.Properties[key]; - entries.Add(new MemberChangeEntry("Removed", p.TypeName, p.Access, p.Modifiers, "Property", p.PropertyName, p.PropertyType, "", "", "")); + entries.Add(new MemberChangeEntry("Removed", p.TypeName, LookupBaseType(p.TypeName, oldSnapshot, newSnapshot), p.Access, p.Modifiers, "Property", p.PropertyName, p.PropertyType, "", "", "")); } // Fields foreach (var key in newSnapshot.Fields.Keys.Except(oldSnapshot.Fields.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var f = newSnapshot.Fields[key]; - entries.Add(new MemberChangeEntry("Added", f.TypeName, f.Access, f.Modifiers, "Field", f.FieldName, StripColonPrefix(f.Details), "", "", "")); + entries.Add(new MemberChangeEntry("Added", f.TypeName, LookupBaseType(f.TypeName, newSnapshot, oldSnapshot), f.Access, f.Modifiers, "Field", f.FieldName, StripColonPrefix(f.Details), "", "", "")); } foreach (var key in oldSnapshot.Fields.Keys.Except(newSnapshot.Fields.Keys, StringComparer.Ordinal).OrderBy(k => k, StringComparer.Ordinal)) { var f = oldSnapshot.Fields[key]; - entries.Add(new MemberChangeEntry("Removed", f.TypeName, f.Access, f.Modifiers, "Field", f.FieldName, StripColonPrefix(f.Details), "", "", "")); + entries.Add(new MemberChangeEntry("Removed", f.TypeName, LookupBaseType(f.TypeName, oldSnapshot, newSnapshot), f.Access, f.Modifiers, "Field", f.FieldName, StripColonPrefix(f.Details), "", "", "")); } + // Sort: by TypeName, then by Change order (Added → Removed → Modified) + entries.Sort((a, b) => + { + int cmp = StringComparer.Ordinal.Compare(a.TypeName, b.TypeName); + if (cmp != 0) return cmp; + return ChangeOrder(a.Change).CompareTo(ChangeOrder(b.Change)); + }); + return new AssemblySemanticChangesSummary { Entries = entries, @@ -143,6 +159,8 @@ private sealed class TypeInfo { public required string Access { get; init; } public required string Kind { get; init; } + public required string BaseType { get; init; } + public required string Modifiers { get; init; } } private sealed class AssemblySnapshot @@ -173,7 +191,9 @@ private static AssemblySnapshot ReadAssemblySnapshot(string assemblyPath) string typeAccess = GetTypeAccessModifier(typeDef.Attributes); string typeKind = GetTypeKind(reader, typeDef); - snapshot.TypeNames[typeName] = new TypeInfo { Access = typeAccess, Kind = typeKind }; + string baseType = GetBaseTypeDisplayName(reader, typeDef); + string typeModifiers = GetTypeModifiers(typeDef.Attributes); + snapshot.TypeNames[typeName] = new TypeInfo { Access = typeAccess, Kind = typeKind, BaseType = baseType, Modifiers = typeModifiers }; // Methods foreach (var methodHandle in typeDef.GetMethods()) @@ -261,6 +281,10 @@ private static string GetFullTypeName(MetadataReader reader, TypeDefinition type return string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}"; } + /// Sort order for Change column: Added=0, Removed=1, Modified=2. / Change 列のソート順。 + private static int ChangeOrder(string change) + => change switch { "Added" => 0, "Removed" => 1, "Modified" => 2, _ => 3 }; + /// Determine the member kind from the IL method name: .ctor → Constructor, .cctor → StaticConstructor, else → Method. / IL メソッド名からメンバー種別を判定。 private static string ToMemberKind(string ilMethodName) => ilMethodName switch @@ -498,6 +522,24 @@ private static string GetTypeAccessModifier(System.Reflection.TypeAttributes att }; } + /// Extract non-access modifiers from type attributes (sealed, abstract, static). / TypeAttributes から非アクセス修飾子を取得。 + private static string GetTypeModifiers(System.Reflection.TypeAttributes attributes) + { + var parts = new List(); + bool isAbstract = (attributes & System.Reflection.TypeAttributes.Abstract) != 0; + bool isSealed = (attributes & System.Reflection.TypeAttributes.Sealed) != 0; + bool isInterface = (attributes & System.Reflection.TypeAttributes.Interface) != 0; + + if (isAbstract && isSealed && !isInterface) + parts.Add("static"); // static classes are abstract + sealed in IL + else if (isSealed && !isInterface) + parts.Add("sealed"); + else if (isAbstract && !isInterface) + parts.Add("abstract"); + + return string.Join(" ", parts); + } + /// /// Determine the type kind: Class, Record, Struct, Interface, or Enum. /// Record is detected heuristically by the presence of an EqualityContract property. @@ -532,6 +574,54 @@ private static string GetTypeKind(MetadataReader reader, TypeDefinition typeDef) return "Class"; } + /// + /// Get the base type and implemented interfaces display string for a type definition, + /// omitting trivial bases (System.Object, System.ValueType, System.Enum) since those + /// are implied by the type kind. Returns e.g. "MyApp.BaseController, System.IDisposable". + /// 型定義の基底型および実装インターフェースの表示文字列を取得。自明な基底型は省略。 + /// + private static string GetBaseTypeDisplayName(MetadataReader reader, TypeDefinition typeDef) + { + var parts = new List(); + + // Base type (skip trivial) + if (!typeDef.BaseType.IsNil) + { + string baseTypeName = GetBaseTypeName(reader, typeDef.BaseType); + if (baseTypeName is not ("System.Object" or "System.ValueType" or "System.Enum" or "")) + parts.Add(baseTypeName); + } + + // Implemented interfaces + foreach (var ifaceHandle in typeDef.GetInterfaceImplementations()) + { + var iface = reader.GetInterfaceImplementation(ifaceHandle); + string ifaceName = GetInterfaceTypeName(reader, iface.Interface); + if (!string.IsNullOrEmpty(ifaceName)) + parts.Add(ifaceName); + } + + return string.Join(", ", parts); + } + + /// Get the full name of an interface from its EntityHandle. + private static string GetInterfaceTypeName(MetadataReader reader, EntityHandle handle) + { + if (handle.Kind == HandleKind.TypeReference) + { + var typeRef = reader.GetTypeReference((TypeReferenceHandle)handle); + string ns = reader.GetString(typeRef.Namespace); + string name = reader.GetString(typeRef.Name); + return string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}"; + } + if (handle.Kind == HandleKind.TypeDefinition) + { + var typeDef = reader.GetTypeDefinition((TypeDefinitionHandle)handle); + return GetFullTypeName(reader, typeDef); + } + return ""; + } + /// Get the full name of a base type from its EntityHandle. private static string GetBaseTypeName(MetadataReader reader, EntityHandle baseTypeHandle) { @@ -660,24 +750,24 @@ internal sealed class SimpleSignatureTypeProvider : ISignatureTypeProvider typeCode switch { - PrimitiveTypeCode.Void => "void", - PrimitiveTypeCode.Boolean => "bool", - PrimitiveTypeCode.Char => "char", - PrimitiveTypeCode.SByte => "sbyte", - PrimitiveTypeCode.Byte => "byte", - PrimitiveTypeCode.Int16 => "short", - PrimitiveTypeCode.UInt16 => "ushort", - PrimitiveTypeCode.Int32 => "int", - PrimitiveTypeCode.UInt32 => "uint", - PrimitiveTypeCode.Int64 => "long", - PrimitiveTypeCode.UInt64 => "ulong", - PrimitiveTypeCode.Single => "float", - PrimitiveTypeCode.Double => "double", - PrimitiveTypeCode.String => "string", - PrimitiveTypeCode.Object => "object", - PrimitiveTypeCode.IntPtr => "nint", - PrimitiveTypeCode.UIntPtr => "nuint", - PrimitiveTypeCode.TypedReference => "TypedReference", + PrimitiveTypeCode.Void => "System.Void", + PrimitiveTypeCode.Boolean => "System.Boolean", + PrimitiveTypeCode.Char => "System.Char", + PrimitiveTypeCode.SByte => "System.SByte", + PrimitiveTypeCode.Byte => "System.Byte", + PrimitiveTypeCode.Int16 => "System.Int16", + PrimitiveTypeCode.UInt16 => "System.UInt16", + PrimitiveTypeCode.Int32 => "System.Int32", + PrimitiveTypeCode.UInt32 => "System.UInt32", + PrimitiveTypeCode.Int64 => "System.Int64", + PrimitiveTypeCode.UInt64 => "System.UInt64", + PrimitiveTypeCode.Single => "System.Single", + PrimitiveTypeCode.Double => "System.Double", + PrimitiveTypeCode.String => "System.String", + PrimitiveTypeCode.Object => "System.Object", + PrimitiveTypeCode.IntPtr => "System.IntPtr", + PrimitiveTypeCode.UIntPtr => "System.UIntPtr", + PrimitiveTypeCode.TypedReference => "System.TypedReference", _ => typeCode.ToString() }; diff --git a/Services/HtmlReport/HtmlReportGenerateService.Css.cs b/Services/HtmlReport/HtmlReportGenerateService.Css.cs index 3bfbb499..eaab10a9 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Css.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Css.cs @@ -139,6 +139,8 @@ private static string GetCss() table.semantic-changes-table { width: auto; border-collapse: collapse; margin: 4px 0; font-size: 12px; } table.semantic-changes-table th { padding: 3px 8px; border: 1px solid #ccc; background: #f6f8fa; font-size: 11px; text-align: left; white-space: nowrap; } table.semantic-changes-table td { padding: 2px 8px; border: 1px solid #e0e0e0; font-size: 12px; white-space: nowrap; } + table.semantic-changes-table td:nth-child(3), table.semantic-changes-table td:nth-child(4), + table.semantic-changes-table td:nth-child(5), table.semantic-changes-table td:nth-child(6) { text-align: center; } table.semantic-changes-table tr.group-cont td { border-top: none; }"; } } diff --git a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs index eef9f037..4fe53893 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs @@ -360,25 +360,26 @@ private void AppendAssemblySemanticChangesRow( if (summary.Entries.Count > 0) { contentBuilder.AppendLine(""); - contentBuilder.AppendLine(""); + contentBuilder.AppendLine(""); contentBuilder.AppendLine(""); string prevType = ""; foreach (var e in summary.Entries) { bool isCont = e.TypeName == prevType; string classTd = !isCont ? HtmlEncode(e.TypeName) : ""; + string baseTypeTd = !isCont ? HtmlEncode(e.BaseType) : ""; prevType = e.TypeName; string trOpen = isCont ? "" : ""; string accessTd = e.Access.Length > 0 ? $"{HtmlEncode(e.Access)}" : ""; string modifiersTd = e.Modifiers.Length > 0 ? $"{HtmlEncode(e.Modifiers)}" : ""; string bodyTd = e.Body.Length > 0 ? $"{HtmlEncode(e.Body)}" : ""; - contentBuilder.AppendLine($"{trOpen}"); + contentBuilder.AppendLine($"{trOpen}"); } contentBuilder.AppendLine("
ClassChangeKindAccessModifiersTypeNameReturnTypeParametersBody
ClassBaseTypeChangeKindAccessModifiersTypeNameReturnTypeParametersBody
{classTd}{HtmlEncode(e.Change)}{HtmlEncode(e.MemberKind)}{accessTd}{modifiersTd}{HtmlEncode(e.MemberType)}{HtmlEncode(e.MemberName)}{HtmlEncode(e.ReturnType)}{HtmlEncode(e.Parameters)}{bodyTd}
{classTd}{baseTypeTd}{HtmlEncode(e.Change)}{HtmlEncode(e.MemberKind)}{accessTd}{modifiersTd}{HtmlEncode(e.MemberType)}{HtmlEncode(e.MemberName)}{HtmlEncode(e.ReturnType)}{HtmlEncode(e.Parameters)}{bodyTd}
"); } else { - contentBuilder.AppendLine("

Other changes only. See IL diff for details.

"); + contentBuilder.AppendLine("

No structural changes detected. See IL diff for implementation-level differences.

"); } if (summary.Entries.Count > 0) @@ -423,17 +424,20 @@ private static void AppendSummaryCountTable(StringBuilder sb, AssemblySemanticCh sb.AppendLine("ClassChangeCount"); sb.AppendLine(""); string prevType = ""; - foreach (var ((typeName, change), count) in counts.OrderBy(kv => kv.Key.TypeName, StringComparer.Ordinal).ThenBy(kv => kv.Key.Change, StringComparer.Ordinal)) + foreach (var ((typeName, change), count) in counts.OrderBy(kv => kv.Key.TypeName, StringComparer.Ordinal).ThenBy(kv => ChangeOrder(kv.Key.Change))) { bool isCont = typeName == prevType; string classTd = !isCont ? HtmlEncode(typeName) : ""; prevType = typeName; string trOpen = isCont ? "" : ""; - sb.AppendLine($"{trOpen}{classTd}{HtmlEncode(change)}{count}"); + sb.AppendLine($"{trOpen}{classTd}{HtmlEncode(change)}{count}"); } sb.AppendLine(""); } + private static int ChangeOrder(string change) + => change switch { "Added" => 0, "Removed" => 1, "Modified" => 2, _ => 3 }; + private void AppendSummarySection(StringBuilder sb, ConfigSettings config) { sb.AppendLine("

Summary

"); diff --git a/Services/ReportGenerateService.SectionWriters.cs b/Services/ReportGenerateService.SectionWriters.cs index c4d06bb5..0995b860 100644 --- a/Services/ReportGenerateService.SectionWriters.cs +++ b/Services/ReportGenerateService.SectionWriters.cs @@ -209,22 +209,24 @@ public void Write(StreamWriter writer, ReportWriteContext ctx) if (summary.Entries.Count > 0) { writer.WriteLine(); - writer.WriteLine("| Class | Change | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | Body |"); - writer.WriteLine("|-------|--------|------|--------|-----------|------|------|------------|------------|------|"); + writer.WriteLine("| Class | BaseType | Change | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | Body |"); + writer.WriteLine("|-------|----------|--------|------|--------|-----------|------|------|------------|------------|------|"); string prevType = ""; foreach (var e in summary.Entries) { - string classCol = e.TypeName != prevType ? EscapeMdTable(e.TypeName) : ""; + bool isCont = e.TypeName == prevType; + string classCol = !isCont ? EscapeMdTable(e.TypeName) : ""; + string baseTypeCol = !isCont ? EscapeMdTable(e.BaseType) : ""; prevType = e.TypeName; string access = e.Access.Length > 0 ? $"`{EscapeMdTable(e.Access)}`" : ""; string modifiers = e.Modifiers.Length > 0 ? $"`{EscapeMdTable(e.Modifiers)}`" : ""; string body = e.Body.Length > 0 ? $"`{EscapeMdTable(e.Body)}`" : ""; - writer.WriteLine($"| {classCol} | `{EscapeMdTable(e.Change)}` | `{EscapeMdTable(e.MemberKind)}` | {access} | {modifiers} | {EscapeMdTable(e.MemberType)} | {EscapeMdTable(e.MemberName)} | {EscapeMdTable(e.ReturnType)} | {EscapeMdTable(e.Parameters)} | {body} |"); + writer.WriteLine($"| {classCol} | {baseTypeCol} | `{EscapeMdTable(e.Change)}` | `{EscapeMdTable(e.MemberKind)}` | {access} | {modifiers} | {EscapeMdTable(e.MemberType)} | {EscapeMdTable(e.MemberName)} | {EscapeMdTable(e.ReturnType)} | {EscapeMdTable(e.Parameters)} | {body} |"); } } else { - writer.WriteLine("- Other changes only. See IL diff for details."); + writer.WriteLine("- No structural changes detected. See IL diff for implementation-level differences."); } if (summary.Entries.Count > 0) @@ -249,7 +251,7 @@ private static void WriteSummaryCountTable(StreamWriter writer, AssemblySemantic } string prevType = ""; - foreach (var ((typeName, change), count) in counts.OrderBy(kv => kv.Key.TypeName, StringComparer.Ordinal).ThenBy(kv => kv.Key.Change, StringComparer.Ordinal)) + foreach (var ((typeName, change), count) in counts.OrderBy(kv => kv.Key.TypeName, StringComparer.Ordinal).ThenBy(kv => ChangeOrder(kv.Key.Change))) { string classCol = typeName != prevType ? EscapeMdTable(typeName) : ""; prevType = typeName; @@ -257,6 +259,10 @@ private static void WriteSummaryCountTable(StreamWriter writer, AssemblySemantic } } + private static int ChangeOrder(string change) + => change switch { "Added" => 0, "Removed" => 1, "Modified" => 2, _ => 3 }; + + /// Escape pipe characters for Markdown table cells. / Markdown テーブルセル用にパイプ文字をエスケープ。 private static string EscapeMdTable(string value) => value.Replace("|", "\\|"); } diff --git a/doc/samples/diff_report.html b/doc/samples/diff_report.html index 784db9c9..3e0359c3 100644 --- a/doc/samples/diff_report.html +++ b/doc/samples/diff_report.html @@ -137,6 +137,8 @@ table.semantic-changes-table { width: auto; border-collapse: collapse; margin: 4px 0; font-size: 12px; } table.semantic-changes-table th { padding: 3px 8px; border: 1px solid #ccc; background: #f6f8fa; font-size: 11px; text-align: left; white-space: nowrap; } table.semantic-changes-table td { padding: 2px 8px; border: 1px solid #e0e0e0; font-size: 12px; white-space: nowrap; } + table.semantic-changes-table td:nth-child(3), table.semantic-changes-table td:nth-child(4), + table.semantic-changes-table td:nth-child(5), table.semantic-changes-table td:nth-child(6) { text-align: center; } table.semantic-changes-table tr.group-cont td { border-top: none; } @@ -438,7 +440,7 @@

[ * ] Modified Files (9)

-
+
#3 Show assembly semantic changes
@@ -479,7 +481,7 @@

[ * ] Modified Files (9)

-
+
#5 Show assembly semantic changes
@@ -546,7 +548,7 @@

[ * ] Modified Files (9)

-
+
#9 Show assembly semantic changes
diff --git a/doc/samples/diff_report.md b/doc/samples/diff_report.md index 7717cfb5..e6c8511c 100644 --- a/doc/samples/diff_report.md +++ b/doc/samples/diff_report.md @@ -56,70 +56,73 @@ ### src/App.dll -| Class | Change | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | Body | -|-------|--------|------|--------|-----------|------|------|------------|------------|------| -| MyApp.Controllers.ApiController | `Added` | `Method` | `public` | | | HealthCheck | string | | | -| | `Added` | `Method` | `public` | | | GetUsers | System.Collections.Generic.IList\ | int page, int pageSize = 20 | | -| | `Removed` | `Method` | `public` | | | GetUsers | System.Collections.Generic.IList\ | int page | | -| | `Modified` | `Method` | `public` | `virtual` | | Search | System.Collections.Generic.IList\ | string query | `Changed` | -| | `Modified` | `Method` | `protected` | | | OnAuthorize | bool | MyApp.Models.UserContext ctx | `Changed` | -| MyApp.Services.DataService | `Modified` | `Method` | `internal` | | | RefreshCache | void | | `Changed` | -| | `Modified` | `Method` | `private` | | | ValidateConnection | bool | string connStr | `Changed` | -| | `Added` | `Property` | `public` | | int | CacheTimeout | | | | -| | `Added` | `Property` | `public` | | MyApp.Models.CachePolicy | Policy | | | | -| | `Added` | `Property` | `internal` | | MyApp.Services.IConnectionPool | ConnectionPool | | | | +| Class | BaseType | Change | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | Body | +|-------|----------|--------|------|--------|-----------|------|------|------------|------------|------| +| MyApp.Controllers.ApiController | MyApp.Controllers.BaseController, System.IDisposable | `Added` | `Method` | `public` | | | HealthCheck | System.String | | | +| | | `Added` | `Method` | `public` | | | GetUsers | System.Collections.Generic.IList\ | System.Int32 page, System.Int32 pageSize = 20 | | +| | | `Removed` | `Method` | `public` | | | GetUsers | System.Collections.Generic.IList\ | System.Int32 page | | +| | | `Modified` | `Method` | `public` | `virtual` | | Search | System.Collections.Generic.IList\ | System.String query | `Changed` | +| | | `Modified` | `Method` | `protected` | | | OnAuthorize | System.Boolean | MyApp.Models.UserContext ctx | `Changed` | +| MyApp.Services.DataService | | `Added` | `Property` | `public` | | System.Int32 | CacheTimeout | | | | +| | | `Added` | `Property` | `public` | | MyApp.Models.CachePolicy | Policy | | | | +| | | `Added` | `Property` | `internal` | | MyApp.Services.IConnectionPool | ConnectionPool | | | | +| | | `Modified` | `Method` | `internal` | | | RefreshCache | System.Void | | `Changed` | +| | | `Modified` | `Method` | `private` | | | ValidateConnection | System.Boolean | System.String connStr | `Changed` | | Class | Change | Count | |-------|--------|-------| | MyApp.Controllers.ApiController | `Added` | 2 | -| | `Modified` | 2 | | | `Removed` | 1 | +| | `Modified` | 2 | | MyApp.Services.DataService | `Added` | 3 | | | `Modified` | 2 | ### src/Service.dll -| Class | Change | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | Body | -|-------|--------|------|--------|-----------|------|------|------------|------------|------| -| MyApp.Services.NewValidator | `Added` | `Class` | `public` | | | | | | | -| | `Added` | `Constructor` | `public` | | | NewValidator | void | | | -| | `Added` | `Method` | `public` | | | Validate | bool | string input | | -| | `Added` | `Method` | `public` | | | Validate | bool | string input, MyApp.Models.ValidationOptions options | | -| | `Added` | `Method` | `private` | | | ParseInput | string | string raw | | -| | `Added` | `Property` | `public` | | MyApp.Models.ValidationResult | LastResult | | | | -| | `Added` | `Field` | `private` | `readonly` | string | _pattern | | | | -| MyApp.Services.OrderService | `Added` | `Method` | `public` | | | ValidateWithNewValidator | bool | string data | | -| | `Removed` | `Method` | `public` | `virtual` | | LegacyValidate | bool | string data | | -| | `Modified` | `Method` | `public` | | | ProcessOrder | void | int orderId | `Changed` | -| | `Modified` | `Method` | `internal` | `static` | | CalculateTotal | decimal | int qty, int price | `Changed` | -| | `Added` | `Property` | `protected` | | MyApp.Models.OrderContext | CurrentContext | | | | -| | `Added` | `Field` | `private` | `readonly` | MyApp.Models.UserRecord | _defaultUser | | | | -| MyApp.Services.LegacyHelper | `Removed` | `Class` | `internal` | | | | | | | -| | `Removed` | `Method` | `public` | | | Convert | string | object value | | -| | `Removed` | `Method` | `public` | `static` | | Format | string | string template, object[] args | | -| MyApp.Models.UserRecord | `Added` | `Record` | `public` | | | | | | | -| | `Added` | `Constructor` | `public` | | | UserRecord | void | string Name, int Age | | -| | `Added` | `Property` | `public` | | string | Name | | | | -| | `Added` | `Property` | `public` | | int | Age | | | | -| | `Added` | `Method` | `public` | `override` | | ToString | string | | | -| | `Added` | `Method` | `public` | `virtual` | | Equals | bool | object obj | | -| | `Added` | `Method` | `public` | `override` | | GetHashCode | int | | | -| MyApp.Models.UserDto | `Removed` | `Class` | `public` | | | | | | | -| | `Removed` | `Property` | `public` | | string | Name | | | | -| | `Removed` | `Property` | `public` | | int | Age | | | | +| Class | BaseType | Change | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | Body | +|-------|----------|--------|------|--------|-----------|------|------|------------|------------|------| +| MyApp.Models.UserDto | | `Removed` | `Class` | `public` | | | | | | | +| | | `Removed` | `Property` | `public` | | System.String | Name | | | | +| | | `Removed` | `Property` | `public` | | System.Int32 | Age | | | | +| MyApp.Models.UserRecord | | `Added` | `Record` | `public` | | | | | | | +| | | `Added` | `Constructor` | `public` | | | UserRecord | System.Void | System.String Name, System.Int32 Age | | +| | | `Added` | `Property` | `public` | | System.String | Name | | | | +| | | `Added` | `Property` | `public` | | System.Int32 | Age | | | | +| | | `Added` | `Method` | `public` | `override` | | ToString | System.String | | | +| | | `Added` | `Method` | `public` | `virtual` | | Equals | System.Boolean | System.Object obj | | +| | | `Added` | `Method` | `public` | `override` | | GetHashCode | System.Int32 | | | +| MyApp.Services.LegacyHelper | | `Removed` | `Class` | `internal` | | | | | | | +| | | `Removed` | `Method` | `public` | | | Convert | System.String | System.Object value | | +| | | `Removed` | `Method` | `public` | `static` | | Format | System.String | System.String template, System.Object[] args | | +| MyApp.Services.NewValidator | MyApp.Services.IValidator | `Added` | `Class` | `public` | `sealed` | | | | | | +| | | `Added` | `Constructor` | `public` | | | NewValidator | System.Void | | | +| | | `Added` | `Method` | `public` | | | Validate | System.Boolean | System.String input | | +| | | `Added` | `Method` | `public` | | | Validate | System.Boolean | System.String input, MyApp.Models.ValidationOptions options | | +| | | `Added` | `Method` | `private` | | | ParseInput | System.String | System.String raw | | +| | | `Added` | `Property` | `public` | | MyApp.Models.ValidationResult | LastResult | | | | +| | | `Added` | `Field` | `private` | `readonly` | System.String | _pattern | | | | +| MyApp.Services.IValidator | | `Added` | `Interface` | `public` | | | | | | | +| | | `Added` | `Method` | `public` | | | Validate | System.Boolean | System.String input | | +| MyApp.Services.OrderService | MyApp.Services.BaseService, MyApp.Services.IOrderProcessor | `Added` | `Method` | `public` | | | ValidateWithNewValidator | System.Boolean | System.String data | | +| | | `Added` | `Property` | `protected` | | MyApp.Models.OrderContext | CurrentContext | | | | +| | | `Added` | `Field` | `private` | `readonly` | MyApp.Models.UserRecord | _defaultUser | | | | +| | | `Removed` | `Method` | `public` | `virtual` | | LegacyValidate | System.Boolean | System.String data | | +| | | `Modified` | `Method` | `public` | | | ProcessOrder | System.Void | System.Int32 orderId | `Changed` | +| | | `Modified` | `Method` | `internal` | `static` | | CalculateTotal | System.Decimal | System.Int32 qty, System.Int32 price | `Changed` | | Class | Change | Count | |-------|--------|-------| | MyApp.Models.UserDto | `Removed` | 3 | | MyApp.Models.UserRecord | `Added` | 7 | +| MyApp.Services.IValidator | `Added` | 2 | | MyApp.Services.LegacyHelper | `Removed` | 3 | | MyApp.Services.NewValidator | `Added` | 7 | | MyApp.Services.OrderService | `Added` | 3 | -| | `Modified` | 2 | | | `Removed` | 1 | +| | `Modified` | 2 | ### util/Legacy.dll -- Other changes only. See IL diff for details. +- No structural changes detected. See IL diff for implementation-level differences. ## IL Cache Stats - Hits : 42 From 0af33f50eb3cdc0d18ffc6a4521465bb97e14289 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 20:13:25 +0000 Subject: [PATCH 21/36] Enhance semantic changes table: dark headers, checkboxes, resizable columns, markdown alignment - Add dark gray header background with white text for semantic changes tables - Add checkbox column to both detail and count tables (with localStorage save/restore) - Make Class, BaseType, Type, Name, ReturnType, Parameters, Body columns resizable - Fix group-cont border hiding (use border-top: hidden instead of none) - Add markdown table alignment (center Change/Kind/Access/Modifiers, right-align Count) - Add abstract class, static constants class, and enum examples to sample report - Update JS: lazy-render re-inits col resize handles and restores checkbox state - Add CSS variables for semantic table column widths with download/clear support https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- .../HtmlReportGenerateService.Css.cs | 41 ++++- .../HtmlReportGenerateService.Js.cs | 102 +++++++++---- .../HtmlReportGenerateService.Sections.cs | 49 +++++- .../ReportGenerateService.SectionWriters.cs | 4 +- doc/samples/diff_report.html | 141 ++++++++++++------ doc/samples/diff_report.md | 24 ++- 6 files changed, 265 insertions(+), 96 deletions(-) diff --git a/Services/HtmlReport/HtmlReportGenerateService.Css.cs b/Services/HtmlReport/HtmlReportGenerateService.Css.cs index eaab10a9..818916d2 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Css.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Css.cs @@ -53,7 +53,9 @@ private static string GetCss() .save-status { font-size: 12px; color: #86868b; } .empty { color: #999; font-size: 12px; margin-bottom: 0.8rem; } /* ── Column width CSS variables ──────────────────────────────────────── */ - :root { --col-reason-w: 10em; --col-notes-w: 10em; --col-path-w: 22em; --col-diff-w: 9em; --col-disasm-w: 28em; } + :root { --col-reason-w: 10em; --col-notes-w: 10em; --col-path-w: 22em; --col-diff-w: 9em; --col-disasm-w: 28em; + --sc-class-w: 14em; --sc-basetype-w: 16em; --sc-type-w: 12em; + --sc-name-w: 10em; --sc-rettype-w: 12em; --sc-params-w: 18em; --sc-body-w: 5em; } col.col-no-g { width: 3.2em; } col.col-cb-g { width: 2.2em; } col.col-reason-g { width: var(--col-reason-w); } @@ -134,14 +136,37 @@ private static string GetCss() p.diff-skipped { color: #735c0f; font-size: 12px; padding: 4px 8px; background: #fffbdd; margin: 0; } /* ── Assembly semantic changes ─────────────────────────────────────── */ - .semantic-changes { padding: 6px 12px; font-size: 12px; } + .semantic-changes { padding: 6px 12px; font-size: 12px; overflow-x: auto; } .semantic-changes p { margin: 4px 0 2px; } - table.semantic-changes-table { width: auto; border-collapse: collapse; margin: 4px 0; font-size: 12px; } - table.semantic-changes-table th { padding: 3px 8px; border: 1px solid #ccc; background: #f6f8fa; font-size: 11px; text-align: left; white-space: nowrap; } - table.semantic-changes-table td { padding: 2px 8px; border: 1px solid #e0e0e0; font-size: 12px; white-space: nowrap; } - table.semantic-changes-table td:nth-child(3), table.semantic-changes-table td:nth-child(4), - table.semantic-changes-table td:nth-child(5), table.semantic-changes-table td:nth-child(6) { text-align: center; } - table.semantic-changes-table tr.group-cont td { border-top: none; }"; + table.semantic-changes-table { border-collapse: collapse; margin: 4px 0; font-size: 12px; table-layout: fixed; width: 1px; } + table.semantic-changes-table th { padding: 3px 8px; border: 1px solid #555; background: #2c2c2e; color: #fff; font-size: 11px; text-align: left; white-space: nowrap; } + table.semantic-changes-table th.th-resizable { position: relative; } + table.semantic-changes-table td { padding: 2px 8px; border: 1px solid #e0e0e0; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + table.semantic-changes-table td.sc-col-cb { width: 2.2em; text-align: center; } + /* sc-detail: checkbox(1) Class(2) BaseType(3) Change(4) Kind(5) Access(6) Modifiers(7) Type(8)… */ + table.semantic-changes-table.sc-detail td:nth-child(4), table.semantic-changes-table.sc-detail td:nth-child(5), + table.semantic-changes-table.sc-detail td:nth-child(6), table.semantic-changes-table.sc-detail td:nth-child(7) { text-align: center; } + /* sc-count: checkbox(1) Class(2) Change(3) Count(4) */ + table.semantic-changes-table.sc-count td:nth-child(3) { text-align: center; } + table.semantic-changes-table.sc-count td:last-child { text-align: right; } + table.semantic-changes-table tr.group-cont td:nth-child(2) { border-top: hidden; } + table.semantic-changes-table.sc-detail tr.group-cont td:nth-child(3) { border-top: hidden; } + /* sc colgroup widths */ + col.sc-col-cb-g { width: 2.2em; } + col.sc-col-class-g { width: var(--sc-class-w); } + col.sc-col-basetype-g { width: var(--sc-basetype-w); } + col.sc-col-change-g { width: 5.5em; } + col.sc-col-kind-g { width: 7em; } + col.sc-col-access-g { width: 5.5em; } + col.sc-col-mods-g { width: 6em; } + col.sc-col-type-g { width: var(--sc-type-w); } + col.sc-col-name-g { width: var(--sc-name-w); } + col.sc-col-rettype-g { width: var(--sc-rettype-w); } + col.sc-col-params-g { width: var(--sc-params-w); } + col.sc-col-body-g { width: var(--sc-body-w); } + col.sc-cnt-class-g { width: var(--sc-class-w); } + col.sc-cnt-change-g { width: 5.5em; } + col.sc-cnt-count-g { width: 4em; }"; } } } diff --git a/Services/HtmlReport/HtmlReportGenerateService.Js.cs b/Services/HtmlReport/HtmlReportGenerateService.Js.cs index 787427ed..b28e65a0 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Js.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Js.cs @@ -68,7 +68,7 @@ private static void AppendJs(StringBuilder sb, string storageKey, string reportD sb.AppendLine(" var openDetails = Array.from(document.querySelectorAll('details[open]'));"); sb.AppendLine(" openDetails.forEach(function(d){ d.removeAttribute('open'); });"); sb.AppendLine(" // 2. Capture current effective column widths to bake into reviewed HTML as defaults"); - sb.AppendLine(" var colVarNames = ['--col-reason-w','--col-notes-w','--col-path-w','--col-diff-w','--col-disasm-w'];"); + sb.AppendLine(" var colVarNames = ['--col-reason-w','--col-notes-w','--col-path-w','--col-diff-w','--col-disasm-w','--sc-class-w','--sc-basetype-w','--sc-type-w','--sc-name-w','--sc-rettype-w','--sc-params-w','--sc-body-w'];"); sb.AppendLine(" var cs = getComputedStyle(root);"); sb.AppendLine(" var curWidths = {};"); sb.AppendLine(" colVarNames.forEach(function(v){ curWidths[v] = (root.style.getPropertyValue(v) || cs.getPropertyValue(v)).trim(); });"); @@ -87,7 +87,14 @@ private static void AppendJs(StringBuilder sb, string storageKey, string reportD sb.AppendLine(" + '; --col-notes-w: ' + curWidths['--col-notes-w']"); sb.AppendLine(" + '; --col-path-w: ' + curWidths['--col-path-w']"); sb.AppendLine(" + '; --col-diff-w: ' + curWidths['--col-diff-w']"); - sb.AppendLine(" + '; --col-disasm-w: ' + curWidths['--col-disasm-w'] + '; }');"); + sb.AppendLine(" + '; --col-disasm-w: ' + curWidths['--col-disasm-w']"); + sb.AppendLine(" + '; --sc-class-w: ' + curWidths['--sc-class-w']"); + sb.AppendLine(" + '; --sc-basetype-w: ' + curWidths['--sc-basetype-w']"); + sb.AppendLine(" + '; --sc-type-w: ' + curWidths['--sc-type-w']"); + sb.AppendLine(" + '; --sc-name-w: ' + curWidths['--sc-name-w']"); + sb.AppendLine(" + '; --sc-rettype-w: ' + curWidths['--sc-rettype-w']"); + sb.AppendLine(" + '; --sc-params-w: ' + curWidths['--sc-params-w']"); + sb.AppendLine(" + '; --sc-body-w: ' + curWidths['--sc-body-w'] + '; }');"); sb.AppendLine(" // Remove inline col-var overrides from element (now baked into :root)"); sb.AppendLine(" html = html.replace(/(]*?) style=\"[^\"]*\"/, '$1');"); sb.AppendLine(" // Replace controls bar with reviewed banner"); @@ -109,7 +116,7 @@ private static void AppendJs(StringBuilder sb, string storageKey, string reportD sb.AppendLine(" document.querySelectorAll('input[type=\"text\"], textarea').forEach(function(inp){ inp.value=''; });"); sb.AppendLine(" // Reset column widths to defaults"); sb.AppendLine(" var root = document.documentElement;"); - sb.AppendLine(" ['--col-reason-w','--col-notes-w','--col-path-w','--col-diff-w','--col-disasm-w'].forEach(function(v){ root.style.removeProperty(v); });"); + sb.AppendLine(" ['--col-reason-w','--col-notes-w','--col-path-w','--col-diff-w','--col-disasm-w','--sc-class-w','--sc-basetype-w','--sc-type-w','--sc-name-w','--sc-rettype-w','--sc-params-w','--sc-body-w'].forEach(function(v){ root.style.removeProperty(v); });"); sb.AppendLine(" syncTableWidths();"); sb.AppendLine(" // Close all open diff/IL-diff details"); sb.AppendLine(" document.querySelectorAll('details[open]').forEach(function(d){ d.removeAttribute('open'); });"); @@ -135,7 +142,33 @@ private static void AppendJs(StringBuilder sb, string storageKey, string reportD sb.AppendLine(" if (!b64) return;"); sb.AppendLine(" d.removeAttribute('data-diff-html');"); sb.AppendLine(" d.removeEventListener('toggle', onToggle);"); - sb.AppendLine(" try { d.insertAdjacentHTML('beforeend', decodeDiffHtml(b64)); } catch(e) {}"); + sb.AppendLine(" try {"); + sb.AppendLine(" d.insertAdjacentHTML('beforeend', decodeDiffHtml(b64));"); + sb.AppendLine(" // Re-init column resize handles on newly rendered tables"); + sb.AppendLine(" d.querySelectorAll('th.th-resizable').forEach(function(th) {"); + sb.AppendLine(" if (th.querySelector('.col-resize-handle')) return;"); + sb.AppendLine(" initColResizeSingle(th);"); + sb.AppendLine(" });"); + sb.AppendLine(" // Wire up save events on new checkboxes"); + sb.AppendLine(" if (__savedState__ === null) {"); + sb.AppendLine(" d.querySelectorAll('input').forEach(function(el) {"); + sb.AppendLine(" el.addEventListener('change', autoSave);"); + sb.AppendLine(" });"); + sb.AppendLine(" }"); + sb.AppendLine(" // Restore state for new inputs"); + sb.AppendLine(" var toRestore = __savedState__ || JSON.parse(localStorage.getItem(__storageKey__) || 'null');"); + sb.AppendLine(" if (toRestore) {"); + sb.AppendLine(" d.querySelectorAll('input[id]').forEach(function(el) {"); + sb.AppendLine(" if (el.id in toRestore) {"); + sb.AppendLine(" if (el.type === 'checkbox') el.checked = Boolean(toRestore[el.id]);"); + sb.AppendLine(" else el.value = String(toRestore[el.id] || '');"); + sb.AppendLine(" }"); + sb.AppendLine(" });"); + sb.AppendLine(" }"); + sb.AppendLine(" if (__savedState__ !== null) {"); + sb.AppendLine(" d.querySelectorAll('input[type=\"checkbox\"]').forEach(function(cb){ cb.style.pointerEvents='none'; cb.style.cursor='default'; });"); + sb.AppendLine(" }"); + sb.AppendLine(" } catch(e) {}"); sb.AppendLine(" });"); sb.AppendLine(" });"); sb.AppendLine(" }"); @@ -151,41 +184,44 @@ private static void AppendJs(StringBuilder sb, string storageKey, string reportD sb.AppendLine(" + px('--col-reason-w', 10) + px('--col-notes-w', 10)"); sb.AppendLine(" + px('--col-path-w', 22) + px('--col-diff-w', 9)"); sb.AppendLine(" + px('--col-disasm-w', 28);"); - sb.AppendLine(" document.querySelectorAll('table:not(.stat-table):not(.diff-table)').forEach(function(t) {"); + sb.AppendLine(" document.querySelectorAll('table:not(.stat-table):not(.diff-table):not(.semantic-changes-table)').forEach(function(t) {"); sb.AppendLine(" t.style.width = w + 'px';"); sb.AppendLine(" });"); sb.AppendLine(" }"); sb.AppendLine(); + sb.AppendLine(" function initColResizeSingle(th) {"); + sb.AppendLine(" var label = document.createElement('span');"); + sb.AppendLine(" label.className = 'th-label';"); + sb.AppendLine(" while (th.childNodes.length) label.appendChild(th.childNodes[0]);"); + sb.AppendLine(" th.appendChild(label);"); + sb.AppendLine(" var handle = document.createElement('div');"); + sb.AppendLine(" handle.className = 'col-resize-handle';"); + sb.AppendLine(" th.appendChild(handle);"); + sb.AppendLine(" var varName = th.dataset.colVar;"); + sb.AppendLine(" handle.addEventListener('mousedown', function(e) {"); + sb.AppendLine(" e.preventDefault();"); + sb.AppendLine(" var startX = e.clientX;"); + sb.AppendLine(" var root = document.documentElement;"); + sb.AppendLine(" var emPx = parseFloat(getComputedStyle(root).fontSize) || 16;"); + sb.AppendLine(" var cur = root.style.getPropertyValue(varName) || getComputedStyle(root).getPropertyValue(varName);"); + sb.AppendLine(" var startPx = (parseFloat(cur) || 10) * emPx;"); + sb.AppendLine(" function onMove(ev) {"); + sb.AppendLine(" var newPx = Math.max(48, startPx + (ev.clientX - startX));"); + sb.AppendLine(" root.style.setProperty(varName, (newPx / emPx).toFixed(2) + 'em');"); + sb.AppendLine(" syncTableWidths();"); + sb.AppendLine(" }"); + sb.AppendLine(" function onUp() {"); + sb.AppendLine(" document.removeEventListener('mousemove', onMove);"); + sb.AppendLine(" document.removeEventListener('mouseup', onUp);"); + sb.AppendLine(" }"); + sb.AppendLine(" document.addEventListener('mousemove', onMove);"); + sb.AppendLine(" document.addEventListener('mouseup', onUp);"); + sb.AppendLine(" });"); + sb.AppendLine(" }"); + sb.AppendLine(); sb.AppendLine(" function initColResize() {"); sb.AppendLine(" document.querySelectorAll('th.th-resizable').forEach(function(th) {"); - sb.AppendLine(" // Wrap text in a block span so overflow:hidden clips reliably at column boundary"); - sb.AppendLine(" var label = document.createElement('span');"); - sb.AppendLine(" label.className = 'th-label';"); - sb.AppendLine(" while (th.childNodes.length) label.appendChild(th.childNodes[0]);"); - sb.AppendLine(" th.appendChild(label);"); - sb.AppendLine(" var handle = document.createElement('div');"); - sb.AppendLine(" handle.className = 'col-resize-handle';"); - sb.AppendLine(" th.appendChild(handle);"); - sb.AppendLine(" var varName = th.dataset.colVar;"); - sb.AppendLine(" handle.addEventListener('mousedown', function(e) {"); - sb.AppendLine(" e.preventDefault();"); - sb.AppendLine(" var startX = e.clientX;"); - sb.AppendLine(" var root = document.documentElement;"); - sb.AppendLine(" var emPx = parseFloat(getComputedStyle(root).fontSize) || 16;"); - sb.AppendLine(" var cur = root.style.getPropertyValue(varName) || getComputedStyle(root).getPropertyValue(varName);"); - sb.AppendLine(" var startPx = (parseFloat(cur) || 10) * emPx;"); - sb.AppendLine(" function onMove(ev) {"); - sb.AppendLine(" var newPx = Math.max(48, startPx + (ev.clientX - startX));"); - sb.AppendLine(" root.style.setProperty(varName, (newPx / emPx).toFixed(2) + 'em');"); - sb.AppendLine(" syncTableWidths();"); - sb.AppendLine(" }"); - sb.AppendLine(" function onUp() {"); - sb.AppendLine(" document.removeEventListener('mousemove', onMove);"); - sb.AppendLine(" document.removeEventListener('mouseup', onUp);"); - sb.AppendLine(" }"); - sb.AppendLine(" document.addEventListener('mousemove', onMove);"); - sb.AppendLine(" document.addEventListener('mouseup', onUp);"); - sb.AppendLine(" });"); + sb.AppendLine(" initColResizeSingle(th);"); sb.AppendLine(" });"); sb.AppendLine(" }"); sb.AppendLine(""); diff --git a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs index 4fe53893..139b5b2f 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs @@ -359,10 +359,35 @@ private void AppendAssemblySemanticChangesRow( if (summary.Entries.Count > 0) { - contentBuilder.AppendLine(""); - contentBuilder.AppendLine(""); + contentBuilder.AppendLine("
ClassBaseTypeChangeKindAccessModifiersTypeNameReturnTypeParametersBody
"); + contentBuilder.AppendLine(""); + contentBuilder.AppendLine(" "); + contentBuilder.AppendLine(" "); + contentBuilder.AppendLine(" "); + contentBuilder.AppendLine(" "); + contentBuilder.AppendLine(" "); + contentBuilder.AppendLine(" "); + contentBuilder.AppendLine(" "); + contentBuilder.AppendLine(" "); + contentBuilder.AppendLine(" "); + contentBuilder.AppendLine(" "); + contentBuilder.AppendLine(" "); + contentBuilder.AppendLine(" "); + contentBuilder.AppendLine(""); + contentBuilder.AppendLine(""); + contentBuilder.AppendLine(" "); + contentBuilder.AppendLine(" "); + contentBuilder.AppendLine(" "); + contentBuilder.AppendLine(" "); + contentBuilder.AppendLine(" "); + contentBuilder.AppendLine(" "); + contentBuilder.AppendLine(" "); + contentBuilder.AppendLine(" "); + contentBuilder.AppendLine(" "); + contentBuilder.AppendLine(""); contentBuilder.AppendLine(""); string prevType = ""; + int scRowIdx = 0; foreach (var e in summary.Entries) { bool isCont = e.TypeName == prevType; @@ -373,7 +398,9 @@ private void AppendAssemblySemanticChangesRow( string accessTd = e.Access.Length > 0 ? $"{HtmlEncode(e.Access)}" : ""; string modifiersTd = e.Modifiers.Length > 0 ? $"{HtmlEncode(e.Modifiers)}" : ""; string bodyTd = e.Body.Length > 0 ? $"{HtmlEncode(e.Body)}" : ""; - contentBuilder.AppendLine($"{trOpen}"); + string cbId = $"sc_{sectionPrefix}_{idx}_{scRowIdx}"; + contentBuilder.AppendLine($"{trOpen}"); + scRowIdx++; } contentBuilder.AppendLine("
ClassBaseTypeChangeKindAccessModifiersTypeNameReturnTypeParametersBody
{classTd}{baseTypeTd}{HtmlEncode(e.Change)}{HtmlEncode(e.MemberKind)}{accessTd}{modifiersTd}{HtmlEncode(e.MemberType)}{HtmlEncode(e.MemberName)}{HtmlEncode(e.ReturnType)}{HtmlEncode(e.Parameters)}{bodyTd}
{classTd}{baseTypeTd}{HtmlEncode(e.Change)}{HtmlEncode(e.MemberKind)}{accessTd}{modifiersTd}{HtmlEncode(e.MemberType)}{HtmlEncode(e.MemberName)}{HtmlEncode(e.ReturnType)}{HtmlEncode(e.Parameters)}{bodyTd}
"); } @@ -420,8 +447,18 @@ private static void AppendSummaryCountTable(StringBuilder sb, AssemblySemanticCh counts[key] = counts.TryGetValue(key, out int c) ? c + 1 : 1; } - sb.AppendLine(""); - sb.AppendLine(""); + sb.AppendLine("
ClassChangeCount
"); + sb.AppendLine(""); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine(""); sb.AppendLine(""); string prevType = ""; foreach (var ((typeName, change), count) in counts.OrderBy(kv => kv.Key.TypeName, StringComparer.Ordinal).ThenBy(kv => ChangeOrder(kv.Key.Change))) @@ -430,7 +467,7 @@ private static void AppendSummaryCountTable(StringBuilder sb, AssemblySemanticCh string classTd = !isCont ? HtmlEncode(typeName) : ""; prevType = typeName; string trOpen = isCont ? "" : ""; - sb.AppendLine($"{trOpen}"); + sb.AppendLine($"{trOpen}"); } sb.AppendLine("
ClassChangeCount
{classTd}{HtmlEncode(change)}{count}
{classTd}{HtmlEncode(change)}{count}
"); } diff --git a/Services/ReportGenerateService.SectionWriters.cs b/Services/ReportGenerateService.SectionWriters.cs index 0995b860..f93635f1 100644 --- a/Services/ReportGenerateService.SectionWriters.cs +++ b/Services/ReportGenerateService.SectionWriters.cs @@ -210,7 +210,7 @@ public void Write(StreamWriter writer, ReportWriteContext ctx) { writer.WriteLine(); writer.WriteLine("| Class | BaseType | Change | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | Body |"); - writer.WriteLine("|-------|----------|--------|------|--------|-----------|------|------|------------|------------|------|"); + writer.WriteLine("|-------|----------|:------:|:----:|:------:|:---------:|------|------|------------|------------|------|"); string prevType = ""; foreach (var e in summary.Entries) { @@ -233,7 +233,7 @@ public void Write(StreamWriter writer, ReportWriteContext ctx) { writer.WriteLine(); writer.WriteLine("| Class | Change | Count |"); - writer.WriteLine("|-------|--------|-------|"); + writer.WriteLine("|-------|:------:|------:|"); WriteSummaryCountTable(writer, summary); } } diff --git a/doc/samples/diff_report.html b/doc/samples/diff_report.html index 3e0359c3..f3167b03 100644 --- a/doc/samples/diff_report.html +++ b/doc/samples/diff_report.html @@ -51,7 +51,9 @@ .save-status { font-size: 12px; color: #86868b; } .empty { color: #999; font-size: 12px; margin-bottom: 0.8rem; } /* ── Column width CSS variables ──────────────────────────────────────── */ - :root { --col-reason-w: 10em; --col-notes-w: 10em; --col-path-w: 22em; --col-diff-w: 9em; --col-disasm-w: 28em; } + :root { --col-reason-w: 10em; --col-notes-w: 10em; --col-path-w: 22em; --col-diff-w: 9em; --col-disasm-w: 28em; + --sc-class-w: 14em; --sc-basetype-w: 16em; --sc-type-w: 12em; + --sc-name-w: 10em; --sc-rettype-w: 12em; --sc-params-w: 18em; --sc-body-w: 5em; } col.col-no-g { width: 3.2em; } col.col-cb-g { width: 2.2em; } col.col-reason-g { width: var(--col-reason-w); } @@ -131,15 +133,35 @@ td.diff-trunc-td { color: #735c0f; padding: 2px 8px; font-style: italic; } p.diff-skipped { color: #735c0f; font-size: 12px; padding: 4px 8px; background: #fffbdd; margin: 0; } - /* ── Method-level changes ──────────────────────────────────────────── */ - .semantic-changes { padding: 6px 12px; font-size: 12px; } + /* ── Assembly semantic changes ─────────────────────────────────────── */ + .semantic-changes { padding: 6px 12px; font-size: 12px; overflow-x: auto; } .semantic-changes p { margin: 4px 0 2px; } - table.semantic-changes-table { width: auto; border-collapse: collapse; margin: 4px 0; font-size: 12px; } - table.semantic-changes-table th { padding: 3px 8px; border: 1px solid #ccc; background: #f6f8fa; font-size: 11px; text-align: left; white-space: nowrap; } - table.semantic-changes-table td { padding: 2px 8px; border: 1px solid #e0e0e0; font-size: 12px; white-space: nowrap; } - table.semantic-changes-table td:nth-child(3), table.semantic-changes-table td:nth-child(4), - table.semantic-changes-table td:nth-child(5), table.semantic-changes-table td:nth-child(6) { text-align: center; } - table.semantic-changes-table tr.group-cont td { border-top: none; } + table.semantic-changes-table { border-collapse: collapse; margin: 4px 0; font-size: 12px; table-layout: fixed; width: 1px; } + table.semantic-changes-table th { padding: 3px 8px; border: 1px solid #555; background: #2c2c2e; color: #fff; font-size: 11px; text-align: left; white-space: nowrap; } + table.semantic-changes-table th.th-resizable { position: relative; } + table.semantic-changes-table td { padding: 2px 8px; border: 1px solid #e0e0e0; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + table.semantic-changes-table td.sc-col-cb { width: 2.2em; text-align: center; } + table.semantic-changes-table.sc-detail td:nth-child(4), table.semantic-changes-table.sc-detail td:nth-child(5), + table.semantic-changes-table.sc-detail td:nth-child(6), table.semantic-changes-table.sc-detail td:nth-child(7) { text-align: center; } + table.semantic-changes-table.sc-count td:nth-child(3) { text-align: center; } + table.semantic-changes-table.sc-count td:last-child { text-align: right; } + table.semantic-changes-table tr.group-cont td:nth-child(2) { border-top: hidden; } + table.semantic-changes-table.sc-detail tr.group-cont td:nth-child(3) { border-top: hidden; } + col.sc-col-cb-g { width: 2.2em; } + col.sc-col-class-g { width: var(--sc-class-w); } + col.sc-col-basetype-g { width: var(--sc-basetype-w); } + col.sc-col-change-g { width: 5.5em; } + col.sc-col-kind-g { width: 7em; } + col.sc-col-access-g { width: 5.5em; } + col.sc-col-mods-g { width: 6em; } + col.sc-col-type-g { width: var(--sc-type-w); } + col.sc-col-name-g { width: var(--sc-name-w); } + col.sc-col-rettype-g { width: var(--sc-rettype-w); } + col.sc-col-params-g { width: var(--sc-params-w); } + col.sc-col-body-g { width: var(--sc-body-w); } + col.sc-cnt-class-g { width: var(--sc-class-w); } + col.sc-cnt-change-g { width: 5.5em; } + col.sc-cnt-count-g { width: 4em; } @@ -440,7 +462,7 @@

[ * ] Modified Files (9)

-
+
#3 Show assembly semantic changes
@@ -481,7 +503,7 @@

[ * ] Modified Files (9)

-
+
#5 Show assembly semantic changes
@@ -701,7 +723,7 @@

[ ! ] Modified Files — Timestamps Regressed (2) var openDetails = Array.from(document.querySelectorAll('details[open]')); openDetails.forEach(function(d){ d.removeAttribute('open'); }); // 2. Capture current effective column widths to bake into reviewed HTML as defaults - var colVarNames = ['--col-reason-w','--col-notes-w','--col-path-w','--col-diff-w','--col-disasm-w']; + var colVarNames = ['--col-reason-w','--col-notes-w','--col-path-w','--col-diff-w','--col-disasm-w','--sc-class-w','--sc-basetype-w','--sc-type-w','--sc-name-w','--sc-rettype-w','--sc-params-w','--sc-body-w']; var cs = getComputedStyle(root); var curWidths = {}; colVarNames.forEach(function(v){ curWidths[v] = (root.style.getPropertyValue(v) || cs.getPropertyValue(v)).trim(); }); @@ -720,7 +742,14 @@

[ ! ] Modified Files — Timestamps Regressed (2) + '; --col-notes-w: ' + curWidths['--col-notes-w'] + '; --col-path-w: ' + curWidths['--col-path-w'] + '; --col-diff-w: ' + curWidths['--col-diff-w'] - + '; --col-disasm-w: ' + curWidths['--col-disasm-w'] + '; }'); + + '; --col-disasm-w: ' + curWidths['--col-disasm-w'] + + '; --sc-class-w: ' + curWidths['--sc-class-w'] + + '; --sc-basetype-w: ' + curWidths['--sc-basetype-w'] + + '; --sc-type-w: ' + curWidths['--sc-type-w'] + + '; --sc-name-w: ' + curWidths['--sc-name-w'] + + '; --sc-rettype-w: ' + curWidths['--sc-rettype-w'] + + '; --sc-params-w: ' + curWidths['--sc-params-w'] + + '; --sc-body-w: ' + curWidths['--sc-body-w'] + '; }'); // Remove inline col-var overrides from element (now baked into :root) html = html.replace(/(]*?) style="[^"]*"/, '$1'); // Replace controls bar with reviewed banner @@ -742,7 +771,7 @@

[ ! ] Modified Files — Timestamps Regressed (2) document.querySelectorAll('input[type="text"], textarea').forEach(function(inp){ inp.value=''; }); // Reset column widths to defaults var root = document.documentElement; - ['--col-reason-w','--col-notes-w','--col-path-w','--col-diff-w','--col-disasm-w'].forEach(function(v){ root.style.removeProperty(v); }); + ['--col-reason-w','--col-notes-w','--col-path-w','--col-diff-w','--col-disasm-w','--sc-class-w','--sc-basetype-w','--sc-type-w','--sc-name-w','--sc-rettype-w','--sc-params-w','--sc-body-w'].forEach(function(v){ root.style.removeProperty(v); }); syncTableWidths(); // Close all open diff/IL-diff details document.querySelectorAll('details[open]').forEach(function(d){ d.removeAttribute('open'); }); @@ -783,41 +812,67 @@

[ ! ] Modified Files — Timestamps Regressed (2) if (!b64) return; d.removeAttribute('data-diff-html'); d.removeEventListener('toggle', onToggle); - try { d.insertAdjacentHTML('beforeend', decodeDiffHtml(b64)); } catch(e) {} + try { + d.insertAdjacentHTML('beforeend', decodeDiffHtml(b64)); + d.querySelectorAll('th.th-resizable').forEach(function(th) { + if (th.querySelector('.col-resize-handle')) return; + initColResizeSingle(th); + }); + if (__savedState__ === null) { + d.querySelectorAll('input').forEach(function(el) { + el.addEventListener('change', autoSave); + }); + } + var toRestore = __savedState__ || JSON.parse(localStorage.getItem(__storageKey__) || 'null'); + if (toRestore) { + d.querySelectorAll('input[id]').forEach(function(el) { + if (el.id in toRestore) { + if (el.type === 'checkbox') el.checked = Boolean(toRestore[el.id]); + else el.value = String(toRestore[el.id] || ''); + } + }); + } + if (__savedState__ !== null) { + d.querySelectorAll('input[type="checkbox"]').forEach(function(cb){ cb.style.pointerEvents='none'; cb.style.cursor='default'; }); + } + } catch(e) {} }); }); } + function initColResizeSingle(th) { + var label = document.createElement('span'); + label.className = 'th-label'; + while (th.childNodes.length) label.appendChild(th.childNodes[0]); + th.appendChild(label); + var handle = document.createElement('div'); + handle.className = 'col-resize-handle'; + th.appendChild(handle); + var varName = th.dataset.colVar; + handle.addEventListener('mousedown', function(e) { + e.preventDefault(); + var startX = e.clientX; + var root = document.documentElement; + var emPx = parseFloat(getComputedStyle(root).fontSize) || 16; + var cur = root.style.getPropertyValue(varName) || getComputedStyle(root).getPropertyValue(varName); + var startPx = (parseFloat(cur) || 10) * emPx; + function onMove(ev) { + var newPx = Math.max(48, startPx + (ev.clientX - startX)); + root.style.setProperty(varName, (newPx / emPx).toFixed(2) + 'em'); + syncTableWidths(); + } + function onUp() { + document.removeEventListener('mousemove', onMove); + document.removeEventListener('mouseup', onUp); + } + document.addEventListener('mousemove', onMove); + document.addEventListener('mouseup', onUp); + }); + } + function initColResize() { document.querySelectorAll('th.th-resizable').forEach(function(th) { - // Wrap text in a block span so overflow:hidden clips reliably at column boundary - var label = document.createElement('span'); - label.className = 'th-label'; - while (th.childNodes.length) label.appendChild(th.childNodes[0]); - th.appendChild(label); - var handle = document.createElement('div'); - handle.className = 'col-resize-handle'; - th.appendChild(handle); - var varName = th.dataset.colVar; - handle.addEventListener('mousedown', function(e) { - e.preventDefault(); - var startX = e.clientX; - var root = document.documentElement; - var emPx = parseFloat(getComputedStyle(root).fontSize) || 16; - var cur = root.style.getPropertyValue(varName) || getComputedStyle(root).getPropertyValue(varName); - var startPx = (parseFloat(cur) || 10) * emPx; - function onMove(ev) { - var newPx = Math.max(48, startPx + (ev.clientX - startX)); - root.style.setProperty(varName, (newPx / emPx).toFixed(2) + 'em'); - syncTableWidths(); - } - function onUp() { - document.removeEventListener('mousemove', onMove); - document.removeEventListener('mouseup', onUp); - } - document.addEventListener('mousemove', onMove); - document.addEventListener('mouseup', onUp); - }); + initColResizeSingle(th); }); } diff --git a/doc/samples/diff_report.md b/doc/samples/diff_report.md index e6c8511c..e63f7b9d 100644 --- a/doc/samples/diff_report.md +++ b/doc/samples/diff_report.md @@ -57,7 +57,7 @@ ### src/App.dll | Class | BaseType | Change | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | Body | -|-------|----------|--------|------|--------|-----------|------|------|------------|------------|------| +|-------|----------|:------:|:----:|:------:|:---------:|------|------|------------|------------|------| | MyApp.Controllers.ApiController | MyApp.Controllers.BaseController, System.IDisposable | `Added` | `Method` | `public` | | | HealthCheck | System.String | | | | | | `Added` | `Method` | `public` | | | GetUsers | System.Collections.Generic.IList\ | System.Int32 page, System.Int32 pageSize = 20 | | | | | `Removed` | `Method` | `public` | | | GetUsers | System.Collections.Generic.IList\ | System.Int32 page | | @@ -70,7 +70,7 @@ | | | `Modified` | `Method` | `private` | | | ValidateConnection | System.Boolean | System.String connStr | `Changed` | | Class | Change | Count | -|-------|--------|-------| +|-------|:------:|------:| | MyApp.Controllers.ApiController | `Added` | 2 | | | `Removed` | 1 | | | `Modified` | 2 | @@ -80,7 +80,7 @@ ### src/Service.dll | Class | BaseType | Change | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | Body | -|-------|----------|--------|------|--------|-----------|------|------|------------|------------|------| +|-------|----------|:------:|:----:|:------:|:---------:|------|------|------------|------------|------| | MyApp.Models.UserDto | | `Removed` | `Class` | `public` | | | | | | | | | | `Removed` | `Property` | `public` | | System.String | Name | | | | | | | `Removed` | `Property` | `public` | | System.Int32 | Age | | | | @@ -103,6 +103,19 @@ | | | `Added` | `Field` | `private` | `readonly` | System.String | _pattern | | | | | MyApp.Services.IValidator | | `Added` | `Interface` | `public` | | | | | | | | | | `Added` | `Method` | `public` | | | Validate | System.Boolean | System.String input | | +| MyApp.Models.OrderStatus | | `Added` | `Enum` | `public` | | | | | | | +| | | `Added` | `Field` | `public` | `static literal` | System.Int32 | Pending | | | | +| | | `Added` | `Field` | `public` | `static literal` | System.Int32 | Processing | | | | +| | | `Added` | `Field` | `public` | `static literal` | System.Int32 | Completed | | | | +| | | `Added` | `Field` | `public` | `static literal` | System.Int32 | Cancelled | | | | +| MyApp.Services.BaseProcessor | | `Added` | `Class` | `public` | `abstract` | | | | | | +| | | `Added` | `Constructor` | `protected` | | | BaseProcessor | System.Void | | | +| | | `Added` | `Method` | `public` | `abstract` | | Execute | System.Threading.Tasks.Task | System.String input | | +| | | `Added` | `Method` | `protected` | `virtual` | | OnCompleted | System.Void | | | +| MyApp.Services.Constants | | `Added` | `Class` | `public` | `static` | | | | | | +| | | `Added` | `Field` | `public` | `static literal` | System.String | DefaultEndpoint | | | | +| | | `Added` | `Field` | `public` | `static literal` | System.Int32 | MaxRetries | | | | +| | | `Added` | `Field` | `public` | `static readonly` | System.TimeSpan | DefaultTimeout | | | | | MyApp.Services.OrderService | MyApp.Services.BaseService, MyApp.Services.IOrderProcessor | `Added` | `Method` | `public` | | | ValidateWithNewValidator | System.Boolean | System.String data | | | | | `Added` | `Property` | `protected` | | MyApp.Models.OrderContext | CurrentContext | | | | | | | `Added` | `Field` | `private` | `readonly` | MyApp.Models.UserRecord | _defaultUser | | | | @@ -111,9 +124,12 @@ | | | `Modified` | `Method` | `internal` | `static` | | CalculateTotal | System.Decimal | System.Int32 qty, System.Int32 price | `Changed` | | Class | Change | Count | -|-------|--------|-------| +|-------|:------:|------:| +| MyApp.Models.OrderStatus | `Added` | 5 | | MyApp.Models.UserDto | `Removed` | 3 | | MyApp.Models.UserRecord | `Added` | 7 | +| MyApp.Services.BaseProcessor | `Added` | 4 | +| MyApp.Services.Constants | `Added` | 4 | | MyApp.Services.IValidator | `Added` | 2 | | MyApp.Services.LegacyHelper | `Removed` | 3 | | MyApp.Services.NewValidator | `Added` | 7 | From c189fe4dfda16726c75d686cde5881bf2d56faf4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 21:18:56 +0000 Subject: [PATCH 22/36] Prevent line wrapping in BaseType, Type, ReturnType, Parameters columns of markdown report Replace spaces with non-breaking spaces (U+00A0) in these columns to keep long values like fully qualified type names and parameter lists on a single line when rendered by markdown viewers. https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- .../ReportGenerateService.SectionWriters.cs | 7 +++- doc/samples/diff_report.md | 38 +++++++++---------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/Services/ReportGenerateService.SectionWriters.cs b/Services/ReportGenerateService.SectionWriters.cs index f93635f1..28570c7d 100644 --- a/Services/ReportGenerateService.SectionWriters.cs +++ b/Services/ReportGenerateService.SectionWriters.cs @@ -216,12 +216,12 @@ public void Write(StreamWriter writer, ReportWriteContext ctx) { bool isCont = e.TypeName == prevType; string classCol = !isCont ? EscapeMdTable(e.TypeName) : ""; - string baseTypeCol = !isCont ? EscapeMdTable(e.BaseType) : ""; + string baseTypeCol = !isCont ? NoWrapMd(EscapeMdTable(e.BaseType)) : ""; prevType = e.TypeName; string access = e.Access.Length > 0 ? $"`{EscapeMdTable(e.Access)}`" : ""; string modifiers = e.Modifiers.Length > 0 ? $"`{EscapeMdTable(e.Modifiers)}`" : ""; string body = e.Body.Length > 0 ? $"`{EscapeMdTable(e.Body)}`" : ""; - writer.WriteLine($"| {classCol} | {baseTypeCol} | `{EscapeMdTable(e.Change)}` | `{EscapeMdTable(e.MemberKind)}` | {access} | {modifiers} | {EscapeMdTable(e.MemberType)} | {EscapeMdTable(e.MemberName)} | {EscapeMdTable(e.ReturnType)} | {EscapeMdTable(e.Parameters)} | {body} |"); + writer.WriteLine($"| {classCol} | {baseTypeCol} | `{EscapeMdTable(e.Change)}` | `{EscapeMdTable(e.MemberKind)}` | {access} | {modifiers} | {NoWrapMd(EscapeMdTable(e.MemberType))} | {EscapeMdTable(e.MemberName)} | {NoWrapMd(EscapeMdTable(e.ReturnType))} | {NoWrapMd(EscapeMdTable(e.Parameters))} | {body} |"); } } else @@ -265,6 +265,9 @@ private static int ChangeOrder(string change) /// Escape pipe characters for Markdown table cells. / Markdown テーブルセル用にパイプ文字をエスケープ。 private static string EscapeMdTable(string value) => value.Replace("|", "\\|"); + + /// Replace spaces with non-breaking spaces to prevent wrapping in Markdown table cells. / Markdown テーブルセル内の折り返しを防ぐためにスペースをノーブレークスペースに置換。 + private static string NoWrapMd(string value) => value.Replace(' ', '\u00A0'); } /// Writes the IL Cache Stats section (only when enabled and ilCache is non-null). / IL Cache Stats セクションを書き込みます。 diff --git a/doc/samples/diff_report.md b/doc/samples/diff_report.md index e63f7b9d..9d885205 100644 --- a/doc/samples/diff_report.md +++ b/doc/samples/diff_report.md @@ -58,16 +58,16 @@ | Class | BaseType | Change | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | Body | |-------|----------|:------:|:----:|:------:|:---------:|------|------|------------|------------|------| -| MyApp.Controllers.ApiController | MyApp.Controllers.BaseController, System.IDisposable | `Added` | `Method` | `public` | | | HealthCheck | System.String | | | -| | | `Added` | `Method` | `public` | | | GetUsers | System.Collections.Generic.IList\ | System.Int32 page, System.Int32 pageSize = 20 | | -| | | `Removed` | `Method` | `public` | | | GetUsers | System.Collections.Generic.IList\ | System.Int32 page | | -| | | `Modified` | `Method` | `public` | `virtual` | | Search | System.Collections.Generic.IList\ | System.String query | `Changed` | -| | | `Modified` | `Method` | `protected` | | | OnAuthorize | System.Boolean | MyApp.Models.UserContext ctx | `Changed` | +| MyApp.Controllers.ApiController | MyApp.Controllers.BaseController, System.IDisposable | `Added` | `Method` | `public` | | | HealthCheck | System.String | | | +| | | `Added` | `Method` | `public` | | | GetUsers | System.Collections.Generic.IList\ | System.Int32 page, System.Int32 pageSize = 20 | | +| | | `Removed` | `Method` | `public` | | | GetUsers | System.Collections.Generic.IList\ | System.Int32 page | | +| | | `Modified` | `Method` | `public` | `virtual` | | Search | System.Collections.Generic.IList\ | System.String query | `Changed` | +| | | `Modified` | `Method` | `protected` | | | OnAuthorize | System.Boolean | MyApp.Models.UserContext ctx | `Changed` | | MyApp.Services.DataService | | `Added` | `Property` | `public` | | System.Int32 | CacheTimeout | | | | | | | `Added` | `Property` | `public` | | MyApp.Models.CachePolicy | Policy | | | | | | | `Added` | `Property` | `internal` | | MyApp.Services.IConnectionPool | ConnectionPool | | | | | | | `Modified` | `Method` | `internal` | | | RefreshCache | System.Void | | `Changed` | -| | | `Modified` | `Method` | `private` | | | ValidateConnection | System.Boolean | System.String connStr | `Changed` | +| | | `Modified` | `Method` | `private` | | | ValidateConnection | System.Boolean | System.String connStr | `Changed` | | Class | Change | Count | |-------|:------:|------:| @@ -85,24 +85,24 @@ | | | `Removed` | `Property` | `public` | | System.String | Name | | | | | | | `Removed` | `Property` | `public` | | System.Int32 | Age | | | | | MyApp.Models.UserRecord | | `Added` | `Record` | `public` | | | | | | | -| | | `Added` | `Constructor` | `public` | | | UserRecord | System.Void | System.String Name, System.Int32 Age | | +| | | `Added` | `Constructor` | `public` | | | UserRecord | System.Void | System.String Name, System.Int32 Age | | | | | `Added` | `Property` | `public` | | System.String | Name | | | | | | | `Added` | `Property` | `public` | | System.Int32 | Age | | | | | | | `Added` | `Method` | `public` | `override` | | ToString | System.String | | | -| | | `Added` | `Method` | `public` | `virtual` | | Equals | System.Boolean | System.Object obj | | +| | | `Added` | `Method` | `public` | `virtual` | | Equals | System.Boolean | System.Object obj | | | | | `Added` | `Method` | `public` | `override` | | GetHashCode | System.Int32 | | | | MyApp.Services.LegacyHelper | | `Removed` | `Class` | `internal` | | | | | | | -| | | `Removed` | `Method` | `public` | | | Convert | System.String | System.Object value | | -| | | `Removed` | `Method` | `public` | `static` | | Format | System.String | System.String template, System.Object[] args | | +| | | `Removed` | `Method` | `public` | | | Convert | System.String | System.Object value | | +| | | `Removed` | `Method` | `public` | `static` | | Format | System.String | System.String template, System.Object[] args | | | MyApp.Services.NewValidator | MyApp.Services.IValidator | `Added` | `Class` | `public` | `sealed` | | | | | | | | | `Added` | `Constructor` | `public` | | | NewValidator | System.Void | | | -| | | `Added` | `Method` | `public` | | | Validate | System.Boolean | System.String input | | -| | | `Added` | `Method` | `public` | | | Validate | System.Boolean | System.String input, MyApp.Models.ValidationOptions options | | -| | | `Added` | `Method` | `private` | | | ParseInput | System.String | System.String raw | | +| | | `Added` | `Method` | `public` | | | Validate | System.Boolean | System.String input | | +| | | `Added` | `Method` | `public` | | | Validate | System.Boolean | System.String input, MyApp.Models.ValidationOptions options | | +| | | `Added` | `Method` | `private` | | | ParseInput | System.String | System.String raw | | | | | `Added` | `Property` | `public` | | MyApp.Models.ValidationResult | LastResult | | | | | | | `Added` | `Field` | `private` | `readonly` | System.String | _pattern | | | | | MyApp.Services.IValidator | | `Added` | `Interface` | `public` | | | | | | | -| | | `Added` | `Method` | `public` | | | Validate | System.Boolean | System.String input | | +| | | `Added` | `Method` | `public` | | | Validate | System.Boolean | System.String input | | | MyApp.Models.OrderStatus | | `Added` | `Enum` | `public` | | | | | | | | | | `Added` | `Field` | `public` | `static literal` | System.Int32 | Pending | | | | | | | `Added` | `Field` | `public` | `static literal` | System.Int32 | Processing | | | | @@ -110,18 +110,18 @@ | | | `Added` | `Field` | `public` | `static literal` | System.Int32 | Cancelled | | | | | MyApp.Services.BaseProcessor | | `Added` | `Class` | `public` | `abstract` | | | | | | | | | `Added` | `Constructor` | `protected` | | | BaseProcessor | System.Void | | | -| | | `Added` | `Method` | `public` | `abstract` | | Execute | System.Threading.Tasks.Task | System.String input | | +| | | `Added` | `Method` | `public` | `abstract` | | Execute | System.Threading.Tasks.Task | System.String input | | | | | `Added` | `Method` | `protected` | `virtual` | | OnCompleted | System.Void | | | | MyApp.Services.Constants | | `Added` | `Class` | `public` | `static` | | | | | | | | | `Added` | `Field` | `public` | `static literal` | System.String | DefaultEndpoint | | | | | | | `Added` | `Field` | `public` | `static literal` | System.Int32 | MaxRetries | | | | | | | `Added` | `Field` | `public` | `static readonly` | System.TimeSpan | DefaultTimeout | | | | -| MyApp.Services.OrderService | MyApp.Services.BaseService, MyApp.Services.IOrderProcessor | `Added` | `Method` | `public` | | | ValidateWithNewValidator | System.Boolean | System.String data | | +| MyApp.Services.OrderService | MyApp.Services.BaseService, MyApp.Services.IOrderProcessor | `Added` | `Method` | `public` | | | ValidateWithNewValidator | System.Boolean | System.String data | | | | | `Added` | `Property` | `protected` | | MyApp.Models.OrderContext | CurrentContext | | | | | | | `Added` | `Field` | `private` | `readonly` | MyApp.Models.UserRecord | _defaultUser | | | | -| | | `Removed` | `Method` | `public` | `virtual` | | LegacyValidate | System.Boolean | System.String data | | -| | | `Modified` | `Method` | `public` | | | ProcessOrder | System.Void | System.Int32 orderId | `Changed` | -| | | `Modified` | `Method` | `internal` | `static` | | CalculateTotal | System.Decimal | System.Int32 qty, System.Int32 price | `Changed` | +| | | `Removed` | `Method` | `public` | `virtual` | | LegacyValidate | System.Boolean | System.String data | | +| | | `Modified` | `Method` | `public` | | | ProcessOrder | System.Void | System.Int32 orderId | `Changed` | +| | | `Modified` | `Method` | `internal` | `static` | | CalculateTotal | System.Decimal | System.Int32 qty, System.Int32 price | `Changed` | | Class | Change | Count | |-------|:------:|------:| From a16e515bdca2d95130f1c522a6f0422e52db1efa Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 21:24:56 +0000 Subject: [PATCH 23/36] Fix HTML semantic changes tables: remove count table checkbox, widen columns, soften header color - Remove checkbox column from Class|Change|Count summary table - Make count table Class column independent (fixed 22em, not linked to detail table CSS var) - Widen fixed columns: Change 6.5em, Kind 8.5em, Access 6.5em, Modifiers 9em - Soften header background from near-black (#2c2c2e) to moderate gray (#6b6b6e) - Update sample HTML and base64 data accordingly https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- .../HtmlReportGenerateService.Css.cs | 21 ++++++++-------- .../HtmlReportGenerateService.Sections.cs | 6 ++--- doc/samples/diff_report.html | 24 ++++++++++--------- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/Services/HtmlReport/HtmlReportGenerateService.Css.cs b/Services/HtmlReport/HtmlReportGenerateService.Css.cs index 818916d2..8285b2db 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Css.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Css.cs @@ -139,33 +139,34 @@ private static string GetCss() .semantic-changes { padding: 6px 12px; font-size: 12px; overflow-x: auto; } .semantic-changes p { margin: 4px 0 2px; } table.semantic-changes-table { border-collapse: collapse; margin: 4px 0; font-size: 12px; table-layout: fixed; width: 1px; } - table.semantic-changes-table th { padding: 3px 8px; border: 1px solid #555; background: #2c2c2e; color: #fff; font-size: 11px; text-align: left; white-space: nowrap; } + table.semantic-changes-table th { padding: 3px 8px; border: 1px solid #999; background: #6b6b6e; color: #fff; font-size: 11px; text-align: left; white-space: nowrap; } table.semantic-changes-table th.th-resizable { position: relative; } table.semantic-changes-table td { padding: 2px 8px; border: 1px solid #e0e0e0; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } table.semantic-changes-table td.sc-col-cb { width: 2.2em; text-align: center; } /* sc-detail: checkbox(1) Class(2) BaseType(3) Change(4) Kind(5) Access(6) Modifiers(7) Type(8)… */ table.semantic-changes-table.sc-detail td:nth-child(4), table.semantic-changes-table.sc-detail td:nth-child(5), table.semantic-changes-table.sc-detail td:nth-child(6), table.semantic-changes-table.sc-detail td:nth-child(7) { text-align: center; } - /* sc-count: checkbox(1) Class(2) Change(3) Count(4) */ - table.semantic-changes-table.sc-count td:nth-child(3) { text-align: center; } + /* sc-count: Class(1) Change(2) Count(3) — no checkbox column */ + table.semantic-changes-table.sc-count td:nth-child(2) { text-align: center; } table.semantic-changes-table.sc-count td:last-child { text-align: right; } - table.semantic-changes-table tr.group-cont td:nth-child(2) { border-top: hidden; } + table.semantic-changes-table.sc-count tr.group-cont td:nth-child(1) { border-top: hidden; } + table.semantic-changes-table.sc-detail tr.group-cont td:nth-child(2) { border-top: hidden; } table.semantic-changes-table.sc-detail tr.group-cont td:nth-child(3) { border-top: hidden; } /* sc colgroup widths */ col.sc-col-cb-g { width: 2.2em; } col.sc-col-class-g { width: var(--sc-class-w); } col.sc-col-basetype-g { width: var(--sc-basetype-w); } - col.sc-col-change-g { width: 5.5em; } - col.sc-col-kind-g { width: 7em; } - col.sc-col-access-g { width: 5.5em; } - col.sc-col-mods-g { width: 6em; } + col.sc-col-change-g { width: 6.5em; } + col.sc-col-kind-g { width: 8.5em; } + col.sc-col-access-g { width: 6.5em; } + col.sc-col-mods-g { width: 9em; } col.sc-col-type-g { width: var(--sc-type-w); } col.sc-col-name-g { width: var(--sc-name-w); } col.sc-col-rettype-g { width: var(--sc-rettype-w); } col.sc-col-params-g { width: var(--sc-params-w); } col.sc-col-body-g { width: var(--sc-body-w); } - col.sc-cnt-class-g { width: var(--sc-class-w); } - col.sc-cnt-change-g { width: 5.5em; } + col.sc-cnt-class-g { width: 22em; } + col.sc-cnt-change-g { width: 6.5em; } col.sc-cnt-count-g { width: 4em; }"; } } diff --git a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs index 139b5b2f..4fc89b33 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs @@ -449,14 +449,12 @@ private static void AppendSummaryCountTable(StringBuilder sb, AssemblySemanticCh sb.AppendLine(""); sb.AppendLine(""); - sb.AppendLine(" "); sb.AppendLine(" "); sb.AppendLine(" "); sb.AppendLine(" "); sb.AppendLine(""); sb.AppendLine(""); - sb.AppendLine(" "); - sb.AppendLine(" "); + sb.AppendLine(" "); sb.AppendLine(" "); sb.AppendLine(""); sb.AppendLine(""); @@ -467,7 +465,7 @@ private static void AppendSummaryCountTable(StringBuilder sb, AssemblySemanticCh string classTd = !isCont ? HtmlEncode(typeName) : ""; prevType = typeName; string trOpen = isCont ? "" : ""; - sb.AppendLine($"{trOpen}"); + sb.AppendLine($"{trOpen}"); } sb.AppendLine("
ClassClassChangeCount
{classTd}{HtmlEncode(change)}{count}
{classTd}{HtmlEncode(change)}{count}
"); } diff --git a/doc/samples/diff_report.html b/doc/samples/diff_report.html index f3167b03..73fdd6d8 100644 --- a/doc/samples/diff_report.html +++ b/doc/samples/diff_report.html @@ -137,30 +137,32 @@ .semantic-changes { padding: 6px 12px; font-size: 12px; overflow-x: auto; } .semantic-changes p { margin: 4px 0 2px; } table.semantic-changes-table { border-collapse: collapse; margin: 4px 0; font-size: 12px; table-layout: fixed; width: 1px; } - table.semantic-changes-table th { padding: 3px 8px; border: 1px solid #555; background: #2c2c2e; color: #fff; font-size: 11px; text-align: left; white-space: nowrap; } + table.semantic-changes-table th { padding: 3px 8px; border: 1px solid #999; background: #6b6b6e; color: #fff; font-size: 11px; text-align: left; white-space: nowrap; } table.semantic-changes-table th.th-resizable { position: relative; } table.semantic-changes-table td { padding: 2px 8px; border: 1px solid #e0e0e0; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } table.semantic-changes-table td.sc-col-cb { width: 2.2em; text-align: center; } table.semantic-changes-table.sc-detail td:nth-child(4), table.semantic-changes-table.sc-detail td:nth-child(5), table.semantic-changes-table.sc-detail td:nth-child(6), table.semantic-changes-table.sc-detail td:nth-child(7) { text-align: center; } - table.semantic-changes-table.sc-count td:nth-child(3) { text-align: center; } + /* sc-count: Class(1) Change(2) Count(3) — no checkbox column */ + table.semantic-changes-table.sc-count td:nth-child(2) { text-align: center; } table.semantic-changes-table.sc-count td:last-child { text-align: right; } - table.semantic-changes-table tr.group-cont td:nth-child(2) { border-top: hidden; } + table.semantic-changes-table.sc-count tr.group-cont td:nth-child(1) { border-top: hidden; } + table.semantic-changes-table.sc-detail tr.group-cont td:nth-child(2) { border-top: hidden; } table.semantic-changes-table.sc-detail tr.group-cont td:nth-child(3) { border-top: hidden; } col.sc-col-cb-g { width: 2.2em; } col.sc-col-class-g { width: var(--sc-class-w); } col.sc-col-basetype-g { width: var(--sc-basetype-w); } - col.sc-col-change-g { width: 5.5em; } - col.sc-col-kind-g { width: 7em; } - col.sc-col-access-g { width: 5.5em; } - col.sc-col-mods-g { width: 6em; } + col.sc-col-change-g { width: 6.5em; } + col.sc-col-kind-g { width: 8.5em; } + col.sc-col-access-g { width: 6.5em; } + col.sc-col-mods-g { width: 9em; } col.sc-col-type-g { width: var(--sc-type-w); } col.sc-col-name-g { width: var(--sc-name-w); } col.sc-col-rettype-g { width: var(--sc-rettype-w); } col.sc-col-params-g { width: var(--sc-params-w); } col.sc-col-body-g { width: var(--sc-body-w); } - col.sc-cnt-class-g { width: var(--sc-class-w); } - col.sc-cnt-change-g { width: 5.5em; } + col.sc-cnt-class-g { width: 22em; } + col.sc-cnt-change-g { width: 6.5em; } col.sc-cnt-count-g { width: 4em; } @@ -462,7 +464,7 @@

[ * ] Modified Files (9)

-
+
#3 Show assembly semantic changes
@@ -503,7 +505,7 @@

[ * ] Modified Files (9)

-
+
#5 Show assembly semantic changes
From 793b450d8045b81dc0724d47a7104008efbccc15 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 21:32:56 +0000 Subject: [PATCH 24/36] Widen Kind/Modifiers/Change/Access columns, add Struct example, sort MD alphabetically - Increase column widths: Kind 10em, Modifiers 11em, Change 7em, Access 8em to prevent truncation of Constructor, static literal, Removed, protected - Add MyApp.Models.Coordinate readonly struct example to both HTML and MD samples - Sort MD Service.dll entries alphabetically to match HTML sample ordering https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- .../HtmlReportGenerateService.Css.cs | 10 +++--- doc/samples/diff_report.html | 12 +++---- doc/samples/diff_report.md | 36 +++++++++++-------- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/Services/HtmlReport/HtmlReportGenerateService.Css.cs b/Services/HtmlReport/HtmlReportGenerateService.Css.cs index 8285b2db..ac0add1e 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Css.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Css.cs @@ -156,17 +156,17 @@ private static string GetCss() col.sc-col-cb-g { width: 2.2em; } col.sc-col-class-g { width: var(--sc-class-w); } col.sc-col-basetype-g { width: var(--sc-basetype-w); } - col.sc-col-change-g { width: 6.5em; } - col.sc-col-kind-g { width: 8.5em; } - col.sc-col-access-g { width: 6.5em; } - col.sc-col-mods-g { width: 9em; } + col.sc-col-change-g { width: 7em; } + col.sc-col-kind-g { width: 10em; } + col.sc-col-access-g { width: 8em; } + col.sc-col-mods-g { width: 11em; } col.sc-col-type-g { width: var(--sc-type-w); } col.sc-col-name-g { width: var(--sc-name-w); } col.sc-col-rettype-g { width: var(--sc-rettype-w); } col.sc-col-params-g { width: var(--sc-params-w); } col.sc-col-body-g { width: var(--sc-body-w); } col.sc-cnt-class-g { width: 22em; } - col.sc-cnt-change-g { width: 6.5em; } + col.sc-cnt-change-g { width: 7em; } col.sc-cnt-count-g { width: 4em; }"; } } diff --git a/doc/samples/diff_report.html b/doc/samples/diff_report.html index 73fdd6d8..f768a621 100644 --- a/doc/samples/diff_report.html +++ b/doc/samples/diff_report.html @@ -152,17 +152,17 @@ col.sc-col-cb-g { width: 2.2em; } col.sc-col-class-g { width: var(--sc-class-w); } col.sc-col-basetype-g { width: var(--sc-basetype-w); } - col.sc-col-change-g { width: 6.5em; } - col.sc-col-kind-g { width: 8.5em; } - col.sc-col-access-g { width: 6.5em; } - col.sc-col-mods-g { width: 9em; } + col.sc-col-change-g { width: 7em; } + col.sc-col-kind-g { width: 10em; } + col.sc-col-access-g { width: 8em; } + col.sc-col-mods-g { width: 11em; } col.sc-col-type-g { width: var(--sc-type-w); } col.sc-col-name-g { width: var(--sc-name-w); } col.sc-col-rettype-g { width: var(--sc-rettype-w); } col.sc-col-params-g { width: var(--sc-params-w); } col.sc-col-body-g { width: var(--sc-body-w); } col.sc-cnt-class-g { width: 22em; } - col.sc-cnt-change-g { width: 6.5em; } + col.sc-cnt-change-g { width: 7em; } col.sc-cnt-count-g { width: 4em; } @@ -505,7 +505,7 @@

[ * ] Modified Files (9)

-
+
#5 Show assembly semantic changes
diff --git a/doc/samples/diff_report.md b/doc/samples/diff_report.md index 9d885205..ec42daa2 100644 --- a/doc/samples/diff_report.md +++ b/doc/samples/diff_report.md @@ -81,6 +81,16 @@ | Class | BaseType | Change | Kind | Access | Modifiers | Type | Name | ReturnType | Parameters | Body | |-------|----------|:------:|:----:|:------:|:---------:|------|------|------------|------------|------| +| MyApp.Models.Coordinate | System.ValueType | `Added` | `Struct` | `public` | `readonly` | | | | | | +| | | `Added` | `Constructor` | `public` | | | Coordinate | System.Void | System.Double\u00A0X,\u00A0System.Double\u00A0Y | | +| | | `Added` | `Property` | `public` | | System.Double | X | | | | +| | | `Added` | `Property` | `public` | | System.Double | Y | | | | +| | | `Added` | `Method` | `public` | `override` | | ToString | System.String | | | +| MyApp.Models.OrderStatus | | `Added` | `Enum` | `public` | | | | | | | +| | | `Added` | `Field` | `public` | `static literal` | System.Int32 | Pending | | | | +| | | `Added` | `Field` | `public` | `static literal` | System.Int32 | Processing | | | | +| | | `Added` | `Field` | `public` | `static literal` | System.Int32 | Completed | | | | +| | | `Added` | `Field` | `public` | `static literal` | System.Int32 | Cancelled | | | | | MyApp.Models.UserDto | | `Removed` | `Class` | `public` | | | | | | | | | | `Removed` | `Property` | `public` | | System.String | Name | | | | | | | `Removed` | `Property` | `public` | | System.Int32 | Age | | | | @@ -91,6 +101,16 @@ | | | `Added` | `Method` | `public` | `override` | | ToString | System.String | | | | | | `Added` | `Method` | `public` | `virtual` | | Equals | System.Boolean | System.Object obj | | | | | `Added` | `Method` | `public` | `override` | | GetHashCode | System.Int32 | | | +| MyApp.Services.BaseProcessor | | `Added` | `Class` | `public` | `abstract` | | | | | | +| | | `Added` | `Constructor` | `protected` | | | BaseProcessor | System.Void | | | +| | | `Added` | `Method` | `public` | `abstract` | | Execute | System.Threading.Tasks.Task | System.String input | | +| | | `Added` | `Method` | `protected` | `virtual` | | OnCompleted | System.Void | | | +| MyApp.Services.Constants | | `Added` | `Class` | `public` | `static` | | | | | | +| | | `Added` | `Field` | `public` | `static literal` | System.String | DefaultEndpoint | | | | +| | | `Added` | `Field` | `public` | `static literal` | System.Int32 | MaxRetries | | | | +| | | `Added` | `Field` | `public` | `static readonly` | System.TimeSpan | DefaultTimeout | | | | +| MyApp.Services.IValidator | | `Added` | `Interface` | `public` | | | | | | | +| | | `Added` | `Method` | `public` | | | Validate | System.Boolean | System.String input | | | MyApp.Services.LegacyHelper | | `Removed` | `Class` | `internal` | | | | | | | | | | `Removed` | `Method` | `public` | | | Convert | System.String | System.Object value | | | | | `Removed` | `Method` | `public` | `static` | | Format | System.String | System.String template, System.Object[] args | | @@ -101,21 +121,6 @@ | | | `Added` | `Method` | `private` | | | ParseInput | System.String | System.String raw | | | | | `Added` | `Property` | `public` | | MyApp.Models.ValidationResult | LastResult | | | | | | | `Added` | `Field` | `private` | `readonly` | System.String | _pattern | | | | -| MyApp.Services.IValidator | | `Added` | `Interface` | `public` | | | | | | | -| | | `Added` | `Method` | `public` | | | Validate | System.Boolean | System.String input | | -| MyApp.Models.OrderStatus | | `Added` | `Enum` | `public` | | | | | | | -| | | `Added` | `Field` | `public` | `static literal` | System.Int32 | Pending | | | | -| | | `Added` | `Field` | `public` | `static literal` | System.Int32 | Processing | | | | -| | | `Added` | `Field` | `public` | `static literal` | System.Int32 | Completed | | | | -| | | `Added` | `Field` | `public` | `static literal` | System.Int32 | Cancelled | | | | -| MyApp.Services.BaseProcessor | | `Added` | `Class` | `public` | `abstract` | | | | | | -| | | `Added` | `Constructor` | `protected` | | | BaseProcessor | System.Void | | | -| | | `Added` | `Method` | `public` | `abstract` | | Execute | System.Threading.Tasks.Task | System.String input | | -| | | `Added` | `Method` | `protected` | `virtual` | | OnCompleted | System.Void | | | -| MyApp.Services.Constants | | `Added` | `Class` | `public` | `static` | | | | | | -| | | `Added` | `Field` | `public` | `static literal` | System.String | DefaultEndpoint | | | | -| | | `Added` | `Field` | `public` | `static literal` | System.Int32 | MaxRetries | | | | -| | | `Added` | `Field` | `public` | `static readonly` | System.TimeSpan | DefaultTimeout | | | | | MyApp.Services.OrderService | MyApp.Services.BaseService, MyApp.Services.IOrderProcessor | `Added` | `Method` | `public` | | | ValidateWithNewValidator | System.Boolean | System.String data | | | | | `Added` | `Property` | `protected` | | MyApp.Models.OrderContext | CurrentContext | | | | | | | `Added` | `Field` | `private` | `readonly` | MyApp.Models.UserRecord | _defaultUser | | | | @@ -125,6 +130,7 @@ | Class | Change | Count | |-------|:------:|------:| +| MyApp.Models.Coordinate | `Added` | 5 | | MyApp.Models.OrderStatus | `Added` | 5 | | MyApp.Models.UserDto | `Removed` | 3 | | MyApp.Models.UserRecord | `Added` | 7 | From 772ceacf1ac1f27991a5ae1f3e9802bc62d3d220 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 22:03:11 +0000 Subject: [PATCH 25/36] Fix Markdown modifier wrapping, lighten HTML table headers, add checkbox/emphasis CSS - Apply NoWrapMd to Modifiers column in Markdown report to prevent multi-word modifiers (e.g. static literal, static readonly) from wrapping in table cells - Lighten semantic-changes table header background from #6b6b6e to #98989d for better readability - Add explicit th.sc-col-cb CSS rule to ensure checkbox column header renders visibly alongside body checkboxes - Update doc/samples/diff_report.md with NBSP in modifier values - Update doc/samples/diff_report.html with matching CSS changes - Add 5 new tests: header color, checkbox header presence, Kind/Access/Modifiers code emphasis, th CSS rule, NBSP modifiers https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- CHANGELOG.md | 12 ++ .../HtmlReportGenerateServiceTests.cs | 113 ++++++++++++++++++ .../Services/ReportGenerateServiceTests.cs | 45 +++++++ .../HtmlReportGenerateService.Css.cs | 3 +- .../ReportGenerateService.SectionWriters.cs | 2 +- doc/samples/diff_report.html | 3 +- doc/samples/diff_report.md | 14 +-- 7 files changed, 182 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97f94c73..0fb8e7a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - Added `Body` column (10th column, rightmost) to the Assembly Semantic Changes table. Displays `Changed` when a method body or field initializer has been modified at the IL level; otherwise empty. Entries with body changes use `Modified` in the Change column. Replaced the `Member count: N (Old) vs N (New)` summary line with `Added: N, Removed: N, Modified: N` counts computed from entries. Removed `OldMethodCount`/`NewMethodCount` properties from `AssemblySemanticChangesSummary` in favour of computed `AddedCount`, `RemovedCount`, and `ModifiedCount` properties. +#### Changed + +- Prevented multi-word Modifiers (e.g. `static literal`, `static readonly`) from wrapping in the Markdown report by applying `NoWrapMd` (non-breaking space) to the Modifiers column values in [`ReportGenerateService.SectionWriters.cs`](Services/ReportGenerateService.SectionWriters.cs). Updated [`doc/samples/diff_report.md`](doc/samples/diff_report.md) to match. + +- Lightened the Assembly Semantic Changes table header background colour from `#6b6b6e` to `#98989d` in [`HtmlReportGenerateService.Css.cs`](Services/HtmlReport/HtmlReportGenerateService.Css.cs) for better readability. Added explicit `th.sc-col-cb` width styling to ensure the checkbox column header renders visibly. Updated [`doc/samples/diff_report.html`](doc/samples/diff_report.html) to match. Added test `GenerateDiffReportHtml_AssemblySemanticChanges_TableHeaderUsesLighterGray` to [`HtmlReportGenerateServiceTests`](FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs). + ### [1.4.1] - 2026-03-20 #### Added @@ -389,6 +395,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - Assembly Semantic Changes テーブルに `Body` 列(10 列目、最右端)を追加。メソッドボディまたはフィールド初期化子が IL レベルで変更された場合に `Changed` を表示、それ以外は空欄。ボディ変更があるエントリの Change 列は `Modified`。集計行を `Member count: N (Old) vs N (New)` から `Added: N, Removed: N, Modified: N`(エントリから算出)に変更。`AssemblySemanticChangesSummary` の `OldMethodCount`/`NewMethodCount` プロパティを削除し、算出プロパティ `AddedCount`、`RemovedCount`、`ModifiedCount` に置き換え。 +#### 変更 + +- Markdown レポートの Modifiers 列に `NoWrapMd`(ノーブレークスペース)を適用し、`static literal` や `static readonly` などの複数語修飾子が折り返されないよう修正。[`ReportGenerateService.SectionWriters.cs`](Services/ReportGenerateService.SectionWriters.cs) を修正。[`doc/samples/diff_report.md`](doc/samples/diff_report.md) を同期。 + +- Assembly Semantic Changes テーブルヘッダの背景色を `#6b6b6e` から `#98989d` に明るく変更し視認性を改善。チェック列ヘッダの `th.sc-col-cb` に明示的な幅スタイルを追加。[`doc/samples/diff_report.html`](doc/samples/diff_report.html) を同期。テスト `GenerateDiffReportHtml_AssemblySemanticChanges_TableHeaderUsesLighterGray` を [`HtmlReportGenerateServiceTests`](FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs) に追加。 + ### [1.4.1] - 2026-03-20 #### 追加 diff --git a/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs b/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs index 908c62f6..bea34524 100644 --- a/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs @@ -806,6 +806,119 @@ public void GenerateDiffReportHtml_AssemblySemanticChanges_LazyRender_EncodesAsB Assert.Contains("data-diff-html", html); } + // ── Assembly Semantic Changes table styling / テーブルスタイル ──────── + + /// + /// Verifies the semantic-changes table header background uses the lighter gray (#98989d). + /// semantic-changes テーブルヘッダ背景に薄い灰色 (#98989d) が使われることを確認する。 + /// + [Fact] + public void GenerateDiffReportHtml_AssemblySemanticChanges_TableHeaderUsesLighterGray() + { + var (oldDir, newDir, reportDir) = MakeDirs("sc-header-color"); + var config = CreateConfig(); + + _service.GenerateDiffReportHtml(oldDir, newDir, reportDir, + appVersion: "1.0", elapsedTimeString: null, + computerName: "test-host", config); + + var html = File.ReadAllText(Path.Combine(reportDir, HtmlReportGenerateService.DIFF_REPORT_HTML_FILE_NAME)); + // Semantic changes table header must use lighter gray (#98989d), not the old dark gray (#6b6b6e) + // semantic-changes テーブルヘッダは旧暗灰色(#6b6b6e)ではなく薄灰色(#98989d)であること + Assert.Contains("background: #98989d", html); + Assert.DoesNotContain("background: #6b6b6e", html); + } + + /// + /// Verifies that the checkbox column header (✓) is present in the semantic-changes detail table. + /// semantic-changes 詳細テーブルにチェック列ヘッダ (✓) が存在することを確認する。 + /// + [Fact] + public void GenerateDiffReportHtml_AssemblySemanticChanges_CheckboxHeaderPresent() + { + var (oldDir, newDir, reportDir) = MakeDirs("sc-cb-header"); + + _resultLists.AddModifiedFileRelativePath("lib.dll"); + _resultLists.RecordDiffDetail("lib.dll", FileDiffResultLists.DiffDetailResult.ILMismatch, "dotnet-ildasm (version: 0.12.0)"); + + _resultLists.FileRelativePathToAssemblySemanticChanges["lib.dll"] = new AssemblySemanticChangesSummary + { + Entries = new List + { + new("Added", "Foo", "", "public", "", "Method", "Bar", "", "void", "", ""), + }, + }; + + var config = CreateConfig(enableInlineDiff: true, lazyRender: false); + config.ShouldIncludeAssemblySemanticChangesInReport = true; + _service.GenerateDiffReportHtml(oldDir, newDir, reportDir, + appVersion: "1.0", elapsedTimeString: null, + computerName: "test-host", config); + + var html = File.ReadAllText(Path.Combine(reportDir, HtmlReportGenerateService.DIFF_REPORT_HTML_FILE_NAME)); + // Detail table must have checkbox header (✓) AND body checkboxes + // 詳細テーブルにチェックヘッダ(✓)とボディのチェックボックスが両方存在すること + Assert.Contains("✓", html); + Assert.Contains(" + /// Verifies that Kind, Access, and Modifiers column body cells use code emphasis (like TextMatch). + /// Kind, Access, Modifiers 列ボディが code 強調表示を使用すること(TextMatch と同等)を確認する。 + ///

+ [Fact] + public void GenerateDiffReportHtml_AssemblySemanticChanges_KindAccessModifiersUseCodeEmphasis() + { + var (oldDir, newDir, reportDir) = MakeDirs("sc-emphasis"); + + _resultLists.AddModifiedFileRelativePath("lib.dll"); + _resultLists.RecordDiffDetail("lib.dll", FileDiffResultLists.DiffDetailResult.ILMismatch, "dotnet-ildasm (version: 0.12.0)"); + + _resultLists.FileRelativePathToAssemblySemanticChanges["lib.dll"] = new AssemblySemanticChangesSummary + { + Entries = new List + { + new("Modified", "MyApp.Svc", "", "public", "virtual", "Method", "Run", "", "void", "", "Changed"), + }, + }; + + var config = CreateConfig(enableInlineDiff: true, lazyRender: false); + config.ShouldIncludeAssemblySemanticChangesInReport = true; + _service.GenerateDiffReportHtml(oldDir, newDir, reportDir, + appVersion: "1.0", elapsedTimeString: null, + computerName: "test-host", config); + + var html = File.ReadAllText(Path.Combine(reportDir, HtmlReportGenerateService.DIFF_REPORT_HTML_FILE_NAME)); + // Kind, Access, and Modifiers must use emphasis (matching TextMatch in other tables) + // Kind、Access、Modifiers は 強調表示を使うこと(他テーブルの TextMatch と同様) + Assert.Contains("Method", html); // Kind + Assert.Contains("public", html); // Access + Assert.Contains("virtual", html); // Modifiers + Assert.Contains("Modified", html); // Change + Assert.Contains("Changed", html); // Body + } + + /// + /// Verifies the th.sc-col-cb CSS rule exists for proper checkbox column header styling. + /// th.sc-col-cb CSS ルールが存在しチェック列ヘッダが正しくスタイリングされることを確認する。 + /// + [Fact] + public void GenerateDiffReportHtml_AssemblySemanticChanges_ThScColCbCssRuleExists() + { + var (oldDir, newDir, reportDir) = MakeDirs("sc-th-css"); + var config = CreateConfig(); + + _service.GenerateDiffReportHtml(oldDir, newDir, reportDir, + appVersion: "1.0", elapsedTimeString: null, + computerName: "test-host", config); + + var html = File.ReadAllText(Path.Combine(reportDir, HtmlReportGenerateService.DIFF_REPORT_HTML_FILE_NAME)); + // Both th.sc-col-cb and td.sc-col-cb CSS rules must exist + // th.sc-col-cb と td.sc-col-cb 両方の CSS ルールが存在すること + Assert.Contains("table.semantic-changes-table th.sc-col-cb", html); + Assert.Contains("table.semantic-changes-table td.sc-col-cb", html); + } + private static ConfigSettings CreateConfig(bool enableInlineDiff = true, bool lazyRender = false) => new() { IgnoredExtensions = new List(), diff --git a/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs b/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs index 51d037fd..7dd4cc9d 100644 --- a/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs @@ -871,6 +871,51 @@ public void GenerateDiffReport_AssemblySemanticChanges_NotIncludedWhenNoChanges( Assert.DoesNotContain("## Assembly Semantic Changes", reportText); } + /// + /// Verifies that multi-word Modifiers (e.g. "static literal") use non-breaking spaces in the Markdown report. + /// 複数語の Modifiers(例: "static literal")が Markdown レポートでノーブレークスペースを使用することを確認する。 + /// + [Fact] + public void GenerateDiffReport_AssemblySemanticChanges_MultiWordModifiersUseNonBreakingSpace() + { + var oldDir = Path.Combine(_rootDir, "old-asc-nbsp"); + var newDir = Path.Combine(_rootDir, "new-asc-nbsp"); + var reportDir = Path.Combine(_rootDir, "report-asc-nbsp"); + Directory.CreateDirectory(oldDir); + Directory.CreateDirectory(newDir); + Directory.CreateDirectory(reportDir); + + _resultLists.AddModifiedFileRelativePath("src/Lib.dll"); + _resultLists.RecordDiffDetail("src/Lib.dll", FileDiffResultLists.DiffDetailResult.ILMismatch, "dotnet-ildasm (version: 0.12.0)"); + + _resultLists.FileRelativePathToAssemblySemanticChanges["src/Lib.dll"] = new AssemblySemanticChangesSummary + { + Entries = new List + { + new("Added", "MyApp.Status", "", "public", "static literal", "Field", "Active", "System.Int32", "", "", ""), + new("Added", "MyApp.Status", "", "public", "static readonly", "Field", "Default", "System.TimeSpan", "", "", ""), + }, + }; + + var config = CreateConfig(); + config.ShouldIncludeAssemblySemanticChangesInReport = true; + _service.GenerateDiffReport( + oldDir, newDir, reportDir, + appVersion: "test", elapsedTimeString: null, computerName: "test-host", + config); + + var reportText = File.ReadAllText(Path.Combine(reportDir, "diff_report.md")); + + // Multi-word modifiers must use non-breaking space (U+00A0) to prevent wrapping + // 複数語修飾子は折り返し防止のためノーブレークスペース (U+00A0) を使用すること + Assert.Contains("`static\u00A0literal`", reportText); + Assert.Contains("`static\u00A0readonly`", reportText); + // Regular space must NOT appear in these modifier values + // これらの修飾子値に通常スペースが含まれないこと + Assert.DoesNotContain("`static literal`", reportText); + Assert.DoesNotContain("`static readonly`", reportText); + } + private static ConfigSettings CreateConfig() => new() { IgnoredExtensions = new List(), diff --git a/Services/HtmlReport/HtmlReportGenerateService.Css.cs b/Services/HtmlReport/HtmlReportGenerateService.Css.cs index ac0add1e..44813243 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Css.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Css.cs @@ -139,9 +139,10 @@ private static string GetCss() .semantic-changes { padding: 6px 12px; font-size: 12px; overflow-x: auto; } .semantic-changes p { margin: 4px 0 2px; } table.semantic-changes-table { border-collapse: collapse; margin: 4px 0; font-size: 12px; table-layout: fixed; width: 1px; } - table.semantic-changes-table th { padding: 3px 8px; border: 1px solid #999; background: #6b6b6e; color: #fff; font-size: 11px; text-align: left; white-space: nowrap; } + table.semantic-changes-table th { padding: 3px 8px; border: 1px solid #bbb; background: #98989d; color: #fff; font-size: 11px; text-align: left; white-space: nowrap; } table.semantic-changes-table th.th-resizable { position: relative; } table.semantic-changes-table td { padding: 2px 8px; border: 1px solid #e0e0e0; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + table.semantic-changes-table th.sc-col-cb { width: 2.2em; text-align: center; } table.semantic-changes-table td.sc-col-cb { width: 2.2em; text-align: center; } /* sc-detail: checkbox(1) Class(2) BaseType(3) Change(4) Kind(5) Access(6) Modifiers(7) Type(8)… */ table.semantic-changes-table.sc-detail td:nth-child(4), table.semantic-changes-table.sc-detail td:nth-child(5), diff --git a/Services/ReportGenerateService.SectionWriters.cs b/Services/ReportGenerateService.SectionWriters.cs index 28570c7d..c90864af 100644 --- a/Services/ReportGenerateService.SectionWriters.cs +++ b/Services/ReportGenerateService.SectionWriters.cs @@ -219,7 +219,7 @@ public void Write(StreamWriter writer, ReportWriteContext ctx) string baseTypeCol = !isCont ? NoWrapMd(EscapeMdTable(e.BaseType)) : ""; prevType = e.TypeName; string access = e.Access.Length > 0 ? $"`{EscapeMdTable(e.Access)}`" : ""; - string modifiers = e.Modifiers.Length > 0 ? $"`{EscapeMdTable(e.Modifiers)}`" : ""; + string modifiers = e.Modifiers.Length > 0 ? $"`{NoWrapMd(EscapeMdTable(e.Modifiers))}`" : ""; string body = e.Body.Length > 0 ? $"`{EscapeMdTable(e.Body)}`" : ""; writer.WriteLine($"| {classCol} | {baseTypeCol} | `{EscapeMdTable(e.Change)}` | `{EscapeMdTable(e.MemberKind)}` | {access} | {modifiers} | {NoWrapMd(EscapeMdTable(e.MemberType))} | {EscapeMdTable(e.MemberName)} | {NoWrapMd(EscapeMdTable(e.ReturnType))} | {NoWrapMd(EscapeMdTable(e.Parameters))} | {body} |"); } diff --git a/doc/samples/diff_report.html b/doc/samples/diff_report.html index f768a621..93d08b97 100644 --- a/doc/samples/diff_report.html +++ b/doc/samples/diff_report.html @@ -137,9 +137,10 @@ .semantic-changes { padding: 6px 12px; font-size: 12px; overflow-x: auto; } .semantic-changes p { margin: 4px 0 2px; } table.semantic-changes-table { border-collapse: collapse; margin: 4px 0; font-size: 12px; table-layout: fixed; width: 1px; } - table.semantic-changes-table th { padding: 3px 8px; border: 1px solid #999; background: #6b6b6e; color: #fff; font-size: 11px; text-align: left; white-space: nowrap; } + table.semantic-changes-table th { padding: 3px 8px; border: 1px solid #bbb; background: #98989d; color: #fff; font-size: 11px; text-align: left; white-space: nowrap; } table.semantic-changes-table th.th-resizable { position: relative; } table.semantic-changes-table td { padding: 2px 8px; border: 1px solid #e0e0e0; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + table.semantic-changes-table th.sc-col-cb { width: 2.2em; text-align: center; } table.semantic-changes-table td.sc-col-cb { width: 2.2em; text-align: center; } table.semantic-changes-table.sc-detail td:nth-child(4), table.semantic-changes-table.sc-detail td:nth-child(5), table.semantic-changes-table.sc-detail td:nth-child(6), table.semantic-changes-table.sc-detail td:nth-child(7) { text-align: center; } diff --git a/doc/samples/diff_report.md b/doc/samples/diff_report.md index ec42daa2..5bce02c3 100644 --- a/doc/samples/diff_report.md +++ b/doc/samples/diff_report.md @@ -87,10 +87,10 @@ | | | `Added` | `Property` | `public` | | System.Double | Y | | | | | | | `Added` | `Method` | `public` | `override` | | ToString | System.String | | | | MyApp.Models.OrderStatus | | `Added` | `Enum` | `public` | | | | | | | -| | | `Added` | `Field` | `public` | `static literal` | System.Int32 | Pending | | | | -| | | `Added` | `Field` | `public` | `static literal` | System.Int32 | Processing | | | | -| | | `Added` | `Field` | `public` | `static literal` | System.Int32 | Completed | | | | -| | | `Added` | `Field` | `public` | `static literal` | System.Int32 | Cancelled | | | | +| | | `Added` | `Field` | `public` | `static literal` | System.Int32 | Pending | | | | +| | | `Added` | `Field` | `public` | `static literal` | System.Int32 | Processing | | | | +| | | `Added` | `Field` | `public` | `static literal` | System.Int32 | Completed | | | | +| | | `Added` | `Field` | `public` | `static literal` | System.Int32 | Cancelled | | | | | MyApp.Models.UserDto | | `Removed` | `Class` | `public` | | | | | | | | | | `Removed` | `Property` | `public` | | System.String | Name | | | | | | | `Removed` | `Property` | `public` | | System.Int32 | Age | | | | @@ -106,9 +106,9 @@ | | | `Added` | `Method` | `public` | `abstract` | | Execute | System.Threading.Tasks.Task | System.String input | | | | | `Added` | `Method` | `protected` | `virtual` | | OnCompleted | System.Void | | | | MyApp.Services.Constants | | `Added` | `Class` | `public` | `static` | | | | | | -| | | `Added` | `Field` | `public` | `static literal` | System.String | DefaultEndpoint | | | | -| | | `Added` | `Field` | `public` | `static literal` | System.Int32 | MaxRetries | | | | -| | | `Added` | `Field` | `public` | `static readonly` | System.TimeSpan | DefaultTimeout | | | | +| | | `Added` | `Field` | `public` | `static literal` | System.String | DefaultEndpoint | | | | +| | | `Added` | `Field` | `public` | `static literal` | System.Int32 | MaxRetries | | | | +| | | `Added` | `Field` | `public` | `static readonly` | System.TimeSpan | DefaultTimeout | | | | | MyApp.Services.IValidator | | `Added` | `Interface` | `public` | | | | | | | | | | `Added` | `Method` | `public` | | | Validate | System.Boolean | System.String input | | | MyApp.Services.LegacyHelper | | `Removed` | `Class` | `internal` | | | | | | | From fd296116a8a0ea28ae2a98ddec21df327b3f7a5d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Mar 2026 06:12:04 +0000 Subject: [PATCH 26/36] fix: semantic-changes-table columns truncated with ellipsis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change table-layout from fixed to auto so columns auto-size to content instead of being squeezed within a constrained container. The container's overflow-x:auto provides horizontal scrolling when needed. Also make the sc-count (Class/Change/Count) table's Class column resizable via --sc-cnt-class-w CSS variable. セマンティック変更テーブルの列がコンテナ幅に圧縮されて「...」が 出る問題を修正。table-layout を auto に変更しコンテンツに 合わせた自動サイズ調整を有効化。sc-count テーブルの Class 列も リサイズ可能に。 https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- Services/HtmlReport/HtmlReportGenerateService.Css.cs | 7 ++++--- Services/HtmlReport/HtmlReportGenerateService.Js.cs | 7 ++++--- Services/HtmlReport/HtmlReportGenerateService.Sections.cs | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Services/HtmlReport/HtmlReportGenerateService.Css.cs b/Services/HtmlReport/HtmlReportGenerateService.Css.cs index 44813243..3d76e6ad 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Css.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Css.cs @@ -55,7 +55,8 @@ private static string GetCss() /* ── Column width CSS variables ──────────────────────────────────────── */ :root { --col-reason-w: 10em; --col-notes-w: 10em; --col-path-w: 22em; --col-diff-w: 9em; --col-disasm-w: 28em; --sc-class-w: 14em; --sc-basetype-w: 16em; --sc-type-w: 12em; - --sc-name-w: 10em; --sc-rettype-w: 12em; --sc-params-w: 18em; --sc-body-w: 5em; } + --sc-name-w: 10em; --sc-rettype-w: 12em; --sc-params-w: 18em; --sc-body-w: 5em; + --sc-cnt-class-w: 22em; } col.col-no-g { width: 3.2em; } col.col-cb-g { width: 2.2em; } col.col-reason-g { width: var(--col-reason-w); } @@ -138,7 +139,7 @@ private static string GetCss() /* ── Assembly semantic changes ─────────────────────────────────────── */ .semantic-changes { padding: 6px 12px; font-size: 12px; overflow-x: auto; } .semantic-changes p { margin: 4px 0 2px; } - table.semantic-changes-table { border-collapse: collapse; margin: 4px 0; font-size: 12px; table-layout: fixed; width: 1px; } + table.semantic-changes-table { border-collapse: collapse; margin: 4px 0; font-size: 12px; table-layout: auto; } table.semantic-changes-table th { padding: 3px 8px; border: 1px solid #bbb; background: #98989d; color: #fff; font-size: 11px; text-align: left; white-space: nowrap; } table.semantic-changes-table th.th-resizable { position: relative; } table.semantic-changes-table td { padding: 2px 8px; border: 1px solid #e0e0e0; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } @@ -166,7 +167,7 @@ private static string GetCss() col.sc-col-rettype-g { width: var(--sc-rettype-w); } col.sc-col-params-g { width: var(--sc-params-w); } col.sc-col-body-g { width: var(--sc-body-w); } - col.sc-cnt-class-g { width: 22em; } + col.sc-cnt-class-g { width: var(--sc-cnt-class-w); } col.sc-cnt-change-g { width: 7em; } col.sc-cnt-count-g { width: 4em; }"; } diff --git a/Services/HtmlReport/HtmlReportGenerateService.Js.cs b/Services/HtmlReport/HtmlReportGenerateService.Js.cs index b28e65a0..7699cebb 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Js.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Js.cs @@ -68,7 +68,7 @@ private static void AppendJs(StringBuilder sb, string storageKey, string reportD sb.AppendLine(" var openDetails = Array.from(document.querySelectorAll('details[open]'));"); sb.AppendLine(" openDetails.forEach(function(d){ d.removeAttribute('open'); });"); sb.AppendLine(" // 2. Capture current effective column widths to bake into reviewed HTML as defaults"); - sb.AppendLine(" var colVarNames = ['--col-reason-w','--col-notes-w','--col-path-w','--col-diff-w','--col-disasm-w','--sc-class-w','--sc-basetype-w','--sc-type-w','--sc-name-w','--sc-rettype-w','--sc-params-w','--sc-body-w'];"); + sb.AppendLine(" var colVarNames = ['--col-reason-w','--col-notes-w','--col-path-w','--col-diff-w','--col-disasm-w','--sc-class-w','--sc-basetype-w','--sc-type-w','--sc-name-w','--sc-rettype-w','--sc-params-w','--sc-body-w','--sc-cnt-class-w'];"); sb.AppendLine(" var cs = getComputedStyle(root);"); sb.AppendLine(" var curWidths = {};"); sb.AppendLine(" colVarNames.forEach(function(v){ curWidths[v] = (root.style.getPropertyValue(v) || cs.getPropertyValue(v)).trim(); });"); @@ -94,7 +94,8 @@ private static void AppendJs(StringBuilder sb, string storageKey, string reportD sb.AppendLine(" + '; --sc-name-w: ' + curWidths['--sc-name-w']"); sb.AppendLine(" + '; --sc-rettype-w: ' + curWidths['--sc-rettype-w']"); sb.AppendLine(" + '; --sc-params-w: ' + curWidths['--sc-params-w']"); - sb.AppendLine(" + '; --sc-body-w: ' + curWidths['--sc-body-w'] + '; }');"); + sb.AppendLine(" + '; --sc-body-w: ' + curWidths['--sc-body-w'] + + '; --sc-cnt-class-w: ' + curWidths['--sc-cnt-class-w'] + '; }');"); sb.AppendLine(" // Remove inline col-var overrides from element (now baked into :root)"); sb.AppendLine(" html = html.replace(/(]*?) style=\"[^\"]*\"/, '$1');"); sb.AppendLine(" // Replace controls bar with reviewed banner"); @@ -116,7 +117,7 @@ private static void AppendJs(StringBuilder sb, string storageKey, string reportD sb.AppendLine(" document.querySelectorAll('input[type=\"text\"], textarea').forEach(function(inp){ inp.value=''; });"); sb.AppendLine(" // Reset column widths to defaults"); sb.AppendLine(" var root = document.documentElement;"); - sb.AppendLine(" ['--col-reason-w','--col-notes-w','--col-path-w','--col-diff-w','--col-disasm-w','--sc-class-w','--sc-basetype-w','--sc-type-w','--sc-name-w','--sc-rettype-w','--sc-params-w','--sc-body-w'].forEach(function(v){ root.style.removeProperty(v); });"); + sb.AppendLine(" ['--col-reason-w','--col-notes-w','--col-path-w','--col-diff-w','--col-disasm-w','--sc-class-w','--sc-basetype-w','--sc-type-w','--sc-name-w','--sc-rettype-w','--sc-params-w','--sc-body-w','--sc-cnt-class-w'].forEach(function(v){ root.style.removeProperty(v); });"); sb.AppendLine(" syncTableWidths();"); sb.AppendLine(" // Close all open diff/IL-diff details"); sb.AppendLine(" document.querySelectorAll('details[open]').forEach(function(d){ d.removeAttribute('open'); });"); diff --git a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs index 4fc89b33..d2d7eefc 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Sections.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Sections.cs @@ -454,7 +454,7 @@ private static void AppendSummaryCountTable(StringBuilder sb, AssemblySemanticCh sb.AppendLine(" "); sb.AppendLine(""); sb.AppendLine(""); - sb.AppendLine(" Class"); + sb.AppendLine(" Class"); sb.AppendLine(" ChangeCount"); sb.AppendLine(""); sb.AppendLine(""); From 9223d9d1ed15875a4aad44753ce76fe918daae02 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Mar 2026 06:21:42 +0000 Subject: [PATCH 27/36] fix: update sample HTML to reflect semantic-changes-table layout fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply the same CSS/JS changes to doc/samples/diff_report.html: - table-layout: fixed → auto for semantic-changes-table - Add --sc-cnt-class-w CSS variable to :root and colgroup - Add --sc-cnt-class-w to JS colVarNames arrays - Update base64-encoded data-diff-html attributes: make sc-count table Class resizable with data-col-var サンプル HTML にも同じ修正を反映。base64 エンコード済み data-diff-html 内の sc-count テーブル Class ヘッダーも更新。 https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- doc/samples/diff_report.html | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/doc/samples/diff_report.html b/doc/samples/diff_report.html index 93d08b97..df2afe5f 100644 --- a/doc/samples/diff_report.html +++ b/doc/samples/diff_report.html @@ -53,7 +53,8 @@ /* ── Column width CSS variables ──────────────────────────────────────── */ :root { --col-reason-w: 10em; --col-notes-w: 10em; --col-path-w: 22em; --col-diff-w: 9em; --col-disasm-w: 28em; --sc-class-w: 14em; --sc-basetype-w: 16em; --sc-type-w: 12em; - --sc-name-w: 10em; --sc-rettype-w: 12em; --sc-params-w: 18em; --sc-body-w: 5em; } + --sc-name-w: 10em; --sc-rettype-w: 12em; --sc-params-w: 18em; --sc-body-w: 5em; + --sc-cnt-class-w: 22em; } col.col-no-g { width: 3.2em; } col.col-cb-g { width: 2.2em; } col.col-reason-g { width: var(--col-reason-w); } @@ -136,7 +137,7 @@ /* ── Assembly semantic changes ─────────────────────────────────────── */ .semantic-changes { padding: 6px 12px; font-size: 12px; overflow-x: auto; } .semantic-changes p { margin: 4px 0 2px; } - table.semantic-changes-table { border-collapse: collapse; margin: 4px 0; font-size: 12px; table-layout: fixed; width: 1px; } + table.semantic-changes-table { border-collapse: collapse; margin: 4px 0; font-size: 12px; table-layout: auto; } table.semantic-changes-table th { padding: 3px 8px; border: 1px solid #bbb; background: #98989d; color: #fff; font-size: 11px; text-align: left; white-space: nowrap; } table.semantic-changes-table th.th-resizable { position: relative; } table.semantic-changes-table td { padding: 2px 8px; border: 1px solid #e0e0e0; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } @@ -162,7 +163,7 @@ col.sc-col-rettype-g { width: var(--sc-rettype-w); } col.sc-col-params-g { width: var(--sc-params-w); } col.sc-col-body-g { width: var(--sc-body-w); } - col.sc-cnt-class-g { width: 22em; } + col.sc-cnt-class-g { width: var(--sc-cnt-class-w); } col.sc-cnt-change-g { width: 7em; } col.sc-cnt-count-g { width: 4em; } @@ -465,7 +466,7 @@

[ * ] Modified Files (9)

-
+
#3 Show assembly semantic changes
@@ -506,7 +507,7 @@

[ * ] Modified Files (9)

-
+
#5 Show assembly semantic changes
@@ -726,7 +727,7 @@

[ ! ] Modified Files — Timestamps Regressed (2) var openDetails = Array.from(document.querySelectorAll('details[open]')); openDetails.forEach(function(d){ d.removeAttribute('open'); }); // 2. Capture current effective column widths to bake into reviewed HTML as defaults - var colVarNames = ['--col-reason-w','--col-notes-w','--col-path-w','--col-diff-w','--col-disasm-w','--sc-class-w','--sc-basetype-w','--sc-type-w','--sc-name-w','--sc-rettype-w','--sc-params-w','--sc-body-w']; + var colVarNames = ['--col-reason-w','--col-notes-w','--col-path-w','--col-diff-w','--col-disasm-w','--sc-class-w','--sc-basetype-w','--sc-type-w','--sc-name-w','--sc-rettype-w','--sc-params-w','--sc-body-w','--sc-cnt-class-w']; var cs = getComputedStyle(root); var curWidths = {}; colVarNames.forEach(function(v){ curWidths[v] = (root.style.getPropertyValue(v) || cs.getPropertyValue(v)).trim(); }); @@ -752,7 +753,8 @@

[ ! ] Modified Files — Timestamps Regressed (2) + '; --sc-name-w: ' + curWidths['--sc-name-w'] + '; --sc-rettype-w: ' + curWidths['--sc-rettype-w'] + '; --sc-params-w: ' + curWidths['--sc-params-w'] - + '; --sc-body-w: ' + curWidths['--sc-body-w'] + '; }'); + + '; --sc-body-w: ' + curWidths['--sc-body-w'] + + '; --sc-cnt-class-w: ' + curWidths['--sc-cnt-class-w'] + '; }'); // Remove inline col-var overrides from element (now baked into :root) html = html.replace(/(]*?) style="[^"]*"/, '$1'); // Replace controls bar with reviewed banner @@ -774,7 +776,7 @@

[ ! ] Modified Files — Timestamps Regressed (2) document.querySelectorAll('input[type="text"], textarea').forEach(function(inp){ inp.value=''; }); // Reset column widths to defaults var root = document.documentElement; - ['--col-reason-w','--col-notes-w','--col-path-w','--col-diff-w','--col-disasm-w','--sc-class-w','--sc-basetype-w','--sc-type-w','--sc-name-w','--sc-rettype-w','--sc-params-w','--sc-body-w'].forEach(function(v){ root.style.removeProperty(v); }); + ['--col-reason-w','--col-notes-w','--col-path-w','--col-diff-w','--col-disasm-w','--sc-class-w','--sc-basetype-w','--sc-type-w','--sc-name-w','--sc-rettype-w','--sc-params-w','--sc-body-w','--sc-cnt-class-w'].forEach(function(v){ root.style.removeProperty(v); }); syncTableWidths(); // Close all open diff/IL-diff details document.querySelectorAll('details[open]').forEach(function(d){ d.removeAttribute('open'); }); From fd512d95de97c21d4a317990c8a850e33b998723 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Mar 2026 06:26:26 +0000 Subject: [PATCH 28/36] fix: restore missing checkbox and Class headers in sample sc-detail tables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The base64-encoded sc-detail table headers were missing: - ✓ (checkbox column header) - Class (resizable Class column header) This caused the header row to be misaligned with data rows that included checkbox cells. サンプル HTML の base64 内 sc-detail テーブルヘッダーに チェック列ヘッダーとリサイズ可能な Class ヘッダーが 欠落していた問題を修正。 https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- doc/samples/diff_report.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/samples/diff_report.html b/doc/samples/diff_report.html index df2afe5f..81428408 100644 --- a/doc/samples/diff_report.html +++ b/doc/samples/diff_report.html @@ -466,7 +466,7 @@

[ * ] Modified Files (9)

-
+
#3 Show assembly semantic changes
@@ -507,7 +507,7 @@

[ * ] Modified Files (9)

-
+
#5 Show assembly semantic changes
From 2a40f375a53e82f521f577fd240e3f35c0be9bb8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Mar 2026 06:55:54 +0000 Subject: [PATCH 29/36] fix: sc-detail table column resize not working freely Root causes: 1. table-layout: auto caused browser auto-layout to fight CSS variable resize values, making columns "stick" during drag 2. JS used root font-size (16px) for em calculations but sc-table columns resolve em at table font-size (12px), causing mismatched widths Changes: - Change table-layout from auto to fixed for semantic-changes-table - Add syncScTableWidths() to compute and set total table width based on column CSS variables (like syncTableWidths does for main table) - Use table font-size (12px) instead of root font-size for sc column resize calculations - Call syncScTableWidths on init, lazy-diff expansion, and resize - Update sample HTML with same fixes https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- .../HtmlReportGenerateService.Css.cs | 2 +- .../HtmlReportGenerateService.Js.cs | 24 ++++++++++++++++- doc/samples/diff_report.html | 26 +++++++++++++++++-- 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/Services/HtmlReport/HtmlReportGenerateService.Css.cs b/Services/HtmlReport/HtmlReportGenerateService.Css.cs index 3d76e6ad..ec794d49 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Css.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Css.cs @@ -139,7 +139,7 @@ private static string GetCss() /* ── Assembly semantic changes ─────────────────────────────────────── */ .semantic-changes { padding: 6px 12px; font-size: 12px; overflow-x: auto; } .semantic-changes p { margin: 4px 0 2px; } - table.semantic-changes-table { border-collapse: collapse; margin: 4px 0; font-size: 12px; table-layout: auto; } + table.semantic-changes-table { border-collapse: collapse; margin: 4px 0; font-size: 12px; table-layout: fixed; } table.semantic-changes-table th { padding: 3px 8px; border: 1px solid #bbb; background: #98989d; color: #fff; font-size: 11px; text-align: left; white-space: nowrap; } table.semantic-changes-table th.th-resizable { position: relative; } table.semantic-changes-table td { padding: 2px 8px; border: 1px solid #e0e0e0; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } diff --git a/Services/HtmlReport/HtmlReportGenerateService.Js.cs b/Services/HtmlReport/HtmlReportGenerateService.Js.cs index 7699cebb..cbc48011 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Js.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Js.cs @@ -43,6 +43,7 @@ private static void AppendJs(StringBuilder sb, string storageKey, string reportD sb.AppendLine(" }"); sb.AppendLine(" initColResize();"); sb.AppendLine(" syncTableWidths();"); + sb.AppendLine(" syncScTableWidths();"); sb.AppendLine(" setupLazyDiff();"); sb.AppendLine(" });"); sb.AppendLine(); @@ -150,6 +151,7 @@ private static void AppendJs(StringBuilder sb, string storageKey, string reportD sb.AppendLine(" if (th.querySelector('.col-resize-handle')) return;"); sb.AppendLine(" initColResizeSingle(th);"); sb.AppendLine(" });"); + sb.AppendLine(" syncScTableWidths();"); sb.AppendLine(" // Wire up save events on new checkboxes"); sb.AppendLine(" if (__savedState__ === null) {"); sb.AppendLine(" d.querySelectorAll('input').forEach(function(el) {"); @@ -190,6 +192,24 @@ private static void AppendJs(StringBuilder sb, string storageKey, string reportD sb.AppendLine(" });"); sb.AppendLine(" }"); sb.AppendLine(); + sb.AppendLine(" function syncScTableWidths() {"); + sb.AppendLine(" var scEmPx = 12;"); + sb.AppendLine(" var root = document.documentElement;"); + sb.AppendLine(" var px = function(v, fb) {"); + sb.AppendLine(" var s = root.style.getPropertyValue(v) || getComputedStyle(root).getPropertyValue(v);"); + sb.AppendLine(" return (parseFloat(s) || fb) * scEmPx;"); + sb.AppendLine(" };"); + sb.AppendLine(" var detW = 2.2 * scEmPx"); + sb.AppendLine(" + px('--sc-class-w', 14) + px('--sc-basetype-w', 16)"); + sb.AppendLine(" + 7 * scEmPx + 10 * scEmPx + 8 * scEmPx + 11 * scEmPx"); + sb.AppendLine(" + px('--sc-type-w', 12) + px('--sc-name-w', 10)"); + sb.AppendLine(" + px('--sc-rettype-w', 12) + px('--sc-params-w', 18)"); + sb.AppendLine(" + px('--sc-body-w', 5);"); + sb.AppendLine(" document.querySelectorAll('table.sc-detail').forEach(function(t) { t.style.width = detW + 'px'; });"); + sb.AppendLine(" var cntW = px('--sc-cnt-class-w', 22) + 7 * scEmPx + 4 * scEmPx;"); + sb.AppendLine(" document.querySelectorAll('table.sc-count').forEach(function(t) { t.style.width = cntW + 'px'; });"); + sb.AppendLine(" }"); + sb.AppendLine(); sb.AppendLine(" function initColResizeSingle(th) {"); sb.AppendLine(" var label = document.createElement('span');"); sb.AppendLine(" label.className = 'th-label';"); @@ -199,17 +219,19 @@ private static void AppendJs(StringBuilder sb, string storageKey, string reportD sb.AppendLine(" handle.className = 'col-resize-handle';"); sb.AppendLine(" th.appendChild(handle);"); sb.AppendLine(" var varName = th.dataset.colVar;"); + sb.AppendLine(" var isSc = !!th.closest('.semantic-changes-table');"); sb.AppendLine(" handle.addEventListener('mousedown', function(e) {"); sb.AppendLine(" e.preventDefault();"); sb.AppendLine(" var startX = e.clientX;"); sb.AppendLine(" var root = document.documentElement;"); - sb.AppendLine(" var emPx = parseFloat(getComputedStyle(root).fontSize) || 16;"); + sb.AppendLine(" var emPx = isSc ? 12 : (parseFloat(getComputedStyle(root).fontSize) || 16);"); sb.AppendLine(" var cur = root.style.getPropertyValue(varName) || getComputedStyle(root).getPropertyValue(varName);"); sb.AppendLine(" var startPx = (parseFloat(cur) || 10) * emPx;"); sb.AppendLine(" function onMove(ev) {"); sb.AppendLine(" var newPx = Math.max(48, startPx + (ev.clientX - startX));"); sb.AppendLine(" root.style.setProperty(varName, (newPx / emPx).toFixed(2) + 'em');"); sb.AppendLine(" syncTableWidths();"); + sb.AppendLine(" if (isSc) syncScTableWidths();"); sb.AppendLine(" }"); sb.AppendLine(" function onUp() {"); sb.AppendLine(" document.removeEventListener('mousemove', onMove);"); diff --git a/doc/samples/diff_report.html b/doc/samples/diff_report.html index 81428408..d41f1c1d 100644 --- a/doc/samples/diff_report.html +++ b/doc/samples/diff_report.html @@ -137,7 +137,7 @@ /* ── Assembly semantic changes ─────────────────────────────────────── */ .semantic-changes { padding: 6px 12px; font-size: 12px; overflow-x: auto; } .semantic-changes p { margin: 4px 0 2px; } - table.semantic-changes-table { border-collapse: collapse; margin: 4px 0; font-size: 12px; table-layout: auto; } + table.semantic-changes-table { border-collapse: collapse; margin: 4px 0; font-size: 12px; table-layout: fixed; } table.semantic-changes-table th { padding: 3px 8px; border: 1px solid #bbb; background: #98989d; color: #fff; font-size: 11px; text-align: left; white-space: nowrap; } table.semantic-changes-table th.th-resizable { position: relative; } table.semantic-changes-table td { padding: 2px 8px; border: 1px solid #e0e0e0; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } @@ -703,6 +703,7 @@

[ ! ] Modified Files — Timestamps Regressed (2) setupLazyDiff(); initColResize(); syncTableWidths(); + syncScTableWidths(); }); function collectState() { @@ -823,6 +824,7 @@

[ ! ] Modified Files — Timestamps Regressed (2) if (th.querySelector('.col-resize-handle')) return; initColResizeSingle(th); }); + syncScTableWidths(); if (__savedState__ === null) { d.querySelectorAll('input').forEach(function(el) { el.addEventListener('change', autoSave); @@ -845,6 +847,24 @@

[ ! ] Modified Files — Timestamps Regressed (2) }); } + function syncScTableWidths() { + var scEmPx = 12; + var root = document.documentElement; + var px = function(v, fb) { + var s = root.style.getPropertyValue(v) || getComputedStyle(root).getPropertyValue(v); + return (parseFloat(s) || fb) * scEmPx; + }; + var detW = 2.2 * scEmPx + + px('--sc-class-w', 14) + px('--sc-basetype-w', 16) + + 7 * scEmPx + 10 * scEmPx + 8 * scEmPx + 11 * scEmPx + + px('--sc-type-w', 12) + px('--sc-name-w', 10) + + px('--sc-rettype-w', 12) + px('--sc-params-w', 18) + + px('--sc-body-w', 5); + document.querySelectorAll('table.sc-detail').forEach(function(t) { t.style.width = detW + 'px'; }); + var cntW = px('--sc-cnt-class-w', 22) + 7 * scEmPx + 4 * scEmPx; + document.querySelectorAll('table.sc-count').forEach(function(t) { t.style.width = cntW + 'px'; }); + } + function initColResizeSingle(th) { var label = document.createElement('span'); label.className = 'th-label'; @@ -854,17 +874,19 @@

[ ! ] Modified Files — Timestamps Regressed (2) handle.className = 'col-resize-handle'; th.appendChild(handle); var varName = th.dataset.colVar; + var isSc = !!th.closest('.semantic-changes-table'); handle.addEventListener('mousedown', function(e) { e.preventDefault(); var startX = e.clientX; var root = document.documentElement; - var emPx = parseFloat(getComputedStyle(root).fontSize) || 16; + var emPx = isSc ? 12 : (parseFloat(getComputedStyle(root).fontSize) || 16); var cur = root.style.getPropertyValue(varName) || getComputedStyle(root).getPropertyValue(varName); var startPx = (parseFloat(cur) || 10) * emPx; function onMove(ev) { var newPx = Math.max(48, startPx + (ev.clientX - startX)); root.style.setProperty(varName, (newPx / emPx).toFixed(2) + 'em'); syncTableWidths(); + if (isSc) syncScTableWidths(); } function onUp() { document.removeEventListener('mousemove', onMove); From 2a2642c319f8ea60fa447ac8017e005394e1a951 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Mar 2026 06:58:58 +0000 Subject: [PATCH 30/36] fix: widen sc-detail checkbox column from 2.2em to 3.2em The checkbox column in semantic changes detail tables was too narrow with table-layout: fixed, causing content to overflow with ellipsis. Increased sc checkbox column width to 3.2em (~1.5x). Main table checkbox column remains at 2.2em. https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- Services/HtmlReport/HtmlReportGenerateService.Css.cs | 6 +++--- Services/HtmlReport/HtmlReportGenerateService.Js.cs | 2 +- doc/samples/diff_report.html | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Services/HtmlReport/HtmlReportGenerateService.Css.cs b/Services/HtmlReport/HtmlReportGenerateService.Css.cs index ec794d49..85e238ad 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Css.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Css.cs @@ -143,8 +143,8 @@ private static string GetCss() table.semantic-changes-table th { padding: 3px 8px; border: 1px solid #bbb; background: #98989d; color: #fff; font-size: 11px; text-align: left; white-space: nowrap; } table.semantic-changes-table th.th-resizable { position: relative; } table.semantic-changes-table td { padding: 2px 8px; border: 1px solid #e0e0e0; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } - table.semantic-changes-table th.sc-col-cb { width: 2.2em; text-align: center; } - table.semantic-changes-table td.sc-col-cb { width: 2.2em; text-align: center; } + table.semantic-changes-table th.sc-col-cb { width: 3.2em; text-align: center; } + table.semantic-changes-table td.sc-col-cb { width: 3.2em; text-align: center; } /* sc-detail: checkbox(1) Class(2) BaseType(3) Change(4) Kind(5) Access(6) Modifiers(7) Type(8)… */ table.semantic-changes-table.sc-detail td:nth-child(4), table.semantic-changes-table.sc-detail td:nth-child(5), table.semantic-changes-table.sc-detail td:nth-child(6), table.semantic-changes-table.sc-detail td:nth-child(7) { text-align: center; } @@ -155,7 +155,7 @@ private static string GetCss() table.semantic-changes-table.sc-detail tr.group-cont td:nth-child(2) { border-top: hidden; } table.semantic-changes-table.sc-detail tr.group-cont td:nth-child(3) { border-top: hidden; } /* sc colgroup widths */ - col.sc-col-cb-g { width: 2.2em; } + col.sc-col-cb-g { width: 3.2em; } col.sc-col-class-g { width: var(--sc-class-w); } col.sc-col-basetype-g { width: var(--sc-basetype-w); } col.sc-col-change-g { width: 7em; } diff --git a/Services/HtmlReport/HtmlReportGenerateService.Js.cs b/Services/HtmlReport/HtmlReportGenerateService.Js.cs index cbc48011..7a766b94 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Js.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Js.cs @@ -199,7 +199,7 @@ private static void AppendJs(StringBuilder sb, string storageKey, string reportD sb.AppendLine(" var s = root.style.getPropertyValue(v) || getComputedStyle(root).getPropertyValue(v);"); sb.AppendLine(" return (parseFloat(s) || fb) * scEmPx;"); sb.AppendLine(" };"); - sb.AppendLine(" var detW = 2.2 * scEmPx"); + sb.AppendLine(" var detW = 3.2 * scEmPx"); sb.AppendLine(" + px('--sc-class-w', 14) + px('--sc-basetype-w', 16)"); sb.AppendLine(" + 7 * scEmPx + 10 * scEmPx + 8 * scEmPx + 11 * scEmPx"); sb.AppendLine(" + px('--sc-type-w', 12) + px('--sc-name-w', 10)"); diff --git a/doc/samples/diff_report.html b/doc/samples/diff_report.html index d41f1c1d..92d76b9e 100644 --- a/doc/samples/diff_report.html +++ b/doc/samples/diff_report.html @@ -141,8 +141,8 @@ table.semantic-changes-table th { padding: 3px 8px; border: 1px solid #bbb; background: #98989d; color: #fff; font-size: 11px; text-align: left; white-space: nowrap; } table.semantic-changes-table th.th-resizable { position: relative; } table.semantic-changes-table td { padding: 2px 8px; border: 1px solid #e0e0e0; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } - table.semantic-changes-table th.sc-col-cb { width: 2.2em; text-align: center; } - table.semantic-changes-table td.sc-col-cb { width: 2.2em; text-align: center; } + table.semantic-changes-table th.sc-col-cb { width: 3.2em; text-align: center; } + table.semantic-changes-table td.sc-col-cb { width: 3.2em; text-align: center; } table.semantic-changes-table.sc-detail td:nth-child(4), table.semantic-changes-table.sc-detail td:nth-child(5), table.semantic-changes-table.sc-detail td:nth-child(6), table.semantic-changes-table.sc-detail td:nth-child(7) { text-align: center; } /* sc-count: Class(1) Change(2) Count(3) — no checkbox column */ @@ -151,7 +151,7 @@ table.semantic-changes-table.sc-count tr.group-cont td:nth-child(1) { border-top: hidden; } table.semantic-changes-table.sc-detail tr.group-cont td:nth-child(2) { border-top: hidden; } table.semantic-changes-table.sc-detail tr.group-cont td:nth-child(3) { border-top: hidden; } - col.sc-col-cb-g { width: 2.2em; } + col.sc-col-cb-g { width: 3.2em; } col.sc-col-class-g { width: var(--sc-class-w); } col.sc-col-basetype-g { width: var(--sc-basetype-w); } col.sc-col-change-g { width: 7em; } @@ -854,7 +854,7 @@

[ ! ] Modified Files — Timestamps Regressed (2) var s = root.style.getPropertyValue(v) || getComputedStyle(root).getPropertyValue(v); return (parseFloat(s) || fb) * scEmPx; }; - var detW = 2.2 * scEmPx + var detW = 3.2 * scEmPx + px('--sc-class-w', 14) + px('--sc-basetype-w', 16) + 7 * scEmPx + 10 * scEmPx + 8 * scEmPx + 11 * scEmPx + px('--sc-type-w', 12) + px('--sc-name-w', 10) From cebbf7cc3a5f2b1defba1b030410daf318eb2b0e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Mar 2026 07:02:25 +0000 Subject: [PATCH 31/36] style: set semantic changes table cell background to white https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- Services/HtmlReport/HtmlReportGenerateService.Css.cs | 2 +- doc/samples/diff_report.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Services/HtmlReport/HtmlReportGenerateService.Css.cs b/Services/HtmlReport/HtmlReportGenerateService.Css.cs index 85e238ad..3e4ca125 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Css.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Css.cs @@ -142,7 +142,7 @@ private static string GetCss() table.semantic-changes-table { border-collapse: collapse; margin: 4px 0; font-size: 12px; table-layout: fixed; } table.semantic-changes-table th { padding: 3px 8px; border: 1px solid #bbb; background: #98989d; color: #fff; font-size: 11px; text-align: left; white-space: nowrap; } table.semantic-changes-table th.th-resizable { position: relative; } - table.semantic-changes-table td { padding: 2px 8px; border: 1px solid #e0e0e0; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + table.semantic-changes-table td { padding: 2px 8px; border: 1px solid #e0e0e0; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; background: #fff; } table.semantic-changes-table th.sc-col-cb { width: 3.2em; text-align: center; } table.semantic-changes-table td.sc-col-cb { width: 3.2em; text-align: center; } /* sc-detail: checkbox(1) Class(2) BaseType(3) Change(4) Kind(5) Access(6) Modifiers(7) Type(8)… */ diff --git a/doc/samples/diff_report.html b/doc/samples/diff_report.html index 92d76b9e..5e2f7c27 100644 --- a/doc/samples/diff_report.html +++ b/doc/samples/diff_report.html @@ -140,7 +140,7 @@ table.semantic-changes-table { border-collapse: collapse; margin: 4px 0; font-size: 12px; table-layout: fixed; } table.semantic-changes-table th { padding: 3px 8px; border: 1px solid #bbb; background: #98989d; color: #fff; font-size: 11px; text-align: left; white-space: nowrap; } table.semantic-changes-table th.th-resizable { position: relative; } - table.semantic-changes-table td { padding: 2px 8px; border: 1px solid #e0e0e0; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + table.semantic-changes-table td { padding: 2px 8px; border: 1px solid #e0e0e0; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; background: #fff; } table.semantic-changes-table th.sc-col-cb { width: 3.2em; text-align: center; } table.semantic-changes-table td.sc-col-cb { width: 3.2em; text-align: center; } table.semantic-changes-table.sc-detail td:nth-child(4), table.semantic-changes-table.sc-detail td:nth-child(5), From 63ed4e780d7aa88b379eef8453ffbad84f66b3b0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Mar 2026 07:16:30 +0000 Subject: [PATCH 32/36] docs: sync sample HTML, CHANGELOG, test guide, and add white-bg test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix colspan="9" → colspan="8" in doc/samples/diff_report.html (12 occurrences) to match the 8-column table generated by HtmlReportGenerateService - Add CHANGELOG entries (EN/JA) for white background on semantic changes td - Add AssemblyMethodAnalyzerTests and AssemblySemanticChangesSummaryTests to TESTING_GUIDE.md test scope map (EN/JA) - Add test GenerateDiffReportHtml_AssemblySemanticChanges_TdHasWhiteBackground https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- CHANGELOG.md | 4 ++++ .../HtmlReportGenerateServiceTests.cs | 20 ++++++++++++++++ doc/TESTING_GUIDE.md | 4 ++-- doc/samples/diff_report.html | 24 +++++++++---------- 4 files changed, 38 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fb8e7a3..891a56a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - Lightened the Assembly Semantic Changes table header background colour from `#6b6b6e` to `#98989d` in [`HtmlReportGenerateService.Css.cs`](Services/HtmlReport/HtmlReportGenerateService.Css.cs) for better readability. Added explicit `th.sc-col-cb` width styling to ensure the checkbox column header renders visibly. Updated [`doc/samples/diff_report.html`](doc/samples/diff_report.html) to match. Added test `GenerateDiffReportHtml_AssemblySemanticChanges_TableHeaderUsesLighterGray` to [`HtmlReportGenerateServiceTests`](FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs). +- Set the Assembly Semantic Changes table body cell (`td`) background to pure white (`#fff`) in [`HtmlReportGenerateService.Css.cs`](Services/HtmlReport/HtmlReportGenerateService.Css.cs) so that data rows are visually distinct from the surrounding `diff-row` grey background (`#f6f8fa`). Updated [`doc/samples/diff_report.html`](doc/samples/diff_report.html) to match. Fixed `colspan` in [`doc/samples/diff_report.html`](doc/samples/diff_report.html) from `9` to `8` to match the actual 8-column table layout generated by [`HtmlReportGenerateService.Sections.cs`](Services/HtmlReport/HtmlReportGenerateService.Sections.cs). Added test `GenerateDiffReportHtml_AssemblySemanticChanges_TdHasWhiteBackground` to [`HtmlReportGenerateServiceTests`](FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs). + ### [1.4.1] - 2026-03-20 #### Added @@ -401,6 +403,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - Assembly Semantic Changes テーブルヘッダの背景色を `#6b6b6e` から `#98989d` に明るく変更し視認性を改善。チェック列ヘッダの `th.sc-col-cb` に明示的な幅スタイルを追加。[`doc/samples/diff_report.html`](doc/samples/diff_report.html) を同期。テスト `GenerateDiffReportHtml_AssemblySemanticChanges_TableHeaderUsesLighterGray` を [`HtmlReportGenerateServiceTests`](FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs) に追加。 +- Assembly Semantic Changes テーブルのデータセル(`td`)の背景色を純白(`#fff`)に設定。[`HtmlReportGenerateService.Css.cs`](Services/HtmlReport/HtmlReportGenerateService.Css.cs) を修正。周囲の `diff-row` グレー背景(`#f6f8fa`)との視覚的区別を向上。[`doc/samples/diff_report.html`](doc/samples/diff_report.html) を同期。同サンプルの `colspan` を実際の 8 列レイアウトに合わせ `9` から `8` に修正。テスト `GenerateDiffReportHtml_AssemblySemanticChanges_TdHasWhiteBackground` を [`HtmlReportGenerateServiceTests`](FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs) に追加。 + ### [1.4.1] - 2026-03-20 #### 追加 diff --git a/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs b/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs index bea34524..6a533df5 100644 --- a/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs @@ -919,6 +919,26 @@ public void GenerateDiffReportHtml_AssemblySemanticChanges_ThScColCbCssRuleExist Assert.Contains("table.semantic-changes-table td.sc-col-cb", html); } + /// + /// Verifies the semantic changes table td cells have white background styling. + /// セマンティック変更テーブルの td セルに白背景スタイルが適用されていることを確認する。 + /// + [Fact] + public void GenerateDiffReportHtml_AssemblySemanticChanges_TdHasWhiteBackground() + { + var (oldDir, newDir, reportDir) = MakeDirs("sc-td-bg"); + var config = CreateConfig(); + + _service.GenerateDiffReportHtml(oldDir, newDir, reportDir, + appVersion: "1.0", elapsedTimeString: null, + computerName: "test-host", config); + + var html = File.ReadAllText(Path.Combine(reportDir, HtmlReportGenerateService.DIFF_REPORT_HTML_FILE_NAME)); + // td cells in the semantic-changes-table must have white background + // semantic-changes-table の td セルは白背景であること + Assert.Contains("background: #fff", html); + } + private static ConfigSettings CreateConfig(bool enableInlineDiff = true, bool lazyRender = false) => new() { IgnoredExtensions = new List(), diff --git a/doc/TESTING_GUIDE.md b/doc/TESTING_GUIDE.md index 638cbf0c..0d66b3bc 100644 --- a/doc/TESTING_GUIDE.md +++ b/doc/TESTING_GUIDE.md @@ -26,7 +26,7 @@ Current tree has `548` total tests in the latest full run (`dotnet test FolderDi | --- | --- | --- | | Entry and configuration | [`ProgramTests`](../FolderDiffIL4DotNet.Tests/ProgramTests.cs), [`ProgramRunnerTests`](../FolderDiffIL4DotNet.Tests/ProgramRunnerTests.cs), [`ConfigServiceTests`](../FolderDiffIL4DotNet.Tests/Services/ConfigServiceTests.cs), [`ConfigSettingsTests`](../FolderDiffIL4DotNet.Tests/Models/ConfigSettingsTests.cs) | `Main` exit codes, typed [`ProgramRunner`](../ProgramRunner.cs) exit-code mapping for invalid arguments vs. config failures, phase ordering around validation vs. config loading, minimal end-to-end execution, code-defined config defaults and override behavior, MD5/timestamp warning console and report output, reflection-backed verification of internal IL cache defaults wired by [`ProgramRunner`](../ProgramRunner.cs); JSON syntax error reporting: trailing comma in object (`{"Key":"v",}`), trailing comma in array (`["a","b",]`), and multiline JSON with line-number verification in the [`InvalidDataException`](https://learn.microsoft.com/en-us/dotnet/api/system.io.invaliddataexception?view=net-8.0) message | | Core diff flow | [`FolderDiffExecutionStrategyTests`](../FolderDiffIL4DotNet.Tests/Services/FolderDiffExecutionStrategyTests.cs), [`FolderDiffServiceTests`](../FolderDiffIL4DotNet.Tests/Services/FolderDiffServiceTests.cs), [`FolderDiffServiceUnitTests`](../FolderDiffIL4DotNet.Tests/Services/FolderDiffServiceUnitTests.cs), [`FileDiffServiceTests`](../FolderDiffIL4DotNet.Tests/Services/FileDiffServiceTests.cs), [`FileDiffServiceUnitTests`](../FolderDiffIL4DotNet.Tests/Services/FileDiffServiceUnitTests.cs), [`FileDiffResultListsTests`](../FolderDiffIL4DotNet.Tests/Models/FileDiffResultListsTests.cs) | Discovery filtering, auto-parallelism policy, classification (`Unchanged/Added/Removed/Modified`), diff detail labels, timestamp-regression detection only for **modified** files (unchanged files with reversed timestamps produce no warning), reset behavior, case-insensitive extension handling, propagated text-diff fallback behavior, permission/I/O failure handling, expected-vs-unexpected exception logging/rethrow behavior, large-batch classification without real disk I/O, IL-precompute batching for large trees, memory-budget-based throttling of large-text chunk comparison, multi-megabyte real-file text comparison, symlink-backed file classification, per-file hash/IL/text error handling without real disk, symlink-loop [`IOException`](https://learn.microsoft.com/en-us/dotnet/api/system.io.ioexception?view=net-8.0) during enumeration (logged and rethrown), [`FileNotFoundException`](https://learn.microsoft.com/en-us/dotnet/api/system.io.filenotfoundexception?view=net-8.0) during per-file comparison classified as `Removed` with a warning (both sequential and parallel modes), `DiffSummaryStatistics`/`SummaryStatistics` snapshot correctness | -| IL/disassembler behavior | [`ILOutputServiceTests`](../FolderDiffIL4DotNet.Tests/Services/ILOutputServiceTests.cs), [`DotNetDisassembleServiceTests`](../FolderDiffIL4DotNet.Tests/Services/DotNetDisassembleServiceTests.cs), [`DisassemblerBlacklistTests`](../FolderDiffIL4DotNet.Tests/Services/DisassemblerBlacklistTests.cs), [`DisassemblerHelperTests`](../FolderDiffIL4DotNet.Tests/Services/DisassemblerHelperTests.cs), [`DotNetDisassemblerCacheTests`](../FolderDiffIL4DotNet.Tests/Services/Caching/DotNetDisassemblerCacheTests.cs), [`DotNetDetectorTests`](../FolderDiffIL4DotNet.Tests/Core/Diagnostics/DotNetDetectorTests.cs) | Same-disassembler pairing, fallback behavior, blacklist logic (including TTL-boundary expiry: entry removed and tool retried after the 10-minute blacklist window), independent per-tool blacklist state, null/whitespace safety for `RegisterFailure`/`ResetFailure` (explicit branch coverage for the null guard true-branch), reset of non-existent command, concurrent `RegisterFailure` with 32 threads, concurrent TTL-expiry race with no exceptions, detection and command handling, failure-vs-non-.NET detection semantics; `ResolveExecutablePath` branch coverage for relative paths containing directory separators (found and not found), whitespace-only `PATH` environment variable, `PATH` entries that are empty strings, and commands found via `PATH` search; Windows-only `EnumerateExecutableNames` tests verifying no duplicate `.exe`/`.cmd`/`.bat` extension variants when the command already carries those suffixes | +| IL/disassembler behavior | [`ILOutputServiceTests`](../FolderDiffIL4DotNet.Tests/Services/ILOutputServiceTests.cs), [`DotNetDisassembleServiceTests`](../FolderDiffIL4DotNet.Tests/Services/DotNetDisassembleServiceTests.cs), [`DisassemblerBlacklistTests`](../FolderDiffIL4DotNet.Tests/Services/DisassemblerBlacklistTests.cs), [`DisassemblerHelperTests`](../FolderDiffIL4DotNet.Tests/Services/DisassemblerHelperTests.cs), [`DotNetDisassemblerCacheTests`](../FolderDiffIL4DotNet.Tests/Services/Caching/DotNetDisassemblerCacheTests.cs), [`DotNetDetectorTests`](../FolderDiffIL4DotNet.Tests/Core/Diagnostics/DotNetDetectorTests.cs), [`AssemblyMethodAnalyzerTests`](../FolderDiffIL4DotNet.Tests/Services/AssemblyMethodAnalyzerTests.cs), [`AssemblySemanticChangesSummaryTests`](../FolderDiffIL4DotNet.Tests/Models/AssemblySemanticChangesSummaryTests.cs) | Same-disassembler pairing, fallback behavior, blacklist logic (including TTL-boundary expiry: entry removed and tool retried after the 10-minute blacklist window), independent per-tool blacklist state, null/whitespace safety for `RegisterFailure`/`ResetFailure` (explicit branch coverage for the null guard true-branch), reset of non-existent command, concurrent `RegisterFailure` with 32 threads, concurrent TTL-expiry race with no exceptions, detection and command handling, failure-vs-non-.NET detection semantics; `ResolveExecutablePath` branch coverage for relative paths containing directory separators (found and not found), whitespace-only `PATH` environment variable, `PATH` entries that are empty strings, and commands found via `PATH` search; Windows-only `EnumerateExecutableNames` tests verifying no duplicate `.exe`/`.cmd`/`.bat` extension variants when the command already carries those suffixes | | Real disassembler E2E | [`RealDisassemblerE2ETests`](../FolderDiffIL4DotNet.Tests/Services/RealDisassemblerE2ETests.cs) | Builds the same small class library twice with `Deterministic=false`, confirms the rebuilt DLLs differ by MD5, and verifies that [`dotnet-ildasm`](https://www.nuget.org/packages/dotnet-ildasm/) still classifies them as `ILMatch` after MVID filtering | | Caching | [`ILCacheTests`](../FolderDiffIL4DotNet.Tests/Services/Caching/ILCacheTests.cs) | memory/disk cache semantics, same-key updates at capacity, eviction coordination, keying behavior | | Reporting/logging/progress | [`ReportGenerateServiceTests`](../FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs), [`HtmlReportGenerateServiceTests`](../FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs), [`LoggerServiceTests`](../FolderDiffIL4DotNet.Tests/Services/LoggerServiceTests.cs), [`ProgressReportServiceTests`](../FolderDiffIL4DotNet.Tests/Services/ProgressReportServiceTests.cs) | report sections/summary formatting, report-only warning responsibility, HTML report file creation, interactive checkbox/input element presence, section colour coding, localStorage sentinel, `ShouldGenerateHtmlReport=false` skip, HTML encoding of special characters, inline diff summary `#N` numbering aligned with the leftmost table column, log output behavior, shared log-file/date formats, progress reporting lifecycle, Unicode filenames (Japanese/Umlaut/Chinese) in Modified and Unchanged sections, large-file-count (10,500) summary statistics correctness, `InlineDiffMaxDiffLines` suppression (diff computed first; skipped when diff output line count exceeds threshold) | @@ -196,7 +196,7 @@ Workflow/config files: [`.github/workflows/dotnet.yml`](../.github/workflows/dot | --- | --- | --- | | エントリーポイント/設定 | [`ProgramTests`](../FolderDiffIL4DotNet.Tests/ProgramTests.cs), [`ProgramRunnerTests`](../FolderDiffIL4DotNet.Tests/ProgramRunnerTests.cs), [`ConfigServiceTests`](../FolderDiffIL4DotNet.Tests/Services/ConfigServiceTests.cs), [`ConfigSettingsTests`](../FolderDiffIL4DotNet.Tests/Models/ConfigSettingsTests.cs) | `Main` の終了コード、引数不正と設定失敗を分ける [`ProgramRunner`](../ProgramRunner.cs) の型付き終了コード分類、引数検証と設定読込の順序、最小構成の実行、コード既定値と override の設定挙動、更新日時警告のコンソール/レポート出力、[`ProgramRunner`](../ProgramRunner.cs) が内部 IL キャッシュ既定値をどう配線するかの検証、JSON 書式エラーの報告(オブジェクト末尾カンマ `{"Key":"v",}`、配列末尾カンマ `["a","b",]`、複数行 JSON での [`InvalidDataException`](https://learn.microsoft.com/ja-jp/dotnet/api/system.io.invaliddataexception?view=net-8.0) メッセージへの行番号付与) | | 差分処理本体 | [`FolderDiffExecutionStrategyTests`](../FolderDiffIL4DotNet.Tests/Services/FolderDiffExecutionStrategyTests.cs), [`FolderDiffServiceTests`](../FolderDiffIL4DotNet.Tests/Services/FolderDiffServiceTests.cs), [`FolderDiffServiceUnitTests`](../FolderDiffIL4DotNet.Tests/Services/FolderDiffServiceUnitTests.cs), [`FileDiffServiceTests`](../FolderDiffIL4DotNet.Tests/Services/FileDiffServiceTests.cs), [`FileDiffServiceUnitTests`](../FolderDiffIL4DotNet.Tests/Services/FileDiffServiceUnitTests.cs), [`FileDiffResultListsTests`](../FolderDiffIL4DotNet.Tests/Models/FileDiffResultListsTests.cs) | 列挙フィルタ、自動並列度ポリシー、`Unchanged/Added/Removed/Modified` の分類、判定理由、**Modified と判定されたファイルのみ**を対象とした更新日時逆転検出(Unchanged ファイルは更新日時が逆転しても警告対象外)、状態リセット、拡張子大小無視、伝播したテキスト比較例外からのフォールバック、権限エラー/出力先 I/O 失敗、想定例外と想定外例外のログ/再スロー境界、大量ファイルの扱い、大規模ツリー向け IL 事前計算バッチ化、大きいテキスト比較のメモリ予算ベース抑制、複数 MiB の実ファイル比較、シンボリックリンク経由の分類、ファイル単位のハッシュ/IL/テキスト分岐の異常系、列挙時のシンボリックリンクループ [`IOException`](https://learn.microsoft.com/ja-jp/dotnet/api/system.io.ioexception?view=net-8.0)(ログ出力のうえ再スロー)、比較前ファイル削除時の [`FileNotFoundException`](https://learn.microsoft.com/ja-jp/dotnet/api/system.io.filenotfoundexception?view=net-8.0) を `Removed` 分類+警告(逐次・並列両対応)、`DiffSummaryStatistics`/`SummaryStatistics` のスナップショット正確性 | -| IL/逆アセンブラ | [`ILOutputServiceTests`](../FolderDiffIL4DotNet.Tests/Services/ILOutputServiceTests.cs), [`DotNetDisassembleServiceTests`](../FolderDiffIL4DotNet.Tests/Services/DotNetDisassembleServiceTests.cs), [`DisassemblerBlacklistTests`](../FolderDiffIL4DotNet.Tests/Services/DisassemblerBlacklistTests.cs), [`DisassemblerHelperTests`](../FolderDiffIL4DotNet.Tests/Services/DisassemblerHelperTests.cs), [`DotNetDisassemblerCacheTests`](../FolderDiffIL4DotNet.Tests/Services/Caching/DotNetDisassemblerCacheTests.cs), [`DotNetDetectorTests`](../FolderDiffIL4DotNet.Tests/Core/Diagnostics/DotNetDetectorTests.cs) | 同一逆アセンブラ比較、フォールバック、ブラックリスト(TTL 境界: 10 分のブラックリスト期間経過後にエントリが削除され、ツールが再試行されることを含む)、ツール独立状態、`RegisterFailure`/`ResetFailure` の null/空白文字ガード(null ガードの true 分岐を明示的にカバー)、存在しないコマンドの reset、32 スレッドの並行 `RegisterFailure`、TTL 切れ境界での並行呼び出し(例外なし)、検出・コマンド処理、判定失敗と非 .NET の区別;`ResolveExecutablePath` のブランチ網羅(ディレクトリ区切り文字を含む相対パス(存在あり/なし)、空白のみの `PATH` 環境変数、空文字列の PATH エントリ、PATH 検索によるコマンド発見);Windows 専用 `EnumerateExecutableNames` テスト(`.exe`/`.cmd`/`.bat` 拡張子を持つコマンドに重複拡張子が追加されないことを検証) | +| IL/逆アセンブラ | [`ILOutputServiceTests`](../FolderDiffIL4DotNet.Tests/Services/ILOutputServiceTests.cs), [`DotNetDisassembleServiceTests`](../FolderDiffIL4DotNet.Tests/Services/DotNetDisassembleServiceTests.cs), [`DisassemblerBlacklistTests`](../FolderDiffIL4DotNet.Tests/Services/DisassemblerBlacklistTests.cs), [`DisassemblerHelperTests`](../FolderDiffIL4DotNet.Tests/Services/DisassemblerHelperTests.cs), [`DotNetDisassemblerCacheTests`](../FolderDiffIL4DotNet.Tests/Services/Caching/DotNetDisassemblerCacheTests.cs), [`DotNetDetectorTests`](../FolderDiffIL4DotNet.Tests/Core/Diagnostics/DotNetDetectorTests.cs), [`AssemblyMethodAnalyzerTests`](../FolderDiffIL4DotNet.Tests/Services/AssemblyMethodAnalyzerTests.cs), [`AssemblySemanticChangesSummaryTests`](../FolderDiffIL4DotNet.Tests/Models/AssemblySemanticChangesSummaryTests.cs) | 同一逆アセンブラ比較、フォールバック、ブラックリスト(TTL 境界: 10 分のブラックリスト期間経過後にエントリが削除され、ツールが再試行されることを含む)、ツール独立状態、`RegisterFailure`/`ResetFailure` の null/空白文字ガード(null ガードの true 分岐を明示的にカバー)、存在しないコマンドの reset、32 スレッドの並行 `RegisterFailure`、TTL 切れ境界での並行呼び出し(例外なし)、検出・コマンド処理、判定失敗と非 .NET の区別;`ResolveExecutablePath` のブランチ網羅(ディレクトリ区切り文字を含む相対パス(存在あり/なし)、空白のみの `PATH` 環境変数、空文字列の PATH エントリ、PATH 検索によるコマンド発見);Windows 専用 `EnumerateExecutableNames` テスト(`.exe`/`.cmd`/`.bat` 拡張子を持つコマンドに重複拡張子が追加されないことを検証) | | 実逆アセンブラ E2E | [`RealDisassemblerE2ETests`](../FolderDiffIL4DotNet.Tests/Services/RealDisassemblerE2ETests.cs) | `Deterministic=false` の同一クラスライブラリを 2 回ビルドし、再ビルド DLL が MD5 では不一致でも、[`dotnet-ildasm`](https://www.nuget.org/packages/dotnet-ildasm/) では MVID 除外後に `ILMatch` になることを検証 | | キャッシュ | [`ILCacheTests`](../FolderDiffIL4DotNet.Tests/Services/Caching/ILCacheTests.cs) | メモリ/ディスクキャッシュの保持、同一キー再保存、退避時の連動削除、キー生成 | | レポート/ログ/進捗 | [`ReportGenerateServiceTests`](../FolderDiffIL4DotNet.Tests/Services/ReportGenerateServiceTests.cs)、[`HtmlReportGenerateServiceTests`](../FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs)、[`LoggerServiceTests`](../FolderDiffIL4DotNet.Tests/Services/LoggerServiceTests.cs)、[`ProgressReportServiceTests`](../FolderDiffIL4DotNet.Tests/Services/ProgressReportServiceTests.cs) | レポート出力内容、ログ動作、共有ログ書式、進捗報告ライフサイクル、HTML レポートのファイル生成・チェックボックス/入力要素の存在・セクション色付け・localStorage センチネル・`ShouldGenerateHtmlReport=false` スキップ・特殊文字の HTML エンコード、インライン差分サマリーの `#N` 番号と左端 `#` 列の整合、Modified・Unchanged セクションでの Unicode ファイル名(日本語/ウムラウト/中国語)のラウンドトリップ、10,500 件の大件数サマリー統計の正確性、`InlineDiffMaxDiffLines` による抑制(差分を計算した後、差分出力行数が閾値を超えた場合にスキップ) | diff --git a/doc/samples/diff_report.html b/doc/samples/diff_report.html index 5e2f7c27..9ac5b1a9 100644 --- a/doc/samples/diff_report.html +++ b/doc/samples/diff_report.html @@ -438,7 +438,7 @@

[ * ] Modified Files (9)

- +
#1 Show diff (+2 / -1)
@@ -465,14 +465,14 @@

[ * ] Modified Files (9)

dotnet-ildasm (version: 0.12.2) - +
#3 Show assembly semantic changes
- +
#3 Show IL diff (+3 / -2)
@@ -489,7 +489,7 @@

[ * ] Modified Files (9)

- +
#4 Show diff (+2 / -2)
@@ -506,14 +506,14 @@

[ * ] Modified Files (9)

dotnet-ildasm (version: 0.12.2) - +
#5 Show assembly semantic changes
- +
#5 Show IL diff (+2 / -2)
@@ -530,7 +530,7 @@

[ * ] Modified Files (9)

- +
#6 Show diff (+1 / -1)
@@ -547,7 +547,7 @@

[ * ] Modified Files (9)

-

#7 Inline diff skipped: edit distance too large (>4000 insertions/deletions in 2001 vs 2001 lines). Increase InlineDiffMaxEditDistance in config to raise the limit.

+

#7 Inline diff skipped: edit distance too large (>4000 insertions/deletions in 2001 vs 2001 lines). Increase InlineDiffMaxEditDistance in config to raise the limit.

8 @@ -560,7 +560,7 @@

[ * ] Modified Files (9)

-

#8 Inline diff skipped: diff too large (12500 diff lines; limit is 10000). Increase InlineDiffMaxDiffLines in config to enable.

+

#8 Inline diff skipped: diff too large (12500 diff lines; limit is 10000). Increase InlineDiffMaxDiffLines in config to enable.

9 @@ -573,14 +573,14 @@

[ * ] Modified Files (9)

dotnet-ildasm (version: 0.12.2) - +
#9 Show assembly semantic changes
- +
#9 Show IL diff (+1 / -1)
@@ -660,7 +660,7 @@

[ ! ] Modified Files — Timestamps Regressed (2) dotnet-ildasm (version: 0.12.2) - +
#2 Show IL diff (+2 / -2)
From 7dd70d7353768bb40906660b06a4f83d48d07782 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Mar 2026 07:21:11 +0000 Subject: [PATCH 33/36] docs: consolidate CHANGELOG entries for squash merge Merge all Unreleased Added/Changed entries into a single structured Added entry covering the full Assembly Semantic Changes feature. https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- CHANGELOG.md | 54 +++++++++++++++++++--------------------------------- 1 file changed, 20 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 891a56a9..68983d85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,23 +11,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), #### Added -- Added member-level change detection for `ILMismatch` assemblies using `System.Reflection.Metadata`. For each modified .NET assembly, the report now shows type/method/property/field additions, removals, and method body changes. This new **Assembly Semantic Changes** section appears between **Summary** and **IL Cache Stats** in the Markdown report, and as an expandable inline row above the IL diff in the HTML report. Controlled by the new `ShouldIncludeAssemblySemanticChangesInReport` config setting (default: `true`). Added [`AssemblyMethodAnalyzer`](Services/AssemblyMethodAnalyzer.cs), [`AssemblySemanticChangesSummary`](Models/AssemblySemanticChangesSummary.cs), and corresponding tests. - -- Restructured the Assembly Semantic Changes table from 9 columns to 10 columns for clarity. Split the former `ReturnType (Type paramName)` column into separate `ReturnType` and `Parameters` columns. Moved `Kind` column before `Access` and `Modifiers` for better readability. Added `Constructor` and `StaticConstructor` as new Kind values (previously `.ctor`/`.cctor` were shown as `Method`). Constructors display the C# class name instead of `.ctor`. The `Type` column shows the declared type for Field/Property entries only. Empty Access/Modifiers cells no longer render as empty backticks. Changed `Method count` label to `Member count`. Renamed section from `Method-Level Changes` to `Assembly Semantic Changes`. Added record type and field variable samples to [`doc/samples/diff_report.md`](doc/samples/diff_report.md). Added bilingual **Assembly Semantic Changes** section to [README.md](README.md). - -- Refined Kind values for type entries: replaced generic `Type` with specific `Class`, `Record`, `Struct`, `Interface`, and `Enum` kinds. Record types are detected heuristically by the presence of an `EqualityContract` property. Type entries now show access modifiers (`public`, `internal`, `protected`, etc.) instead of leaving the Access column empty. Removed the redundant `Assembly` column from the semantic changes table (10→9 columns) since the assembly name is already shown as the section header. Enriched sample reports with `protected` access modifier examples, `Removed` type entries, Added+Removed pairs (move/rename pattern), method overload examples, and custom application-defined type properties. - -- Removed parentheses from the `Parameters` column values. Since Parameters is now an independent column, wrapping values in `(…)` is redundant. Values now display as `string name, int count = 0` instead of `(string name, int count = 0)`. Empty parameter lists display as blank instead of `()`. - -- Added `Body` column (10th column, rightmost) to the Assembly Semantic Changes table. Displays `Changed` when a method body or field initializer has been modified at the IL level; otherwise empty. Entries with body changes use `Modified` in the Change column. Replaced the `Member count: N (Old) vs N (New)` summary line with `Added: N, Removed: N, Modified: N` counts computed from entries. Removed `OldMethodCount`/`NewMethodCount` properties from `AssemblySemanticChangesSummary` in favour of computed `AddedCount`, `RemovedCount`, and `ModifiedCount` properties. - -#### Changed - -- Prevented multi-word Modifiers (e.g. `static literal`, `static readonly`) from wrapping in the Markdown report by applying `NoWrapMd` (non-breaking space) to the Modifiers column values in [`ReportGenerateService.SectionWriters.cs`](Services/ReportGenerateService.SectionWriters.cs). Updated [`doc/samples/diff_report.md`](doc/samples/diff_report.md) to match. - -- Lightened the Assembly Semantic Changes table header background colour from `#6b6b6e` to `#98989d` in [`HtmlReportGenerateService.Css.cs`](Services/HtmlReport/HtmlReportGenerateService.Css.cs) for better readability. Added explicit `th.sc-col-cb` width styling to ensure the checkbox column header renders visibly. Updated [`doc/samples/diff_report.html`](doc/samples/diff_report.html) to match. Added test `GenerateDiffReportHtml_AssemblySemanticChanges_TableHeaderUsesLighterGray` to [`HtmlReportGenerateServiceTests`](FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs). - -- Set the Assembly Semantic Changes table body cell (`td`) background to pure white (`#fff`) in [`HtmlReportGenerateService.Css.cs`](Services/HtmlReport/HtmlReportGenerateService.Css.cs) so that data rows are visually distinct from the surrounding `diff-row` grey background (`#f6f8fa`). Updated [`doc/samples/diff_report.html`](doc/samples/diff_report.html) to match. Fixed `colspan` in [`doc/samples/diff_report.html`](doc/samples/diff_report.html) from `9` to `8` to match the actual 8-column table layout generated by [`HtmlReportGenerateService.Sections.cs`](Services/HtmlReport/HtmlReportGenerateService.Sections.cs). Added test `GenerateDiffReportHtml_AssemblySemanticChanges_TdHasWhiteBackground` to [`HtmlReportGenerateServiceTests`](FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs). +- Added **Assembly Semantic Changes** — member-level change detection for `ILMismatch` assemblies using `System.Reflection.Metadata`. For each modified .NET assembly, the report now shows type/method/property/field additions, removals, and method body changes. Controlled by the new `ShouldIncludeAssemblySemanticChangesInReport` config setting (default: `true`). + - **Report placement**: appears between **Summary** and **IL Cache Stats** in the Markdown report; shown as an expandable inline row above the IL diff in the HTML report. + - **Table columns** (12 in HTML including checkbox; 11 in Markdown): `✓` (checkbox, HTML only), `Class` (fully qualified type name), `BaseType` (base class + interfaces), `Change` (`Added`/`Removed`/`Modified`), `Kind` (`Class`/`Record`/`Struct`/`Interface`/`Enum`/`Constructor`/`StaticConstructor`/`Method`/`Property`/`Field`), `Access` (`public`/`internal`/`protected`/`private`), `Modifiers` (`static`/`abstract`/`virtual`/`override`/`sealed`/`readonly`/`const`/`static literal`/`static readonly`), `Type` (declared type for Field/Property only), `Name`, `ReturnType`, `Parameters` (displayed without parentheses), `Body` (`Changed` when method body or field initializer IL has changed; otherwise empty). + - **Summary count table**: a second table grouped by Class showing `Added`/`Removed`/`Modified` counts. Consecutive rows for the same class merge the Class cell. + - **Record detection**: records are identified heuristically by the presence of an `EqualityContract` property. + - **HTML styling**: table header background `#98989d` (light grey), data cell (`td`) background `#fff` (white) for contrast against the `diff-row` grey, per-row checkbox with auto-save, resizable columns via CSS custom properties and drag handles. + - **Markdown styling**: multi-word Modifiers (e.g. `static literal`) use non-breaking spaces to prevent wrapping. + - **New files**: [`AssemblyMethodAnalyzer`](Services/AssemblyMethodAnalyzer.cs), [`AssemblySemanticChangesSummary`](Models/AssemblySemanticChangesSummary.cs) (with computed `AddedCount`/`RemovedCount`/`ModifiedCount` properties). + - **Tests**: [`AssemblyMethodAnalyzerTests`](FolderDiffIL4DotNet.Tests/Services/AssemblyMethodAnalyzerTests.cs), [`AssemblySemanticChangesSummaryTests`](FolderDiffIL4DotNet.Tests/Models/AssemblySemanticChangesSummaryTests.cs), and new assertions in [`HtmlReportGenerateServiceTests`](FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs) (`TableHeaderUsesLighterGray`, `ThScColCbCssRuleExists`, `TdHasWhiteBackground`). + - **Docs**: added bilingual **Assembly Semantic Changes** section to [README.md](README.md), [`AssemblyMethodAnalyzerTests`](FolderDiffIL4DotNet.Tests/Services/AssemblyMethodAnalyzerTests.cs) and [`AssemblySemanticChangesSummaryTests`](FolderDiffIL4DotNet.Tests/Models/AssemblySemanticChangesSummaryTests.cs) to [TESTING_GUIDE.md](doc/TESTING_GUIDE.md), updated [`doc/samples/diff_report.html`](doc/samples/diff_report.html) and [`doc/samples/diff_report.md`](doc/samples/diff_report.md). ### [1.4.1] - 2026-03-20 @@ -387,23 +380,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), #### 追加 -- `System.Reflection.Metadata` を使用した `ILMismatch` アセンブリのメンバーレベル変更検出を追加。変更のあった各 .NET アセンブリについて、型・メソッド・プロパティ・フィールドの増減およびメソッドボディの変更をレポートに出力します。Markdown レポートでは **Summary** と **IL Cache Stats** の間に **Assembly Semantic Changes** セクションとして表示され、HTML レポートでは IL diff の上に展開可能なインライン行として表示されます。新しい設定項目 `ShouldIncludeAssemblySemanticChangesInReport`(既定: `true`)で制御可能。[`AssemblyMethodAnalyzer`](Services/AssemblyMethodAnalyzer.cs)、[`AssemblySemanticChangesSummary`](Models/AssemblySemanticChangesSummary.cs)、および対応するテストを追加。 - -- Assembly Semantic Changes テーブルを 9 列から 10 列に再構成し明確化。旧 `ReturnType (Type paramName)` 列を `ReturnType` 列と `Parameters` 列に分離。`Kind` 列を `Access`・`Modifiers` の前に移動。Kind 値に `Constructor` と `StaticConstructor` を追加(従来 `.ctor`/`.cctor` は `Method` として表示)。コンストラクタは `.ctor` ではなく C# のクラス名で表示。`Type` 列は Field/Property の宣言型のみを表示。空の Access/Modifiers セルは空バッククォートではなく空欄に。`Method count` ラベルを `Member count` に変更。セクション名を `Method-Level Changes` から `Assembly Semantic Changes` に改名。[`doc/samples/diff_report.md`](doc/samples/diff_report.md) に record 型およびフィールド変数のサンプルを追加。[README.md](README.md) にバイリンガルの **アセンブリ セマンティック変更** セクションを追加。 - -- Kind 値を詳細化: 汎用の `Type` を `Class`、`Record`、`Struct`、`Interface`、`Enum` に置き換え。Record 型は `EqualityContract` プロパティの有無で推定。型エントリの Access 列に空欄ではなくアクセス修飾子(`public`、`internal`、`protected` 等)を表示。セクションヘッダに既にアセンブリ名が表示されるため冗長な `Assembly` 列を削除(10→9 列)。サンプルレポートに `protected` アクセス修飾子、`Removed` 型エントリ、Added+Removed ペア(移動/リネームパターン)、メソッドオーバーロード、アプリケーション独自型のプロパティを追加。 - -- `Parameters` 列の値から括弧を削除。Parameters が独立列となったため `(…)` は冗長。値は `(string name, int count = 0)` ではなく `string name, int count = 0` で表示。引数なしは `()` ではなく空欄。 - -- Assembly Semantic Changes テーブルに `Body` 列(10 列目、最右端)を追加。メソッドボディまたはフィールド初期化子が IL レベルで変更された場合に `Changed` を表示、それ以外は空欄。ボディ変更があるエントリの Change 列は `Modified`。集計行を `Member count: N (Old) vs N (New)` から `Added: N, Removed: N, Modified: N`(エントリから算出)に変更。`AssemblySemanticChangesSummary` の `OldMethodCount`/`NewMethodCount` プロパティを削除し、算出プロパティ `AddedCount`、`RemovedCount`、`ModifiedCount` に置き換え。 - -#### 変更 - -- Markdown レポートの Modifiers 列に `NoWrapMd`(ノーブレークスペース)を適用し、`static literal` や `static readonly` などの複数語修飾子が折り返されないよう修正。[`ReportGenerateService.SectionWriters.cs`](Services/ReportGenerateService.SectionWriters.cs) を修正。[`doc/samples/diff_report.md`](doc/samples/diff_report.md) を同期。 - -- Assembly Semantic Changes テーブルヘッダの背景色を `#6b6b6e` から `#98989d` に明るく変更し視認性を改善。チェック列ヘッダの `th.sc-col-cb` に明示的な幅スタイルを追加。[`doc/samples/diff_report.html`](doc/samples/diff_report.html) を同期。テスト `GenerateDiffReportHtml_AssemblySemanticChanges_TableHeaderUsesLighterGray` を [`HtmlReportGenerateServiceTests`](FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs) に追加。 - -- Assembly Semantic Changes テーブルのデータセル(`td`)の背景色を純白(`#fff`)に設定。[`HtmlReportGenerateService.Css.cs`](Services/HtmlReport/HtmlReportGenerateService.Css.cs) を修正。周囲の `diff-row` グレー背景(`#f6f8fa`)との視覚的区別を向上。[`doc/samples/diff_report.html`](doc/samples/diff_report.html) を同期。同サンプルの `colspan` を実際の 8 列レイアウトに合わせ `9` から `8` に修正。テスト `GenerateDiffReportHtml_AssemblySemanticChanges_TdHasWhiteBackground` を [`HtmlReportGenerateServiceTests`](FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs) に追加。 +- **Assembly Semantic Changes** を追加 — `System.Reflection.Metadata` を使用した `ILMismatch` アセンブリのメンバーレベル変更検出。変更のあった各 .NET アセンブリについて、型・メソッド・プロパティ・フィールドの増減およびメソッドボディの変更をレポートに出力します。新しい設定項目 `ShouldIncludeAssemblySemanticChangesInReport`(既定: `true`)で制御可能。 + - **レポート配置**: Markdown レポートでは **Summary** と **IL Cache Stats** の間に表示。HTML レポートでは IL diff の上に展開可能なインライン行として表示。 + - **テーブル列**(HTML: チェックボックス含む 12 列、Markdown: 11 列): `✓`(チェックボックス、HTML のみ)、`Class`(完全修飾型名)、`BaseType`(基底クラス+インターフェース)、`Change`(`Added`/`Removed`/`Modified`)、`Kind`(`Class`/`Record`/`Struct`/`Interface`/`Enum`/`Constructor`/`StaticConstructor`/`Method`/`Property`/`Field`)、`Access`(`public`/`internal`/`protected`/`private`)、`Modifiers`(`static`/`abstract`/`virtual`/`override`/`sealed`/`readonly`/`const`/`static literal`/`static readonly`)、`Type`(Field/Property の宣言型のみ)、`Name`、`ReturnType`、`Parameters`(括弧なしで表示)、`Body`(メソッドボディまたはフィールド初期化子の IL 変更時に `Changed`、それ以外は空欄)。 + - **集計テーブル**: Class 別に `Added`/`Removed`/`Modified` の件数を表示する第二テーブル。同一クラスの連続行は Class セルを結合。 + - **Record 検出**: `EqualityContract` プロパティの有無で Record 型をヒューリスティックに判定。 + - **HTML スタイル**: テーブルヘッダ背景 `#98989d`(ライトグレー)、データセル(`td`)背景 `#fff`(白)で `diff-row` グレーとのコントラストを確保、行単位チェックボックス(auto-save 対応)、CSS カスタムプロパティとドラッグハンドルによるリサイズ可能な列。 + - **Markdown スタイル**: 複数語の Modifiers(例: `static literal`)にノーブレークスペースを適用し折り返しを防止。 + - **新規ファイル**: [`AssemblyMethodAnalyzer`](Services/AssemblyMethodAnalyzer.cs)、[`AssemblySemanticChangesSummary`](Models/AssemblySemanticChangesSummary.cs)(算出プロパティ `AddedCount`/`RemovedCount`/`ModifiedCount`)。 + - **テスト**: [`AssemblyMethodAnalyzerTests`](FolderDiffIL4DotNet.Tests/Services/AssemblyMethodAnalyzerTests.cs)、[`AssemblySemanticChangesSummaryTests`](FolderDiffIL4DotNet.Tests/Models/AssemblySemanticChangesSummaryTests.cs)、[`HtmlReportGenerateServiceTests`](FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs) に新規アサーション(`TableHeaderUsesLighterGray`、`ThScColCbCssRuleExists`、`TdHasWhiteBackground`)。 + - **ドキュメント**: [README.md](README.md) にバイリンガルの **Assembly Semantic Changes** セクションを追加、[TESTING_GUIDE.md](doc/TESTING_GUIDE.md) に [`AssemblyMethodAnalyzerTests`](FolderDiffIL4DotNet.Tests/Services/AssemblyMethodAnalyzerTests.cs)・[`AssemblySemanticChangesSummaryTests`](FolderDiffIL4DotNet.Tests/Models/AssemblySemanticChangesSummaryTests.cs) を追加、[`doc/samples/diff_report.html`](doc/samples/diff_report.html)・[`doc/samples/diff_report.md`](doc/samples/diff_report.md) を同期。 ### [1.4.1] - 2026-03-20 From c48aaf0ce46749826f287767eadf1d7f77688126 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Mar 2026 07:38:55 +0000 Subject: [PATCH 34/36] fix: split broken string literal across two AppendLine calls in Js.cs Line 98 was missing the closing `");` causing the string to span two lines, which is invalid in a regular C# string literal (CS1010). https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- Services/HtmlReport/HtmlReportGenerateService.Js.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Services/HtmlReport/HtmlReportGenerateService.Js.cs b/Services/HtmlReport/HtmlReportGenerateService.Js.cs index 7a766b94..43221639 100644 --- a/Services/HtmlReport/HtmlReportGenerateService.Js.cs +++ b/Services/HtmlReport/HtmlReportGenerateService.Js.cs @@ -95,8 +95,8 @@ private static void AppendJs(StringBuilder sb, string storageKey, string reportD sb.AppendLine(" + '; --sc-name-w: ' + curWidths['--sc-name-w']"); sb.AppendLine(" + '; --sc-rettype-w: ' + curWidths['--sc-rettype-w']"); sb.AppendLine(" + '; --sc-params-w: ' + curWidths['--sc-params-w']"); - sb.AppendLine(" + '; --sc-body-w: ' + curWidths['--sc-body-w'] - + '; --sc-cnt-class-w: ' + curWidths['--sc-cnt-class-w'] + '; }');"); + sb.AppendLine(" + '; --sc-body-w: ' + curWidths['--sc-body-w']"); + sb.AppendLine(" + '; --sc-cnt-class-w: ' + curWidths['--sc-cnt-class-w'] + '; }');"); sb.AppendLine(" // Remove inline col-var overrides from element (now baked into :root)"); sb.AppendLine(" html = html.replace(/(]*?) style=\"[^\"]*\"/, '$1');"); sb.AppendLine(" // Replace controls bar with reviewed banner"); From 4b0259d977e37636b99f4afa2bda9d5a9788a05d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Mar 2026 07:42:07 +0000 Subject: [PATCH 35/36] fix: add missing using directives for SignatureDecoder and Dictionary - AssemblyMethodAnalyzer.cs: add System.Reflection.Metadata.Ecma335 - ReportGenerateService.SectionWriters.cs: add System.Collections.Generic https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- Services/AssemblyMethodAnalyzer.cs | 1 + Services/ReportGenerateService.SectionWriters.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/Services/AssemblyMethodAnalyzer.cs b/Services/AssemblyMethodAnalyzer.cs index 6472bde9..0b403391 100644 --- a/Services/AssemblyMethodAnalyzer.cs +++ b/Services/AssemblyMethodAnalyzer.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; using FolderDiffIL4DotNet.Models; diff --git a/Services/ReportGenerateService.SectionWriters.cs b/Services/ReportGenerateService.SectionWriters.cs index c90864af..9f478243 100644 --- a/Services/ReportGenerateService.SectionWriters.cs +++ b/Services/ReportGenerateService.SectionWriters.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using FolderDiffIL4DotNet.Common; From 22dc08a6a3ccf8503dd3ddab8e7d1c60aa5a0660 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Mar 2026 07:46:47 +0000 Subject: [PATCH 36/36] fix: correct test assertions for NoWrapMd and lazy render CSS - ReportGenerateServiceTests: use non-breaking spaces (\u00A0) in expected strings to match NoWrapMd output - HtmlReportGenerateServiceTests: check for table markup tag instead of CSS class name, since CSS always contains the class selector https://claude.ai/code/session_01WZNArDL3XqpEvLGTGRy3pf --- .../Services/HtmlReportGenerateServiceTests.cs | 2 +- .../Services/ReportGenerateServiceTests.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs b/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs index 6a533df5..70546ea3 100644 --- a/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs +++ b/FolderDiffIL4DotNet.Tests/Services/HtmlReportGenerateServiceTests.cs @@ -802,7 +802,7 @@ public void GenerateDiffReportHtml_AssemblySemanticChanges_LazyRender_EncodesAsB Assert.Contains("semantic_mod_0", html); Assert.Contains("Show assembly semantic changes", html); // Content should NOT be inline (lazy rendered) — table markup is base64-encoded - Assert.DoesNotContain("semantic-changes-table", html); + Assert.DoesNotContain("