2020package com .puppycrawl .tools .checkstyle .checks .javadoc ;
2121
2222import java .util .ArrayList ;
23+ import java .util .Arrays ;
2324import java .util .Collection ;
2425import java .util .List ;
2526import java .util .Set ;
@@ -110,6 +111,9 @@ public class JavadocTypeCheck
110111 /** Space literal. */
111112 private static final String SPACE = " " ;
112113
114+ /** Javadoc tag token literal. */
115+ private static final String JAVADOC_TAG_TOKEN = "@" ;
116+
113117 /** Pattern to match type name within angle brackets in javadoc param tag. */
114118 private static final Pattern TYPE_NAME_IN_JAVADOC_TAG =
115119 Pattern .compile ("^<([^>]+)" );
@@ -296,13 +300,69 @@ private List<JavadocTag> getJavadocTags(TextBlock textBlock) {
296300 final JavadocTags tags = JavadocUtil .getJavadocTags (textBlock ,
297301 JavadocUtil .JavadocTagType .BLOCK );
298302 if (!allowUnknownTags ) {
299- tags .getInvalidTags ().forEach (tag -> {
300- log (tag .getLine (), tag .getCol (), MSG_UNKNOWN_TAG , tag .getName ());
301- });
303+ final String [] lines = textBlock .getText ();
304+ tags .getInvalidTags ().stream ()
305+ .filter (tag -> !isTagInsideCodeOrLiteralBlock (lines , textBlock , tag ))
306+ .forEach (tag -> {
307+ log (tag .getLine (), tag .getCol (), MSG_UNKNOWN_TAG , tag .getName ());
308+ });
302309 }
303310 return tags .getValidTags ();
304311 }
305312
313+ /**
314+ * Checks if a tag is positioned inside a {@code @code} or {@code @literal} inline tag block.
315+ * Since block tags must appear at line-start position (per BlockTagUtil regex pattern),
316+ * we only need to check content from previous lines - there cannot be inline content
317+ * before a block tag on the same line.
318+ *
319+ * @param lines the Javadoc comment lines.
320+ * @param textBlock the text block containing the Javadoc.
321+ * @param tag the invalid tag to check.
322+ * @return true if the tag is inside a code or literal block.
323+ */
324+ private static boolean isTagInsideCodeOrLiteralBlock (String [] lines ,
325+ TextBlock textBlock ,
326+ InvalidJavadocTag tag ) {
327+ final int tagLineIndex = tag .getLine () - textBlock .getStartLineNo ();
328+
329+ final String textBefore = String .join ("\n " , Arrays .copyOfRange (lines , 0 , tagLineIndex ));
330+ return isInsideInlineTag (textBefore );
331+ }
332+
333+ /**
334+ * Determines if the position is inside an unclosed {@code @code}, {@code @literal},
335+ * or {@code @snippet} inline tag by counting opening and closing braces.
336+ * These tags display content verbatim and should not be parsed for Javadoc block tags.
337+ *
338+ * @param textBefore the text from the start of Javadoc up to the tag position.
339+ * @return true if inside an unclosed code, literal, or snippet inline tag.
340+ */
341+ private static boolean isInsideInlineTag (String textBefore ) {
342+ boolean insideVerbatimTag = false ;
343+ int braceDepth = 0 ;
344+
345+ for (int index = 0 ; index < textBefore .length (); index ++) {
346+ final char ch = textBefore .charAt (index );
347+ if (ch == '{' ) {
348+ if (textBefore .startsWith ("{@code" , index )
349+ || textBefore .startsWith ("{@literal" , index )
350+ || textBefore .startsWith ("{@snippet" , index )) {
351+ insideVerbatimTag = true ;
352+ }
353+ braceDepth ++;
354+ }
355+ else if (ch == '}' ) {
356+ braceDepth --;
357+ if (braceDepth == 0 ) {
358+ insideVerbatimTag = false ;
359+ }
360+ }
361+ }
362+
363+ return insideVerbatimTag ;
364+ }
365+
306366 /**
307367 * Verifies that a type definition has a required tag.
308368 *
@@ -315,18 +375,18 @@ private void checkTag(DetailAST ast, Iterable<JavadocTag> tags, String tagName,
315375 Pattern formatPattern ) {
316376 if (formatPattern != null ) {
317377 boolean hasTag = false ;
318- final String tagPrefix = "@" ;
319378
320- for (final JavadocTag tag :tags ) {
379+ for (final JavadocTag tag : tags ) {
321380 if (tag .getTagName ().equals (tagName )) {
322381 hasTag = true ;
323382 if (!formatPattern .matcher (tag .getFirstArg ()).find ()) {
324- log (ast , MSG_TAG_FORMAT , tagPrefix + tagName , formatPattern .pattern ());
383+ log (ast , MSG_TAG_FORMAT , JAVADOC_TAG_TOKEN + tagName ,
384+ formatPattern .pattern ());
325385 }
326386 }
327387 }
328388 if (!hasTag ) {
329- log (ast , MSG_MISSING_TAG , tagPrefix + tagName );
389+ log (ast , MSG_MISSING_TAG , JAVADOC_TAG_TOKEN + tagName );
330390 }
331391 }
332392 }
@@ -397,15 +457,15 @@ private void checkUnusedParamTags(
397457 || recordComponentNames .contains (paramName );
398458
399459 if (!found ) {
400- if (paramName .isEmpty ()) {
460+ final String displayName = TYPE_NAME_IN_JAVADOC_TAG_SPLITTER
461+ .split (tag .getFirstArg (), -1 )[0 ];
462+ if (displayName .isEmpty ()) {
401463 log (tag .getLineNo (), tag .getColumnNo (), MSG_UNUSED_TAG_GENERAL );
402464 }
403465 else {
404- final String actualParamName =
405- TYPE_NAME_IN_JAVADOC_TAG_SPLITTER .split (tag .getFirstArg (), -1 )[0 ];
406466 log (tag .getLineNo (), tag .getColumnNo (),
407467 MSG_UNUSED_TAG ,
408- JavadocTagInfo .PARAM .getText (), actualParamName );
468+ JavadocTagInfo .PARAM .getText (), displayName );
409469 }
410470 }
411471 }
@@ -420,16 +480,16 @@ private void checkUnusedParamTags(
420480 * @return extracts type parameter name from tag
421481 */
422482 private static String extractParamNameFromTag (JavadocTag tag ) {
423- final String typeParamName ;
424- final Matcher matchInAngleBrackets =
425- TYPE_NAME_IN_JAVADOC_TAG . matcher ( tag . getFirstArg ()) ;
483+ final String firstArg = tag . getFirstArg () ;
484+ final Matcher matchInAngleBrackets = TYPE_NAME_IN_JAVADOC_TAG . matcher ( firstArg );
485+ final String paramName ;
426486 if (matchInAngleBrackets .find ()) {
427- typeParamName = matchInAngleBrackets .group (1 ).trim ();
487+ paramName = matchInAngleBrackets .group (1 ).trim ();
428488 }
429489 else {
430- typeParamName = TYPE_NAME_IN_JAVADOC_TAG_SPLITTER .split (tag . getFirstArg () , -1 )[0 ];
490+ paramName = TYPE_NAME_IN_JAVADOC_TAG_SPLITTER .split (firstArg , -1 )[0 ];
431491 }
432- return typeParamName ;
492+ return paramName ;
433493 }
434494
435495 /**
0 commit comments