From c325f3698fe61412472e3c6d4107722c9bc6b63c Mon Sep 17 00:00:00 2001 From: Atsushi Eno Date: Wed, 11 Oct 2017 10:42:50 +0900 Subject: [PATCH] [class-parse] add support for external parameter name descriptor files. The text file format is described at https://github.com/atsushieno/xamarin-android-docimporter-ng/blob/bee36c1181ac0778b63350987aba20a7c3d4410c/Xamarin.Android.Tools.JavaStubImporter/JavaApiParameterNamesXmlExporter.cs#L78 too. --- .../ClassPath.cs | 14 ++ .../JavaParameterNamesLoader.cs | 195 ++++++++++++++++++ .../Tests/ClassFileFixture.cs | 7 +- .../Tests/ParameterDescription.txt | 3 + .../Tests/ParameterFixupTests.cs | 16 ++ ...amarin.Android.Tools.Bytecode-Tests.csproj | 17 +- .../Xamarin.Android.Tools.Bytecode.csproj | 1 + tools/class-parse/Program.cs | 5 + 8 files changed, 247 insertions(+), 11 deletions(-) create mode 100644 src/Xamarin.Android.Tools.Bytecode/JavaParameterNamesLoader.cs create mode 100644 src/Xamarin.Android.Tools.Bytecode/Tests/ParameterDescription.txt diff --git a/src/Xamarin.Android.Tools.Bytecode/ClassPath.cs b/src/Xamarin.Android.Tools.Bytecode/ClassPath.cs index 8b7303cd6..e69c14ffb 100644 --- a/src/Xamarin.Android.Tools.Bytecode/ClassPath.cs +++ b/src/Xamarin.Android.Tools.Bytecode/ClassPath.cs @@ -30,6 +30,8 @@ public class ClassPath { public string AndroidFrameworkPlatform { get; set; } + public IEnumerable ParametersDescriptionFiles { get; set; } + public bool AutoRename { get; set; } public ClassPath (string path = null) @@ -231,6 +233,17 @@ void FixupParametersFromDocs (XElement api) } } + void FixupParametersFromParametersDescription (XElement api) + { + if (ParametersDescriptionFiles == null) + return; + foreach (var path in ParametersDescriptionFiles) { + if (!File.Exists (path)) + continue; + new JavaParameterNamesLoader ().ApplyParameterNameChanges (api, path); + } + } + IAndroidDocScraper CreateDocScraper (string src) { switch (AndroidDocScraper.GetDocletType (src)) { @@ -297,6 +310,7 @@ public XElement ToXElement () packagesDictionary [p].OrderBy (c => c.ThisClass.Name.Value, StringComparer.OrdinalIgnoreCase) .Select (c => new XmlClassDeclarationBuilder (c).ToXElement ())))); FixupParametersFromDocs (api); + FixupParametersFromParametersDescription (api); return api; } diff --git a/src/Xamarin.Android.Tools.Bytecode/JavaParameterNamesLoader.cs b/src/Xamarin.Android.Tools.Bytecode/JavaParameterNamesLoader.cs new file mode 100644 index 000000000..3c4bce5ab --- /dev/null +++ b/src/Xamarin.Android.Tools.Bytecode/JavaParameterNamesLoader.cs @@ -0,0 +1,195 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml.Linq; +using System.Xml.XPath; + +namespace Xamarin.Android.Tools.Bytecode +{ + public class JavaParameterNamesLoader + { + void FillSyntheticMethodsFixup (List fixup, XElement type, HashSet alreadyChecked, Dictionary fullNameMap) + { + if (alreadyChecked.Contains (type)) + return; + alreadyChecked.Add (type); + + var fpkg = fixup.FirstOrDefault (p => p.Name == type.Parent.Attribute ("name").Value); + if (fpkg == null) + return; + var ftype = fpkg.Types.FirstOrDefault (t => t.Name == type.Attribute ("name").Value); + if (ftype == null) + return; + + var extends = type.Attribute ("extends")?.Value; + var bt = extends != null ? fullNameMap [extends] : null; + if (bt == null || bt.Attribute ("visibility").Value != "") + return; + var fbpkg = fixup.FirstOrDefault (p => p.Name == bt.Parent.Attribute ("name").Value); + if (fbpkg == null) + return; + var fbtype = fbpkg.Types.FirstOrDefault (t => t.Name == bt.Attribute ("name").Value); + if (fbtype == null) + return; + + // FIXME: it is hacky; it should remove the conflicting synthetic methods. + ftype.Methods = fbtype.Methods.Concat (ftype.Methods).ToList (); + } + + public void ApplyParameterNameChanges (XElement api, string path) + { + var fixup = LoadParameterFixupDescription (path); + + // We have to supply "dummy" fixups for "synthetic" methods that might come + // from non-public ancestor classes. + // Unfortunately ancestor types are unknown in the fixup description, so + // they have to be extracted from XML API metadata. So, do it here. + var hashset = new HashSet (); + var fullNameMap = api.Elements ("package").SelectMany (p => p.Elements ()).ToDictionary (e => e.Parent.Attribute ("name").Value + "." + e.Attribute ("name").Value); + foreach (var t in api.Elements ("package").SelectMany (p => p.Elements ())) + FillSyntheticMethodsFixup (fixup, t, hashset, fullNameMap); + + var methods = api.XPathSelectElements ("package/*/*"); + foreach (var method in methods) { + switch (method.Name.LocalName) { + case "method": + case "constructor": + + string package = method.Parent.Parent.Attribute ("name").Value; + string type = method.Parent.Attribute ("name").Value; + string mname = method.Attribute ("name").Value; + var parameters = method.Elements ("parameter").ToArray (); + if (!parameters.Any ()) // we don't care about parameterless methods. + continue; + var matchedPackage = fixup.FirstOrDefault (p => p.Name == package); + if (matchedPackage == null) { + Log.Warning (0, "Package {0} not found.", package); + continue; + } + var matchedType = matchedPackage.Types.FirstOrDefault (t => t.Name == type); + if (matchedType == null) { + Log.Warning (0, "Type {0} not found.", package + '.' + type); + continue; + } + var matchedMethods = matchedType.Methods.Where (m => (m.Name == "#ctor" ? method.Name.LocalName == "constructor" : m.Name == mname) && m.Parameters.Count == parameters.Length); + if (!matchedMethods.Any ()) { + Log.Warning (0, "Method {0} with {1} parameters not found.", package + '.' + type + '.' + mname, parameters.Length); + continue; + } + var matched = matchedMethods.FirstOrDefault (m => m.Parameters.Zip (parameters, (f, x) => f.Type == x.Attribute ("type").Value.Replace (", ", ",")).All (b => b)); + + if (matched == null) { + Log.Warning (0, "Method {0}({1}) not found.", + package + '.' + type + '.' + mname, + string.Join (",", parameters.Select (para => para.Attribute ("type").Value))); + continue; + } + + for (int i = 0; i < parameters.Length; i++) + parameters [i].SetAttributeValue ("name", matched.Parameters [i].Name); + + matched.Applied = true; + break; + } + } + foreach (var p in fixup) + foreach (var t in p.Types) + foreach (var m in t.Methods.Where (m => !m.Applied)) + Log.Warning (0, "Method parameter description for {0}.{1}.{2}({3}) is never applied", + p.Name, t.Name, m.Name, string.Join (",", m.Parameters.Select (para => para.Type))); + } + + class Parameter + { + public string Type { get; set; } + public string Name { get; set; } + } + + class Method + { + public string Name { get; set; } + public List Parameters { get; set; } + public bool Applied { get; set; } + } + + class Type + { + public string Name { get; set; } + public List Methods { get; set; } + } + + class Package + { + public string Name { get; set; } + public List Types { get; set; } + } + + // from https://github.com/atsushieno/xamarin-android-docimporter-ng/blob/master/Xamarin.Android.Tools.JavaStubImporter/JavaApiParameterNamesXmlExporter.cs#L78 + /* + * The Text Format is: + * + * package {packagename} + * #--------------------------------------- + * interface {interfacename}{optional_type_parameters} -or- + * class {classname}{optional_type_parameters} + * {optional_type_parameters}{methodname}({parameters}) + * + * Anything after # is treated as comment. + * + * optional_type_parameters: "" -or- "" (no constraints allowed) + * parameters: type1 p0, type2 p1 (pairs of {type} {name}, joined by ", ") + * + * It is with strict indentations. two spaces for types, four spaces for methods. + * + * Constructors are named as "#ctor". + * + * Commas are used by both parameter types and parameter separators, + * but only parameter separators can be followed by a whitespace. + * It is useful when writing text parsers for this format. + * + * Type names may contain whitespaces in case it is with generic constraints (e.g. "? extends FooBar"), + * so when parsing a parameter type-name pair, the only trustworthy whitespace for tokenizing name is the *last* one. + * + */ + List LoadParameterFixupDescription (string path) + { + var fixup = new List (); + string package = null; + var types = new List (); + string type = null; + var methods = new List (); + foreach (var l in File.ReadAllLines (path)) { + var line = l.IndexOf ('#') >= 0 ? l.Substring (0, l.IndexOf ('#')) : l; + if (line.Trim ().Length == 0) + continue; + if (line.StartsWith ("package ", StringComparison.Ordinal)) { + package = line.Substring ("package ".Length); + types = new List (); + fixup.Add (new Package { Name = package, Types = types }); + continue; + } else if (line.StartsWith (" ", StringComparison.Ordinal)) { + int open = line.IndexOf ('('); + string parameters = line.Substring (open + 1).TrimEnd (')'); + string name = line.Substring (4, open - 4); + if (name.FirstOrDefault () == '<') // generic method can begin with type parameters. + name = name.Substring (name.IndexOf (' ') + 1); + methods.Add (new Method { + Name = name, + Parameters = parameters.Replace (", ", "\0").Split ('\0') + .Select (s => s.Split (' ')) + .Select (a => new Parameter { Type = string.Join (" ", a.Take (a.Length - 1)), Name = a.Last () }).ToList () + }); + } else { + type = line.Substring (line.IndexOf (' ', 2) + 1); + // To match type name from class-parse, we need to strip off generic arguments here (generics are erased). + if (type.IndexOf ('<') > 0) + type = type.Substring (0, type.IndexOf ('<')); + methods = new List (); + types.Add (new Type { Name = type, Methods = methods }); + } + } + return fixup; + } + } +} diff --git a/src/Xamarin.Android.Tools.Bytecode/Tests/ClassFileFixture.cs b/src/Xamarin.Android.Tools.Bytecode/Tests/ClassFileFixture.cs index 2eac73bc6..2b112409d 100644 --- a/src/Xamarin.Android.Tools.Bytecode/Tests/ClassFileFixture.cs +++ b/src/Xamarin.Android.Tools.Bytecode/Tests/ClassFileFixture.cs @@ -55,13 +55,16 @@ protected static void AssertXmlDeclaration (string classResource, string xmlReso Assert.AreEqual (expected, actual.ToString ()); } - protected static void AssertXmlDeclaration (string[] classResources, string xmlResource, string documentationPath = null) + protected static void AssertXmlDeclaration (string[] classResources, string xmlResource, string documentationPath = null, string parameterDescriptionFile = null) { var classPathBuilder = new ClassPath () { ApiSource = "class-parse", - DocumentationPaths = new string[] { + DocumentationPaths = documentationPath == null ? null : new string[] { documentationPath, }, + ParametersDescriptionFiles = parameterDescriptionFile == null ? null : new string [] { + parameterDescriptionFile, + }, AutoRename = true }; foreach(var classFile in classResources.Select(s => LoadClassFile (s))) diff --git a/src/Xamarin.Android.Tools.Bytecode/Tests/ParameterDescription.txt b/src/Xamarin.Android.Tools.Bytecode/Tests/ParameterDescription.txt new file mode 100644 index 000000000..6737ff3f7 --- /dev/null +++ b/src/Xamarin.Android.Tools.Bytecode/Tests/ParameterDescription.txt @@ -0,0 +1,3 @@ +package java.util + class Collection + add(E e) diff --git a/src/Xamarin.Android.Tools.Bytecode/Tests/ParameterFixupTests.cs b/src/Xamarin.Android.Tools.Bytecode/Tests/ParameterFixupTests.cs index d8ff131ad..3c655f49b 100644 --- a/src/Xamarin.Android.Tools.Bytecode/Tests/ParameterFixupTests.cs +++ b/src/Xamarin.Android.Tools.Bytecode/Tests/ParameterFixupTests.cs @@ -87,6 +87,22 @@ public void DocletType_ShouldDetectDroidDocs () Assert.AreEqual(JavaDocletType.DroidDoc2, AndroidDocScraper.GetDocletType(droidDocsPath)); } + + [Test] + public void XmlDeclaration_FixedUpFromParameterDescription () + { + var androidSdkPath = Environment.GetEnvironmentVariable ("ANDROID_SDK_PATH"); + if (string.IsNullOrEmpty (androidSdkPath)) { + Assert.Ignore ("The `ANDROID_SDK_PATH` environment variable isn't set; " + + "cannot test importing parameter names from HTML. Skipping..."); + return; + } + try { + AssertXmlDeclaration (new string [] {"Collection.class"}, "ParameterFixupFromDocs.xml", null, "ParameterDescription.txt"); + } catch (Exception ex) { + Assert.Fail ("An unexpected exception was thrown : {0}", ex); + } + } } } diff --git a/src/Xamarin.Android.Tools.Bytecode/Tests/Xamarin.Android.Tools.Bytecode-Tests.csproj b/src/Xamarin.Android.Tools.Bytecode/Tests/Xamarin.Android.Tools.Bytecode-Tests.csproj index ffbbc7b0d..19ec22f3e 100644 --- a/src/Xamarin.Android.Tools.Bytecode/Tests/Xamarin.Android.Tools.Bytecode-Tests.csproj +++ b/src/Xamarin.Android.Tools.Bytecode/Tests/Xamarin.Android.Tools.Bytecode-Tests.csproj @@ -57,13 +57,8 @@ - - + + @@ -74,8 +69,8 @@ - - + + @@ -165,5 +160,9 @@ + + ParameterDescription.txt + PreserveNewest + diff --git a/src/Xamarin.Android.Tools.Bytecode/Xamarin.Android.Tools.Bytecode.csproj b/src/Xamarin.Android.Tools.Bytecode/Xamarin.Android.Tools.Bytecode.csproj index 9a0d7e3c5..c2379706f 100644 --- a/src/Xamarin.Android.Tools.Bytecode/Xamarin.Android.Tools.Bytecode.csproj +++ b/src/Xamarin.Android.Tools.Bytecode/Xamarin.Android.Tools.Bytecode.csproj @@ -49,6 +49,7 @@ + diff --git a/tools/class-parse/Program.cs b/tools/class-parse/Program.cs index d3123f77e..f83e83a0e 100644 --- a/tools/class-parse/Program.cs +++ b/tools/class-parse/Program.cs @@ -23,6 +23,7 @@ public static void Main (string[] args) var outputFile = (string) null; string platform = null; var docsPaths = new List (); + var paramDescs = new List (); var p = new OptionSet () { "usage: class-dump [-dump] FILES", "", @@ -38,6 +39,9 @@ public static void Main (string[] args) { "docspath=", "Documentation {PATH} for parameter fixup", doc => docsPaths.Add (doc) }, + { "parameters-description=", + "Parameter description file {PATH}", + desc => paramDescs.Add (desc) }, { "docstype=", "OBSOLETE: Previously used to specify a doc type (now auto detected).", t => docsType = t != null }, @@ -71,6 +75,7 @@ public static void Main (string[] args) ApiSource = "class-parse", AndroidFrameworkPlatform = platform, DocumentationPaths = docsPaths.Count == 0 ? null : docsPaths, + ParametersDescriptionFiles = paramDescs.Count == 0 ? null : paramDescs, AutoRename = autorename }; foreach (var file in files) {