| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
|
first point is a nice to have but it does not hurt too much to have the import. I might have missed it but we should have a test with classes with the same simple name, ex: org.assertj.assertions.generator.data.nba.Player and org.assertj.assertions.generator.data.tennis.Player. I have moved this to the 3.0 version to give use time testing this and because I'm going to release 2.1.0 soonish. |
Sorry, something went wrong.
| : entryPointClassPackage; | ||
| entryPointAssertionsClassContent = replace(entryPointAssertionsClassContent, PACKAGE, classPackage); | ||
|
|
||
| Set<String> imports; |
There was a problem hiding this comment.
extract imports resolution in a method, maybe resolveImports ?
Sorry, something went wrong.
There was a problem hiding this comment.
resolvedImports? As they are already resolved once they are in there. However, if we just use the map then we won't need this
Sorry, something went wrong.
| return entryPointAssertionsClassContent; | ||
| } | ||
|
|
||
| private static String listNeededImports(Collection<String> imports) { |
There was a problem hiding this comment.
the name is confusing I was expecting it to return a list, rename to buildImports ?
Sorry, something went wrong.
There was a problem hiding this comment.
Will do, just so you know, I used this name because it is already used to create the imports for the assertion classes. I thought there is some reason so I kept it
Sorry, something went wrong.
There was a problem hiding this comment.
I was thinking to have the code looking like:
String imports = resolveImports(classDescriptionSet);
entryPointAssertionsClassContent = replace(entryPointAssertionsClassContent, IMPORTS, listNeededImports(imports));and do whatever we need to do in resolveImports to get all the imports.
Sorry, something went wrong.
There was a problem hiding this comment.
I see that we need the set of imports later in generateAssertionEntryPointMethodsFor but I think it is better to recompute them in generateAssertionEntryPointMethodsFor as we are passing the Set<ClassDescription> which is enough to recompute the imports.
I like when we only pass the minimal parameters to a method, to generate assertThat methods we only need the template and the Set<ClassDescription>.
Sorry, something went wrong.
There was a problem hiding this comment.
There is also a check that makes sure that there is ${imports} in the entry point template, if there is not then we don't do anything. Are you willing to enforce this and make this a breaking change?
If we recompute the imports in generateAssertionEntryPointMethodsFor then we need to pass a parameter that is going to tell whether we need to use imports or not.
Sorry, something went wrong.
There was a problem hiding this comment.
Since it has been decided to go with the imports, I'm ok with the breaking change, users that have used their own templates would only have to add ${imports} at the top, it seems reasonable to me if we document that clearly.
Sorry, something went wrong.
| private static Set<String> extractImports(Set<ClassDescription> classDescriptionSet) { | ||
| Set<String> imports = new TreeSet<>(); | ||
| Map<String, String> importedQualifiedName = new HashMap<>(classDescriptionSet.size() * 2); | ||
| for (ClassDescription description : new TreeSet<>(classDescriptionSet)) { |
There was a problem hiding this comment.
why not using a simple TreeSet instead of a map and just add everything ? the returned set will guarantee there will be no duplicates and the code will be really simple.
Sorry, something went wrong.
There was a problem hiding this comment.
return a SortedSet to make it clear that imports are sorted.
Sorry, something went wrong.
There was a problem hiding this comment.
If I add all the imports then we will have collisions when we have classes with the same name but different FQN. We might not need the Set if we just use the Map and do the check in the next method.
Will return a SortedSet to make it clear
Sorry, something went wrong.
There was a problem hiding this comment.
in case of collision, we only add the import for the first class but not for the second, correct ?
this method needs comments to explain clearly hoe we deal with colliding class names.
Sorry, something went wrong.
There was a problem hiding this comment.
Yes if we have a map then we would only add the FQN for the first simple class later one if an import exists we wouldn't do it.
Sorry, something went wrong.
There was a problem hiding this comment.
can you add a comment with an example to make this clear for the next guy that will read the code ? (likely me in 6 months 😸 )
Sorry, something went wrong.
| // resolve class assert (ex: PlayerAssert) | ||
| // in case of inner classes like Movie.PublicCategory, class assert will be MoviePublicCategoryAssert | ||
| String customAssertionClass = fullyQualifiedAssertClassName(classDescription); | ||
| if (imports.contains(customAssertionClass)) { |
There was a problem hiding this comment.
I think that if the import was not already known, we should add it to the import list but I feel that at this point we should already have it. WDYT ?
Sorry, something went wrong.
There was a problem hiding this comment.
that makes me think that we should not pas the imports here, if we have done a good job at finding all of them, we can use simple class name everywhere (except for simple class name collision but does it apply here, not sure ...)
Sorry, something went wrong.
There was a problem hiding this comment.
We need it so we can choose whether to use the simple class name or the FQN. Maybe better would be to pass a map from simple class name to FQN and if the simple class name matches and the FQN matches then we use that otherwise we use the FQN (Java 8 would be awesome here 😄).
The imports are created outside of this. I agree with you the best approach would be to compute the imports here (we already are iterating over the ClassDescription, but in order to do that we need to change the return so we can later add them to the entry point.
Sorry, something went wrong.
There was a problem hiding this comment.
I haven't understood that last sentence.
Sorry, something went wrong.
There was a problem hiding this comment.
This method here returns a String which is the content for entry points. If we want to compute the imports here (we can actually do this) then we need to return the content for the entry points and also the imports that need to be added to the entry point class
Sorry, something went wrong.
There was a problem hiding this comment.
I was thinking to recompute the imports here only to be able to decide whether to use a FQN or not, replacing ${imports} would still be done in generateAssertionsEntryPointClassContent.
I prefer to have a simpler method signature (without Set<String> imports parameter) at the cost of recompute the set of imports which is not an overly expensive operation, I usually don't like to pass parameters when they can be inferred in the method itself.
Sorry, something went wrong.
There was a problem hiding this comment.
What about making the generateAssertionEntryPointMethodsFor return some wrapper over the content and the imports? We'll need duplicate logic if we handle in in 2 places.
Sorry, something went wrong.
|
|
||
| /** | ||
| * Gets the simple name of the outer class, if the class is not nested then this is the same as | ||
| * {@link Class#getSimpleName()}. |
There was a problem hiding this comment.
an example would be welcome
Sorry, something went wrong.
|
Thanks for the feedback @joel-costigliola. I've replied to your comments inline. And here to your review comment:
I'll see what we can do about this.
From what I saw one can pass the options to the BaseGenerator and then we can use them there. This will be much more simple then passing it via the API.
Yes there is a test it is the AssertionsForClassesWithSameName.expected.txt and for this one there are imports for one Team and TeamAssert and FQN for the other.
Yes sure, 3.0 makes more sense so we can have t tested good. Looking forward to 2.1.0 |
Sorry, something went wrong.
|
I think we don't need the toggle feature / configuration flag, if we can make it work it is better to have imports. |
Sorry, something went wrong.
| importedQualifiedName.put(simpleAssertClassName(description), toImport); | ||
| } | ||
| } | ||
| return imports; |
There was a problem hiding this comment.
is that imports = importedQualifiedName.values() ? if so we can get rid of the imports set
Sorry, something went wrong.
There was a problem hiding this comment.
yes it is, we can actually do this.
Sorry, something went wrong.
| // resolve class (ex: Player) | ||
| // in case of inner classes like Movie.PublicCategory use class name with outer class i.e. Movie.PublicCategory. | ||
| String classToAssert = classDescription.getFullyQualifiedClassName(); | ||
| if (imports.contains(classDescription.getPackageName() + "." + classDescription.getOuterClassName())) { |
There was a problem hiding this comment.
why not if (imports.contains(classToAssert) ? if there is a legitimate case for not using that we need a comment explaining why with an example.
Sorry, something went wrong.
There was a problem hiding this comment.
So we can do imports only for first level classes, nested classes will go via the first level class.
For example for Movie.PublicCategory. We'll have an import for Movie and we then we can use Movie.PublicCategory in the template. What do you prefer more?
Sorry, something went wrong.
There was a problem hiding this comment.
I think it is better to use Movie.PublicCategory over PublicCategory, inner classes name without their outer class can be ambiguous.
Sorry, something went wrong.
|
@joel-costigliola thanks for all the useful comments on the PR. I would also like to explain my reasoning behind some of the decisions. The reason why I want to consolidate the generation of the imports into one place is due to performance. If we need to compute the imports once in generateAssertionsEntryPointClassContent then we recompute them again in generateAssertionEntryPointMethodsFor (by reusing the same method so we get a set) and then we generate the entry points methods it means that we are going to iterate the classDescriptionSet at least 3 times and we are going to create a TreeSet of the classDescriptionSet 3 times:
We can reduce this to 2 times, but then we will need to have the same logic we have in extractImports (will rename it) in the generateAssertionEntryPointMethodsFor. Do you think that this can have some performance hit in the execution. I am not sure the amount of classes that people have when using this. However, I am always for faster builds and with large classes that have assertions generated this might have an impact on it |
Sorry, something went wrong.
|
@joel-costigliola I update the PR with what we discussed in the comments. While doing the last commit I thought about the templates. Are there some mandatory variables that a user has to use. For example: ${all_assertions_entry_points}, ${package} in the entry point classes template (there are others as well)? Will it make sense to create an issue where we would enforce the mandatory variables in the templates? |
Sorry, something went wrong.
|
I never had performance issue with the generator, I don't think it is gonna be used on more than 10 000 classes, I favor simple code over more complex but more performant one, so let's avoid premature optimization, keep it as simple as possible and wait until we or someone else raise a performance issue. As for mandatory variables, more or less everything is mandatory, should we verify the templates ? interesting question, I would say that users that change templates should test the generated result, it can't be our job, moreover even if the variables are there you can still screw up the generated code by using them at improper location - not much we can do about it. |
Sorry, something went wrong.
|
Sorry about premature optimization, I've been having problems with that lately (not with the generator), so I am in that mindset a bit :). I addressed your previous comments. When you have time you can have another look at this. I think it is much cleaner now. I created #107 for a discussion for the mandatory variables. It is just an idea to provide a fail fast alternative when a user does not use a variable. |
Sorry, something went wrong.
| */ | ||
| private static SortedSet<String> extractImports(Set<ClassDescription> classDescriptionSet) { | ||
| Map<String, String> importedQualifiedName = new HashMap<>(classDescriptionSet.size() * 2); | ||
| for (final ClassDescription description : new TreeSet<>(classDescriptionSet)) { |
There was a problem hiding this comment.
do we need to create a new TreeSet<> to iterate on classDescriptionSet ?
Sorry, something went wrong.
There was a problem hiding this comment.
Not necessarily, do we want to have some ordering of the imports, i.e. Do we want to first methods to always have a normal import and then the methods below to use FQN in case that is needed? What we can do is to pass a SortedSet<ClassDescription> to generateAssertionsEntryPointClassContent as we in any case create those down the line.
Sorry, something went wrong.
| * @return a sorted set with all the FQN that can be imported | ||
| */ | ||
| private static SortedSet<String> extractImports(Set<ClassDescription> classDescriptionSet) { | ||
| Map<String, String> importedQualifiedName = new HashMap<>(classDescriptionSet.size() * 2); |
There was a problem hiding this comment.
I think importByClassName would be more expressive than importedQualifiedName
Sorry, something went wrong.
| for (final ClassDescription description : new TreeSet<>(classDescriptionSet)) { | ||
| final String outerClassName = description.getOuterClassName(); | ||
| if (!importedQualifiedName.containsKey(outerClassName)) { | ||
| String toImport = description.getPackageName() + "." + outerClassName; |
There was a problem hiding this comment.
description.getPackageName() + "." + outerClassName should be extracted to a ClassDescription method (getOuterClassFullyQualifiedName ?), the same code is used later on in generateAssertionEntryPointMethodsFor.
Sorry, something went wrong.
| // resolve class (ex: Player) | ||
| // in case of inner classes like Movie.PublicCategory use class name with outer class i.e. Movie.PublicCategory. | ||
| String classToAssert = classDescription.getFullyQualifiedClassName(); | ||
| if (resolvedImports.contains(classDescription.getPackageName() + "." + classDescription.getOuterClassName())) { |
There was a problem hiding this comment.
use the classDescription.getOuterClassFullyQualifiedName() (see previous comment)
Sorry, something went wrong.
| assertThat(ClassUtil.getSimpleNameWithOuterClass(String.class)).isEqualTo("String"); | ||
| } | ||
|
|
||
| @Test |
There was a problem hiding this comment.
rename the test using an english sentence to express what the test verifies then put _ instead of space.
Note: starting test method is redundant since we already have an @Test annotation
Sorry, something went wrong.
|
@joel-costigliola I just saw that #101 has been merged. This overhauls the type system that was used before. Can we say that it is safe to not use generics for this? The generator does not generate generic classes right? |
Sorry, something went wrong.
|
what do you mean exactly by not use generics for this ? |
Sorry, something went wrong.
|
Don't worry about the conflicts, I will fix them when integrating this PR |
Sorry, something went wrong.
|
I am wrong, the classToAssert can be a generic class. Taking this into consideration now we cannot generate the entry point without knowing whether the type is imported or not. I am not worried about the conflicts the problems are more on a conceptual level. If we have this entry point: public com.example.MyGenericAssert assertThat(MyGeneric<com.example.Test> value) {
...
}I even think that the code that will be created will be wrong. I think that we are going to generate something like MyGenericAssert<comExampleTest> (I have not tested it). There are no test that use generics. Let's that we generate the entry points correctly, now we need to import MyGenericAssert, MyGeneric and Test. This means that we will need to know whether we are creating a class that is imported or not. |
Sorry, something went wrong.
|
Yes that does not work out of the box in master but I think it is solvable. public static org.assertj.assertions.generator.data.MyGenericAssert<T> assertThat(org.assertj.assertions.generator.data.MyGeneric<Object> actual) {",
return new org.assertj.assertions.generator.data.MyGenericAssert<T>(actual);",
}when giving: public class MyGeneric<T> {
public T field;
public T getValue() {
return null;
}
}I'm using Class.getTypeParameters to get the generic parameters information. Still lot of work to do though ... |
Sorry, something went wrong.
|
Generics are quite tricky, especially on runtime when we have access only to the reflection API. I can make the following suggestion, let's wrap up the generics first and then we go back to this one. What do you think? As a direction for the generics I can propose the following: instead of doing everything in the single type we do it recursively, i.e. we have an Object that holds the raw type and the other generic parameters. We can have a factory that creates this types and this factory will know if a type can be imported (use non FQN) or not (use FQN). Then when we create the template we can just take the name for using from the type. Then for the imports we get what we can import from the type themselves. I can point you to another project I am working on (MapStruct) and we have something like this. We generate Java classes with non FQN when possible. |
Sorry, something went wrong.
|
Postponing this PR was my plan indeed (one thing at a time). I'm refactoring the code to give ClassDescription the responsibility to hold all type information needed to generate assertions (raw type, generics parameters, computing Assert class names, etc ...). Is that what you had in mind ? I'm not sure I get why we would need the Factory you are talking about, I think ClassDescription could list the required imports and the BaseGenerator decide whether to use FQN or not, or maybe ClassDescription can decide that given a base package. |
Sorry, something went wrong.
Yes something like that. What I had in mind was ClassDescription to be responsible for outputting the type to String, the base generator would just invoke ClassDescription#asString() for example. The Factory class will make sure to create the ClassDescription correctly. Let me know once you are done, so I can rebase this and continue working on it 😄 |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
Fixes #97
This is a first go in trying to use imports instead of using FQN in the assertions class.
I am not sure if this is complete: