1 /*
2 * SPDX-FileCopyrightText: Copyright (c) 2011-2026 Yegor Bugayenko
3 * SPDX-License-Identifier: MIT
4 */
5 package com.qulice.checkstyle;
6
7 import java.util.HashMap;
8 import java.util.Map;
9 import java.util.regex.Matcher;
10 import java.util.regex.Pattern;
11
12 /**
13 * Check the required JavaDoc tag in the lines.
14 * <p>Correct format is the following (of a class javadoc):
15 *
16 * <pre>
17 * /**
18 * * This is my new class.
19 * *
20 * * @since 0.3
21 * */
22 * public final class Foo {
23 * /**
24 * * This is my other class.
25 * *
26 * * @since 0.3
27 * */
28 * public final class Boo {
29 * // ...
30 * </pre>
31 *
32 * @since 0.23.1
33 */
34 final class RequiredJavaDocTag {
35
36 /**
37 * Tag name.
38 */
39 private final String name;
40
41 /**
42 * Pattern for searching a tag in a string.
43 */
44 private final Pattern tag;
45
46 /**
47 * Pattern for checking the contents of a tag in a string.
48 */
49 private final Pattern content;
50
51 /**
52 * Reference to a method for writing a message to the log.
53 */
54 private final Reporter reporter;
55
56 /**
57 * Ctor.
58 * @param cname Tag name
59 * @param ptag Pattern for searching a tag in a string
60 * @param patt Pattern for checking the contents of a tag in a string
61 * @param rep Reference to a method for writing a message to the log
62 * @checkstyle ParameterNumberCheck (3 lines)
63 */
64 RequiredJavaDocTag(
65 final String cname,
66 final Pattern ptag,
67 final Pattern patt,
68 final Reporter rep
69 ) {
70 this.name = cname;
71 this.tag = ptag;
72 this.content = patt;
73 this.reporter = rep;
74 }
75
76 /**
77 * Check if the tag text matches the format from pattern.
78 * @param lines List of all lines
79 * @param start Line number where comment starts
80 * @param end Line number where comment ends
81 */
82 void matchTagFormat(
83 final String[] lines,
84 final int start,
85 final int end
86 ) {
87 final Pattern loose = Pattern.compile(
88 String.format("@%s\\b", this.name)
89 );
90 Integer looseline = null;
91 final Map<Integer, String> found = new HashMap<>(1);
92 for (int pos = start; pos <= end; pos += 1) {
93 final String line = lines[pos];
94 final Matcher matcher = this.tag.matcher(line);
95 if (RequiredJavaDocTag.tagFound(matcher)) {
96 found.put(pos, matcher.group("cont"));
97 break;
98 }
99 if (looseline == null && loose.matcher(line).find()) {
100 looseline = pos;
101 }
102 }
103 if (found.isEmpty()) {
104 this.logMissing(start, looseline);
105 } else {
106 this.logMismatches(found);
107 }
108 }
109
110 /**
111 * Log either "missing" or "malformed" depending on whether a loose
112 * match was found in the comment.
113 * @param start Line number where the comment starts
114 * @param looseline Line number where a loose match was found, or
115 * {@code null} if none was found
116 */
117 private void logMissing(final int start, final Integer looseline) {
118 if (looseline == null) {
119 this.reporter.log(
120 start + 1,
121 "Missing ''@{0}'' tag in class/interface comment",
122 this.name
123 );
124 } else {
125 this.reporter.log(
126 looseline + 1,
127 "Malformed ''@{0}'' tag, expected '' * @{0} <value>'' format",
128 this.name
129 );
130 }
131 }
132
133 /**
134 * Log a violation for every tag whose text does not match the
135 * configured pattern.
136 * @param found Map of line number to tag text
137 */
138 private void logMismatches(final Map<Integer, String> found) {
139 for (final Map.Entry<Integer, String> item : found.entrySet()) {
140 if (!this.content.matcher(item.getValue()).matches()) {
141 this.reporter.log(
142 item.getKey() + 1,
143 "Tag text ''{0}'' does not match the pattern ''{1}''",
144 item.getValue(),
145 this.content.toString()
146 );
147 }
148 }
149 }
150
151 /**
152 * Finds the tag name and the following sentences.
153 * @param matcher Tag name matcher
154 * @return True if the tag and its clauses are found
155 */
156 private static boolean tagFound(final Matcher matcher) {
157 return matcher.matches()
158 && !RequiredJavaDocTag.empty(matcher.group("name"))
159 && !RequiredJavaDocTag.empty(matcher.group("cont"));
160 }
161
162 /**
163 * Checks for an empty string.
164 * @param str Line to check
165 * @return True if str is empty
166 */
167 private static boolean empty(final String str) {
168 return str == null || str.chars().allMatch(Character::isWhitespace);
169 }
170
171 /**
172 * Logger.
173 * @see com.puppycrawl.tools.checkstyle.api.AbstractCheck#log(int, String, Object...)
174 * @since 0.23.1
175 */
176 @FunctionalInterface
177 interface Reporter {
178
179 /**
180 * Log a message that has no column information.
181 * @param line The line number where the audit event was found
182 * @param msg The message that describes the audit event
183 * @param args The details of the message
184 * @see java.text.MessageFormat
185 */
186 void log(int line, String msg, Object... args);
187 }
188 }