Browse Source

Support optional children in roles and generated DoMatch

Preparation for making optional single-value slots nullable: a role may omit
its null object and a slot may be cleared via SetChildByRole(role, null), and
the generated DoMatch emits MatchOptional for a nullable single child so an
absent child matches correctly. Both are inert until a slot is actually made
nullable.
Assisted-by: Claude:claude-opus-4-8:Claude Code
pull/3807/head
Siegfried Pammer 3 weeks ago committed by Siegfried Pammer
parent
commit
92f54a94a8
  1. 20
      ICSharpCode.Decompiler.Generators/DecompilerSyntaxTreeGenerator.cs
  2. 12
      ICSharpCode.Decompiler/CSharp/Syntax/AstNode.cs
  3. 6
      ICSharpCode.Decompiler/CSharp/Syntax/Role.cs

20
ICSharpCode.Decompiler.Generators/DecompilerSyntaxTreeGenerator.cs

@ -30,7 +30,7 @@ namespace ICSharpCode.Decompiler.Generators;
[Generator] [Generator]
internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator
{ {
record AstNodeAdditions(string NodeName, bool NeedsAcceptImpls, bool NeedsVisitor, bool NeedsNullNode, bool NeedsPatternPlaceholder, int NullNodeBaseCtorParamCount, bool IsTypeNode, string VisitMethodName, string VisitMethodParamType, EquatableArray<(string Member, string TypeName, bool RecursiveMatch, bool MatchAny)>? MembersToMatch, EquatableArray<(string RoleExpr, bool IsCollection, string PropertyName, string PropertyType, string ElementType, bool IsOverride, bool IsNullable, string KindName)>? Slots); record AstNodeAdditions(string NodeName, bool NeedsAcceptImpls, bool NeedsVisitor, bool NeedsNullNode, bool NeedsPatternPlaceholder, int NullNodeBaseCtorParamCount, bool IsTypeNode, string VisitMethodName, string VisitMethodParamType, EquatableArray<(string Member, string TypeName, bool RecursiveMatch, bool MatchAny, bool Nullable)>? MembersToMatch, EquatableArray<(string RoleExpr, bool IsCollection, string PropertyName, string PropertyType, string ElementType, bool IsOverride, bool IsNullable, string KindName)>? Slots);
// Derives the shared SlotKind name from a [Slot] role expression: the last dotted segment with a // Derives the shared SlotKind name from a [Slot] role expression: the last dotted segment with a
// trailing "Role" removed (e.g. "Roles.EmbeddedStatement" -> "EmbeddedStatement", "LeftRole" -> // trailing "Role" removed (e.g. "Roles.EmbeddedStatement" -> "EmbeddedStatement", "LeftRole" ->
@ -56,7 +56,7 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator
_ => (targetSymbol.Name, targetSymbol.Name), _ => (targetSymbol.Name, targetSymbol.Name),
}; };
List<(string Member, string TypeName, bool RecursiveMatch, bool MatchAny)>? membersToMatch = null; List<(string Member, string TypeName, bool RecursiveMatch, bool MatchAny, bool Nullable)>? membersToMatch = null;
if (!targetSymbol.MemberNames.Contains("DoMatch")) if (!targetSymbol.MemberNames.Contains("DoMatch"))
{ {
@ -65,7 +65,7 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator
var astNodeType = (INamedTypeSymbol)context.SemanticModel.GetSpeculativeSymbolInfo(context.TargetNode.Span.Start, SyntaxFactory.ParseTypeName("AstNode"), SpeculativeBindingOption.BindAsTypeOrNamespace).Symbol!; var astNodeType = (INamedTypeSymbol)context.SemanticModel.GetSpeculativeSymbolInfo(context.TargetNode.Span.Start, SyntaxFactory.ParseTypeName("AstNode"), SpeculativeBindingOption.BindAsTypeOrNamespace).Symbol!;
if (targetSymbol.BaseType!.MemberNames.Contains("MatchAttributesAndModifiers")) if (targetSymbol.BaseType!.MemberNames.Contains("MatchAttributesAndModifiers"))
membersToMatch.Add(("MatchAttributesAndModifiers", null!, false, false)); membersToMatch.Add(("MatchAttributesAndModifiers", null!, false, false, false));
foreach (var m in targetSymbol.GetMembers()) foreach (var m in targetSymbol.GetMembers())
{ {
@ -75,16 +75,17 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator
continue; continue;
if (property.Type.MetadataName is "CSharpTokenNode" or "TextLocation") if (property.Type.MetadataName is "CSharpTokenNode" or "TextLocation")
continue; continue;
bool nullable = property.NullableAnnotation == NullableAnnotation.Annotated;
switch (property.Type) switch (property.Type)
{ {
case INamedTypeSymbol named when named.IsDerivedFrom(astNodeType) || named.MetadataName == "AstNodeCollection`1": case INamedTypeSymbol named when named.IsDerivedFrom(astNodeType) || named.MetadataName == "AstNodeCollection`1":
membersToMatch.Add((property.Name, named.Name, true, false)); membersToMatch.Add((property.Name, named.Name, true, false, nullable));
break; break;
case INamedTypeSymbol { TypeKind: TypeKind.Enum } named when named.GetMembers().Any(_ => _.Name == "Any"): case INamedTypeSymbol { TypeKind: TypeKind.Enum } named when named.GetMembers().Any(_ => _.Name == "Any"):
membersToMatch.Add((property.Name, named.Name, false, true)); membersToMatch.Add((property.Name, named.Name, false, true, false));
break; break;
default: default:
membersToMatch.Add((property.Name, property.Type.Name, false, false)); membersToMatch.Add((property.Name, property.Type.Name, false, false, false));
break; break;
} }
} }
@ -278,12 +279,17 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator
{{ {{
return other is {source.NodeName} o && !o.IsNull"); return other is {source.NodeName} o && !o.IsNull");
foreach (var (member, typeName, recursive, hasAny) in source.MembersToMatch) foreach (var (member, typeName, recursive, hasAny, nullable) in source.MembersToMatch)
{ {
if (member == "MatchAttributesAndModifiers") if (member == "MatchAttributesAndModifiers")
{ {
builder.Append($"\r\n\t\t\t&& this.MatchAttributesAndModifiers(o, match)"); builder.Append($"\r\n\t\t\t&& this.MatchAttributesAndModifiers(o, match)");
} }
else if (recursive && nullable && typeName != "AstNodeCollection")
{
// An optional single-value child is null when absent; match null-safely.
builder.Append($"\r\n\t\t\t&& MatchOptional(this.{member}, o.{member}, match)");
}
else if (recursive) else if (recursive)
{ {
builder.Append($"\r\n\t\t\t&& this.{member}.DoMatch(o.{member}, match)"); builder.Append($"\r\n\t\t\t&& this.{member}.DoMatch(o.{member}, match)");

12
ICSharpCode.Decompiler/CSharp/Syntax/AstNode.cs

@ -383,7 +383,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
return collection != null ? (AstNodeCollection<T>)collection : new AstNodeCollection<T>(this, role); return collection != null ? (AstNodeCollection<T>)collection : new AstNodeCollection<T>(this, role);
} }
protected void SetChildByRole<T>(Role<T> role, T newChild) where T : AstNode protected void SetChildByRole<T>(Role<T> role, T? newChild) where T : AstNode
{ {
SetChildByRoleUntyped(role, newChild); SetChildByRoleUntyped(role, newChild);
} }
@ -724,6 +724,16 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
return PatternMatching.Pattern.MatchString(pattern, text); return PatternMatching.Pattern.MatchString(pattern, text);
} }
/// <summary>
/// Matches two optional children: both absent (null), or both present and matching.
/// When the pattern side is present, its own DoMatch decides (e.g. an OptionalNode matches an
/// absent candidate). When it is absent, the candidate must be absent too.
/// </summary>
protected static bool MatchOptional(AstNode? thisChild, AstNode? otherChild, PatternMatching.Match match)
{
return thisChild != null ? thisChild.DoMatch(otherChild, match) : otherChild == null;
}
protected internal abstract bool DoMatch(AstNode? other, PatternMatching.Match match); protected internal abstract bool DoMatch(AstNode? other, PatternMatching.Match match);
bool PatternMatching.INode.DoMatch(PatternMatching.INode? other, PatternMatching.Match match) bool PatternMatching.INode.DoMatch(PatternMatching.INode? other, PatternMatching.Match match)

6
ICSharpCode.Decompiler/CSharp/Syntax/Role.cs

@ -96,10 +96,12 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
return node is T; return node is T;
} }
public Role(string name, T nullObject) public Role(string name, T? nullObject)
{ {
this.name = name ?? throw new ArgumentNullException(nameof(name)); this.name = name ?? throw new ArgumentNullException(nameof(name));
this.nullObject = nullObject; // Roles used only for collections may omit the null object (pass null); NullObject then
// returns null, matching the documented contract. Single-value roles always pass one.
this.nullObject = nullObject!;
} }
public override string ToString() public override string ToString()

Loading…
Cancel
Save