001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2025 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018/////////////////////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.checks.javadoc.utils; 021 022import java.util.ArrayList; 023import java.util.List; 024import java.util.regex.Matcher; 025import java.util.regex.Pattern; 026 027import com.puppycrawl.tools.checkstyle.api.LineColumn; 028 029/** 030 * Tools for extracting inline tags from Javadoc comments. 031 * 032 */ 033public final class InlineTagUtil { 034 035 /** 036 * Inline tag pattern. 037 */ 038 private static final Pattern INLINE_TAG_PATTERN = Pattern.compile( 039 "\\{@(\\p{Alpha}+)\\b(.*?)}", Pattern.DOTALL); 040 041 /** Pattern to recognize leading "*" characters in Javadoc. */ 042 private static final Pattern JAVADOC_PREFIX_PATTERN = Pattern.compile( 043 "^\\s*\\*", Pattern.MULTILINE); 044 045 /** Pattern matching whitespace, used by {@link InlineTagUtil#collapseWhitespace(String)}. */ 046 private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+"); 047 048 /** Pattern matching a newline. */ 049 private static final Pattern NEWLINE_PATTERN = Pattern.compile("\\n"); 050 051 /** Line feed character. */ 052 private static final char LINE_FEED = '\n'; 053 054 /** Carriage return character. */ 055 private static final char CARRIAGE_RETURN = '\r'; 056 057 /** Prevent instantiation. */ 058 private InlineTagUtil() { 059 } 060 061 /** 062 * Extract inline Javadoc tags from the given comment. 063 * 064 * @param lines The Javadoc comment (as lines). 065 * @return The extracted inline Javadoc tags. 066 * @throws IllegalArgumentException when comment lines contain newlines 067 */ 068 public static List<TagInfo> extractInlineTags(String... lines) { 069 for (String line : lines) { 070 if (line.indexOf(LINE_FEED) != -1 || line.indexOf(CARRIAGE_RETURN) != -1) { 071 throw new IllegalArgumentException("comment lines cannot contain newlines"); 072 } 073 } 074 075 final String commentText = convertLinesToString(lines); 076 final Matcher inlineTagMatcher = INLINE_TAG_PATTERN.matcher(commentText); 077 078 final List<TagInfo> tags = new ArrayList<>(); 079 080 while (inlineTagMatcher.find()) { 081 final String tagName = inlineTagMatcher.group(1); 082 083 // Remove the leading asterisks (in case the tag spans a line) and collapse 084 // the whitespace. 085 String matchedTagValue = inlineTagMatcher.group(2); 086 matchedTagValue = removeLeadingJavaDoc(matchedTagValue); 087 matchedTagValue = collapseWhitespace(matchedTagValue); 088 089 final String tagValue = matchedTagValue; 090 091 final int startIndex = inlineTagMatcher.start(1); 092 final LineColumn position = getLineColumnOfIndex(commentText, 093 // correct start index offset 094 startIndex - 1); 095 096 tags.add(new TagInfo(tagName, tagValue, position)); 097 } 098 099 return tags; 100 } 101 102 /** 103 * Convert array of string to single String. 104 * 105 * @param lines A number of lines, in order. 106 * @return The lines, joined together with newlines, as a single string. 107 */ 108 private static String convertLinesToString(String... lines) { 109 final StringBuilder builder = new StringBuilder(1024); 110 for (String line : lines) { 111 builder.append(line); 112 builder.append(LINE_FEED); 113 } 114 return builder.toString(); 115 } 116 117 /** 118 * Get LineColumn from string till index. 119 * 120 * @param source Source string. 121 * @param index An index into the string. 122 * @return A position in the source representing what line and column that index appears on. 123 */ 124 private static LineColumn getLineColumnOfIndex(String source, int index) { 125 final String precedingText = source.subSequence(0, index).toString(); 126 final String[] precedingLines = NEWLINE_PATTERN.split(precedingText); 127 final String lastLine = precedingLines[precedingLines.length - 1]; 128 return new LineColumn(precedingLines.length, lastLine.length()); 129 } 130 131 /** 132 * Collapse whitespaces. 133 * 134 * @param str Source string. 135 * @return The given string with all whitespace collapsed. 136 */ 137 private static String collapseWhitespace(String str) { 138 final Matcher matcher = WHITESPACE_PATTERN.matcher(str); 139 return matcher.replaceAll(" ").trim(); 140 } 141 142 /** 143 * Remove leading JavaDoc. 144 * 145 * @param source A string to remove leading Javadoc from. 146 * @return The given string with leading Javadoc "*" characters from each line removed. 147 */ 148 private static String removeLeadingJavaDoc(String source) { 149 final Matcher matcher = JAVADOC_PREFIX_PATTERN.matcher(source); 150 return matcher.replaceAll(""); 151 } 152 153}