Skip to content

Commit cd12700

Browse files
committed
feat: add annotation type support to parser and autocomplete
- Parse @interface declarations in ASTFactory (previously threw IOException) - Filter completions after '@' to show only annotation types - Add annotation parameter completion inside @annotation(...) - Resolve annotation element Javadoc from source attachments
1 parent 207eea7 commit cd12700

7 files changed

Lines changed: 409 additions & 8 deletions

File tree

RSTALanguageSupport/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ dependencies {
88
api 'com.fifesoft:rsyntaxtextarea:3.6.1'
99
api 'com.fifesoft:autocomplete:3.3.3'
1010
implementation 'org.mozilla:rhino-all:1.9.0'
11+
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
12+
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
1113
}
1214

1315
base {
@@ -30,6 +32,7 @@ jar {
3032
}
3133
}
3234
test {
35+
useJUnitPlatform()
3336
testLogging {
3437
events 'failed' //, 'passed', 'skipped', 'standardOut', 'standardError'
3538

RSTALanguageSupport/src/main/java/org/fife/rsta/ac/java/ClassCompletion.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,16 @@ public String getSummary() {
181181
}
182182

183183

184+
/**
185+
* Returns whether this class is an annotation type (has ACC_ANNOTATION flag).
186+
*
187+
* @return Whether this is an annotation type.
188+
*/
189+
public boolean isAnnotationType() {
190+
return (cf.getAccessFlags() & AccessFlags.ACC_ANNOTATION) != 0;
191+
}
192+
193+
184194
@Override
185195
public String getToolTipText() {
186196
return "class " + getReplacementText();

RSTALanguageSupport/src/main/java/org/fife/rsta/ac/java/SourceCompletionProvider.java

Lines changed: 234 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.fife.rsta.ac.ShorthandCompletionCache;
3030
import org.fife.rsta.ac.java.buildpath.LibraryInfo;
3131
import org.fife.rsta.ac.java.buildpath.SourceLocation;
32+
import org.fife.rsta.ac.java.classreader.AccessFlags;
3233
import org.fife.rsta.ac.java.classreader.ClassFile;
3334
import org.fife.rsta.ac.java.classreader.FieldInfo;
3435
import org.fife.rsta.ac.java.classreader.MemberInfo;
@@ -46,6 +47,7 @@
4647
import org.fife.rsta.ac.java.rjc.lang.Type;
4748
import org.fife.rsta.ac.java.rjc.lang.TypeArgument;
4849
import org.fife.rsta.ac.java.rjc.lang.TypeParameter;
50+
import org.fife.ui.autocomplete.BasicCompletion;
4951
import org.fife.ui.autocomplete.Completion;
5052
import org.fife.ui.autocomplete.DefaultCompletionProvider;
5153
import org.fife.ui.rsyntaxtextarea.RSyntaxDocument;
@@ -261,6 +263,93 @@ public void setShorthandCache(ShorthandCompletionCache shorthandCache) {
261263
}
262264

263265

266+
/**
267+
* Adds completions for annotation element parameters when the caret
268+
* is inside an annotation's parentheses (e.g. {@code @Serialize(nam|)}).
269+
*
270+
* @param set The set to add completions to.
271+
* @param cu The compilation unit being parsed.
272+
* @param annotationClassName The simple name of the annotation type.
273+
* @return {@code true} if annotation completions were added (caller
274+
* should return them directly), {@code false} if not in
275+
* annotation context.
276+
* @since 3.3.0
277+
*/
278+
private boolean addAnnotationElementCompletions(Set<Completion> set,
279+
CompilationUnit cu, String annotationClassName) {
280+
281+
ClassFile cf = getClassFileFor(cu, annotationClassName);
282+
if (cf == null ||
283+
(cf.getAccessFlags() & AccessFlags.ACC_ANNOTATION) == 0) {
284+
return false;
285+
}
286+
287+
// Add annotation element methods as completions
288+
int methodCount = cf.getMethodCount();
289+
for (int i=0; i<methodCount; i++) {
290+
MethodInfo method = cf.getMethodInfo(i);
291+
String name = method.getName();
292+
293+
// Skip synthetic methods and inherited Object/Annotation methods
294+
if (name.startsWith("<") || name.equals("values") ||
295+
name.equals("valueOf") || name.equals("hashCode") ||
296+
name.equals("toString") || name.equals("annotationType") ||
297+
name.equals("equals")) {
298+
continue;
299+
}
300+
301+
// Display shows "name" but insertion produces "name="
302+
BasicCompletion bc = new BasicCompletion(this, name + "=") {
303+
@Override
304+
public String getInputText() {
305+
return name;
306+
}
307+
};
308+
bc.setShortDescription(method.getReturnTypeString(false));
309+
310+
// Build summary from source Javadoc if available
311+
SourceLocation loc = getSourceLocForClass(
312+
cf.getClassName(true));
313+
if (loc != null) {
314+
CompilationUnit annotCu =
315+
Util.getCompilationUnitFromDisk(loc, cf);
316+
if (annotCu != null) {
317+
Iterator<TypeDeclaration> tdi =
318+
annotCu.getTypeDeclarationIterator();
319+
while (tdi.hasNext()) {
320+
TypeDeclaration td = tdi.next();
321+
if (td.getName().equals(
322+
cf.getClassName(false))) {
323+
Iterator<Member> members =
324+
td.getMemberIterator();
325+
while (members.hasNext()) {
326+
Member m = members.next();
327+
if (m instanceof Method &&
328+
((Method) m).getName()
329+
.equals(name)) {
330+
String doc =
331+
((Method) m).getDocComment();
332+
if (doc != null &&
333+
doc.startsWith("/**")) {
334+
bc.setSummary(Util
335+
.docCommentToHtml(doc));
336+
}
337+
break;
338+
}
339+
}
340+
break;
341+
}
342+
}
343+
}
344+
}
345+
346+
set.add(bc);
347+
}
348+
349+
return !set.isEmpty();
350+
}
351+
352+
264353
/**
265354
* Gets the {@link ClassFile} for a class.
266355
*
@@ -523,6 +612,34 @@ protected List<Completion> getCompletionsImpl(JTextComponent comp) {
523612
// Note: getAlreadyEnteredText() never returns null
524613
String text = getAlreadyEnteredText(comp);
525614

615+
// Check for annotation parameter context: @Annotation(param|
616+
String annotationClassName = getAnnotationClassName(comp);
617+
if (annotationClassName != null && text.indexOf('.') == -1) {
618+
if (addAnnotationElementCompletions(set, cu,
619+
annotationClassName)) {
620+
completions = new ArrayList<>(set);
621+
Collections.sort(completions);
622+
text = text.substring(text.lastIndexOf('.') + 1);
623+
@SuppressWarnings("unchecked")
624+
int startIdx = Collections.binarySearch(completions,
625+
text, comparator);
626+
if (startIdx < 0) {
627+
startIdx = -(startIdx + 1);
628+
}
629+
else {
630+
while (startIdx > 0 && comparator.compare(
631+
completions.get(startIdx - 1), text) == 0) {
632+
startIdx--;
633+
}
634+
}
635+
@SuppressWarnings("unchecked")
636+
int endIdx = Collections.binarySearch(completions,
637+
text + '{', comparator);
638+
endIdx = -(endIdx + 1);
639+
return completions.subList(startIdx, endIdx);
640+
}
641+
}
642+
526643
// Special case - end of a String literal
527644
boolean stringLiteralMember = checkStringLiteralMember(comp, text, cu,
528645
set);
@@ -552,6 +669,20 @@ protected List<Completion> getCompletionsImpl(JTextComponent comp) {
552669

553670
// Do a final sort of all of our completions and we're good to go!
554671
completions = new ArrayList<>(set);
672+
673+
// If in annotation context (@), filter to only annotation types
674+
boolean annotationContext = isAnnotationContext(comp, text);
675+
if (annotationContext) {
676+
Iterator<Completion> it = completions.iterator();
677+
while (it.hasNext()) {
678+
Completion c = it.next();
679+
if (!(c instanceof ClassCompletion) ||
680+
!((ClassCompletion) c).isAnnotationType()) {
681+
it.remove();
682+
}
683+
}
684+
}
685+
555686
Collections.sort(completions);
556687

557688
// Only match based on stuff after the final '.', since that's what is
@@ -600,9 +731,109 @@ public List<LibraryInfo> getJars() {
600731

601732

602733

603-
public SourceLocation getSourceLocForClass(String className) {
604-
return jarManager.getSourceLocForClass(className);
605-
}
734+
/**
735+
* Returns the source location for the specified class, if available.
736+
*
737+
* @param className The fully qualified class name.
738+
* @return The source location, or {@code null} if not found.
739+
* @since 3.3.0
740+
*/
741+
public SourceLocation getSourceLocForClass(String className) {
742+
return jarManager.getSourceLocForClass(className);
743+
}
744+
745+
746+
/**
747+
* Checks if the character immediately before the already-entered text
748+
* is '@', indicating an annotation context.
749+
*/
750+
private boolean isAnnotationContext(JTextComponent comp,
751+
String alreadyEntered) {
752+
try {
753+
int caret = comp.getCaretPosition();
754+
int textStart = caret - alreadyEntered.length();
755+
if (textStart > 0) {
756+
String prev = comp.getDocument().getText(
757+
textStart - 1, 1);
758+
return prev.charAt(0) == '@';
759+
}
760+
}
761+
catch (BadLocationException e) {
762+
// ignore
763+
}
764+
return false;
765+
}
766+
767+
768+
/**
769+
* If the caret is inside an annotation's parentheses, returns the
770+
* annotation class name (simple name, to be resolved via imports).
771+
* Returns null if not in annotation parameter context.
772+
*/
773+
private String getAnnotationClassName(JTextComponent comp) {
774+
try {
775+
javax.swing.text.Document doc = comp.getDocument();
776+
int caret = comp.getCaretPosition();
777+
javax.swing.text.Element root = doc.getDefaultRootElement();
778+
// Scan up to 3 lines back (annotation params may span lines)
779+
int lineIdx = root.getElementIndex(caret);
780+
int scanStart = root.getElement(
781+
Math.max(0, lineIdx - 3)).getStartOffset();
782+
String text = doc.getText(scanStart, caret - scanStart);
783+
784+
// Find unmatched '(' scanning backward
785+
int depth = 0;
786+
int i = text.length() - 1;
787+
int parenPos = -1;
788+
while (i >= 0) {
789+
char c = text.charAt(i);
790+
if (c == ')') {
791+
depth++;
792+
}
793+
else if (c == '(') {
794+
if (depth == 0) {
795+
parenPos = i;
796+
break;
797+
}
798+
depth--;
799+
}
800+
i--;
801+
}
802+
if (parenPos < 0) {
803+
return null;
804+
}
805+
806+
// Extract identifier before '('
807+
int nameEnd = parenPos;
808+
int nameStart = nameEnd - 1;
809+
while (nameStart >= 0 &&
810+
Character.isJavaIdentifierPart(
811+
text.charAt(nameStart))) {
812+
nameStart--;
813+
}
814+
nameStart++;
815+
if (nameStart >= nameEnd) {
816+
return null;
817+
}
818+
819+
// Check for '@' before the identifier
820+
int atPos = nameStart - 1;
821+
// Skip whitespace between @ and name
822+
while (atPos >= 0 &&
823+
Character.isWhitespace(text.charAt(atPos))) {
824+
atPos--;
825+
}
826+
if (atPos < 0 || text.charAt(atPos) != '@') {
827+
return null;
828+
}
829+
830+
return text.substring(nameStart, nameEnd);
831+
}
832+
catch (BadLocationException e) {
833+
return null;
834+
}
835+
}
836+
606837

607838
/**
608839
* Returns whether a method defined by a super class is accessible to

RSTALanguageSupport/src/main/java/org/fife/rsta/ac/java/rjc/parser/ASTFactory.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -461,9 +461,11 @@ private TypeDeclaration getClassOrInterfaceDeclaration(CompilationUnit cu,
461461
break;
462462

463463
case ANNOTATION_START:
464-
// TODO: AnnotationTypeDeclaration, implement me.
465-
throw new IOException(
466-
"AnnotationTypeDeclaration not implemented");
464+
// @interface — annotation type declaration.
465+
// The '@' token has been consumed; now consume the 'interface' keyword.
466+
s.yylexNonNull(KEYWORD_INTERFACE, "'interface' expected after '@'");
467+
td = getNormalInterfaceDeclaration(cu, s, addTo);
468+
break;
467469

468470
default:
469471
ParserNotice notice = new ParserNotice(t,
@@ -873,9 +875,10 @@ else if (methodDecl) {
873875
type.incrementBracketPairCount(s.skipBracketPairs());
874876
}
875877
List<String> thrownTypeNames = getThrownTypeNames(cu, s);
876-
t = s.yylexNonNull("'{' or ';' expected");
878+
t = s.yylexNonNull("';' expected");
877879
if (t.getType() != SEPARATOR_SEMICOLON) {
878-
throw new IOException("';' expected");
880+
// Could be 'default' clause in annotation type element — skip to ';'
881+
s.eatThroughNextSkippingBlocks(SEPARATOR_SEMICOLON);
879882
}
880883
Method m = new Method(s, modList, type, methodNameToken, formalParams,
881884
thrownTypeNames);
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* This library is distributed under a modified BSD license. See the included
3+
* RSTALanguageSupport.License.txt file for details.
4+
*/
5+
package org.fife.rsta.ac.java;
6+
7+
import static org.junit.jupiter.api.Assertions.*;
8+
9+
import javax.swing.JTextArea;
10+
11+
import org.junit.jupiter.api.BeforeEach;
12+
import org.junit.jupiter.api.Test;
13+
14+
/**
15+
* Unit tests for annotation-related autocomplete features in
16+
* {@link SourceCompletionProvider}.
17+
*/
18+
class SourceCompletionProviderTest {
19+
20+
private SourceCompletionProvider provider;
21+
22+
@BeforeEach
23+
void setUp() {
24+
provider = new SourceCompletionProvider();
25+
}
26+
27+
/**
28+
* Helper: creates a JTextArea with the given text and caret at the end,
29+
* then returns getAlreadyEnteredText result.
30+
*/
31+
private String getEnteredText(String text) {
32+
JTextArea textArea = new JTextArea(text);
33+
textArea.setCaretPosition(text.length());
34+
return provider.getAlreadyEnteredText(textArea);
35+
}
36+
37+
// --- annotation prefix tests ---
38+
39+
@Test
40+
void testAnnotationPrefix() {
41+
// @ is not a Java identifier part, so it should NOT be included
42+
assertEquals("Serialize", getEnteredText("@Serialize"));
43+
}
44+
45+
@Test
46+
void testAnnotationPrefixPartial() {
47+
assertEquals("Ser", getEnteredText("@Ser"));
48+
}
49+
50+
}

0 commit comments

Comments
 (0)