Skip to content

Commit

Permalink
[generator] Separate metadata fixup step from parsing step. (#822)
Browse files Browse the repository at this point in the history
Context: #789

One issue with the proposal to flip the order of `ApiXmlAdjuster` and
`metadata` in Issue #789 is that the "apply metadata" step is
intertwined with the "parse API xml document into POCO's" step.
This means that the adjuster step cannot be inserted between them,
as the code is currently written.

Separate out the two steps so that they can be reordered in the future.

Additionally, refactor the metadata fixup step so that it can be unit
tested more easily by moving it to `Java.Interop.Tools.Generator.dll`.
Unit tests for applying metadata have been added.
  • Loading branch information
jpobst authored Apr 22, 2021
1 parent f9faaab commit f4e68b5
Show file tree
Hide file tree
Showing 29 changed files with 591 additions and 223 deletions.
14 changes: 14 additions & 0 deletions src/Java.Interop.Tools.Generator/Extensions/UtilityExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Xml;
using System.Xml.Linq;

namespace Java.Interop.Tools.Generator
{
Expand Down Expand Up @@ -27,5 +29,17 @@ public static bool StartsWithAny (this string value, params string [] values)
}

public static bool HasValue ([NotNullWhen (true)]this string? str) => !string.IsNullOrEmpty (str);

public static XDocument? LoadXmlDocument (string filename)
{
try {
return XDocument.Load (filename, LoadOptions.SetBaseUri | LoadOptions.SetLineInfo);
} catch (XmlException e) {
Report.Verbose (0, "Exception: {0}", e);
Report.LogCodedWarning (0, Report.WarningInvalidXmlFile, e, filename, e.Message);
}

return null;
}
}
}
26 changes: 26 additions & 0 deletions src/Java.Interop.Tools.Generator/Extensions/XmlExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;
using System.Globalization;
using System.Xml.Linq;
using System.Xml.XPath;

namespace Xamarin.Android.Tools
{
static class XmlExtensions
{
public static string? XGetAttribute (this XElement element, string name)
=> element.Attribute (name)?.Value.Trim ();

public static string? XGetAttribute (this XPathNavigator nav, string name, string ns)
=> nav.GetAttribute (name, ns)?.Trim ();

public static int? XGetAttributeAsInt (this XElement element, string name)
{
var value = element.XGetAttribute (name);

if (int.TryParse (value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
return result;

return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@
<Compile Include="..\utils\NullableAttributes.cs" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Java.Interop.Localization\Java.Interop.Localization.csproj" />
</ItemGroup>

</Project>
207 changes: 207 additions & 0 deletions src/Java.Interop.Tools.Generator/Metadata/FixupXmlDocument.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
using System;
using System.Linq;
using System.Xml.XPath;
using System.Xml.Linq;

using Xamarin.Android.Tools;

namespace Java.Interop.Tools.Generator
{
public class FixupXmlDocument
{
public XDocument FixupDocument { get; }

public FixupXmlDocument (XDocument fixupDocument)
{
FixupDocument = fixupDocument;
}

public static FixupXmlDocument? Load (string filename)
{
if (UtilityExtensions.LoadXmlDocument (filename) is XDocument doc)
return new FixupXmlDocument (doc);

return null;
}

public void Apply (ApiXmlDocument apiDocument, string apiLevelString, int productVersion)
{
// Defaulting to 0 here is fine
int.TryParse (apiLevelString, out var apiLevel);

var metadataChildren = FixupDocument.XPathSelectElements ("/metadata/*");

string? prev_path = null;
XElement? attr_last_cache = null;

foreach (var metaitem in metadataChildren) {
if (ShouldSkip (metaitem, apiLevel, productVersion))
continue;
if (!ShouldApply (metaitem, apiDocument))
continue;

var path = metaitem.XGetAttribute ("path");

if (path != prev_path)
attr_last_cache = null;

prev_path = path;

switch (metaitem.Name.LocalName) {
case "remove-node":
try {
var nodes = apiDocument.ApiDocument.XPathSelectElements (path).ToArray ();

if (nodes.Any ())
foreach (var node in nodes)
node.Remove ();
else
// BG8A00
Report.LogCodedWarning (0, Report.WarningRemoveNodeMatchedNoNodes, null, metaitem, $"<remove-node path=\"{path}\" />");
} catch (XPathException e) {
// BG4301
Report.LogCodedError (Report.ErrorRemoveNodeInvalidXPath, e, metaitem, path);
}
break;
case "add-node":
try {
var nodes = apiDocument.ApiDocument.XPathSelectElements (path);

if (!nodes.Any ())
// BG8A01
Report.LogCodedWarning (0, Report.WarningAddNodeMatchedNoNodes, null, metaitem, $"<add-node path=\"{path}\" />");
else {
foreach (var node in nodes)
node.Add (metaitem.Nodes ());
}
} catch (XPathException e) {
// BG4302
Report.LogCodedError (Report.ErrorAddNodeInvalidXPath, e, metaitem, path);
}
break;
case "change-node":
try {
var nodes = apiDocument.ApiDocument.XPathSelectElements (path);
var matched = false;

foreach (var node in nodes) {
var newChild = new XElement (metaitem.Value);
newChild.Add (node.Attributes ());
newChild.Add (node.Nodes ());
node.ReplaceWith (newChild);
matched = true;
}

if (!matched)
// BG8A03
Report.LogCodedWarning (0, Report.WarningChangeNodeTypeMatchedNoNodes, null, metaitem, $"<change-node-type path=\"{path}\" />");
} catch (XPathException e) {
// BG4303
Report.LogCodedError (Report.ErrorChangeNodeInvalidXPath, e, metaitem, path);
}
break;
case "attr":
try {
var attr_name = metaitem.XGetAttribute ("name");

if (string.IsNullOrEmpty (attr_name))
// BG4307
Report.LogCodedError (Report.ErrorMissingAttrName, null, metaitem, path);
var nodes = attr_last_cache != null ? new XElement [] { attr_last_cache } : apiDocument.ApiDocument.XPathSelectElements (path);
var attr_matched = 0;

foreach (var n in nodes) {
n.SetAttributeValue (attr_name, metaitem.Value);
attr_matched++;
}
if (attr_matched == 0)
// BG8A04
Report.LogCodedWarning (0, Report.WarningAttrMatchedNoNodes, null, metaitem, $"<attr path=\"{path}\" />");
if (attr_matched != 1)
attr_last_cache = null;
} catch (XPathException e) {
// BG4304
Report.LogCodedError (Report.ErrorAttrInvalidXPath, e, metaitem, path);
}
break;
case "move-node":
try {
var parent = metaitem.Value;
var parents = apiDocument.ApiDocument.XPathSelectElements (parent);
var matched = false;

foreach (var parent_node in parents) {
var nodes = parent_node.XPathSelectElements (path).ToArray ();
foreach (var node in nodes)
node.Remove ();
parent_node.Add (nodes);
matched = true;
}
if (!matched)
// BG8A05
Report.LogCodedWarning (0, Report.WarningMoveNodeMatchedNoNodes, null, metaitem, $"<move-node path=\"{path}\" />");
} catch (XPathException e) {
// BG4305
Report.LogCodedError (Report.ErrorMoveNodeInvalidXPath, e, metaitem, path);
}
break;
case "remove-attr":
try {
var name = metaitem.XGetAttribute ("name");
var nodes = apiDocument.ApiDocument.XPathSelectElements (path);
var matched = false;

foreach (var node in nodes) {
node.RemoveAttributes ();
matched = true;
}

if (!matched)
// BG8A06
Report.LogCodedWarning (0, Report.WarningRemoveAttrMatchedNoNodes, null, metaitem, $"<remove-attr path=\"{path}\" />");
} catch (XPathException e) {
// BG4306
Report.LogCodedError (Report.ErrorRemoveAttrInvalidXPath, e, metaitem, path);
}
break;
}
}
}

bool ShouldSkip (XElement node, int apiLevel, int productVersion)
{
if (apiLevel > 0) {
var since = node.XGetAttributeAsInt ("api-since");
var until = node.XGetAttributeAsInt ("api-until");

if (since is int since_int && since_int > apiLevel)
return true;
else if (until is int until_int && until_int < apiLevel)
return true;
}

if (productVersion > 0) {
var product_version = node.XGetAttributeAsInt ("product-version");

if (product_version is int version && version > productVersion)
return true;

}
return false;
}

bool ShouldApply (XElement node, ApiXmlDocument apiDocument)
{
if (apiDocument.ApiSource.HasValue ()) {
var targetsource = node.XGetAttribute ("api-source");

if (!targetsource.HasValue ())
return true;

return targetsource == apiDocument.ApiSource;
}

return true;
}
}
}
47 changes: 47 additions & 0 deletions src/Java.Interop.Tools.Generator/Utilities/ApiXmlDocument.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;
using System.Xml;
using System.Xml.Linq;
using Xamarin.Android.Tools;

namespace Java.Interop.Tools.Generator
{
public class ApiXmlDocument
{
public XDocument ApiDocument { get; }
public string ApiLevel { get; }
public int ProductVersion { get; }

public string? ApiSource => ApiDocument.Root?.XGetAttribute ("api-source");

public ApiXmlDocument (XDocument document, string apiLevel, int productVersion)
{
ApiDocument = document;
ApiLevel = apiLevel;
ProductVersion = productVersion;
}

public static ApiXmlDocument? Load (string filename, string apiLevel, int productVersion)
{
if (UtilityExtensions.LoadXmlDocument (filename) is XDocument doc)
return new ApiXmlDocument (doc, apiLevel, productVersion);

return null;
}

public void ApplyFixupFile (string filename)
{
if (FixupXmlDocument.Load (filename) is FixupXmlDocument fixup)
ApplyFixupFile (fixup);
}

public void ApplyFixupFile (FixupXmlDocument fixup)
{
try {
fixup.Apply (this, ApiLevel, ProductVersion);
} catch (XmlException ex) {
// BG4200
Report.LogCodedError (Report.ErrorFailedToProcessMetadata, ex.Message);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MonoDroid.Generation
namespace Java.Interop.Tools.Generator
{
public interface ISourceLineInfo
{
Expand Down
Loading

0 comments on commit f4e68b5

Please sign in to comment.