Skip to content

Commit 54cfaa6

Browse files
Markdown Javadoc links to URLs not properly interpreted 4531
markdown links [description](url) syntax now interpreting properly Fix: #4531
1 parent 4d31764 commit 54cfaa6

File tree

4 files changed

+258
-3
lines changed

4 files changed

+258
-3
lines changed

org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/parser/AbstractCommentParser.java

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.util.HashMap;
2727
import java.util.List;
2828
import java.util.Map;
29+
import java.util.Set;
2930
import org.eclipse.jdt.core.compiler.CharOperation;
3031
import org.eclipse.jdt.core.compiler.InvalidInputException;
3132
import org.eclipse.jdt.internal.compiler.ast.TypeReference;
@@ -1393,6 +1394,89 @@ protected boolean parseReference() throws InvalidInputException {
13931394
return parseReference(false);
13941395
}
13951396

1397+
// Checks if current identifier is a URL scheme (http, https, ftp, etc.)
1398+
private boolean isURLScheme(char[] identifier) {
1399+
return CharOperation.equals("http".toCharArray(), identifier) || //$NON-NLS-1$
1400+
CharOperation.equals("https".toCharArray(), identifier) || //$NON-NLS-1$
1401+
CharOperation.equals("ftp".toCharArray(), identifier) || //$NON-NLS-1$
1402+
CharOperation.equals("file".toCharArray(), identifier); //$NON-NLS-1$
1403+
}
1404+
1405+
// Checks if the current position is followed by "://" URL pattern
1406+
private boolean isFollowedByURLPattern() {
1407+
int pos = this.scanner.currentPosition;
1408+
try {
1409+
if (pos + 2 >= this.source.length) return false;
1410+
1411+
return this.source[pos] == ':' &&
1412+
this.source[pos + 1] == '/' &&
1413+
this.source[pos + 2] == '/';
1414+
} catch(Exception e) {
1415+
return false;
1416+
}
1417+
}
1418+
1419+
// Parses a complete URL reference starting from current position
1420+
private Object parseURLReference() throws InvalidInputException {
1421+
StringBuilder urlBuilder = new StringBuilder();
1422+
int firstTokenStartPos = this.scanner.startPosition;
1423+
//start with the current identifier
1424+
if (this.currentTokenType == TerminalToken.TokenNameIdentifier) {
1425+
urlBuilder.append(this.scanner.getCurrentTokenSource());
1426+
consumeToken();
1427+
}
1428+
1429+
// Add :
1430+
TerminalToken token1 = readTokenSafely();
1431+
if (token1 == TerminalToken.TokenNameCOLON) {
1432+
urlBuilder.append(this.scanner.getCurrentTokenSource());
1433+
consumeToken();
1434+
}
1435+
1436+
int pos = this.scanner.getCurrentTokenEndPosition() + 1;
1437+
char c;
1438+
while (pos < this.source.length) {
1439+
c = (this.source[pos] == ')') ? this.source[pos] : readChar();
1440+
1441+
if (c == ':') continue;
1442+
if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '*' || c == ')' || c == '(' || c == '[' || c == ']' ) {
1443+
break;
1444+
}
1445+
urlBuilder.append(c);
1446+
pos++;
1447+
}
1448+
char[] fullURL = urlBuilder.toString().toCharArray();
1449+
this.identifierPtr = 0;
1450+
this.identifierStack[this.identifierPtr] = fullURL;
1451+
this.identifierPositionStack[this.identifierPtr] = (((long) firstTokenStartPos) << 32) + (pos - 1);
1452+
this.identifierLengthStack[this.identifierLengthPtr] = 1;
1453+
return createTypeReference(TokenNameInvalid, true);
1454+
}
1455+
1456+
// Ensure the markdown URL bracket syntax follows []() or [][]
1457+
private void checkMarkdownLinkSyntaxValid(int currStartPos) {
1458+
char secondBracket = this.source[currStartPos - 2];
1459+
char thirdBracket = this.source[currStartPos - 1];
1460+
1461+
char[] generatedURL = this.identifierStack[this.identifierPtr];
1462+
char fourthBracket = this.source[currStartPos + generatedURL.length];
1463+
1464+
String pattern = new String(new char[] {secondBracket, thirdBracket, fourthBracket});
1465+
//invalid bracket patterns
1466+
Set<String> invalidPatterns = Set.of(
1467+
")[)", //$NON-NLS-1$
1468+
"](]", //$NON-NLS-1$
1469+
")[]", //$NON-NLS-1$
1470+
"][)", //$NON-NLS-1$
1471+
"([)", //$NON-NLS-1$
1472+
"(][", //$NON-NLS-1$
1473+
"[)(", //$NON-NLS-1$
1474+
"(](" //$NON-NLS-1$
1475+
);
1476+
if (invalidPatterns.contains(pattern)) this.abort = true;
1477+
1478+
}
1479+
13961480
/*
13971481
* Parse a reference in @see tag
13981482
*/
@@ -1488,6 +1572,18 @@ else if (this.tagValue == TAG_VALUE_VALUE) {
14881572
break nextToken;
14891573
case TokenNameUNDERSCORE:
14901574
case TokenNameIdentifier :
1575+
char[] identifier = this.scanner.getCurrentIdentifierSource();
1576+
if (isURLScheme(identifier)) {
1577+
if (isFollowedByURLPattern()) {
1578+
if (typeRef == null) {
1579+
int currStartPos = this.scanner.startPosition;
1580+
typeRefStartPosition = this.scanner.getCurrentTokenStartPosition();
1581+
typeRef = parseURLReference();
1582+
checkMarkdownLinkSyntaxValid(currStartPos);
1583+
if (this.abort) return false;
1584+
}
1585+
}
1586+
}
14911587
if (typeRef == null) {
14921588
typeRefStartPosition = this.scanner.getCurrentTokenStartPosition();
14931589
typeRef = parseQualifiedName(true, allowModule);
@@ -3612,6 +3708,7 @@ protected boolean verifySpaceOrEndComment() {
36123708
// Whitespace or inline tag closing brace
36133709
char ch = peekChar();
36143710
switch (ch) {
3711+
case ')':
36153712
case ']':
36163713
// TODO: Check if we need to exclude escaped ]
36173714
if (this.markdown)

org.eclipse.jdt.core.compiler.batch/src/org/eclipse/jdt/internal/compiler/parser/JavadocParser.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -573,7 +573,7 @@ protected boolean parseMarkdownLinks(int previousPosition) throws InvalidInputEx
573573
}
574574
break;
575575
case ']':
576-
if (peekChar() == '[') {
576+
if (peekChar() == '[' || peekChar() == '(') {
577577
// We might want to store the description in case of DOM parser
578578
// but the compiler does not need it
579579
//int length = this.index - start - 1;

org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/dom/ASTConverterMarkdownTest.java

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1914,4 +1914,161 @@ public class Table {}
19141914
assertEquals("Incorrect TagElement", 1, (fragments.get(3).getFlags() & ASTNode.MALFORMED)); //MALFOUND flag
19151915
}
19161916
}
1917+
1918+
public void testMarkdownURLs4531_01() throws JavaModelException {
1919+
String source = """
1920+
/// @see [Ex Si](ex.com)
1921+
public class Markdown() {}
1922+
""";
1923+
this.workingCopies = new ICompilationUnit[1];
1924+
this.workingCopies[0] = getWorkingCopy("/Converter_25/src/markdown/gh3761/Markdown.java", source, null);
1925+
if (this.docCommentSupport.equals(JavaCore.ENABLED)) {
1926+
CompilationUnit compilUnit = (CompilationUnit) runConversion(this.workingCopies[0], true);
1927+
TypeDeclaration typedeclaration = (TypeDeclaration) compilUnit.types().get(0);
1928+
Javadoc javadoc = typedeclaration.getJavadoc();
1929+
TagElement tags = (TagElement) javadoc.tags().get(0);
1930+
assertEquals("fragments count does not match", 2, tags.fragments().size());
1931+
TagElement tagElement = (TagElement) tags.fragments().get(1);
1932+
List<?> tagFragments = tagElement.fragments();
1933+
assertTrue(tagFragments.get(0) instanceof TextElement);
1934+
assertTrue(tagFragments.get(1) instanceof QualifiedName);
1935+
}
1936+
}
1937+
1938+
public void testMarkdownURLs4531_02() throws JavaModelException {
1939+
String source = """
1940+
/// @see [Ex Si](http://ex.com)
1941+
public class Markdown() {}
1942+
""";
1943+
this.workingCopies = new ICompilationUnit[1];
1944+
this.workingCopies[0] = getWorkingCopy("/Converter_25/src/markdown/gh3761/Markdown.java", source, null);
1945+
if (this.docCommentSupport.equals(JavaCore.ENABLED)) {
1946+
CompilationUnit compilUnit = (CompilationUnit) runConversion(this.workingCopies[0], true);
1947+
TypeDeclaration typedeclaration = (TypeDeclaration) compilUnit.types().get(0);
1948+
Javadoc javadoc = typedeclaration.getJavadoc();
1949+
TagElement tags = (TagElement) javadoc.tags().get(0);
1950+
assertEquals("fragments count does not match", 2, tags.fragments().size());
1951+
TagElement tagElement = (TagElement) tags.fragments().get(1);
1952+
List<?> tagFragments = tagElement.fragments();
1953+
assertTrue(tagFragments.get(0) instanceof TextElement);
1954+
assertTrue(tagFragments.get(1) instanceof SimpleName);
1955+
}
1956+
}
1957+
1958+
public void testMarkdownURLs4531_03() throws JavaModelException {
1959+
String source = """
1960+
/// @see [Ex Si](https://ex.com/a)
1961+
public class Markdown() {}
1962+
""";
1963+
this.workingCopies = new ICompilationUnit[1];
1964+
this.workingCopies[0] = getWorkingCopy("/Converter_25/src/markdown/gh3761/Markdown.java", source, null);
1965+
if (this.docCommentSupport.equals(JavaCore.ENABLED)) {
1966+
CompilationUnit compilUnit = (CompilationUnit) runConversion(this.workingCopies[0], true);
1967+
TypeDeclaration typedeclaration = (TypeDeclaration) compilUnit.types().get(0);
1968+
Javadoc javadoc = typedeclaration.getJavadoc();
1969+
TagElement tags = (TagElement) javadoc.tags().get(0);
1970+
assertEquals("fragments count does not match", 2, tags.fragments().size());
1971+
TagElement tagElement = (TagElement) tags.fragments().get(1);
1972+
List<?> tagFragments = tagElement.fragments();
1973+
assertTrue(tagFragments.get(0) instanceof TextElement);
1974+
assertTrue(tagFragments.get(1) instanceof SimpleName);
1975+
}
1976+
}
1977+
1978+
public void testMarkdownURLs4531_04() throws JavaModelException {
1979+
String source = """
1980+
/// @see [Ex Si](https://www.ex.net/a)
1981+
public class Markdown() {}
1982+
""";
1983+
this.workingCopies = new ICompilationUnit[1];
1984+
this.workingCopies[0] = getWorkingCopy("/Converter_25/src/markdown/gh3761/Markdown.java", source, null);
1985+
if (this.docCommentSupport.equals(JavaCore.ENABLED)) {
1986+
CompilationUnit compilUnit = (CompilationUnit) runConversion(this.workingCopies[0], true);
1987+
TypeDeclaration typedeclaration = (TypeDeclaration) compilUnit.types().get(0);
1988+
Javadoc javadoc = typedeclaration.getJavadoc();
1989+
TagElement tags = (TagElement) javadoc.tags().get(0);
1990+
assertEquals("fragments count does not match", 2, tags.fragments().size());
1991+
TagElement tagElement = (TagElement) tags.fragments().get(1);
1992+
List<?> tagFragments = tagElement.fragments();
1993+
assertTrue(tagFragments.get(0) instanceof TextElement);
1994+
assertTrue(tagFragments.get(1) instanceof SimpleName);
1995+
}
1996+
}
1997+
1998+
public void testMarkdownURLs4531_05() throws JavaModelException {
1999+
String source = """
2000+
/// @see [Ex Si][http://ex.com]
2001+
public class Markdown() {}
2002+
""";
2003+
this.workingCopies = new ICompilationUnit[1];
2004+
this.workingCopies[0] = getWorkingCopy("/Converter_25/src/markdown/gh3761/Markdown.java", source, null);
2005+
if (this.docCommentSupport.equals(JavaCore.ENABLED)) {
2006+
CompilationUnit compilUnit = (CompilationUnit) runConversion(this.workingCopies[0], true);
2007+
TypeDeclaration typedeclaration = (TypeDeclaration) compilUnit.types().get(0);
2008+
Javadoc javadoc = typedeclaration.getJavadoc();
2009+
TagElement tags = (TagElement) javadoc.tags().get(0);
2010+
assertEquals("fragments count does not match", 2, tags.fragments().size());
2011+
TagElement tagElement = (TagElement) tags.fragments().get(1);
2012+
List<?> tagFragments = tagElement.fragments();
2013+
assertTrue(tagFragments.get(0) instanceof TextElement);
2014+
assertTrue(tagFragments.get(1) instanceof SimpleName);
2015+
}
2016+
}
2017+
2018+
// [)[) - invalid condition
2019+
public void testMarkdownURLs4531_06() throws JavaModelException {
2020+
String source = """
2021+
/// @see [Ex Si)[http://ex.com)
2022+
public class Markdown() {}
2023+
""";
2024+
this.workingCopies = new ICompilationUnit[1];
2025+
this.workingCopies[0] = getWorkingCopy("/Converter_25/src/markdown/gh3761/Markdown.java", source, null);
2026+
if (this.docCommentSupport.equals(JavaCore.ENABLED)) {
2027+
CompilationUnit compilUnit = (CompilationUnit) runConversion(this.workingCopies[0], true);
2028+
TypeDeclaration typedeclaration = (TypeDeclaration) compilUnit.types().get(0);
2029+
Javadoc javadoc = typedeclaration.getJavadoc();
2030+
TagElement tags = (TagElement) javadoc.tags().get(0);
2031+
assertEquals("fragments count does not match", 3, tags.fragments().size());
2032+
assertTrue(tags.fragments().get(0) instanceof TextElement);
2033+
assertTrue(tags.fragments().get(1) instanceof TextElement);
2034+
assertTrue(tags.fragments().get(2) instanceof TextElement);
2035+
}
2036+
}
2037+
2038+
// (](] - invalid condition
2039+
public void testMarkdownURLs4531_07() throws JavaModelException {
2040+
String source = """
2041+
/// @see (Ex Si](http://ex.com]
2042+
public class Markdown() {}
2043+
""";
2044+
this.workingCopies = new ICompilationUnit[1];
2045+
this.workingCopies[0] = getWorkingCopy("/Converter_25/src/markdown/gh3761/Markdown.java", source, null);
2046+
if (this.docCommentSupport.equals(JavaCore.ENABLED)) {
2047+
CompilationUnit compilUnit = (CompilationUnit) runConversion(this.workingCopies[0], true);
2048+
TypeDeclaration typedeclaration = (TypeDeclaration) compilUnit.types().get(0);
2049+
Javadoc javadoc = typedeclaration.getJavadoc();
2050+
TagElement tags = (TagElement) javadoc.tags().get(0);
2051+
assertEquals("fragments count does not match", 1, tags.fragments().size());
2052+
assertTrue(tags.fragments().get(0) instanceof TextElement);
2053+
}
2054+
}
2055+
2056+
// ()[] - invalid condition
2057+
public void testMarkdownURLs4531_08() throws JavaModelException {
2058+
String source = """
2059+
/// @see (Ex Si)[http://ex.com]
2060+
public class Markdown() {}
2061+
""";
2062+
this.workingCopies = new ICompilationUnit[1];
2063+
this.workingCopies[0] = getWorkingCopy("/Converter_25/src/markdown/gh3761/Markdown.java", source, null);
2064+
if (this.docCommentSupport.equals(JavaCore.ENABLED)) {
2065+
CompilationUnit compilUnit = (CompilationUnit) runConversion(this.workingCopies[0], true);
2066+
TypeDeclaration typedeclaration = (TypeDeclaration) compilUnit.types().get(0);
2067+
Javadoc javadoc = typedeclaration.getJavadoc();
2068+
TagElement tags = (TagElement) javadoc.tags().get(0);
2069+
assertEquals("fragments count does not match", 2, tags.fragments().size());
2070+
assertTrue(tags.fragments().get(0) instanceof TextElement);
2071+
assertTrue(tags.fragments().get(1) instanceof TextElement);
2072+
}
2073+
}
19172074
}

org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/DocCommentParser.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -733,13 +733,14 @@ protected boolean parseMarkdownLinks(int previousPosition) throws InvalidInputEx
733733
readChar();
734734
}
735735
break;
736+
case ')':
736737
case ']':
737-
if (peekChar() == '[') {
738+
if ((peekChar() == '[' ) || peekChar() == '(') {
738739
tStart = start;
739740
tEnd = this.index - 1;
740741
currentChar = readChar();
741742
start = this.index;
742-
} else {
743+
} else if (peekChar() != ']') {
743744
int eofBkup = this.scanner.eofPosition;
744745
this.scanner.eofPosition = this.index - 1;
745746
this.scanner.resetTo(start, this.javadocEnd);

0 commit comments

Comments
 (0)