mirror of
https://github.com/icsharpcode/ILSpy.git
synced 2026-10-12 02:15:58 +00:00
Every other decision about how a member reference is spelled is made while the expression is built, behind Disambiguator, and CallBuilder already writes target.ExtensionMethod for delegate references and method groups. Extension calls were the last spelling decided afterwards, by an AST transform that had to rebuild a resolver from a UsingScope annotation on the tree root and re-derive every argument's resolve result from AST annotations. The check now runs against the resolver ExpressionBuilder already owns, whose using scope is the namespace superset the DecompileRun collected from the IL. That superset is wider than the imports actually printed, so it can only refuse infix syntax where the transform allowed it, never bind a call to a different overload. Two consequences had to be handled. The declaring type is no longer named anywhere once the call is infix, so IntroduceUsingDeclarations would not have imported its namespace and the output would not compile; it now imports from the invocation itself. And an extension GetEnumerator in infix form is spelled like an instance call, so the foreach matcher can no longer read a plain match as an instance method. Writing the receiver as the target means skipping an argument, which the argument list only supported where there were neither names nor optional arguments: it skipped before trimming the optional tail, so a skip past an omitted argument took one argument too many, and it gave up entirely on names. Both now skip after trimming, which also settles a latent off-by-one on the collection-initializer path. Assisted-by: Claude:claude-opus-5:Claude Code
453 lines
15 KiB
C#
453 lines
15 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.Collections.Immutable;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
|
|
using ICSharpCode.Decompiler.CSharp.Resolver;
|
|
using ICSharpCode.Decompiler.CSharp.Syntax;
|
|
#if STEP
|
|
using ICSharpCode.Decompiler.CSharp.Syntax.PatternMatching;
|
|
#endif
|
|
using ICSharpCode.Decompiler.IL;
|
|
using ICSharpCode.Decompiler.Semantics;
|
|
using ICSharpCode.Decompiler.TypeSystem;
|
|
using ICSharpCode.Decompiler.TypeSystem.Implementation;
|
|
|
|
namespace ICSharpCode.Decompiler.CSharp.Transforms
|
|
{
|
|
/// <summary>
|
|
/// Introduces using declarations.
|
|
/// </summary>
|
|
public class IntroduceUsingDeclarations : IAstTransform
|
|
{
|
|
public void Run(AstNode rootNode, TransformContext context)
|
|
{
|
|
// First determine all the namespaces that need to be imported:
|
|
var requiredImports = new FindRequiredImports(context);
|
|
rootNode.AcceptVisitor(requiredImports);
|
|
|
|
List<INamespace> resolvedNamespaces = new List<INamespace>();
|
|
|
|
if (context.Settings.UsingDeclarations)
|
|
{
|
|
// #define directives are leading trivia on the SyntaxTree, not children, so using
|
|
// declarations go at the very start of the member list.
|
|
AstNode? insertionPoint = null;
|
|
|
|
// Now add using declarations for those namespaces:
|
|
IOrderedEnumerable<string> sortedImports = requiredImports.ImportedNamespaces
|
|
.OrderBy(n => n.StartsWith("System", StringComparison.Ordinal))
|
|
.ThenByDescending(n => n);
|
|
foreach (string ns in sortedImports)
|
|
{
|
|
Debug.Assert(context.RequiredNamespacesSuperset.Contains(ns), $"Should not insert using declaration for namespace that is missing from the superset: {ns}");
|
|
// we go backwards (OrderByDescending) through the list of namespaces because we insert them backwards
|
|
// (always inserting at the start of the list)
|
|
string[] parts = ns.Split('.');
|
|
AstType nsType = new SimpleType(parts[0]);
|
|
for (int i = 1; i < parts.Length; i++)
|
|
{
|
|
nsType = new MemberType { Target = nsType, MemberName = parts[i] };
|
|
}
|
|
var resolvedNamespace = context.TypeSystem.GetNamespaceByFullName(ns);
|
|
if (resolvedNamespace != null)
|
|
{
|
|
resolvedNamespaces.Add(resolvedNamespace);
|
|
}
|
|
context.Step("Add using declaration", rootNode);
|
|
var node = new UsingDeclaration { Import = nsType };
|
|
rootNode.InsertChildAfter(insertionPoint, node, Slots.Member);
|
|
context.EndStep(node);
|
|
}
|
|
}
|
|
|
|
var usingScope = new UsingScope(
|
|
new CSharpTypeResolveContext(context.TypeSystem.MainModule),
|
|
context.TypeSystem.RootNamespace,
|
|
resolvedNamespaces.ToImmutableArray()
|
|
);
|
|
rootNode.AddAnnotation(usingScope);
|
|
|
|
// verify that the SimpleTypes refer to the correct type (no ambiguities)
|
|
rootNode.AcceptVisitor(new FullyQualifyAmbiguousTypeNamesVisitor(context, usingScope));
|
|
}
|
|
|
|
sealed class FindRequiredImports : DepthFirstAstVisitor
|
|
{
|
|
string currentNamespace;
|
|
|
|
public readonly HashSet<string> DeclaredNamespaces = new HashSet<string>() { string.Empty };
|
|
public readonly HashSet<string> ImportedNamespaces = new HashSet<string>();
|
|
|
|
public FindRequiredImports(TransformContext context)
|
|
{
|
|
this.currentNamespace = context.CurrentTypeDefinition?.Namespace ?? string.Empty;
|
|
}
|
|
|
|
bool IsParentOfCurrentNamespace(string ns)
|
|
{
|
|
if (ns.Length == 0)
|
|
return true;
|
|
if (currentNamespace.StartsWith(ns, StringComparison.Ordinal))
|
|
{
|
|
if (currentNamespace.Length == ns.Length)
|
|
return true;
|
|
if (currentNamespace[ns.Length] == '.')
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public override void VisitSimpleType(SimpleType simpleType)
|
|
{
|
|
var trr = simpleType.Annotation<TypeResolveResult>();
|
|
AddImportedNamespace(trr?.Type);
|
|
base.VisitSimpleType(simpleType); // also visit type arguments
|
|
}
|
|
|
|
private void AddImportedNamespace(IType? type)
|
|
{
|
|
if (type != null && !IsParentOfCurrentNamespace(type.Namespace))
|
|
{
|
|
ImportedNamespaces.Add(type.Namespace);
|
|
}
|
|
}
|
|
|
|
public override void VisitNamespaceDeclaration(NamespaceDeclaration namespaceDeclaration)
|
|
{
|
|
string oldNamespace = currentNamespace;
|
|
foreach (string ident in namespaceDeclaration.Identifiers)
|
|
{
|
|
currentNamespace = NamespaceDeclaration.BuildQualifiedName(currentNamespace, ident);
|
|
DeclaredNamespaces.Add(currentNamespace);
|
|
}
|
|
base.VisitNamespaceDeclaration(namespaceDeclaration);
|
|
currentNamespace = oldNamespace;
|
|
}
|
|
|
|
public override void VisitInvocationExpression(InvocationExpression invocationExpression)
|
|
{
|
|
// A call written in extension method syntax no longer names its declaring type,
|
|
// so nothing else in the tree asks for that namespace to be imported - and
|
|
// without the import the name does not resolve back to this method.
|
|
if (invocationExpression.GetResolveResult() is CSharpInvocationResolveResult {
|
|
IsExtensionMethodInvocation: true, Member: var extensionMethod
|
|
})
|
|
{
|
|
AddImportedNamespace(extensionMethod.DeclaringType);
|
|
}
|
|
base.VisitInvocationExpression(invocationExpression);
|
|
}
|
|
|
|
public override void VisitForeachStatement(ForeachStatement foreachStatement)
|
|
{
|
|
var annotation = foreachStatement.Annotation<ForeachAnnotation>();
|
|
if (annotation?.GetEnumeratorCall is CallInstruction { Method: { IsExtensionMethod: true, DeclaringType: var type } })
|
|
{
|
|
AddImportedNamespace(type);
|
|
}
|
|
base.VisitForeachStatement(foreachStatement);
|
|
}
|
|
|
|
public override void VisitParenthesizedVariableDesignation(ParenthesizedVariableDesignation parenthesizedVariableDesignation)
|
|
{
|
|
var annotation = parenthesizedVariableDesignation.Annotation<MatchInstruction>();
|
|
if (annotation?.Method is IMethod { DeclaringType: var type })
|
|
{
|
|
AddImportedNamespace(type);
|
|
}
|
|
base.VisitParenthesizedVariableDesignation(parenthesizedVariableDesignation);
|
|
}
|
|
|
|
public override void VisitTupleExpression(TupleExpression tupleExpression)
|
|
{
|
|
var annotation = tupleExpression.Annotation<MatchInstruction>();
|
|
if (annotation?.Method is IMethod { DeclaringType: var type })
|
|
{
|
|
AddImportedNamespace(type);
|
|
}
|
|
base.VisitTupleExpression(tupleExpression);
|
|
}
|
|
|
|
public override void VisitArrayInitializerExpression(ArrayInitializerExpression arrayInitializerExpression)
|
|
{
|
|
foreach (var item in arrayInitializerExpression.Elements)
|
|
{
|
|
var optionalCall = item.Annotation<CallInstruction>();
|
|
if (optionalCall?.Method is { IsExtensionMethod: true, Name: "Add" })
|
|
{
|
|
AddImportedNamespace(optionalCall.Method.DeclaringType);
|
|
}
|
|
}
|
|
base.VisitArrayInitializerExpression(arrayInitializerExpression);
|
|
}
|
|
}
|
|
|
|
sealed class FullyQualifyAmbiguousTypeNamesVisitor : DepthFirstAstVisitor
|
|
{
|
|
readonly bool ignoreUsingScope;
|
|
readonly DecompilerSettings settings;
|
|
readonly TransformContext context;
|
|
|
|
CSharpResolver resolver;
|
|
TypeSystemAstBuilder astBuilder;
|
|
|
|
bool inPrimaryConstructor;
|
|
|
|
public FullyQualifyAmbiguousTypeNamesVisitor(TransformContext context, UsingScope usingScope)
|
|
{
|
|
this.context = context;
|
|
this.ignoreUsingScope = !context.Settings.UsingDeclarations;
|
|
this.settings = context.Settings;
|
|
this.resolver = new CSharpResolver(new CSharpTypeResolveContext(context.TypeSystem.MainModule));
|
|
|
|
if (!ignoreUsingScope)
|
|
{
|
|
if (!string.IsNullOrEmpty(context.CurrentTypeDefinition?.Namespace))
|
|
{
|
|
foreach (string ns in context.CurrentTypeDefinition.Namespace.Split('.'))
|
|
{
|
|
usingScope = usingScope.WithNestedNamespace(ns);
|
|
}
|
|
}
|
|
this.resolver = this.resolver.WithCurrentUsingScope(usingScope)
|
|
.WithCurrentTypeDefinition(context.CurrentTypeDefinition);
|
|
}
|
|
this.astBuilder = CreateAstBuilder(resolver);
|
|
}
|
|
|
|
TypeSystemAstBuilder CreateAstBuilder(CSharpResolver resolver, IL.ILFunction? function = null)
|
|
{
|
|
if (function != null)
|
|
{
|
|
var variables = new Dictionary<string, IVariable>();
|
|
foreach (var v in function.Variables)
|
|
{
|
|
if (v.Kind != IL.VariableKind.Parameter && v.Name != null && !variables.ContainsKey(v.Name))
|
|
variables.Add(v.Name, new DefaultVariable(v.Type, v.Name));
|
|
}
|
|
resolver = resolver.AddVariables(variables);
|
|
}
|
|
|
|
return new TypeSystemAstBuilder(resolver) {
|
|
UseNullableSpecifierForValueTypes = settings.LiftNullables,
|
|
AlwaysUseGlobal = settings.AlwaysUseGlobal,
|
|
AddResolveResultAnnotations = true,
|
|
UseAliases = true
|
|
};
|
|
}
|
|
|
|
public override void VisitNamespaceDeclaration(NamespaceDeclaration namespaceDeclaration)
|
|
{
|
|
if (ignoreUsingScope)
|
|
{
|
|
base.VisitNamespaceDeclaration(namespaceDeclaration);
|
|
return;
|
|
}
|
|
var previousResolver = resolver;
|
|
var previousAstBuilder = astBuilder;
|
|
var usingScope = resolver.CurrentUsingScope;
|
|
foreach (string ident in namespaceDeclaration.Identifiers)
|
|
{
|
|
usingScope = usingScope.WithNestedNamespace(ident);
|
|
}
|
|
resolver = resolver.WithCurrentUsingScope(usingScope);
|
|
try
|
|
{
|
|
astBuilder = CreateAstBuilder(resolver);
|
|
base.VisitNamespaceDeclaration(namespaceDeclaration);
|
|
}
|
|
finally
|
|
{
|
|
astBuilder = previousAstBuilder;
|
|
resolver = previousResolver;
|
|
}
|
|
}
|
|
|
|
public override void VisitTypeDeclaration(TypeDeclaration typeDeclaration)
|
|
{
|
|
if (ignoreUsingScope)
|
|
{
|
|
base.VisitTypeDeclaration(typeDeclaration);
|
|
return;
|
|
}
|
|
|
|
if (typeDeclaration.HasPrimaryConstructor)
|
|
{
|
|
inPrimaryConstructor = true;
|
|
|
|
try
|
|
{
|
|
typeDeclaration.PrimaryConstructorParameters.AcceptVisitor(this);
|
|
}
|
|
finally
|
|
{
|
|
inPrimaryConstructor = false;
|
|
}
|
|
}
|
|
|
|
var previousResolver = resolver;
|
|
var previousAstBuilder = astBuilder;
|
|
|
|
resolver = resolver.WithCurrentTypeDefinition(typeDeclaration.GetSymbol() as ITypeDefinition);
|
|
|
|
try
|
|
{
|
|
astBuilder = CreateAstBuilder(resolver);
|
|
base.VisitTypeDeclaration(typeDeclaration);
|
|
}
|
|
finally
|
|
{
|
|
astBuilder = previousAstBuilder;
|
|
resolver = previousResolver;
|
|
}
|
|
}
|
|
|
|
public override void VisitParameterDeclaration(ParameterDeclaration parameterDeclaration)
|
|
{
|
|
// Parameters of primary constructors are visited separately from the rest of the
|
|
// type declaration since their types are at the same scope as the type declaration
|
|
// and so need to use the outer resolver. This check ensures that the visitor only
|
|
// runs once per parameter since their AstNodes will get revisited by the call to
|
|
// `base.VisitTypeDeclaration(typeDeclaration)` in `VisitTypeDeclaration` above.
|
|
if (inPrimaryConstructor || parameterDeclaration.Parent is not TypeDeclaration)
|
|
base.VisitParameterDeclaration(parameterDeclaration);
|
|
}
|
|
|
|
public override void VisitMethodDeclaration(MethodDeclaration methodDeclaration)
|
|
{
|
|
Visit(methodDeclaration, base.VisitMethodDeclaration);
|
|
}
|
|
|
|
public override void VisitAccessor(Accessor accessor)
|
|
{
|
|
Visit(accessor, base.VisitAccessor);
|
|
}
|
|
|
|
public override void VisitConstructorDeclaration(ConstructorDeclaration constructorDeclaration)
|
|
{
|
|
Visit(constructorDeclaration, base.VisitConstructorDeclaration);
|
|
}
|
|
|
|
public override void VisitDestructorDeclaration(DestructorDeclaration destructorDeclaration)
|
|
{
|
|
Visit(destructorDeclaration, base.VisitDestructorDeclaration);
|
|
}
|
|
|
|
public override void VisitOperatorDeclaration(OperatorDeclaration operatorDeclaration)
|
|
{
|
|
Visit(operatorDeclaration, base.VisitOperatorDeclaration);
|
|
}
|
|
|
|
void Visit<T>(T entityDeclaration, Action<T> baseCall) where T : EntityDeclaration
|
|
{
|
|
if (ignoreUsingScope)
|
|
{
|
|
baseCall(entityDeclaration);
|
|
return;
|
|
}
|
|
if (entityDeclaration.GetSymbol() is IMethod method)
|
|
{
|
|
var previousResolver = resolver;
|
|
var previousAstBuilder = astBuilder;
|
|
if (CSharpDecompiler.IsWindowsFormsInitializeComponentMethod(method))
|
|
{
|
|
var currentContext = new CSharpTypeResolveContext(previousResolver.Compilation.MainModule);
|
|
resolver = new CSharpResolver(currentContext);
|
|
}
|
|
else
|
|
{
|
|
resolver = resolver.WithCurrentMember(method);
|
|
}
|
|
try
|
|
{
|
|
var function = entityDeclaration.Annotation<IL.ILFunction>();
|
|
astBuilder = CreateAstBuilder(resolver, function);
|
|
baseCall(entityDeclaration);
|
|
}
|
|
finally
|
|
{
|
|
resolver = previousResolver;
|
|
astBuilder = previousAstBuilder;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
baseCall(entityDeclaration);
|
|
}
|
|
}
|
|
|
|
public override void VisitSimpleType(SimpleType simpleType)
|
|
{
|
|
TypeResolveResult? rr;
|
|
if ((rr = simpleType.Annotation<TypeResolveResult>()) == null)
|
|
{
|
|
base.VisitSimpleType(simpleType);
|
|
return;
|
|
}
|
|
astBuilder.NameLookupMode = simpleType.GetNameLookupMode();
|
|
if (astBuilder.NameLookupMode == NameLookupMode.Type)
|
|
{
|
|
AstType outermostType = simpleType;
|
|
while (outermostType.Parent is AstType parent)
|
|
outermostType = parent;
|
|
if (outermostType.Parent is TypeReferenceExpression)
|
|
{
|
|
// ILSpy uses TypeReferenceExpression in expression context even when the C# parser
|
|
// wouldn't know that it's a type reference.
|
|
// Fall back to expression-mode lookup in these cases:
|
|
astBuilder.NameLookupMode = NameLookupMode.Expression;
|
|
}
|
|
}
|
|
if (simpleType.Parent is Syntax.Attribute)
|
|
{
|
|
ReplaceAndRecordStep("Qualify ambiguous attribute type", simpleType, astBuilder.ConvertAttributeType(rr.Type));
|
|
}
|
|
else
|
|
{
|
|
ReplaceAndRecordStep("Qualify ambiguous type", simpleType, astBuilder.ConvertType(rr.Type));
|
|
}
|
|
}
|
|
|
|
void ReplaceAndRecordStep(string stepDescription, SimpleType simpleType, AstType replacement)
|
|
{
|
|
#if STEP
|
|
// Record a debug step only when the converted type actually differs from the
|
|
// original, so the step list stays meaningful. This structural comparison is
|
|
// debug-only (it allocates a pattern Match); release builds skip it entirely and
|
|
// just perform the replacement below.
|
|
bool changed = !simpleType.IsMatch(replacement);
|
|
if (changed)
|
|
context.Step(stepDescription, simpleType);
|
|
#endif
|
|
simpleType.ReplaceWith(replacement);
|
|
#if STEP
|
|
if (changed)
|
|
context.EndStep(replacement);
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
}
|