Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[mono-api-html] Improve attribute handling. #22273

Merged
merged 2 commits into from
Mar 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions tools/api-tools/mono-api-html/ApiChange.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,23 @@ public ApiChange Append (string text)
return this;
}

public ApiChange AppendAdded (string text, bool breaking = false)
public ApiChange AppendAdded (string text, bool breaking)
{
State.Formatter.DiffAddition (Member, text, breaking);
Breaking |= breaking;
AnyChange = true;
return this;
}

public ApiChange AppendRemoved (string text, bool breaking = true)
public ApiChange AppendRemoved (string text, bool breaking)
{
State.Formatter.DiffRemoval (Member, text, breaking);
Breaking |= breaking;
AnyChange = true;
return this;
}

public ApiChange AppendModified (string old, string @new, bool breaking = true)
public ApiChange AppendModified (string old, string @new, bool breaking)
{
State.Formatter.DiffModification (Member, old, @new, breaking);
Breaking |= breaking;
Expand Down
8 changes: 8 additions & 0 deletions tools/api-tools/mono-api-html/AssemblyComparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ public AssemblyComparer (XDocument sourceFile, XDocument targetFile, State state
comparer = new NamespaceComparer (state);
}

public override string GroupName {
get { return "assemblies"; }
}

public override string ElementName {
get { return "assembly"; }
}

public string SourceAssembly { get; private set; }
public string TargetAssembly { get; private set; }

Expand Down
37 changes: 21 additions & 16 deletions tools/api-tools/mono-api-html/ClassComparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using Microsoft.VisualBasic;

namespace Mono.ApiTools {

Expand All @@ -53,6 +56,14 @@ public ClassComparer (State state)
mcomparer = new MethodComparer (state);
}

public override string GroupName {
get { return "classes"; }
}

public override string ElementName {
get { return "class"; }
}

public override void SetContext (XElement current)
{
State.Type = current.GetAttribute ("name");
Expand Down Expand Up @@ -88,18 +99,7 @@ public void AddedInner (XElement target)

var type = target.Attribute ("type").Value;

if (type == "enum") {
// check if [Flags] is present
var cattrs = target.Element ("attributes");
if (cattrs is not null) {
foreach (var ca in cattrs.Elements ("attribute")) {
if (ca.GetAttribute ("name") == "System.FlagsAttribute") {
Indent ().WriteLine ("[Flags]");
break;
}
}
}
}
WriteAttributes (target);

Indent ().Write ("public");

Expand Down Expand Up @@ -276,6 +276,14 @@ public override void Modified (XElement source, XElement target, ApiChanges diff
// hack - there could be changes that we're not monitoring (e.g. attributes properties)
Formatter.PushOutput ();

var attributeDiff = new ApiChange ("", State);
RenderAttributes (source, target, attributeDiff);
if (attributeDiff.AnyChange) {
Formatter.BeginAttributeModification ();
Formatter.Diff (attributeDiff);
Formatter.EndAttributeModification ();
}

var sb = source.GetAttribute ("base");
var tb = target.GetAttribute ("base");
var rm = $"{State.Namespace}.{State.Type}: Modified base type: '{sb}' to '{tb}'";
Expand Down Expand Up @@ -319,17 +327,14 @@ public override void Modified (XElement source, XElement target, ApiChanges diff

public override void Removed (XElement source)
{
if (source.Elements ("attributes").SelectMany (a => a.Elements ("attribute")).Any (c => c.Attribute ("name")?.Value == "System.ObsoleteAttribute"))
return;

string name = State.Namespace + "." + State.Type;

var memberDescription = $"{name}: Removed type";
State.LogDebugMessage ($"Possible -r value: {memberDescription}");
if (State.IgnoreRemoved.Any (re => re.IsMatch (name)))
return;

Formatter.BeginTypeRemoval ();
Formatter.BeginTypeRemoval (!source.IsExperimental ());
Formatter.EndTypeRemoval ();
}

Expand Down
93 changes: 93 additions & 0 deletions tools/api-tools/mono-api-html/Comparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Linq;

namespace Mono.ApiTools {
Expand All @@ -37,13 +38,105 @@ abstract class Comparer {
protected List<XElement> removed;
protected ApiChanges modified;

public abstract string GroupName { get; }
public abstract string ElementName { get; }

public Comparer (State state)
{
State = state;
removed = new List<XElement> ();
modified = new ApiChanges (state);
}

protected void WriteAttributes (XElement element)
{
foreach (var attribute in element.EnumerateAttributes ())
Indent ().WriteLine (RenderAttribute (attribute));
}

protected string RenderAttribute (XElement attribute)
{
var sb = new StringBuilder ();
sb.Append ("[");
sb.Append (attribute.GetAttribute ("name"));
sb.Append ("(");
var args = attribute.Element ("arguments");
if (args is not null) {
var arguments = args.Elements ("argument").ToArray ();
foreach (var arg in arguments) {
var value = arg.GetAttribute ("value");
var isString = arg.GetAttribute ("type") == "System.String";
if (isString)
sb.Append ('"');
sb.Append (value);
if (isString)
sb.Append ('"');
if (arg != arguments.Last ())
sb.Append (", ");
}
}
var props = attribute.Element ("properties");
if (props is not null) {
var properties = props.Elements ("property").ToArray ();
foreach (var prop in properties) {
sb.Append (prop.GetAttribute ("name"));
sb.Append (" = ");
sb.Append (prop.GetAttribute ("value"));
if (prop != properties.Last ())
sb.Append (", ");
}
}
sb.Append (")]");
return sb.ToString ();
}

protected void RenderAttributes (XElement source, XElement target, ApiChange diff)
{
var srcAttributes = source.EnumerateAttributes ().Select (RenderAttribute).OrderBy (v => v).ToArray ();
var tgtAttributes = target.EnumerateAttributes ().Select (RenderAttribute).OrderBy (v => v).ToArray ();
if (srcAttributes.SequenceEqual (tgtAttributes))
return;

var added = tgtAttributes.Except (srcAttributes).ToList ();
var removed = srcAttributes.Except (tgtAttributes).ToList ();
var modified = new List<(string Source, string Target)> ();

for (var i = added.Count - 1; i >= 0; i--) {
var addedType = added [i].Substring (0, added [i].IndexOf ('('));
var removedOfSameTypeIndex = removed.FindIndex ((v) => v.StartsWith (addedType));
if (removedOfSameTypeIndex == -1)
continue;

modified.Add ((removed [removedOfSameTypeIndex], added [i]));
added.RemoveAt (i);
removed.RemoveAt (removedOfSameTypeIndex);
}

if (added.Any ()) {
foreach (var a in added) {
var breaking = a.StartsWith ("[System.Diagnostics.CodeAnalysis.ExperimentalAttribute");
diff.AppendAdded (a + "\n", breaking);
}
}
if (modified.Any ()) {
foreach (var a in modified) {
diff.AppendModified (a.Source + "\n", a.Target + "\n", false);
}
}
if (removed.Any ()) {
foreach (var a in removed) {
diff.AppendRemoved (a + "\n", false);
}
}
}

protected virtual bool IsBreakingRemoval (XElement e)
{
if (e.IsExperimental ())
return false;
return true;
}

public State State { get; }

public Formatter Output {
Expand Down
1 change: 1 addition & 0 deletions tools/api-tools/mono-api-html/ConstructorComparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ public override bool Equals (XElement source, XElement target, ApiChanges change

var change = new ApiChange (GetDescription (source), State);
change.Header = "Modified " + GroupName;
RenderAttributes (source, target, change);
RenderMethodAttributes (source, target, change);
RenderReturnType (source, target, change);
RenderName (source, target, change);
Expand Down
1 change: 1 addition & 0 deletions tools/api-tools/mono-api-html/EventComparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public override bool Equals (XElement source, XElement target, ApiChanges change

var change = new ApiChange (GetDescription (source), State);
change.Header = "Modified " + GroupName;
RenderAttributes (source, target, change);
change.Append ("public event ");

var srcEventType = source.GetTypeName ("eventtype", State);
Expand Down
8 changes: 5 additions & 3 deletions tools/api-tools/mono-api-html/FieldComparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ void RenderFieldAttributes (FieldAttributes source, FieldAttributes target, ApiC
if (srcNotSerialized != tgtNotSerialized) {
// this is not a breaking change, so only render it if it changed.
if (srcNotSerialized) {
change.AppendRemoved ($"[NonSerialized]{Environment.NewLine}");
change.AppendRemoved ($"[NonSerialized]{Environment.NewLine}", false);
} else {
change.AppendAdded ($"[NonSerialized]{Environment.NewLine}");
change.AppendAdded ($"[NonSerialized]{Environment.NewLine}", false);
}
}

Expand Down Expand Up @@ -134,6 +134,8 @@ public override bool Equals (XElement source, XElement target, ApiChanges change
var change = new ApiChange (GetDescription (source), State);
change.Header = "Modified " + GroupName;

RenderAttributes (source, target, change);

if (State.BaseType == "System.Enum") {
change.Append (name).Append (" = ");
if (srcValue != tgtValue) {
Expand Down Expand Up @@ -170,7 +172,7 @@ public override bool Equals (XElement source, XElement target, ApiChanges change

// Hardcode that changes to ObjCRuntime.Constants.[Sdk]Version aren't breaking.
var fullname = GetFullName (source);
var breaking = true;
var breaking = !source.IsExperimental ();
switch (fullname) {
case "ObjCRuntime.Constants.Version":
case "ObjCRuntime.Constants.SdkVersion":
Expand Down
9 changes: 7 additions & 2 deletions tools/api-tools/mono-api-html/Formatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,23 +150,28 @@ public virtual void EndTypeModification ()
{
}

public abstract void BeginTypeRemoval ();
public abstract void BeginTypeRemoval (bool breaking);
public virtual void EndTypeRemoval ()
{
}

public abstract void BeginAttributeModification ();
public abstract void AddAttributeModification (string source, string target, bool breaking);
public abstract void EndAttributeModification ();

public abstract void BeginMemberAddition (IEnumerable<XElement> list, MemberComparer member);
public abstract void AddMember (MemberComparer member, bool isInterfaceBreakingChange, string obsolete, string description);
public abstract void EndMemberAddition ();

public abstract void BeginMemberModification (string sectionName);
public abstract void EndMemberModification ();

public abstract void BeginMemberRemoval (IEnumerable<XElement> list, MemberComparer member);
public abstract void BeginMemberRemoval (IEnumerable<XElement> list, MemberComparer member, bool breaking);
public abstract void RemoveMember (MemberComparer member, bool breaking, string obsolete, string description);
public abstract void EndMemberRemoval ();

public abstract void RenderObsoleteMessage (TextChunk chunk, MemberComparer member, string description, string optionalObsoleteMessage);
public abstract void RenderAttribute (TextChunk chunk, Comparer member, string attributeName, bool breaking, string description, params string [] attributeArguments);

public abstract void DiffAddition (TextChunk chunk, string text, bool breaking);
public abstract void DiffModification (TextChunk chunk, string old, string @new, bool breaking);
Expand Down
67 changes: 53 additions & 14 deletions tools/api-tools/mono-api-html/Helpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,25 +47,64 @@ public static string GetAttribute (this XElement self, string name)
return n.Value;
}

// null == no obsolete, String.Empty == no description
public static string GetObsoleteMessage (this XElement self)
// if this member/type or potentially any of its parents is marked as experimental
public static bool IsExperimental (this XElement self, bool recursive, out string diagnosticId)
{
var cattrs = self.Element ("attributes");
if (cattrs is null)
return null;
return TryGetAttributeProperty (self, "System.Diagnostics.CodeAnalysis.ExperimentalAttribute", recursive, out diagnosticId);
}

// if this member/type or any of its parents is marked as experimental
public static bool IsExperimental (this XElement self)
{
return IsExperimental (self, true, out var _);
}

foreach (var ca in cattrs.Elements ("attribute")) {
if (ca.GetAttribute ("name") != "System.ObsoleteAttribute")
public static IEnumerable<XElement> EnumerateAttributes (this XElement self, string attributeName = null)
{
if (self is null)
yield break;

var attribs = self.Element ("attributes");
if (attribs is null)
yield break;

foreach (var attrib in attribs.Elements ("attribute")) {
if (!string.IsNullOrEmpty (attributeName) && attrib.GetAttribute ("name") != attributeName)
continue;
var props = ca.Element ("properties");
if (props is null)
return String.Empty; // no description
foreach (var p in props.Elements ("property")) {
if (p.GetAttribute ("name") != "Message")
continue;
return p.GetAttribute ("value");
yield return attrib;
}
}

static bool TryGetAttributeProperty (this XElement self, string attributeName, bool recursive, out string firstArgument)
{
firstArgument = null;

if (self is null)
return false;

foreach (var ca in self.EnumerateAttributes (attributeName)) {
var args = ca.Element ("arguments");
if (args is not null) {
var firstCtorArgument = args.Elements ("argument")?.FirstOrDefault ();
if (firstCtorArgument is not null && firstCtorArgument.GetAttribute ("type") == "System.String") {
firstArgument = firstCtorArgument.GetAttribute ("value");
}
}

return true;
}

if (recursive)
return TryGetAttributeProperty (self.Parent, attributeName, recursive, out firstArgument);

return false;
}

// null == no obsolete, String.Empty == no description
public static string GetObsoleteMessage (this XElement self)
{
if (TryGetAttributeProperty (self, "System.ObsoleteAttribute", false, out string message))
return message ?? String.Empty;
return null;
}

Expand Down
Loading
Loading