创建 Roslyn C# 分析器,它可以识别程序集中 class 的构造函数参数类型
Create Roslyn C# analyzer that is aware of constructor argument types for class in assembly
背景:
我有一个属性指示对象 IsMagic
中的 属性 字段。我还有一个 Magician
class 运行 覆盖任何对象,MakesMagic
通过提取每个字段和 属性 IsMagic
并将其包装在Magic
包装器。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace MagicTest
{
/// <summary>
/// An attribute that allows us to decorate a class with information that identifies which member is magic.
/// </summary>
[AttributeUsage(AttributeTargets.Property|AttributeTargets.Field, AllowMultiple = false)]
class IsMagic : Attribute { }
public class Magic
{
// Internal data storage
readonly public dynamic value;
#region My ever-growing list of constructors
public Magic(int input) { value = input; }
public Magic(string input) { value = input; }
public Magic(IEnumerable<bool> input) { value = input; }
// ...
#endregion
public bool CanMakeMagicFromType(Type targetType)
{
if (targetType == null) return false;
ConstructorInfo publicConstructor = typeof(Magic).GetConstructor(new[] { targetType });
if (publicConstructor != null) return true; // We can make Magic from this input type!!!
return false;
}
public override string ToString()
{
return value.ToString();
}
}
public static class Magician
{
/// <summary>
/// A method that returns the members of anObject that have been marked with an IsMagic attribute.
/// Each member will be wrapped in Magic.
/// </summary>
/// <param name="anObject"></param>
/// <returns></returns>
public static List<Magic> MakeMagic(object anObject)
{
Type type = anObject?.GetType() ?? null;
if (type == null) return null; // Sanity check
List<Magic> returnList = new List<Magic>();
// Any field or property of the class that IsMagic gets added to the returnList in a Magic wrapper
MemberInfo[] objectMembers = type.GetMembers();
foreach (MemberInfo mi in objectMembers)
{
bool isMagic = (mi.GetCustomAttributes<IsMagic>().Count() > 0);
if (isMagic)
{
dynamic memberValue = null;
if (mi.MemberType == MemberTypes.Property) memberValue = ((PropertyInfo)mi).GetValue(anObject);
else if (mi.MemberType == MemberTypes.Field) memberValue = ((FieldInfo)mi).GetValue(anObject);
if (memberValue == null) continue;
returnList.Add(new Magic(memberValue)); // This could fail at run-time!!!
}
}
return returnList;
}
}
}
魔术师可以 MakeMagic
在 anObject
上使用至少一个字段或 属性 IsMagic
来生成 [=17= 的通用 List
],像这样:
using System;
using System.Collections.Generic;
namespace MagicTest
{
class Program
{
class Mundane
{
[IsMagic] public string foo;
[IsMagic] public int feep;
public float zorp; // If this [IsMagic], we'll have a run-time error
}
static void Main(string[] args)
{
Mundane anObject = new Mundane
{
foo = "this is foo",
feep = -10,
zorp = 1.3f
};
Console.WriteLine("Magic:");
List<Magic> myMagics = Magician.MakeMagic(anObject);
foreach (Magic aMagic in myMagics) Console.WriteLine(" {0}",aMagic.ToString());
Console.WriteLine("More Magic: {0}", new Magic("this works!"));
//Console.WriteLine("More Magic: {0}", new Magic(Mundane)); // build-time error!
Console.WriteLine("\nPress Enter to continue");
Console.ReadLine();
}
}
}
请注意,Magic
包装器只能绕过某些类型的属性或字段。这意味着只有 属性 或包含特定类型数据的字段应标记为 IsMagic
。使事情变得更复杂的是,我希望特定类型的列表会随着业务需求的发展而改变(因为对 Magic 编程的需求如此之高)。
好消息是 Magic
具有一定的构建时安全性。如果我尝试添加像 new Magic(true)
这样的代码,Visual Studio 会告诉我这是错误的,因为 Magic
没有接受 bool
的构造函数。还有一些 运行 时间检查,因为 Magic.CanMakeMagicFromType
方法可用于捕获动态变量的问题。
问题描述:
坏消息是 IsMagic
属性没有构建时检查。我可以很高兴地在某些 class IsMagic
中说出一个 Dictionary<string,bool>
字段,直到 运行 时间我才被告知这是一个问题。更糟糕的是,我的神奇代码的用户将创建他们自己的普通 classes 并使用 IsMagic
属性装饰他们的属性和字段。我想帮助他们在问题成为问题之前看到问题。
建议的解决方案:
理想情况下,我可以在我的 IsMagic
属性上放置某种 AttributeUsage 标志,以告诉 Visual Studio 使用 Magic.CanMakeMagicFromType()
方法检查 属性 或字段类型IsMagic
属性被附加到。不幸的是,似乎没有这样的属性。
但是,当 IsMagic
被放置在字段上或 属性 具有 Type
时,似乎应该可以使用 Roslyn 来显示错误包裹在 Magic
.
我需要帮助的地方:
我在设计 Roslyn 分析器时遇到了问题。问题的核心是 Magic.CanMakeMagicFromType
接受 System.Type
,但 Roslyn 使用 ITypeSymbol
来表示对象类型。
理想的分析仪应该是:
- 不需要我保留可以包含在
Magic
中的允许类型的列表。毕竟,Magic
有一个用于此目的的构造函数列表。
- 允许类型的自然转换。例如,如果
Magic
有一个接受 IEnumerable<bool>
的构造函数,那么 Roslyn 应该允许 IsMagic
附加到 属性 类型 List<bool>
或 bool[]
。这种魔法的施放对魔术师的功能至关重要。
对于如何编写 Magic
中构造函数的 "aware" Roslyn 分析器的任何指导,我将不胜感激。
您需要使用 Roslyn 的语义模型 API 和 ITypeSymbol
.
重写 CanMakeMagicFromType()
首先调用 Compilation.GetTypeByMetadataName()
to get the INamedTypeSymbol
for Magic
. You can then enumerate its constructors & parameters and call .ClassifyConversion
以查看它们是否与 属性 类型兼容。
根据 SLaks 的出色建议,我编写了一个完整的解决方案。
发现错误应用属性的代码分析器如下所示:
using System;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
namespace AttributeAnalyzer
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class AttributeAnalyzerAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "AttributeAnalyzer";
private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(
id: DiagnosticId,
title: "Magic cannot be constructed from Type",
messageFormat: "Magic cannot be built from Type '{0}'.",
category: "Design",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "The IsMagic attribue needs to be attached to Types that can be rendered as Magic."
);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }
public override void Initialize(AnalysisContext context)
{
context.RegisterSyntaxNodeAction(
AnalyzeSyntax,
SyntaxKind.PropertyDeclaration, SyntaxKind.FieldDeclaration
);
}
private static void AnalyzeSyntax(SyntaxNodeAnalysisContext context)
{
ITypeSymbol memberTypeSymbol = null;
if (context.ContainingSymbol is IPropertySymbol)
{
memberTypeSymbol = (context.ContainingSymbol as IPropertySymbol)?.GetMethod?.ReturnType;
}
else if (context.ContainingSymbol is IFieldSymbol)
{
memberTypeSymbol = (context.ContainingSymbol as IFieldSymbol)?.Type;
}
else throw new InvalidOperationException("Can only analyze property and field declarations.");
// Check if this property of field is decorated with the IsMagic attribute
INamedTypeSymbol isMagicAttribute = context.SemanticModel.Compilation.GetTypeByMetadataName("MagicTest.IsMagic");
ISymbol thisSymbol = context.ContainingSymbol;
ImmutableArray<AttributeData> attributes = thisSymbol.GetAttributes();
bool hasMagic = false;
Location attributeLocation = null;
foreach (AttributeData attribute in attributes)
{
if (attribute.AttributeClass != isMagicAttribute) continue;
hasMagic = true;
attributeLocation = attribute.ApplicationSyntaxReference.SyntaxTree.GetLocation(attribute.ApplicationSyntaxReference.Span);
break;
}
if (!hasMagic) return;
// Check if we can make Magic using the current property or field type
if (!CanMakeMagic(context,memberTypeSymbol))
{
var diagnostic = Diagnostic.Create(Rule, attributeLocation, memberTypeSymbol.Name);
context.ReportDiagnostic(diagnostic);
}
}
/// <summary>
/// Check if a given type can be wrapped in Magic in the current context.
/// </summary>
/// <param name="context"></param>
/// <param name="sourceTypeSymbol"></param>
/// <returns></returns>
private static bool CanMakeMagic(SyntaxNodeAnalysisContext context, ITypeSymbol sourceTypeSymbol)
{
INamedTypeSymbol magic = context.SemanticModel.Compilation.GetTypeByMetadataName("MagicTest.Magic");
ImmutableArray<IMethodSymbol> constructors = magic.Constructors;
foreach (IMethodSymbol methodSymbol in constructors)
{
ImmutableArray<IParameterSymbol> parameters = methodSymbol.Parameters;
IParameterSymbol param = parameters[0]; // All Magic constructors take one parameter
ITypeSymbol paramType = param.Type;
Conversion conversion = context.Compilation.ClassifyConversion(sourceTypeSymbol, paramType);
if (conversion.Exists && conversion.IsImplicit) return true; // We've found at least one way to make Magic
}
return false;
}
}
}
CanMakeMagic 函数有 SLaks 为我拼出的神奇解决方案。
代码修复提供程序如下所示:
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace AttributeAnalyzer
{
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AttributeAnalyzerCodeFixProvider)), Shared]
public class AttributeAnalyzerCodeFixProvider : CodeFixProvider
{
public sealed override ImmutableArray<string> FixableDiagnosticIds
{
get { return ImmutableArray.Create(AttributeAnalyzerAnalyzer.DiagnosticId); }
}
public sealed override FixAllProvider GetFixAllProvider()
{
// See https://github.com/dotnet/roslyn/blob/master/docs/analyzers/FixAllProvider.md for more information on Fix All Providers
return WellKnownFixAllProviders.BatchFixer;
}
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
Diagnostic diagnostic = context.Diagnostics.First();
TextSpan diagnosticSpan = diagnostic.Location.SourceSpan;
context.RegisterCodeFix(
CodeAction.Create(
title: "Remove attribute",
createChangedDocument: c => RemoveAttributeAsync(context.Document, diagnosticSpan, context.CancellationToken),
equivalenceKey: "Remove_Attribute"
),
diagnostic
);
}
private async Task<Document> RemoveAttributeAsync(Document document, TextSpan diagnosticSpan, CancellationToken cancellation)
{
SyntaxNode root = await document.GetSyntaxRootAsync(cancellation).ConfigureAwait(false);
AttributeListSyntax attributeListDeclaration = root.FindNode(diagnosticSpan).FirstAncestorOrSelf<AttributeListSyntax>();
SeparatedSyntaxList<AttributeSyntax> attributes = attributeListDeclaration.Attributes;
if (attributes.Count > 1)
{
AttributeSyntax targetAttribute = root.FindNode(diagnosticSpan).FirstAncestorOrSelf<AttributeSyntax>();
return document.WithSyntaxRoot(
root.RemoveNode(targetAttribute,
SyntaxRemoveOptions.KeepExteriorTrivia | SyntaxRemoveOptions.KeepEndOfLine | SyntaxRemoveOptions.KeepDirectives)
);
}
if (attributes.Count==1)
{
return document.WithSyntaxRoot(
root.RemoveNode(attributeListDeclaration,
SyntaxRemoveOptions.KeepExteriorTrivia | SyntaxRemoveOptions.KeepEndOfLine | SyntaxRemoveOptions.KeepDirectives)
);
}
return document;
}
}
}
这里唯一需要的技巧是有时删除单个属性,有时删除整个属性列表。
我将此标记为已接受的答案;但是,为了全面披露,如果没有 SLaks 的帮助,我永远不会想出这个。
背景:
我有一个属性指示对象 IsMagic
中的 属性 字段。我还有一个 Magician
class 运行 覆盖任何对象,MakesMagic
通过提取每个字段和 属性 IsMagic
并将其包装在Magic
包装器。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace MagicTest
{
/// <summary>
/// An attribute that allows us to decorate a class with information that identifies which member is magic.
/// </summary>
[AttributeUsage(AttributeTargets.Property|AttributeTargets.Field, AllowMultiple = false)]
class IsMagic : Attribute { }
public class Magic
{
// Internal data storage
readonly public dynamic value;
#region My ever-growing list of constructors
public Magic(int input) { value = input; }
public Magic(string input) { value = input; }
public Magic(IEnumerable<bool> input) { value = input; }
// ...
#endregion
public bool CanMakeMagicFromType(Type targetType)
{
if (targetType == null) return false;
ConstructorInfo publicConstructor = typeof(Magic).GetConstructor(new[] { targetType });
if (publicConstructor != null) return true; // We can make Magic from this input type!!!
return false;
}
public override string ToString()
{
return value.ToString();
}
}
public static class Magician
{
/// <summary>
/// A method that returns the members of anObject that have been marked with an IsMagic attribute.
/// Each member will be wrapped in Magic.
/// </summary>
/// <param name="anObject"></param>
/// <returns></returns>
public static List<Magic> MakeMagic(object anObject)
{
Type type = anObject?.GetType() ?? null;
if (type == null) return null; // Sanity check
List<Magic> returnList = new List<Magic>();
// Any field or property of the class that IsMagic gets added to the returnList in a Magic wrapper
MemberInfo[] objectMembers = type.GetMembers();
foreach (MemberInfo mi in objectMembers)
{
bool isMagic = (mi.GetCustomAttributes<IsMagic>().Count() > 0);
if (isMagic)
{
dynamic memberValue = null;
if (mi.MemberType == MemberTypes.Property) memberValue = ((PropertyInfo)mi).GetValue(anObject);
else if (mi.MemberType == MemberTypes.Field) memberValue = ((FieldInfo)mi).GetValue(anObject);
if (memberValue == null) continue;
returnList.Add(new Magic(memberValue)); // This could fail at run-time!!!
}
}
return returnList;
}
}
}
魔术师可以 MakeMagic
在 anObject
上使用至少一个字段或 属性 IsMagic
来生成 [=17= 的通用 List
],像这样:
using System;
using System.Collections.Generic;
namespace MagicTest
{
class Program
{
class Mundane
{
[IsMagic] public string foo;
[IsMagic] public int feep;
public float zorp; // If this [IsMagic], we'll have a run-time error
}
static void Main(string[] args)
{
Mundane anObject = new Mundane
{
foo = "this is foo",
feep = -10,
zorp = 1.3f
};
Console.WriteLine("Magic:");
List<Magic> myMagics = Magician.MakeMagic(anObject);
foreach (Magic aMagic in myMagics) Console.WriteLine(" {0}",aMagic.ToString());
Console.WriteLine("More Magic: {0}", new Magic("this works!"));
//Console.WriteLine("More Magic: {0}", new Magic(Mundane)); // build-time error!
Console.WriteLine("\nPress Enter to continue");
Console.ReadLine();
}
}
}
请注意,Magic
包装器只能绕过某些类型的属性或字段。这意味着只有 属性 或包含特定类型数据的字段应标记为 IsMagic
。使事情变得更复杂的是,我希望特定类型的列表会随着业务需求的发展而改变(因为对 Magic 编程的需求如此之高)。
好消息是 Magic
具有一定的构建时安全性。如果我尝试添加像 new Magic(true)
这样的代码,Visual Studio 会告诉我这是错误的,因为 Magic
没有接受 bool
的构造函数。还有一些 运行 时间检查,因为 Magic.CanMakeMagicFromType
方法可用于捕获动态变量的问题。
问题描述:
坏消息是 IsMagic
属性没有构建时检查。我可以很高兴地在某些 class IsMagic
中说出一个 Dictionary<string,bool>
字段,直到 运行 时间我才被告知这是一个问题。更糟糕的是,我的神奇代码的用户将创建他们自己的普通 classes 并使用 IsMagic
属性装饰他们的属性和字段。我想帮助他们在问题成为问题之前看到问题。
建议的解决方案:
理想情况下,我可以在我的 IsMagic
属性上放置某种 AttributeUsage 标志,以告诉 Visual Studio 使用 Magic.CanMakeMagicFromType()
方法检查 属性 或字段类型IsMagic
属性被附加到。不幸的是,似乎没有这样的属性。
但是,当 IsMagic
被放置在字段上或 属性 具有 Type
时,似乎应该可以使用 Roslyn 来显示错误包裹在 Magic
.
我需要帮助的地方:
我在设计 Roslyn 分析器时遇到了问题。问题的核心是 Magic.CanMakeMagicFromType
接受 System.Type
,但 Roslyn 使用 ITypeSymbol
来表示对象类型。
理想的分析仪应该是:
- 不需要我保留可以包含在
Magic
中的允许类型的列表。毕竟,Magic
有一个用于此目的的构造函数列表。 - 允许类型的自然转换。例如,如果
Magic
有一个接受IEnumerable<bool>
的构造函数,那么 Roslyn 应该允许IsMagic
附加到 属性 类型List<bool>
或bool[]
。这种魔法的施放对魔术师的功能至关重要。
对于如何编写 Magic
中构造函数的 "aware" Roslyn 分析器的任何指导,我将不胜感激。
您需要使用 Roslyn 的语义模型 API 和 ITypeSymbol
.
CanMakeMagicFromType()
首先调用 Compilation.GetTypeByMetadataName()
to get the INamedTypeSymbol
for Magic
. You can then enumerate its constructors & parameters and call .ClassifyConversion
以查看它们是否与 属性 类型兼容。
根据 SLaks 的出色建议,我编写了一个完整的解决方案。
发现错误应用属性的代码分析器如下所示:
using System;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
namespace AttributeAnalyzer
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class AttributeAnalyzerAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "AttributeAnalyzer";
private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(
id: DiagnosticId,
title: "Magic cannot be constructed from Type",
messageFormat: "Magic cannot be built from Type '{0}'.",
category: "Design",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "The IsMagic attribue needs to be attached to Types that can be rendered as Magic."
);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }
public override void Initialize(AnalysisContext context)
{
context.RegisterSyntaxNodeAction(
AnalyzeSyntax,
SyntaxKind.PropertyDeclaration, SyntaxKind.FieldDeclaration
);
}
private static void AnalyzeSyntax(SyntaxNodeAnalysisContext context)
{
ITypeSymbol memberTypeSymbol = null;
if (context.ContainingSymbol is IPropertySymbol)
{
memberTypeSymbol = (context.ContainingSymbol as IPropertySymbol)?.GetMethod?.ReturnType;
}
else if (context.ContainingSymbol is IFieldSymbol)
{
memberTypeSymbol = (context.ContainingSymbol as IFieldSymbol)?.Type;
}
else throw new InvalidOperationException("Can only analyze property and field declarations.");
// Check if this property of field is decorated with the IsMagic attribute
INamedTypeSymbol isMagicAttribute = context.SemanticModel.Compilation.GetTypeByMetadataName("MagicTest.IsMagic");
ISymbol thisSymbol = context.ContainingSymbol;
ImmutableArray<AttributeData> attributes = thisSymbol.GetAttributes();
bool hasMagic = false;
Location attributeLocation = null;
foreach (AttributeData attribute in attributes)
{
if (attribute.AttributeClass != isMagicAttribute) continue;
hasMagic = true;
attributeLocation = attribute.ApplicationSyntaxReference.SyntaxTree.GetLocation(attribute.ApplicationSyntaxReference.Span);
break;
}
if (!hasMagic) return;
// Check if we can make Magic using the current property or field type
if (!CanMakeMagic(context,memberTypeSymbol))
{
var diagnostic = Diagnostic.Create(Rule, attributeLocation, memberTypeSymbol.Name);
context.ReportDiagnostic(diagnostic);
}
}
/// <summary>
/// Check if a given type can be wrapped in Magic in the current context.
/// </summary>
/// <param name="context"></param>
/// <param name="sourceTypeSymbol"></param>
/// <returns></returns>
private static bool CanMakeMagic(SyntaxNodeAnalysisContext context, ITypeSymbol sourceTypeSymbol)
{
INamedTypeSymbol magic = context.SemanticModel.Compilation.GetTypeByMetadataName("MagicTest.Magic");
ImmutableArray<IMethodSymbol> constructors = magic.Constructors;
foreach (IMethodSymbol methodSymbol in constructors)
{
ImmutableArray<IParameterSymbol> parameters = methodSymbol.Parameters;
IParameterSymbol param = parameters[0]; // All Magic constructors take one parameter
ITypeSymbol paramType = param.Type;
Conversion conversion = context.Compilation.ClassifyConversion(sourceTypeSymbol, paramType);
if (conversion.Exists && conversion.IsImplicit) return true; // We've found at least one way to make Magic
}
return false;
}
}
}
CanMakeMagic 函数有 SLaks 为我拼出的神奇解决方案。
代码修复提供程序如下所示:
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace AttributeAnalyzer
{
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AttributeAnalyzerCodeFixProvider)), Shared]
public class AttributeAnalyzerCodeFixProvider : CodeFixProvider
{
public sealed override ImmutableArray<string> FixableDiagnosticIds
{
get { return ImmutableArray.Create(AttributeAnalyzerAnalyzer.DiagnosticId); }
}
public sealed override FixAllProvider GetFixAllProvider()
{
// See https://github.com/dotnet/roslyn/blob/master/docs/analyzers/FixAllProvider.md for more information on Fix All Providers
return WellKnownFixAllProviders.BatchFixer;
}
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
Diagnostic diagnostic = context.Diagnostics.First();
TextSpan diagnosticSpan = diagnostic.Location.SourceSpan;
context.RegisterCodeFix(
CodeAction.Create(
title: "Remove attribute",
createChangedDocument: c => RemoveAttributeAsync(context.Document, diagnosticSpan, context.CancellationToken),
equivalenceKey: "Remove_Attribute"
),
diagnostic
);
}
private async Task<Document> RemoveAttributeAsync(Document document, TextSpan diagnosticSpan, CancellationToken cancellation)
{
SyntaxNode root = await document.GetSyntaxRootAsync(cancellation).ConfigureAwait(false);
AttributeListSyntax attributeListDeclaration = root.FindNode(diagnosticSpan).FirstAncestorOrSelf<AttributeListSyntax>();
SeparatedSyntaxList<AttributeSyntax> attributes = attributeListDeclaration.Attributes;
if (attributes.Count > 1)
{
AttributeSyntax targetAttribute = root.FindNode(diagnosticSpan).FirstAncestorOrSelf<AttributeSyntax>();
return document.WithSyntaxRoot(
root.RemoveNode(targetAttribute,
SyntaxRemoveOptions.KeepExteriorTrivia | SyntaxRemoveOptions.KeepEndOfLine | SyntaxRemoveOptions.KeepDirectives)
);
}
if (attributes.Count==1)
{
return document.WithSyntaxRoot(
root.RemoveNode(attributeListDeclaration,
SyntaxRemoveOptions.KeepExteriorTrivia | SyntaxRemoveOptions.KeepEndOfLine | SyntaxRemoveOptions.KeepDirectives)
);
}
return document;
}
}
}
这里唯一需要的技巧是有时删除单个属性,有时删除整个属性列表。
我将此标记为已接受的答案;但是,为了全面披露,如果没有 SLaks 的帮助,我永远不会想出这个。