Skip to content

Commit c1bc5c8

Browse files
authored
[java-source-utils] Better support orphaned Javadocs (dotnet#757)
Context: dotnet#687 (comment) What happens when there's a "regular" Java comment in between a Javadoc comment and a member? /* partial */ class Object { /** Create and return a copy of this object… */ // BEGIN Android-changed: Use native local helper for clone() // … protected Object clone() throws CloneNotSupportedException {…} } What happens is that the Javadoc becomes *orphaned*. Commit 69e1b80 attempted to handle such orphaned Javadocs via heuristic, using the first orphaned Javadoc comment in the parent scope. This didn't work reliably, as the parent scope could contain multiple "*unrelated*" orphaned Javadoc comments: class Outer { /** Orphaned dotnet#1 */ // cause orphaning class Inner {} void m() {} } Because containing types are fully processed before contained types, `Outer.m()` would grab the Javadoc for `Outer.Inner` before `Outer.Inner` would have a chance to grab it. Re-work the logic to associate orphaned Javadocs with their members, by requiring that the Javadoc comment begin *before* the member of interest, and *after* any preceding members. This should prevent incorrect correlation of orphaned Javadoc comment blocks. Additionally, update gradle to use javaparser 3.18.0, from 3.16.1: * javaparser/javaparser@javaparser-parent-3.16.1...javaparser-parent-3.18.0
1 parent 0cb8e2d commit c1bc5c8

File tree

4 files changed

+74
-28
lines changed

4 files changed

+74
-28
lines changed

tools/java-source-utils/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ repositories {
3030

3131
dependencies {
3232
// This dependency is used by the application.
33-
implementation 'com.github.javaparser:javaparser-core:3.16.1'
34-
implementation 'com.github.javaparser:javaparser-symbol-solver-core:3.16.1'
33+
implementation 'com.github.javaparser:javaparser-core:3.18.0'
34+
implementation 'com.github.javaparser:javaparser-symbol-solver-core:3.18.0'
3535

3636
// Use JUnit test framework
3737
testImplementation 'junit:junit:4.12'

tools/java-source-utils/src/main/java/com/microsoft/android/JniPackagesInfoFactory.java

Lines changed: 67 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.microsoft.android;
22

33
import java.io.File;
4+
import java.util.ArrayList;
45
import java.util.Collection;
56
import java.util.List;
67
import java.util.Optional;
@@ -35,8 +36,6 @@
3536

3637
import com.microsoft.android.ast.*;
3738

38-
import javassist.compiler.ast.FieldDecl;
39-
4039
import static com.microsoft.android.util.Parameter.*;
4140

4241
public final class JniPackagesInfoFactory {
@@ -90,6 +89,8 @@ private void parse(final JniPackagesInfo packages, final CompilationUnit unit) t
9089
: "";
9190
final JniPackageInfo packageInfo = packages.getPackage(packageName);
9291

92+
fixJavadocComments(unit, unit.getTypes());
93+
9394
for (final TypeDeclaration<?> type : unit.getTypes()) {
9495
if (JavaSourceUtilsOptions.verboseOutput && type.getFullyQualifiedName().isPresent()) {
9596
System.out.println("Processing: " + type.getFullyQualifiedName().get());
@@ -171,6 +172,7 @@ static ClassOrInterfaceType getTypeParameterBound(TypeParameter typeParameter) {
171172
}
172173

173174
private final void parseType(final JniPackageInfo packageInfo, final JniTypeInfo typeInfo, TypeDeclaration<?> typeDecl) {
175+
fixJavadocComments(typeDecl, getUndocumentedBodyMembers(typeDecl.getMembers()));
174176
for (final BodyDeclaration<?> body : typeDecl.getMembers()) {
175177
if (body.isAnnotationDeclaration()) {
176178
final AnnotationDeclaration annoDecl = body.asAnnotationDeclaration();
@@ -215,6 +217,68 @@ private final void parseType(final JniPackageInfo packageInfo, final JniTypeInfo
215217
}
216218
}
217219

220+
private final void fixJavadocComments(final Node decl, final Iterable<? extends BodyDeclaration<?>> bodyMembers) {
221+
final List<BodyDeclaration<?>> members = getUndocumentedBodyMembers(bodyMembers);
222+
final List<JavadocComment> orphanedComments = getOrphanComments(decl);
223+
224+
if (members.size() == 0)
225+
return;
226+
227+
final BodyDeclaration<?> firstMember = members.get(0);
228+
JavadocComment comment = orphanedComments.stream()
229+
.filter(c -> c.getBegin().get().isBefore(firstMember.getBegin().get()))
230+
.reduce((a, b) -> b)
231+
.orElse(null);
232+
if (comment != null) {
233+
((NodeWithJavadoc<?>) firstMember).setJavadocComment(comment);
234+
}
235+
236+
for (int i = 1; i < members.size(); ++i) {
237+
BodyDeclaration<?> prevMember = members.get(i-1);
238+
BodyDeclaration<?> member = members.get(i);
239+
240+
Optional<JavadocComment> commentOpt = orphanedComments.stream()
241+
.filter(c -> c.getBegin().get().isAfter(prevMember.getEnd().get()) &&
242+
c.getEnd().get().isBefore(member.getBegin().get()))
243+
.findFirst();
244+
if (!commentOpt.isPresent())
245+
continue;
246+
((NodeWithJavadoc<?>)member).setJavadocComment(commentOpt.get());
247+
}
248+
}
249+
250+
private final List<BodyDeclaration<?>> getUndocumentedBodyMembers(Iterable<? extends BodyDeclaration<?>> bodyMembers) {
251+
final List<BodyDeclaration<?>> members = new ArrayList<BodyDeclaration<?>> ();
252+
for (BodyDeclaration<?> member : bodyMembers) {
253+
if (!(member instanceof NodeWithJavadoc<?>)) {
254+
continue;
255+
}
256+
final NodeWithJavadoc<?> memberJavadoc = (NodeWithJavadoc<?>) member;
257+
if (memberJavadoc.getJavadocComment().isPresent())
258+
continue;
259+
final Optional<Position> memberBeginOpt = member.getBegin();
260+
if (!memberBeginOpt.isPresent())
261+
continue;
262+
members.add(member);
263+
}
264+
members.sort((a, b) -> a.getBegin().get().compareTo(b.getBegin().get()));
265+
return members;
266+
}
267+
268+
private final List<JavadocComment> getOrphanComments(Node decl) {
269+
final List<JavadocComment> orphanedComments = new ArrayList<JavadocComment>(decl.getOrphanComments().size());
270+
for (Comment c : decl.getOrphanComments()) {
271+
if (!c.isJavadocComment())
272+
continue;
273+
final Optional<Position> commentBeginOpt = c.getBegin();
274+
if (!commentBeginOpt.isPresent())
275+
continue;
276+
orphanedComments.add(c.asJavadocComment());
277+
}
278+
orphanedComments.sort((a, b) -> a.getBegin().get().compareTo(b.getBegin().get()));
279+
return orphanedComments;
280+
}
281+
218282
private final void parseAnnotationMemberDecl(final JniTypeInfo typeInfo, final AnnotationMemberDeclaration memberDecl) {
219283
final JniMethodInfo methodInfo = new JniMethodInfo(typeInfo, memberDecl.getNameAsString());
220284
typeInfo.add(methodInfo);
@@ -266,30 +330,8 @@ private static final void fillJavadoc(final HasJavadocComment member, NodeWithJa
266330
JavadocComment javadoc = null;
267331
if (nodeWithJavadoc.getJavadocComment().isPresent()) {
268332
javadoc = nodeWithJavadoc.getJavadocComment().get();
269-
} else {
270-
Node node = (Node) nodeWithJavadoc;
271-
if (!node.getParentNode().isPresent())
272-
return;
273-
274-
/*
275-
* Sometimes `JavaParser` won't associate a Javadoc comment block with
276-
* the AST node we expect it to. In such circumstances the Javadoc
277-
* comment will become an "orphan" comment, unassociated with anything.
278-
*
279-
* If `nodeWithJavadoc` has no Javadoc comment, use the *first*
280-
* orphan Javadoc comment in the *parent* scope, then *remove* that
281-
* comment so that it doesn't "stick around" for the next member we
282-
* attempt to grab Javadoc comments for.
283-
*/
284-
Node parent = node.getParentNode().get();
285-
for (Comment c : parent.getOrphanComments()) {
286-
if (c.isJavadocComment()) {
287-
javadoc = c.asJavadocComment();
288-
c.remove();
289-
break;
290-
}
291-
}
292333
}
334+
293335
if (javadoc != null) {
294336
member.setJavadocComment(javadoc.parse().toText());
295337
}

tools/java-source-utils/src/test/java/com/microsoft/android/JavadocXmlGeneratorTest.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import java.io.File;
55
import java.io.FileNotFoundException;
66
import java.io.PrintStream;
7+
import java.io.FileOutputStream;
78
import java.io.UnsupportedEncodingException;
89
import java.util.Arrays;
910
import javax.xml.parsers.ParserConfigurationException;
@@ -120,6 +121,9 @@ private static void testWritePackages(final String resourceJava, final String re
120121
final String expected = JniPackagesInfoTest.getResourceContents(resourceXml);
121122

122123
generator.writePackages(packagesInfo);
124+
// try (FileOutputStream o = new FileOutputStream(resourceXml + "-jonp.xml")) {
125+
// bytes.writeTo(o);
126+
// }
123127
assertEquals(resourceJava + " Javadoc XML", expected, bytes.toString());
124128
}
125129
}

tools/java-source-utils/src/test/resources/com/microsoft/android/Outer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
*
1010
* JNI sig: Lexample/Outer;
1111
*/
12-
12+
// Cause the above Javadoc block to be orphaned from the below class declaration
1313
@Outer.MyAnnotation(keys={"a", "b", "c"})
1414
public class Outer<T extends Object & Runnable, U extends Error> {
1515

0 commit comments

Comments
 (0)