Skip to content
This repository was archived by the owner on Jul 6, 2026. It is now read-only.

Commit c1bc5c8

Browse files
authored
[java-source-utils] Better support orphaned Javadocs (#757)
Context: #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 #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

4 files changed

Lines changed: 74 additions & 28 deletions

File tree

‎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
packagecom.microsoft.android;
22

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

3637
importcom.microsoft.android.ast.*;
3738

38-
importjavassist.compiler.ast.FieldDecl;
39-
4039
importstaticcom.microsoft.android.util.Parameter.*;
4140

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

92+
fixJavadocComments(unit, unit.getTypes());
93+
9394
for (finalTypeDeclaration<?> 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
privatefinalvoidparseType(finalJniPackageInfopackageInfo, finalJniTypeInfotypeInfo, TypeDeclaration<?> typeDecl) {
175+
fixJavadocComments(typeDecl, getUndocumentedBodyMembers(typeDecl.getMembers()));
174176
for (finalBodyDeclaration<?> body : typeDecl.getMembers()) {
175177
if (body.isAnnotationDeclaration()) {
176178
finalAnnotationDeclarationannoDecl = body.asAnnotationDeclaration();
@@ -215,6 +217,68 @@ private final void parseType(final JniPackageInfo packageInfo, final JniTypeInfo
215217
}
216218
}
217219

220+
privatefinalvoidfixJavadocComments(finalNodedecl, finalIterable<? extendsBodyDeclaration<?>> bodyMembers) {
221+
finalList<BodyDeclaration<?>> members = getUndocumentedBodyMembers(bodyMembers);
222+
finalList<JavadocComment> orphanedComments = getOrphanComments(decl);
223+
224+
if (members.size() == 0)
225+
return;
226+
227+
finalBodyDeclaration<?> firstMember = members.get(0);
228+
JavadocCommentcomment = 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 (inti = 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+
privatefinalList<BodyDeclaration<?>> getUndocumentedBodyMembers(Iterable<? extendsBodyDeclaration<?>> bodyMembers) {
251+
finalList<BodyDeclaration<?>> members = newArrayList<BodyDeclaration<?>> ();
252+
for (BodyDeclaration<?> member : bodyMembers) {
253+
if (!(memberinstanceofNodeWithJavadoc<?>)) {
254+
continue;
255+
}
256+
finalNodeWithJavadoc<?> memberJavadoc = (NodeWithJavadoc<?>) member;
257+
if (memberJavadoc.getJavadocComment().isPresent())
258+
continue;
259+
finalOptional<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+
returnmembers;
266+
}
267+
268+
privatefinalList<JavadocComment> getOrphanComments(Nodedecl) {
269+
finalList<JavadocComment> orphanedComments = newArrayList<JavadocComment>(decl.getOrphanComments().size());
270+
for (Commentc : decl.getOrphanComments()) {
271+
if (!c.isJavadocComment())
272+
continue;
273+
finalOptional<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+
returnorphanedComments;
280+
}
281+
218282
privatefinalvoidparseAnnotationMemberDecl(finalJniTypeInfotypeInfo, finalAnnotationMemberDeclarationmemberDecl) {
219283
finalJniMethodInfomethodInfo = newJniMethodInfo(typeInfo, memberDecl.getNameAsString());
220284
typeInfo.add(methodInfo);
@@ -266,30 +330,8 @@ private static final void fillJavadoc(final HasJavadocComment member, NodeWithJa
266330
JavadocCommentjavadoc = null;
267331
if (nodeWithJavadoc.getJavadocComment().isPresent()) {
268332
javadoc = nodeWithJavadoc.getJavadocComment().get();
269-
} else {
270-
Nodenode = (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-
Nodeparent = node.getParentNode().get();
285-
for (Commentc : 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
importjava.io.File;
55
importjava.io.FileNotFoundException;
66
importjava.io.PrintStream;
7+
importjava.io.FileOutputStream;
78
importjava.io.UnsupportedEncodingException;
89
importjava.util.Arrays;
910
importjavax.xml.parsers.ParserConfigurationException;
@@ -120,6 +121,9 @@ private static void testWritePackages(final String resourceJava, final String re
120121
finalStringexpected = 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
publicclassOuter<TextendsObject & Runnable, UextendsError> {
1515

0 commit comments

Comments
 (0)