mirror of
https://github.com/icsharpcode/ILSpy.git
synced 2026-10-12 02:15:58 +00:00
Three places decided independently whether a backing field would still be declared, and they disagreed. ReplaceBackingFieldUsage rewrote a constructor store into a property assignment whenever the property looked collapsible, without asking whether PatternStatementTransform would actually remove the declaration; ConvertField printed the "field" keyword on the FieldKeyword setting alone, ignoring the GetterOnlyAutomaticProperties veto that MemberIsHidden applies to the same field. Two consequences, both silent. A setter-less property under GetterOnlyAutomaticProperties = false kept its declaration and got "field" in the getter anyway, so the keyword bound to a second synthesized field and the declared one went unwritten. A settable property under AutomaticProperties = false had its initializing store turned into a property assignment that TransformFieldAndConstructorInitializers could no longer lift, leaving the constructor in the output with an unconverted base-constructor call. BackingFieldWillBeRemoved is now the single verdict every branch consults, and it mirrors the transform's own entry gate. A property that keeps explicit accessors is no longer addressed by name: the store stays a field reference and becomes the property initializer, which is what field-backed storage means. The one exception is a setter-less property's constructor store, which C# allows to be written as an assignment and which has no other expressible form. IsBackingFieldOfAutomaticProperty now answers through TryGetBackingField instead of its own name check, so the two directions of "is this field that property's storage" cannot diverge on staticness or field type. ReplaceBackingFieldUsage dispatches on the resolve result rather than the identifier's spelling; after ConvertField the same field appears both as "field" and under its metadata name carrying the same annotation, so matching the name was matching the wrong thing. The keyword's own precondition moves into a CanUseFieldKeyword local function. Seven clauses with comment blocks wedged between them had to be read as one expression; as one early return per rule the list reads in order. Assisted-by: Claude:claude-opus-5[1m]:Claude Code
1446 lines
57 KiB
C#
1446 lines
57 KiB
C#
// Copyright (c) 2011 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.
|
|
|
|
#nullable enable
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Reflection.Metadata;
|
|
|
|
using ICSharpCode.Decompiler.CSharp.Syntax;
|
|
using ICSharpCode.Decompiler.CSharp.Syntax.PatternMatching;
|
|
using ICSharpCode.Decompiler.Semantics;
|
|
using ICSharpCode.Decompiler.TypeSystem;
|
|
|
|
namespace ICSharpCode.Decompiler.CSharp.Transforms
|
|
{
|
|
/// <summary>
|
|
/// Finds the expanded form of using statements using pattern matching and replaces it with a UsingStatement.
|
|
/// </summary>
|
|
public sealed class PatternStatementTransform : ContextTrackingVisitor<AstNode>, IAstTransform
|
|
{
|
|
readonly DeclareVariables declareVariables = new DeclareVariables();
|
|
[AllowNull]
|
|
TransformContext context;
|
|
|
|
public void Run(AstNode rootNode, TransformContext context)
|
|
{
|
|
if (this.context != null)
|
|
throw new InvalidOperationException("Reentrancy in PatternStatementTransform.Run?");
|
|
try
|
|
{
|
|
this.context = context;
|
|
base.Initialize(context);
|
|
declareVariables.Analyze(rootNode);
|
|
rootNode.AcceptVisitor(this);
|
|
}
|
|
finally
|
|
{
|
|
this.context = null;
|
|
base.Uninitialize();
|
|
declareVariables.ClearAnalysisResults();
|
|
}
|
|
}
|
|
|
|
#region Visitor Overrides
|
|
protected override AstNode VisitChildren(AstNode node)
|
|
{
|
|
// Go through the children, and keep visiting a node as long as it changes.
|
|
// Because some transforms delete/replace nodes before and after the node being transformed, we rely
|
|
// on the transform's return value to know where we need to keep iterating.
|
|
for (AstNode? child = node.FirstChild; child != null; child = child.NextSibling)
|
|
{
|
|
AstNode oldChild;
|
|
do
|
|
{
|
|
oldChild = child;
|
|
child = child.AcceptVisitor(this);
|
|
Debug.Assert(child != null && child.Parent == node);
|
|
} while (child != oldChild);
|
|
}
|
|
return node;
|
|
}
|
|
|
|
public override AstNode VisitExpressionStatement(ExpressionStatement expressionStatement)
|
|
{
|
|
AstNode? result = TransformForeachOnMultiDimArray(expressionStatement);
|
|
if (result != null)
|
|
return result;
|
|
result = TransformFor(expressionStatement);
|
|
if (result != null)
|
|
return result;
|
|
return base.VisitExpressionStatement(expressionStatement);
|
|
}
|
|
|
|
public override AstNode VisitForStatement(ForStatement forStatement)
|
|
{
|
|
AstNode? result = TransformForeachOnArray(forStatement);
|
|
if (result != null)
|
|
return result;
|
|
result = TransformForeachOnInlineArray(forStatement);
|
|
if (result != null)
|
|
return result;
|
|
return base.VisitForStatement(forStatement);
|
|
}
|
|
|
|
public override AstNode VisitIfElseStatement(IfElseStatement ifElseStatement)
|
|
{
|
|
AstNode? simplifiedIfElse = SimplifyCascadingIfElseStatements(ifElseStatement);
|
|
if (simplifiedIfElse != null)
|
|
return simplifiedIfElse;
|
|
return base.VisitIfElseStatement(ifElseStatement);
|
|
}
|
|
|
|
public override AstNode VisitPropertyDeclaration(PropertyDeclaration propertyDeclaration)
|
|
{
|
|
// Same rule as CSharpDecompiler.MemberIsHidden applies to the backing field: either
|
|
// setting on its own allows the field declaration to disappear, and
|
|
// GetterOnlyAutomaticProperties vetoes the getter-only case for both. Asking only
|
|
// about AutomaticProperties would skip the transform for a field-backed property
|
|
// while ExpressionBuilder.ConvertField has already printed "field" in its accessors,
|
|
// leaving the declaration and the keyword in the same output.
|
|
if ((context.Settings.AutomaticProperties || context.Settings.FieldKeyword)
|
|
&& (propertyDeclaration.Setter is not null || context.Settings.GetterOnlyAutomaticProperties))
|
|
{
|
|
AstNode? result = TransformAutomaticProperty(propertyDeclaration);
|
|
if (result != null)
|
|
return result;
|
|
}
|
|
return base.VisitPropertyDeclaration(propertyDeclaration);
|
|
}
|
|
|
|
public override AstNode VisitEventDeclaration(EventDeclaration eventDeclaration)
|
|
{
|
|
// A field-like event declaration hides its backing field; remove the field declaration
|
|
// if it was emitted because other members reference it. (This happens for events that
|
|
// CSharpDecompiler.DoDecompile already recognized as automatic: the backing field is
|
|
// hidden from the normal member list, but re-emitted via the work list when referenced.)
|
|
if (context.Settings.AutomaticEvents && eventDeclaration.GetSymbol() is IEvent symbol)
|
|
{
|
|
var fieldDecl = eventDeclaration.Parent?.Children.OfType<FieldDeclaration>()
|
|
.FirstOrDefault(fd => IsEventBackingFieldDeclaration(fd, symbol));
|
|
fieldDecl?.Remove();
|
|
}
|
|
return base.VisitEventDeclaration(eventDeclaration);
|
|
}
|
|
|
|
public override AstNode VisitMethodDeclaration(MethodDeclaration methodDeclaration)
|
|
{
|
|
return TransformDestructor(methodDeclaration) ?? base.VisitMethodDeclaration(methodDeclaration);
|
|
}
|
|
|
|
public override AstNode VisitDestructorDeclaration(DestructorDeclaration destructorDeclaration)
|
|
{
|
|
return TransformDestructorBody(destructorDeclaration) ?? base.VisitDestructorDeclaration(destructorDeclaration);
|
|
}
|
|
|
|
public override AstNode VisitTryCatchStatement(TryCatchStatement tryCatchStatement)
|
|
{
|
|
return TransformTryCatchFinally(tryCatchStatement) ?? base.VisitTryCatchStatement(tryCatchStatement);
|
|
}
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// $variable = $initializer;
|
|
/// </summary>
|
|
static readonly AstNode variableAssignPattern = new ExpressionStatement(
|
|
new AssignmentExpression(
|
|
new NamedNode("variable", new IdentifierExpression(Pattern.AnyString)),
|
|
new AnyNode("initializer")
|
|
));
|
|
|
|
#region for
|
|
static readonly WhileStatement forPattern = new WhileStatement {
|
|
Condition = new BinaryOperatorExpression {
|
|
Left = new NamedNode("ident", new IdentifierExpression(Pattern.AnyString)),
|
|
Operator = BinaryOperatorType.Any,
|
|
Right = new AnyNode("endExpr")
|
|
},
|
|
EmbeddedStatement = new BlockStatement {
|
|
Statements = {
|
|
new Repeat(new AnyNode("statement")),
|
|
new NamedNode(
|
|
"iterator",
|
|
new ExpressionStatement(
|
|
new AssignmentExpression {
|
|
Left = new Backreference("ident"),
|
|
Operator = AssignmentOperatorType.Any,
|
|
Right = new AnyNode()
|
|
}))
|
|
}
|
|
}
|
|
};
|
|
|
|
public ForStatement? TransformFor(ExpressionStatement node)
|
|
{
|
|
if (!context.Settings.ForStatement)
|
|
return null;
|
|
Match m1 = variableAssignPattern.Match(node);
|
|
if (!m1.Success)
|
|
return null;
|
|
var variable = m1.Get<IdentifierExpression>("variable").Single().GetILVariable();
|
|
AstNode? next = node.NextSibling;
|
|
if (next == null)
|
|
return null;
|
|
if (next is ForStatement forStatement && ForStatementUsesVariable(forStatement, variable))
|
|
{
|
|
context.Step("Move declaration into for initializer", node);
|
|
node.Remove();
|
|
next.InsertChildAfter(null, node, Slots.ForInitializer);
|
|
return (ForStatement)next;
|
|
}
|
|
Match m3 = forPattern.Match(next);
|
|
if (!m3.Success)
|
|
return null;
|
|
// ensure the variable in the for pattern is the same as in the declaration
|
|
if (variable != m3.Get<IdentifierExpression>("ident").Single().GetILVariable())
|
|
return null;
|
|
WhileStatement loop = (WhileStatement)next;
|
|
// Cannot convert to for loop, if the iteration variable is a ref local used after the loop: its
|
|
// declaration is hoisted in front, leaving a headless `for (; cond; v = ref ...)` whose only
|
|
// initialization is the for-initializer ref-assignment -- which can't be split from a ref local
|
|
// (CS8174). Keeping it a while-loop matches the source and keeps the initializer on the decl.
|
|
if (variable != null && variable.Type.IsByRefLike && IsVariableUsedAfter(loop, variable))
|
|
return null;
|
|
// Cannot convert to for loop, if any variable that is used in the "iterator" part of the pattern,
|
|
// will be declared in the body of the while-loop.
|
|
var iteratorStatement = m3.Get<Statement>("iterator").Single();
|
|
if (IteratorVariablesDeclaredInsideLoopBody(iteratorStatement))
|
|
return null;
|
|
// Cannot convert to for loop, because that would change the semantics of the program.
|
|
// continue in while jumps to the condition block.
|
|
// Whereas continue in for jumps to the increment block.
|
|
if (loop.DescendantNodes(DescendIntoStatement).OfType<Statement>().Any(s => s is ContinueStatement))
|
|
return null;
|
|
context.Step("Transform while loop to for", loop);
|
|
node.Remove();
|
|
BlockStatement newBody = new BlockStatement();
|
|
foreach (Statement stmt in m3.Get<Statement>("statement"))
|
|
newBody.Add(stmt.Detach());
|
|
forStatement = new ForStatement();
|
|
forStatement.CopyAnnotationsFrom(loop);
|
|
forStatement.Initializers.Add(node);
|
|
forStatement.Condition = loop.Condition.Detach();
|
|
forStatement.Iterators.Add(iteratorStatement.Detach());
|
|
forStatement.EmbeddedStatement = newBody;
|
|
loop.ReplaceWith(forStatement);
|
|
context.EndStep(forStatement);
|
|
return forStatement;
|
|
}
|
|
|
|
bool DescendIntoStatement(AstNode node)
|
|
{
|
|
if (node is Expression || node is ExpressionStatement)
|
|
return false;
|
|
if (node is WhileStatement || node is ForeachStatement || node is DoWhileStatement || node is ForStatement)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
bool ForStatementUsesVariable(ForStatement statement, IL.ILVariable? variable)
|
|
{
|
|
if (statement.Condition?.DescendantsAndSelf.OfType<IdentifierExpression>().Any(ie => ie.GetILVariable() == variable) == true)
|
|
return true;
|
|
if (statement.Iterators.Any(i => i.DescendantsAndSelf.OfType<IdentifierExpression>().Any(ie => ie.GetILVariable() == variable)))
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
bool IsVariableUsedAfter(Statement loop, IL.ILVariable variable)
|
|
{
|
|
for (AstNode? sibling = loop.NextSibling; sibling != null; sibling = sibling.NextSibling)
|
|
{
|
|
if (sibling.DescendantsAndSelf.OfType<IdentifierExpression>().Any(ie => ie.GetILVariable() == variable))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool IteratorVariablesDeclaredInsideLoopBody(Statement iteratorStatement)
|
|
{
|
|
foreach (var id in iteratorStatement.DescendantsAndSelf.OfType<IdentifierExpression>())
|
|
{
|
|
var v = id.GetILVariable();
|
|
if (v == null || !DeclareVariables.VariableNeedsDeclaration(v.Kind))
|
|
continue;
|
|
if (declareVariables.GetDeclarationPoint(v).Parent == iteratorStatement.Parent)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
#endregion
|
|
|
|
#region foreach
|
|
|
|
static readonly ForStatement forOnArrayPattern = new ForStatement {
|
|
Initializers = {
|
|
new ExpressionStatement(
|
|
new AssignmentExpression(
|
|
new NamedNode("indexVariable", new IdentifierExpression(Pattern.AnyString)),
|
|
new PrimitiveExpression(0)
|
|
))
|
|
},
|
|
Condition = new BinaryOperatorExpression(
|
|
new IdentifierExpressionBackreference("indexVariable"),
|
|
BinaryOperatorType.LessThan,
|
|
new MemberReferenceExpression(new NamedNode("arrayVariable", new IdentifierExpression(Pattern.AnyString)), "Length")
|
|
),
|
|
Iterators = {
|
|
new ExpressionStatement(
|
|
new AssignmentExpression(
|
|
new IdentifierExpressionBackreference("indexVariable"),
|
|
new BinaryOperatorExpression(new IdentifierExpressionBackreference("indexVariable"), BinaryOperatorType.Add, new PrimitiveExpression(1))
|
|
))
|
|
},
|
|
EmbeddedStatement = new BlockStatement {
|
|
Statements = {
|
|
new ExpressionStatement(new AssignmentExpression(
|
|
new NamedNode("itemVariable", new IdentifierExpression(Pattern.AnyString)),
|
|
new IndexerExpression(new IdentifierExpressionBackreference("arrayVariable"), new IdentifierExpressionBackreference("indexVariable"))
|
|
)),
|
|
new Repeat(new AnyNode("statements"))
|
|
}
|
|
}
|
|
};
|
|
|
|
bool VariableCanBeUsedAsForeachLocal(IL.ILVariable itemVar, Statement loop)
|
|
{
|
|
if (itemVar == null || !(itemVar.Kind == IL.VariableKind.Local || itemVar.Kind == IL.VariableKind.StackSlot))
|
|
{
|
|
// only locals/temporaries can be converted into foreach loop variable
|
|
return false;
|
|
}
|
|
|
|
var blockContainer = loop.Annotation<IL.BlockContainer>();
|
|
|
|
if (!itemVar.IsSingleDefinition)
|
|
{
|
|
// foreach variable cannot be assigned to.
|
|
// As a special case, we accept taking the address for a method call,
|
|
// but only if the call is the only use, so that any mutation by the call
|
|
// cannot be observed.
|
|
if (!AddressUsedForSingleCall(itemVar, blockContainer))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (itemVar.CaptureScope != null && itemVar.CaptureScope != blockContainer)
|
|
{
|
|
// captured variables cannot be declared in the loop unless the loop is their capture scope
|
|
return false;
|
|
}
|
|
|
|
AstNode declPoint = declareVariables.GetDeclarationPoint(itemVar);
|
|
return declPoint.Ancestors.Contains(loop) && !declareVariables.WasMerged(itemVar);
|
|
}
|
|
|
|
static bool AddressUsedForSingleCall(IL.ILVariable v, IL.BlockContainer? loop)
|
|
{
|
|
if (v.StoreCount == 1 && v.AddressCount == 1 && v.LoadCount == 0 && v.Type.IsReferenceType == false)
|
|
{
|
|
if (v.AddressInstructions[0].Parent is IL.Call call
|
|
&& v.AddressInstructions[0].ChildIndex == 0
|
|
&& !call.Method.IsStatic)
|
|
{
|
|
// used as this pointer for a method call
|
|
// this is OK iff the call is not within a nested loop
|
|
for (var node = call.Parent; node != null; node = node.Parent)
|
|
{
|
|
if (node == loop)
|
|
return true;
|
|
else if (node is IL.BlockContainer)
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Statement? TransformForeachOnArray(ForStatement forStatement)
|
|
{
|
|
if (!context.Settings.ForEachStatement)
|
|
return null;
|
|
Match m = forOnArrayPattern.Match(forStatement);
|
|
if (!m.Success)
|
|
return null;
|
|
var itemVariable = m.Get<IdentifierExpression>("itemVariable").Single().GetILVariable();
|
|
var indexVariable = m.Get<IdentifierExpression>("indexVariable").Single().GetILVariable();
|
|
var arrayVariable = m.Get<IdentifierExpression>("arrayVariable").Single().GetILVariable();
|
|
if (itemVariable == null || indexVariable == null || arrayVariable == null)
|
|
return null;
|
|
if (arrayVariable.Type.Kind != TypeKind.Array && !arrayVariable.Type.IsKnownType(KnownTypeCode.String))
|
|
return null;
|
|
if (!VariableCanBeUsedAsForeachLocal(itemVariable, forStatement))
|
|
return null;
|
|
if (indexVariable.StoreCount != 2 || indexVariable.LoadCount != 3 || indexVariable.AddressCount != 0)
|
|
return null;
|
|
context.Step("Introduce foreach over array", forStatement);
|
|
var body = new BlockStatement();
|
|
foreach (var statement in m.Get<Statement>("statements"))
|
|
body.Statements.Add(statement.Detach());
|
|
var foreachStmt = new ForeachStatement {
|
|
VariableType = context.Settings.AnonymousTypes && itemVariable.Type.ContainsAnonymousType() ? new SimpleType("var") : context.TypeSystemAstBuilder.ConvertType(itemVariable.Type),
|
|
VariableDesignation = new SingleVariableDesignation { Identifier = itemVariable.Name! },
|
|
InExpression = m.Get<IdentifierExpression>("arrayVariable").Single().Detach(),
|
|
EmbeddedStatement = body
|
|
};
|
|
foreachStmt.CopyAnnotationsFrom(forStatement);
|
|
itemVariable.Kind = IL.VariableKind.ForeachLocal;
|
|
// Add the variable annotation for highlighting (TokenTextWriter expects it directly on the ForeachStatement).
|
|
foreachStmt.VariableDesignation.AddAnnotation(new ILVariableResolveResult(itemVariable, itemVariable.Type));
|
|
// TODO : add ForeachAnnotation
|
|
forStatement.ReplaceWith(foreachStmt);
|
|
context.EndStep(foreachStmt);
|
|
return foreachStmt;
|
|
}
|
|
|
|
static readonly ForStatement forOnInlineArrayPattern = new ForStatement {
|
|
Initializers = {
|
|
new ExpressionStatement(
|
|
new AssignmentExpression(
|
|
new NamedNode("indexVariable", new IdentifierExpression(Pattern.AnyString)),
|
|
new PrimitiveExpression(0)
|
|
))
|
|
},
|
|
Condition = new BinaryOperatorExpression(
|
|
new IdentifierExpressionBackreference("indexVariable"),
|
|
BinaryOperatorType.LessThan,
|
|
new NamedNode("length", new PrimitiveExpression(PrimitiveExpression.AnyValue))
|
|
),
|
|
Iterators = {
|
|
new ExpressionStatement(
|
|
new AssignmentExpression(
|
|
new IdentifierExpressionBackreference("indexVariable"),
|
|
new BinaryOperatorExpression(new IdentifierExpressionBackreference("indexVariable"), BinaryOperatorType.Add, new PrimitiveExpression(1))
|
|
))
|
|
},
|
|
EmbeddedStatement = new BlockStatement {
|
|
Statements = {
|
|
new ExpressionStatement(new AssignmentExpression(
|
|
new NamedNode("itemVariable", new IdentifierExpression(Pattern.AnyString)),
|
|
new NamedNode("elementAccess", new AnyNode())
|
|
)),
|
|
new Repeat(new AnyNode("statements"))
|
|
}
|
|
}
|
|
};
|
|
|
|
/// <summary>
|
|
/// Reconstructs a <c>foreach</c> over an inline array from the <c>for</c> loop the compiler
|
|
/// lowers it to: <c>for (i = 0; i < N; i++) { item = <PrivateImplementationDetails>.InlineArrayElementRef(ref buffer, i); ... }</c>.
|
|
/// The rewrite is only sound because the loop bound <c>N</c> equals the inline array length,
|
|
/// which proves the index is always in range: <c>InlineArrayElementRef</c> is the compiler's
|
|
/// unchecked element accessor, whereas the C# inline-array indexer <c>buffer[i]</c> is
|
|
/// bounds-checked, so the two only agree when the index is provably in-bounds. A loop that
|
|
/// does not match this exact shape keeps the (unnameable but faithful) helper call.
|
|
/// </summary>
|
|
Statement? TransformForeachOnInlineArray(ForStatement forStatement)
|
|
{
|
|
if (!context.Settings.ForEachStatement || !context.Settings.InlineArrays)
|
|
return null;
|
|
Match m = forOnInlineArrayPattern.Match(forStatement);
|
|
if (!m.Success)
|
|
return null;
|
|
var itemVariable = m.Get<IdentifierExpression>("itemVariable").Single().GetILVariable();
|
|
var indexVariable = m.Get<IdentifierExpression>("indexVariable").Single().GetILVariable();
|
|
if (itemVariable == null || indexVariable == null)
|
|
return null;
|
|
|
|
// The loop body must start with `item = InlineArrayElementRef(ref buffer, index)`.
|
|
if (m.Get<Expression>("elementAccess").Single() is not InvocationExpression elementAccess)
|
|
return null;
|
|
if (elementAccess.GetSymbol() is not IMethod { DeclaringType.FullName: "<PrivateImplementationDetails>" } helper)
|
|
return null;
|
|
if (helper.Name is not ("InlineArrayElementRef" or "InlineArrayElementRefReadOnly"))
|
|
return null;
|
|
if (elementAccess.Arguments.Count != 2)
|
|
return null;
|
|
// arg0: `ref buffer`, arg1: the loop index.
|
|
if (elementAccess.Arguments.First() is not DirectionExpression { Expression: IdentifierExpression bufferIdentifier })
|
|
return null;
|
|
var bufferVariable = bufferIdentifier.GetILVariable();
|
|
if (bufferVariable == null)
|
|
return null;
|
|
if (elementAccess.Arguments.Last() is not IdentifierExpression indexIdentifier
|
|
|| indexIdentifier.GetILVariable() != indexVariable)
|
|
return null;
|
|
|
|
// Soundness: the loop counts 0..length-1 over exactly the inline array's length, so the
|
|
// index is provably in range. Any other bound (or a non-inline-array buffer) is rejected.
|
|
if (bufferVariable.Type.GetInlineArrayLength() is not int arrayLength)
|
|
return null;
|
|
if (m.Get<PrimitiveExpression>("length").Single().Value is not int loopBound || loopBound != arrayLength)
|
|
return null;
|
|
|
|
if (!VariableCanBeUsedAsForeachLocal(itemVariable, forStatement))
|
|
return null;
|
|
// The index is a pure counter: stored at init + increment, loaded at the condition,
|
|
// the increment, and the element access; never captured by address.
|
|
if (indexVariable.StoreCount != 2 || indexVariable.LoadCount != 3 || indexVariable.AddressCount != 0)
|
|
return null;
|
|
|
|
context.Step("Introduce foreach over inline array", forStatement);
|
|
// Take the buffer reference for the `in` expression before dropping the element access.
|
|
var inExpression = bufferIdentifier.Detach();
|
|
// Reuse the loop body (preserving its annotations) after removing its leading
|
|
// `item = <PrivateImplementationDetails>.InlineArrayElementRef(ref buffer, i)` statement.
|
|
var body = (BlockStatement)forStatement.EmbeddedStatement;
|
|
body.Statements.First().Remove();
|
|
var foreachStmt = new ForeachStatement {
|
|
VariableType = context.Settings.AnonymousTypes && itemVariable.Type.ContainsAnonymousType() ? new SimpleType("var") : context.TypeSystemAstBuilder.ConvertType(itemVariable.Type),
|
|
VariableDesignation = new SingleVariableDesignation { Identifier = itemVariable.Name! },
|
|
InExpression = inExpression,
|
|
EmbeddedStatement = body.Detach()
|
|
};
|
|
foreachStmt.CopyAnnotationsFrom(forStatement);
|
|
itemVariable.Kind = IL.VariableKind.ForeachLocal;
|
|
foreachStmt.VariableDesignation.AddAnnotation(new ILVariableResolveResult(itemVariable, itemVariable.Type));
|
|
forStatement.ReplaceWith(foreachStmt);
|
|
context.EndStep(foreachStmt);
|
|
return foreachStmt;
|
|
}
|
|
|
|
static readonly ForStatement forOnArrayMultiDimPattern = new ForStatement {
|
|
Initializers = { },
|
|
Condition = new BinaryOperatorExpression(
|
|
new NamedNode("indexVariable", new IdentifierExpression(Pattern.AnyString)),
|
|
BinaryOperatorType.LessThanOrEqual,
|
|
new NamedNode("upperBoundVariable", new IdentifierExpression(Pattern.AnyString))
|
|
),
|
|
Iterators = {
|
|
new ExpressionStatement(
|
|
new AssignmentExpression(
|
|
new IdentifierExpressionBackreference("indexVariable"),
|
|
new BinaryOperatorExpression(new IdentifierExpressionBackreference("indexVariable"), BinaryOperatorType.Add, new PrimitiveExpression(1))
|
|
))
|
|
},
|
|
EmbeddedStatement = new BlockStatement { Statements = { new AnyNode("lowerBoundAssign"), new Repeat(new AnyNode("statements")) } }
|
|
};
|
|
|
|
/// <summary>
|
|
/// $variable = $collection.GetUpperBound($index);
|
|
/// </summary>
|
|
static readonly AstNode variableAssignUpperBoundPattern = new ExpressionStatement(
|
|
new AssignmentExpression(
|
|
new NamedNode("variable", new IdentifierExpression(Pattern.AnyString)),
|
|
new InvocationExpression(
|
|
new MemberReferenceExpression(
|
|
new NamedNode("collection", new IdentifierExpression(Pattern.AnyString)),
|
|
"GetUpperBound"
|
|
),
|
|
new NamedNode("index", new PrimitiveExpression(PrimitiveExpression.AnyValue))
|
|
)));
|
|
|
|
/// <summary>
|
|
/// $variable = $collection.GetLowerBound($index);
|
|
/// </summary>
|
|
static readonly ExpressionStatement variableAssignLowerBoundPattern = new ExpressionStatement(
|
|
new AssignmentExpression(
|
|
new NamedNode("variable", new IdentifierExpression(Pattern.AnyString)),
|
|
new InvocationExpression(
|
|
new MemberReferenceExpression(
|
|
new NamedNode("collection", new IdentifierExpression(Pattern.AnyString)),
|
|
"GetLowerBound"
|
|
),
|
|
new NamedNode("index", new PrimitiveExpression(PrimitiveExpression.AnyValue))
|
|
)));
|
|
|
|
/// <summary>
|
|
/// $variable = $collection[$index1, $index2, ...];
|
|
/// </summary>
|
|
static readonly ExpressionStatement foreachVariableOnMultArrayAssignPattern = new ExpressionStatement(
|
|
new AssignmentExpression(
|
|
new NamedNode("variable", new IdentifierExpression(Pattern.AnyString)),
|
|
new IndexerExpression(
|
|
new NamedNode("collection", new IdentifierExpression(Pattern.AnyString)),
|
|
new Repeat(new NamedNode("index", new IdentifierExpression(Pattern.AnyString))
|
|
)
|
|
)));
|
|
|
|
bool MatchLowerBound(int indexNum, [NotNullWhen(true)] out IL.ILVariable? index, IL.ILVariable collection, Statement statement)
|
|
{
|
|
index = null;
|
|
var m = variableAssignLowerBoundPattern.Match(statement);
|
|
if (!m.Success)
|
|
return false;
|
|
if (!int.TryParse(m.Get<PrimitiveExpression>("index").Single().Value.ToString(), out int i) || indexNum != i)
|
|
return false;
|
|
index = m.Get<IdentifierExpression>("variable").Single().GetILVariable();
|
|
return m.Get<IdentifierExpression>("collection").Single().GetILVariable() == collection;
|
|
}
|
|
|
|
bool MatchForeachOnMultiDimArray(IL.ILVariable[] upperBounds, IL.ILVariable collection, Statement firstInitializerStatement, [NotNullWhen(true)] out IdentifierExpression? foreachVariable, [NotNullWhen(true)] out IList<Statement>? statements, out IL.ILVariable[] lowerBounds)
|
|
{
|
|
int i = 0;
|
|
foreachVariable = null;
|
|
statements = null;
|
|
lowerBounds = new IL.ILVariable[upperBounds.Length];
|
|
Statement stmt = firstInitializerStatement;
|
|
Match m = default(Match);
|
|
while (i < upperBounds.Length && MatchLowerBound(i, out var indexVariable, collection, stmt))
|
|
{
|
|
m = forOnArrayMultiDimPattern.Match(stmt.GetNextStatement());
|
|
if (!m.Success)
|
|
return false;
|
|
var upperBound = m.Get<IdentifierExpression>("upperBoundVariable").Single().GetILVariable();
|
|
if (upperBounds[i] != upperBound)
|
|
return false;
|
|
stmt = m.Get<Statement>("lowerBoundAssign").Single();
|
|
lowerBounds[i] = indexVariable;
|
|
i++;
|
|
}
|
|
if (collection.Type.Kind != TypeKind.Array)
|
|
return false;
|
|
var m2 = foreachVariableOnMultArrayAssignPattern.Match(stmt);
|
|
if (!m2.Success)
|
|
return false;
|
|
var collection2 = m2.Get<IdentifierExpression>("collection").Single().GetILVariable();
|
|
if (collection2 != collection)
|
|
return false;
|
|
foreachVariable = m2.Get<IdentifierExpression>("variable").Single();
|
|
statements = m.Get<Statement>("statements").ToList();
|
|
return true;
|
|
}
|
|
|
|
Statement? TransformForeachOnMultiDimArray(ExpressionStatement expressionStatement)
|
|
{
|
|
if (!context.Settings.ForEachStatement)
|
|
return null;
|
|
Match m;
|
|
Statement? stmt = expressionStatement;
|
|
IL.ILVariable? collection = null;
|
|
IL.ILVariable[]? upperBounds = null;
|
|
List<Statement> statementsToDelete = new List<Statement>();
|
|
int i = 0;
|
|
// first we look for all the upper bound initializations
|
|
do
|
|
{
|
|
m = variableAssignUpperBoundPattern.Match(stmt);
|
|
if (!m.Success)
|
|
break;
|
|
if (upperBounds == null)
|
|
{
|
|
collection = m.Get<IdentifierExpression>("collection").Single().GetILVariable();
|
|
if (!(collection?.Type is Decompiler.TypeSystem.ArrayType arrayType))
|
|
break;
|
|
upperBounds = new IL.ILVariable[arrayType.Dimensions];
|
|
}
|
|
else
|
|
{
|
|
statementsToDelete.Add(stmt);
|
|
}
|
|
var nextCollection = m.Get<IdentifierExpression>("collection").Single().GetILVariable();
|
|
if (nextCollection != collection)
|
|
break;
|
|
if (!int.TryParse(m.Get<PrimitiveExpression>("index").Single().Value?.ToString() ?? "", out int index) || index != i)
|
|
break;
|
|
upperBounds[i] = m.Get<IdentifierExpression>("variable").Single().GetILVariable()!;
|
|
stmt = stmt.GetNextStatement();
|
|
i++;
|
|
} while (stmt != null && upperBounds != null && i < upperBounds.Length);
|
|
|
|
if (upperBounds?.LastOrDefault() == null || collection == null || stmt == null)
|
|
return null;
|
|
if (!MatchForeachOnMultiDimArray(upperBounds, collection, stmt, out var foreachVariable, out var statements, out var lowerBounds))
|
|
return null;
|
|
statementsToDelete.Add(stmt);
|
|
// The matched multi-dimensional foreach pattern guarantees a statement after stmt.
|
|
statementsToDelete.Add(stmt.GetNextStatement()!);
|
|
var itemVariable = foreachVariable.GetILVariable();
|
|
if (itemVariable == null || !itemVariable.IsSingleDefinition
|
|
|| (itemVariable.Kind != IL.VariableKind.Local && itemVariable.Kind != IL.VariableKind.StackSlot)
|
|
|| !upperBounds.All(ub => ub.IsSingleDefinition && ub.LoadCount == 1)
|
|
|| !lowerBounds.All(lb => lb.StoreCount == 2 && lb.LoadCount == 3 && lb.AddressCount == 0))
|
|
return null;
|
|
context.Step("Introduce foreach over multidimensional array", expressionStatement);
|
|
var body = new BlockStatement();
|
|
foreach (var statement in statements)
|
|
body.Statements.Add(statement.Detach());
|
|
var foreachStmt = new ForeachStatement {
|
|
VariableType = context.Settings.AnonymousTypes && itemVariable.Type.ContainsAnonymousType() ? new SimpleType("var") : context.TypeSystemAstBuilder.ConvertType(itemVariable.Type),
|
|
VariableDesignation = new SingleVariableDesignation { Identifier = itemVariable.Name! },
|
|
InExpression = m.Get<IdentifierExpression>("collection").Single().Detach(),
|
|
EmbeddedStatement = body
|
|
};
|
|
foreach (var statement in statementsToDelete)
|
|
statement.Detach();
|
|
//foreachStmt.CopyAnnotationsFrom(forStatement);
|
|
itemVariable.Kind = IL.VariableKind.ForeachLocal;
|
|
// Add the variable annotation for highlighting (TokenTextWriter expects it directly on the ForeachStatement).
|
|
foreachStmt.VariableDesignation.AddAnnotation(new ILVariableResolveResult(itemVariable, itemVariable.Type));
|
|
// TODO : add ForeachAnnotation
|
|
expressionStatement.ReplaceWith(foreachStmt);
|
|
context.EndStep(foreachStmt);
|
|
return foreachStmt;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Automatic Properties
|
|
static readonly PropertyDeclaration automaticPropertyPattern = new PropertyDeclaration {
|
|
Attributes = { new Repeat(new AnyNode()) },
|
|
Modifiers = Modifiers.Any,
|
|
ReturnType = new AnyNode(),
|
|
PrivateImplementationType = new OptionalNode(new AnyNode()),
|
|
Name = Pattern.AnyString,
|
|
Getter = new Accessor {
|
|
Attributes = { new Repeat(new AnyNode()) },
|
|
Modifiers = Modifiers.Any,
|
|
Body = new BlockStatement {
|
|
new ReturnStatement {
|
|
Expression = new AnyNode("fieldReference")
|
|
}
|
|
}
|
|
},
|
|
Setter = new Accessor {
|
|
Attributes = { new Repeat(new AnyNode()) },
|
|
Modifiers = Modifiers.Any,
|
|
Body = new BlockStatement {
|
|
new AssignmentExpression {
|
|
Left = new Backreference("fieldReference"),
|
|
Right = new IdentifierExpression("value")
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
static readonly PropertyDeclaration automaticReadonlyPropertyPattern = new PropertyDeclaration {
|
|
Attributes = { new Repeat(new AnyNode()) },
|
|
Modifiers = Modifiers.Any,
|
|
ReturnType = new AnyNode(),
|
|
PrivateImplementationType = new OptionalNode(new AnyNode()),
|
|
Name = Pattern.AnyString,
|
|
Getter = new Accessor {
|
|
Attributes = { new Repeat(new AnyNode()) },
|
|
Modifiers = Modifiers.Any,
|
|
Body = new BlockStatement {
|
|
new ReturnStatement {
|
|
Expression = new AnyNode("fieldReference")
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
bool CanTransformToAutomaticProperty(IProperty property, bool accessorsMustBeCompilerGenerated)
|
|
{
|
|
if (!property.CanGet)
|
|
return false;
|
|
if (accessorsMustBeCompilerGenerated && !property.Getter.IsCompilerGenerated())
|
|
return false;
|
|
if (property.Setter is IMethod setter)
|
|
{
|
|
if (accessorsMustBeCompilerGenerated && !setter.IsCompilerGenerated())
|
|
return false;
|
|
if (setter.HasReadonlyModifier())
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
PropertyDeclaration? TransformAutomaticProperty(PropertyDeclaration propertyDeclaration)
|
|
{
|
|
IProperty? property = propertyDeclaration.GetSymbol() as IProperty;
|
|
if (property == null)
|
|
return null;
|
|
if (context.Settings.FieldKeyword)
|
|
return TransformFieldBackedProperty(propertyDeclaration, property);
|
|
if (!CanTransformToAutomaticProperty(property, !(property.DeclaringTypeDefinition?.Fields.Any(f => f.Name == "_" + property.Name && f.IsCompilerGenerated()) ?? false)))
|
|
return null;
|
|
IField? field = null;
|
|
Match m = automaticPropertyPattern.Match(propertyDeclaration);
|
|
if (m.Success)
|
|
{
|
|
field = m.Get<AstNode>("fieldReference").Single().GetSymbol() as IField;
|
|
}
|
|
else
|
|
{
|
|
Match m2 = automaticReadonlyPropertyPattern.Match(propertyDeclaration);
|
|
if (m2.Success)
|
|
{
|
|
field = m2.Get<AstNode>("fieldReference").Single().GetSymbol() as IField;
|
|
}
|
|
}
|
|
if (field == null || !NameCouldBeBackingFieldOfAutomaticProperty(field.Name, out _))
|
|
return null;
|
|
// In generic types the accessor bodies reference the field specialized by the
|
|
// type's own type parameters; the field declaration's symbol is the definition.
|
|
field = (IField)field.MemberDefinition;
|
|
if (propertyDeclaration.Setter?.HasModifier(Modifiers.Readonly) == true || (propertyDeclaration.HasModifier(Modifiers.Readonly) && propertyDeclaration.Setter is not null))
|
|
return null;
|
|
if (field.IsCompilerGenerated() && field.DeclaringTypeDefinition == property.DeclaringTypeDefinition)
|
|
{
|
|
context.Step("Convert property to auto-property", propertyDeclaration);
|
|
// Clearing the accessor body turns it into an auto-property accessor.
|
|
var getter = propertyDeclaration.Getter;
|
|
var setter = propertyDeclaration.Setter;
|
|
if (getter is not null)
|
|
{
|
|
RemoveCompilerGeneratedAttribute(getter.Attributes);
|
|
getter.Body = null;
|
|
}
|
|
if (setter is not null)
|
|
{
|
|
RemoveCompilerGeneratedAttribute(setter.Attributes);
|
|
setter.Body = null;
|
|
}
|
|
propertyDeclaration.Modifiers &= ~Modifiers.Readonly;
|
|
if (getter is not null)
|
|
getter.Modifiers &= ~Modifiers.Readonly;
|
|
|
|
var fieldDecl = propertyDeclaration.Parent?.Children.OfType<FieldDeclaration>()
|
|
.FirstOrDefault(fd => field.Equals(fd.GetSymbol()));
|
|
if (fieldDecl != null)
|
|
{
|
|
fieldDecl.Remove();
|
|
// Add C# 7.3 attributes on backing field:
|
|
CSharpDecompiler.RemoveAttribute(fieldDecl, KnownAttribute.CompilerGenerated);
|
|
CSharpDecompiler.RemoveAttribute(fieldDecl, KnownAttribute.DebuggerBrowsable);
|
|
foreach (var section in fieldDecl.Attributes)
|
|
{
|
|
section.AttributeTarget = "field";
|
|
propertyDeclaration.Attributes.Add(section.Detach());
|
|
}
|
|
}
|
|
}
|
|
// Since the property instance is not changed, we can continue in the visitor as usual, so return null
|
|
return null;
|
|
}
|
|
|
|
static readonly BlockStatement trivialFieldGetterBody = new BlockStatement {
|
|
new ReturnStatement {
|
|
Expression = new NamedNode("fieldReference", new IdentifierExpression("field"))
|
|
}
|
|
};
|
|
|
|
static readonly BlockStatement trivialFieldSetterBody = new BlockStatement {
|
|
new AssignmentExpression {
|
|
Left = new NamedNode("fieldReference", new IdentifierExpression("field")),
|
|
Right = new IdentifierExpression("value")
|
|
}
|
|
};
|
|
|
|
/// <summary>
|
|
/// Handles all field-backed properties when the C# 14 "field" keyword is available:
|
|
/// accessor bodies already refer to the backing field as "field" (see
|
|
/// ExpressionBuilder.ConvertField), so compiler-generated trivial accessors collapse
|
|
/// individually to "get;"/"set;" and the backing-field declaration disappears, with its
|
|
/// attributes re-hosted as "field:" sections on the property.
|
|
/// </summary>
|
|
PropertyDeclaration? TransformFieldBackedProperty(PropertyDeclaration propertyDeclaration, IProperty property)
|
|
{
|
|
if (!TryGetBackingField(property, out var field))
|
|
return null;
|
|
if (!OutsideReferencesAreExpressible(propertyDeclaration, field))
|
|
{
|
|
// The field stays declared, so the "field" keyword references emitted by
|
|
// ExpressionBuilder.ConvertField have to become ordinary field references again.
|
|
foreach (var identifierExpression in propertyDeclaration.Descendants.OfType<IdentifierExpression>())
|
|
{
|
|
if (identifierExpression.Identifier == "field"
|
|
&& identifierExpression.GetSymbol() is IField referencedField
|
|
&& field.Equals(referencedField.MemberDefinition))
|
|
{
|
|
identifierExpression.Identifier = field.Name;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
context.Step("Transform field-backed property", propertyDeclaration);
|
|
var getter = propertyDeclaration.Getter;
|
|
var setter = propertyDeclaration.Setter;
|
|
if (context.Settings.AutomaticProperties)
|
|
{
|
|
// VB auto-properties do not mark their accessors [CompilerGenerated]; the
|
|
// pre-C# 14 transform recognizes them by their "_<PropertyName>" backing field
|
|
// instead. Keep that rule, or a VB auto-property grows explicit "field"
|
|
// accessors where every other compiler's collapses to "{ get; set; }".
|
|
bool accessorsMustBeCompilerGenerated = field.Name != "_" + property.Name;
|
|
CollapseTrivialAccessor(getter, trivialFieldGetterBody, field, accessorsMustBeCompilerGenerated);
|
|
// A readonly setter cannot become an auto-accessor (same rule as the pre-C# 14
|
|
// transform); readonly getters collapse fine because auto-getters are
|
|
// implicitly readonly.
|
|
if (setter?.HasModifier(Modifiers.Readonly) != true
|
|
&& !(propertyDeclaration.HasModifier(Modifiers.Readonly) && setter is not null))
|
|
{
|
|
CollapseTrivialAccessor(setter, trivialFieldSetterBody, field, accessorsMustBeCompilerGenerated);
|
|
}
|
|
}
|
|
if (getter?.Body == null && setter?.Body == null)
|
|
{
|
|
// The property became a full auto-property; readonly is implied like before.
|
|
propertyDeclaration.Modifiers &= ~Modifiers.Readonly;
|
|
if (getter is not null)
|
|
getter.Modifiers &= ~Modifiers.Readonly;
|
|
}
|
|
var fieldDecl = propertyDeclaration.Parent?.Children.OfType<FieldDeclaration>()
|
|
.FirstOrDefault(fd => field.Equals(fd.GetSymbol()));
|
|
if (fieldDecl != null)
|
|
{
|
|
fieldDecl.Remove();
|
|
CSharpDecompiler.RemoveAttribute(fieldDecl, KnownAttribute.CompilerGenerated);
|
|
CSharpDecompiler.RemoveAttribute(fieldDecl, KnownAttribute.DebuggerBrowsable);
|
|
foreach (var section in fieldDecl.Attributes)
|
|
{
|
|
section.AttributeTarget = "field";
|
|
propertyDeclaration.Attributes.Add(section.Detach());
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
void CollapseTrivialAccessor(Accessor? accessor, BlockStatement pattern, IField field,
|
|
bool accessorMustBeCompilerGenerated)
|
|
{
|
|
if (accessor?.Body is null)
|
|
return;
|
|
if (accessorMustBeCompilerGenerated
|
|
&& (accessor.GetSymbol() is not IMethod method || !method.IsCompilerGenerated()))
|
|
{
|
|
return;
|
|
}
|
|
Match m = pattern.Match(accessor.Body);
|
|
if (!m.Success)
|
|
return;
|
|
if (m.Get<AstNode>("fieldReference").Single().GetSymbol() is not IField referencedField
|
|
|| !field.Equals(referencedField.MemberDefinition))
|
|
{
|
|
return;
|
|
}
|
|
RemoveCompilerGeneratedAttribute(accessor.Attributes);
|
|
// Auto-accessors are implicitly readonly.
|
|
accessor.Modifiers &= ~Modifiers.Readonly;
|
|
// Clearing the accessor body turns it into an auto-property accessor.
|
|
accessor.Body = null;
|
|
}
|
|
|
|
internal static bool TryGetBackingField(IProperty property, [NotNullWhen(true)] out IField? field)
|
|
{
|
|
field = null;
|
|
if (property.Parameters.Count > 0 || property.DeclaringTypeDefinition == null)
|
|
return false;
|
|
// A type definition's fields are unspecialized, so compare against the property
|
|
// DEFINITION's return type; a specialized property in a generic type would
|
|
// otherwise never match its own backing field.
|
|
var propertyType = ((IProperty)property.MemberDefinition).ReturnType;
|
|
foreach (var candidate in property.DeclaringTypeDefinition.Fields)
|
|
{
|
|
if (candidate.IsCompilerGenerated()
|
|
&& candidate.IsStatic == property.IsStatic
|
|
// The trivial accessor bodies of a classic auto-property guaranteed this
|
|
// structurally; arbitrary accessor bodies do not. A field of a different
|
|
// type is not this property's storage, and removing it while printing
|
|
// `field` would substitute storage of the property's type instead.
|
|
&& candidate.Type.Equals(propertyType)
|
|
&& NameCouldBeBackingFieldOfAutomaticProperty(candidate.Name, out var propertyName)
|
|
&& propertyName == property.Name)
|
|
{
|
|
field = candidate;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The "field" keyword cannot express backing-field accesses outside the owning
|
|
/// property's accessors. The only shapes C# can express are stores in a constructor of
|
|
/// the declaring type (property initializers, or assignments to setter-less
|
|
/// properties); anything else means the field declaration has to be kept.
|
|
/// </summary>
|
|
bool OutsideReferencesAreExpressible(AstNode nodeInTree, IField field)
|
|
{
|
|
var root = nodeInTree.Ancestors.LastOrDefault() ?? nodeInTree;
|
|
if (outsideReferenceRoot != root)
|
|
{
|
|
outsideReferenceRoot = root;
|
|
outsideReferenceVerdicts = BuildOutsideReferenceIndex(root);
|
|
}
|
|
// Absent means no reference to this field was found outside its own property.
|
|
return !outsideReferenceVerdicts!.TryGetValue((IField)field.MemberDefinition, out bool expressible)
|
|
|| expressible;
|
|
}
|
|
|
|
// The verdict per backing field for one syntax tree. Answering each property with its
|
|
// own full walk made whole-module output quadratic in the number of properties, which
|
|
// a property-heavy assembly feels as minutes instead of seconds. Later transforms only
|
|
// ever REMOVE references to the field of the property they are rewriting, so a verdict
|
|
// computed up front stays valid for every other property in the tree.
|
|
AstNode? outsideReferenceRoot;
|
|
Dictionary<IField, bool>? outsideReferenceVerdicts;
|
|
|
|
/// <summary>
|
|
/// Whether a constructor store to <paramref name="field"/> still has a home once the
|
|
/// field declaration is gone. Something has to turn it into an initializer or a
|
|
/// property assignment, and not every store qualifies.
|
|
/// </summary>
|
|
bool StoreSurvivesAsInitializer(IField field)
|
|
{
|
|
// A setter-less property's store is rewritten to a property assignment by
|
|
// ReplaceBackingFieldUsage, in this same transform - no later transform involved.
|
|
if (IsBackingFieldOfAutomaticProperty(field, out var property) && !property.CanSet)
|
|
return true;
|
|
// Everything else waits for TransformFieldAndConstructorInitializers, which runs
|
|
// after this transform and declines to move non-constant stores out of an EXPLICIT
|
|
// static constructor (see its `onlyMoveConstants`). Removing the declaration here
|
|
// would leave that store referencing a field that no longer exists.
|
|
if (field.IsStatic && !context.Settings.AlwaysMoveInitializer
|
|
&& !IsBeforeFieldInit(field.DeclaringTypeDefinition))
|
|
{
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool IsBeforeFieldInit(ITypeDefinition? typeDefinition)
|
|
{
|
|
if (typeDefinition?.MetadataToken.IsNil != false)
|
|
return false;
|
|
var metadata = context.TypeSystem.MainModule.MetadataFile.Metadata;
|
|
var td = metadata.GetTypeDefinition((TypeDefinitionHandle)typeDefinition.MetadataToken);
|
|
return td.HasFlag(System.Reflection.TypeAttributes.BeforeFieldInit);
|
|
}
|
|
|
|
Dictionary<IField, bool> BuildOutsideReferenceIndex(AstNode root)
|
|
{
|
|
var verdicts = new Dictionary<IField, bool>();
|
|
foreach (var node in root.Descendants)
|
|
{
|
|
if (node is not (IdentifierExpression or MemberReferenceExpression))
|
|
continue;
|
|
if (node.GetSymbol() is not IField referencedField)
|
|
continue;
|
|
var definition = (IField)referencedField.MemberDefinition;
|
|
// A reference inside the accessors of the field's own property is exactly what
|
|
// the "field" keyword expresses; anything else is an outside reference.
|
|
if (node.Ancestors.OfType<PropertyDeclaration>().FirstOrDefault()?.GetSymbol() is IProperty owner
|
|
&& TryGetBackingField(owner, out var ownerField)
|
|
&& definition.Equals(ownerField.MemberDefinition))
|
|
{
|
|
continue;
|
|
}
|
|
var enclosingMethod = node.Ancestors.OfType<EntityDeclaration>().FirstOrDefault()?.GetSymbol() as IMethod;
|
|
if (!IsConstructorStore(node, definition, enclosingMethod) || !StoreSurvivesAsInitializer(definition))
|
|
verdicts[definition] = false;
|
|
else if (!verdicts.ContainsKey(definition))
|
|
verdicts[definition] = true;
|
|
}
|
|
return verdicts;
|
|
}
|
|
|
|
static void RemoveCompilerGeneratedAttribute(AstNodeCollection<AttributeSection> attributeSections)
|
|
{
|
|
RemoveCompilerGeneratedAttribute(attributeSections, "System.Runtime.CompilerServices.CompilerGeneratedAttribute");
|
|
}
|
|
|
|
static void RemoveCompilerGeneratedAttribute(AstNodeCollection<AttributeSection> attributeSections, params string[] attributesToRemove)
|
|
{
|
|
foreach (AttributeSection section in attributeSections)
|
|
{
|
|
foreach (var attr in section.Attributes)
|
|
{
|
|
var tr = attr.Type.GetSymbol() as IType;
|
|
if (tr != null && attributesToRemove.Contains(tr.FullName))
|
|
{
|
|
attr.Remove();
|
|
}
|
|
}
|
|
if (section.Attributes.Count == 0)
|
|
section.Remove();
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
public override AstNode VisitIdentifier(Identifier identifier)
|
|
{
|
|
if (context.Settings.AutomaticProperties || context.Settings.FieldKeyword)
|
|
{
|
|
var newIdentifier = ReplaceBackingFieldUsage(identifier);
|
|
if (newIdentifier != null)
|
|
{
|
|
identifier.ReplaceWith(newIdentifier);
|
|
context.EndStep(newIdentifier);
|
|
return newIdentifier;
|
|
}
|
|
}
|
|
return base.VisitIdentifier(identifier);
|
|
}
|
|
|
|
internal static bool IsBackingFieldOfAutomaticProperty(IField field, [NotNullWhen(true)] out IProperty? property)
|
|
{
|
|
property = null;
|
|
if (!NameCouldBeBackingFieldOfAutomaticProperty(field.Name, out var propertyName))
|
|
return false;
|
|
var candidate = field.DeclaringTypeDefinition?
|
|
.GetProperties(p => p.Name == propertyName, GetMemberOptions.IgnoreInheritedMembers)
|
|
.FirstOrDefault();
|
|
// Answering through TryGetBackingField keeps the two directions of the same question
|
|
// from disagreeing: it is the predicate every transform consults before removing a
|
|
// declaration, and it checks more than the name (compiler-generated, staticness and
|
|
// field type all have to match the property).
|
|
if (candidate == null || !TryGetBackingField(candidate, out var backingField)
|
|
|| !field.MemberDefinition.Equals(backingField.MemberDefinition))
|
|
{
|
|
return false;
|
|
}
|
|
property = candidate;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// This matches the following patterns
|
|
/// <list type="bullet">
|
|
/// <item><Property>k__BackingField (used by C#)</item>
|
|
/// <item>_Property (used by VB)</item>
|
|
/// </list>
|
|
/// </summary>
|
|
static readonly System.Text.RegularExpressions.Regex automaticPropertyBackingFieldNameRegex
|
|
= new System.Text.RegularExpressions.Regex(@"^(<(?<name>.+)>k__BackingField|_(?<name>.+))$");
|
|
|
|
internal static bool NameCouldBeBackingFieldOfAutomaticProperty(string name, [NotNullWhen(true)] out string? propertyName)
|
|
{
|
|
propertyName = null;
|
|
var m = automaticPropertyBackingFieldNameRegex.Match(name);
|
|
if (!m.Success)
|
|
return false;
|
|
propertyName = m.Groups["name"].Value;
|
|
return true;
|
|
}
|
|
|
|
Identifier? ReplaceBackingFieldUsage(Identifier identifier)
|
|
{
|
|
// The resolve result, not the spelling, identifies the member: after
|
|
// ExpressionBuilder.ConvertField the same backing field appears both as the C# 14 "field"
|
|
// keyword and under its metadata name, carrying the same annotation either way.
|
|
var parent = identifier.Parent;
|
|
if (parent == null)
|
|
return null;
|
|
var mrr = parent.Annotation<MemberResolveResult>();
|
|
if (mrr?.Member is not IField field || !IsBackingFieldOfAutomaticProperty(field, out var property)
|
|
|| currentMethod?.AccessorOwner == property)
|
|
{
|
|
return null;
|
|
}
|
|
// With the keyword available TransformAutomaticProperty routes every property through
|
|
// TransformFieldBackedProperty, so its verdict on the declaration is the only one that
|
|
// counts: while the field stays declared, a field reference remains the correct - and
|
|
// only compilable - rendering.
|
|
if (context.Settings.FieldKeyword && !BackingFieldWillBeRemoved(property, field, parent))
|
|
return null;
|
|
if (context.Settings.AutomaticProperties
|
|
&& CanTransformToAutomaticProperty(property, !(field.IsCompilerGenerated() && field.Name == "_" + property.Name)))
|
|
{
|
|
if (!property.CanSet && !context.Settings.GetterOnlyAutomaticProperties)
|
|
return null;
|
|
}
|
|
else if (context.Settings.FieldKeyword && !property.CanSet && IsConstructorStoreTarget(parent, field))
|
|
{
|
|
// A setter-less field-backed property keeping explicit accessors is still assignable
|
|
// by name inside a constructor of its declaring type, and that is the only form the
|
|
// store has left once the declaration is gone.
|
|
}
|
|
else
|
|
{
|
|
// The property keeps explicit accessors and a setter, so its name would invoke that
|
|
// setter. The store stays a field reference and TransformFieldAndConstructorInitializers
|
|
// lifts it into the property initializer, which is what field-backed storage means.
|
|
return null;
|
|
}
|
|
context.Step("Replace backing field use with property", identifier);
|
|
parent.RemoveAnnotations<MemberResolveResult>();
|
|
parent.AddAnnotation(new MemberResolveResult(mrr.TargetResult, property));
|
|
return Identifier.Create(property.Name);
|
|
}
|
|
|
|
bool IsConstructorStoreTarget(AstNode node, IField field)
|
|
=> IsConstructorStore(node, field, currentMethod);
|
|
|
|
/// <summary>
|
|
/// Whether <see cref="TransformFieldBackedProperty"/> will remove this field's
|
|
/// declaration. Rewriting a store before knowing that produces an assignment to a
|
|
/// property that keeps explicit, setter-less accessors.
|
|
/// </summary>
|
|
bool BackingFieldWillBeRemoved(IProperty property, IField field, AstNode nodeInTree)
|
|
{
|
|
return context.Settings.FieldKeyword
|
|
// The same gate VisitPropertyDeclaration applies: a setter-less property is only
|
|
// transformed where getter-only auto-properties are allowed.
|
|
&& (property.CanSet || context.Settings.GetterOnlyAutomaticProperties)
|
|
&& TryGetBackingField(property, out var backingField)
|
|
&& field.MemberDefinition.Equals(backingField.MemberDefinition)
|
|
&& OutsideReferencesAreExpressible(nodeInTree, backingField);
|
|
}
|
|
|
|
/// <summary>
|
|
/// True when <paramref name="node"/> is the left-hand side of a plain assignment to
|
|
/// <paramref name="field"/> inside a constructor of the field's declaring type - the
|
|
/// only outside reference the "field" keyword can still express (as a property
|
|
/// initializer, or an assignment to a setter-less property).
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Shared by <see cref="OutsideReferencesAreExpressible"/>, which decides whether the
|
|
/// field declaration may be removed, and <see cref="ReplaceBackingFieldUsage"/>, which
|
|
/// rewrites the store. The two must agree: if only one of them accepts a store, the
|
|
/// output either references a removed field or assigns a property that stayed
|
|
/// field-backed. They differ only in how the enclosing method is known, which is why
|
|
/// it is a parameter here.
|
|
/// </remarks>
|
|
static bool IsConstructorStore(AstNode node, IField field, IMethod? enclosingMethod)
|
|
{
|
|
if (node.Parent is not AssignmentExpression { Operator: AssignmentOperatorType.Assign } assignment
|
|
|| assignment.Left != node)
|
|
{
|
|
return false;
|
|
}
|
|
return enclosingMethod is { IsConstructor: true } ctor
|
|
&& ctor.IsStatic == field.IsStatic
|
|
&& ctor.DeclaringTypeDefinition == field.DeclaringTypeDefinition;
|
|
}
|
|
|
|
#region Automatic Events
|
|
internal static readonly string[] attributeTypesToRemoveFromAutoProperties = new[] {
|
|
"System.Runtime.CompilerServices.CompilerGeneratedAttribute",
|
|
"System.Diagnostics.DebuggerBrowsableAttribute"
|
|
};
|
|
|
|
static bool IsEventBackingFieldDeclaration(FieldDeclaration fd, IEvent ev)
|
|
{
|
|
if (fd.Variables.Count > 1)
|
|
return false;
|
|
if (fd.GetSymbol() is not IField f)
|
|
return false;
|
|
if (f.ParentModule is not MetadataModule module)
|
|
return false;
|
|
return f.Accessibility == Accessibility.Private
|
|
&& ev.ReturnType.Equals(f.ReturnType)
|
|
&& module.MetadataFile.PropertyAndEventBackingFieldLookup.IsEventBackingField((FieldDefinitionHandle)f.MetadataToken, out _);
|
|
}
|
|
#endregion
|
|
|
|
#region Destructor
|
|
static readonly BlockStatement destructorBodyPattern = new BlockStatement {
|
|
new TryCatchStatement {
|
|
TryBlock = new AnyNode("body"),
|
|
FinallyBlock = new BlockStatement {
|
|
new InvocationExpression(new MemberReferenceExpression(new BaseReferenceExpression(), "Finalize"))
|
|
}
|
|
}
|
|
};
|
|
|
|
static readonly MethodDeclaration destructorPattern = new MethodDeclaration {
|
|
Attributes = { new Repeat(new AnyNode()) },
|
|
Modifiers = Modifiers.Any,
|
|
ReturnType = new PrimitiveType("void"),
|
|
Name = "Finalize",
|
|
Body = destructorBodyPattern
|
|
};
|
|
|
|
DestructorDeclaration? TransformDestructor(MethodDeclaration methodDef)
|
|
{
|
|
Match m = destructorPattern.Match(methodDef);
|
|
if (m.Success)
|
|
{
|
|
context.Step("Convert Finalize method to destructor", methodDef);
|
|
DestructorDeclaration dd = new DestructorDeclaration();
|
|
methodDef.Attributes.MoveTo(dd.Attributes);
|
|
dd.CopyAnnotationsFrom(methodDef);
|
|
dd.Modifiers = methodDef.Modifiers & ~(Modifiers.Protected | Modifiers.Override);
|
|
dd.Body = m.Get<BlockStatement>("body").Single().Detach();
|
|
// A destructor only appears inside a type declaration, so the context tracker
|
|
// has an enclosing type at this point.
|
|
dd.Name = currentTypeDefinition!.Name;
|
|
methodDef.ReplaceWith(dd);
|
|
context.EndStep(dd);
|
|
return dd;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
DestructorDeclaration? TransformDestructorBody(DestructorDeclaration dtorDef)
|
|
{
|
|
Match m = destructorBodyPattern.Match(dtorDef.Body);
|
|
if (m.Success)
|
|
{
|
|
context.Step("Simplify destructor body", dtorDef);
|
|
dtorDef.Body = m.Get<BlockStatement>("body").Single().Detach();
|
|
return dtorDef;
|
|
}
|
|
return null;
|
|
}
|
|
#endregion
|
|
|
|
#region Try-Catch-Finally
|
|
static readonly TryCatchStatement tryCatchFinallyPattern = new TryCatchStatement {
|
|
TryBlock = new BlockStatement {
|
|
new TryCatchStatement {
|
|
TryBlock = new AnyNode(),
|
|
CatchClauses = { new Repeat(new AnyNode()) }
|
|
}
|
|
},
|
|
FinallyBlock = new AnyNode()
|
|
};
|
|
|
|
/// <summary>
|
|
/// Simplify nested 'try { try {} catch {} } finally {}'.
|
|
/// This transformation must run after the using/lock tranformations.
|
|
/// </summary>
|
|
TryCatchStatement? TransformTryCatchFinally(TryCatchStatement tryFinally)
|
|
{
|
|
if (tryCatchFinallyPattern.IsMatch(tryFinally))
|
|
{
|
|
context.Step("Merge nested try-catch-finally", tryFinally);
|
|
TryCatchStatement tryCatch = (TryCatchStatement)tryFinally.TryBlock.Statements.Single();
|
|
tryFinally.TryBlock = tryCatch.TryBlock.Detach();
|
|
tryCatch.CatchClauses.MoveTo(tryFinally.CatchClauses);
|
|
}
|
|
// Since the tryFinally instance is not changed, we can continue in the visitor as usual, so return null
|
|
return null;
|
|
}
|
|
#endregion
|
|
|
|
#region Simplify cascading if-else-if statements
|
|
static readonly IfElseStatement cascadingIfElsePattern = new IfElseStatement {
|
|
Condition = new AnyNode(),
|
|
TrueStatement = new AnyNode(),
|
|
FalseStatement = new BlockStatement {
|
|
Statements = {
|
|
new NamedNode(
|
|
"nestedIfStatement",
|
|
new IfElseStatement {
|
|
Condition = new AnyNode(),
|
|
TrueStatement = new AnyNode(),
|
|
FalseStatement = new OptionalNode(new AnyNode())
|
|
}
|
|
)
|
|
}
|
|
}
|
|
};
|
|
|
|
AstNode? SimplifyCascadingIfElseStatements(IfElseStatement node)
|
|
{
|
|
Match m = cascadingIfElsePattern.Match(node);
|
|
if (m.Success)
|
|
{
|
|
context.Step("Simplify cascading if-else", node);
|
|
IfElseStatement elseIf = m.Get<IfElseStatement>("nestedIfStatement").Single();
|
|
node.FalseStatement = elseIf.Detach();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Use associativity of logic operators to avoid parentheses.
|
|
/// </summary>
|
|
public override AstNode VisitBinaryOperatorExpression(BinaryOperatorExpression expr)
|
|
{
|
|
switch (expr.Operator)
|
|
{
|
|
case BinaryOperatorType.ConditionalAnd:
|
|
case BinaryOperatorType.ConditionalOr:
|
|
// a && (b && c) ==> (a && b) && c
|
|
var bAndC = expr.Right as BinaryOperatorExpression;
|
|
if (bAndC != null && bAndC.Operator == expr.Operator)
|
|
{
|
|
context.Step("Reassociate conditional logic", expr);
|
|
// make bAndC the parent and expr the child.
|
|
// A conditional-and/or operator always has both operands present.
|
|
var b = bAndC.Left!.Detach();
|
|
var c = bAndC.Right!.Detach();
|
|
expr.ReplaceWith(bAndC.Detach());
|
|
bAndC.Left = expr;
|
|
bAndC.Right = c;
|
|
expr.Right = b;
|
|
context.EndStep(bAndC);
|
|
return base.VisitBinaryOperatorExpression(bAndC);
|
|
}
|
|
break;
|
|
}
|
|
return base.VisitBinaryOperatorExpression(expr);
|
|
}
|
|
|
|
public override AstNode VisitUnaryOperatorExpression(UnaryOperatorExpression expr)
|
|
{
|
|
if (expr.Operator == UnaryOperatorType.Not && expr.Expression is BinaryOperatorExpression { Operator: BinaryOperatorType.Equality } binary)
|
|
{
|
|
context.Step("Replace negated equality with inequality", expr);
|
|
binary.Operator = BinaryOperatorType.InEquality;
|
|
expr.ReplaceWith(binary.Detach());
|
|
context.EndStep(binary);
|
|
return VisitBinaryOperatorExpression(binary);
|
|
}
|
|
return base.VisitUnaryOperatorExpression(expr);
|
|
}
|
|
#endregion
|
|
|
|
#region C# 7.3 pattern based fixed (for value types)
|
|
// reference types are handled by DetectPinnedRegions.IsCustomRefPinPattern
|
|
static readonly Expression addressOfPinnableReference = new UnaryOperatorExpression {
|
|
Operator = UnaryOperatorType.AddressOf,
|
|
Expression = new InvocationExpression {
|
|
Target = new MemberReferenceExpression(new AnyNode("target"), "GetPinnableReference"),
|
|
Arguments = { }
|
|
}
|
|
};
|
|
|
|
public override AstNode VisitFixedStatement(FixedStatement fixedStatement)
|
|
{
|
|
if (context.Settings.PatternBasedFixedStatement)
|
|
{
|
|
foreach (var v in fixedStatement.Variables)
|
|
{
|
|
var m = addressOfPinnableReference.Match(v.Initializer);
|
|
if (m.Success)
|
|
{
|
|
Expression target = m.Get<Expression>("target").Single();
|
|
if (target.GetResolveResult().Type.IsReferenceType == false)
|
|
{
|
|
context.Step("Use pattern-based fixed statement", fixedStatement);
|
|
v.Initializer = target.Detach();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return base.VisitFixedStatement(fixedStatement);
|
|
}
|
|
#endregion
|
|
|
|
#region C# 8.0 Using variables
|
|
public override AstNode VisitUsingStatement(UsingStatement usingStatement)
|
|
{
|
|
usingStatement = (UsingStatement)base.VisitUsingStatement(usingStatement);
|
|
if (!context.Settings.UseEnhancedUsing)
|
|
return usingStatement;
|
|
|
|
if (usingStatement.GetNextStatement() != null || !(usingStatement.Parent is BlockStatement))
|
|
return usingStatement;
|
|
|
|
if (!(usingStatement.ResourceAcquisition is VariableDeclarationStatement))
|
|
return usingStatement;
|
|
|
|
context.Step("Use enhanced using statement", usingStatement);
|
|
usingStatement.IsEnhanced = true;
|
|
return usingStatement;
|
|
}
|
|
#endregion
|
|
}
|
|
}
|