Browse Source

Merge pull request #3970 from icsharpcode/settings-source-generator

Generate DecompilerSettings boilerplate from [DecompilerSetting] attributes
pull/3969/head
Siegfried Pammer 1 month ago committed by GitHub
parent
commit
51e662dbc5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      ICSharpCode.Decompiler.Generators/AnalyzerReleases.Unshipped.md
  2. 369
      ICSharpCode.Decompiler.Generators/DecompilerSettingsGenerator.cs
  3. 12
      ICSharpCode.Decompiler.Generators/DecompilerSyntaxTreeGenerator.cs
  4. 15
      ICSharpCode.Decompiler.Generators/RoslynHelpers.cs
  5. 53
      ICSharpCode.Decompiler.Tests/DecompilerSettingsTests.cs
  6. 30
      ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs
  7. 1988
      ICSharpCode.Decompiler/DecompilerSettings.cs

4
ICSharpCode.Decompiler.Generators/AnalyzerReleases.Unshipped.md

@ -6,3 +6,7 @@
Rule ID | Category | Severity | Notes Rule ID | Category | Severity | Notes
--------|----------|----------|------- --------|----------|----------|-------
DSTG001 | DecompilerSyntaxTreeGenerator | Error | Slot kind must map to a single child type DSTG001 | DecompilerSyntaxTreeGenerator | Error | Slot kind must map to a single child type
DSTG002 | DecompilerSettingsGenerator | Error | [DecompilerSetting] target must be a partial instance bool property
DSTG003 | DecompilerSettingsGenerator | Error | Version-gated setting must not declare [Category]
DSTG004 | DecompilerSettingsGenerator | Error | Language version has no display category
DSTG005 | DecompilerSettingsGenerator | Error | Setting must be declared in a non-nested partial class

369
ICSharpCode.Decompiler.Generators/DecompilerSettingsGenerator.cs

@ -0,0 +1,369 @@
// Copyright (c) 2026 Siegfried Pammer
//
// 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.Immutable;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace ICSharpCode.Decompiler.Generators;
/// <summary>
/// Generates the boilerplate behind [DecompilerSetting] partial properties: the backing field,
/// the accessors with change notification, and - from the per-setting language version - the
/// [Category] attribute plus the SetLanguageVersion and GetMinimumRequiredVersion methods,
/// so that a setting's version is declared in exactly one place.
/// </summary>
[Generator]
internal class DecompilerSettingsGenerator : IIncrementalGenerator
{
static readonly DiagnosticDescriptor InvalidSettingProperty = new(
id: "DSTG002",
title: "[DecompilerSetting] target must be a partial instance bool property",
messageFormat: "Setting property '{0}' must be a partial instance bool property with get and set accessors and an uppercase-start name (the generated backing field uses the camelCase form)",
category: "DecompilerSettingsGenerator",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
// The generated [Category] would join a handwritten one on the merged partial property, and
// GetCustomAttribute<CategoryAttribute>() (used by the settings UI) throws on duplicates.
static readonly DiagnosticDescriptor CategoryOnVersionedSetting = new(
id: "DSTG003",
title: "Version-gated setting must not declare [Category]",
messageFormat: "Setting '{0}' derives its [Category] from the language version; remove the handwritten [Category] attribute",
category: "DecompilerSettingsGenerator",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
static readonly DiagnosticDescriptor UnsupportedLanguageVersion = new(
id: "DSTG004",
title: "Language version has no display category",
messageFormat: "Language version '{0}' has no display category; gate settings on a released C# version, or add the new version to DecompilerSettingsGenerator.CategoryByVersion",
category: "DecompilerSettingsGenerator",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
// The generated implementation is emitted as a top-level partial class; a nested or
// non-partial containing type would make it merge nowhere (or into a stray new type).
static readonly DiagnosticDescriptor InvalidContainingType = new(
id: "DSTG005",
title: "Setting must be declared in a non-nested partial class",
messageFormat: "Setting '{0}' must be declared in a partial, non-nested class so the generated implementation merges into it",
category: "DecompilerSettingsGenerator",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
static readonly Dictionary<string, DiagnosticDescriptor> DescriptorsById =
new DiagnosticDescriptor[] { InvalidSettingProperty, CategoryOnVersionedSetting, UnsupportedLanguageVersion, InvalidContainingType }
.ToDictionary(d => d.Id);
// Display category per released C# version; the settings UI groups options by these strings.
static readonly Dictionary<string, string> CategoryByVersion = new() {
["CSharp1"] = "C# 1.0 / VS .NET",
["CSharp2"] = "C# 2.0 / VS 2005",
["CSharp3"] = "C# 3.0 / VS 2008",
["CSharp4"] = "C# 4.0 / VS 2010",
["CSharp5"] = "C# 5.0 / VS 2012",
["CSharp6"] = "C# 6.0 / VS 2015",
["CSharp7"] = "C# 7.0 / VS 2017",
["CSharp7_1"] = "C# 7.1 / VS 2017.3",
["CSharp7_2"] = "C# 7.2 / VS 2017.4",
["CSharp7_3"] = "C# 7.3 / VS 2017.7",
["CSharp8_0"] = "C# 8.0 / VS 2019",
["CSharp9_0"] = "C# 9.0 / VS 2019.8",
["CSharp10_0"] = "C# 10.0 / VS 2022",
["CSharp11_0"] = "C# 11.0 / VS 2022.4",
["CSharp12_0"] = "C# 12.0 / VS 2022.8",
["CSharp13_0"] = "C# 13.0 / VS 2022.12",
["CSharp14_0"] = "C# 14.0 / VS 2026",
};
readonly record struct SettingInfo(
string Namespace, string ClassName, string Accessibility, string PropertyName, string FieldName,
bool DefaultValue, int VersionValue, string? VersionName, string? Category,
string FilePath, int SpanStart);
// A diagnostic captured during the transform; kept as plain values so the pipeline stays cacheable.
readonly record struct DiagInfo(string Id, string MessageArg, string FilePath, int SpanStart, int SpanLength,
int StartLine, int StartChar, int EndLine, int EndChar);
readonly record struct SettingResult(SettingInfo? Setting, EquatableArray<DiagInfo>? Diagnostics);
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterPostInitializationOutput(i => i.AddSource("DecompilerSettingsGeneratorAttributes.g.cs", RoslynHelpers.EmbeddedAttributeSource + @"
namespace ICSharpCode.Decompiler
{
[global::Microsoft.CodeAnalysis.EmbeddedAttribute]
[global::System.AttributeUsage(global::System.AttributeTargets.Property)]
sealed class DecompilerSettingAttribute : global::System.Attribute
{
public DecompilerSettingAttribute() { }
public DecompilerSettingAttribute(global::ICSharpCode.Decompiler.CSharp.LanguageVersion introducedIn) { }
/// <summary>Initial value of the setting. Defaults to true.</summary>
public bool DefaultValue { get; set; } = true;
}
}
"));
var settings = context.SyntaxProvider.ForAttributeWithMetadataName(
"ICSharpCode.Decompiler.DecompilerSettingAttribute",
(n, ct) => n is PropertyDeclarationSyntax,
GetSetting);
context.RegisterSourceOutput(settings.Collect(), WriteSettingsClasses);
}
static SettingResult GetSetting(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken)
{
var property = (IPropertySymbol)context.TargetSymbol;
var node = (PropertyDeclarationSyntax)context.TargetNode;
var diagnostics = new List<DiagInfo>();
if (property.Type.SpecialType != SpecialType.System_Boolean || property.IsStatic
|| property.GetMethod == null || property.SetMethod == null || property.SetMethod.IsInitOnly
|| !char.IsUpper(property.Name[0])
|| !node.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)))
{
diagnostics.Add(MakeDiagInfo(InvalidSettingProperty.Id, property.Name, node));
return new SettingResult(null, diagnostics.ToEquatableArray());
}
if (property.ContainingType.ContainingType != null
|| node.Parent is not ClassDeclarationSyntax containingClass
|| !containingClass.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)))
{
diagnostics.Add(MakeDiagInfo(InvalidContainingType.Id, property.Name, node));
return new SettingResult(null, diagnostics.ToEquatableArray());
}
var attribute = context.Attributes[0];
int versionValue = 0;
string? versionName = null;
string? category = null;
if (attribute.ConstructorArguments.Length == 1)
{
var versionArgument = attribute.ConstructorArguments[0];
if (versionArgument.Kind == TypedConstantKind.Error || versionArgument.Value is not int boundVersion || versionArgument.Type is null)
{
// The argument did not bind (e.g. a typo'd enum member); the compiler already
// reports that error at the argument, so just skip the setting instead of
// crashing the whole generator.
return new SettingResult(null, null);
}
versionValue = boundVersion;
versionName = VersionNameFromSyntax(attribute)
?? versionArgument.Type.GetMembers()
.OfType<IFieldSymbol>()
.FirstOrDefault(f => f.HasConstantValue && Equals(f.ConstantValue, versionValue))?.Name
?? versionValue.ToString();
if (!CategoryByVersion.TryGetValue(versionName, out category))
{
diagnostics.Add(MakeDiagInfo(UnsupportedLanguageVersion.Id, versionName, node));
return new SettingResult(null, diagnostics.ToEquatableArray());
}
if (property.GetAttributes().Any(a => a.AttributeClass?.ToDisplayString() == "System.ComponentModel.CategoryAttribute"))
{
diagnostics.Add(MakeDiagInfo(CategoryOnVersionedSetting.Id, property.Name, node));
// The compiler would otherwise also flag the generated [Category] as a duplicate;
// suppress it so the mistake surfaces as the single DSTG003.
category = null;
}
}
bool defaultValue = true;
foreach (var named in attribute.NamedArguments)
{
// A named argument that failed to bind is already a compiler error; ignore it here.
if (named.Value.Value is not bool namedValue)
continue;
if (named.Key == "DefaultValue")
defaultValue = namedValue;
}
string fieldName = char.ToLowerInvariant(property.Name[0]) + property.Name.Substring(1);
if (SyntaxFacts.GetKeywordKind(fieldName) != SyntaxKind.None)
fieldName = "@" + fieldName;
var setting = new SettingInfo(
property.ContainingNamespace.IsGlobalNamespace ? "" : property.ContainingNamespace.ToDisplayString(),
property.ContainingType.Name,
SyntaxFacts.GetText(property.DeclaredAccessibility),
property.Name,
fieldName,
defaultValue,
versionValue,
versionName,
category,
node.SyntaxTree.FilePath,
node.SpanStart);
return new SettingResult(setting, diagnostics.Count == 0 ? null : diagnostics.ToEquatableArray());
}
// Prefer the enum member name as spelled at the use site: constant values are not unique in
// LanguageVersion (CSharp15_0 and Preview share a value), so a value-based reverse lookup can
// name an alias the user never wrote.
static string? VersionNameFromSyntax(AttributeData attribute)
{
if (attribute.ApplicationSyntaxReference?.GetSyntax() is not AttributeSyntax { ArgumentList.Arguments: { Count: >= 1 } arguments })
return null;
if (arguments[0].NameEquals != null)
return null;
return arguments[0].Expression switch {
MemberAccessExpressionSyntax memberAccess => memberAccess.Name.Identifier.Text,
IdentifierNameSyntax identifier => identifier.Identifier.Text,
_ => null,
};
}
static DiagInfo MakeDiagInfo(string id, string messageArg, SyntaxNode node)
{
var lineSpan = node.GetLocation().GetLineSpan();
return new DiagInfo(id, messageArg, node.SyntaxTree.FilePath, node.Span.Start, node.Span.Length,
lineSpan.StartLinePosition.Line, lineSpan.StartLinePosition.Character,
lineSpan.EndLinePosition.Line, lineSpan.EndLinePosition.Character);
}
static void WriteSettingsClasses(SourceProductionContext context, ImmutableArray<SettingResult> results)
{
foreach (var result in results)
{
if (result.Diagnostics is not { } resultDiagnostics)
continue;
foreach (var diag in resultDiagnostics)
{
// Indexer lookup so a diagnostic id missing from the map fails loudly instead of
// being reported under an unrelated descriptor.
var descriptor = DescriptorsById[diag.Id];
var location = Location.Create(diag.FilePath, new TextSpan(diag.SpanStart, diag.SpanLength),
new LinePositionSpan(new LinePosition(diag.StartLine, diag.StartChar), new LinePosition(diag.EndLine, diag.EndChar)));
context.ReportDiagnostic(Diagnostic.Create(descriptor, location, diag.MessageArg));
}
}
var settings = results
.Where(r => r.Setting != null)
.Select(r => r.Setting!.Value)
.OrderBy(s => s.FilePath, StringComparer.Ordinal)
.ThenBy(s => s.SpanStart);
foreach (var settingsClass in settings.GroupBy(s => (s.Namespace, s.ClassName)))
{
WriteSettingsClass(context, settingsClass.Key.Namespace, settingsClass.Key.ClassName, settingsClass.ToArray());
}
}
static void WriteSettingsClass(SourceProductionContext context, string ns, string className, SettingInfo[] settings)
{
var builder = new StringBuilder();
builder.AppendLine("// <auto-generated/>");
builder.AppendLine("#nullable enable");
builder.AppendLine();
if (ns.Length > 0)
{
builder.AppendLine($"namespace {ns}");
builder.AppendLine("{");
}
builder.AppendLine($"\tpartial class {className}");
builder.AppendLine("\t{");
foreach (var setting in settings)
{
builder.AppendLine($"\t\tbool {setting.FieldName} = {(setting.DefaultValue ? "true" : "false")};");
builder.AppendLine();
if (setting.Category != null)
{
builder.AppendLine($"\t\t[global::System.ComponentModel.Category(\"{setting.Category}\")]");
}
builder.AppendLine($"\t\t{setting.Accessibility} partial bool {setting.PropertyName} {{");
builder.AppendLine($"\t\t\tget {{ return {setting.FieldName}; }}");
builder.AppendLine("\t\t\tset {");
builder.AppendLine($"\t\t\t\tif ({setting.FieldName} != value)");
builder.AppendLine("\t\t\t\t{");
builder.AppendLine($"\t\t\t\t\t{setting.FieldName} = value;");
builder.AppendLine("\t\t\t\t\tOnPropertyChanged();");
builder.AppendLine("\t\t\t\t}");
builder.AppendLine("\t\t\t}");
builder.AppendLine("\t\t}");
builder.AppendLine();
}
var versionBuckets = settings
.Where(s => s.VersionName != null)
.GroupBy(s => s.VersionValue)
.OrderBy(g => g.Key)
.ToArray();
if (versionBuckets.Length > 0)
{
WriteSetLanguageVersion(builder, versionBuckets);
builder.AppendLine();
WriteGetMinimumRequiredVersion(builder, versionBuckets);
}
builder.AppendLine("\t}");
if (ns.Length > 0)
{
builder.AppendLine("}");
}
// The hint name must carry the full grouping key: two same-named settings classes in
// different namespaces would otherwise collide in AddSource and kill the generator.
string hintName = ns.Length == 0 ? $"{className}.Settings.g.cs" : $"{ns}.{className}.Settings.g.cs";
context.AddSource(hintName, SourceText.From(builder.ToString().Replace("\r\n", "\n"), Encoding.UTF8));
}
// Emitted as partial implementing declarations: the containing class supplies the defining
// stubs, which is where the XML documentation lives (the docs on a partial method's defining
// declaration apply as long as the implementation carries none).
static void WriteSetLanguageVersion(StringBuilder builder, IGrouping<int, SettingInfo>[] versionBuckets)
{
builder.AppendLine("\t\tpublic partial void SetLanguageVersion(global::ICSharpCode.Decompiler.CSharp.LanguageVersion languageVersion)");
builder.AppendLine("\t\t{");
builder.AppendLine("\t\t\t// By default, all decompiler features are enabled.");
builder.AppendLine("\t\t\t// Disable some of them based on language version:");
foreach (var bucket in versionBuckets)
{
builder.AppendLine($"\t\t\tif (languageVersion < global::ICSharpCode.Decompiler.CSharp.LanguageVersion.{bucket.First().VersionName})");
builder.AppendLine("\t\t\t{");
foreach (var setting in bucket)
{
builder.AppendLine($"\t\t\t\t{setting.FieldName} = false;");
}
builder.AppendLine("\t\t\t}");
}
builder.AppendLine("\t\t}");
}
static void WriteGetMinimumRequiredVersion(StringBuilder builder, IGrouping<int, SettingInfo>[] versionBuckets)
{
builder.AppendLine("\t\tpublic partial global::ICSharpCode.Decompiler.CSharp.LanguageVersion GetMinimumRequiredVersion()");
builder.AppendLine("\t\t{");
foreach (var bucket in versionBuckets.Reverse())
{
builder.AppendLine($"\t\t\tif ({string.Join(" || ", bucket.Select(s => s.FieldName))})");
builder.AppendLine($"\t\t\t\treturn global::ICSharpCode.Decompiler.CSharp.LanguageVersion.{bucket.First().VersionName};");
}
builder.AppendLine("\t\t\treturn global::ICSharpCode.Decompiler.CSharp.LanguageVersion.CSharp1;");
builder.AppendLine("\t\t}");
}
}

12
ICSharpCode.Decompiler.Generators/DecompilerSyntaxTreeGenerator.cs

@ -843,17 +843,7 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator
var visitorMembers = astNodeAdditions.Collect(); var visitorMembers = astNodeAdditions.Collect();
context context
.RegisterPostInitializationOutput(i => i.AddSource("DecompilerSyntaxTreeGeneratorAttributes.g.cs", @" .RegisterPostInitializationOutput(i => i.AddSource("DecompilerSyntaxTreeGeneratorAttributes.g.cs", RoslynHelpers.EmbeddedAttributeSource + @"
using System;
namespace Microsoft.CodeAnalysis
{
internal sealed partial class EmbeddedAttribute : global::System.Attribute
{
}
}
namespace ICSharpCode.Decompiler.CSharp.Syntax namespace ICSharpCode.Decompiler.CSharp.Syntax
{ {
[global::Microsoft.CodeAnalysis.EmbeddedAttribute] [global::Microsoft.CodeAnalysis.EmbeddedAttribute]

15
ICSharpCode.Decompiler.Generators/RoslynHelpers.cs

@ -22,6 +22,21 @@ namespace ICSharpCode.Decompiler.Generators;
public static class RoslynHelpers public static class RoslynHelpers
{ {
/// <summary>
/// Post-init source declaring Microsoft.CodeAnalysis.EmbeddedAttribute. Every generator that
/// emits embedded attribute types has to ship its own copy in its own post-init output; the
/// declarations merge only while they stay partial and textually compatible, so all generators
/// must build their copy from this single constant.
/// </summary>
public const string EmbeddedAttributeSource = @"
namespace Microsoft.CodeAnalysis
{
internal sealed partial class EmbeddedAttribute : global::System.Attribute
{
}
}
";
public static bool IsDerivedFrom(this INamedTypeSymbol type, INamedTypeSymbol baseType) public static bool IsDerivedFrom(this INamedTypeSymbol type, INamedTypeSymbol baseType)
{ {
INamedTypeSymbol? t = type; INamedTypeSymbol? t = type;

53
ICSharpCode.Decompiler.Tests/DecompilerSettingsTests.cs

@ -0,0 +1,53 @@
// Copyright (c) 2026 Siegfried Pammer
//
// 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 ICSharpCode.Decompiler.CSharp;
using NUnit.Framework;
namespace ICSharpCode.Decompiler.Tests
{
[TestFixture]
class DecompilerSettingsTests
{
[Test]
public void GetMinimumRequiredVersionReturnsTheHighestEnabledFeatureVersion()
{
var settings = new DecompilerSettings(LanguageVersion.CSharp1);
Assert.That(settings.GetMinimumRequiredVersion(), Is.EqualTo(LanguageVersion.CSharp1));
settings.AnonymousMethods = true;
Assert.That(settings.GetMinimumRequiredVersion(), Is.EqualTo(LanguageVersion.CSharp2));
// Syntax-preference settings participate too: enabled, they emit syntax of their version.
settings.UseEnhancedUsing = true;
Assert.That(settings.GetMinimumRequiredVersion(), Is.EqualTo(LanguageVersion.CSharp8_0));
settings.SwitchOnReadOnlySpanChar = true;
Assert.That(settings.GetMinimumRequiredVersion(), Is.EqualTo(LanguageVersion.CSharp11_0));
// The scan must pick the highest enabled feature, not the first match bottom-up.
settings.ParamsCollections = true;
Assert.That(settings.GetMinimumRequiredVersion(), Is.EqualTo(LanguageVersion.CSharp13_0));
settings.AnonymousMethods = false;
settings.ParamsCollections = false;
Assert.That(settings.GetMinimumRequiredVersion(), Is.EqualTo(LanguageVersion.CSharp11_0));
}
}
}

30
ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs

@ -60,17 +60,31 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler
LanguageVersion? languageVersion; LanguageVersion? languageVersion;
/// <summary>
/// The C# language version written into the exported project file as LangVersion.
/// This is an export parameter, not decompiler state: when not set explicitly, it defaults
/// to <see cref="DecompilerSettings.GetMinimumRequiredVersion"/> of the current settings,
/// and an explicit value below that minimum is rejected (here and again when the export
/// starts) because the emitted code could not compile under it.
/// </summary>
public LanguageVersion LanguageVersion { public LanguageVersion LanguageVersion {
get { return languageVersion ?? Settings.GetMinimumRequiredVersion(); } get { return languageVersion ?? Settings.GetMinimumRequiredVersion(); }
set { set {
var minVersion = Settings.GetMinimumRequiredVersion(); ValidateLanguageVersion(value);
if (value < minVersion)
throw new InvalidOperationException($"The chosen settings require at least {minVersion}." +
$" Please change the DecompilerSettings accordingly.");
languageVersion = value; languageVersion = value;
} }
} }
void ValidateLanguageVersion(LanguageVersion version)
{
var minVersion = Settings.GetMinimumRequiredVersion();
if (version < minVersion)
{
throw new InvalidOperationException($"The chosen settings require at least {minVersion}." +
" Please change the DecompilerSettings accordingly.");
}
}
bool IProjectInfoProvider.CheckForOverflowUnderflow => Settings.CheckForOverflowUnderflow; bool IProjectInfoProvider.CheckForOverflowUnderflow => Settings.CheckForOverflowUnderflow;
public IAssemblyResolver AssemblyResolver { get; } public IAssemblyResolver AssemblyResolver { get; }
@ -154,6 +168,14 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler
{ {
throw new InvalidOperationException("Must set TargetDirectory"); throw new InvalidOperationException("Must set TargetDirectory");
} }
// The LanguageVersion setter already rejects a version below what the settings require,
// but Settings is mutable and shared, so re-validate against the settings actually in
// effect now - otherwise the exported project would carry a LangVersion under which the
// emitted code cannot compile.
if (languageVersion is { } explicitVersion)
{
ValidateLanguageVersion(explicitVersion);
}
DecompilerEventSource.Log.ProjectDecompilationStart(file.Name); DecompilerEventSource.Log.ProjectDecompilationStart(file.Name);
int codeFileCount = 0, resourceFileCount = 0; int codeFileCount = 0, resourceFileCount = 0;
try try

1988
ICSharpCode.Decompiler/DecompilerSettings.cs

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save