Skip to content
Closed
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
14 changes: 14 additions & 0 deletions src/Xamarin.Android.Tools.Bytecode/ClassPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public class ClassPath {

public string AndroidFrameworkPlatform { get; set; }

public IEnumerable<string> ParametersDescriptionFiles { get; set; }

public bool AutoRename { get; set; }

public ClassPath (string path = null)
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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;
}

Expand Down
195 changes: 195 additions & 0 deletions src/Xamarin.Android.Tools.Bytecode/JavaParameterNamesLoader.cs
Original file line number Diff line number Diff line change
@@ -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<Package> fixup, XElement type, HashSet<XElement> alreadyChecked, Dictionary<string,XElement> 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<XElement> ();
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<Parameter> Parameters { get; set; }
public bool Applied { get; set; }
}

class Type
{
public string Name { get; set; }
public List<Method> Methods { get; set; }
}

class Package
{
public string Name { get; set; }
public List<Type> 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- "<A,B,C>" (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<Package> LoadParameterFixupDescription (string path)
{
var fixup = new List<Package> ();
string package = null;
var types = new List<Type> ();
string type = null;
var methods = new List<Method> ();
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<Type> ();
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<Method> ();
types.Add (new Type { Name = type, Methods = methods });
}
}
return fixup;
}
}
}
7 changes: 5 additions & 2 deletions src/Xamarin.Android.Tools.Bytecode/Tests/ClassFileFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package java.util
class Collection<E>
add(E e)
16 changes: 16 additions & 0 deletions src/Xamarin.Android.Tools.Bytecode/Tests/ParameterFixupTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,8 @@
<Compile Include="ParameterFixupTests.cs" />
</ItemGroup>
<ItemGroup>
<TestJar
Include="java\**\*.java"
Exclude="java\java\util\Collection.java"
/>
<TestJarNoParameters
Include="java\java\util\Collection.java"
/>
<TestJar Include="java\**\*.java" Exclude="java\java\util\Collection.java" />
<TestJarNoParameters Include="java\java\util\Collection.java" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
Expand All @@ -74,8 +69,8 @@
</PropertyGroup>
<Target Name="BuildClasses" Inputs="@(TestJar)" Outputs="@(TestJar-&gt;'$(IntermediateOutputPath)classes\%(RecursiveDir)%(Filename).class')">
<MakeDir Directories="$(IntermediateOutputPath)classes" />
<Exec Command="&quot;$(JavaCPath)&quot; -parameters -source 1.5 -target 1.6 -g -d &quot;$(IntermediateOutputPath)classes&quot; @(TestJar->'%(Identity)', ' ')" />
<Exec Command="&quot;$(JavaCPath)&quot; -source 1.5 -target 1.6 -g -d &quot;$(IntermediateOutputPath)classes&quot; @(TestJarNoParameters->'%(Identity)', ' ')" />
<Exec Command="&quot;$(JavaCPath)&quot; -parameters -source 1.5 -target 1.6 -g -d &quot;$(IntermediateOutputPath)classes&quot; @(TestJar-&gt;'%(Identity)', ' ')" />
<Exec Command="&quot;$(JavaCPath)&quot; -source 1.5 -target 1.6 -g -d &quot;$(IntermediateOutputPath)classes&quot; @(TestJarNoParameters-&gt;'%(Identity)', ' ')" />
</Target>
<ItemGroup>
<ProjectReference Include="..\Xamarin.Android.Tools.Bytecode.csproj">
Expand Down Expand Up @@ -165,5 +160,9 @@
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
<None Include="ParameterDescription.txt">
<LogicalName>ParameterDescription.txt</LogicalName>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
<Compile Include="JavaDocumentScraper.cs" />
<Compile Include="ClassPath.cs" />
<Compile Include="Log.cs" />
<Compile Include="JavaParameterNamesLoader.cs" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
</Project>
5 changes: 5 additions & 0 deletions tools/class-parse/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public static void Main (string[] args)
var outputFile = (string) null;
string platform = null;
var docsPaths = new List<string> ();
var paramDescs = new List<string> ();
var p = new OptionSet () {
"usage: class-dump [-dump] FILES",
"",
Expand All @@ -38,6 +39,9 @@ public static void Main (string[] args)
{ "docspath=",
"Documentation {PATH} for parameter fixup",
doc => docsPaths.Add (doc) },
{ "parameters-description=",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why a new class-parse --parameters-description=FILE option, instead of reusing class-parse --docspath? They're both related to parameter name fixups.

Also, PR #202 has examples of unit testing ClassParse.DocumentationPaths.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not only because there is no reason to mess existing PUBLIC arguments with INTERNAL ONLY feature, it is NOT A DOCS PATH. Whatever feature that uses this feature for NON-docs path is WRONG.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have allowed non-"documentation" paths to be used with class-parse --docspath since PR #97 which you -- at the time -- thought was acceptable.

The fundamental purpose to class-parse --docspath is to provide parameter name information for methods.

The fundamental purpose of this PR is to provide parameter name information for methods.

That sounds ~identical to me, and I don't see why we need a completely different abstraction in the form of JavaParameterNamesLoader when we have an existing abstraction in IAndroidDocScraper.GetParameterNames(). (Sure, the name is misleading -- "scraping" documentation isn't required -- but the semantics are the same: provide method parameter names. Rename IAndroidDocScraper to IJavaMethodParameterNameProvider and the name is no longer misleading.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not preferred but acceptable. Asking for pointless change is not the same.

"Parameter description file {PATH}",
desc => paramDescs.Add (desc) },
{ "docstype=",
"OBSOLETE: Previously used to specify a doc type (now auto detected).",
t => docsType = t != null },
Expand Down Expand Up @@ -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) {
Expand Down