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

[generator] Fix generated code that caused CS0169 warnings. #625

Merged
merged 2 commits into from
Apr 23, 2020

Conversation

mblowey
Copy link
Contributor

@mblowey mblowey commented Apr 13, 2020

Motivation

When generating C# bindings for Java binaries, the generator tool adds the internal static IntPtr class_ref field to the C# class. When adding this field, the generator tool needs to determine if the new modifier needs to be applied to the field, which indicates that the field is purposefully shadowing the class_ref field of the base class.

The current approach to determine if the new modifier is needed is to check if the target Java class is either java.lang.Object or java.lang.Throwable. If the target class is one of those two, then the new modifier is not applied. Because the class_ref field is internal, this is not a thorough enough check. If the target class's base class is from a different Java binary (and thus a different api.xml), the new modifier is not necessary for the target class's class_ref field. This is because the class_ref field is marked internal and is always hidden when it's in a different assembly. In this case, the C# class will compile with the following warning: warning CS0109: The member 'TargetClass.class_ref' does not hide an accessible member. The new keyword is not required.

  • Note that while most of this pull request description uses java.lang.Object as the base class from an external assembly, the issue and fix still apply to other base classes from other external assemblies.

Example

First, some very very simple Kotlin classes. (This is also reproducible with Java).

package com.testing.bindingexample

open class MyBase {}

class MyDerived : MyBase() {}

interface MyInterface {}

class MyImplementor : MyInterface {}

I then compile these classes into an Android library and build the AAR file. That AAR file is then added to a Xamarin Android Class Library project. No edits are made to any of the XML files in the Transforms folder. When the Xamarin project is built, it produces the following output:

1>------ Rebuild All started: Project: XamBindingExample, Configuration: Debug Any CPU ------
1>C:\Users\Mitchell\source\Projects\XamBindingExample\XamBindingExample\obj\Debug\generated\src\Com.Testing.Bindingexample.BuildConfig.cs(42,30,42,39): warning CS0109: The member 'BuildConfig.class_ref' does not hide an accessible member. The new keyword is not required.
1>C:\Users\Mitchell\source\Projects\XamBindingExample\XamBindingExample\obj\Debug\generated\src\Com.Testing.Bindingexample.MyBase.cs(13,30,13,39): warning CS0109: The member 'MyBase.class_ref' does not hide an accessible member. The new keyword is not required.
1>C:\Users\Mitchell\source\Projects\XamBindingExample\XamBindingExample\obj\Debug\generated\src\Com.Testing.Bindingexample.MyImplementor.cs(13,30,13,39): warning CS0109: The member 'MyImplementor.class_ref' does not hide an accessible member. The new keyword is not required.
1>  XamBindingExample -> C:\Users\Mitchell\source\Projects\XamBindingExample\XamBindingExample\bin\Debug\XamBindingExample.dll
========== Rebuild All: 1 succeeded, 0 failed, 0 skipped ==========

Note how the MyBase and MyImplementor classes, which inherit from Java.Lang.Object — an external base class — produce a CS0109 warning, while MyDerived does not.

This Pull Request

This pull request implements a more thorough check when determining if the new modifier is necessary, updates existing unit tests to account for the new behavior, and adds new unit tests for the new behavior.

About Generator Tool Changes

First, for context, the ClassGen.InheritsObject property is set by the following code in the ClassGen constructor.

InheritsObject = true;

if (Namespace == "Java.Lang" && (Name == "Object" || Name == "Throwable"))
	InheritsObject = false;

Previous Code

The previous check for determining if the new modifier is needed was

bool requireNew = @class.InheritsObject;
if (!requireNew) {
	for (var bg = @class.BaseGen; bg != null && bg is ClassGen classGen && classGen.FromXml; bg = bg.BaseGen) {
		if (bg.InheritsObject) {
			requireNew = true;
			break;
		}
	}
}
WriteClassHandle (@class, indent, requireNew);

The loop inside the if statement is not necessary. Whenever requireNew is false, @class is either Java.Lang.Object or Java.Lang.Throwable, and thus doesn't have a baseclass. Therefore, the loop will never be run as bg should always be null when requireNew == false

PR Code

The previous code is replaced by the following:

bool baseFromSameAssembly = @class?.BaseGen?.FromXml ?? false;
bool requireNew = @class.InheritsObject && baseFromSameAssembly;
WriteClassHandle (@class, indent, requireNew);

This code uses the FromXml property to determine if the base class is from the same assembly. The null checks are necessary not just for handling @class == Java.Lang.Object but also for unit tests where a ClassGen object may not have a base class set.

This code is much more succinct and clear about what it is being checked. An explanatory comment is provided in the code as well.

About Unit Test Changes

Existing Unit Test Changes

Some of the existing unit tests needed to have their expected output text files modified. This was mostly due to ClassGen objects in the tests not setting a base class, but still expecting the new modifier on the class_ref field.

New Unit Tests

The unit tests added by this PR are WriteClassExternalBase and WriteClassInternalBase. Both create and register the Java.Lang.Object class with the SymbolTable so that a ClassGen object can be created for it. Then both tests create a new ClassGen object called @class that represents the java.code.MyClass and validate it, which populates its base class members using the SymbolTable.

The WriteClassInternalBase test then sets the FromXml property of @class.BaseGen to indicate that the base class of java.code.MyClass comes from the same assembly. This is not done in the WriteClassExternalBase.

The tests conclude by generating the C# code for java.code.MyClass and comparing it to the contents of the corresponding expected results text file. The content of these text files was created by copying the content of the corresponding WriteClass.txt and modifying it as little as necessary.

Conclusion

I've tested this fix for the CS0109 warning not just on the binaries from the arbitrary Kotlin example above, but also on our organization's binding projects. Additionally, all generator tool unit tests are passing. Unfortunately I was unable to setup a build environment on Windows that allows me to run all the tests contained in the solution, however, since all changes were contained to the generator tool, I believe they'll still pass.

Thank you for reviewing this pull request. Please let me know if I'm making any over-simplifications or am missing some context that would render my changes invalid. I've only had my head in this project for about a day, so I definitely don't have deep knowledge of the project and its various 'gotchas'.

@dnfclas
Copy link

dnfclas commented Apr 13, 2020

CLA assistant check
All CLA requirements met.

@jpobst
Copy link
Contributor

jpobst commented Apr 13, 2020

Nice! I had gotten the warnings cleaned up inside Mono.Android.dll itself, which cleaned up some in external bindings, but I hadn't gotten around to fully cleaning the external ones up. I'll try to verify this this week.

@mblowey
Copy link
Contributor Author

mblowey commented Apr 15, 2020

Sounds good!

I am curious about how you cleaned up the warnings inside Mono.Android.dll. Is there a way to do so using metadata.xml or are the bindings for Mono.Android.dll hand-rolled?

@jpobst
Copy link
Contributor

jpobst commented Apr 15, 2020

I made the changes in generator, so all projects benefit. I just meant that I hadn't yet fixed cases that only exist outside Mono.Android.dll, like your change:

#522
#567

@jpobst
Copy link
Contributor

jpobst commented Apr 17, 2020

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@jpobst
Copy link
Contributor

jpobst commented Apr 17, 2020

This looks great, the one thing I want to do differently is the new unit tests (which are also great!).

As you've seen, having unit tests that compare expected output can be a pain when you want to make changes to the code we generate, because you have to update tons of .txt files. So for things like this where we are testing a very specific thing I've been trying to do the test in a way that doesn't require adding expected output text files:

https://github.com/xamarin/java.interop/blob/master/tests/generator-Tests/Unit-Tests/CodeGeneratorTests.cs#L1016-L1017

I think the 2 tests you added could be changed to variations of:

Assert.False (result.Contains ("internal static new IntPtr class_ref"));

or

Assert.True (result.Contains ("internal static new IntPtr class_ref"));

instead of adding all the new .txt files.

Then this should be good to go!

@brendanzagaeski
Copy link
Contributor

brendanzagaeski commented Apr 21, 2020

Draft release note

Here's a candidate Xamarin.Android release note for this change. For team members and the author of this PR, feel free to suggest changes or even a whole new wording. Thanks!

### Issues fixed

#### Bindings projects

- [Java.Interop GitHub PR 625](https://github.com/xamarin/java.interop/pull/625):
  Warnings similar to `warning CS0109: The member 'Class1.class_ref' does not
  hide an accessible member. The new keyword is not required.` could appear
  during bindings projects builds because the `new` keyword was added
  automatically to `class_ref` fields of generated bindings types in some cases
  where it wasn't needed.

@jpobst
Copy link
Contributor

jpobst commented Apr 23, 2020

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@mblowey
Copy link
Contributor Author

mblowey commented Apr 23, 2020

Hey, thank you for handling the unit test changes. Its been a busy week at work, so I haven't had a chance to get back to this PR.

mblowey and others added 2 commits April 23, 2020 11:21
…r to internal fields (like class_ref) when the class's base is in an external assembly.

  - This prevents a CS0109 warnings during binding generation.
  - Updated expected generator output files for existing tests.
  - Added two new tests that verify the expected output when the base class is in an external assembly and when the base class is in the same assembly as the derived class.
@jpobst
Copy link
Contributor

jpobst commented Apr 23, 2020

No worries! There's an upcoming deadline for getting things into 16.7 and I wanted to make sure this made it in time. 😄

@jpobst jpobst merged commit ce8dc40 into dotnet:master Apr 23, 2020
jpobst pushed a commit that referenced this pull request Apr 23, 2020
* Modified the generator tool so that it no longer adds a 'new' modifier to internal fields (like class_ref) when the class's base is in an external assembly.
  - This prevents a CS0109 warnings during binding generation.
  - Updated expected generator output files for existing tests.
  - Added two new tests that verify the expected output when the base class is in an external assembly and when the base class is in the same assembly as the derived class.

(cherry picked from commit ce8dc40)
@jpobst jpobst added this to the 10.4 (16.7 / 8.7) milestone Apr 23, 2020
@jpobst jpobst changed the title A fix for CS0109 warnings that are created when the generator tool is run on a type with an external assembly base class. [generator] Fix generated code that caused CS0169 warnings. Apr 23, 2020
jonpryor pushed a commit that referenced this pull request Aug 14, 2020
`generator` is an amazingly powerful tool that has proven to be very
versatile over the past decade.  However, many of the remaining bugs
fall into a category that is hard to fix given `generator`'s current
design, which is:

 1. Read in Java model
 2. Make tweaks to Java model
 3. Open `<TYPE>.cs`
 4. Loop through Java model determining what C# to write

The issue with this is that we are deciding **what** to generate
on-the-fly to a write-only stream.  This means that once we've written
to the stream we cannot change it if future information suggests that
we should.

A good example is issue #461.

The Java model is: #461

Given:

	// Java:
	interface AnimatorListener {
	  onAnimationEnd (int p0);
	  onAnimationEnd (int p0, p1);
	}

Looping through this, we see we need to generate an `EventArgs` class for
`AnimatorListener.onAnimationEnd(int p0)`, so we do:

	public partial class OnAnimationEndEventArgs : global::System.EventArgs {
	    int p0;

	    public OnAnimationEndEventArgs (int p0) { this.p0= p0; }

	    public int P0 { get { return p0; } }
	}

Then we get to the second method
`AnimatorListener.onAnimationEnd(int p0, int p1)` and see that we need
to add additional parameters to `OnAnimationEndEventArgs`.  However we
cannot modify the already written class, so we generate a second
`OnAnimationEndEventArgs` with different parameters, which causes
CS0101 compilation errors in C#.

Another example is we can generate an empty `InterfaceConsts` class.
This is because we write the class opening because we think we have
members to place in it (`public static class InterfaceConsts {`), but
as we loop through the members we thought we were going to add they
all get eliminated.  At that point all we can do is close the class
with no members.

The solution to both examples above is "simple": we need to first loop
through the rest of the Java model and determine what we **actually**
need to generate before we start writing anything.  We can then merge
the two `OnAnimationEndEventArgs` classes, or forgo writing an empty
`InterfaceConsts` type.

However, rather than adding this additional logic in each instance
that needs it, there is enough benefit to convert the entire
`generator` architecture to work by first building a C# model of what
needs to be generated that can be tweaked before writing to a file,
resulting in a design of:

 1. Read in Java model
 2. Make tweaks to Java model
 3. ***Build C# model from Java model***
 4. ***Make tweaks to C# model***
 5. Open `<TYPE>.cs`
 6. Loop through C# model, writing code

Having a C# model to tweak should also help in a few other tricky
cases we fail at today, like determining `override`s (#367, #586) and
implementing `abstract` methods for `abstract` classes inheriting an
`interface` (#470).

Additionally, having distinct logic and source writing steps will make
unit testing better.  Currently, the only way to test the *logic* of if
we are going to generate something is to write out the source code and
compare it to "expected" source code.  This means tiny fixes can have
many "expected" files that need to change (#625).

With separate steps, we can have a set of unit tests that test what
code we are writing, and a set that tests the logic of determining what
to generate.  To test the above "2 `EventArgs` classes" example, we can
test a fix by looking at the created C# model to ensure that only a
single combined `OnAnimationEndEventArgs` class is created.  We would
not need to write out the generated source code and compare it to
expected source code.

To assist in this new design, add a new `Xamarin.SourceWriter.dll`
assembly which is responsible for generating the C# source code.
This eliminates a lot of hard to read and error prone code such as:

	writer.WriteLine ("{0}{1}{2}{3}{4} unsafe {5} {6}{7} ({8})",
	        indent,
	        visibility,
	        static_arg,
	        virt_ov,
	        seal,
	        ret,
	        is_explicit ? GetDeclaringTypeOfExplicitInterfaceMethod (method.OverriddenInterfaceMethod) + '.' : string.Empty,
	        method.AdjustedName,
	        method.GetSignature (opt));

and replaces it with:

	var m = new MethodWriter {
	    IsPublic = true,
	    IsUnsafe = true,
	    IsOverride = true,
	    Name = method.AdjustedName
	};

	m.Write (…);

which will emit:

	public unsafe override void DoSomething ()
	{
	}

`Xamarin.SourceWriter.CodeWriter` is a wrapper around a
`System.IO.Stream` that tracks the current indent level, rather than
needing to pass `indent` around to every method.

	cw.WriteLine ("{");
	cw.Indent ();
	// ... write block body ...
	cw.Unindent ();
	cw.WriteLine ("}");

~~ Testing ~~

Many existing unit tests call directly into methods like
`CodeGenerator.WriteProperties()`.  These methods are no longer
available when using `JavaInterop`/`XAJavaInterop`.  These tests were
either changed to use `CodeGenerator.WriteType()` or were moved to only
run for `XamarinAndroid` codegen target.

Tests were updated to ignore whitespace and line endings differences,
as the refactor did not preserve 100% identical whitespace.
@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.

4 participants