mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
Assisted-by: Claude:claude-opus-4-7:Claude Code Assisted-by: Claude:claude-opus-4-7:Claude Codepull/3755/head
5 changed files with 466 additions and 7 deletions
@ -0,0 +1,164 @@
@@ -0,0 +1,164 @@
|
||||
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System.Collections.Generic; |
||||
using System.Collections.Immutable; |
||||
using System.Linq; |
||||
using System.Reflection; |
||||
|
||||
using AwesomeAssertions; |
||||
|
||||
using ILSpy.Metadata.Filters; |
||||
|
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.ILSpy.Tests.Metadata; |
||||
|
||||
/// <summary>
|
||||
/// Round-trips a <see cref="FilterState"/> through XML — the storage shape behind
|
||||
/// <see cref="FilterStatePersistence"/>. Tests cover the empty case (so persistence
|
||||
/// doesn't write noise to ILSpy.xml when the user clears a filter), the three mutator
|
||||
/// surfaces (mutex selections, tri-state flag states, independent-mode), and schema-
|
||||
/// drift resilience (XML references groups/flags the current schema no longer carries).
|
||||
/// </summary>
|
||||
[TestFixture] |
||||
public class FilterStateSerializerTests |
||||
{ |
||||
[Test] |
||||
public void ToXml_Empty_FilterState_Produces_Element_With_No_Constraint_Children() |
||||
{ |
||||
// Newly-constructed state with every chip = Any and every flag = DontCare must
|
||||
// round-trip to an empty <FilterState /> so SessionSettings doesn't bloat with
|
||||
// no-op entries.
|
||||
var state = new FilterState(BuildSchema()); |
||||
|
||||
var xml = FilterStatePersistence.ToXml(state); |
||||
|
||||
xml.Name.LocalName.Should().Be("FilterState"); |
||||
xml.Elements("Mutex").Should().BeEmpty(); |
||||
xml.Elements("Flag").Should().BeEmpty(); |
||||
// IndependentMode = All (default) is implicit; only Any is written explicitly.
|
||||
xml.Element("IndependentMode")?.Value.Should().NotBe("All"); |
||||
} |
||||
|
||||
[Test] |
||||
public void RoundTrip_Preserves_Mutex_Selection_Across_Multiple_Values() |
||||
{ |
||||
// Mutex chip groups support multi-selection — the user can pick "Public" + "Family"
|
||||
// from VisibilityMask, and that pair must survive a session restart.
|
||||
var schema = BuildSchema(); |
||||
var original = new FilterState(schema); |
||||
original.SetMutexSelection("Visibility", |
||||
ImmutableHashSet.Create<uint>(2u, 4u)); |
||||
|
||||
var xml = FilterStatePersistence.ToXml(original); |
||||
var restored = new FilterState(schema); |
||||
FilterStatePersistence.ApplyXml(restored, xml); |
||||
|
||||
restored.MutexSelections["Visibility"].Should().BeEquivalentTo(new uint[] { 2u, 4u }); |
||||
} |
||||
|
||||
[Test] |
||||
public void RoundTrip_Preserves_TriState_Flag_States() |
||||
{ |
||||
// Required / Excluded / DontCare each have distinct semantics in the filter
|
||||
// predicate, so all three need to round-trip exactly.
|
||||
var schema = BuildSchema(); |
||||
var original = new FilterState(schema); |
||||
original.SetFlagState("Sealed", TriState.Required); |
||||
original.SetFlagState("Abstract", TriState.Excluded); |
||||
// "Static" deliberately left as DontCare — must NOT be re-emitted as a Flag entry.
|
||||
|
||||
var xml = FilterStatePersistence.ToXml(original); |
||||
var restored = new FilterState(schema); |
||||
FilterStatePersistence.ApplyXml(restored, xml); |
||||
|
||||
restored.FlagStates["Sealed"].Should().Be(TriState.Required); |
||||
restored.FlagStates["Abstract"].Should().Be(TriState.Excluded); |
||||
restored.FlagStates["Static"].Should().Be(TriState.DontCare); |
||||
} |
||||
|
||||
[Test] |
||||
public void RoundTrip_Preserves_IndependentMode_When_It_Differs_From_Default() |
||||
{ |
||||
var schema = BuildSchema(); |
||||
var original = new FilterState(schema) { IndependentMode = MatchMode.Any }; |
||||
|
||||
var xml = FilterStatePersistence.ToXml(original); |
||||
var restored = new FilterState(schema); |
||||
FilterStatePersistence.ApplyXml(restored, xml); |
||||
|
||||
restored.IndependentMode.Should().Be(MatchMode.Any); |
||||
} |
||||
|
||||
[Test] |
||||
public void ApplyXml_Ignores_Mutex_Entries_For_Groups_Missing_From_The_Current_Schema() |
||||
{ |
||||
// Schema drift: an older .ILSpy.xml might mention a group the new schema dropped.
|
||||
// Apply should silently skip it instead of throwing — the user otherwise loses
|
||||
// every saved filter on a schema upgrade.
|
||||
var xml = new System.Xml.Linq.XElement("FilterState", |
||||
new System.Xml.Linq.XElement("Mutex", |
||||
new System.Xml.Linq.XAttribute("group", "GoneFromSchema"), |
||||
new System.Xml.Linq.XElement("Value", "1"))); |
||||
var state = new FilterState(BuildSchema()); |
||||
|
||||
var act = () => FilterStatePersistence.ApplyXml(state, xml); |
||||
|
||||
act.Should().NotThrow(); |
||||
state.IsEmpty.Should().BeTrue("the unknown group entry must be silently ignored"); |
||||
} |
||||
|
||||
[Test] |
||||
public void ApplyXml_Ignores_Flag_Entries_For_Flags_Missing_From_The_Current_Schema() |
||||
{ |
||||
var xml = new System.Xml.Linq.XElement("FilterState", |
||||
new System.Xml.Linq.XElement("Flag", |
||||
new System.Xml.Linq.XAttribute("name", "FlagThatNoLongerExists"), |
||||
new System.Xml.Linq.XAttribute("state", "Required"))); |
||||
var state = new FilterState(BuildSchema()); |
||||
|
||||
var act = () => FilterStatePersistence.ApplyXml(state, xml); |
||||
|
||||
act.Should().NotThrow(); |
||||
state.IsEmpty.Should().BeTrue(); |
||||
} |
||||
|
||||
// --- helpers ---------------------------------------------------------------------------
|
||||
|
||||
[System.Flags] |
||||
enum SampleAttrs : uint |
||||
{ |
||||
Public = 1, |
||||
Family = 2, |
||||
Internal = 4, |
||||
Private = 8, |
||||
VisibilityMask = 0xF, |
||||
Static = 0x10, |
||||
Abstract = 0x20, |
||||
Sealed = 0x40, |
||||
} |
||||
|
||||
static FlagsSchema BuildSchema() |
||||
{ |
||||
// Build the schema by inferring it off our test enum, matching what production code
|
||||
// does for real metadata enums. Saves us from authoring the schema by hand and
|
||||
// keeps the test contract aligned with the live FlagsSchemaInferer surface.
|
||||
return FlagsSchemaInferer.For(typeof(SampleAttrs)); |
||||
} |
||||
} |
||||
@ -0,0 +1,94 @@
@@ -0,0 +1,94 @@
|
||||
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System.Xml.Linq; |
||||
|
||||
using AwesomeAssertions; |
||||
|
||||
using ILSpy; |
||||
|
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.ILSpy.Tests; |
||||
|
||||
/// <summary>
|
||||
/// Pins the SessionSettings ↔ XML shape that backs <c>FilterStatePersistence</c>'s
|
||||
/// cross-session cache. Without these tests a refactor of <c>LoadFromXml</c> /
|
||||
/// <c>SaveToXml</c> could silently break the persistence round-trip — the schema
|
||||
/// drift test alone wouldn't catch a wiring change at the SessionSettings level.
|
||||
/// </summary>
|
||||
[TestFixture] |
||||
public class SessionSettingsFilterStatesTests |
||||
{ |
||||
[Test] |
||||
public void Round_Trip_Preserves_Multiple_Entries_Across_Multiple_Pages() |
||||
{ |
||||
// Two different metadata-table pages each with one filtered column. Verifies the
|
||||
// outer <FilterStates>/<Page>/<Column> nesting AND that the inner FilterState
|
||||
// payload (an opaque XElement to SessionSettings) survives unmolested.
|
||||
var original = new SessionSettings(); |
||||
original.FilterStates[("ILSpy.Metadata.CorTables.TypeDefEntry", "Attributes")] = |
||||
new XElement("FilterState", |
||||
new XElement("Flag", |
||||
new XAttribute("name", "Sealed"), |
||||
new XAttribute("state", "Required"))); |
||||
original.FilterStates[("ILSpy.Metadata.CorTables.MethodDefEntry", "Attributes")] = |
||||
new XElement("FilterState", |
||||
new XElement("Mutex", |
||||
new XAttribute("group", "MemberAccess"), |
||||
new XElement("Value", "6"))); |
||||
|
||||
var xml = original.SaveToXml(); |
||||
var restored = new SessionSettings(); |
||||
restored.LoadFromXml(xml); |
||||
|
||||
restored.FilterStates.Should().HaveCount(2); |
||||
restored.FilterStates[("ILSpy.Metadata.CorTables.TypeDefEntry", "Attributes")] |
||||
.Element("Flag")!.Attribute("name")!.Value.Should().Be("Sealed"); |
||||
restored.FilterStates[("ILSpy.Metadata.CorTables.MethodDefEntry", "Attributes")] |
||||
.Element("Mutex")!.Element("Value")!.Value.Should().Be("6"); |
||||
} |
||||
|
||||
[Test] |
||||
public void Empty_FilterStates_Does_Not_Emit_The_Container_Element() |
||||
{ |
||||
// We don't want to bloat ILSpy.xml with an empty <FilterStates /> wrapper every
|
||||
// time a user closes the app without touching any filter dropdowns.
|
||||
var settings = new SessionSettings(); |
||||
|
||||
var xml = settings.SaveToXml(); |
||||
|
||||
xml.Element("FilterStates").Should().BeNull( |
||||
"omit the container entirely when no filter has been persisted"); |
||||
} |
||||
|
||||
[Test] |
||||
public void LoadFromXml_Tolerates_Missing_FilterStates_Section() |
||||
{ |
||||
// Settings files written by versions before this feature have no <FilterStates>;
|
||||
// LoadFromXml must accept them silently and leave FilterStates empty.
|
||||
var settings = new SessionSettings(); |
||||
var legacyXml = new XElement("SessionSettings", |
||||
new XElement("HideEmptyMetadataTables", "true")); |
||||
|
||||
var act = () => settings.LoadFromXml(legacyXml); |
||||
|
||||
act.Should().NotThrow(); |
||||
settings.FilterStates.Should().BeEmpty(); |
||||
} |
||||
} |
||||
@ -0,0 +1,110 @@
@@ -0,0 +1,110 @@
|
||||
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Collections.Immutable; |
||||
using System.Globalization; |
||||
using System.Linq; |
||||
using System.Xml.Linq; |
||||
|
||||
namespace ILSpy.Metadata.Filters |
||||
{ |
||||
/// <summary>
|
||||
/// Round-trips a <see cref="FilterState"/> through an <see cref="XElement"/> so the
|
||||
/// schema-driven flag-filter dropdown's user-set state survives a session restart.
|
||||
/// Storage is keyed externally (by page-entry type + column name); this class only
|
||||
/// owns the per-state XML shape.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Wire shape (only non-default constraints are written):
|
||||
/// <code>
|
||||
/// <FilterState>
|
||||
/// <Mutex group="VisibilityMask">
|
||||
/// <Value>2</Value>
|
||||
/// <Value>4</Value>
|
||||
/// </Mutex>
|
||||
/// <Flag name="Sealed" state="Required" />
|
||||
/// <IndependentMode>Any</IndependentMode>
|
||||
/// </FilterState>
|
||||
/// </code>
|
||||
/// Apply tolerates unknown group/flag names so an older saved XML can't break a newer
|
||||
/// schema (the offending entry is silently skipped, others still apply).
|
||||
/// </remarks>
|
||||
public static class FilterStatePersistence |
||||
{ |
||||
const string Root = "FilterState"; |
||||
const string MutexElement = "Mutex"; |
||||
const string FlagElement = "Flag"; |
||||
const string ValueElement = "Value"; |
||||
const string IndependentModeElement = "IndependentMode"; |
||||
|
||||
public static XElement ToXml(FilterState state) |
||||
{ |
||||
ArgumentNullException.ThrowIfNull(state); |
||||
var element = new XElement(Root); |
||||
foreach (var (groupName, selection) in state.MutexSelections) |
||||
{ |
||||
if (selection is null) |
||||
continue; // "any" — write nothing
|
||||
element.Add(new XElement(MutexElement, |
||||
new XAttribute("group", groupName), |
||||
selection.OrderBy(v => v).Select(v => |
||||
new XElement(ValueElement, v.ToString(CultureInfo.InvariantCulture))))); |
||||
} |
||||
foreach (var (flagName, triState) in state.FlagStates) |
||||
{ |
||||
if (triState == TriState.DontCare) |
||||
continue; // default — write nothing
|
||||
element.Add(new XElement(FlagElement, |
||||
new XAttribute("name", flagName), |
||||
new XAttribute("state", triState.ToString()))); |
||||
} |
||||
if (state.IndependentMode != MatchMode.All) |
||||
element.Add(new XElement(IndependentModeElement, state.IndependentMode.ToString())); |
||||
return element; |
||||
} |
||||
|
||||
public static void ApplyXml(FilterState state, XElement xml) |
||||
{ |
||||
ArgumentNullException.ThrowIfNull(state); |
||||
ArgumentNullException.ThrowIfNull(xml); |
||||
foreach (var mutex in xml.Elements(MutexElement)) |
||||
{ |
||||
var groupName = (string?)mutex.Attribute("group"); |
||||
if (groupName is null || !state.MutexSelections.ContainsKey(groupName)) |
||||
continue; // schema drift: group no longer present
|
||||
var values = mutex.Elements(ValueElement) |
||||
.Select(v => uint.TryParse(v.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var n) ? n : (uint?)null) |
||||
.Where(v => v.HasValue) |
||||
.Select(v => v!.Value) |
||||
.ToImmutableHashSet(); |
||||
state.SetMutexSelection(groupName, values.IsEmpty ? null : values); |
||||
} |
||||
foreach (var flag in xml.Elements(FlagElement)) |
||||
{ |
||||
var flagName = (string?)flag.Attribute("name"); |
||||
if (flagName is null || !state.FlagStates.ContainsKey(flagName)) |
||||
continue; // schema drift: flag no longer present
|
||||
if (Enum.TryParse<TriState>((string?)flag.Attribute("state"), out var tri)) |
||||
state.SetFlagState(flagName, tri); |
||||
} |
||||
if (Enum.TryParse<MatchMode>(xml.Element(IndependentModeElement)?.Value, out var mode)) |
||||
state.IndependentMode = mode; |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue