Skip to content

Conversation

@jpobst
Copy link
Contributor

@jpobst jpobst commented Jan 11, 2022

Fixes: #727

Today, when generator creates a .NET namespace from a Java package, it applies a simple pascal case transformation to make the namespace match established .NET naming standards.

For example: package android.database becomes namespace Android.Database.

However there are a few scenarios that this is not a good fit for.

(1) Words where Pascal case is not desired: androidx -> AndroidX
(2) Java package names are often longer than C# namespaces: com.google.android.material.animation -> Google.Android.Material.Animation.

Both of these scenarios can lead to many repeated metadata lines to fix:

<attr path="/api/package[@name='androidx.core.accessibilityservice']" name="managedName">AndroidX.Core.AccessibilityService</attr>
<attr path="/api/package[@name='androidx.core.app']" name="managedName">AndroidX.Core.App</attr>
<attr path="/api/package[@name='androidx.core.content']" name="managedName">AndroidX.Core.Content</attr>
<attr path="/api/package[@name='androidx.core.database']" name="managedName">AndroidX.Core.Database</attr>
<attr path="/api/package[@name='androidx.core.graphics']" name="managedName">AndroidX.Core.Graphics</attr>
<attr path="/api/package[@name='androidx.core.graphics.drawable']" name="managedName">AndroidX.Core.Graphics.Drawable</attr>
<attr path="/api/package[@name='androidx.core.hardware.display']" name="managedName">AndroidX.Core.Hardware.Display</attr>
<attr path="/api/package[@name='androidx.core.hardware.fingerprint']" name="managedName">AndroidX.Core.Hardware.Fingerprint</attr>
<attr path="/api/package[@name='androidx.core.internal']" name="managedName">AndroidX.Core.Internal</attr>
<attr path="/api/package[@name='androidx.core.internal.view']" name="managedName">AndroidX.Core.Internal.View</attr>
etc..

(Source)

Solution

To help these scenarios, we introduce a new metadata-specified item that would allow simple replacements. (Note that generator accepts <ns-replace> in metadata files, though we will likely provide the MSBuild item specified in #727 as the preferred way for users to specify replacements.)

Examples:

<metadata>
  <ns-replace source='Androidx' replacement='AndroidX' />
  <ns-replace source='Com' replacement='' />
  <ns-replace source='Com.Google.' replacement='Google' />
</metadata>

Implementation Notes

These replacements will only be run for <package> elements that do not specify a @managedName attribute. If you use @managedName you are opting to provide the exact name, we will not process it further.

Unlike unused metadata, these replacement will not raise a warning if they are unused.

Case Sensitivity

Replacements take place after the automatic Pascal case transform, but the compare is case-insensitive.

Thus, both of the following are equivalent:

<ns-replace source='Androidx' replacement='AndroidX' />
<ns-replace source='androidx' replacement='AndroidX' />

Word Bounds

Replacements take place only on full words (namespace parts).

Thus,

<ns-replace source='Com' replacement='' />

Matches Com.Google.Library, but not Common.Google.Library or Google.Imaging.Dicom.

Multiple full words can be used:

<ns-replace source='Com.Google' replacement='Google' />
<ns-replace source='Com.Androidx' replacement='Xamarin.AndroidX' />

Word Position

The word part match can be constrained to the beginning or end of a namespace by appending a . or prepending a ., respectively.

<ns-replace source='Androidx.' replacement='Xamarin.AndroidX' />

matches Androidx.Core, but not Square.OkHttp.Androidx.

Similarly,

<ns-replace source='.Compose' replacement='ComposeUI' />

matches Google.AndroidX.Compose, but not Google.Compose.Writer.

Both prepending and appending a . makes it an exact match.

<ns-replace source='.Androidx.' replacement='Xamarin.AndroidX' />

matches Androidx, but not Google.Androidx.Core.

Replacement Order

Replacements run in the order specified by the metadata file, however adding replacements to multiple metadata files may result in an unintended order.

Replacements are run sequentially, and multiple replacements may affect a single namespace.

<ns-replace source='Androidx' replacement='Xamarin.AndroidX' />
<ns-replace source='View' replacement='Views' />

changes Androidx.View to Xamarin.AndroidX.Views.

@jpobst jpobst marked this pull request as ready for review January 13, 2022 19:11
@jpobst jpobst requested a review from jonpryor January 13, 2022 19:11
@jpobst
Copy link
Contributor Author

jpobst commented Jan 18, 2022

Good feedback, added support for .androidx. meaning "full match", and updated PR description with explanation and example.


foreach (var xe in FixupDocument.XPathSelectElements ("/metadata/ns-replace")) {
if (NamespaceTransform.TryParse (xe, out var transform))
list.Add (transform);
Copy link
Contributor

Choose a reason for hiding this comment

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

What should happen if there's a duplicate? Should we care? Emit a warning?

Particularly if we're thinking of adding Metadata.xml into NuGet packages for "reuse" by "downstream binding projects", the likelihood for duplicates will increase. It shouldn't be an error, but would a warning be useful? Or would it be too common for warnings?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it's fine to ignore duplicates, we do the same for other metadata.

I'm not aware of any plan to add Metadata.xml into NuGet packages.


public List<NamespaceTransform> GetNamespaceTransforms ()
{
var list = new List<NamespaceTransform> ();
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be a List<T>, or should it be a HashSet<T>?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Order is significant, so we need to stick with List.

}
}

public List<NamespaceTransform> GetNamespaceTransforms ()
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this return an interface instead of a List<T> from a public method in a public class? Means we can't change the runtime type easily, and -- when taking "duplicates" into consideration -- a Set<T> of some form might be ideal?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think we consider this public API that we are guaranteeing API compatibility with, but changed anyways.


namespace Java.Interop.Tools.Generator
{
public class NamespaceTransform
Copy link
Contributor

Choose a reason for hiding this comment

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

Rando aside: would this be a good use case for C#9 records? https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-9#record-types

Copy link
Contributor

Choose a reason for hiding this comment

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

Doesn't look like you can override primary constructors (?!), so this isn't a good use case.

Copy link
Contributor

Choose a reason for hiding this comment

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

However, you can still use record types without using primary constructors…

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reading through the link, I don't see any benefits to using record, and we haven't bumped to C# 9 yet.

@jonpryor
Copy link
Contributor

Commit message for review:

Fixes: https://github.com/xamarin/java.interop/issues/727

Today, when `generator` creates a .NET `namespace` from a
Java `package`, it applies a simple PascalCase transformation to make
the namespace match established .NET naming standards.  For example.
`package android.database` becomes `namespace Android.Database`.

However there are a few scenarios that this is not a good fit for:

 1. Word phrases where upper-casing only the first letter is not
    desirable, e.g. `package androidx` to `namespace AndroidX`.

 2. When Java package names are longer than the desired C# namespaces;
    `com.` is a common Java package prefix, but isn't common in C#:
    `package com.google.android.material.animation` should become
    `namespace Google.Android.Material.Animation`.

Both of these scenarios can require many repeated `metadata` lines
[to fix][0]:

	<attr path="/api/package[@name='androidx.core.accessibilityservice']" name="managedName">AndroidX.Core.AccessibilityService</attr>
	<attr path="/api/package[@name='androidx.core.app']" name="managedName">AndroidX.Core.App</attr>
	

Improve support for this scenario by adding a support for a new
`<ns-replace/>` element to `Metadata.xml` transform files.
(We may also add support for an `@(AndroidNamespaceReplacement)` item
group as suggested in xamarin/java.interop#727 in the future.)

	<metadata>
	  <ns-replace source='Androidx' replacement='Xamarin.AndroidX' />
	  <ns-replace source='.Androidx.' replacement='Xamarin.AndroidX' />
	  <ns-replace source='Com' replacement='' />
	  <ns-replace source='Com.Google.' replacement='Google' />
	  <ns-replace source='.Compose' replacement='ComposeUI' />
	</metadata>

The `//ns-replace/@source` attribute is a Java [PackageName][1] to
replace.  Note: the Java PackageName grammar is:

> *PackageName*:
>   - *Identifier*
>   - *PackageName* `.` *Identifier*

The `//ns-replace` values *do not* override the [managedName][2]
attribute if already specified within metadata.

The `//ns-replace/@source` attribute:

  * Specifies a "match"; when it matches, instances of `@source` are
    replaced with `@replacement`.

  * Is interpreted in a *case-insensitive* manner; `Androidx` matches
    `androidx` and `AndroidX` and `ANDROIDX`.

  * Only matches on full [Identifier][3]s and [PackageName][1]s;

        <ns-replace source='Com' replacement='' />

    Matches `Com.Google.Library`, but not `Common.Google.Library` or
    `Google.Imaging.Dicom`.

  * Multiple "dotted" Identifiers may be used, and all may match
    *anywhere*, *in order*, in the Java package name.

        <ns-replace source='Com.Google' replacement='Google' />

    will match `com.google.library` and `example.com.google`, but
    won't match `Common.Google` or `Com.Googles`.

  * *May start with* a `.`, which means that `//ns-replace/@source`
    is only matched at the *end* of a Java package name:

        <ns-replace source='.Compose' replacement='ComposeUI' />

    will match `com.google.androidx.compose`,
    but not `com.google.androidx.compose.writer`.

  * *May end with* a `.`, which means that `//ns-replace/@source` is
    only matched at the *beginning* of the package name:

        <ns-replace source='Androidx.' replacement='Xamarin.AndroidX' />

    matches `androidx.core`, but not `com.square.okhttp.androidx`.

  * May both begin and end with a `.`, which means that only exact
    (case insensitive) Java package names are matched:

        <ns-replace source='.Androidx.' replacement='Xamarin.AndroidX' />

    matches `androidx`, but not `com.google.androidx.core`.

Additionally, duplicate `//ns-replace/@source` values are *ignored*.

`//ns-replace` elements are processed "in order" for a given
`Metadata.xml` file, but the ordering of `generator --fixup` files /
`@(TransformFile)`s is unspecified.

Multiple replacements may affect a single namespace:

	<ns-replace source='Androidx'   replacement='Xamarin.AndroidX' />
	<ns-replace source='View'       replacement='Views' />

changes `Androidx.View` to `Xamarin.AndroidX.Views`.

[0]: https://github.com/xamarin/AndroidX/blob/f97553ff428f9b6ea754f173567d220048245a16/source/androidx.core/core/Transforms/Metadata.Namespaces.xml#L10-L34
[1]: https://docs.oracle.com/javase/specs/jls/se8/html/jls-6.html#jls-6.5
[2]: https://docs.microsoft.com/en-us/xamarin/android/platform/binding-java-library/customizing-bindings/java-bindings-metadata#managedname
[3]: https://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-Identifier

@jonpryor jonpryor merged commit 0c90cf5 into main Jan 20, 2022
@jonpryor jonpryor deleted the ns-replace branch January 20, 2022 15:43
@github-actions github-actions bot locked and limited conversation to collaborators Apr 13, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow for bulk namespace changes

3 participants