Skip to content

Commit d7733b1

Browse files
authored
render javadoc descriptions safely and ignore block tags (fixes #1039, via #1258)
1 parent c1957f0 commit d7733b1

10 files changed

Lines changed: 1044 additions & 26 deletions

File tree

allure-descriptions-javadoc/src/main/java/io/qameta/allure/description/JavaDocDescriptionRenderer.java

Lines changed: 562 additions & 0 deletions
Large diffs are not rendered by default.

allure-descriptions-javadoc/src/main/java/io/qameta/allure/description/JavaDocDescriptionsProcessor.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public class JavaDocDescriptionsProcessor extends AbstractProcessor {
5454
private Filer filer;
5555
private Elements elementUtils;
5656
private Messager messager;
57+
private JavaDocDescriptionRenderer renderer;
5758

5859
@Override
5960
@SuppressWarnings("PMD.AvoidSynchronizedAtMethodLevel")
@@ -62,6 +63,7 @@ public synchronized void init(final ProcessingEnvironment env) {
6263
filer = env.getFiler();
6364
elementUtils = env.getElementUtils();
6465
messager = env.getMessager();
66+
renderer = new JavaDocDescriptionRenderer();
6567
}
6668

6769
@Override
@@ -76,12 +78,11 @@ public boolean process(final Set<? extends TypeElement> annotations, final Round
7678
final Set<ExecutableElement> methods = ElementFilter.methodsIn(elements);
7779
methods.forEach(method -> {
7880
final String rawDocs = elementUtils.getDocComment(method);
79-
8081
if (rawDocs == null) {
8182
return;
8283
}
8384

84-
final String docs = rawDocs.trim();
85+
final String docs = renderer.render(rawDocs);
8586
if (docs.isEmpty()) {
8687
return;
8788
}
Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
/*
2+
* Copyright 2016-2026 Qameta Software Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.qameta.allure.description;
17+
18+
import org.junit.jupiter.api.Test;
19+
20+
import java.util.List;
21+
22+
import static org.assertj.core.api.Assertions.assertThat;
23+
24+
class JavaDocDescriptionRendererTest {
25+
26+
private final JavaDocDescriptionRenderer renderer = new JavaDocDescriptionRenderer();
27+
28+
@Test
29+
void shouldRenderPlainTextAndTrimBlankLines() {
30+
final String rendered = renderer.render(
31+
"\r\n"
32+
+ " First line \r\n"
33+
+ "\r\n"
34+
+ " Second line\t\r\n"
35+
+ "\r\n"
36+
);
37+
38+
assertThat(rendered)
39+
.isEqualTo("First line\n\nSecond line");
40+
}
41+
42+
@Test
43+
void shouldReturnEmptyStringWhenBodyContainsOnlyBlockTags() {
44+
final String rendered = renderer.render(
45+
"@param value description\n"
46+
+ "@throws Exception description"
47+
);
48+
49+
assertThat(rendered)
50+
.isEmpty();
51+
}
52+
53+
@Test
54+
void shouldIgnoreBlockTagsAndEverythingAfterThem() {
55+
final String rendered = renderer.render(
56+
"Summary paragraph.\n"
57+
+ "\n"
58+
+ "@param value Description of the value.\n"
59+
+ "Continuation that should also be ignored."
60+
);
61+
62+
assertThat(rendered)
63+
.isEqualTo("Summary paragraph.");
64+
}
65+
66+
@Test
67+
void shouldIgnoreStandardBlockTagsAfterMainDescription() {
68+
final List<String> blockTags = List.of(
69+
"author",
70+
"deprecated",
71+
"exception",
72+
"hidden",
73+
"param",
74+
"provides",
75+
"return",
76+
"see",
77+
"serial",
78+
"serialData",
79+
"serialField",
80+
"since",
81+
"spec",
82+
"throws",
83+
"uses",
84+
"version"
85+
);
86+
87+
for (String blockTag : blockTags) {
88+
assertThat(renderer.render("Summary paragraph.\n@" + blockTag + " metadata"))
89+
.as(blockTag)
90+
.isEqualTo("Summary paragraph.");
91+
}
92+
}
93+
94+
@Test
95+
void shouldNotTreatAtSignsInsideTextAsBlockTags() {
96+
final String rendered = renderer.render(
97+
"Email support@example.com\n"
98+
+ "Use @smoke in prose."
99+
);
100+
101+
assertThat(rendered)
102+
.isEqualTo("Email support@example.com\nUse @smoke in prose.");
103+
}
104+
105+
@Test
106+
void shouldDecodeEscapedAtEntityBeforeBlockTags() {
107+
final String rendered = renderer.render(
108+
"&#064;version stays in prose.\n"
109+
+ "@version 2.4.0"
110+
);
111+
112+
assertThat(rendered)
113+
.isEqualTo("@version stays in prose.");
114+
}
115+
116+
@Test
117+
void shouldPreserveUnicodeCharactersInDescriptions() {
118+
final String rendered = renderer.render("Release notes: cafe, café, Привет, 東京, λ.");
119+
120+
assertThat(rendered)
121+
.isEqualTo("Release notes: cafe, café, Привет, 東京, λ.");
122+
}
123+
124+
@Test
125+
void shouldDecodeSupportedNamedAndNumericEntities() {
126+
final String rendered = renderer.render(
127+
"Use &lt;tag&gt;, &amp;, &lbrace;x&rbrace;, &#064;, &#955;, and &#x03BB;."
128+
);
129+
130+
assertThat(rendered)
131+
.isEqualTo("Use &lt;tag&gt;, &amp;, {x}, @, λ, and λ.");
132+
}
133+
134+
@Test
135+
void shouldRenderSupportedInlineTags() {
136+
final String rendered = renderer.render(
137+
"Use {@code a < b}, {@literal <safe>}, "
138+
+ "{@link java.lang.String}, "
139+
+ "{@linkplain java.lang.String#valueOf(Object)}, "
140+
+ "{@link java.util.List list docs}."
141+
);
142+
143+
assertThat(rendered)
144+
.isEqualTo("Use `a &lt; b`, &lt;safe&gt;, String, valueOf(Object), list docs.");
145+
}
146+
147+
@Test
148+
void shouldSupportBalancedBracesInsideInlineTags() {
149+
final String rendered = renderer.render(
150+
"Payload {@code {\"outer\": {\"inner\": true}}}."
151+
);
152+
153+
assertThat(rendered)
154+
.isEqualTo("Payload `{\"outer\": {\"inner\": true}}`.");
155+
}
156+
157+
@Test
158+
void shouldNotTreatAtLinesInsideBalancedInlineTagsAsBlockTags() {
159+
final String rendered = renderer.render(
160+
"Summary {@literal first line\n"
161+
+ "@notATag\n"
162+
+ "last line}\n"
163+
+ "@param ignored"
164+
);
165+
166+
assertThat(rendered)
167+
.isEqualTo("Summary first line\n@notATag\nlast line");
168+
}
169+
170+
@Test
171+
void shouldRenderNestedInlineTagsInsideLinkLabels() {
172+
final String rendered = renderer.render(
173+
"See {@linkplain java.util.List docs with {@code List}}."
174+
);
175+
176+
assertThat(rendered)
177+
.isEqualTo("See docs with `List`.");
178+
}
179+
180+
@Test
181+
void shouldSafelyDegradeUnsupportedStandardInlineTags() {
182+
final String rendered = renderer.render(
183+
"Fallbacks: {@docRoot}, {@inheritDoc}, {@index release}, "
184+
+ "{@summary quick summary}, {@systemProperty user.home}, "
185+
+ "{@value java.lang.Integer#MAX_VALUE}."
186+
);
187+
188+
assertThat(rendered)
189+
.isEqualTo(
190+
"Fallbacks: docRoot, inheritDoc, index release, summary quick summary, "
191+
+ "systemProperty user.home, value java.lang.Integer#MAX_VALUE."
192+
);
193+
}
194+
195+
@Test
196+
void shouldSafelyDegradeSnippetTags() {
197+
final String rendered = renderer.render(
198+
"Snippet {@snippet :\n"
199+
+ "int answer = 42;\n"
200+
+ "@highlight substring=\"answer\"\n"
201+
+ "}."
202+
);
203+
204+
assertThat(rendered)
205+
.isEqualTo("Snippet snippet :\nint answer = 42;\n@highlight substring=\"answer\".");
206+
}
207+
208+
@Test
209+
void shouldEscapeUnknownInlineTags() {
210+
final String rendered = renderer.render("Unsupported {@unknown <tag>} clause.");
211+
212+
assertThat(rendered)
213+
.isEqualTo("Unsupported unknown &lt;tag&gt; clause.");
214+
}
215+
216+
@Test
217+
void shouldPreserveMalformedInlineTagsAsText() {
218+
final String rendered = renderer.render("Broken {@code tag");
219+
220+
assertThat(rendered)
221+
.isEqualTo("Broken {@code tag");
222+
}
223+
224+
@Test
225+
void shouldRenderSupportedHtmlStructure() {
226+
final String rendered = renderer.render(
227+
"First<p>Second<br>Third<ul><li>one</li><li>two</li></ul><ol><li>three</li></ol>"
228+
);
229+
230+
assertThat(rendered)
231+
.isEqualTo("First\n\nSecond\nThird\n\n- one\n- two\n\n- three");
232+
}
233+
234+
@Test
235+
void shouldIgnoreUnclosedHtmlTagsSafely() {
236+
final String rendered = renderer.render("Broken <b>bold <i>text");
237+
238+
assertThat(rendered)
239+
.isEqualTo("Broken bold text");
240+
}
241+
242+
@Test
243+
void shouldPreserveAngleBracketComparisonsAsText() {
244+
final String rendered = renderer.render("Math says a < b > c.");
245+
246+
assertThat(rendered)
247+
.isEqualTo("Math says a &lt; b &gt; c.");
248+
}
249+
250+
@Test
251+
void shouldIgnoreUnmatchedCodeHtmlTagsSafely() {
252+
final String rendered = renderer.render("Broken <code>value < limit and stray </code>tag");
253+
254+
assertThat(rendered)
255+
.isEqualTo("Broken `value &lt; limit and stray `tag");
256+
}
257+
258+
@Test
259+
void shouldIgnoreOpeningCodeTagWithoutClosingTag() {
260+
final String rendered = renderer.render("Broken <code>value < limit");
261+
262+
assertThat(rendered)
263+
.isEqualTo("Broken value &lt; limit");
264+
}
265+
266+
@Test
267+
void shouldIgnoreClosingCodeTagWithoutOpeningTag() {
268+
final String rendered = renderer.render("Broken </code>tag");
269+
270+
assertThat(rendered)
271+
.isEqualTo("Broken tag");
272+
}
273+
274+
@Test
275+
void shouldRenderHtmlCodeTagAsCodeSpan() {
276+
final String rendered = renderer.render("<code>name < value & more</code>");
277+
278+
assertThat(rendered)
279+
.isEqualTo("`name &lt; value &amp; more`");
280+
}
281+
282+
@Test
283+
void shouldLeaveUnknownEntitiesEscaped() {
284+
final String rendered = renderer.render("Keep &notAnEntity; literal.");
285+
286+
assertThat(rendered)
287+
.isEqualTo("Keep &amp;notAnEntity; literal.");
288+
}
289+
290+
@Test
291+
void shouldDropUnknownHtmlTagsButKeepTheirTextContentEscaped() {
292+
final String rendered = renderer.render(
293+
"prefix <script>alert(\"x\")</script> <div>safe & sound</div>"
294+
);
295+
296+
assertThat(rendered)
297+
.isEqualTo("prefix alert(\"x\") safe &amp; sound");
298+
}
299+
300+
@Test
301+
void shouldRenderComplexModernJavadocExampleSafely() {
302+
final String rendered = renderer.render(
303+
"Fetches release metadata for the current build.\n"
304+
+ "\n"
305+
+ "<p>Use {@link java.net.URI URIs} for endpoint configuration.</p>\n"
306+
+ "<ul>\n"
307+
+ "<li>Supports café, Привет, 東京, and λ.</li>\n"
308+
+ "<li>See the <a href=\"https://docs.oracle.com/\">Javadoc specification</a> "
309+
+ "and {@linkplain java.lang.String#formatted(Object...) formatted examples}.</li>\n"
310+
+ "</ul>\n"
311+
+ "Example: <code>client.fetch(\"v2\")</code>\n"
312+
+ "&#064;beta remains prose.\n"
313+
+ "@author Jane Doe\n"
314+
+ "@version 2.3.0\n"
315+
+ "@since 2.0"
316+
);
317+
318+
assertThat(rendered)
319+
.isEqualTo(
320+
"Fetches release metadata for the current build.\n\n"
321+
+ "Use URIs for endpoint configuration.\n\n"
322+
+ "- Supports café, Привет, 東京, and λ.\n"
323+
+ "- See the Javadoc specification and formatted examples.\n\n"
324+
+ "Example: `client.fetch(\"v2\")`\n"
325+
+ "@beta remains prose."
326+
);
327+
}
328+
}

0 commit comments

Comments
 (0)